import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; const linkedServiceIds = [ "vaultwarden", "forgejo", "wiki", "aws-start", "grafana", "uptime-kuma", "prometheus", "backrest", "n8n", "open-webui", "comfyui", "models", "adminer", "assistant", "suna", "cockpit-infra", "forgejo-ssh-relay", ]; test.describe("dashboard page QA gate", () => { test("renders the model-driven dashboard on desktop", async ({ page, }, testInfo) => { test.skip(testInfo.project.name !== "chromium-desktop"); await page.goto("/"); await expect(page.getByRole("main")).toHaveAttribute( "aria-labelledby", /title/, ); await expect( page.getByRole("heading", { level: 1, name: "System Overview" }), ).toBeVisible(); await expect(page.getByText("Infra RAM")).toBeVisible(); await expect(page.getByRole("link", { name: /Vaultwarden/i })).toBeVisible(); await expect(page.locator("[data-model-id='vaultwarden']")).toBeVisible(); const bodyBox = await page.locator("body").boundingBox(); expect(bodyBox?.width).toBeGreaterThan(1000); await expect(page).toHaveScreenshot("dashboard-desktop.png", { fullPage: true, }); }); test("keeps the first screen usable on mobile", async ({ page }, testInfo) => { test.skip(testInfo.project.name !== "chromium-mobile"); await page.goto("/"); await expect( page.getByRole("heading", { level: 1, name: "System Overview" }), ).toBeVisible(); await expect(page.getByLabel("Service groups")).toBeVisible(); await expect(page).toHaveScreenshot("dashboard-mobile.png", { fullPage: true, }); }); test("exposes usable landmarks and a visible keyboard focus state", async ({ page, }) => { await page.goto("/"); await expect(page.getByRole("main")).toHaveCount(1); for (const serviceId of linkedServiceIds) { await page.keyboard.press("Tab"); const focused = page.locator(":focus"); await expect(focused).toHaveAttribute("data-model-id", serviceId); const focusBoxShadow = await focused.evaluate((element) => { return window.getComputedStyle(element).boxShadow; }); expect(focusBoxShadow).not.toBe("none"); } }); test("passes automated accessibility checks", async ({ page }) => { await page.goto("/"); const results = await new AxeBuilder({ page }).analyze(); expect(results.violations).toEqual([]); }); test("honors reduced-motion preferences", async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await page.goto("/"); const durations = await page.evaluate(() => { const element = document.createElement("div"); element.style.animation = "qa-motion-check 10s infinite"; element.style.transition = "opacity 10s linear"; document.body.append(element); const styles = window.getComputedStyle(element); return { animation: styles.animationDuration, transition: styles.transitionDuration, }; }); expect(cssDurationToMilliseconds(durations.animation)).toBeLessThanOrEqual( 0.01, ); expect(cssDurationToMilliseconds(durations.transition)).toBeLessThanOrEqual( 0.01, ); }); }); function cssDurationToMilliseconds(duration: string): number { if (duration.endsWith("ms")) return Number.parseFloat(duration); if (duration.endsWith("s")) return Number.parseFloat(duration) * 1000; return Number.NaN; }