dimensionlab-website/tests/e2e/dashboard.spec.ts
2026-06-18 19:50:28 +02:00

122 lines
3.5 KiB
TypeScript

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;
}