diff --git a/e2e/work-orders/work-orders.visual.spec.ts b/e2e/work-orders/work-orders.visual.spec.ts index 547723a6..17f8d49b 100644 --- a/e2e/work-orders/work-orders.visual.spec.ts +++ b/e2e/work-orders/work-orders.visual.spec.ts @@ -204,6 +204,12 @@ async function openWorkOrderPage(page: Page, mode: "default" | "empty" | "error" }); } +async function expectWorkOrderPageReady(page: Page) { + await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ + timeout: 90_000, + }); +} + async function expectStableScreenshot(page: Page, name: string) { await page.waitForTimeout(250); await page.evaluate( @@ -217,15 +223,18 @@ async function expectStableScreenshot(page: Page, name: string) { } test.describe("Work Orders deterministic pixel regression", () => { + test.setTimeout(120_000); + test("list", async ({ page }) => { await openWorkOrderPage(page); - await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible(); + await expectWorkOrderPageReady(page); await expect(page.getByText("WO-501").first()).toBeVisible(); await expectStableScreenshot(page, "wo-list.png"); }); test("filters", async ({ page }) => { await openWorkOrderPage(page); + await expectWorkOrderPageReady(page); await page.getByRole("button", { name: "Advanced Filters" }).click(); await expect(page.getByRole("dialog", { name: "Advanced Filters" })).toBeVisible(); await expectStableScreenshot(page, "wo-filters.png"); @@ -233,6 +242,7 @@ test.describe("Work Orders deterministic pixel regression", () => { test("new", async ({ page }) => { await openWorkOrderPage(page); + await expectWorkOrderPageReady(page); await page.getByRole("button", { name: "New WO" }).click(); await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeVisible(); await expectStableScreenshot(page, "wo-new.png"); @@ -240,6 +250,7 @@ test.describe("Work Orders deterministic pixel regression", () => { test("detail", async ({ page }) => { await openWorkOrderPage(page); + await expectWorkOrderPageReady(page); const row = page.locator("#wo-row-1"); await row.hover(); await row.getByRole("button", { name: "View details" }).click(); @@ -249,7 +260,7 @@ test.describe("Work Orders deterministic pixel regression", () => { test("empty", async ({ page }) => { await openWorkOrderPage(page, "empty"); - await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible(); + await expectWorkOrderPageReady(page); await page.getByLabel("Search work orders").fill("zz"); await expect(page.getByText("No work orders match your search")).toBeVisible(); await expectStableScreenshot(page, "wo-empty.png"); @@ -257,7 +268,7 @@ test.describe("Work Orders deterministic pixel regression", () => { test("error", async ({ page }) => { await openWorkOrderPage(page, "error"); - await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible(); + await expectWorkOrderPageReady(page); const alert = page.getByRole("main").getByRole("alert"); await expect(alert).toBeVisible(); await expect(alert).toContainText(/server error/i); @@ -267,6 +278,7 @@ test.describe("Work Orders deterministic pixel regression", () => { test("mobile", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await openWorkOrderPage(page); + await expectWorkOrderPageReady(page); await expect(page.getByText("WO-501").first()).toBeVisible(); await expectStableScreenshot(page, "wo-mobile.png"); diff --git a/src/app/(protected)/team-members/_components/team-member-detail-drawer.tsx b/src/app/(protected)/team-members/_components/team-member-detail-drawer.tsx index 58bdf118..c5014fcb 100644 --- a/src/app/(protected)/team-members/_components/team-member-detail-drawer.tsx +++ b/src/app/(protected)/team-members/_components/team-member-detail-drawer.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, + useRef, useState, type Dispatch, type ReactNode, @@ -264,14 +265,20 @@ function useTeamMemberEditor( const [form, setForm] = useState(emptyForm); const [submitted, setSubmitted] = useState(false); const updateTeamMember = useUpdateTeamMember(); + const currentMemberRef = useRef(currentMember); useEffect(() => { - if (currentMember) { - setForm(formFromMember(currentMember)); + currentMemberRef.current = currentMember; + }, [currentMember]); + + useEffect(() => { + const member = currentMemberRef.current; + if (member) { + setForm(formFromMember(member)); setEditing(false); setSubmitted(false); } - }, [currentMember]); + }, [currentMember?.id]); useEffect(() => { setEditing(false); @@ -351,7 +358,11 @@ function useTeamMemberEditor( toggleArea, togglePermission, save, - cancel: () => setEditing(false), + cancel: () => { + if (currentMember) setForm(formFromMember(currentMember)); + setSubmitted(false); + setEditing(false); + }, beginEdit: () => setEditing(true), }; } diff --git a/src/app/(protected)/team-members/_components/team-members-table.tsx b/src/app/(protected)/team-members/_components/team-members-table.tsx index b608b9de..777aeea4 100644 --- a/src/app/(protected)/team-members/_components/team-members-table.tsx +++ b/src/app/(protected)/team-members/_components/team-members-table.tsx @@ -22,11 +22,13 @@ function memberStatusLabel(member: TeamMemberListItem) { export function TeamMembersTable({ rows, isLoading, + hasError, tab, onOpen, }: { rows: TeamMemberListItem[]; isLoading: boolean; + hasError: boolean; tab: "active" | "inactive"; onOpen: (member: TeamMemberListItem) => void; }) { @@ -52,6 +54,15 @@ export function TeamMembersTable({ + ) : hasError ? ( + + + Unable to load team members + + Check your connection and try again. + + + ) : rows.length === 0 ? ( diff --git a/src/app/(protected)/team-members/index.tsx b/src/app/(protected)/team-members/index.tsx index 36f2be91..02e9c2ab 100644 --- a/src/app/(protected)/team-members/index.tsx +++ b/src/app/(protected)/team-members/index.tsx @@ -85,7 +85,13 @@ export default function TeamMembersListPage() { )} - + { ), ); }); + + it("keeps edits when the detail query refreshes", async () => { + const user = userEvent.setup(); + const { rerender } = renderWithProviders( + , + { withAuth: false }, + ); + + await user.click(screen.getByRole("button", { name: "Edit" })); + const name = screen.getByRole("textbox", { name: "Name" }); + await user.clear(name); + await user.type(name, "Taylor Updated"); + + vi.mocked(useTeamMemberDetail).mockReturnValue({ + data: { ...detail, phone: "555-0101" }, + isLoading: false, + error: null, + } as unknown as ReturnType); + rerender(); + + expect(screen.getByRole("textbox", { name: "Name" })).toHaveValue("Taylor Updated"); + expect(screen.getByRole("textbox", { name: "Phone" })).toHaveValue("555-0100"); + }); + + it("restores the saved detail when editing is cancelled", async () => { + const user = userEvent.setup(); + renderWithProviders(, { + withAuth: false, + }); + + await user.click(screen.getByRole("button", { name: "Edit" })); + const name = screen.getByRole("textbox", { name: "Name" }); + await user.clear(name); + await user.type(name, "Discarded draft"); + await user.click(screen.getByRole("button", { name: "Cancel" })); + + expect(screen.getByRole("button", { name: "Edit" })).toBeInTheDocument(); + expect(screen.getAllByText("Taylor Dispatcher")).not.toHaveLength(0); + expect(screen.queryByDisplayValue("Discarded draft")).not.toBeInTheDocument(); + }); });