Initial curated projectbrain source from 8a7b1e0651dbdab97d744cbd2e653ab3970ac6c5
This commit is contained in:
commit
5aa3e8d685
700 files changed
+132801
No files matched your search
@@ -0,0 +1,112 @@
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { AutoModeState } from "../src/shared/api/autonomyApi";
|
||||
|
||||
const projectId = "widefrog";
|
||||
const envelope = (data: unknown) => ({ data, meta: { requestId: "attention-auto-e2e", timestamp: new Date().toISOString() } });
|
||||
|
||||
const autoModeState = (): AutoModeState => ({
|
||||
version: 1,
|
||||
enabled: true,
|
||||
status: "running",
|
||||
autoBrainstorm: true,
|
||||
maxBrainstormIterations: 5,
|
||||
maxConsecutiveFailures: 3,
|
||||
leaseMinutes: 30,
|
||||
maxWorkItemsPerRun: 8,
|
||||
maxRunMinutes: 45,
|
||||
roadmap: {
|
||||
id: "roadmap-e2e",
|
||||
title: "E2E-roadmap",
|
||||
sourceMarkdown: "# E2E-roadmap\n\n## Eerste fase\n- [ ] Exact werkitem",
|
||||
status: "active",
|
||||
currentPhaseIndex: 0,
|
||||
importedAt: "2026-09-01T00:00:00.000Z",
|
||||
phases: [{
|
||||
id: "phase-e2e",
|
||||
title: "Eerste fase",
|
||||
objective: "Eerste fase",
|
||||
status: "active",
|
||||
brainstormIterations: 0,
|
||||
brainstormConverged: false,
|
||||
validationFailures: 0,
|
||||
items: [{ id: "item-e2e", title: "Exact werkitem", status: "pending", attempts: 0 }],
|
||||
}],
|
||||
},
|
||||
completedLeases: [],
|
||||
events: [],
|
||||
updatedAt: "2026-09-01T00:00:00.000Z",
|
||||
updatedBy: "owner",
|
||||
});
|
||||
|
||||
test("attention inbox exposes every visible item as an actionable destination", async ({ page }) => {
|
||||
await page.goto(`/projects/${projectId}/attention`);
|
||||
await expect(page.getByRole("heading", { name: "Jouw aandacht", exact: true })).toBeVisible();
|
||||
await expect(page.getByText(/item\(s\) vragen jouw aandacht|Niets wacht op jou/)).toBeVisible();
|
||||
|
||||
const reviewAction = page.getByRole("button", { name: /Beoordelen/ }).first();
|
||||
if (await reviewAction.isVisible()) {
|
||||
await reviewAction.click();
|
||||
await expect(page).toHaveURL(/\/review-queue\?reviewItemId=/);
|
||||
}
|
||||
|
||||
const accessibility = await new AxeBuilder({ page }).analyze();
|
||||
expect(accessibility.violations.filter((violation) => ["serious", "critical"].includes(violation.impact ?? ""))).toEqual([]);
|
||||
});
|
||||
|
||||
test("Auto Mode distinguishes queued work from real execution and protects roadmap progress", async ({ page }) => {
|
||||
let state = autoModeState();
|
||||
let roadmapImports = 0;
|
||||
await page.route(`**/api/projects/${projectId}/autonomy**`, async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
if (request.method() === "GET") {
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(state)) });
|
||||
return;
|
||||
}
|
||||
if (request.method() === "POST" && url.pathname.endsWith("/roadmap")) {
|
||||
roadmapImports += 1;
|
||||
await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify(envelope(state)) });
|
||||
return;
|
||||
}
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(envelope(state)) });
|
||||
});
|
||||
|
||||
await page.goto(`/projects/${projectId}/auto`);
|
||||
await expect(page.getByRole("heading", { name: "Auto Mode", exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Klaar voor uitvoering", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText("Werk staat veilig in de wachtrij")).toBeVisible();
|
||||
await expect(page.getByText("ProjectBrain werkt", { exact: true })).toHaveCount(0);
|
||||
|
||||
await page.getByText("Autonomiebeleid en geavanceerde instellingen", { exact: true }).click();
|
||||
await page.getByText("Geavanceerde grenzen", { exact: true }).click();
|
||||
await expect(page.getByLabel("Werkitems per run")).toHaveValue("8");
|
||||
await expect(page.getByLabel("Looptijd per run")).toHaveValue("45");
|
||||
|
||||
await expect(page.getByText("Roadmap handmatig importeren", { exact: true })).toBeVisible();
|
||||
await page.getByLabel("Roadmap").fill("# Nieuwe roadmap\n\n## Nieuwe fase\n- [ ] Nieuw werk");
|
||||
await page.getByRole("button", { name: "Roadmap bijwerken" }).click();
|
||||
await expect(page.getByText("Bestaande roadmap en voortgang vervangen?")).toBeVisible();
|
||||
expect(roadmapImports).toBe(0);
|
||||
await page.getByRole("button", { name: "Annuleren" }).click();
|
||||
|
||||
state = {
|
||||
...state,
|
||||
activeLease: {
|
||||
token: "00000000-0000-4000-8000-000000000001",
|
||||
kind: "implement",
|
||||
actor: "chatgpt-agent",
|
||||
roadmapId: "roadmap-e2e",
|
||||
phaseId: "phase-e2e",
|
||||
itemId: "item-e2e",
|
||||
createdAt: new Date(Date.now() - 60_000).toISOString(),
|
||||
expiresAt: new Date(Date.now() + 30 * 60_000).toISOString(),
|
||||
},
|
||||
};
|
||||
await page.getByRole("button", { name: "Vernieuwen" }).click();
|
||||
await expect(page.getByText("ProjectBrain werkt", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Exact werkitem", { exact: true }).first()).toBeVisible();
|
||||
|
||||
const accessibility = await new AxeBuilder({ page }).analyze();
|
||||
expect(accessibility.violations.filter((violation) => ["serious", "critical"].includes(violation.impact ?? ""))).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
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();
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import type { AutoModeState } from "../src/shared/api/autonomyApi";
|
||||
|
||||
const stateFor = (project: string): AutoModeState => ({
|
||||
version: 1, enabled: false, status: "off", autoBrainstorm: true, maxBrainstormIterations: 5,
|
||||
maxConsecutiveFailures: 3, leaseMinutes: 30, maxWorkItemsPerRun: 8, maxRunMinutes: 45,
|
||||
runs: [], events: [], updatedAt: new Date().toISOString(), updatedBy: "test",
|
||||
roadmap: { id: project, title: `${project} roadmap`, sourceMarkdown: `## ${project}\n- [ ] ${project} task`, status: "active", currentPhaseIndex: 0, importedAt: new Date().toISOString(), phases: [
|
||||
{ id: `${project}-phase`, title: project, objective: "Finish work", status: "active", brainstormIterations: 0, brainstormConverged: false, validationFailures: 0,
|
||||
items: [{ id: `${project}-task`, title: `${project} task`, status: "pending", attempts: 1 }] },
|
||||
] },
|
||||
});
|
||||
const envelope = (data: unknown) => ({ data, meta: { requestId: "platform-state", timestamp: new Date().toISOString() } });
|
||||
const navigateWithoutReload = async (page: Page, path: string) => page.evaluate((next) => {
|
||||
window.history.pushState({}, "", next);
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
}, path);
|
||||
|
||||
const openAutoAdvancedSettings = async (page: Page) => {
|
||||
const advanced = page.getByText("Autonomiebeleid en geavanceerde instellingen", { exact: true });
|
||||
if (await advanced.locator("xpath=..").getAttribute("open") === null) await advanced.click();
|
||||
};
|
||||
|
||||
const openManualRoadmapImport = async (page: Page) => {
|
||||
await openAutoAdvancedSettings(page);
|
||||
await expect(page.getByText("Roadmap handmatig importeren", { exact: true })).toBeVisible();
|
||||
};
|
||||
|
||||
const openAutoAdvancedLimits = async (page: Page) => {
|
||||
await openAutoAdvancedSettings(page);
|
||||
const advancedLimits = page.getByText("Geavanceerde grenzen", { exact: true });
|
||||
if (await advancedLimits.locator("xpath=..").getAttribute("open") === null) await advancedLimits.click();
|
||||
};
|
||||
|
||||
test("project switches never carry an instruction into another project", async ({ page }) => {
|
||||
await page.goto("/projects/widefrog/workspace");
|
||||
await page.getByLabel("Jouw opdracht", { exact: true }).fill("Only change WideFrog onboarding");
|
||||
await navigateWithoutReload(page, "/projects/nebula/workspace");
|
||||
await expect(page.getByLabel("Jouw opdracht", { exact: true })).toHaveValue("");
|
||||
await expect(page.getByRole("button", { name: "Aan ProjectBrain geven" })).toBeDisabled();
|
||||
});
|
||||
|
||||
test("project switches discard old roadmap drafts and delayed responses", async ({ page }) => {
|
||||
let delayWidefrog = false;
|
||||
let releaseResponse!: () => void;
|
||||
const responseGate = new Promise<void>((resolve) => { releaseResponse = resolve; });
|
||||
let requestStarted!: () => void;
|
||||
const started = new Promise<void>((resolve) => { requestStarted = resolve; });
|
||||
await page.route("**/api/projects/*/autonomy", async (route) => {
|
||||
const project = new URL(route.request().url()).pathname.split("/")[3];
|
||||
if (project === "widefrog" && delayWidefrog) { requestStarted(); await responseGate; }
|
||||
await route.fulfill({ json: envelope(stateFor(project)) });
|
||||
});
|
||||
|
||||
await page.goto("/projects/widefrog/auto");
|
||||
await openManualRoadmapImport(page);
|
||||
await expect(page.getByLabel("Roadmap", { exact: true })).toContainText("widefrog");
|
||||
await page.getByLabel("Roadmap", { exact: true }).fill("## Private draft\n- Only WideFrog");
|
||||
|
||||
delayWidefrog = true;
|
||||
await page.getByRole("button", { name: "Vernieuwen" }).click();
|
||||
await started;
|
||||
|
||||
const nebulaResponse = page.waitForResponse((response) => response.url().endsWith("/projects/nebula/autonomy"));
|
||||
await navigateWithoutReload(page, "/projects/nebula/auto");
|
||||
await nebulaResponse;
|
||||
await openManualRoadmapImport(page);
|
||||
await expect(page.getByLabel("Titel", { exact: true })).toHaveValue("nebula roadmap");
|
||||
await expect(page.getByLabel("Roadmap", { exact: true })).toHaveValue("## nebula\n- [ ] nebula task");
|
||||
|
||||
const oldResponse = page.waitForResponse((response) => response.url().endsWith("/projects/widefrog/autonomy"));
|
||||
releaseResponse();
|
||||
await oldResponse;
|
||||
|
||||
await expect(page.getByLabel("Titel", { exact: true })).toHaveValue("nebula roadmap");
|
||||
await expect(page.getByLabel("Roadmap", { exact: true })).toHaveValue("## nebula\n- [ ] nebula task");
|
||||
await expect(page.getByText("widefrog task", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("reimporting an unchanged roadmap preserves work and toggling Auto preserves unsaved policy", async ({ page }) => {
|
||||
let state = stateFor("widefrog");
|
||||
let imports = 0;
|
||||
await page.route("**/api/projects/widefrog/autonomy**", async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method() === "POST" && request.url().endsWith("/roadmap")) imports += 1;
|
||||
if (request.method() === "PATCH" && request.url().endsWith("/autonomy")) {
|
||||
state = { ...state, ...request.postDataJSON() };
|
||||
}
|
||||
await route.fulfill({ json: envelope(state) });
|
||||
});
|
||||
await page.goto("/projects/widefrog/auto");
|
||||
await openManualRoadmapImport(page);
|
||||
await page.getByRole("button", { name: "Roadmap bijwerken" }).click();
|
||||
await expect(page.getByText("Deze roadmap is al opgeslagen. De voortgang blijft behouden.")).toBeVisible();
|
||||
expect(imports).toBe(0);
|
||||
await openAutoAdvancedLimits(page);
|
||||
await page.getByLabel("Werkitems per run").fill("12");
|
||||
await page.getByRole("switch", { name: "Auto", exact: true }).click();
|
||||
await expect(page.getByRole("switch", { name: "Auto", exact: true })).toBeChecked();
|
||||
await expect(page.getByLabel("Werkitems per run")).toHaveValue("12");
|
||||
await expect(page.getByRole("button", { name: "Beleid opslaan" })).toBeEnabled();
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("project search and sort survive reload and clearing preserves sorting", async ({ page }) => {
|
||||
await page.goto("/projects?q=widefrog&sort=name");
|
||||
await expect(page.getByRole("textbox", { name: "Projecten zoeken", exact: true })).toHaveValue("widefrog");
|
||||
await expect(page.getByRole("button", { name: "Naam", exact: true })).toHaveAttribute("aria-pressed", "true");
|
||||
await page.getByRole("textbox", { name: "Projecten zoeken", exact: true }).fill("no-project-matches-this");
|
||||
await page.reload();
|
||||
await expect(page.getByRole("textbox", { name: "Projecten zoeken", exact: true })).toHaveValue("no-project-matches-this");
|
||||
await page.getByRole("button", { name: "Selectie wissen", exact: true }).click();
|
||||
await expect(page.getByRole("textbox", { name: "Projecten zoeken", exact: true })).toHaveValue("");
|
||||
await expect(page.getByRole("button", { name: "Naam", exact: true })).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(page).toHaveURL(/sort=name/);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("create, filter, complete and reload a local task", async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.goto("/projects/widefrog/tasks");
|
||||
await expect(page.getByRole("heading", { name: "Tasks", exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "New Task", exact: true }).click();
|
||||
await page.getByLabel("Task Title").fill("Task queue browser regression");
|
||||
await page.getByLabel("Linked Feature").click();
|
||||
await page.getByRole("option").first().click();
|
||||
await page.getByRole("button", { name: "Create Task", exact: true }).click();
|
||||
await page.getByRole("textbox", { name: "Search tasks", exact: true }).fill("Task queue browser regression");
|
||||
await expect(page.getByText("Task queue browser regression", { exact: true })).toBeVisible();
|
||||
await page.getByRole("checkbox", { name: "Mark Task queue browser regression done", exact: true }).check();
|
||||
await page.reload();
|
||||
await expect(page.getByRole("checkbox", { name: "Mark Task queue browser regression done", exact: true })).toBeChecked();
|
||||
await expect(page.getByRole("textbox", { name: "Search tasks", exact: true })).toHaveValue("Task queue browser regression");
|
||||
await page.getByRole("textbox", { name: "Search tasks", exact: true }).fill("no-such-task-xyz");
|
||||
await expect(page.getByText("No matching tasks", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Clear filters" }).click();
|
||||
await expect(page.getByText("Task queue browser regression", { exact: true })).toBeVisible();
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
Reference in new issue
Block a user