Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m41s
CI / Build container images (pull_request) Successful in 4m4s
CI / Auth e2e pack (pull_request) Successful in 11m40s
CI / Import/export fidelity gate (pull_request) Successful in 52s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
apply-theme.ts derives BOTH modes' accent tokens from the stored choice
(ui.theme.accent: preset id or {custom:'#hex'}) and writes them as
<style id="user-theme"> with :root:root + :root:root[data-theme='dark']
blocks — the doubled :root beats tokens.css regardless of document
order, since theme-init.js injects the ui.theme.css cache during <head>
parsing, before the bundle styles. The default preset means NO override
(hand-tuned tokens.css values stay). main.tsx re-derives from the
choice at startup, healing stale caches after app updates.
Settings: accent radiogroup inside the Appearance section (visible
names, color never the only cue) with per-mode preview swatches on
each mode's canonical background, plus a custom color input; i18n
de+en. The second fieldset made bare .settings-fieldset locators
ambiguous — theme specs now scope via input[name] (fence stays).
Tests: apply-theme unit pack, BASE_PALETTE<->tokens.css drift fence in
theme-contrast.test.ts, e2e theme-accent.spec (instant apply, pre-paint
persistence, default removes override, axe smoke with garish yellow in
both modes). ADR 0018 amendment documents the stage-B details.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
117 lines
4.4 KiB
TypeScript
117 lines
4.4 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
|
|
import { contextForUser } from './helpers';
|
|
|
|
/**
|
|
* Theme-Umschalter (issue #180): die Hell/Dunkel/System-Wahl in den
|
|
* Einstellungen wirkt sofort, überlebt den Reload (localStorage) und folgt
|
|
* im System-Modus Live-Änderungen des OS-Schemas. Klassen-Hooks statt
|
|
* lokalisierter Texte (UI-Sprache folgt dem Profil-Locale).
|
|
*/
|
|
|
|
const BASE = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
|
|
|
const radio = (page: Page, value: string) =>
|
|
page.locator(`.settings-fieldset input[value="${value}"]`);
|
|
|
|
const effectiveTheme = (page: Page) => page.evaluate(() => document.documentElement.dataset.theme);
|
|
|
|
test('theme choice applies instantly, persists, and system mode follows the OS', async ({
|
|
browser,
|
|
}) => {
|
|
const context = await contextForUser(browser, BASE, 'fixture-user');
|
|
const page = await context.newPage();
|
|
await page.emulateMedia({ colorScheme: 'light' });
|
|
await page.goto('/settings');
|
|
await page.locator('input[name="theme-mode"]').first().waitFor();
|
|
|
|
// Default: System, auf einem hellen OS also light.
|
|
await expect(radio(page, 'system')).toBeChecked();
|
|
expect(await effectiveTheme(page)).toBe('light');
|
|
|
|
// Explizit Dunkel: wirkt sofort, ohne Reload, trotz hellem OS.
|
|
await radio(page, 'dark').check();
|
|
expect(await effectiveTheme(page)).toBe('dark');
|
|
await expect(page.locator('meta[name="theme-color"]').first()).toHaveAttribute(
|
|
'content',
|
|
'#10161d',
|
|
);
|
|
|
|
// Persistenz: Wahl und Theme überleben den Reload (theme-init.js liest
|
|
// denselben localStorage-Key vor dem ersten Paint).
|
|
await page.reload();
|
|
await page.locator('input[name="theme-mode"]').first().waitFor();
|
|
await expect(radio(page, 'dark')).toBeChecked();
|
|
expect(await effectiveTheme(page)).toBe('dark');
|
|
expect(await page.evaluate(() => window.localStorage.getItem('ui.theme.mode'))).toBe('"dark"');
|
|
|
|
// Zurück auf System: folgt dem hellen OS …
|
|
await radio(page, 'system').check();
|
|
expect(await effectiveTheme(page)).toBe('light');
|
|
|
|
// … und Live-Wechseln des OS-Schemas ohne Interaktion.
|
|
await page.emulateMedia({ colorScheme: 'dark' });
|
|
await expect
|
|
.poll(async () => effectiveTheme(page), { message: 'system mode follows OS change' })
|
|
.toBe('dark');
|
|
await page.emulateMedia({ colorScheme: 'light' });
|
|
await expect.poll(async () => effectiveTheme(page)).toBe('light');
|
|
|
|
// Explizite Wahl gewinnt gegen das OS-Schema.
|
|
await page.emulateMedia({ colorScheme: 'dark' });
|
|
await radio(page, 'light').check();
|
|
expect(await effectiveTheme(page)).toBe('light');
|
|
|
|
await context.close();
|
|
});
|
|
|
|
test('the top-bar toggle cycles the mode and stays in sync with the radios', async ({
|
|
browser,
|
|
}) => {
|
|
const context = await contextForUser(browser, BASE, 'fixture-user');
|
|
const page = await context.newPage();
|
|
await page.emulateMedia({ colorScheme: 'light' });
|
|
await page.goto('/settings');
|
|
await page.locator('input[name="theme-mode"]').first().waitFor();
|
|
const toggle = page.locator('.topbar__theme');
|
|
|
|
// Default System → ein Klick zykelt in Radio-Reihenfolge weiter zu Hell,
|
|
// dann Dunkel, dann zurück zu System; die Radios folgen (issue #182).
|
|
await toggle.click();
|
|
await expect(radio(page, 'light')).toBeChecked();
|
|
expect(await effectiveTheme(page)).toBe('light');
|
|
|
|
await toggle.click();
|
|
await expect(radio(page, 'dark')).toBeChecked();
|
|
expect(await effectiveTheme(page)).toBe('dark');
|
|
|
|
await toggle.click();
|
|
await expect(radio(page, 'system')).toBeChecked();
|
|
expect(await effectiveTheme(page)).toBe('light'); // helles OS
|
|
|
|
// Auch andersherum: eine Radio-Wahl versetzt den Zyklus des Toggles.
|
|
await radio(page, 'dark').check();
|
|
await toggle.click();
|
|
await expect(radio(page, 'system')).toBeChecked();
|
|
|
|
// Persistenz wie bei den Radios (gleicher localStorage-Key).
|
|
await toggle.click(); // → light
|
|
await page.reload();
|
|
await page.locator('input[name="theme-mode"]').first().waitFor();
|
|
await expect(radio(page, 'light')).toBeChecked();
|
|
expect(await effectiveTheme(page)).toBe('light');
|
|
|
|
await context.close();
|
|
});
|
|
|
|
test('the theme toggle works for signed-out visitors', async ({ page }) => {
|
|
await page.emulateMedia({ colorScheme: 'light' });
|
|
await page.goto(`${BASE}/login`);
|
|
const toggle = page.locator('.topbar__theme');
|
|
await toggle.waitFor();
|
|
|
|
await toggle.click(); // System → Hell
|
|
await toggle.click(); // Hell → Dunkel
|
|
expect(await effectiveTheme(page)).toBe('dark');
|
|
});
|