/** * Project management E2E tests: * 1. Rename a project inline * 2. Delete a project * 3. Create a folder inside a project * 4. File upload type validation (wrong type rejected) * * Prerequisite: auth.setup.ts has already saved the session to e2e/.auth/user.json. * All tests run with the authenticated storageState configured in playwright.config.ts — * no test.use override is needed here. * * Each test creates its own uniquely-named project so tests are fully isolated. */ import { test, expect } from "@playwright/test"; import path from "path"; const PDF_FIXTURE = path.join(__dirname, "fixtures/test.pdf"); // ─── Shared helper ──────────────────────────────────────────────────────────── /** * Creates a new project via the "New project" modal and waits until * NewProjectModal's onCreated handler redirects to /projects/. * * Pass `filePath` to also upload a document during creation. This matters for * the folder test: ProjectPage only renders the document tree (and therefore * the root "Add Subfolder" input) when the project is NOT empty — an empty * project shows the "Drop PDF or DOCX files here" placeholder instead, which * has no folder input. */ async function createProject( page: import("@playwright/test").Page, projectName: string, filePath?: string, ) { /* These tests are throttled by the local Supabase stack, which the app hammers on every modal open (see the settle note below). The per-test `{ timeout }` option passed to test() is silently ignored by Playwright (that object only accepts tag/annotation), so the tests would otherwise run at the 30s default — too tight for the directory storm. Raise it here, where the slow work happens, so every caller gets the budget. The storm scales with the number of projects the test user has accumulated, and the modal now mounts FileDirectory on its second ("Add Documents") step, so creation alone can consume most of a 120s budget and starve whatever the caller does afterwards. */ test.setTimeout(180_000); await page.goto("/projects"); await expect(page).toHaveURL(/\/projects/, { timeout: 10_000 }); /* The Plus icon button in the header has aria-label="New project" */ const createBtn = page.getByRole("button", { name: "New project" }); await expect(createBtn).toBeVisible({ timeout: 10_000 }); await createBtn.click(); const nameInput = page.getByPlaceholder("Project name"); await expect(nameInput).toBeVisible({ timeout: 5_000 }); await nameInput.fill(projectName); /* NewProjectModal is a two-step wizard: "Details" (name / CM number / practice / colleagues) then "Add Documents". Only the second step has a submit button — the first step's primary action is a plain "Next". */ await page.getByRole("button", { name: "Next", exact: true }).click(); if (filePath) { /* On the documents step the footer "Upload" button opens a hidden file input, and its label gains a "(n)" count once files are attached. */ const fileChooserPromise = page.waitForEvent("filechooser"); await page.getByRole("button", { name: /^Upload/ }).click(); (await fileChooserPromise).setFiles(filePath); await expect( page.getByRole("button", { name: /^Upload \(1\)/ }), ).toBeVisible({ timeout: 5_000 }); } /* The modal mounts a FileDirectory whose useDirectoryData hook fires several Supabase-backed requests (listProjects + listStandaloneDocuments, plus a getProject per project) the moment the modal opens. Submitting while those are still in flight makes the local Supabase gateway (Kong) buckle under the concurrent load and return a 502 — "An invalid response was received from the upstream server" — which the API surfaces as a 500 and the modal shows inline (text-red-500). Letting the modal's requests settle first makes creation reliable (measured 6/6 vs 3/6 without this wait). useDirectoryData fires getProject() for EVERY existing project, so this storm grows with the project count and can take ~20s once many test projects have accumulated. Bound the wait so we never hang on it; if it doesn't fully settle, the submit retry below absorbs any residual 502. */ await page .waitForLoadState("networkidle", { timeout: 45_000 }) .catch(() => {}); /* Submit — NewProjectModal's onCreated calls router.push(`/projects/${id}`). The PDF upload runs (awaited) inside handleSubmit before onCreated fires, so allow extra time for navigation when a file is attached. Even with the settle above, a transient upstream 502 can still slip through occasionally. NewProjectModal surfaces it inline (text-red-500) while keeping the modal open and the form state intact, so re-clicking submit retries safely (the failed request created nothing). Race the navigation against that inline error so a failure is detected immediately and retried, rather than burning the full navigation timeout each time. */ const navTimeout = filePath ? 30_000 : 15_000; const inlineError = page.locator("form p.text-red-500"); for (let attempt = 1; attempt <= 5; attempt++) { /* The documents step's primary action submits the form (its label flips to "Creating…" while in flight, so match on the submit role instead). */ await page.locator('button[type="submit"]').click(); const outcome = await Promise.race([ page .waitForURL(/\/projects\/.+/, { timeout: navTimeout }) .then(() => "nav" as const) .catch(() => "timeout" as const), inlineError .waitFor({ state: "visible", timeout: navTimeout }) .then(() => "error" as const) .catch(() => "timeout" as const), ]); if (outcome === "nav") return; if (attempt === 5) { throw new Error( `createProject: never navigated to /projects/ (last outcome: ${outcome})`, ); } // transient upstream error (or stall) → wait for the inline message to // clear so the next iteration's race can't latch onto the stale one, // then resubmit. await inlineError .waitFor({ state: "hidden", timeout: 2_000 }) .catch(() => {}); } } /** * Navigate to the projects list and return the table row for `projectName`, * re-navigating (which refetches) if the list doesn't render it in time. * ProjectsOverview gates its table on listProjects(); under the local-Supabase * load that call can be slow or transiently 502 (the page then shows "Could not * load projects."), so a single goto isn't reliable. Rows are
; * the sidebar "Recent Projects" renders the same name as a