Publish curated Knowledge Hub source from 2ed833f290b439a7bc91e5fd19b042c269e3bee9
This commit is contained in:
1 parent
10f2edcd29
commit
59f15b2d08
218 files changed
+23995
-1849
No files matched your search
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:headed": "playwright test --headed",
|
||||
"install:browsers": "playwright install --with-deps chromium"
|
||||
"install:browsers": "playwright install --with-deps chromium firefox webkit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.10.2",
|
||||
|
||||
@@ -27,6 +27,18 @@ export default defineConfig({
|
||||
{
|
||||
name: "chromium-mobile",
|
||||
use: { ...devices["Pixel 7"] }
|
||||
},
|
||||
{
|
||||
name: "firefox-desktop",
|
||||
// Keep the broad historical suite on its established projects. This
|
||||
// cross-browser smoke covers the new platform workflows on each release.
|
||||
testMatch: "**/platform-productization.spec.ts",
|
||||
use: { ...devices["Desktop Firefox"] }
|
||||
},
|
||||
{
|
||||
name: "webkit-mobile",
|
||||
testMatch: "**/platform-productization.spec.ts",
|
||||
use: { ...devices["iPhone 13"] }
|
||||
}
|
||||
],
|
||||
expect: {
|
||||
|
||||
@@ -43,18 +43,26 @@ test.describe("discovery: search, revisions, backlinks and tags", () => {
|
||||
// Scoped to the search results list specifically: on a desktop viewport the
|
||||
// permanent sidebar tree also shows a same-named entry once the page exists, so an
|
||||
// unscoped locator would be ambiguous.
|
||||
const searchResults = page.getByRole("list", { name: "Zoekresultaten" });
|
||||
const searchResults = page.getByRole("region", { name: "Zoekresultaten" });
|
||||
|
||||
await page.getByLabel("Zoekterm").fill("zoekbaarheidsproef");
|
||||
await page.getByRole("button", { name: "Zoekopdracht uitvoeren" }).click();
|
||||
await expect(searchResults.getByRole("button", { name: new RegExp(uniqueTitle) })).toBeVisible();
|
||||
await expect(searchResults.getByRole("link", { name: new RegExp(uniqueTitle) })).toBeVisible();
|
||||
|
||||
// Tag filter narrows results to just the tagged page.
|
||||
await page.getByLabel("Label").fill(tag);
|
||||
await page.getByRole("button", { name: "Zoekopdracht uitvoeren" }).click();
|
||||
await expect(searchResults.getByRole("button", { name: new RegExp(uniqueTitle) })).toBeVisible();
|
||||
await expect(searchResults.getByRole("link", { name: new RegExp(uniqueTitle) })).toBeVisible();
|
||||
|
||||
await searchResults.getByRole("button", { name: new RegExp(uniqueTitle) }).click();
|
||||
await page.getByLabel("Label", { exact: true }).fill(`${tag}-absent`);
|
||||
await page.getByRole("button", { name: "Zoekopdracht uitvoeren" }).click();
|
||||
await expect(searchResults.getByText("Geen resultaten voor deze zoekterm.")).toBeVisible();
|
||||
await page.getByLabel("Zoekterm").fill("");
|
||||
await page.getByLabel("Label", { exact: true }).fill(tag);
|
||||
await page.getByRole("button", { name: "Zoekopdracht uitvoeren" }).click();
|
||||
await expect(searchResults.getByRole("link", { name: new RegExp(uniqueTitle) })).toBeVisible();
|
||||
|
||||
await searchResults.getByRole("link", { name: new RegExp(uniqueTitle) }).click();
|
||||
await expect(page.getByRole("heading", { name: uniqueTitle })).toBeVisible();
|
||||
|
||||
// Scoped to the backlinks panel: on a desktop viewport the permanent sidebar tree
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { authenticatedStatePath } from "../global-setup";
|
||||
@@ -5,17 +6,17 @@ import { authenticatedStatePath } from "../global-setup";
|
||||
test.use({ storageState: authenticatedStatePath });
|
||||
|
||||
async function createProposal(page: Page): Promise<string> {
|
||||
return page.evaluate(async () => {
|
||||
return page.evaluate(async ({ idempotencyKey, correlationId }) => {
|
||||
const csrfResponse = await fetch("/api/v1/auth/csrf");
|
||||
const csrf = await csrfResponse.json() as { token: string; headerName: string };
|
||||
const response = await fetch("/api/v1/ingestion-proposals", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", [csrf.headerName]: csrf.token, "Idempotency-Key": crypto.randomUUID() },
|
||||
body: JSON.stringify({ manifest: "# E2E mobiele beoordeling", correlationId: `e2e-review-${crypto.randomUUID()}` }),
|
||||
headers: { "content-type": "application/json", [csrf.headerName]: csrf.token, "Idempotency-Key": idempotencyKey },
|
||||
body: JSON.stringify({ manifest: "# E2E mobiele beoordeling", correlationId }),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Voorstel maken mislukte (${response.status}): ${await response.text()}`);
|
||||
return (await response.json() as { id: string }).id;
|
||||
});
|
||||
}, { idempotencyKey: randomUUID(), correlationId: `e2e-review-${randomUUID()}` });
|
||||
}
|
||||
|
||||
test.describe("intakebeoordeling: responsieve, toegankelijke werkruimte", () => {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { authenticatedStatePath } from "../global-setup";
|
||||
import { expectAuthenticatedShell } from "./support/v2";
|
||||
|
||||
test.describe("platform workflows at supported widths", () => {
|
||||
test.use({ storageState: authenticatedStatePath });
|
||||
|
||||
for (const width of [320, 360, 390, 768, 1280]) {
|
||||
test(`search and administration reflow without page overflow at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 850 });
|
||||
for (const [path, heading] of [
|
||||
["/search?q=knowledge", "Zoeken"],
|
||||
["/admin/jobs", "Achtergrondtaken"],
|
||||
["/admin/audit", "Beheeraudit"],
|
||||
["/admin/platform", "Platforminstellingen"]
|
||||
] as const) {
|
||||
await page.goto(path);
|
||||
await expect(page.getByRole("heading", { name: heading, level: 1 })).toBeVisible();
|
||||
await expectAuthenticatedShell(page);
|
||||
const overflow = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth - document.documentElement.clientWidth
|
||||
);
|
||||
expect(overflow, `${path} overflowed at ${width}px`).toBeLessThanOrEqual(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("keyboard search and admin navigation remain accessible", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await page.goto("/search?q=knowledge");
|
||||
await expect(page.getByRole("heading", { name: "Zoeken", level: 1 })).toBeVisible();
|
||||
expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]);
|
||||
|
||||
await page.getByRole("button", { name: "Opdrachten en zoeken openen" }).click();
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
await page.keyboard.press("Tab");
|
||||
expect(await page.evaluate(() => {
|
||||
const dialog = document.querySelector("[role=dialog]");
|
||||
return dialog?.contains(document.activeElement) ?? false;
|
||||
})).toBeTruthy();
|
||||
const reducedDuration = await page.getByRole("dialog").evaluate(
|
||||
(dialog) => parseFloat(getComputedStyle(dialog).transitionDuration)
|
||||
);
|
||||
expect(reducedDuration).toBeLessThanOrEqual(0.01);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByRole("dialog")).toBeHidden();
|
||||
|
||||
await page.goto("/admin/jobs");
|
||||
await expect(page.getByRole("heading", { name: "Achtergrondtaken", level: 1 })).toBeVisible();
|
||||
expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]);
|
||||
});
|
||||
|
||||
test("narrow mobile controls stay touchable with a short virtual-keyboard viewport", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 320, height: 430 });
|
||||
await page.goto("/admin/jobs");
|
||||
await expect(page.getByRole("heading", { name: "Achtergrondtaken", level: 1 })).toBeVisible();
|
||||
for (const name of ["Navigatie openen", "Opdrachten en zoeken openen", "Accountmenu"]) {
|
||||
const box = await page.getByRole("button", { name }).boundingBox();
|
||||
// Browser layout rounds CSS's 44px touch target to fractional device pixels.
|
||||
expect(box?.width ?? 0, `${name} width`).toBeGreaterThanOrEqual(43.9);
|
||||
expect(box?.height ?? 0, `${name} height`).toBeGreaterThanOrEqual(43.9);
|
||||
}
|
||||
await page.getByRole("button", { name: "Navigatie openen" }).click();
|
||||
await expect(page.getByRole("navigation", { name: "Hoofdnavigatie" })).toBeVisible();
|
||||
expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]);
|
||||
});
|
||||
|
||||
test("server preferences survive a reload and can be restored", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
await expect(page.getByRole("heading", { name: "Instellingen", level: 1 })).toBeVisible();
|
||||
await expect(page.locator(".preference-sync-status")).toContainText("Voorkeuren gesynchroniseerd.");
|
||||
await page.getByRole("button", { name: "Donker" }).click();
|
||||
await expect.poll(async () => {
|
||||
const response = await page.request.get("/api/v1/me/preferences");
|
||||
return (await response.json() as { theme: string }).theme;
|
||||
}).toBe("dark");
|
||||
await page.reload();
|
||||
await expect(page.getByRole("button", { name: "Donker" })).toHaveClass(/selected/);
|
||||
await page.getByRole("button", { name: "Systeem" }).click();
|
||||
await expect.poll(async () => {
|
||||
const response = await page.request.get("/api/v1/me/preferences");
|
||||
return (await response.json() as { theme: string }).theme;
|
||||
}).toBe("system");
|
||||
});
|
||||
|
||||
test("administrator can register an external source and record an observation", async ({ page }) => {
|
||||
const sourceKey = `e2e-source-${crypto.randomUUID()}`;
|
||||
const displayName = `E2E bron ${sourceKey.slice(-8)}`;
|
||||
await page.goto("/admin/integrations");
|
||||
await expect(page.getByRole("heading", { name: "Externe gegevensbronnen", level: 1 })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Bron toevoegen" }).click();
|
||||
await page.getByRole("textbox", { name: "Unieke broncode" }).fill(sourceKey);
|
||||
await page.getByRole("textbox", { name: "Weergavenaam" }).fill(displayName);
|
||||
await page.getByRole("button", { name: "Bron registreren" }).click();
|
||||
await expect(page.locator(".form-success")).toContainText("Externe gegevensbron geregistreerd.");
|
||||
const row = page.locator(".data-source-row", { hasText: sourceKey });
|
||||
await expect(row).toBeVisible();
|
||||
await row.getByText("Operatorwaarneming registreren").click();
|
||||
await row.getByRole("spinbutton", { name: "Waargenomen recordaantal" }).fill("42");
|
||||
await row.getByRole("button", { name: "Waarneming opslaan" }).click();
|
||||
await expect(page.locator(".form-success")).toContainText(`Operatorwaarneming voor ${displayName} geregistreerd.`);
|
||||
await page.reload();
|
||||
await expect(page.locator(".data-source-row", { hasText: sourceKey })).toContainText("42 records");
|
||||
});
|
||||
|
||||
test("administrator sees durable backup job progress after queuing", async ({ page }) => {
|
||||
await page.goto("/admin/jobs");
|
||||
await expect(page.getByRole("heading", { name: "Achtergrondtaken", level: 1 })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Back-uptaak inplannen" }).click();
|
||||
await expect(page.locator(".form-success")).toContainText("Back-uptaak is ingepland.");
|
||||
await expect(page).toHaveURL(/jobId=/);
|
||||
await expect(page.getByRole("heading", { name: /Back-up · / })).toBeVisible();
|
||||
await expect(page.locator(".admin-job-detail").getByRole("progressbar", { name: /Back-up/ })).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(page.getByRole("heading", { name: "Back-up · Geslaagd" })).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
});
|
||||
@@ -66,8 +66,18 @@ test.describe("V2-M3 knowledge editor: LeafWiki-floor proofs", () => {
|
||||
|
||||
const editor = markdownEditor(page);
|
||||
await editor.click();
|
||||
const savedSeparator = page.waitForResponse((response) => {
|
||||
const request = response.request();
|
||||
return request.method() === "PUT"
|
||||
&& request.url().includes("/api/v1/drafts/")
|
||||
&& (request.postDataJSON() as { markdown?: string }).markdown === "Voor de afbeelding: ";
|
||||
});
|
||||
await page.keyboard.type("Voor de afbeelding: ");
|
||||
|
||||
const draftResponse = await savedSeparator;
|
||||
expect(draftResponse.status()).toBe(200);
|
||||
await expect.poll(() => readMarkdown(page)).toBe("Voor de afbeelding: ");
|
||||
|
||||
const accessibility = await new AxeBuilder({ page }).analyze();
|
||||
expect(accessibility.violations).toEqual([]);
|
||||
|
||||
@@ -86,7 +96,7 @@ test.describe("V2-M3 knowledge editor: LeafWiki-floor proofs", () => {
|
||||
// placeholder "pending-attachment:" token exists anymore, so the same reference
|
||||
// inserted at the cursor is already correct even before publish.
|
||||
await expect.poll(() => readMarkdown(page)).toContain("toolbar-inline.png");
|
||||
expect(await readMarkdown(page)).toMatch(/Voor de afbeelding: !?\[toolbar-inline\.png\]\(\/api\/v1\/attachments\//);
|
||||
expect(await readMarkdown(page)).toMatch(/^Voor de afbeelding: !?\[toolbar-inline\.png\]\(\/api\/v1\/attachments\/[^)]+\)$/);
|
||||
await expect(page.getByText("1 bijlage klaar — nog niet gepubliceerd")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Publiceren", exact: true }).click();
|
||||
@@ -189,13 +199,13 @@ test.describe("V2-M3 knowledge editor: LeafWiki-floor proofs", () => {
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
await page.getByLabel("Zoeken en opdrachten").fill(uniqueTitle.slice(0, 22));
|
||||
|
||||
const knowledgeGroup = page.getByRole("group", { name: "Kennisbank" });
|
||||
await expect(knowledgeGroup.getByRole("option", { name: new RegExp(uniqueTitle) })).toBeVisible();
|
||||
const searchResultsGroup = page.getByRole("group", { name: "Zoekresultaten" });
|
||||
await expect(searchResultsGroup.getByRole("option", { name: new RegExp(uniqueTitle) })).toBeVisible();
|
||||
|
||||
const accessibility = await new AxeBuilder({ page }).analyze();
|
||||
expect(accessibility.violations).toEqual([]);
|
||||
|
||||
await knowledgeGroup.getByRole("option", { name: new RegExp(uniqueTitle) }).click();
|
||||
await searchResultsGroup.getByRole("option", { name: new RegExp(uniqueTitle) }).click();
|
||||
await expect(page.getByRole("heading", { name: uniqueTitle })).toBeVisible();
|
||||
await expect(page.getByRole("dialog")).toBeHidden();
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ test.describe("V2-M4 infrastructure review, editorial ownership and overrides",
|
||||
const detectedName = `e2e-accept-service-${Date.now()}`;
|
||||
await seedProposedService(page, sourceKey, detectedName);
|
||||
|
||||
await page.goto("/admin");
|
||||
await page.goto("/admin/inventory");
|
||||
await expectAuthenticatedShell(page);
|
||||
await expect(page.getByRole("heading", { name: "Wijzigingen sinds snapshots" })).toBeVisible();
|
||||
|
||||
@@ -141,7 +141,7 @@ test.describe("V2-M4 infrastructure review, editorial ownership and overrides",
|
||||
const detectedName = `e2e-reject-service-${Date.now()}`;
|
||||
await seedProposedService(page, sourceKey, detectedName);
|
||||
|
||||
await page.goto("/admin");
|
||||
await page.goto("/admin/inventory");
|
||||
await expectAuthenticatedShell(page);
|
||||
const proposalCard = page.locator(".inventory-proposals article", { hasText: detectedName });
|
||||
await expect(proposalCard).toBeVisible();
|
||||
|
||||
@@ -117,7 +117,7 @@ test.describe("V2-M5 workspace fidelity spot-check", () => {
|
||||
});
|
||||
expect(snapshotResponse.ok(), await snapshotResponse.text()).toBeTruthy();
|
||||
|
||||
await page.goto("/admin");
|
||||
await page.goto("/admin/inventory");
|
||||
const proposalCard = page.locator(".inventory-proposals article", { hasText: detectedName });
|
||||
await expect(proposalCard).toBeVisible();
|
||||
await proposalCard.getByRole("button", { name: "Bevestigen" }).click();
|
||||
|
||||
@@ -90,6 +90,12 @@ test.describe("V2-M6 motion, matrices and dashboard personalization", () => {
|
||||
await page.goto("/settings");
|
||||
await expectAuthenticatedShell(page);
|
||||
await expect(page.getByRole("heading", { name: "Dashboardwidgets" })).toBeVisible();
|
||||
await expect(page.locator(".preference-sync-status")).toContainText("Voorkeuren gesynchroniseerd.");
|
||||
await page.getByRole("button", { name: "Widgets herstellen" }).click();
|
||||
await expect.poll(async () => {
|
||||
const values = await (await page.request.get("/api/v1/me/preferences")).json();
|
||||
return [values.dashboardWidgetOrder[0], values.hiddenDashboardWidgets.length];
|
||||
}).toEqual(["attention", 0]);
|
||||
|
||||
// Hide the "Projecten" widget entirely.
|
||||
await page.getByRole("checkbox", { name: "Projecten tonen" }).uncheck();
|
||||
@@ -100,6 +106,13 @@ test.describe("V2-M6 motion, matrices and dashboard personalization", () => {
|
||||
await expect(page.getByRole("button", { name: "Operationele signalen omhoog" })).toBeDisabled();
|
||||
await page.getByRole("button", { name: "Unraid-overzicht omhoog" }).click();
|
||||
|
||||
// A full page navigation must wait for the server save, as indicated by the
|
||||
// settings UI, instead of interrupting the pending preference request.
|
||||
await expect.poll(async () => {
|
||||
const values = await (await page.request.get("/api/v1/me/preferences")).json();
|
||||
return [values.dashboardWidgetOrder[0], values.hiddenDashboardWidgets.includes("projects")];
|
||||
}).toEqual(["server", true]);
|
||||
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { name: "Operationeel overzicht" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Projecten", exact: true })).toHaveCount(0);
|
||||
@@ -120,7 +133,11 @@ test.describe("V2-M6 motion, matrices and dashboard personalization", () => {
|
||||
// Restore the default so this test does not leak state into any other test that
|
||||
// shares the same authenticated storage state / owner account.
|
||||
await page.goto("/settings");
|
||||
await page.getByRole("checkbox", { name: "Projecten tonen" }).check();
|
||||
await page.getByRole("button", { name: "Unraid-overzicht omlaag" }).click();
|
||||
await expect(page.locator(".preference-sync-status")).toContainText("Voorkeuren gesynchroniseerd.");
|
||||
await page.getByRole("button", { name: "Widgets herstellen" }).click();
|
||||
await expect.poll(async () => {
|
||||
const values = await (await page.request.get("/api/v1/me/preferences")).json();
|
||||
return [values.dashboardWidgetOrder[0], values.hiddenDashboardWidgets.length];
|
||||
}).toEqual(["attention", 0]);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { authenticatedStatePath } from "../global-setup";
|
||||
@@ -17,35 +19,52 @@ import { expectAuthenticatedShell } from "./support/v2";
|
||||
|
||||
test.use({ storageState: authenticatedStatePath });
|
||||
|
||||
// Keep the ordinary local-stack name as the zero-configuration default, while
|
||||
// allowing clean-room/release verification to target its isolated app container.
|
||||
// Without this override the fixture is written into a different healthy stack and
|
||||
// the preflight correctly rejects the path as absent from its own imports mount.
|
||||
const CONTAINER_NAME =
|
||||
process.env.KNOWLEDGE_HUB_E2E_APP_CONTAINER ?? "itworx-knowledge-hub-app-1";
|
||||
const IMPORTS_ROOT = "/var/lib/knowledge-hub/imports";
|
||||
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const expectedImportsRoot = path.resolve(testDirectory, "../../../.local-appdata/e2e-imports");
|
||||
const seededFixtureDirectories = new Set<string>();
|
||||
|
||||
function seedLeafWikiFixture(relativeDir: string, title: string) {
|
||||
// Writes a minimal, synthetic Markdown fixture directly into the running app
|
||||
// container's imports volume (never a real LeafWiki source — CLAUDE.md's
|
||||
// read-only-source rule only ever applies to a real snapshot, which this is not).
|
||||
// Uses `docker exec` from the test runner rather than a Playwright API call, since
|
||||
// the import endpoint deliberately only ever reads a server-side mounted path, by
|
||||
// design (docs/17-leafwiki-migration.md) — there is no upload endpoint to seed it
|
||||
// through.
|
||||
execFileSync("docker", [
|
||||
"exec",
|
||||
CONTAINER_NAME,
|
||||
"sh",
|
||||
"-c",
|
||||
`mkdir -p '${IMPORTS_ROOT}/${relativeDir}' && printf '# ${title}\\n\\nDit is een synthetische testpagina voor de V2-M7 importproef.\\n' > '${IMPORTS_ROOT}/${relativeDir}/welkom.md'`
|
||||
]);
|
||||
// The import API reads its configured server-side fixture mount. Fail closed unless
|
||||
// both the API and this test are explicitly pointed at this repository-local test
|
||||
// directory; never discover or fall back to a Docker container, which could be a
|
||||
// production deployment.
|
||||
const configuredImportsRoot = process.env.KNOWLEDGE_HUB_E2E_IMPORTS_ROOT;
|
||||
if (!configuredImportsRoot || path.resolve(configuredImportsRoot) !== expectedImportsRoot) {
|
||||
throw new Error(
|
||||
"Set KNOWLEDGE_HUB_E2E_IMPORTS_ROOT to this checkout's absolute .local-appdata/e2e-imports path before running the LeafWiki import browser test."
|
||||
);
|
||||
}
|
||||
|
||||
const fixtureDirectory = path.resolve(expectedImportsRoot, relativeDir);
|
||||
if (!fixtureDirectory.startsWith(expectedImportsRoot + path.sep)) {
|
||||
throw new Error("The synthetic LeafWiki fixture must stay inside .local-appdata/e2e-imports.");
|
||||
}
|
||||
|
||||
mkdirSync(fixtureDirectory, { recursive: true });
|
||||
seededFixtureDirectories.add(fixtureDirectory);
|
||||
writeFileSync(
|
||||
path.join(fixtureDirectory, "welkom.md"),
|
||||
"# " + title + "\n\nDit is een synthetische testpagina voor de V2-M7 importproef.\n",
|
||||
{ encoding: "utf8", flag: "wx" }
|
||||
);
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
for (const fixtureDirectory of seededFixtureDirectories) {
|
||||
if (
|
||||
path.dirname(fixtureDirectory) === expectedImportsRoot
|
||||
&& path.basename(fixtureDirectory).startsWith("e2e-leafwiki-")
|
||||
) {
|
||||
rmSync(fixtureDirectory, { recursive: true, force: true });
|
||||
}
|
||||
seededFixtureDirectories.delete(fixtureDirectory);
|
||||
}
|
||||
});
|
||||
|
||||
test.describe("V2-M7 admin actions: recovery tokens, trash purge, LeafWiki import", () => {
|
||||
test("an administrator can issue a one-time recovery token for another user from the admin UI", async ({ page }) => {
|
||||
const targetEmail = `e2e-recovery-target-${Date.now()}@itworx.e2e`;
|
||||
await page.goto("/admin");
|
||||
await page.goto("/admin/users");
|
||||
await expectAuthenticatedShell(page);
|
||||
await expect(page.getByRole("heading", { name: "Gebruikers", exact: true })).toBeVisible();
|
||||
|
||||
@@ -72,12 +91,19 @@ test.describe("V2-M7 admin actions: recovery tokens, trash purge, LeafWiki impor
|
||||
});
|
||||
|
||||
test("an administrator can trigger a trash purge from the admin UI, and it honestly reports nothing eligible yet", async ({ page }) => {
|
||||
await page.goto("/admin");
|
||||
await page.goto("/admin/backups");
|
||||
await expectAuthenticatedShell(page);
|
||||
await expect(page.getByRole("heading", { name: "Prullenbak" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Prullenbak legen" }).click();
|
||||
|
||||
const purgeConfirmation = page.getByRole("group", { name: "Definitieve verwijdering bevestigen" });
|
||||
await expect(purgeConfirmation).toBeVisible();
|
||||
await expect(
|
||||
purgeConfirmation.getByText("Dit verwijdert definitief alleen pagina's waarvan de bewaartermijn is verstreken. Deze actie kan niet ongedaan worden gemaakt.")
|
||||
).toBeVisible();
|
||||
await purgeConfirmation.getByRole("button", { name: "Bevestig definitieve verwijdering" }).click();
|
||||
|
||||
// PurgeTrashUseCase only removes pages whose retention window has already
|
||||
// elapsed — a fresh test run has nothing eligible, and the UI must say so
|
||||
// honestly rather than implying something was deleted.
|
||||
@@ -100,7 +126,7 @@ test.describe("V2-M7 admin actions: recovery tokens, trash purge, LeafWiki impor
|
||||
const pageTitle = `Welkom bij LeafWiki ${relativeDir}`;
|
||||
seedLeafWikiFixture(relativeDir, pageTitle);
|
||||
|
||||
await page.goto("/admin");
|
||||
await page.goto("/admin/knowledge");
|
||||
await expectAuthenticatedShell(page);
|
||||
await expect(page.getByRole("heading", { name: "LeafWiki-import" })).toBeVisible();
|
||||
|
||||
@@ -184,9 +210,10 @@ async function findImportedPage(page: Page, title: string) {
|
||||
// finished, which a bare locator check on the title alone could not distinguish from
|
||||
// "the request has not come back yet".
|
||||
await expect(
|
||||
page.getByRole("list", { name: "Zoekresultaten" }).or(page.getByText("Geen resultaten gevonden.")),
|
||||
page.getByRole("region", { name: "Zoekresultaten", exact: true }),
|
||||
).toBeVisible();
|
||||
return page.getByRole("button", { name: new RegExp(escapeForRegExp(title)) });
|
||||
return page.getByRole("region", { name: "Zoekresultaten", exact: true })
|
||||
.getByRole("link", { name: new RegExp(escapeForRegExp(title)) });
|
||||
}
|
||||
|
||||
function escapeForRegExp(value: string) {
|
||||
|
||||
@@ -153,7 +153,7 @@ test.describe("V2-M8 accessibility: previously unscanned screens and states", ()
|
||||
});
|
||||
expect(snapshotResponse.ok(), await snapshotResponse.text()).toBeTruthy();
|
||||
|
||||
await page.goto("/admin");
|
||||
await page.goto("/admin/inventory");
|
||||
await expectAuthenticatedShell(page);
|
||||
const proposalCard = page.locator(".inventory-proposals article", { hasText: detectedName });
|
||||
await expect(proposalCard).toBeVisible();
|
||||
|
||||
@@ -179,6 +179,8 @@ test.describe("V2-M2 accessibility (axe) on primary routes, both themes", () =>
|
||||
|
||||
for (const theme of ["Licht", "Donker"] as const) {
|
||||
test(`primary routes have zero axe violations in ${theme === "Licht" ? "light" : "dark"} mode`, async ({ page }) => {
|
||||
// Seven complete route loads and axe scans share this test's total budget.
|
||||
test.slow();
|
||||
await setTheme(page, theme);
|
||||
for (const routePath of routes) {
|
||||
await page.goto(routePath);
|
||||
@@ -206,6 +208,8 @@ test.describe("V2-M2 accessibility (axe) on primary routes, both themes", () =>
|
||||
|
||||
test.describe("V2-M2 visual fidelity capture (compared against artifacts/stitch/v2/screens by hand)", () => {
|
||||
test("capture implementation screenshots for the required routes/breakpoints/themes", async ({ page }) => {
|
||||
// Eleven captures plus persisted theme changes form one visual evidence set.
|
||||
test.slow();
|
||||
await setTheme(page, "Donker");
|
||||
await page.setViewportSize({ width: 1600, height: 1000 });
|
||||
await page.goto("/");
|
||||
|
||||
Reference in new issue
Block a user