Files
Lumotia/tests/e2e/smoke.spec.ts
Jake 100e04fa70 v0.2 Phase 0+1: planning doc + tooling baseline
Phase 0 — docs/release/v0.2-frontend-overhaul.md as single source of
truth for the v0.2 frontend coherence pass. Records hard rules,
tooling pins, sacred-behaviour contract list, wrapper catalogue,
per-page migration order, verification matrix, KI-05 plan, and the
explicit "DO NOT add Skeleton" line.

Phase 1 — tooling baseline. All exact-pinned per the plan:

- @playwright/test@1.60.0 + playwright@1.60.0 + @axe-core/playwright@4.11.3
- rollup-plugin-visualizer@7.0.1 (wired behind ANALYZE=1)
- @vitest/browser@4.1.6 + @vitest/browser-playwright@4.1.6 (provider)
- vitest-browser-svelte@2.1.1 (runes-aware Svelte 5 bridge)
- cargo-nextest installed globally

New configs: playwright.config.ts (frontend-only, dev:frontend webServer,
900x700 + 1440x900 projects, visual baselines deferred), vitest.browser.config.js
(separate from jsdom suite). New scripts: test:e2e, test:e2e:ui,
test:browser, analyze, test:rust:fast, guard:no-skeleton.

guard-no-skeleton.mjs walks package.json + package-lock + src/ for any
@skeletonlabs reference and exits 1 if found — locks in the no-Skeleton
hard rule for any future agent.

Smoke baseline (tests/e2e/smoke.spec.ts): 16 tests passing across two
viewports — app loads without Tauri runtime, keyboard nav, axe scan
(color-contrast deferred to Phase 7 per docs/release/v0.1-contrast-audit.md),
light/dark theme cycle, all three sensory zones (cave/energy/reset).
10 screenshots emitted to test-results/ as non-failing artefacts.

Surfaced + fixed two browser-preview bugs while landing the baseline:

- src/lib/utils/osInfo.ts: FALLBACK_BROWSER_INFO was evaluated at SSR
  module-load (navigator undefined → os: 'unknown'), and the UA check
  ran before navigator.platform — so Playwright's Windows-UA Chromium
  on a Linux runner detected as Windows, useCustomChrome went true,
  Titlebar mounted and tripped Tauri-only APIs. Now lazy-built per
  call; platform is the primary signal, UA only a fallback.

- src/lib/components/Titlebar.svelte: defensive hasTauriRuntime() guard
  on every handler and the $effect. Titlebar should not crash if any
  future code path mounts it without Tauri.

.gitignore: reports/, .playwright/, test-results/, playwright-report/.

Per-phase gate green: npm run check (0/0), npm test (0/0), npm run
test:e2e (16/16), npm run guard:no-skeleton (clean).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:28:51 +01:00

92 lines
3.6 KiB
TypeScript

// 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; <html lang> 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,
});
});
}
});