diff --git a/web/e2e/theme-toggle.spec.ts b/web/e2e/theme-toggle.spec.ts new file mode 100644 index 00000000..4f4cfff7 --- /dev/null +++ b/web/e2e/theme-toggle.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test' +import { setEnglishLocale } from './helpers/auth-fixtures' + +test.describe('Light and dark theme', () => { + test.beforeEach(async ({ page }) => { + await setEnglishLocale(page) + await page.context().setExtraHTTPHeaders({ 'X-Mock-User-Id': 'local-user' }) + await page.addInitScript(() => { + if (!window.sessionStorage.getItem('theme-test-initialized')) { + window.localStorage.removeItem('skillhub-theme') + window.sessionStorage.setItem('theme-test-initialized', 'true') + } + }) + }) + + test('switches themes and restores only the browser-local selection', async ({ page }, testInfo) => { + const consoleErrors: string[] = [] + const pageErrors: string[] = [] + page.on('console', (message) => { + if (message.type() === 'error') consoleErrors.push(message.text()) + }) + page.on('pageerror', (error) => pageErrors.push(error.stack ?? error.message)) + + await page.goto('/') + await expect(page.locator('html')).not.toHaveClass(/dark/) + + await page.getByRole('button', { name: 'Switch to dark theme' }).click() + await expect(page.locator('html')).toHaveClass(/dark/) + await expect.poll(() => page.evaluate(() => window.localStorage.getItem('skillhub-theme'))).toBe('dark') + + await page.reload() + await expect(page.locator('html')).toHaveClass(/dark/) + await expect(page.getByRole('button', { name: 'Switch to light theme' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'SkillHub', exact: true })).toBeVisible() + await page.screenshot({ path: testInfo.outputPath('dark-desktop.png'), fullPage: true }) + + await page.setViewportSize({ width: 390, height: 844 }) + await expect(page.getByRole('button', { name: 'Switch to light theme' })).toBeVisible() + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true) + await page.screenshot({ path: testInfo.outputPath('dark-mobile.png'), fullPage: true }) + + const unexpectedConsoleErrors = consoleErrors.filter((message) => ( + !message.includes("frame-ancestors' is ignored when delivered via a element") + )) + expect(unexpectedConsoleErrors).toEqual([]) + expect(pageErrors).toEqual([]) + }) +}) diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 7a52720b..af700349 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Outlet, Link, useRouterState } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { useAuth } from '@/features/auth/use-auth' import { LanguageSwitcher } from '@/shared/components/language-switcher' +import { ThemeToggle } from '@/shared/components/theme-toggle' import { UserMenu } from '@/shared/components/user-menu' import { NotificationBell } from '@/features/notification/notification-bell' import { dismissOpenOverlays } from '@/shared/lib/dismiss-open-overlays' @@ -115,7 +116,8 @@ export function Layout() { })} -
+
+ {user && } {isLoading ? null : user ? ( @@ -149,7 +151,7 @@ export function Layout() { {/* Footer */} -