import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; import { authenticatedStatePath } from "../global-setup"; import { expectAuthenticatedShell } from "./support/v2"; test.use({ storageState: authenticatedStatePath }); test.describe("V2 workspaces: projects, architecture decisions and runbooks", () => { test("a project can be created, its status changed, and an update added to its timeline", async ({ page }) => { const uniqueName = `E2E-project ${Date.now()}`; await page.goto("/"); await expectAuthenticatedShell(page); await page.goto("/projects"); await expect(page.getByRole("heading", { name: "Projecten", exact: true })).toBeVisible(); const accessibility = await new AxeBuilder({ page }).analyze(); expect(accessibility.violations).toEqual([]); await page.getByRole("button", { name: "Nieuw project" }).click(); await page.getByLabel("Naam").fill(uniqueName); await page.getByLabel("Samenvatting").fill("E2E-testproject."); await page.getByRole("button", { name: "Project aanmaken" }).click(); await expect(page.getByRole("heading", { name: uniqueName })).toBeVisible(); await expect(page.getByText("Concept", { exact: true }).first()).toBeVisible(); const detailAccessibility = await new AxeBuilder({ page }).analyze(); expect(detailAccessibility.violations).toEqual([]); // Only documented lifecycle transitions are selectable from the V2 workspace. const status = page.getByLabel("Projectstatus wijzigen"); await expect(status.getByRole("option", { name: "Actief" })).toHaveCount(0); await status.selectOption("Planned"); await expect(page.getByText("Gepland", { exact: true }).first()).toBeVisible(); await page.getByRole("button", { name: "Update", exact: true }).click(); await page.getByLabel("Titel").fill("Eerste voortgangsupdate"); await page.getByRole("button", { name: "Update toevoegen" }).click(); await expect(page.getByText("Eerste voortgangsupdate")).toBeVisible(); }); test("an ADR can be proposed and then accepted", async ({ page }) => { const title = `E2E-ADR ${Date.now()}`; const projectName = `E2E-ADR-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 page.getByRole("button", { name: "Beslissing", exact: true }).click(); await page.getByLabel("Titel").fill(title); await page.getByLabel("Samenvatting").fill("Testbeslissing."); await page.getByRole("button", { name: "Beslissing aanmaken" }).click(); await expect(page.getByRole("heading", { name: title })).toBeVisible(); await expect(page.getByText("Voorgesteld")).toBeVisible(); const decisionAccessibility = await new AxeBuilder({ page }).analyze(); expect(decisionAccessibility.violations).toEqual([]); await page.getByRole("button", { name: "Accepteren" }).click(); await expect(page.getByText("Geaccepteerd")).toBeVisible(); }); // V2-M5: the "Nieuwe beslissing" form now exposes a "Vervangt beslissing" field // (previously API-only, see CURRENT_STATE.md and ADR-014 decision 12). This proves // the full supersession lineage end to end via the UI alone: propose+accept an // original decision, propose+accept a second decision that supersedes it, and // confirm both directions of the bidirectional lineage link render correctly — // matching DecisionTests.Accepting_a_decision_that_supersedes_an_accepted_one_flips_ // the_original_to_superseded_with_bidirectional_links at the integration level. test("a decision can supersede an already-accepted one, and the bidirectional lineage is reachable from the UI", async ({ page }) => { const projectName = `E2E-supersede-project ${Date.now()}`; const originalTitle = `E2E-ADR-original ${Date.now()}`; const replacementTitle = `E2E-ADR-replacement ${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(); // Original decision: propose then accept. await page.getByRole("button", { name: "Beslissing", exact: true }).click(); await page.getByLabel("Titel").fill(originalTitle); await page.getByLabel("Samenvatting").fill("Oorspronkelijke keuze."); // A freshly created project has no Accepted decisions yet, so the only option is // "Geen". await expect(page.getByLabel("Vervangt beslissing (optioneel)")).toHaveValue(""); await page.getByRole("button", { name: "Beslissing aanmaken" }).click(); await expect(page.getByRole("heading", { name: originalTitle })).toBeVisible(); await page.getByRole("button", { name: "Accepteren" }).click(); await expect(page.getByText("Geaccepteerd")).toBeVisible(); await page.getByRole("button", { name: "← Terug naar beslissingen" }).click(); await expect(page.getByRole("heading", { name: projectName })).toBeVisible(); // Replacement decision: now the original, Accepted decision is a selectable // supersession target. await page.getByRole("button", { name: "Beslissing", exact: true }).click(); await page.getByLabel("Titel").fill(replacementTitle); await page.getByLabel("Samenvatting").fill("Herziene keuze na evaluatie."); const supersedesField = page.getByLabel("Vervangt beslissing (optioneel)"); //