test(work-orders): cover search page sync after a shrinking mutation

This commit is contained in:
Arthur Bassi 2026-08-25 14:10:28 -03:00
parent 35d34866af
commit b317cdc718

View file

@ -0,0 +1,247 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { WorkOrderBoardSearchQueryParams } from "@/domain/work-orders/types/work-order-board";
import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { renderWithProviders } from "@/test/test-utils";
const { searchBoard, getBoard, patchBoardField, emptyList } = vi.hoisted(() => ({
searchBoard: vi.fn(),
getBoard: vi.fn(),
patchBoardField: vi.fn(),
emptyList: [] as const,
}));
vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
workOrdersApi: {
searchBoard: (...args: unknown[]) => searchBoard(...args),
getBoard: (...args: unknown[]) => getBoard(...args),
patchBoardField: (...args: unknown[]) => patchBoardField(...args),
},
}));
vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({
useWorkOrderLocations: () => ({ data: emptyList }),
useWorkOrderVendors: () => ({ data: emptyList }),
}));
vi.mock("@/domain/locations/use-cases/use-location-sites", () => ({
useLocationSites: () => ({ data: emptyList }),
}));
vi.mock("@/app/(protected)/workorders/_components/list/table/use-wo-table-inline-create", () => ({
useWoTableInlineCreate: () => ({ handleInlineSave: vi.fn(), saving: false }),
}));
vi.mock("react-toastify", () => ({ toast: { error: vi.fn(), success: vi.fn() } }));
import { WoTable } from "@/app/(protected)/workorders/_components/list/table/wo-table";
import { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
import { useWorkOrdersTableData } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-data";
import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
const PAGE_SIZE = 25;
const INITIAL_TOTAL = 26;
const SHRUNK_TOTAL = 20;
function makeRow(overrides: Partial<WorkOrderTableRow> = {}): WorkOrderTableRow {
return {
id: "1",
woNumber: "WO-1",
site: "CLV2",
type: "PM",
severity: null,
dispatcherId: "",
dispatcherName: "",
scheduledOn: "2026-08-17",
completedDate: "",
targetWeek: "",
dayGroup: "",
dayLabel: "",
location: "Cleveland, OH",
locationId: "",
pm: "Backflow",
serviceNotes: "",
extraServices: [],
company: "",
tech: "",
techPhone: "",
apptStart: "",
apptEnd: "",
apptTime: "—",
status: "Incomplete",
docStatus: "No",
pocName: "",
pocPhone: "",
pocNotes: "",
dueDate: "",
rescheduleCount: 0,
carriedOver: 0,
isAddOn: false,
originalWeek: "",
originalDate: "",
isPastDue: false,
scheduleWeekOnly: false,
mediaCount: 0,
title: "",
priority: "",
vendorId: "",
primaryDispatchId: null,
rowVersion: "v1",
dispatchRowVersion: "",
flagColor: null,
additionalContacts: [],
...overrides,
};
}
function emptyBoardResult() {
return {
items: [] as WorkOrderTableRow[],
scheduled: [] as WorkOrderTableRow[],
unscheduled: [] as WorkOrderTableRow[],
totalCount: 0,
returnedCount: 0,
page: 0,
pageSize: PAGE_SIZE,
};
}
function pagedResult(rows: WorkOrderTableRow[], page: number, pageSize: number) {
const items = rows.slice(page * pageSize, page * pageSize + pageSize);
return {
items,
scheduled: items,
unscheduled: [] as WorkOrderTableRow[],
totalCount: rows.length,
returnedCount: items.length,
page,
pageSize,
};
}
function pagesAfter(callCount: number): number[] {
return searchBoard.mock.calls.slice(callCount).map((call) => {
const params = call[0] as WorkOrderBoardSearchQueryParams;
return params.page ?? 0;
});
}
function AdvSearchPageSyncHarness() {
const filters = useWorkOrdersListFilters();
const tableData = useWorkOrdersTableData(filters);
const mutations = useWorkOrderTableMutations({
onPatch: tableData.patchRow,
clearPatch: tableData.clearRowPatch,
getRow: (id) => tableData.rows.find((row) => String(row.id) === String(id)),
});
return (
<div>
<div data-testid="harness-state">
{`adv=${String(tableData.advActive)} rows=${tableData.advResults.length} total=${tableData.scopeTotalCount} page=${filters.page}`}
</div>
<button
type="button"
onClick={() => {
filters.setPageSize(PAGE_SIZE);
filters.applyAdvancedFilters({ ...EMPTY_ADVANCED_FILTERS, rescheduled: true });
}}
>
Apply advanced filters
</button>
<button
type="button"
onClick={() => {
const row = tableData.rows[0];
if (row) {
void mutations.patchFieldAsync(row.id, { serviceNotes: "after-mutation" });
}
}}
>
Save row patch
</button>
<WoTable
tableData={tableData}
users={[]}
sites={[]}
vendors={[]}
pmTypes={[]}
weekMonday={filters.weekMonday}
onNavigateToDay={filters.navigateToDay}
onOpenSlideOver={() => undefined}
onOpenMedia={() => undefined}
onPatch={(id, patch) => {
void mutations.patchFieldAsync(id, patch);
}}
/>
</div>
);
}
describe("advanced search page sync after mutation", () => {
let serverRows: WorkOrderTableRow[];
beforeEach(() => {
serverRows = Array.from({ length: INITIAL_TOTAL }, (_, index) =>
makeRow({ id: `wo-${index}`, woNumber: `WO-${index}` }),
);
searchBoard.mockReset();
getBoard.mockReset();
patchBoardField.mockReset();
getBoard.mockResolvedValue(emptyBoardResult());
searchBoard.mockImplementation(async (params: WorkOrderBoardSearchQueryParams) =>
pagedResult(serverRows, params.page ?? 0, params.pageSize ?? PAGE_SIZE),
);
patchBoardField.mockImplementation(async (id: string | number) => {
serverRows = serverRows.slice(0, SHRUNK_TOTAL);
return makeRow({
id,
woNumber: `WO-${String(id).replace("wo-", "")}`,
serviceNotes: "after-mutation",
});
});
});
it("refetches the last valid search page after a mutation shrinks results to one page", async () => {
renderWithProviders(<AdvSearchPageSyncHarness />, { withAuth: false });
fireEvent.click(screen.getByRole("button", { name: "Apply advanced filters" }));
await waitFor(() => {
expect(screen.getByTestId("harness-state")).toHaveTextContent(
"adv=true rows=25 total=26 page=0",
);
});
expect(screen.getByText("WO-0")).toBeInTheDocument();
expect(screen.getByText("Page 1 of 2")).toBeInTheDocument();
expect(screen.queryByText("WO-25")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Next page" }));
await waitFor(() => {
expect(screen.getByTestId("harness-state")).toHaveTextContent("page=1");
expect(screen.getByText("WO-25")).toBeInTheDocument();
expect(screen.getByText("Page 2 of 2")).toBeInTheDocument();
});
const callsBeforeMutation = searchBoard.mock.calls.length;
fireEvent.click(screen.getByRole("button", { name: "Save row patch" }));
await waitFor(() => {
expect(patchBoardField).toHaveBeenCalled();
expect(pagesAfter(callsBeforeMutation)).toContain(1);
expect(pagesAfter(callsBeforeMutation)).toContain(0);
});
await waitFor(() => {
expect(screen.getByTestId("harness-state")).toHaveTextContent(
"adv=true rows=20 total=20 page=0",
);
expect(screen.getByText("Page 1 of 1")).toBeInTheDocument();
});
expect(screen.getByText("WO-0")).toBeInTheDocument();
expect(screen.queryByText("WO-25")).not.toBeInTheDocument();
expect(screen.getByText("RESULTS · 20 items")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Next page" })).toBeDisabled();
}, 15000);
});