Audit of the full E2E suite (43 specs) surfaced four issues that were hiding behind 'all 96/96 web unit tests pass' but actually meant the browser-level coverage was broken end-to-end. All four are fixed and the suite now passes 43/43. 1. Port conflict silently testing wrong app. playwright.config.ts hard- coded baseURL=http://localhost:3000 with reuseExistingServer:true on non-CI hosts. When the dev host had ANY service on :3000 (Grafana, chronomind, etc), Playwright happily ran the entire E2E suite against the wrong app and reported the unrelated failures as 'real'. Now honors NOTELETT_WEB_PORT env (default 3000) so a contributor can opt into any free port and Playwright drives both baseURL and the dev-server PORT consistently. 2. Missing test dependency. web/e2e/accessibility.spec.ts imports @axe-core/playwright but web/package.json never declared it. The accessibility coverage was DOA — every CI run that included this spec would module-not-found-error before a single check ran. Added @axe-core/playwright to devDependencies. 3. Mock that never fires. smart-actions.spec.ts 'history API mock returns items' used page.route() to mock /api/note-prompts/history then bypassed the mock entirely with page.request.get() (which uses Playwright's separate request context, not the browser context that page.route intercepts). The request went to the dev server and got 404. Replaced with page.goto + page.evaluate(fetch(...)) so the browser-side fetch hits the page.route mock as intended. 4. Missing visual-regression baselines. visual-regression.spec.ts had no committed baseline screenshots for dashboard / workspaces / search. First run on a clean host always reported 'snapshot doesn't exist, writing actual'. Generated and committed darwin baselines. Verified end-to-end (NOTELETT_WEB_PORT=3050 against this host's free port): 43 passed (34.8s) Total test-tier counts on main now: backend unit + integration (memory) 380/380 backend cosmos emulator (live) 4/4 web vitest 96/96 mobile vitest 97/97 web playwright e2e 43/43 --- TOTAL 620/620
207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
|
|
test.describe("Smart Actions", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Mock all backend API calls
|
|
await page.route("**/api/note-prompts**", (route) => {
|
|
if (route.request().method() === "GET") {
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
items: [
|
|
{
|
|
id: "builtin-summarize",
|
|
slug: "summarize",
|
|
name: "Summarize",
|
|
category: "transform",
|
|
isBuiltin: true,
|
|
inputType: "text",
|
|
outputType: "new_note",
|
|
},
|
|
{
|
|
id: "custom-1",
|
|
slug: "my-custom",
|
|
name: "My Custom Template",
|
|
category: "generate",
|
|
isBuiltin: false,
|
|
inputType: "text",
|
|
outputType: "replace",
|
|
},
|
|
],
|
|
total: 2,
|
|
}),
|
|
});
|
|
}
|
|
if (route.request().method() === "POST") {
|
|
return route.fulfill({
|
|
status: 201,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
id: "new-template-1",
|
|
slug: "new-template",
|
|
name: "New Template",
|
|
category: "transform",
|
|
isBuiltin: false,
|
|
}),
|
|
});
|
|
}
|
|
if (route.request().method() === "DELETE") {
|
|
return route.fulfill({ status: 204 });
|
|
}
|
|
return route.fulfill({ status: 200, contentType: "application/json", body: "{}" });
|
|
});
|
|
|
|
await page.route("**/api/notes**", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ items: [], total: 0 }),
|
|
})
|
|
);
|
|
|
|
await page.route("**/api/workspaces**", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ items: [], total: 0 }),
|
|
})
|
|
);
|
|
|
|
await page.route("**/api/prompt-schedules**", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ items: [], total: 0 }),
|
|
})
|
|
);
|
|
|
|
await page.route("**/api/prompt-webhooks**", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ items: [], total: 0 }),
|
|
})
|
|
);
|
|
|
|
await page.route("**/api/**", (route) =>
|
|
route.fulfill({ status: 200, contentType: "application/json", body: "{}" })
|
|
);
|
|
});
|
|
|
|
test("dashboard page loads with smart actions API mocked", async ({ page }) => {
|
|
await page.goto("/dashboard");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await expect(page.locator("body")).toBeVisible();
|
|
});
|
|
|
|
test("note detail page loads without JS errors", async ({ page }) => {
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (err) => errors.push(err.message));
|
|
|
|
await page.goto("/notes/test-note-1");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await expect(page.locator("body")).toBeVisible();
|
|
|
|
const realErrors = errors.filter(
|
|
(e) =>
|
|
!e.includes("fetch") &&
|
|
!e.includes("Failed") &&
|
|
!e.includes("Unexpected") &&
|
|
!e.includes("hydration")
|
|
);
|
|
expect(realErrors).toHaveLength(0);
|
|
});
|
|
|
|
test("settings page loads without JS errors", async ({ page }) => {
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (err) => errors.push(err.message));
|
|
|
|
await page.goto("/settings");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await expect(page.locator("body")).toBeVisible();
|
|
|
|
const realErrors = errors.filter(
|
|
(e) =>
|
|
!e.includes("fetch") &&
|
|
!e.includes("Failed") &&
|
|
!e.includes("Unexpected") &&
|
|
!e.includes("hydration")
|
|
);
|
|
expect(realErrors).toHaveLength(0);
|
|
});
|
|
|
|
test("prompts page loads with template list", async ({ page }) => {
|
|
await page.goto("/prompts");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await expect(page.locator("body")).toBeVisible();
|
|
});
|
|
|
|
test("knowledge gaps page loads", async ({ page }) => {
|
|
await page.route("**/api/workspaces/ws-1/knowledge-gaps", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ gaps: [], topicMap: {} }),
|
|
})
|
|
);
|
|
await page.goto("/workspaces/ws-1/gaps");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await expect(page.locator("body")).toBeVisible();
|
|
});
|
|
|
|
test("search page loads without JS errors", async ({ page }) => {
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (err) => errors.push(err.message));
|
|
|
|
await page.goto("/search");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await expect(page.locator("body")).toBeVisible();
|
|
|
|
const realErrors = errors.filter(
|
|
(e) =>
|
|
!e.includes("fetch") &&
|
|
!e.includes("Failed") &&
|
|
!e.includes("Unexpected") &&
|
|
!e.includes("hydration")
|
|
);
|
|
expect(realErrors).toHaveLength(0);
|
|
});
|
|
|
|
test("workspaces page loads", async ({ page }) => {
|
|
await page.goto("/workspaces");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await expect(page.locator("body")).toBeVisible();
|
|
});
|
|
|
|
test("reviews page loads", async ({ page }) => {
|
|
await page.goto("/reviews");
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await expect(page.locator("body")).toBeVisible();
|
|
});
|
|
|
|
test("history API mock returns items", async ({ page }) => {
|
|
// page.route() intercepts BROWSER network requests, while
|
|
// page.request.get() uses Playwright's separate request context
|
|
// which bypasses page.route() entirely. Drive the request from
|
|
// within the page (fetch) so the mock applies.
|
|
await page.route("**/api/note-prompts/history**", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
items: [{ id: "h1", noteId: "n1", workspaceId: "ws1", toolName: "summarize", state: "applied", createdAt: "2026-01-01T00:00:00Z" }],
|
|
total: 1,
|
|
}),
|
|
})
|
|
);
|
|
await page.goto("/dashboard");
|
|
const data = await page.evaluate(async () => {
|
|
const res = await fetch("/api/note-prompts/history?workspaceId=ws1");
|
|
return { status: res.status, body: await res.json() } as { status: number; body: { items: unknown[]; total: number } };
|
|
});
|
|
expect(data.status).toBe(200);
|
|
expect(data.body.items).toHaveLength(1);
|
|
});
|
|
});
|