import AxeBuilder from "@axe-core/playwright"; import { expect, test, type Page } from "@playwright/test"; const projectId = "widefrog"; const workspaceHeading = /Wat wil je dat ProjectBrain bereikt\?/i; const activityHeading = /Activiteit/i; const assertHealthyPage = async (page: Page, heading: RegExp) => { const consoleErrors: string[] = []; const failedRequests: string[] = []; page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); page.on("requestfailed", (request) => failedRequests.push(`${request.method()} ${request.url()}`)); await expect(page.getByRole("heading", { name: heading }).first()).toBeVisible(); await expect(page.locator("body")).not.toContainText("Something went wrong"); const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); expect(overflow).toBeLessThanOrEqual(1); const accessibility = await new AxeBuilder({ page }).analyze(); expect(accessibility.violations.filter((violation) => ["serious", "critical"].includes(violation.impact ?? ""))).toEqual([]); expect(consoleErrors).toEqual([]); expect(failedRequests).toEqual([]); }; test("1. opens the project portfolio and reopens persistent project memory", async ({ page }) => { test.setTimeout(75_000); await page.goto("/projects"); await assertHealthyPage(page, /Projectcatalogus/i); await page.goto(`/projects/${projectId}/workspace`); await assertHealthyPage(page, workspaceHeading); await expect(page.getByRole("button", { name: /WideFrog/i }).first()).toBeVisible(); await page.reload(); await assertHealthyPage(page, workspaceHeading); await expect(page.getByRole("button", { name: /WideFrog/i }).first()).toBeVisible(); }); test("2. capture and review surfaces form one navigable intake journey", async ({ page }) => { await page.goto(`/projects/${projectId}/capture`); await assertHealthyPage(page, /Capture Center/i); await page.goto(`/projects/${projectId}/review-queue`); await assertHealthyPage(page, /Te beoordelen/i); }); test("3. review and accepted-truth history surfaces remain recoverable", async ({ page }) => { await page.goto(`/projects/${projectId}/review-queue`); await assertHealthyPage(page, /Te beoordelen/i); expect(await page.locator('[aria-live="polite"], [role="status"], [role="alert"]').count()).toBeGreaterThan(0); await page.goto(`/projects/${projectId}/history`); await assertHealthyPage(page, activityHeading); }); test("9. critical route remains operable on a constrained connection", async ({ page }) => { await page.route("**/*.js", async (route) => { await new Promise((resolve) => setTimeout(resolve, 75)); await route.continue(); }); await page.goto(`/projects/${projectId}/review-queue`); await assertHealthyPage(page, /Te beoordelen/i); await page.getByText("Proces, bewijs en beheer", { exact: true }).click(); await expect(page.getByRole("button", { name: "Zichtbare reviews exporteren" })).toBeEnabled(); }); test("10. action surfaces expose one primary action while system status stays diagnostic", async ({ page }) => { for (const route of ["workspace", "review-queue", "execution"]) { await page.goto(`/projects/${projectId}/${route}`); await expect(page.locator('[data-page-primary="true"]:visible')).toHaveCount(1); } await page.goto(`/projects/${projectId}/runtime`); await expect(page.getByRole("heading", { name: "Systeemstatus", exact: true })).toBeVisible(); await expect(page.getByText("ProjectBrain API", { exact: true })).toBeVisible(); await expect(page.getByText("Database", { exact: true }).first()).toBeVisible(); await expect(page.getByText("Runners", { exact: true }).first()).toBeVisible(); await expect(page.getByRole("button", { name: "Bijwerken", exact: true })).toBeVisible(); }); test("4. context, handoff and session-pack chain renders end to end", async ({ page }) => { for (const [route, heading] of [["context-packs", /Context Packs/i], ["handoffs", /Codex Handoffs/i], ["session-packs", /Session Packs/i]] as const) { await page.goto(`/projects/${projectId}/${route}`); await assertHealthyPage(page, heading); } }); test("5. execution feedback links back to durable project history", async ({ page }) => { await page.goto(`/projects/${projectId}/execution-feedback`); await assertHealthyPage(page, /Execution Feedback/i); await page.goto(`/projects/${projectId}/history`); await assertHealthyPage(page, activityHeading); }); test("6. release evidence and release center surfaces render", async ({ page }) => { await page.goto(`/projects/${projectId}/runtime`); await assertHealthyPage(page, /Systeemstatus/i); await page.goto(`/projects/${projectId}/release`); await assertHealthyPage(page, /Releasecentrum/i); }); test("7. mock-mode development identity survives reload", async ({ page }) => { await page.goto(`/projects/${projectId}/workspace`); await assertHealthyPage(page, workspaceHeading); await expect(page.getByRole("button", { name: /WideFrog/i }).first()).toBeVisible(); await page.reload(); await expect(page.getByRole("heading", { name: workspaceHeading })).toHaveCount(1); await expect(page.getByRole("button", { name: /WideFrog/i }).first()).toBeVisible(); await expect(page.getByRole("heading", { name: /Aanmelden/i })).toHaveCount(0); }); test("8. keyboard, route focus, reduced motion and 200% zoom stay usable", async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await page.goto(`/projects/${projectId}/workspace`); await page.keyboard.press("Tab"); const skipLink = page.getByRole("link", { name: "Ga naar hoofdinhoud" }); await expect(skipLink).toBeFocused(); await skipLink.press("Enter"); await expect(page.locator("#projectbrain-main")).toBeFocused(); await page.evaluate(() => { document.documentElement.style.zoom = "2"; }); const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); expect(overflow).toBeLessThanOrEqual(1); await expect(page.getByRole("heading", { name: workspaceHeading })).toHaveCount(1); await page.evaluate(() => { document.documentElement.style.zoom = "1"; }); const moreButton = page.getByRole("button", { name: "Meer" }); if (await moreButton.isVisible()) { await moreButton.click(); const navigationDialog = page.getByRole("dialog", { name: "Projectnavigatie" }); await expect(navigationDialog).toBeVisible(); await expect(navigationDialog).toContainText("ProjectBrain"); await page.keyboard.press("Escape"); await expect(navigationDialog).toBeHidden(); await expect(moreButton).toBeFocused(); } await page.goto(`/projects/${projectId}/capture`); await expect(page.locator("#projectbrain-main")).toBeFocused(); }); test("11. development workbench creates an isolated branch, commits a file and opens a pull request", async ({ page }) => { await page.goto(`/projects/${projectId}/develop`); await expect(page.getByRole("heading", { name: /WideFrog · repository/i })).toBeVisible(); const localRepositoryBridge = page.getByRole("button", { name: "Repository voorbereiden" }); if (await localRepositoryBridge.isVisible()) { await expect(page.getByText("Werk lokaal verder in Repositorybouwer")).toBeVisible(); await expect(localRepositoryBridge).toBeEnabled(); return; } let branchCreated = false; let fileSaved = false; let validationSynced = false; let pullRequestCreated = false; const envelope = (data: unknown) => ({ data, meta: { requestId: "e2e-workbench", timestamp: new Date().toISOString() } }); const link = { id: "link-1", projectId, projectSlug: projectId, projectName: "WideFrog", owner: "Jens", repository: "widefrog", fullName: "Jens/widefrog", defaultBranch: "main", accessMode: "read_write", role: "primary", isPrimary: true }; const delivery = () => ({ id: "delivery-1", projectId, title: "Continue WideFrog", intent: "Deliver a reviewed repository change", priority: "important", approvalState: "approved", spec: { version: 1, problem: "The repository needs an updated readme", desiredOutcome: "The readme is current", users: ["maintainer"], goals: ["Update README"], nonGoals: [], acceptanceCriteria: ["README is updated"], implementationPlan: ["Edit README.md"], risks: [], validationCommands: ["pnpm test"], status: pullRequestCreated ? "pull_request_open" : validationSynced ? "validating" : branchCreated ? "executing" : "approved", branch: branchCreated ? "chatgpt/widefrog-work" : undefined, repositoryId: "link-1", changedFiles: fileSaved ? ["README.md"] : [], progressNotes: [], validationResults: validationSynced ? [{ command: "pnpm test", status: "passed" }] : [], validationEvidence: validationSynced ? { provider: "gitea_actions", runId: 42, runNumber: 42, url: "https://gitea.example/Jens/widefrog/actions/runs/42", workflowPath: ".gitea/workflows/ci.yml", headSha: "work-sha", conclusion: "success", verifiedAt: new Date().toISOString() } : undefined, pullRequest: pullRequestCreated ? { number: 7, url: "https://gitea.example/Jens/widefrog/pulls/7", title: "ProjectBrain: continue WideFrog", state: "open" } : undefined, updatedBy: "e2e", updatedAt: new Date().toISOString(), }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); await page.route(`**/api/projects/${projectId}/feature-deliveries`, async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ items: [delivery()] })) }); }); await page.route(`**/api/projects/${projectId}/feature-deliveries/delivery-1/validation/sync`, async (route) => { validationSynced = true; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ delivery: delivery() })) }); }); await page.route(`**/api/projects/${projectId}/gitea/**`, async (route) => { const request = route.request(); const url = new URL(request.url()); if (url.pathname.endsWith("/repositories")) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope([link])) }); return; } if (url.pathname.endsWith("/workspace")) { const ref = url.searchParams.get("ref") || "main"; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ link, path: url.searchParams.get("path") || "", ref, entries: [{ type: "file", name: "README.md", path: "README.md", sha: "sha-1", size: 24, html_url: "https://gitea.example/Jens/widefrog/src/branch/main/README.md" }], branches: [{ name: "main", commit: { id: "main-sha" }, protected: true }, ...(branchCreated ? [{ name: "chatgpt/widefrog-work", commit: { id: "work-sha" }, protected: false }] : [])], pullRequests: pullRequestCreated ? [{ id: 1, number: 7, title: "ProjectBrain: continue WideFrog", state: "open", html_url: "https://gitea.example/Jens/widefrog/pulls/7", merged: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), head: { ref: "chatgpt/widefrog-work", sha: "work-sha" }, base: { ref: "main", sha: "main-sha" } }] : [], })) }); return; } if (url.pathname.endsWith("/file") && request.method() === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ path: "README.md", ref: url.searchParams.get("ref") || "main", sha: "sha-1", size: 24, content: fileSaved ? "# WideFrog\n\nUpdated." : "# WideFrog\n" })) }); return; } if (url.pathname.endsWith("/branches") && request.method() === "POST") { branchCreated = true; await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify(envelope({ repository: "Jens/widefrog", branch: { name: "chatgpt/widefrog-work", commit: { id: "work-sha" }, protected: false } })) }); return; } if (url.pathname.endsWith("/file") && request.method() === "PUT") { expect(request.postDataJSON()).toMatchObject({ expectedSha: "sha-1" }); fileSaved = true; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope({ repository: "Jens/widefrog", branch: "chatgpt/widefrog-work", path: "README.md", action: "updated" })) }); return; } if (url.pathname.endsWith("/pull-requests") && request.method() === "POST") { pullRequestCreated = true; await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify(envelope({ repository: "Jens/widefrog", pullRequest: { number: 7 } })) }); return; } await route.abort(); }); await page.goto(`/projects/${projectId}/develop`); await expect(page.getByRole("heading", { name: /WideFrog · repository/i })).toBeVisible(); await page.getByRole("textbox", { name: "Nieuwe deliverybranch" }).fill("chatgpt/widefrog-work"); await page.getByRole("button", { name: /Werkbranch maken/i }).click(); await expect(page.getByText("chatgpt/widefrog-work").first()).toBeVisible(); await page.getByRole("button", { name: /README.md/i }).click(); await page.getByRole("textbox", { name: "Repository file content" }).fill("# WideFrog\n\nUpdated."); await page.getByRole("button", { name: /Bestand committen/i }).click(); expect(fileSaved).toBe(true); await page.getByRole("button", { name: /Actions verifiëren/i }).click(); expect(validationSynced).toBe(true); await page.getByRole("button", { name: /Pull request maken/i }).click(); expect(pullRequestCreated).toBe(true); }); test("12. daily navigation hides specialist modules while keeping them searchable", async ({ page }) => { await page.goto(`/projects/${projectId}/workspace`); const moreButton = page.getByRole("button", { name: "Meer" }); if (await moreButton.isVisible()) await moreButton.click(); await expect(page.getByRole("link", { name: /Opdracht geven/i }).first()).toBeVisible(); await expect(page.getByRole("link", { name: /Repositoryanalyses/i })).toHaveCount(0); const advancedModulesButton = page.getByRole("button", { name: /Technische details tonen/i }); if (await advancedModulesButton.isVisible()) { await advancedModulesButton.click(); await expect(page.getByRole("link", { name: /Repositoryanalyses/i })).toBeVisible(); await page.getByRole("button", { name: /Technische details verbergen/i }).click(); await expect(page.getByRole("link", { name: /Repositoryanalyses/i })).toHaveCount(0); return; } await page.getByRole("button", { name: "Open zoek- en opdrachtenmenu" }).click(); await page.getByRole("combobox", { name: "Zoek pagina's, projectitems en opdrachten" }).fill("Repositoryanalyses"); await page.getByText("Repositoryanalyses", { exact: true }).first().click(); await expect(page).toHaveURL(new RegExp(`/projects/${projectId}/analysis-jobs$`)); await expect(page.getByRole("link", { name: "Repositoryanalyses", exact: true })).toBeVisible(); }); test("13. mobile shell keeps five-intent route context and search available", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/projects/nebula/workspace"); await page.evaluate(() => { window.history.pushState({}, "", "/projects/widefrog/findings"); window.dispatchEvent(new PopStateEvent("popstate")); }); await expect(page).toHaveURL(/\/projects\/widefrog\/findings$/); await expect(page.getByRole("link", { name: "Overzicht", exact: true })).toHaveAttribute("href", "/projects/widefrog/overview"); await expect(page.getByRole("link", { name: "Opdracht", exact: true })).toHaveAttribute("href", "/projects/widefrog/workspace"); await expect(page.getByRole("link", { name: "Aandacht", exact: true })).toHaveAttribute("href", "/projects/widefrog/attention"); await expect(page.getByRole("link", { name: "Resultaten", exact: true })).toHaveAttribute("href", "/projects/widefrog/results"); await expect(page.getByRole("link", { name: "Auto", exact: true })).toHaveCount(0); await expect(page.getByRole("button", { name: "Meer" })).toHaveAttribute("aria-current", "page"); const searchButton = page.getByRole("button", { name: "Open zoek- en opdrachtenmenu" }); await expect(searchButton).toBeVisible(); await searchButton.click(); await expect(page.getByRole("dialog", { name: "Opdrachtenpalet" })).toBeVisible(); await expect(page.getByRole("combobox", { name: "Zoek pagina's, projectitems en opdrachten" })).toBeFocused(); });