Files
itworx-knowledge-hub-Public/tests/e2e/specs/project-hub.spec.ts
T

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();
});
});