169 lines
9.1 KiB
TypeScript
169 lines
9.1 KiB
TypeScript
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)");
|
|
// <option> elements report as Playwright-"hidden" while their <select> is closed
|
|
// (same reason the pre-existing "Actief"-option-count assertion above uses
|
|
// toHaveCount rather than toBeVisible) — assert presence by count, not visibility.
|
|
await expect(supersedesField.getByRole("option", { name: /Oorspronkelijke keuze\./ })).toHaveCount(1);
|
|
// A brand-new project's decisions are numbered from 1 (ADR-014 decision 3: numbering
|
|
// is scoped per project), so the just-accepted original decision is always ADR-1.
|
|
await supersedesField.selectOption({ label: "ADR-1: Oorspronkelijke keuze." });
|
|
await page.getByRole("button", { name: "Beslissing aanmaken" }).click();
|
|
await expect(page.getByRole("heading", { name: replacementTitle })).toBeVisible();
|
|
|
|
const accessibility = await new AxeBuilder({ page }).analyze();
|
|
expect(accessibility.violations).toEqual([]);
|
|
|
|
await page.getByRole("button", { name: "Accepteren" }).click();
|
|
await expect(page.getByText("Geaccepteerd")).toBeVisible();
|
|
|
|
// The replacement decision itself shows the "vervangt" direction of the lineage.
|
|
const replacementLineageLink = page.getByRole("button", { name: /^Vervangt ADR-\d+: Oorspronkelijke keuze\./ });
|
|
await expect(replacementLineageLink).toBeVisible();
|
|
|
|
const lineageAccessibility = await new AxeBuilder({ page }).analyze();
|
|
expect(lineageAccessibility.violations).toEqual([]);
|
|
|
|
// Following the lineage link shows the original decision now flipped to
|
|
// Superseded, with the "vervangen door" direction of the same bidirectional link.
|
|
await replacementLineageLink.click();
|
|
await expect(page.getByRole("heading", { name: originalTitle })).toBeVisible();
|
|
await expect(page.getByText("Vervangen", { exact: true })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: /^Vervangen door ADR-\d+: Herziene keuze na evaluatie\./ })).toBeVisible();
|
|
// A Superseded decision offers no further lifecycle actions.
|
|
await expect(page.getByRole("button", { name: "Accepteren" })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "Intrekken" })).toHaveCount(0);
|
|
});
|
|
|
|
test("a runbook can be created, shows its stale-validation state honestly, and can be validated", async ({ page }) => {
|
|
const uniqueTitle = `E2E-runbook ${Date.now()}`;
|
|
|
|
await page.goto("/runbooks");
|
|
await expectAuthenticatedShell(page);
|
|
await expect(page.getByRole("heading", { name: "Runbooks", exact: true })).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: "Nieuw runbook", exact: true }).click();
|
|
await page.getByLabel("Titel").fill(uniqueTitle);
|
|
await page.getByRole("button", { name: "Runbook aanmaken" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: uniqueTitle })).toBeVisible();
|
|
await expect(page.getByText(/moet opnieuw worden gevalideerd/)).toBeVisible();
|
|
|
|
const runbookAccessibility = await new AxeBuilder({ page }).analyze();
|
|
expect(runbookAccessibility.violations).toEqual([]);
|
|
|
|
await page.getByRole("button", { name: "Valideren tegen huidige revisie" }).click();
|
|
await expect(page.getByText(/Laatst gevalideerd op/)).toBeVisible();
|
|
await expect(page.getByText(/moet opnieuw worden gevalideerd/)).not.toBeVisible();
|
|
|
|
// The platform never offers to execute the procedure — only to view/validate it
|
|
// (docs/16-runbooks-and-adrs.md: "The platform never runs its commands.").
|
|
await expect(page.getByRole("button", { name: /uitvoeren/i })).not.toBeVisible();
|
|
});
|
|
});
|