test(web): add release journey e2e coverage
This commit is contained in:
parent
a5fedf1f08
commit
62cda1fb22
290
web/e2e/release-flows.spec.ts
Normal file
290
web/e2e/release-flows.spec.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||
|
||||
const workspace = {
|
||||
id: "ws-1",
|
||||
name: "Launch Workspace",
|
||||
description: "Release notes",
|
||||
members: [{ userId: "user-1", role: "owner" }],
|
||||
updatedAt: "2026-05-05T00:00:00.000Z",
|
||||
updatedBy: "user-1",
|
||||
noteCount: 2,
|
||||
};
|
||||
|
||||
const relatedNote = {
|
||||
id: "note-2",
|
||||
workspaceId: "ws-1",
|
||||
title: "Related rollout note",
|
||||
body: "<p>Related body</p>",
|
||||
status: "active",
|
||||
tags: ["release"],
|
||||
links: [],
|
||||
updatedAt: "2026-05-04T00:00:00.000Z",
|
||||
updatedBy: "user-1",
|
||||
createdBy: "user-1",
|
||||
createdAt: "2026-05-04T00:00:00.000Z",
|
||||
sourceType: "manual",
|
||||
};
|
||||
|
||||
function accessToken() {
|
||||
const payload = Buffer.from(JSON.stringify({ exp: 4102444800 })).toString("base64url");
|
||||
return `test.${payload}.token`;
|
||||
}
|
||||
|
||||
async function seedAuth(page: Page) {
|
||||
await page.addInitScript((token) => {
|
||||
localStorage.setItem("notelett_auth_user", JSON.stringify({ id: "user-1", email: "user@example.com", name: "Release Tester" }));
|
||||
localStorage.setItem("notelett_access_token", token);
|
||||
localStorage.setItem("notelett_refresh_token", "refresh-token");
|
||||
}, accessToken());
|
||||
}
|
||||
|
||||
async function mockReleaseApis(page: Page) {
|
||||
let note = {
|
||||
id: "note-1",
|
||||
workspaceId: "ws-1",
|
||||
title: "Launch readiness note",
|
||||
body: "<p>Initial release body</p>",
|
||||
status: "active",
|
||||
tags: ["launch"],
|
||||
links: [],
|
||||
updatedAt: "2026-05-05T00:00:00.000Z",
|
||||
updatedBy: "user-1",
|
||||
createdBy: "user-1",
|
||||
createdAt: "2026-05-05T00:00:00.000Z",
|
||||
sourceType: "manual",
|
||||
};
|
||||
|
||||
let reviewItems = [
|
||||
actionDoc("act-1", "Approve release summary", "proposed"),
|
||||
actionDoc("act-2", "Reject stale task", "proposed"),
|
||||
];
|
||||
|
||||
const calls = {
|
||||
createdNote: false,
|
||||
editedNote: false,
|
||||
archived: false,
|
||||
restored: false,
|
||||
linked: false,
|
||||
promptRun: false,
|
||||
intake: false,
|
||||
approved: false,
|
||||
rejected: false,
|
||||
};
|
||||
(page as Page & { releaseCalls?: typeof calls }).releaseCalls = calls;
|
||||
|
||||
await page.route("**/api/auth/refresh", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ accessToken: accessToken(), refreshToken: "refresh-token" }),
|
||||
}),
|
||||
);
|
||||
await page.route("**/api/kill-switch**", (route) =>
|
||||
route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ disabled: false, message: null }) }),
|
||||
);
|
||||
|
||||
await page.route("**/api/**", async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const path = url.pathname.replace(/^\/api/, "");
|
||||
const method = request.method();
|
||||
|
||||
if (path === "/workspaces/summaries") {
|
||||
return json(route, { items: [workspace], total: 1 });
|
||||
}
|
||||
if (path === "/workspaces") {
|
||||
return json(route, { items: [workspace], total: 1 });
|
||||
}
|
||||
if (path === "/notes" && method === "POST") {
|
||||
calls.createdNote = true;
|
||||
const input = request.postDataJSON() as { id: string; title: string; body: string; workspaceId: string; tags?: string[] };
|
||||
note = { ...note, ...input, status: "draft", updatedAt: "2026-05-05T01:00:00.000Z" };
|
||||
return json(route, note, 201);
|
||||
}
|
||||
if (path === "/notes" && method === "GET") {
|
||||
const search = url.searchParams.get("search");
|
||||
if (search === "note-1") return json(route, { items: [note], total: 1 });
|
||||
if (search === "Related") return json(route, { items: [relatedNote], total: 1 });
|
||||
return json(route, { items: [note, relatedNote], total: 2 });
|
||||
}
|
||||
if (path === "/notes/search" && method === "POST") {
|
||||
return json(route, { items: [], total: 0, mode: "lexical" });
|
||||
}
|
||||
if (path === "/notes/note-1" && method === "GET") {
|
||||
return json(route, note);
|
||||
}
|
||||
if (path === "/notes/note-1" && method === "PATCH") {
|
||||
calls.editedNote = true;
|
||||
const input = request.postDataJSON() as { title?: string; body?: string };
|
||||
note = { ...note, ...input, updatedAt: "2026-05-05T02:00:00.000Z" };
|
||||
return json(route, note);
|
||||
}
|
||||
if (path === "/notes/note-1/archive") {
|
||||
calls.archived = true;
|
||||
note = { ...note, status: "archived" };
|
||||
return json(route, note);
|
||||
}
|
||||
if (path === "/notes/note-1/restore") {
|
||||
calls.restored = true;
|
||||
note = { ...note, status: "active" };
|
||||
return json(route, note);
|
||||
}
|
||||
if (path === "/notes/note-1/versions") {
|
||||
return json(route, { items: [], total: 0 });
|
||||
}
|
||||
if (path === "/note-tasks") return json(route, { items: [], total: 0 });
|
||||
if (path === "/note-artifacts") return json(route, { items: [], total: 0 });
|
||||
if (path === "/note-relationships" && method === "POST") {
|
||||
calls.linked = true;
|
||||
return json(route, { id: "rel-1" }, 201);
|
||||
}
|
||||
if (path === "/note-relationships") return json(route, { items: [], total: 0 });
|
||||
if (path === "/note-agent-actions/pending" && method === "GET") {
|
||||
return json(route, { items: reviewItems, total: reviewItems.length });
|
||||
}
|
||||
if (path === "/note-agent-actions" && method === "GET") {
|
||||
return json(route, { items: [], total: 0 });
|
||||
}
|
||||
if (path.startsWith("/note-agent-actions/") && method === "PATCH") {
|
||||
const id = path.split("/").pop();
|
||||
const body = request.postDataJSON() as { state: "approved" | "rejected" };
|
||||
calls.approved ||= body.state === "approved";
|
||||
calls.rejected ||= body.state === "rejected";
|
||||
reviewItems = reviewItems.filter((item) => item.id !== id);
|
||||
return json(route, actionDoc(id ?? "act", body.state === "approved" ? "Approved" : "Rejected", body.state));
|
||||
}
|
||||
if (path === "/note-prompts") {
|
||||
return json(route, {
|
||||
items: [{
|
||||
id: "tmpl-1",
|
||||
slug: "summarize",
|
||||
name: "Summarize",
|
||||
description: "Summarize the current note",
|
||||
category: "transform",
|
||||
inputType: "text",
|
||||
outputType: "text",
|
||||
isBuiltin: true,
|
||||
}],
|
||||
total: 1,
|
||||
});
|
||||
}
|
||||
if (path === "/notes/note-1/reading-time") return json(route, { wordCount: 120, readingTimeMinutes: 1 });
|
||||
if (path === "/note-prompts/run") {
|
||||
calls.promptRun = true;
|
||||
return json(route, { content: "Prompt summary result", model: "mock", usage: { totalTokens: 12 } });
|
||||
}
|
||||
if (path === "/intake") {
|
||||
calls.intake = true;
|
||||
return json(route, { jobId: "job-1", noteId: "note-1", contentType: "article", ruleMatched: null, templateSlug: "summarize", status: "queued" }, 201);
|
||||
}
|
||||
if (path === "/public/note-shares/public-token") {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
headers: { "Cache-Control": "no-store", "X-Robots-Tag": "noindex, nofollow" },
|
||||
body: JSON.stringify({
|
||||
product: "NoteLett",
|
||||
noteId: "note-public",
|
||||
workspaceId: "ws-1",
|
||||
title: "Public release note",
|
||||
body: "<p>Read-only public body</p>",
|
||||
updatedAt: "2026-05-05T00:00:00.000Z",
|
||||
expiresAt: "2026-06-05T00:00:00.000Z",
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (path === "/broadcasts/active") return json(route, { items: [], total: 0 });
|
||||
if (path === "/surveys/active") return json(route, { items: [], total: 0 });
|
||||
if (path === "/saved-views") return json(route, { items: [], total: 0 });
|
||||
|
||||
return json(route, {});
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await seedAuth(page);
|
||||
await mockReleaseApis(page);
|
||||
});
|
||||
|
||||
test("create note and intake URL from dashboard", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
await page.getByLabel("URL to process").fill("https://example.com/release-plan");
|
||||
await page.getByRole("button", { name: "Process URL" }).click();
|
||||
await expect.poll(() => (page as Page & { releaseCalls: Record<string, boolean> }).releaseCalls.intake).toBe(true);
|
||||
|
||||
await page.getByRole("button", { name: "New Note" }).click({ force: true });
|
||||
await page.getByLabel("Note title").fill("Release E2E note");
|
||||
await page.getByLabel("Note body").fill("Created by Playwright");
|
||||
await page.getByRole("button", { name: "Create" }).click();
|
||||
await expect.poll(() => (page as Page & { releaseCalls: Record<string, boolean> }).releaseCalls.createdNote).toBe(true);
|
||||
});
|
||||
|
||||
test("edit, archive, restore, link, and run a prompt from note detail", async ({ page }) => {
|
||||
await page.goto("/notes/note-1");
|
||||
await expect(page.locator("#note-title")).toHaveValue("Launch readiness note");
|
||||
await page.getByRole("textbox", { name: "Title", exact: true }).fill("Edited release note");
|
||||
await page.getByRole("button", { name: "Save now" }).click();
|
||||
await expect.poll(() => (page as Page & { releaseCalls: Record<string, boolean> }).releaseCalls.editedNote).toBe(true);
|
||||
|
||||
await page.getByRole("button", { name: "Archive" }).click();
|
||||
await expect.poll(() => (page as Page & { releaseCalls: Record<string, boolean> }).releaseCalls.archived).toBe(true);
|
||||
await page.getByRole("button", { name: "Restore" }).click();
|
||||
await expect.poll(() => (page as Page & { releaseCalls: Record<string, boolean> }).releaseCalls.restored).toBe(true);
|
||||
|
||||
await page.getByRole("button", { name: "Link Note" }).click();
|
||||
await page.getByLabel("Search notes to link").fill("Related");
|
||||
await page.getByRole("button", { name: "Search" }).click();
|
||||
await page.getByRole("button", { name: "Select note: Related rollout note" }).click();
|
||||
await page.getByRole("button", { name: "Link", exact: true }).click();
|
||||
await expect.poll(() => (page as Page & { releaseCalls: Record<string, boolean> }).releaseCalls.linked).toBe(true);
|
||||
|
||||
await page.getByRole("button", { name: "Run: Summarize" }).click();
|
||||
await expect(page.getByText("Prompt summary result")).toBeVisible();
|
||||
await expect.poll(() => (page as Page & { releaseCalls: Record<string, boolean> }).releaseCalls.promptRun).toBe(true);
|
||||
});
|
||||
|
||||
test("approve and reject review proposals", async ({ page }) => {
|
||||
await page.goto("/reviews");
|
||||
await expect(page.getByRole("button", { name: /Approve release summary/ })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Approve", exact: true }).click();
|
||||
await expect.poll(() => (page as Page & { releaseCalls: Record<string, boolean> }).releaseCalls.approved).toBe(true);
|
||||
|
||||
await expect(page.getByRole("button", { name: /Reject stale task/ })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Reject", exact: true }).click();
|
||||
await expect.poll(() => (page as Page & { releaseCalls: Record<string, boolean> }).releaseCalls.rejected).toBe(true);
|
||||
});
|
||||
|
||||
test("settings smoke and public share page", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
||||
await expect(page.getByText("Connect your agent (MCP)")).toBeVisible();
|
||||
|
||||
await page.goto("/share/public-token");
|
||||
await expect(page.getByText("Read-only public share")).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Public release note" })).toBeVisible();
|
||||
await expect(page.getByText("Read-only public body")).toBeVisible();
|
||||
});
|
||||
|
||||
function actionDoc(id: string, afterSummary: string, state: string) {
|
||||
return {
|
||||
id,
|
||||
productId: "notelett",
|
||||
workspaceId: "ws-1",
|
||||
noteId: "note-1",
|
||||
actorId: "agent-1",
|
||||
actorType: "agent",
|
||||
actionType: "update",
|
||||
state,
|
||||
afterSummary,
|
||||
beforeSummary: "Before text",
|
||||
updatedAt: "2026-05-05T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function json(route: Route, body: unknown, status = 200) {
|
||||
return route.fulfill({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user