mirror of
https://github.com/willchen96/mike.git
synced 2026-07-26 23:51:08 +02:00
With the directory fetch storm eliminated (previous commit: batched listProjects?include=documents instead of a getProject burst per modal open), the local Supabase gateway no longer collapses under modal-open load, so the specs' 5xx-oriented scaffolding is removed: - create-with-retry loops (project create x2, chat create x2, workflow create, tabular-review create x10-attempt, review add-document x6) - reload-guards for transient "Project not found" (waitForProjectLoaded, gotoProjectRow, project-assistant tab loop) - networkidle settle-waits whose purpose was to let the per-project fetch burst drain before submitting (NewProjectModal / NewTRModal) - oversized per-test timeouts justified by the storm (180s -> 60-120s) Kept, deliberately: - the title-generation settle logic in chat rename/delete - chat submit's model-seeding retry loop (guards a documented useSelectedModel localStorage hydration race, unrelated to the gateway) - account display-name toPass block (guards a documented profile hydration race, unrelated to the gateway) - generic short networkidle settles after login/logout navigation - Playwright's own retries: 2 on CI Verified: with the fix, 3 consecutive full-suite runs (plus 2 more before de-retry) were green with zero Playwright retries, running against an account seeded with ~40 projects — a larger directory fan-out than CI's fresh database ever produces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
248 lines
11 KiB
TypeScript
248 lines
11 KiB
TypeScript
/**
|
|
* Tabular Review E2E tests:
|
|
* 1. Navigate to /tabular-reviews — the list page loads correctly
|
|
* 2. Create a new tabular review — modal flow, API call, redirect to detail
|
|
* 3. Review detail page — table structure and toolbar controls render
|
|
* 4. Add a document — upload via AddDocumentsModal, row appears in table
|
|
*
|
|
* Prerequisite: auth.setup.ts has already saved the session to e2e/.auth/user.json
|
|
* Test user: e2e@mike.local / E2eTestPass1! (storageState inherited from playwright.config.ts)
|
|
*/
|
|
import { test, expect } from "@playwright/test";
|
|
import path from "path";
|
|
|
|
const PDF_FIXTURE = path.join(__dirname, "fixtures/test.pdf");
|
|
|
|
// Run these tests sequentially in a single worker: they share the one test
|
|
// user's review list, so concurrent create/detail runs would race each other.
|
|
test.describe.configure({ mode: "serial" });
|
|
|
|
/* ─── Helpers ────────────────────────────────────────────────────────────── */
|
|
|
|
/**
|
|
* Click the "New review" Plus icon button in the Tabular Reviews list-page header.
|
|
*
|
|
* The button has no aria-label or visible text — it is an icon-only button
|
|
* rendered immediately after the HeaderSearchBtn (magnifier icon) in the same
|
|
* flex container that is the sibling of the <h1>Tabular Reviews</h1>.
|
|
*
|
|
* DOM structure:
|
|
* <div class="… justify-between …">
|
|
* <h1>Tabular Reviews</h1>
|
|
* <div class="flex items-center gap-2"> ← xpath=../div[1] from h1
|
|
* <div>…<button>{SearchIcon}</button></div> ← HeaderSearchBtn
|
|
* <button>{PlusIcon}</button> ← new-review button (.last())
|
|
* </div>
|
|
* </div>
|
|
*
|
|
* TODO: once aria-label="New review" is added to that button, replace with:
|
|
* page.getByRole("button", { name: "New review" })
|
|
*/
|
|
async function clickNewReviewBtn(page: import("@playwright/test").Page) {
|
|
// Walk from the h1 to the parent div, then select its first div child
|
|
// (the actions container); the last button within it is the Plus icon.
|
|
const actionsDiv = page
|
|
.getByRole("heading", { name: "Tabular Reviews" })
|
|
.locator("xpath=../div[1]"); // TODO: verify selector
|
|
await actionsDiv.getByRole("button").last().click();
|
|
}
|
|
|
|
/** Predicate matching the create-review request: POST /tabular-review (exact). */
|
|
const isCreateReviewPost = (r: import("@playwright/test").Response) =>
|
|
/\/tabular-review\/?$/.test(new URL(r.url()).pathname) &&
|
|
r.request().method() === "POST";
|
|
|
|
/**
|
|
* Open the AddNewTRModal (assumes /tabular-reviews is already loaded) and return
|
|
* its "Review name" input once visible.
|
|
*/
|
|
async function openNewReviewModal(page: import("@playwright/test").Page) {
|
|
await clickNewReviewBtn(page);
|
|
const titleInput = page.getByPlaceholder("Review name");
|
|
await expect(titleInput).toBeVisible({ timeout: 10_000 });
|
|
return titleInput;
|
|
}
|
|
|
|
/**
|
|
* Create a tabular review through the real modal flow and land on its detail
|
|
* page. Returns the title that was entered so callers can assert on it.
|
|
*
|
|
* @param onFirstOpen optional assertion run against the modal on the first open
|
|
* (used by Test 2 to verify the workflow-template default renders).
|
|
*/
|
|
async function createReview(
|
|
page: import("@playwright/test").Page,
|
|
label = "E2E Review",
|
|
onFirstOpen?: () => Promise<void>,
|
|
): Promise<string> {
|
|
await page.goto("/tabular-reviews");
|
|
await expect(
|
|
page.getByRole("heading", { name: "Tabular Reviews" }),
|
|
).toBeVisible({ timeout: 10_000 });
|
|
|
|
const reviewName = `${label} ${Date.now()}`;
|
|
|
|
const titleInput = await openNewReviewModal(page);
|
|
if (onFirstOpen) await onFirstOpen();
|
|
await titleInput.fill(reviewName);
|
|
|
|
// NewTRModal is a two-step wizard ("Details" → "Add Documents"); the
|
|
// "Create" submit button only exists on the second step, and "Next" only
|
|
// enables once the review has a name.
|
|
await page.getByRole("button", { name: "Next", exact: true }).click();
|
|
|
|
const respP = page.waitForResponse(isCreateReviewPost, {
|
|
timeout: 30_000,
|
|
});
|
|
// The modal footer's submit button and the page's own "Create" CTA both
|
|
// read "Create"; scope to the modal's submit (button[name="modalAction"]
|
|
// value="create-review") to avoid a strict-mode ambiguity.
|
|
await page
|
|
.locator('button[name="modalAction"][value="create-review"]')
|
|
.click();
|
|
const resp = await respP;
|
|
expect(
|
|
resp.ok(),
|
|
`POST /tabular-review returned ${resp.status()} — create flow broken?`,
|
|
).toBe(true);
|
|
const review = (await resp.json()) as { id: string };
|
|
|
|
// createTabularReview() → router.push("/tabular-reviews/<id>").
|
|
await expect(page).toHaveURL(
|
|
new RegExp(`/tabular-reviews/${review.id}`),
|
|
{ timeout: 30_000 },
|
|
);
|
|
|
|
return reviewName;
|
|
}
|
|
|
|
/* ─── Test 1: list page loads ─────────────────────────────────────────────── */
|
|
|
|
test("navigates to /tabular-reviews and the list page renders", async ({
|
|
page,
|
|
}) => {
|
|
// REGRESSION: fails if the /tabular-reviews route is removed or broken
|
|
await page.goto("/tabular-reviews");
|
|
|
|
await expect(page).toHaveURL(/\/tabular-reviews/);
|
|
|
|
// The page renders an h1 heading with the section title
|
|
await expect(
|
|
page.getByRole("heading", { name: "Tabular Reviews" }),
|
|
).toBeVisible({ timeout: 10_000 });
|
|
|
|
// The ToolbarTabs bar renders the "All" tab
|
|
// TODO: verify selector if ToolbarTabs uses role="tab" instead of role="button"
|
|
await expect(page.getByText("All")).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
|
|
/* ─── Test 2: create a new tabular review ─────────────────────────────────── */
|
|
|
|
test("creates a new tabular review and is redirected to the detail page", async ({
|
|
page,
|
|
}) => {
|
|
// Headroom for the create round-trip plus the first navigation to the
|
|
// dynamic /tabular-reviews/[id] route.
|
|
test.setTimeout(60_000);
|
|
// REGRESSION: fails if createTabularReview() API call is removed or the
|
|
// /tabular-reviews POST route is broken (createReview's response
|
|
// assertion then trips).
|
|
//
|
|
// createReview opens the modal, verifies the workflow-template default
|
|
// renders, submits, and lands on the new review's detail page.
|
|
const reviewName = await createReview(page, "E2E Review", async () => {
|
|
// The workflow template control defaults to "No template - start from
|
|
// scratch" once the templates request resolves (it shows "Loading
|
|
// templates…" until then), so allow time for that listWorkflows() fetch.
|
|
// NewTRModal renders it as a button, with a hyphen — not an em dash.
|
|
await expect(
|
|
page.getByRole("button", { name: "No template - start from scratch" }),
|
|
).toBeVisible({ timeout: 15_000 });
|
|
});
|
|
|
|
// The new review's title appears in the page breadcrumb header
|
|
await expect(page.getByText(reviewName)).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
/* ─── Test 3: review detail page table structure ─────────────────────────── */
|
|
|
|
test("review detail page renders the table structure and toolbar controls", async ({
|
|
page,
|
|
}) => {
|
|
// Headroom for the create round-trip plus the detail-route navigation.
|
|
test.setTimeout(60_000);
|
|
// REGRESSION: fails if the /tabular-reviews/[id] route, TRView, or TRTable
|
|
// component is broken
|
|
const reviewName = await createReview(page, "E2E Table Review");
|
|
|
|
// The breadcrumb header shows the review title via RenameableTitle
|
|
await expect(page.getByText(reviewName)).toBeVisible({ timeout: 10_000 });
|
|
|
|
// The breadcrumb also contains a "Tabular Reviews" back-nav button. Scope to
|
|
// the <main> landmark: the left sidebar nav also has a "Tabular Reviews"
|
|
// button, so an unscoped role query is a strict-mode violation. exact:true
|
|
// avoids also matching the mobile-only "Back to Tabular Reviews" control.
|
|
await expect(
|
|
page
|
|
.getByRole("main")
|
|
.getByRole("button", { name: "Tabular Reviews", exact: true }),
|
|
).toBeVisible({ timeout: 5_000 });
|
|
|
|
// TRTable always renders a "Document" column header, even when the review
|
|
// is empty. This is visible in both the empty-state and populated states.
|
|
await expect(
|
|
page.getByText("Document", { exact: true }),
|
|
).toBeVisible({ timeout: 10_000 });
|
|
|
|
// The toolbar renders "Add Columns" and "Add Documents" once loading is done.
|
|
// Both may also appear in TRTable's empty-state CTA, so .first() is used.
|
|
await expect(
|
|
page.getByRole("button", { name: /Add Columns/ }).first(),
|
|
).toBeVisible({ timeout: 5_000 });
|
|
await expect(
|
|
page.getByRole("button", { name: /Add Documents/ }).first(),
|
|
).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
|
|
/* ─── Test 4: add a document to a review ─────────────────────────────────── */
|
|
|
|
test("adds a document to a tabular review and the row appears in the table", async ({
|
|
page,
|
|
}) => {
|
|
// Headroom for the create round-trip plus the upload + document-link
|
|
// round-trips.
|
|
test.setTimeout(90_000);
|
|
// REGRESSION: fails if the document-to-review linking
|
|
// (PATCH /tabular-reviews/:id with document_ids) or the upload endpoint breaks
|
|
const reviewName = await createReview(page, "E2E Doc Review");
|
|
// reviewName is already confirmed visible on the detail page by createReview
|
|
|
|
const row = page.getByText("test.pdf").first();
|
|
const confirmBtn = page.getByRole("button", { name: "Confirm" });
|
|
|
|
// Open AddDocumentsModal (standalone path → AddDocumentsModal, not
|
|
// AddProjectDocsModal). first() handles both toolbar & empty-state CTA.
|
|
const addDocsBtn = page
|
|
.getByRole("button", { name: /Add Documents/ })
|
|
.first();
|
|
await expect(addDocsBtn).toBeVisible({ timeout: 10_000 });
|
|
await addDocsBtn.click();
|
|
|
|
// The footer's "Upload" button programmatically clicks a hidden
|
|
// <input type="file"> — Playwright intercepts it as a file-chooser event.
|
|
const uploadBtn = page.getByRole("button", { name: "Upload" });
|
|
await expect(uploadBtn).toBeVisible({ timeout: 5_000 });
|
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
|
await uploadBtn.click();
|
|
const fileChooser = await fileChooserPromise;
|
|
await fileChooser.setFiles(PDF_FIXTURE);
|
|
|
|
// After a successful upload the server document is auto-selected and the
|
|
// "Confirm" button transitions disabled → enabled.
|
|
await expect(confirmBtn).toBeEnabled({ timeout: 20_000 });
|
|
|
|
// Confirm → onSelect() → handleAddDocuments() → updateTabularReview()
|
|
// PATCH → setDocuments() → TRTable renders the new row with doc.filename.
|
|
await confirmBtn.click();
|
|
await expect(row).toBeVisible({ timeout: 15_000 });
|
|
});
|