e2e/upgrade-acceptance.spec.ts
Linked repository file from this build.
import { fixtureReviewKey } from "../src/programs/snap-negative/fixture-key";
import { appendDemoFixture, resetDemoFixture } from "./demo-fixture";
import { currentFfyFilters } from "../src/features/reports/report-data";
import type { ReportResponse } from "../src/features/reports/report-data";
import { expect, test, type APIRequestContext } from "@playwright/test";
import { completedReviewAnswers, demoScenarioTemplate } from "../src/programs/snap-negative/seed";
import type { ReviewCommand } from "../src/features/reviews/commands";
import { demoActor } from "../src/lib/auth/roster";
import type { DemoRole } from "../src/features/reviews/types";
import { enterMoney } from "./money-input";
async function command(
request: APIRequestContext,
value: ReviewCommand,
role: DemoRole = "examiner",
) {
const stored = await (
await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")
).json();
const response = await request.post("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d", {
headers: { "x-qc-test-staff": demoActor(role).staffId! },
data: {
version: stored.version,
requestId: crypto.randomUUID(),
commands: [
{
id: crypto.randomUUID(),
command: value,
},
],
},
});
expect(response.ok(), await response.text()).toBe(true);
}
test.beforeEach(async ({ request }) => {
await resetDemoFixture();
await command(request, { type: "reset_demo" });
});
test("supervisor can schedule a subordinate review and examiner can read the saved appointment", async ({
page,
request,
}) => {
await page
.context()
.addCookies([{ name: "qc-test-staff", value: "201", url: "http://localhost:3001" }]);
await page.goto("/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d");
await page.locator("#appointment").fill("2026-09-15T14:30");
await page.locator("#appointment").blur();
await page.getByRole("button", { name: "Save review", exact: true }).click();
await expect
.poll(
async () =>
(await (await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json())
.review.appointment,
)
.toBe("2026-09-15T14:30");
await page
.context()
.addCookies([{ name: "qc-test-staff", value: "101", url: "http://localhost:3001" }]);
await page.reload();
await expect(page.locator("#appointment")).toHaveValue("2026-09-15T14:30");
});
test("ERC reads saved review envelopes and counts released outcomes", async ({ page, request }) => {
await command(request, {
type: "scenario",
scenario: "eligibility",
value: demoScenarioTemplate(5000),
});
await command(request, {
type: "scenario",
scenario: "compI",
value: demoScenarioTemplate(1800),
});
await command(request, { type: "answers", value: completedReviewAnswers({ finding: "2" }) });
await command(request, { type: "apply_final" });
await command(request, { type: "outcome", value: "clean" });
await command(request, { type: "transition", action: "submit", note: "" });
const submitted = await (
await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")
).json();
const at = new Date().toISOString();
await appendDemoFixture({
...submitted.review,
state: "released_to_reporting",
supervisorApproved: { actor: demoActor("supervisor"), at },
pecApproved: { actor: demoActor("pec"), at },
memoApproved: { actor: demoActor("program_manager"), at },
errorSummaryReleased: { actor: demoActor("program_manager"), at },
});
await page
.context()
.addCookies([{ name: "qc-test-staff", value: "501", url: "http://localhost:3001" }]);
await page.goto("/reports");
const reportResponse = await request.get(
`/api/reports?${new URLSearchParams(currentFfyFilters())}`,
{
headers: { "x-qc-test-staff": "501" },
},
);
expect(reportResponse.ok()).toBe(true);
const errors = ((await reportResponse.json()) as ReportResponse).summary.error;
let documentLoads = 0;
page.on("request", (sent) => {
if (sent.isNavigationRequest()) documentLoads++;
});
await page.getByRole("button", { name: "Refresh", exact: true }).click();
await expect(page.getByRole("button", { name: "Refresh", exact: true })).toBeEnabled();
expect(documentLoads).toBe(0);
await expect(page.getByText(new RegExp(`Valid: \\d+ · Invalid: ${errors}$`))).toBeVisible();
});
test("desktop tabs stay usable while a finish confirmation is open", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d");
await expect(page.locator("#client-name")).toBeVisible();
await page.getByRole("button", { name: "Save & Submit", exact: true }).click();
for (const name of ["Computation", "Review", "Activity", "Household"]) {
await page.getByRole("tab", { name, exact: true }).click();
expect(await page.evaluate(() => document.body.scrollHeight <= innerHeight + 1)).toBe(true);
}
await expect(page.getByRole("button", { name: "Confirm submit", exact: true })).toBeVisible();
});
test("inbox stays in staff scope and header search opens results with the keyboard", async ({
page,
request,
}) => {
const inbox = await request.get("/api/inbox");
expect(inbox.ok()).toBe(true);
const data = await inbox.json();
for (const row of [...data.newReviews, ...data.lastTouched, ...data.awaitingReview]) {
expect(row.examiner).toBe("101");
}
await page.goto("/dashboard");
await expect(page.getByRole("heading", { name: "Home", exact: true })).toBeVisible();
await expect(
page
.getByRole("navigation", { name: "Main navigation" })
.getByRole("link", { name: "Reports" }),
).toHaveCount(0);
const search = page.getByRole("textbox", { name: "Search reviews", exact: true });
await search.fill("60001");
await page.getByRole("link", { name: /^See all \d+ results?$/ }).click();
await expect(page).toHaveURL(/\/search\?/);
await page
.locator('a[href="/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d"]')
.filter({ hasText: /^60001$/ })
.click();
await expect(page).toHaveURL(/\/reviews\/aa1635c0-c99d-5f12-8f26-54d3f23bf13d$/);
await expect(page.locator("#client-name")).toBeVisible();
});
test("cross-team URLs and searches are scoped and forged roles cannot grant access", async ({
request,
}) => {
const headers = { "x-qc-test-staff": "105" };
expect((await request.get("/api/reviews/51fa29a3-2d17-5b91-8a1b-5dfb27c28100")).status()).toBe(
404,
);
expect(
(await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d", { headers })).status(),
).toBe(403);
const search = await request.get("/api/search?q=60001", { headers });
expect(search.ok()).toBe(true);
const scopedReviews = (await search.json()).reviews;
expect(
scopedReviews.some(
(review: { reviewKey: string }) =>
review.reviewKey === "aa1635c0-c99d-5f12-8f26-54d3f23bf13d",
),
).toBe(false);
expect(scopedReviews.every((review: { examiner: string }) => review.examiner === "105")).toBe(
true,
);
expect((await request.get("/api/reviews")).status()).toBe(403);
const snapshot = await (
await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")
).json();
const post = (entry: Record<string, unknown>) =>
request.post("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d", {
headers,
data: { version: snapshot.version, requestId: crypto.randomUUID(), commands: [entry] },
});
const note = { id: crypto.randomUUID(), command: { type: "note", body: "Forged admin" } };
// The session decides who acts. A body that names an actor is refused outright.
expect((await post(note)).status()).toBe(403);
expect((await post({ ...note, actor: demoActor("admin") })).status()).toBe(400);
});
test("notification opens its review and marks only that event read", async ({ page, request }) => {
await command(request, { type: "appointment", value: "2026-09-18T09:15" }, "supervisor");
const before = await (await request.get("/api/inbox/events")).json();
const event = before.events.find(
(entry: { reviewKey: string; kind: string }) =>
entry.reviewKey === "aa1635c0-c99d-5f12-8f26-54d3f23bf13d" &&
entry.kind === "appointment_changed",
);
expect(event).toBeTruthy();
await page.goto("/dashboard");
await page.getByRole("button", { name: /^Notifications/ }).click();
await page
.getByRole("button")
.filter({ hasText: "Appointment changed" })
.filter({ hasText: "60001 · 2026" })
.first()
.click();
await expect(page).toHaveURL(/\/reviews\/aa1635c0-c99d-5f12-8f26-54d3f23bf13d$/);
await expect
.poll(async () => {
const after = await (await request.get("/api/inbox/events")).json();
return after.events.some((entry: { id: string }) => entry.id === event.id);
})
.toBe(false);
});
test("appearance choices survive reload and keep locked values readable", async ({ page }) => {
await page
.context()
.addCookies([{ name: "qc-test-staff", value: "601", url: "http://localhost:3001" }]);
await page.goto("/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d");
await page.getByRole("button", { name: "Appearance", exact: true }).click();
const settings = page.getByLabel("Appearance").last();
await settings.getByRole("radiogroup", { name: "Mode" }).getByText("Dark").click();
await settings.getByRole("radiogroup", { name: "Theme" }).getByText("Gruvbox").click();
await settings
.getByRole("radiogroup", { name: "Layout" })
.getByText("Sidebar", { exact: true })
.click();
await settings
.getByRole("radiogroup", { name: "Accessibility" })
.getByText("Red-green safe")
.click();
await expect(page.locator("html")).toHaveClass(/dark/);
await expect(page.locator("html")).toHaveAttribute("data-palette", "gruvbox");
await expect(page.locator("html")).toHaveAttribute("data-layout", "workbench");
await expect(page.locator("html")).toHaveAttribute("data-vision", "rg-safe");
await page.reload();
await expect(page.locator("html")).toHaveClass(/dark/);
await expect(page.locator("html")).toHaveAttribute("data-palette", "gruvbox");
await expect(page.locator("html")).toHaveAttribute("data-layout", "workbench");
await expect(page.locator("html")).toHaveAttribute("data-vision", "rg-safe");
const client = page.locator("#client-name");
await expect(client).toHaveAttribute("readonly", "");
expect(await client.evaluate((element) => getComputedStyle(element).opacity)).toBe("1");
await client.focus();
await expect(client).toBeFocused();
});
test("autosave defaults off and field and computation edits require their explicit saves", async ({
page,
request,
}) => {
await page.goto("/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d");
await expect(page.locator("#client-name")).toBeVisible();
await page.clock.install();
const before = await (
await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")
).json();
await page.locator("#client-name").fill("Idle save ergonomics");
await page.clock.fastForward(59_000);
expect(
(await (await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json()).version,
).toBe(before.version);
await page.clock.fastForward(1_000);
expect(
(await (await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json()).version,
).toBe(before.version);
await page.getByRole("button", { name: "Save review", exact: true }).click();
await expect
.poll(
async () =>
(await (await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json())
.review.clientName,
)
.toBe("Idle save ergonomics");
await page.locator("#client-name").fill("Blur save ergonomics");
await page.locator("#client-name").blur();
await page.clock.fastForward(59_000);
expect(
(await (await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json()).review
.clientName,
).toBe("Idle save ergonomics");
await page.clock.fastForward(1_000);
expect(
(await (await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json()).review
.clientName,
).toBe("Idle save ergonomics");
await page.getByRole("button", { name: "Save review", exact: true }).click();
await expect
.poll(
async () =>
(await (await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json())
.review.clientName,
)
.toBe("Blur save ergonomics");
const saved = await (
await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")
).json();
await page.getByRole("tab", { name: "Computation", exact: true }).click();
const amount = page.locator('[id*="compI-dependentCareCents"]').first();
await enterMoney(amount, "125.00");
await amount.blur();
await page.clock.fastForward(61_000);
expect(
(await (await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json()).version,
).toBe(saved.version);
await expect(page.getByText("Unsaved draft", { exact: true })).toBeVisible();
});
test("dashboard uses four compact lists with independent pagination and resizes within half the viewport", async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 720 });
let pageSize = 0;
await page.route("**/api/inbox?**", async (route) => {
const query = new URL(route.request().url()).searchParams;
pageSize = Number(query.get("pageSize"));
const rows = (pageKey: string) =>
Array.from({ length: pageSize }, (_, index) => {
const number = String(62000 + Number(query.get(pageKey)) * pageSize + index);
return {
reviewKey: `${fixtureReviewKey(number)}`,
fiscalYear: 2026,
reviewNumber: number,
clientName: `Pagination fixture ${number}`,
caseNumber: `SYN-${number}`,
examiner: "101",
state: "new_active",
appointment: null,
touchedAt: new Date().toISOString(),
};
});
await route.fulfill({
json: {
asOf: new Date().toISOString(),
since: new Date().toISOString(),
lastInboxSeenAt: null,
section: query.get("section") || "all",
newReviews: rows("newPage"),
lastTouched: rows("touchedPage"),
awaitingReview: rows("queuePage"),
events: rows("eventsPage").map((row) => ({
...row,
id: row.reviewKey,
kind: "appointment_changed",
createdAt: new Date().toISOString(),
})),
totals: { newReviews: 100, lastTouched: 100, awaitingReview: 100, events: 100 },
},
});
});
await page
.context()
.addCookies([{ name: "qc-inbox-page-size", value: "1", url: "http://localhost:3001" }]);
await page.goto("/dashboard");
await expect.poll(() => pageSize).toBe(5);
const titles = ["New", "Last touched", "Since last visit", "With reviewers"];
const panels = titles.map((name) => page.getByRole("region", { name, exact: true }));
for (const panel of panels) await expect(panel).toHaveAttribute("aria-busy", "false");
const boxes = await Promise.all(panels.map((panel) => panel.boundingBox()));
for (let index = 0; index < boxes.length; index++) {
expect(boxes[index]).not.toBeNull();
expect(boxes[index]!.height).toBeLessThanOrEqual(360);
expect(boxes[index]!.x).toBeGreaterThanOrEqual(0);
expect(boxes[index]!.x + boxes[index]!.width).toBeLessThanOrEqual(1280);
}
const newList = panels[0];
const touched = panels[1];
await expect(newList.getByRole("listitem")).toHaveCount(5);
const firstNew = await newList.getByRole("link").first().getAttribute("href");
const firstTouched = await touched.getByRole("link").first().getAttribute("href");
await newList.getByRole("button", { name: "Next page of New", exact: true }).click();
await expect(newList).toHaveAttribute("aria-busy", "false");
await expect
.poll(() => newList.getByRole("link").first().getAttribute("href"))
.not.toBe(firstNew);
await expect(touched.getByRole("link").first()).toHaveAttribute("href", firstTouched!);
await page.setViewportSize({ width: 1280, height: 600 });
await expect.poll(() => pageSize).toBe(4);
await expect(newList.getByRole("listitem")).toHaveCount(4);
await expect(newList.getByRole("link").first()).toHaveAttribute("href", firstNew!);
for (const panel of panels) {
expect((await panel.boundingBox())!.height).toBeLessThanOrEqual(300);
expect(await panel.evaluate((element) => element.scrollHeight <= element.clientHeight)).toBe(
true,
);
}
await page.setViewportSize({ width: 375, height: 700 });
await expect
.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth))
.toBe(true);
const mobileBoxes = await Promise.all(panels.map((panel) => panel.boundingBox()));
for (let index = 1; index < mobileBoxes.length; index++) {
expect(Math.abs(mobileBoxes[index]!.x - mobileBoxes[0]!.x)).toBeLessThanOrEqual(1);
expect(mobileBoxes[index]!.y).toBeGreaterThan(mobileBoxes[index - 1]!.y);
}
await page.setViewportSize({ width: 1280, height: 720 });
await expect(newList.getByRole("listitem")).toHaveCount(5);
});
test("admin desk has compact paginated rows and appends multiple PDFs with clear partial failures", async ({
page,
}) => {
await page.setViewportSize({ width: 1440, height: 800 });
await page
.context()
.addCookies([{ name: "qc-test-staff", value: "401", url: "http://localhost:3001" }]);
await page.goto("/search");
const table = page.getByRole("table", { name: "Search results" });
await expect(table).toBeVisible();
const first = table.locator("tbody tr").first();
expect((await first.boundingBox())!.height).toBeLessThanOrEqual(34);
const firstText = await first.getByRole("link").last().innerText();
await page.getByRole("button", { name: "Next", exact: true }).click();
await expect(first.getByRole("link").last()).not.toHaveText(firstText);
await page.getByRole("textbox", { name: "Search reviews", exact: true }).fill("60001");
await page.getByRole("textbox", { name: "Search reviews", exact: true }).press("Enter");
const picker = table
.locator("tbody tr")
.filter({ has: page.locator('a[href="/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d"]') })
.getByLabel("Mailed PDFs for review 60001");
await expect(picker).toBeAttached();
const pdf = (name: string) => ({
name,
mimeType: "application/pdf",
buffer: Buffer.from("%PDF-1.4\n%%EOF"),
});
await picker.setInputFiles([
pdf("first.pdf"),
{ name: "invalid.pdf", mimeType: "application/pdf", buffer: Buffer.from("invalid") },
pdf("third.pdf"),
]);
for (const name of ["first.pdf", "invalid.pdf", "third.pdf"])
await page.getByLabel(`Purpose for ${name}`).fill(`Mailed evidence from ${name}`);
await page.getByRole("button", { name: "Attach selected PDFs", exact: true }).click();
await expect(page.getByRole("dialog").getByRole("alert")).toContainText(
"Remaining files were not attached",
);
await expect(
page.getByRole("dialog").getByText("1 PDF attached.", { exact: true }),
).toBeVisible();
await page.getByRole("button", { name: "Cancel selection", exact: true }).click();
await picker.setInputFiles([pdf("second.pdf"), pdf("third.pdf")]);
for (const name of ["second.pdf", "third.pdf"])
await page.getByLabel(`Purpose for ${name}`).fill(`Mailed evidence from ${name}`);
await page.getByRole("button", { name: "Attach selected PDFs", exact: true }).click();
await expect(
page.getByRole("dialog", { name: "Name the incoming documents" }).getByRole("status"),
).toHaveText("2 PDFs attached.");
const stored = await (
await page.request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")
).json();
expect(stored.review.documents.map((doc: { filename: string }) => doc.filename)).toEqual([
"first.pdf",
"second.pdf",
"third.pdf",
]);
});
test("narrow workspace keeps General details and tab content reachable", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 700 });
await page.goto("/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d");
const general = page.getByRole("region", { name: "Identifying Information (II)", exact: true });
await expect(general).toBeHidden();
await page.getByRole("button", { name: "General details", exact: true }).click();
await expect(general).toBeVisible();
await page.locator("#sample-month").fill("2026-08");
await page.locator("#sample-month").blur();
await page.getByRole("button", { name: "Save review", exact: true }).click();
await expect
.poll(
async () =>
(await (await page.request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json())
.review.sampleMonth,
)
.toBe("2026-08");
await page.getByRole("button", { name: "General details", exact: true }).click();
for (const name of ["Review", "Computation", "Activity", "Household"]) {
await page.getByRole("tab", { name, exact: true }).click();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(
true,
);
}
await page.setViewportSize({ width: 1280, height: 800 });
await expect(general).toBeVisible();
});
test("workspace edits narrative and appointment time", async ({ page, request }) => {
await page.goto("/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d");
await expect(page.locator("#client-name")).toBeVisible();
await page.locator("#appointment").fill("2026-09-18T14:45");
await expect(page.locator("#appointment")).toHaveValue("2026-09-18T14:45");
await page.getByRole("tab", { name: "Review", exact: true }).click();
await page.locator("#narrative").fill("Explanation entered on the Review tab.");
await page.getByRole("button", { name: "Save review", exact: true }).click();
await expect
.poll(
async () =>
(await (await request.get("/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d")).json())
.review.facts.answers.narrative,
)
.toBe("Explanation entered on the Review tab.");
await page.getByRole("tab", { name: "Household", exact: true }).click();
page.once("dialog", (dialog) => dialog.accept());
const household = page.getByRole("region", { name: "Household members", exact: true });
await household.getByRole("button", { name: "Clear", exact: true }).click();
await expect(household.getByRole("button", { name: "Clear", exact: true })).toBeDisabled();
});