Files
itworx-knowledge-hub-Public/tests/e2e/specs/v2-m6-motion-and-matrices.spec.ts
T

127 lines
6.8 KiB
TypeScript

import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { authenticatedStatePath } from "../global-setup";
import { expectAuthenticatedShell } from "./support/v2";
/**
* V2-M6: visualizations, motion, matrices and dashboard.
*
* `ServerMap`/`ServiceTopology`/`ProjectSignature` and `DataTable` were already
* implemented from the prior Codex session (see docs/v2/V2_ROADMAP.md's V2-M6
* entry). This spec adds the missing real proofs for this milestone's specific
* acceptance criteria: reduced-motion renders a *genuine final state* (not just
* "axe is clean", which src/visualizations/*.test.tsx and the pre-existing
* v2-routing-and-fidelity.spec.ts reduced-motion test already cover at the
* component/axe level), a real matrix route has no page-level horizontal overflow,
* and dashboard widget order/visibility actually round-trips through Settings → a
* real reload of the dashboard route (not just localStorage keys, already proven at
* the unit level in PreferencesContext.test.tsx).
*/
test.use({ storageState: authenticatedStatePath });
test.describe("V2-M6 motion, matrices and dashboard personalization", () => {
test("the server map renders its final, fully-opaque state under prefers-reduced-motion, not a mid-animation frame", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/");
await expect(page.getByRole("heading", { name: "Operationeel overzicht" })).toBeVisible();
const serverMap = page.locator(".server-map");
await expect(serverMap).toBeVisible();
const opacities = await serverMap.locator("svg > g, svg > path").evaluateAll((elements) =>
elements.map((element) => window.getComputedStyle(element).opacity)
);
expect(opacities.length).toBeGreaterThan(0);
for (const opacity of opacities) {
expect(Number(opacity)).toBeCloseTo(1, 2);
}
});
test("a project's signature illustration renders its final, fully-opaque state under prefers-reduced-motion", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
const projectName = `E2E-motion-project ${Date.now()}`;
await page.goto("/projects");
await expectAuthenticatedShell(page);
await page.getByRole("button", { name: "Nieuw project" }).click();
await page.getByLabel("Naam").fill(projectName);
await page.getByRole("button", { name: "Project aanmaken" }).click();
await expect(page.getByRole("heading", { name: projectName })).toBeVisible();
const signature = page.locator(".project-signature");
// Below 900px the decorative signature is deliberately not rendered at all (OUX-6:
// operational data comes before decoration on a narrow screen), so there is no
// final animation state to assert there. Asserting it visible on every viewport
// would be asserting the opposite of the product decision.
const viewport = page.viewportSize();
test.skip(
(viewport?.width ?? 0) < 900,
"The project signature is intentionally hidden below 900px; nothing animates there.",
);
await expect(signature).toBeVisible();
// Only the ".project-core" diamond marker animates opacity (0 → 1 via `animate`);
// the decorative rings around it use a permanent, layered per-ring `opacity`
// *attribute* (e.g. 0.72, 0.64, ...) as a static design choice, not an
// animate-in artifact — asserting rings end at opacity 1 would be a real, wrong
// assumption about this component (confirmed by reading ProjectSignature.tsx,
// not guessed after a confusing first failure).
const coreOpacity = await signature.locator("svg .project-core").evaluate(
(element) => window.getComputedStyle(element).opacity
);
expect(Number(coreOpacity)).toBeCloseTo(1, 2);
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations).toEqual([]);
});
test("the storage matrix causes no page-level horizontal overflow at a realistic viewport", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto("/infrastructure/storage");
await expect(page.getByRole("heading", { name: "Opslag en capaciteit" })).toBeVisible();
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth
);
expect(overflow).toBeLessThanOrEqual(1);
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations).toEqual([]);
});
test("hiding and reordering a dashboard widget in Settings actually changes the real dashboard, and survives a reload", async ({ page }) => {
await page.goto("/settings");
await expectAuthenticatedShell(page);
await expect(page.getByRole("heading", { name: "Dashboardwidgets" })).toBeVisible();
// Hide the "Projecten" widget entirely.
await page.getByRole("checkbox", { name: "Projecten tonen" }).uncheck();
// "Operationele signalen" is the default first widget since OUX-4 (critical
// signals before the topology illustration), so its "up" control is correctly
// disabled. Reorder in the direction that is actually available: move
// "Unraid-overzicht" above it.
await expect(page.getByRole("button", { name: "Operationele signalen omhoog" })).toBeDisabled();
await page.getByRole("button", { name: "Unraid-overzicht omhoog" }).click();
await page.goto("/");
await expect(page.getByRole("heading", { name: "Operationeel overzicht" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Projecten", exact: true })).toHaveCount(0);
const headingOrder = await page.getByRole("main").getByRole("heading", { level: 2 }).allTextContents();
expect(headingOrder.indexOf("Unraid-overzicht")).toBeLessThan(headingOrder.indexOf("Operationele signalen"));
// Reload the dashboard directly (not a fresh in-app navigation) to prove this is
// real persisted state, not just in-memory React state surviving a client route
// change.
await page.reload();
await expect(page.getByRole("heading", { name: "Operationeel overzicht" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Projecten", exact: true })).toHaveCount(0);
const headingOrderAfterReload = await page.getByRole("main").getByRole("heading", { level: 2 }).allTextContents();
expect(headingOrderAfterReload.indexOf("Unraid-overzicht")).toBeLessThan(
headingOrderAfterReload.indexOf("Operationele signalen")
);
// Restore the default so this test does not leak state into any other test that
// shares the same authenticated storage state / owner account.
await page.goto("/settings");
await page.getByRole("checkbox", { name: "Projecten tonen" }).check();
await page.getByRole("button", { name: "Unraid-overzicht omlaag" }).click();
});
});