233 lines
10 KiB
TypeScript
233 lines
10 KiB
TypeScript
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<void> {
|
|
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");
|
|
});
|
|
});
|