// Phase 1 smoke tests. Frontend-only — no Tauri IPC. // Visual screenshots are saved as artifacts; they do NOT fail the build at // this stage (see playwright.config.ts header). Promotion to failing visual // diffs happens after Phase 7 stabilises the UI. import { test, expect } from "@playwright/test"; import AxeBuilder from "@axe-core/playwright"; const ZONES = ["cave", "energy", "reset"] as const; const THEMES = ["light", "dark"] as const; test.describe("Lumotia frontend smoke (no Tauri IPC)", () => { test("app loads without Tauri runtime", async ({ page }) => { // Suppress noisy invoke-rejected promises from the layout's onMount; // they are expected without Tauri. We only fail on unhandled crashes. const fatalErrors: string[] = []; page.on("pageerror", (err) => fatalErrors.push(err.message)); await page.goto("/"); await expect(page.locator("main")).toBeVisible(); expect(fatalErrors).toEqual([]); }); test("keyboard navigation reaches main + sidebar surface", async ({ page }) => { await page.goto("/"); await page.locator("main").waitFor({ state: "visible" }); // Tab once and confirm focus moves to a focusable element somewhere in // the document, not nothing. We don't assert the specific element to // keep the test robust through chrome migration. await page.keyboard.press("Tab"); const focused = await page.evaluate(() => document.activeElement && document.activeElement !== document.body ? document.activeElement.tagName : null, ); expect(focused).not.toBeNull(); }); test("axe scan on / has no critical structural violations", async ({ page }) => { await page.goto("/"); await page.locator("main").waitFor({ state: "visible" }); const results = await new AxeBuilder({ page }) .disableRules([ // svelte-i18n initialises asynchronously; is set after // first paint in browser-preview mode. "html-has-lang", // Colour-contrast is owned by docs/release/v0.1-contrast-audit.md // and gets re-enabled in Phase 7 after the wrapper palette settles. // Phase 1's smoke baseline focuses on structural/semantic a11y. "color-contrast", ]) .analyze(); const criticals = results.violations.filter( (v) => v.impact === "critical" || v.impact === "serious", ); expect(criticals).toEqual([]); }); for (const theme of THEMES) { test(`theme smoke — ${theme}`, async ({ page }, testInfo) => { await page.goto("/"); await page.locator("main").waitFor({ state: "visible" }); // Apply theme without depending on the preferences store, which // needs Tauri persistence. The data-theme attribute is what the CSS // tokens key off; the preferences store also writes this attribute // in production. await page.evaluate((t) => { document.documentElement.dataset.theme = t; }, theme); await page.waitForTimeout(50); await page.screenshot({ path: `test-results/theme-${theme}-${testInfo.project.name}.png`, fullPage: false, }); }); } for (const zone of ZONES) { test(`sensory zone smoke — ${zone}`, async ({ page }, testInfo) => { await page.goto("/"); await page.locator("main").waitFor({ state: "visible" }); await page.evaluate((z) => { document.documentElement.dataset.zone = z; }, zone); await page.waitForTimeout(50); await page.screenshot({ path: `test-results/zone-${zone}-${testInfo.project.name}.png`, fullPage: false, }); }); } });