From 6c562f05d826a882317a40089e8ba9501b8d388c Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Wed, 6 May 2026 13:28:57 -0700 Subject: [PATCH] test(ui): add review visual smoke --- web/e2e/reviews-visual.spec.ts | 97 ++++++++++++++++++++++++++++++ web/src/app/(app)/reviews/page.tsx | 2 +- web/src/app/globals.css | 13 +++- 3 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 web/e2e/reviews-visual.spec.ts diff --git a/web/e2e/reviews-visual.spec.ts b/web/e2e/reviews-visual.spec.ts new file mode 100644 index 0000000..9eec487 --- /dev/null +++ b/web/e2e/reviews-visual.spec.ts @@ -0,0 +1,97 @@ +import { expect, test, type Page, type Route } from "@playwright/test"; + +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: "Review Tester" })); + localStorage.setItem("notelett_access_token", token); + localStorage.setItem("notelett_refresh_token", "refresh-token"); + }, accessToken()); +} + +function json(route: Route, body: unknown, status = 200) { + return route.fulfill({ + status, + contentType: "application/json", + body: JSON.stringify(body), + }); +} + +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", + updatedBy: "agent-1", + }; +} + +async function mockReviewApis(page: Page) { + await page.route("**/api/auth/refresh", (route) => + json(route, { accessToken: accessToken(), refreshToken: "refresh-token" }), + ); + await page.route("**/api/kill-switch**", (route) => + json(route, { disabled: false, message: null }), + ); + await page.route("**/api/**", (route) => { + const url = new URL(route.request().url()); + const path = url.pathname.replace(/^\/api/, ""); + + if (path === "/note-agent-actions/pending") { + return json(route, { + items: [ + actionDoc("act-1", "Approve release summary", "proposed"), + actionDoc("act-2", "Reject stale task", "proposed"), + ], + total: 2, + }); + } + if (path === "/note-agent-actions") { + return json(route, { + items: [actionDoc("act-3", "Previous review", "approved")], + total: 1, + }); + } + if (path === "/broadcasts/active" || path === "/surveys/active") { + return json(route, { items: [], total: 0 }); + } + + return json(route, {}); + }); +} + +test.beforeEach(async ({ page }) => { + await seedAuth(page); + await mockReviewApis(page); +}); + +for (const viewport of [ + { name: "desktop", width: 1440, height: 1000 }, + { name: "mobile", width: 390, height: 900 }, +]) { + test(`reviews visual smoke - ${viewport.name}`, async ({ page }) => { + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await page.goto("/reviews"); + await expect(page.getByRole("heading", { name: "Agent review" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Approval queue" })).toBeVisible(); + await expect(page.getByRole("button", { name: /Approve release summary/ })).toBeVisible(); + + const overflow = await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth); + expect(overflow).toBe(false); + + const screenshot = await page.screenshot({ fullPage: true }); + expect(screenshot.byteLength).toBeGreaterThan(10_000); + }); +} diff --git a/web/src/app/(app)/reviews/page.tsx b/web/src/app/(app)/reviews/page.tsx index aac6b99..21b6995 100644 --- a/web/src/app/(app)/reviews/page.tsx +++ b/web/src/app/(app)/reviews/page.tsx @@ -274,7 +274,7 @@ export default function ReviewsPage() { description="Approval queue, proposal comparison, and audit-oriented review surfaces for agent-mediated edits." actions={Operator workflow shell} > -
+
diff --git a/web/src/app/globals.css b/web/src/app/globals.css index cf54e43..9013571 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -136,6 +136,16 @@ button { gap: var(--nl-space-6); } +.review-workflow-grid { + display: grid; + grid-template-columns: minmax(260px, 320px) minmax(0, 1fr); + gap: var(--nl-space-4); +} + +.review-workflow-grid > * { + min-width: 0; +} + @media (max-width: 980px) { .app-shell { padding-left: 0; @@ -151,7 +161,8 @@ button { } .note-detail-grid, - .workspace-layout-grid { + .workspace-layout-grid, + .review-workflow-grid { grid-template-columns: minmax(0, 1fr) !important; } }