Add E2E tests for internationalization across multiple pages

- Implemented navigation.spec.ts to test language switching and translation of navigation elements.
- Created sessions-page.spec.ts to verify translations on the sessions page, including headers, status badges, and date formatting.
- Developed settings-page.spec.ts to ensure settings page content is translated and persists across sessions.
- Added skills-page.spec.ts to validate translations for skill categories, action buttons, and empty states.
This commit is contained in:
catlog22
2026-01-30 22:54:21 +08:00
parent e78e95049b
commit 81725c94b1
150 changed files with 25341 additions and 1448 deletions

View File

@@ -0,0 +1,221 @@
// ========================================
// i18n Helper Utilities for E2E Tests
// ========================================
// Reusable utilities for internationalization validation
import { Page, Locator } from '@playwright/test';
/**
* Switch language and verify complete state update
* @param page - Playwright Page object
* @param locale - Target locale ('en' or 'zh')
* @param languageSwitcher - Optional pre-located language switcher locator
*/
export async function switchLanguageAndVerify(
page: Page,
locale: 'en' | 'zh',
languageSwitcher?: Locator
): Promise<void> {
const switcher = languageSwitcher || page.getByRole('combobox', { name: /select language/i }).first();
// Click to open dropdown
await switcher.click();
// Select the target language option
const targetOption = locale === 'zh'
? page.getByText('中文')
: page.getByText('English');
await expectToBeVisible(targetOption);
await targetOption.click();
// Wait for language change to take effect
// Note: Using hardcoded wait as per existing pattern - should be improved in future
await page.waitForTimeout(500);
// Verify the switcher text content is updated
const expectedText = locale === 'zh' ? '中文' : 'English';
const switcherText = await switcher.textContent();
if (!switcherText?.includes(expectedText)) {
throw new Error(`Expected language switcher to show "${expectedText}" but got "${switcherText}"`);
}
}
/**
* Verify complete i18n state after language switch
* @param page - Playwright Page object
* @param locale - Expected locale ('en' or 'zh')
* @returns Object containing verification results
*/
export async function verifyI18nState(
page: Page,
locale: 'en' | 'zh'
): Promise<{
langAttribute: string;
localStorage: string | null;
storageVerified: boolean;
}> {
// Verify lang attribute
const langAttribute = await page.evaluate(() => document.documentElement.lang);
if (langAttribute !== locale) {
throw new Error(`Expected lang="${locale}" but got lang="${langAttribute}"`);
}
// Verify localStorage
const storage = await page.evaluate(() => localStorage.getItem('ccw-app-store'));
const storageVerified = storage ? storage.includes(`"locale":"${locale}"`) : false;
if (!storageVerified) {
throw new Error(`localStorage does not contain locale="${locale}"`);
}
return { langAttribute, localStorage: storage, storageVerified };
}
/**
* Verify language persists after page reload
* @param page - Playwright Page object
* @param locale - Expected locale ('en' or 'zh')
*/
export async function verifyPersistenceAfterReload(
page: Page,
locale: 'en' | 'zh'
): Promise<void> {
// Reload the page
await page.reload();
await page.waitForLoadState('networkidle');
// Verify language is maintained
const lang = await page.evaluate(() => document.documentElement.lang);
if (lang !== locale) {
throw new Error(`Language not persisted after reload. Expected "${locale}" but got "${lang}"`);
}
// Verify language switcher still shows correct locale (check text content)
const switcher = page.getByRole('combobox', { name: /select language/i }).first();
const expectedText = locale === 'zh' ? '中文' : 'English';
const switcherText = await switcher.textContent();
if (!switcherText?.includes(expectedText)) {
throw new Error(`Language switcher does not show "${expectedText}" after reload. Got "${switcherText}"`);
}
}
/**
* Check if translated content is visible on the page
* @param page - Playwright Page object
* @param locale - Expected locale ('en' or 'zh')
* @param expectedText - Expected text content to verify
*/
export async function verifyTranslationCompleteness(
page: Page,
locale: 'en' | 'zh',
expectedText?: string
): Promise<boolean> {
if (expectedText) {
const element = page.getByText(expectedText);
const isVisible = await element.isVisible().catch(() => false);
if (!isVisible) {
throw new Error(`Expected translated text "${expectedText}" not found for locale "${locale}"`);
}
return true;
}
// If no specific text provided, check for general locale content
const pageContent = await page.content();
if (locale === 'zh') {
// Check for Chinese characters
return /[\u4e00-\u9fa5]/.test(pageContent);
}
// For English, check that we have English content
return pageContent.length > 0;
}
/**
* Verify aria-label is updated on language change
* @param page - Playwright Page object
* @param locator - Element locator to check
* @param expectedLabel - Expected aria-label (partial match supported)
*/
export async function verifyAriaLabelUpdated(
page: Page,
locator: Locator,
expectedLabel: string
): Promise<void> {
const ariaLabel = await locator.getAttribute('aria-label');
if (!ariaLabel || !ariaLabel.includes(expectedLabel)) {
throw new Error(`Expected aria-label to include "${expectedLabel}" but got "${ariaLabel}"`);
}
}
/**
* Navigate to a route and verify language is maintained
* @param page - Playwright Page object
* @param route - Route path to navigate to
* @param locale - Expected locale ('en' or 'zh')
*/
export async function navigateAndVerifyLanguage(
page: Page,
route: string,
locale: 'en' | 'zh'
): Promise<void> {
await page.goto(route, { waitUntil: 'networkidle' as const });
// Verify language is maintained after navigation
const lang = await page.evaluate(() => document.documentElement.lang);
if (lang !== locale) {
throw new Error(`Language not maintained after navigation to ${route}. Expected "${locale}" but got "${lang}"`);
}
}
/**
* Get localStorage state as parsed object
* @param page - Playwright Page object
* @returns Parsed localStorage object or null
*/
export async function getLocalStorageState(page: Page): Promise<any> {
const storage = await page.evaluate(() => {
const item = localStorage.getItem('ccw-app-store');
return item ? JSON.parse(item) : null;
});
return storage;
}
/**
* Set localStorage state (useful for pre-test setup)
* @param page - Playwright Page object
* @param state - State object to set in localStorage
*/
export async function setLocalStorageState(page: Page, state: any): Promise<void> {
await page.evaluate((s) => {
localStorage.setItem('ccw-app-store', JSON.stringify(s));
}, state);
}
/**
* Expect wrapper to handle errors gracefully
*/
async function expectToBeVisible(locator: Locator): Promise<void> {
const isVisible = await locator.isVisible().catch(() => false);
if (!isVisible) {
throw new Error(`Expected element to be visible but it was not found`);
}
}
/**
* Verify language switcher shows the correct selected option
* Note: Custom Select component uses button, not input, so we check visible text
*/
async function expectToHaveValue(locator: Locator, value: string): Promise<void> {
// For the custom Select component, check if the target language text is visible
// The SelectValue displays the selected option text (English or 中文)
const expectedText = value === 'zh' ? '中文' : 'English';
// Check if the switcher contains the expected language text
const switcherText = await locator.textContent().catch(() => '');
// The switcher may show flag emoji + label or just the label
if (!switcherText.includes(expectedText)) {
throw new Error(`Expected language switcher to show "${expectedText}" but got "${switcherText}"`);
}
}

