From 2729e7a03518ddc0039845c4aa1c5ba7d9b109bb Mon Sep 17 00:00:00 2001 From: IK Date: Mon, 21 Sep 2026 17:48:17 +0700 Subject: [PATCH] test: cover theme persistence and reduced motion --- apps/web/tests/theme-acceptance.spec.ts | 39 +++++++++++++++++++++++++ docs/ROADMAP.md | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 apps/web/tests/theme-acceptance.spec.ts diff --git a/apps/web/tests/theme-acceptance.spec.ts b/apps/web/tests/theme-acceptance.spec.ts new file mode 100644 index 0000000..acc5a2b --- /dev/null +++ b/apps/web/tests/theme-acceptance.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from "@playwright/test"; + +test("theme switcher persists the explicit choice and keeps browser metadata in sync", async ({ page, context }) => { + await context.clearCookies(); + await page.goto("/"); + + await expect(page.locator("html")).not.toHaveAttribute("data-theme"); + await expect(page.locator('[data-theme-option="system"]')).toHaveAttribute("aria-pressed", "true"); + + await page.locator('[data-theme-option="dark"]').click(); + await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); + await expect(page.locator('[data-theme-option="dark"]')).toHaveAttribute("aria-pressed", "true"); + await expect(page.locator('meta[data-theme-color="dark"]')).toHaveAttribute("media", "all"); + await expect(page.locator('meta[data-theme-color="light"]')).toHaveAttribute("media", "not all"); + + const cookies = await context.cookies(); + expect(cookies.find(cookie => cookie.name === "rf4-theme")).toMatchObject({ value: "dark", sameSite: "Lax" }); + + await page.reload(); + await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); + await expect(page.locator('[data-theme-option="dark"]')).toHaveAttribute("aria-pressed", "true"); + + await page.locator('[data-theme-option="system"]').click(); + await expect(page.locator("html")).not.toHaveAttribute("data-theme"); + await expect(page.locator('[data-theme-option="system"]')).toHaveAttribute("aria-pressed", "true"); + await expect(page.locator('meta[data-theme-color="dark"]')).toHaveAttribute("media", "(prefers-color-scheme: dark)"); +}); + +test("reduced motion disables decorative animation loops", async ({ page }) => { + await page.emulateMedia({ reducedMotion: "reduce" }); + await page.goto("/"); + + const motion = await page.locator(".pulse-orb span").evaluate(element => { + const style = getComputedStyle(element); + return { duration: style.animationDuration, iterations: style.animationIterationCount }; + }); + expect(motion.duration).toBe("0.01s"); + expect(motion.iterations).toBe("1"); +}); diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 33718bd..c839adb 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -84,7 +84,7 @@ - [x] **D02 · Системный режим без вспышки.** Первый визит следует `prefers-color-scheme` полностью через CSS: серверный HTML сразу совместим с системной темой, состояние не хранится, inline bootstrap не добавлен и CSP не ослаблена. Явное переопределение появится только вместе с D03–D04. - [x] **D03 · Переключатель темы.** В header добавлен доступный трёхпозиционный выбор «Системная / Светлая / Тёмная»; на узких экранах он сохраняет три понятные иконки и доступные названия. Кнопки работают с клавиатуры, имеют видимый focus и синхронизируют `aria-pressed`. - [x] **D04 · Сохранение и SSR-согласование.** Выбор хранится год в allowlist-cookie `rf4-theme` с `SameSite=Lax` и `Secure` на HTTPS; Astro SSR выставляет `data-theme` до отрисовки, а системный режим оставляет выбор браузеру. Собранный Astro client script переключает тему без inline-кода, localStorage и ослабления CSP. -- [ ] **D05 · Темизация компонентов и графики.** На семантические light/dark-токены переведены базовые и admin-поверхности, header, каталог, breadcrumb-леска, ссылки сущностей, таблицы, формы, moderation-карточки, provenance/quality/status badges, паспорта данных, pagination, empty/loading-состояния, радар, timeline, силуэты и глифы; добавлен forced-colors layer. In-app Chromium подтвердил переключение light/dark/system на медиатеке во всех четырёх ширинах; automated media acceptance отдельно подтверждает light/dark на 320 px, standalone axe проходит `12/12` маршрутов плюс 6 доменных маршрутов, forced-colors и print regression подтверждены. Launcher рассчитан на полную screenshot-матрицу `204` PNG без document overflow; representative review тени/градиентов выполнен, а reference-набор остаётся внешним артефактом. Источники и статусы сохраняют текстовые подписи и не различаются только цветом. +- [ ] **D05 · Темизация компонентов и графики.** На семантические light/dark-токены переведены базовые и admin-поверхности, header, каталог, breadcrumb-леска, ссылки сущностей, таблицы, формы, moderation-карточки, provenance/quality/status badges, паспорта данных, pagination, empty/loading-состояния, радар, timeline, силуэты и глифы; добавлен forced-colors layer. In-app Chromium подтвердил переключение light/dark/system на медиатеке во всех четырёх ширинах и сохранение явной темы после reload с синхронными `theme-color`; automated media acceptance отдельно подтверждает light/dark на 320 px, standalone axe проходит `12/12` маршрутов плюс 6 доменных маршрутов, forced-colors и print regression подтверждены. Focused `theme-acceptance` regression фиксирует SSR/client-согласование cookie и мета-тегов, а также отключение декоративных animation loops при `prefers-reduced-motion`. Launcher рассчитан на полную screenshot-матрицу `204` PNG без document overflow; representative review тени/градиентов выполнен, а reference-набор остаётся внешним артефактом. Источники и статусы сохраняют текстовые подписи и не различаются только цветом. - [x] **D06 · Метаданные браузера и CSP.** Добавлены парные `theme-color` для системной light/dark схемы; явный cookie-выбор согласуется с SSR и мгновенно переключает активный meta-тег. PWA manifest использует устойчивый тёмный brand chrome и splash background. Production build сохраняет `inlinedScripts: []`; новые inline script/style/attributes не появились, ослабление CSP не потребовалось. - [ ] **D07 · Визуальная и accessibility-приёмка.** In-app Chromium подтвердил отсутствие горизонтального scroll на публичных `/`, `/waterbodies`, `/records`, `/report`, `/status`, `/media`, `/rules`, waterbody detail `/waterbodies/р-вьюнок` и fish detail `/fish/pike`, а также admin `/admin`, `/admin/moderation`, `/admin/external-sources`, `/admin/media` в 320/390/768/1280 px; исправлены tablet header и media-grid overflow. Автоматизированный visual-matrix regression и `scripts/capture-visual-matrix.mjs` подтверждают 17 маршрутов × 4 ширины × 3 режима (`204` комбинации), включая spot detail, plan и tackle analytics: document overflow отсутствует, везде есть `main`/`h1`, SSR-тема совпадает; launcher настроен на полный внешний capture. Для media и двух detail-страниц light/dark/system корректно переключаются, broken images — `0`; axe `12/12`, отдельная domain accessibility-проверка охватывает 6 public/domain routes, keyboard/focus, forced-colors и print-regression проходят. Representative manual review и ограничения reference-хранения зафиксированы в `docs/ux-reference-review-2026-09-21.md`. Для обеих тем обеспечить WCAG AA, отсутствие горизонтального scroll и CLS, reduced motion и переключение без потери введённых данных; сохранить эталонные screenshots и краткий отчёт.