import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import AxeBuilder from "@axe-core/playwright"; import { expect, test, type Page } from "@playwright/test"; import { authenticatedStatePath } from "../global-setup"; import { expectAuthenticatedShell } from "./support/v2"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); /** * V2-M5: services and project workspaces. * * V2-M2's fidelity pass (docs/v2/V2_EVIDENCE.md "V2-M2") deliberately covered the * *list/overview* screens only (dashboard, infrastructure overview, projects overview, * knowledge tree) and narrowed the full per-route implementation-fidelity-checklist * sweep to V2-M8. This spec adds the missing spot-check for the *detail/workspace* * screens this milestone is actually about — project workspace (screen 10), service * detail (screen 7) and runbook detail (screen 13) — captured with real, if modest, * seeded content rather than an empty state, since Stitch's own references show * populated data. Concrete match/divergence notes are recorded in V2_EVIDENCE.md * "V2-M5", not just "screenshot exists". */ test.use({ storageState: authenticatedStatePath }); const screenshotDir = path.resolve(__dirname, "../../../artifacts/v2-m5/screenshots"); fs.mkdirSync(screenshotDir, { recursive: true }); async function postWithCsrf(page: Page, url: string, data: unknown) { const csrfResponse = await page.request.get("/api/v1/auth/csrf"); const csrf = (await csrfResponse.json()) as { token: string; headerName: string }; return page.request.post(url, { headers: { [csrf.headerName]: csrf.token }, data }); } test.describe("V2-M5 workspace fidelity spot-check", () => { test("project, service and runbook detail workspaces are captured with real content for a fidelity comparison", async ({ page }) => { const stamp = Date.now(); // --- Project workspace, with a milestone/decision/update so the panels are not // all in their empty state, matching what screen 10 (project detail) shows. const projectName = `Facturatieplatform ${stamp}`; await page.goto("/projects"); await expectAuthenticatedShell(page); await page.getByRole("button", { name: "Nieuw project" }).click(); await page.getByLabel("Naam").fill(projectName); await page.getByLabel("Samenvatting").fill("Vernieuwing van het interne facturatieplatform."); await page.getByRole("button", { name: "Project aanmaken" }).click(); await expect(page.getByRole("heading", { name: projectName })).toBeVisible(); await page.getByRole("button", { name: "Mijlpaal" }).click(); await page.getByLabel("Naam").fill("Bèta-release"); await page.getByRole("button", { name: "Mijlpaal toevoegen" }).click(); await expect(page.getByText("Bèta-release")).toBeVisible(); await page.getByRole("button", { name: "Update", exact: true }).click(); await page.getByLabel("Titel").fill("Databasemigratie afgerond"); await page.getByRole("button", { name: "Update toevoegen" }).click(); await expect(page.getByText("Databasemigratie afgerond")).toBeVisible(); await page.getByRole("button", { name: "Beslissing", exact: true }).click(); await page.getByLabel("Titel").fill("Kies event-driven facturatie"); await page.getByLabel("Samenvatting").fill("Ontkoppelt facturatie van het bestelproces."); await page.getByRole("button", { name: "Beslissing aanmaken" }).click(); await expect(page.getByRole("heading", { name: "Kies event-driven facturatie" })).toBeVisible(); await page.goBack(); await expect(page.getByRole("heading", { name: projectName })).toBeVisible(); // A history navigation can still be resolving the route's lazy chunk while the // heading assertion already passes against the outgoing render. Scanning at that // moment reports the transient empty document -- including "page must have a // level-one heading" -- rather than the workspace under test. Seen once as a // 35-violation failure between two clean runs. await page.waitForLoadState("networkidle"); const projectAccessibility = await new AxeBuilder({ page }).analyze(); expect(projectAccessibility.violations).toEqual([]); await page.screenshot({ path: path.join(screenshotDir, "10-project-workspace-populated.png"), fullPage: true }); // --- Runbook detail, validated (so it is not stuck in its initial stale-only // state), matching screen 13 (runbook detail). await page.goto("/runbooks"); await page.getByRole("button", { name: "Nieuw runbook", exact: true }).click(); await page.getByLabel("Titel").fill(`PostgreSQL failover ${stamp}`); await page.getByRole("button", { name: "Runbook aanmaken" }).click(); await expect(page.getByRole("heading", { name: `PostgreSQL failover ${stamp}` })).toBeVisible(); await page.getByRole("button", { name: "Valideren tegen huidige revisie" }).click(); await expect(page.getByText(/Laatst gevalideerd op/)).toBeVisible(); const runbookAccessibility = await new AxeBuilder({ page }).analyze(); expect(runbookAccessibility.violations).toEqual([]); await page.screenshot({ path: path.join(screenshotDir, "13-runbook-detail-validated.png"), fullPage: true }); // --- Service detail, accepted from a seeded proposal (same mechanism as // v2-infrastructure-review.spec.ts), matching screen 7 (service detail). const sourceKey = `docker:e2e-fidelity-${stamp}`; const detectedName = `facturatie-api-${stamp}`; const snapshotResponse = await postWithCsrf(page, "/api/v1/infrastructure/inventory/snapshots", { sourceKey: "inventory-e2e", sourceHash: `hash-fidelity-${stamp}`, capturedAt: new Date().toISOString(), hosts: [ { sourceKey: "host:e2e-fidelity", hostname: "E2E-Fidelity-Host", operatingSystem: "Linux", kernelVersion: null, architecture: "x86_64", logicalCpuCount: 8, totalMemoryBytes: 16_000_000_000 } ], services: [ { sourceKey, hostSourceKey: "host:e2e-fidelity", detectedName, runtimeKind: "container", runtimeStatus: "running", health: "Healthy", imageReference: "itworx/facturatie-api:2.3.0", imageDigest: null, runtimeUser: "app", readOnlyRootFilesystem: true, noNewPrivileges: true, privileged: false, confidence: "High", components: null, endpoints: null, networks: null, volumes: null, databases: null, dependencies: null, healthObservations: null, deployments: null, backups: null } ] }); expect(snapshotResponse.ok(), await snapshotResponse.text()).toBeTruthy(); await page.goto("/admin/inventory"); const proposalCard = page.locator(".inventory-proposals article", { hasText: detectedName }); await expect(proposalCard).toBeVisible(); await proposalCard.getByRole("button", { name: "Bevestigen" }).click(); await expect(proposalCard).toBeHidden(); await page.goto("/infrastructure/services"); await page.getByRole("link", { name: new RegExp(detectedName) }).click(); await expect(page.getByRole("heading", { name: detectedName })).toBeVisible(); const serviceAccessibility = await new AxeBuilder({ page }).analyze(); expect(serviceAccessibility.violations).toEqual([]); await page.screenshot({ path: path.join(screenshotDir, "07-service-detail-populated.png"), fullPage: true }); }); });