View File

@@ -0,0 +1,254 @@
// ========================================
// E2E Tests: Issues Page i18n
// ========================================
// Tests for issues page internationalization
import { test, expect } from '@playwright/test';
import {
switchLanguageAndVerify,
verifyI18nState,
} from './helpers/i18n-helpers';
test.describe('[Issues Page] - i18n E2E Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
// Navigate to issues page
const issuesLink = page.getByRole('link', { name: /issues/i });
const isVisible = await issuesLink.isVisible().catch(() => false);
if (isVisible) {
await issuesLink.click();
await page.waitForLoadState('networkidle');
}
});
/**
* ISS-01: Verify issue list headers are translated
* Priority: P1
*/
test('ISS-01: should translate issue list table headers', async ({ page }) => {
// Look for table headers in English
const tableHeaders = page.locator('table th, thead th');
const count = await tableHeaders.count();
if (count > 0) {
// Get initial header text
const firstHeaderText = await tableHeaders.first().textContent();
expect(firstHeaderText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify i18n state
await verifyI18nState(page, 'zh');
// Get updated header text
const updatedHeaderText = await tableHeaders.first().textContent();
expect(updatedHeaderText).toBeTruthy();
// Headers should contain Chinese characters or be different
const hasChineseOrDifferent = updatedHeaderText !== firstHeaderText ||
/[\u4e00-\u9fa5]/.test(updatedHeaderText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* ISS-02: Verify status badges are translated
* Priority: P2
*/
test('ISS-02: should translate status badges', async ({ page }) => {
// Look for status badges
const statusBadges = page.locator('[class*="status"], [class*="badge"], span.badge').first();
const isVisible = await statusBadges.isVisible().catch(() => false);
if (isVisible) {
// Get initial status text
const initialStatus = await statusBadges.textContent();
expect(initialStatus).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated status text
const updatedStatus = await statusBadges.textContent();
expect(updatedStatus).toBeTruthy();
// Status should contain Chinese characters or be different
const hasChineseOrDifferent = updatedStatus !== initialStatus ||
/[\u4e00-\u9fa5]/.test(updatedStatus || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* ISS-03: Verify action buttons are translated
* Priority: P1
*/
test('ISS-03: should translate action buttons', async ({ page }) => {
// Look for action buttons (view, edit, delete, etc.)
const actionButtons = page.locator('button').filter({ hasText: /^(view|edit|delete|create)/i });
const count = await actionButtons.count();
if (count > 0) {
// Get first action button text
const initialText = await actionButtons.first().textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated button text
const updatedText = await actionButtons.first().textContent();
expect(updatedText).toBeTruthy();
// Button text should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* ISS-04: Verify date formatting per locale
* Priority: P2
*/
test('ISS-04: should format dates according to locale', async ({ page }) => {
// Look for date elements
const dateElements = page.locator('time, [datetime], [class*="date"], [class*="time"]');
const count = await dateElements.count();
if (count > 0) {
// Get initial date format
const initialDate = await dateElements.first().textContent();
expect(initialDate).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated date
const updatedDate = await dateElements.first().textContent();
expect(updatedDate).toBeTruthy();
// Date might be formatted differently or contain Chinese date characters
// This is a basic check that dates exist in both locales
const hasDateContent = updatedDate && updatedDate.length > 0;
expect(hasDateContent).toBeTruthy();
}
});
/**
* Additional: Verify empty state message is translated
*/
test('should translate empty state message', async ({ page }) => {
// Look for empty state indicators
const emptyState = page.getByText(/no issues|empty|no data/i);
const isVisible = await emptyState.isVisible().catch(() => false);
if (isVisible) {
// Get initial empty state text
const initialText = await emptyState.textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated empty state text
const updatedText = await emptyState.textContent();
expect(updatedText).toBeTruthy();
// Empty state should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify pagination controls are translated
*/
test('should translate pagination controls', async ({ page }) => {
// Look for pagination controls
const pagination = page.locator('[class*="pagination"], nav[aria-label*="pagination"]');
const isVisible = await pagination.isVisible().catch(() => false);
if (isVisible) {
// Get initial pagination text
const initialText = await pagination.textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated pagination text
const updatedText = await pagination.textContent();
expect(updatedText).toBeTruthy();
// Pagination should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify filter controls are translated
*/
test('should translate filter controls', async ({ page }) => {
// Look for filter controls
const filterSelect = page.locator('select').filter({ hasText: /filter|status|all/i }).first();
const isVisible = await filterSelect.isVisible().catch(() => false);
if (isVisible) {
// Get initial filter options
const initialOptions = await filterSelect.locator('option').allTextContents();
expect(initialOptions.length).toBeGreaterThan(0);
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated filter options
const updatedOptions = await filterSelect.locator('option').allTextContents();
expect(updatedOptions.length).toBeGreaterThan(0);
// At least some options should be different or contain Chinese
const hasTranslation = updatedOptions.some((opt, i) =>
opt !== initialOptions[i] || /[\u4e00-\u9fa5]/.test(opt)
);
expect(hasTranslation).toBeTruthy();
}
});
/**
* Additional: Verify language persists when viewing issue details
*/
test('should maintain language when viewing issue details', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Look for issue detail links
const detailLink = page.locator('a').filter({ hasText: /#/ }).first();
const isVisible = await detailLink.isVisible().catch(() => false);
if (isVisible) {
await detailLink.click();
await page.waitForLoadState('networkidle');
// Verify language is maintained
await verifyI18nState(page, 'zh');
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('zh');
}
});
});

View File

@@ -0,0 +1,213 @@
// ========================================
// E2E Tests: Language Switching
// ========================================
// End-to-end tests for internationalization
import { test, expect } from '@playwright/test';
test.describe('Language Switching E2E', () => {
test.beforeEach(async ({ page }) => {
// Navigate to the app
await page.goto('/');
// Wait for the page to be fully loaded
await page.waitForLoadState('networkidle');
});
test('should display language switcher in header', async ({ page }) => {
// Find the language switcher select element
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
// Verify language switcher is visible
await expect(languageSwitcher).toBeVisible();
});
test('should switch to Chinese and verify text updates', async ({ page }) => {
// Find the language switcher
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
// Verify initial locale is English (check text content)
await expect(languageSwitcher).toContainText('English');
// Click the language switcher
await languageSwitcher.click();
// Wait for dropdown to appear and click Chinese option
const chineseOption = page.getByText('中文');
await expect(chineseOption).toBeVisible();
await chineseOption.click();
// Wait for language change to take effect
await page.waitForTimeout(500);
// Verify locale is now Chinese (check text content)
await expect(languageSwitcher).toContainText('中文');
// Verify document lang attribute is updated
await expect(page.locator('html')).toHaveAttribute('lang', 'zh');
// Verify some translated text is displayed
// This will check for Chinese characters on the page
const pageContent = await page.content();
expect(pageContent).toContain('中文');
});
test('should persist language selection after page reload', async ({ page }) => {
// Find the language switcher and switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await languageSwitcher.click();
const chineseOption = page.getByText('中文');
await chineseOption.click();
// Wait for the change to take effect
await page.waitForTimeout(500);
// Reload the page
await page.reload();
// Wait for page to fully load
await page.waitForLoadState('networkidle');
// Verify locale is still Chinese after reload (check text content)
await expect(languageSwitcher).toContainText('中文');
await expect(page.locator('html')).toHaveAttribute('lang', 'zh');
});
test('should switch back to English from Chinese', async ({ page }) => {
// First switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await languageSwitcher.click();
const chineseOption = page.getByText('中文');
await chineseOption.click();
await page.waitForTimeout(500);
// Verify we're in Chinese (check text content)
await expect(languageSwitcher).toContainText('中文');
// Switch back to English
await languageSwitcher.click();
const englishOption = page.getByText('English');
await expect(englishOption).toBeVisible();
await englishOption.click();
// Wait for the change to take effect
await page.waitForTimeout(500);
// Verify we're back in English (check text content)
await expect(languageSwitcher).toContainText('English');
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
});
test('should display correct flag icons in language options', async ({ page }) => {
// Click the language switcher to open dropdown
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await languageSwitcher.click();
// Verify both flags are visible (use first() to avoid strict mode violation)
await expect(page.getByText('🇺🇸').first()).toBeVisible();
await expect(page.getByText('🇨🇳').first()).toBeVisible();
// Verify the labels are correct (use first() to avoid strict mode violation)
await expect(page.getByText('English').first()).toBeVisible();
await expect(page.getByText('中文').first()).toBeVisible();
});
test('should maintain language selection across navigation', async ({ page }) => {
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await languageSwitcher.click();
const chineseOption = page.getByText('中文');
await chineseOption.click();
await page.waitForTimeout(500);
// Navigate to a different page (if available)
const settingsLink = page.getByRole('link', { name: /settings/i });
if (await settingsLink.isVisible()) {
await settingsLink.click();
await page.waitForLoadState('networkidle');
// Verify language is still Chinese (check text content)
await expect(languageSwitcher).toContainText('中文');
}
});
test('should update aria-labels when language changes', async ({ page }) => {
// Get the theme toggle button - try to find it by icon or role
const themeButton = page.locator('button[aria-label*="switch"], button[aria-label*="mode"]').first();
// Verify theme button is visible
await expect(themeButton).toBeVisible();
// Get initial aria-label
const initialAriaLabel = await themeButton.getAttribute('aria-label');
expect(initialAriaLabel).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await languageSwitcher.click();
const chineseOption = page.getByText('中文');
await chineseOption.click();
await page.waitForTimeout(500);
// Verify aria-label is different (should be translated or changed)
// The exact content depends on the translation files
await expect(themeButton).toBeVisible();
});
test('should store language preference in localStorage', async ({ page }) => {
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await languageSwitcher.click();
const chineseOption = page.getByText('中文');
await chineseOption.click();
await page.waitForTimeout(500);
// Check localStorage
const storage = await page.evaluate(() => {
const item = localStorage.getItem('ccw-app-store');
return item ? JSON.parse(item) : null;
});
// Verify locale is stored
expect(storage).not.toBeNull();
expect(storage?.state?.locale).toBe('zh');
});
test('should load language preference from localStorage on first visit', async ({ browser }) => {
// Create a new context and page
const context = await browser.newContext();
const page = await context.newPage();
// Set localStorage before navigating
await page.goto('/');
await page.evaluate(() => {
localStorage.setItem('ccw-app-store', JSON.stringify({
state: { locale: 'zh', theme: 'system', sidebarCollapsed: false },
version: 0,
}));
});
// Reload to apply the stored locale
await page.reload();
// Wait for page to fully load
await page.waitForLoadState('networkidle');
// Verify language is loaded from localStorage (check text content)
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await expect(languageSwitcher).toContainText('中文');
await context.close();
});
});

View File

@@ -0,0 +1,248 @@
// ========================================
// E2E Tests: Memory Page i18n
// ========================================
// Tests for memory page internationalization
import { test, expect } from '@playwright/test';
import {
switchLanguageAndVerify,
verifyI18nState,
} from './helpers/i18n-helpers';
test.describe('[Memory Page] - i18n E2E Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
// Navigate to memory page
const memoryLink = page.getByRole('link', { name: /memory/i });
const isVisible = await memoryLink.isVisible().catch(() => false);
if (isVisible) {
await memoryLink.click();
await page.waitForLoadState('networkidle');
}
});
/**
* MEM-01: Verify memory page renders in English
* Priority: P2
*/
test('MEM-01: should render memory page in English', async ({ page }) => {
// Verify initial locale is English
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('en');
// Verify memory page has content
const pageContent = await page.content();
expect(pageContent).toBeTruthy();
// Look for page heading
const heading = page.getByRole('heading', { level: 1 }).or(
page.getByRole('heading', { level: 2 })
).first();
const isVisible = await heading.isVisible().catch(() => false);
if (isVisible) {
const headingText = await heading.textContent();
expect(headingText).toBeTruthy();
}
});
/**
* MEM-02: Verify language switch works on memory page
* Priority: P2
*/
test('MEM-02: should switch language on memory page', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify i18n state
await verifyI18nState(page, 'zh');
// Verify memory page has Chinese content
const pageContent = await page.content();
expect(pageContent).toMatch(/[\u4e00-\u9fa5]/);
// Switch back to English
await switchLanguageAndVerify(page, 'en', languageSwitcher);
// Verify back in English
const englishContent = await page.content();
expect(englishContent).toBeTruthy();
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('en');
});
/**
* Additional: Verify memory entries are displayed in selected language
*/
test('should display memory entries in selected language', async ({ page }) => {
// Look for memory entries or content sections
const entries = page.locator('[class*="entry"], [class*="item"], [class*="memory"], article');
const count = await entries.count();
if (count > 0) {
// Get initial entry content
const initialContent = await entries.first().textContent();
expect(initialContent).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated entry content
const updatedContent = await entries.first().textContent();
expect(updatedContent).toBeTruthy();
// Content should be different or contain Chinese characters
const hasChineseOrDifferent = updatedContent !== initialContent ||
/[\u4e00-\u9fa5]/.test(updatedContent || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify empty state is translated
*/
test('should translate empty state message', async ({ page }) => {
// Look for empty state
const emptyState = page.getByText(/no memory|empty|no data/i);
const isVisible = await emptyState.isVisible().catch(() => false);
if (isVisible) {
// Get initial text
const initialText = await emptyState.textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated text
const updatedText = await emptyState.textContent();
expect(updatedText).toBeTruthy();
// Should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify search/filter controls are translated
*/
test('should translate search and filter controls', async ({ page }) => {
// Look for search inputs
const searchInput = page.getByPlaceholder(/search|filter|find/i).or(
page.getByRole('searchbox')
).first();
const isVisible = await searchInput.isVisible().catch(() => false);
if (isVisible) {
// Get initial placeholder
const initialPlaceholder = await searchInput.getAttribute('placeholder');
expect(initialPlaceholder).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated placeholder
const updatedPlaceholder = await searchInput.getAttribute('placeholder');
// Placeholder should be different or contain Chinese characters
if (updatedPlaceholder) {
const hasChineseOrDifferent = updatedPlaceholder !== initialPlaceholder ||
/[\u4e00-\u9fa5]/.test(updatedPlaceholder);
expect(hasChineseOrDifferent).toBeTruthy();
}
}
});
/**
* Additional: Verify memory action buttons are translated
*/
test('should translate memory action buttons', async ({ page }) => {
// Look for action buttons
const actionButtons = page.locator('button').filter({
hasText: /^(add|create|delete|export|clear)/i
});
const count = await actionButtons.count();
if (count > 0) {
// Get initial button text
const initialText = await actionButtons.first().textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated button text
const updatedText = await actionButtons.first().textContent();
expect(updatedText).toBeTruthy();
// Button text should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify category tags are translated
*/
test('should translate memory category tags', async ({ page }) => {
// Look for category tags
const categoryTags = page.locator('[class*="tag"], [class*="category"], [class*="label"]');
const count = await categoryTags.count();
if (count > 0) {
// Get initial tag text
const initialTag = await categoryTags.first().textContent();
expect(initialTag).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated tag text
const updatedTag = await categoryTags.first().textContent();
expect(updatedTag).toBeTruthy();
// Tag should contain Chinese characters or be different
const hasChineseOrDifferent = updatedTag !== initialTag ||
/[\u4e00-\u9fa5]/.test(updatedTag || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Smoke test - Verify basic functionality
*/
test('smoke test: memory page loads and responds to language change', async ({ page }) => {
// Verify page loads
await expect(page).toHaveURL(/.*memory.*/);
// Verify language switcher is present
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await expect(languageSwitcher).toBeVisible();
// Verify initial state
const initialLang = await page.evaluate(() => document.documentElement.lang);
expect(initialLang).toBe('en');
// Switch language and verify
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify basic Chinese content exists
const hasChineseContent = await page.evaluate(() =>
/[\u4e00-\u9fa5]/.test(document.body.textContent || '')
);
expect(hasChineseContent).toBeTruthy();
});
});

View File

@@ -0,0 +1,179 @@
// ========================================
// E2E Tests: Navigation i18n
// ========================================
// Tests for navigation internationalization across the application
import { test, expect } from '@playwright/test';
import {
switchLanguageAndVerify,
verifyI18nState,
verifyPersistenceAfterReload,
navigateAndVerifyLanguage,
} from './helpers/i18n-helpers';
test.describe('[Navigation] - i18n E2E Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
});
/**
* NAV-01: Verify navigation links are translated
* Priority: P0
*/
test('NAV-01: should translate navigation links after language switch', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
// Verify initial English state (check text content)
await expect(languageSwitcher).toContainText('English');
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify i18n state is complete
await verifyI18nState(page, 'zh');
// Verify navigation elements have translated content
// Check for Chinese characters in navigation
const navContent = await page.locator('nav').textContent();
expect(navContent).toMatch(/[\u4e00-\u9fa5]/);
});
/**
* NAV-02: Verify page titles update on language change
* Priority: P0
*/
test('NAV-02: should update page titles when language changes', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
// Get initial page title in English
const initialTitle = await page.title();
expect(initialTitle).toBeTruthy();
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Wait for title update and verify
await page.waitForTimeout(500);
const updatedTitle = await page.title();
// Title should be different (translated)
// Note: Specific content depends on actual translations
expect(updatedTitle).toBeTruthy();
// Verify lang attribute on document
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('zh');
});
/**
* NAV-03: Verify aria-label updates on navigation
* Priority: P1
*/
test('NAV-03: should update aria-labels on navigation items', async ({ page }) => {
// Get a navigation element with aria-label (e.g., theme toggle)
const themeButton = page.locator('button[aria-label*="switch"], button[aria-label*="mode"]').first();
// Verify initial aria-label
await expect(themeButton).toBeVisible();
const initialAriaLabel = await themeButton.getAttribute('aria-label');
expect(initialAriaLabel).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify theme button is still visible
await expect(themeButton).toBeVisible();
});
/**
* NAV-04: Verify lang attribute on language switch
* Priority: P1
*/
test('NAV-04: should update lang attribute when switching language', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
// Verify initial lang attribute
const initialLang = await page.evaluate(() => document.documentElement.lang);
expect(initialLang).toBe('en');
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify lang attribute is updated
const langAfterSwitch = await page.evaluate(() => document.documentElement.lang);
expect(langAfterSwitch).toBe('zh');
// Verify persistence after reload
await verifyPersistenceAfterReload(page, 'zh');
// Switch back to English
await switchLanguageAndVerify(page, 'en', languageSwitcher);
// Verify lang attribute returns to English
const langAfterReturn = await page.evaluate(() => document.documentElement.lang);
expect(langAfterReturn).toBe('en');
});
/**
* Additional: Navigation maintains language across routes
*/
test('should maintain language when navigating between pages', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Try to navigate to settings if available
const settingsLink = page.getByRole('link', { name: /settings/i });
const isVisible = await settingsLink.isVisible().catch(() => false);
if (isVisible) {
await settingsLink.click();
await page.waitForLoadState('networkidle');
// Verify language is maintained on new page
await verifyI18nState(page, 'zh');
// Verify navigation still shows Chinese (check text content)
await expect(languageSwitcher).toContainText('中文');
}
});
/**
* Additional: Verify navigation links work in both languages
*/
test('navigation links should work in both English and Chinese', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
// Find navigation links
const navLinks = page.locator('nav a').first();
const isVisible = await navLinks.isVisible().catch(() => false);
if (isVisible) {
// Click link in English
await navLinks.click();
await page.waitForLoadState('networkidle');
const englishTitle = await page.title();
// Go back
await page.goBack();
await page.waitForLoadState('networkidle');
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Click same link in Chinese
await navLinks.click();
await page.waitForLoadState('networkidle');
// Verify page loaded successfully
const chineseTitle = await page.title();
expect(chineseTitle).toBeTruthy();
// Language should be maintained
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('zh');
}
});
});

View File

@@ -0,0 +1,324 @@
// ========================================
// E2E Tests: Sessions Page i18n
// ========================================
// Tests for sessions page internationalization
import { test, expect } from '@playwright/test';
import {
switchLanguageAndVerify,
verifyI18nState,
} from './helpers/i18n-helpers';
test.describe('[Sessions Page] - i18n E2E Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
// Navigate to sessions page
const sessionsLink = page.getByRole('link', { name: /sessions|history/i });
const isVisible = await sessionsLink.isVisible().catch(() => false);
if (isVisible) {
await sessionsLink.click();
await page.waitForLoadState('networkidle');
}
});
/**
* SES-01: Verify sessions list headers are translated
* Priority: P1
*/
test('SES-01: should translate sessions list headers', async ({ page }) => {
// Look for table headers or list headers
const headers = page.locator('table th, thead th, [role="columnheader"]');
const count = await headers.count();
if (count > 0) {
// Get initial header text
const initialHeader = await headers.first().textContent();
expect(initialHeader).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify i18n state
await verifyI18nState(page, 'zh');
// Get updated header
const updatedHeader = await headers.first().textContent();
expect(updatedHeader).toBeTruthy();
// Header should contain Chinese characters or be different
const hasChineseOrDifferent = updatedHeader !== initialHeader ||
/[\u4e00-\u9fa5]/.test(updatedHeader || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* SES-02: Verify session status badges are translated
* Priority: P2
*/
test('SES-02: should translate session status badges', async ({ page }) => {
// Look for status badges
const statusBadges = page.locator('[class*="status"], [class*="badge"], span[title*="status"]');
const count = await statusBadges.count();
if (count > 0) {
// Get initial status text
const initialStatus = await statusBadges.first().textContent();
expect(initialStatus).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated status
const updatedStatus = await statusBadges.first().textContent();
expect(updatedStatus).toBeTruthy();
// Status should contain Chinese characters or be different
const hasChineseOrDifferent = updatedStatus !== initialStatus ||
/[\u4e00-\u9fa5]/.test(updatedStatus || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* SES-03: Verify date/time formatting per locale
* Priority: P2
*/
test('SES-03: should format date/time according to locale', async ({ page }) => {
// Look for date/time elements
const dateElements = page.locator('time, [datetime], [class*="date"], [class*="time"]');
const count = await dateElements.count();
if (count > 0) {
// Get initial date
const initialDate = await dateElements.first().textContent();
expect(initialDate).toBeTruthy();
expect(initialDate?.length).toBeGreaterThan(0);
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated date
const updatedDate = await dateElements.first().textContent();
expect(updatedDate).toBeTruthy();
// Date should be present (formatting may differ by locale)
expect(updatedDate?.length).toBeGreaterThan(0);
// Verify lang attribute is correct
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('zh');
}
});
/**
* SES-04: Verify session detail view translated
* Priority: P1
*/
test('SES-04: should translate session detail view', async ({ page }) => {
// Look for session detail links
const detailLink = page.locator('a').filter({ hasText: /view|details|#/i }).first();
const isVisible = await detailLink.isVisible().catch(() => false);
if (isVisible) {
// Get initial page content
const initialContent = await page.content();
expect(initialContent).toBeTruthy();
// Click detail link
await detailLink.click();
await page.waitForLoadState('networkidle');
// Get detail page content
const detailContent = await page.content();
expect(detailContent).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify i18n state
await verifyI18nState(page, 'zh');
// Get updated detail content
const updatedContent = await page.content();
// Content should contain Chinese characters
expect(updatedContent).toMatch(/[\u4e00-\u9fa5]/);
}
});
/**
* Additional: Verify action buttons are translated
*/
test('should translate session action buttons', async ({ page }) => {
// Look for action buttons
const actionButtons = page.locator('button').filter({
hasText: /^(resume|delete|export|view|continue)/i
});
const count = await actionButtons.count();
if (count > 0) {
// Get initial button text
const initialText = await actionButtons.first().textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated button text
const updatedText = await actionButtons.first().textContent();
expect(updatedText).toBeTruthy();
// Button text should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify session type/category labels are translated
*/
test('should translate session type labels', async ({ page }) => {
// Look for type/category labels
const typeLabels = page.locator('[class*="type"], [class*="category"], span[title*="type"]');
const count = await typeLabels.count();
if (count > 0) {
// Get initial label text
const initialLabel = await typeLabels.first().textContent();
expect(initialLabel).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated label
const updatedLabel = await typeLabels.first().textContent();
expect(updatedLabel).toBeTruthy();
// Label should contain Chinese characters or be different
const hasChineseOrDifferent = updatedLabel !== initialLabel ||
/[\u4e00-\u9fa5]/.test(updatedLabel || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify empty state is translated
*/
test('should translate empty state message', async ({ page }) => {
// Look for empty state
const emptyState = page.getByText(/no sessions|empty|no history/i);
const isVisible = await emptyState.isVisible().catch(() => false);
if (isVisible) {
// Get initial text
const initialText = await emptyState.textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated text
const updatedText = await emptyState.textContent();
expect(updatedText).toBeTruthy();
// Should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify pagination controls are translated
*/
test('should translate pagination controls', async ({ page }) => {
// Look for pagination
const pagination = page.locator('[class*="pagination"], nav[aria-label*="pagination"]');
const isVisible = await pagination.isVisible().catch(() => false);
if (isVisible) {
// Get initial pagination text
const initialText = await pagination.textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated pagination text
const updatedText = await pagination.textContent();
expect(updatedText).toBeTruthy();
// Pagination should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify search/filter controls are translated
*/
test('should translate search and filter controls', async ({ page }) => {
// Look for search inputs
const searchInput = page.getByPlaceholder(/search|filter|find/i).or(
page.getByRole('searchbox')
).first();
const isVisible = await searchInput.isVisible().catch(() => false);
if (isVisible) {
// Get initial placeholder
const initialPlaceholder = await searchInput.getAttribute('placeholder');
expect(initialPlaceholder).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated placeholder
const updatedPlaceholder = await searchInput.getAttribute('placeholder');
// Placeholder should be different or contain Chinese characters
if (updatedPlaceholder) {
const hasChineseOrDifferent = updatedPlaceholder !== initialPlaceholder ||
/[\u4e00-\u9fa5]/.test(updatedPlaceholder);
expect(hasChineseOrDifferent).toBeTruthy();
}
}
});
/**
* Additional: Verify language persists when viewing session details
*/
test('should maintain language when navigating session history', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Try to navigate (simulate session navigation)
const navLinks = await page.locator('a').count();
if (navLinks > 0) {
const firstLink = page.locator('a').first();
await firstLink.click();
await page.waitForLoadState('networkidle');
// Verify language is maintained
await verifyI18nState(page, 'zh');
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('zh');
}
});
});

View File

@@ -0,0 +1,235 @@
// ========================================
// E2E Tests: Settings Page i18n
// ========================================
// Tests for settings page internationalization
import { test, expect } from '@playwright/test';
import {
switchLanguageAndVerify,
verifyI18nState,
verifyPersistenceAfterReload,
} from './helpers/i18n-helpers';
test.describe('[Settings Page] - i18n E2E Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
// Navigate to settings page
const settingsLink = page.getByRole('link', { name: /settings/i });
const isVisible = await settingsLink.isVisible().catch(() => false);
if (isVisible) {
await settingsLink.click();
await page.waitForLoadState('networkidle');
}
});
/**
* SET-01: Verify settings page render in English
* Priority: P0
*/
test('SET-01: should render settings page in English', async ({ page }) => {
// Verify initial locale is English
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('en');
// Verify settings page has English content
const pageContent = await page.content();
expect(pageContent).toBeTruthy();
// Look for settings-related headings or labels
const heading = page.getByRole('heading', { name: /settings/i });
const isVisible = await heading.isVisible().catch(() => false);
if (isVisible) {
await expect(heading).toBeVisible();
}
});
/**
* SET-02: Verify settings page render in Chinese
* Priority: P0
*/
test('SET-02: should render settings page in Chinese', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify i18n state
await verifyI18nState(page, 'zh');
// Verify settings page has Chinese content
const pageContent = await page.content();
expect(pageContent).toMatch(/[\u4e00-\u9fa5]/);
});
/**
* SET-03: Verify localStorage persists locale selection
* Priority: P0
*/
test('SET-03: should persist locale selection to localStorage', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify localStorage contains locale
const storage = await page.evaluate(() => {
const item = localStorage.getItem('ccw-app-store');
return item ? JSON.parse(item) : null;
});
expect(storage).not.toBeNull();
expect(storage?.state?.locale).toBe('zh');
// Verify persistence after reload
await verifyPersistenceAfterReload(page, 'zh');
});
/**
* SET-04: Verify aria-label updates on settings form
* Priority: P0
*/
test('SET-04: should update aria-labels on settings form controls', async ({ page }) => {
// Find form controls with aria-label
const formInputs = page.locator('input[aria-label], select[aria-label], button[aria-label]').first();
const isVisible = await formInputs.isVisible().catch(() => false);
if (isVisible) {
// Get initial aria-label
const initialAriaLabel = await formInputs.getAttribute('aria-label');
expect(initialAriaLabel).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated aria-label
const updatedAriaLabel = await formInputs.getAttribute('aria-label');
expect(updatedAriaLabel).toBeTruthy();
// Aria-label should be different (translated)
// Note: Some aria-labels might not change if they're UI-independent
if (initialAriaLabel === updatedAriaLabel) {
// If same, verify at least lang attribute changed
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('zh');
}
}
});
/**
* SET-05: Verify form input placeholders are translated
* Priority: P0
*/
test('SET-05: should translate form input placeholders', async ({ page }) => {
// Find inputs with placeholders
const inputsWithPlaceholder = page.locator('input[placeholder]').first();
const isVisible = await inputsWithPlaceholder.isVisible().catch(() => false);
if (isVisible) {
// Get initial placeholder
const initialPlaceholder = await inputsWithPlaceholder.getAttribute('placeholder');
expect(initialPlaceholder).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated placeholder
const updatedPlaceholder = await inputsWithPlaceholder.getAttribute('placeholder');
// Placeholder should be different (translated) or contain Chinese characters
if (updatedPlaceholder) {
const hasChineseOrDifferent = updatedPlaceholder !== initialPlaceholder ||
/[\u4e00-\u9fa5]/.test(updatedPlaceholder);
expect(hasChineseOrDifferent).toBeTruthy();
}
}
});
/**
* Additional: Verify save/cancel buttons are translated
*/
test('should translate save and cancel buttons', async ({ page }) => {
// Find action buttons
const saveButton = page.getByRole('button', { name: /save|apply/i }).first();
const isSaveVisible = await saveButton.isVisible().catch(() => false);
if (isSaveVisible) {
// Get initial button text
const initialText = await saveButton.textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated button text
const updatedText = await saveButton.textContent();
expect(updatedText).toBeTruthy();
// Text should be different (translated) or contain Chinese characters
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify settings state persists across sessions
*/
test('should maintain language preference across browser sessions', async ({ page, context }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
// Switch to Chinese
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Close and reopen page
await page.close();
const newPage = await context.newPage();
await newPage.goto('/', { waitUntil: 'networkidle' });
// Navigate to settings again
const settingsLink = newPage.getByRole('link', { name: /settings/i });
const isVisible = await settingsLink.isVisible().catch(() => false);
if (isVisible) {
await settingsLink.click();
await newPage.waitForLoadState('networkidle');
}
// Verify language is still Chinese (check text content)
const newLanguageSwitcher = newPage.getByRole('combobox', { name: /select language/i }).first();
await expect(newLanguageSwitcher).toContainText('中文');
const lang = await newPage.evaluate(() => document.documentElement.lang);
expect(lang).toBe('zh');
});
/**
* Additional: Verify form validation messages are translated
*/
test('should translate form validation messages', async ({ page }) => {
// Try to find a required input
const requiredInput = page.locator('input[required], select[required]').first();
const isVisible = await requiredInput.isVisible().catch(() => false);
if (isVisible) {
// Get validation message in English
const englishMessage = await requiredInput.evaluate(el =>
(el as HTMLInputElement).validationMessage
);
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i }).first();
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Note: Browser validation messages might not translate
// This test verifies the mechanism exists even if browser provides English
const lang = await page.evaluate(() => document.documentElement.lang);
expect(lang).toBe('zh');
}
});
});

View File

@@ -0,0 +1,261 @@
// ========================================
// E2E Tests: Skills Page i18n
// ========================================
// Tests for skills page internationalization
import { test, expect } from '@playwright/test';
import {
switchLanguageAndVerify,
verifyI18nState,
} from './helpers/i18n-helpers';
test.describe('[Skills Page] - i18n E2E Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
// Navigate to skills page
const skillsLink = page.getByRole('link', { name: /skills/i });
const isVisible = await skillsLink.isVisible().catch(() => false);
if (isVisible) {
await skillsLink.click();
await page.waitForLoadState('networkidle');
}
});
/**
* SKL-01: Verify skills list headers are translated
* Priority: P1
*/
test('SKL-01: should translate skills list headers', async ({ page }) => {
// Look for page headers
const pageHeading = page.getByRole('heading', { level: 1 }).or(
page.getByRole('heading', { level: 2 })
).first();
const initialHeading = await pageHeading.textContent();
expect(initialHeading).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Verify i18n state
await verifyI18nState(page, 'zh');
// Get updated heading
const updatedHeading = await pageHeading.textContent();
expect(updatedHeading).toBeTruthy();
// Heading should contain Chinese characters or be different
const hasChineseOrDifferent = updatedHeading !== initialHeading ||
/[\u4e00-\u9fa5]/.test(updatedHeading || '');
expect(hasChineseOrDifferent).toBeTruthy();
});
/**
* SKL-02: Verify skill categories are translated
* Priority: P1
*/
test('SKL-02: should translate skill categories', async ({ page }) => {
// Look for category labels or sections
const categoryElements = page.locator('[class*="category"], h3, h4').filter({ hasText: /.+/ });
const count = await categoryElements.count();
if (count > 0) {
// Get initial category text
const initialCategory = await categoryElements.first().textContent();
expect(initialCategory).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated category text
const updatedCategory = await categoryElements.first().textContent();
expect(updatedCategory).toBeTruthy();
// Category should contain Chinese characters or be different
const hasChineseOrDifferent = updatedCategory !== initialCategory ||
/[\u4e00-\u9fa5]/.test(updatedCategory || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* SKL-03: Verify action buttons are translated
* Priority: P1
*/
test('SKL-03: should translate action buttons', async ({ page }) => {
// Look for action buttons
const actionButtons = page.locator('button').filter({
hasText: /^(use|execute|run|create|edit|delete)/i
});
const count = await actionButtons.count();
if (count > 0) {
// Get first button text
const initialText = await actionButtons.first().textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated button text
const updatedText = await actionButtons.first().textContent();
expect(updatedText).toBeTruthy();
// Button text should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* SKL-04: Verify description text is translated
* Priority: P1
*/
test('SKL-04: should translate skill descriptions', async ({ page }) => {
// Look for description elements
const descriptions = page.locator('p, [class*="description"], [class*="detail"]');
const count = await descriptions.count();
if (count > 0) {
// Get initial description text
const initialDescription = await descriptions.first().textContent();
expect(initialDescription).toBeTruthy();
expect(initialDescription?.length).toBeGreaterThan(10);
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated description
const updatedDescription = await descriptions.first().textContent();
expect(updatedDescription).toBeTruthy();
// Description should contain Chinese characters or be different
const hasChineseOrDifferent = updatedDescription !== initialDescription ||
/[\u4e00-\u9fa5]/.test(updatedDescription || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify search/filter controls are translated
*/
test('should translate search and filter controls', async ({ page }) => {
// Look for search inputs
const searchInput = page.getByPlaceholder(/search|filter/i).or(
page.getByRole('searchbox')
).first();
const isVisible = await searchInput.isVisible().catch(() => false);
if (isVisible) {
// Get initial placeholder
const initialPlaceholder = await searchInput.getAttribute('placeholder');
expect(initialPlaceholder).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated placeholder
const updatedPlaceholder = await searchInput.getAttribute('placeholder');
// Placeholder should be different or contain Chinese characters
if (updatedPlaceholder) {
const hasChineseOrDifferent = updatedPlaceholder !== initialPlaceholder ||
/[\u4e00-\u9fa5]/.test(updatedPlaceholder);
expect(hasChineseOrDifferent).toBeTruthy();
}
}
});
/**
* Additional: Verify skill cards display translated content
*/
test('should translate skill card content', async ({ page }) => {
// Look for skill cards
const skillCards = page.locator('[class*="card"], article').filter({ hasText: /.+/ });
const count = await skillCards.count();
if (count > 0) {
// Get initial card content
const initialContent = await skillCards.first().textContent();
expect(initialContent).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated card content
const updatedContent = await skillCards.first().textContent();
expect(updatedContent).toBeTruthy();
// Card content should contain Chinese characters or be different
const hasChineseOrDifferent = updatedContent !== initialContent ||
/[\u4e00-\u9fa5]/.test(updatedContent || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify empty state is translated
*/
test('should translate empty state message', async ({ page }) => {
// Look for empty state
const emptyState = page.getByText(/no skills|empty|no data/i);
const isVisible = await emptyState.isVisible().catch(() => false);
if (isVisible) {
// Get initial text
const initialText = await emptyState.textContent();
expect(initialText).toBeTruthy();
// Switch to Chinese
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Get updated text
const updatedText = await emptyState.textContent();
expect(updatedText).toBeTruthy();
// Should contain Chinese characters or be different
const hasChineseOrDifferent = updatedText !== initialText ||
/[\u4e00-\u9fa5]/.test(updatedText || '');
expect(hasChineseOrDifferent).toBeTruthy();
}
});
/**
* Additional: Verify modal/dialog content is translated
*/
test('should translate modal content when viewing skill details', async ({ page }) => {
const languageSwitcher = page.getByRole('combobox', { name: /select language/i });
// Switch to Chinese first
await switchLanguageAndVerify(page, 'zh', languageSwitcher);
// Look for skill detail buttons
const detailButton = page.getByRole('button', { name: /view|details|more/i }).first();
const isVisible = await detailButton.isVisible().catch(() => false);
if (isVisible) {
await detailButton.click();
await page.waitForTimeout(500);
// Look for modal/dialog
const modal = page.locator('[role="dialog"], .modal, [class*="modal"]');
const modalVisible = await modal.isVisible().catch(() => false);
if (modalVisible) {
// Verify modal has Chinese content
const modalContent = await modal.textContent();
expect(modalContent).toMatch(/[\u4e00-\u9fa5]/);
}
}
});
});