import AxeBuilder from "@axe-core/playwright"; import { expect, test, type Page } from "@playwright/test"; import type { IngestionProposalDetail, IngestionProposalSummary, PaginatedIngestionProposals, } from "../../../src/frontend/src/api/intakeTypes"; import type { SessionResponse } from "../../../src/frontend/src/api/types"; import { authenticatedStatePath } from "../global-setup"; test.use({ storageState: authenticatedStatePath }); const proposalId = "8b8df97b-6015-4f7b-9bd0-9d9b2fb0e54a"; const correlationId = "e2e-v3-intake-pilot"; const proposalEndpoint = "/api/v1/ingestion-proposals"; function createProposalFixture(): IngestionProposalDetail { const now = new Date().toISOString(); return { id: proposalId, correlationId, status: "ReadyForReview", revisionNumber: 7, sourceCount: 1, submitterId: "91d38469-1c14-43c5-ae32-c806b4906bd0", createdAt: now, destination: { pageId: "e3f5b858-cd07-4e6e-9bb8-63e1e1712ae4", parentPageId: null, title: "Runbook bestandsherstel", }, metadata: { documentType: "Runbook", documentStatus: "Draft", projectId: null, risk: "medium", }, mutationKind: "UpdatePage", manifest: "# Bestandsherstel\n\nControleer eerst de meest recente back-up.", analysis: "De wijziging voegt herstelstappen toe en verwijst naar een controleerbare back-up.", reviewerComment: "Controleer de herstelvolgorde en de genoemde revisie.", retryCount: 0, maxPublishRetries: 3, isRetryEligible: false, failedLifecycleStatus: null, failedProcessingStage: null, failureErrorType: null, failureMessage: null, sources: [{ id: "6dce39d5-d022-48b6-90d6-6f1ea8ad0c93", fileName: "herstelstappen.md", mimeType: "text/markdown", byteSize: 512, digestSha256: "a".repeat(64), scanResult: "accepted", attachmentReferenceId: "08ab9571-5fb5-4a77-b151-8bdc4ef302cc", }], revisions: [], etag: "proposal-revision-7", publishedPageId: null, publishedPageRevisionNumber: null, attachments: [], expectedTargetETag: "page-revision-12", citations: [{ label: "Back-upcontrole", pageId: "e3f5b858-cd07-4e6e-9bb8-63e1e1712ae4", excerpt: "Vergelijk de hash met de vorige volledige back-up.", confidence: 0.94, }], warnings: [{ code: "manual-review", message: "Handmatige beoordeling vereist voor deze wijziging.", severity: "medium", }], duplicateCandidates: [], conflictCandidates: [{ pageId: "e3f5b858-cd07-4e6e-9bb8-63e1e1712ae4", title: "Runbook bestandsherstel", kind: "target-page", }], diff: { before: "De huidige proceduretekst.", after: "De voorgestelde herstelprocedure met hashcontrole.", summary: "Voegt controle van de herstelhash toe.", }, permissions: { canEdit: true, canDecide: true, canPublish: false }, }; } async function installProposalFixture(page: Page): Promise { let currentDetail = createProposalFixture(); const toSummary = (detail: IngestionProposalDetail): IngestionProposalSummary => ({ id: detail.id, correlationId: detail.correlationId, status: detail.status, revisionNumber: detail.revisionNumber, sourceCount: detail.sourceCount, submitterId: detail.submitterId ?? "91d38469-1c14-43c5-ae32-c806b4906bd0", createdAt: detail.createdAt ?? new Date().toISOString(), destination: detail.destination ?? null, metadata: detail.metadata, mutationKind: detail.mutationKind, }); const queue: PaginatedIngestionProposals = { items: [toSummary(currentDetail)], nextCursor: null, page: 1, pageSize: 50, total: 1, correlationId, }; // Keep the pilot flag disabled by default in the running service; this synthetic // UI contract explicitly opts the authenticated fixture session into the route. await page.route("**/api/v1/auth/session", async (route) => { const response = await route.fetch(); if (!response.ok()) { await route.fulfill({ response }); return; } const session = await response.json() as SessionResponse; await route.fulfill({ response, json: { ...session, features: { ...session.features, ingestionProposals: true } }, }); }); await page.route(/\/api\/v1\/ingestion-proposals(?:\/|\?|$)/, async (route) => { const request = route.request(); const pathname = new URL(request.url()).pathname; if (request.method() === "GET" && pathname === proposalEndpoint) { await route.fulfill({ status: 200, json: { ...queue, items: [toSummary(currentDetail)] } }); return; } if (request.method() === "GET" && pathname === `${proposalEndpoint}/${proposalId}`) { await route.fulfill({ status: 200, json: currentDetail }); return; } if (request.method() === "POST" && pathname === `${proposalEndpoint}/${proposalId}/approve`) { currentDetail = { ...currentDetail, status: "Approved", revisionNumber: currentDetail.revisionNumber + 1, etag: "proposal-revision-8", }; await route.fulfill({ status: 200, json: currentDetail }); return; } await route.continue(); }); } test.describe("V3 intakepilot: beoordeling op desktop en mobiel", () => { test("reviewer kan waarschuwingen, verschil, bestemming en beslissing toetsen met toetsenbord", async ({ page }, testInfo) => { const isMobile = testInfo.project.name === "chromium-mobile"; await page.setViewportSize(isMobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 }); await installProposalFixture(page); await page.goto("/"); await expect(page.getByRole("heading", { name: "Operationeel overzicht" })).toBeVisible(); await expect(page.getByRole("region", { name: "Kennisintake" })).toHaveCount(0); if (isMobile) await page.getByRole("button", { name: "Navigatie openen" }).click(); const intakeNavigationLink = page.getByRole("navigation", { name: "Hoofdnavigatie" }).getByRole("link", { name: "Intake", exact: true }); await expect(intakeNavigationLink).toHaveAttribute("href", "/knowledge/intake"); await intakeNavigationLink.click(); await expect(page.getByRole("heading", { name: "Intakewachtrij" })).toBeVisible(); const proposalLink = page.getByRole("link", { name: "Open voorstel Runbook bestandsherstel" }); await proposalLink.focus(); await page.keyboard.press("Enter"); await expect(page.getByLabel("Voorsteloverzicht").getByRole("heading", { name: "Voorstel beoordelen" })).toBeVisible(); await expect(page.locator('[data-status="ReadyForReview"]')).toContainText("Klaar voor beoordeling"); await expect(page.getByRole("heading", { name: "Bronnen en herkomst" })).toBeVisible(); if (isMobile) { await expect(page.locator('[data-testid="ingestion-progress"]')).toHaveText("Stap 1 van 3: Bronnen"); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); await page.getByRole("button", { name: "Volgende stap" }).press("Enter"); await expect(page.locator('[data-testid="ingestion-progress"]')).toHaveText("Stap 2 van 3: Onderbouwing"); await expect(page.locator('[data-mobile-step="sources"]')).toBeHidden(); await expect(page.locator('[data-mobile-step="evidence"]')).toBeVisible(); await expect(page.getByRole("heading", { name: "Waarschuwingen en conflicten" })).toBeVisible(); await expect(page.getByText("Handmatige beoordeling vereist voor deze wijziging.")).toBeVisible(); await expect(page.getByText("Tegenstrijdige pagina: Runbook bestandsherstel")).toBeVisible(); await expect(page.getByRole("heading", { name: "Voorgestelde inhoud" })).toBeVisible(); await expect(page.getByText("De huidige proceduretekst.", { exact: true })).toBeVisible(); await expect(page.getByText("De voorgestelde herstelprocedure met hashcontrole.", { exact: true })).toBeVisible(); await expect(page.getByRole("heading", { name: "Bestemming en beoordeling" })).toBeVisible(); await expect(page.getByText("Runbook bestandsherstel", { exact: true })).toBeVisible(); await expect(page.getByText("Verwachte paginaversie: page-revision-12")).toBeVisible(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); const evidenceAccessibility = await new AxeBuilder({ page }).analyze(); expect(evidenceAccessibility.violations).toEqual([]); // The mobile step moves focus to the active pane; normal Tab order then enters // the reviewer controls after the read-only warning and diff evidence. await page.keyboard.press("Tab"); await expect(page.getByRole("textbox", { name: "Voorstel (beoordelaar)" })).toBeFocused(); await page.keyboard.press("Tab"); await expect(page.getByRole("textbox", { name: "Onderbouwing (beoordelaar)" })).toBeFocused(); await page.getByRole("button", { name: "Volgende stap" }).press("Enter"); await expect(page.locator('[data-testid="ingestion-progress"]')).toHaveText("Stap 3 van 3: Beslissing"); await expect(page.locator('[data-mobile-step="evidence"]')).toBeHidden(); await expect(page.locator('[data-mobile-step="decision"]')).toBeVisible(); await expect(page.getByRole("button", { name: "Goedkeuren" })).toBeVisible(); const decisionAccessibility = await new AxeBuilder({ page }).analyze(); expect(decisionAccessibility.violations).toEqual([]); } else { await expect(page.getByRole("heading", { name: "Waarschuwingen en conflicten" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Voorgestelde inhoud" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Bestemming en beoordeling" })).toBeVisible(); } expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); const accessibility = await new AxeBuilder({ page }).analyze(); expect(accessibility.violations).toEqual([]); await page.getByRole("button", { name: "Goedkeuren" }).focus(); await page.keyboard.press("Enter"); await expect(page.locator('[data-status="Approved"]')).toContainText("Goedgekeurd"); }); });