UX全体にテーマと言語設定を追加。 - lib/db/migrations/004_user_prefs.sql: users に theme/locale 列を追加(既定 dark/en) - lib/i18n/: dictionary(en/ja) + I18nProvider(クライアントcontext: locale/theme/t/setLocale/setTheme) + server.ts(SSR用 getLocale/getTheme/translate) + constants.ts - app/layout.tsx: theme/locale Cookie を読み <html class/lang> をSSR、I18nProvider でラップ(フラッシュなし) - tailwind darkMode:'class' + 全画面に dark: バリアントを一括付与(Nodeスクリプト lookbehind で安全に変換) - components/layout/ThemeToggle: 即時クラス切替+Cookie+永続化 - app/profile: テーマ/言語セレクタ、chrome翻訳(Header/ProjectNav/login/dashboard/profile) - PATCH /api/users/me: theme/locale を受理(バリデーション→400)、Cookieを設定 - E2E: locale=ja storageState で既存JAアサーションを維持、theme-i18n.spec で既定en/darkと切替を検証
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test';
|
|
|
|
function unique(prefix: string): string {
|
|
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
|
|
}
|
|
|
|
async function setupOwner(page: Page): Promise<number> {
|
|
const email = unique('owner') + '@example.com';
|
|
await page.goto('/login');
|
|
await page.getByRole('button', { name: '新規登録はこちら' }).click();
|
|
await page.getByLabel('表示名').fill('Owner');
|
|
await page.getByLabel('メールアドレス').fill(email);
|
|
await page.getByLabel('パスワード').fill('password123');
|
|
await page.getByRole('button', { name: '登録する' }).click();
|
|
await expect(page).toHaveURL(/\/dashboard/);
|
|
await page.getByLabel('プロジェクト名').fill(unique('Proj'));
|
|
await page.getByRole('button', { name: '新規プロジェクト' }).click();
|
|
await expect(page).toHaveURL(/\/projects\/\d+$/);
|
|
return Number(page.url().match(/\/projects\/(\d+)/)![1]);
|
|
}
|
|
|
|
test.describe('theme & i18n', () => {
|
|
test('a fresh visitor gets English locale and dark theme by default', async ({
|
|
browser,
|
|
}) => {
|
|
// storageState を持たない = 初回訪問者(既定 en / dark)
|
|
const ctx = await browser.newContext();
|
|
const page = await ctx.newPage();
|
|
await page.goto('/login');
|
|
await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible();
|
|
const className = await page.evaluate(
|
|
() => document.documentElement.className
|
|
);
|
|
expect(className).toContain('dark');
|
|
await ctx.close();
|
|
});
|
|
|
|
test('theme toggle switches dark<->light and persists', async ({ page }) => {
|
|
await setupOwner(page);
|
|
// 既定は dark
|
|
expect(
|
|
await page.evaluate(() => document.documentElement.className)
|
|
).toContain('dark');
|
|
|
|
await page.getByTestId('theme-toggle').click();
|
|
expect(
|
|
await page.evaluate(() => document.documentElement.className)
|
|
).not.toContain('dark');
|
|
|
|
// リロード後もライトが維持される(Cookie永続化)
|
|
await page.reload();
|
|
expect(
|
|
await page.evaluate(() => document.documentElement.className)
|
|
).not.toContain('dark');
|
|
});
|
|
|
|
test('language switch ja -> en updates chrome and persists', async ({
|
|
page,
|
|
}) => {
|
|
const projectId = await setupOwner(page);
|
|
await page.goto(`/projects/${projectId}`);
|
|
// E2E既定は ja → ヘッダーに ダッシュボード
|
|
await expect(
|
|
page.getByRole('link', { name: 'ダッシュボード' })
|
|
).toBeVisible();
|
|
|
|
// プロフィールで en に切替
|
|
await page.goto('/profile');
|
|
await page.getByTestId('profile-locale-select').selectOption('en');
|
|
|
|
// クロムが英語に切替
|
|
await page.goto(`/projects/${projectId}`);
|
|
await expect(page.getByRole('link', { name: 'Dashboard' })).toBeVisible();
|
|
});
|
|
});
|