mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 01:52:04 +00:00
computeIsPastDue and the day view's todayKey derived today from
toISOString() (UTC). West of UTC that rolls over hours early, so work
scheduled for today showed as Past Due every evening. Both now use the
local todayIso() helper the board already uses; regression tests pin
23:30 and 00:30 in America/Sao_Paulo with fake Date.
Test stability:
- adv-search page sync: scope pagination queries with within() instead of
scanning ~330 buttons / 3000 nodes on every waitFor poll
- pm-schedules-list-actions: static import of the edit page so module
transform does not count against the 5s test budget
- vendor create modal / detail drawer: userEvent.setup({ delay: null });
15s describe budget for full MUI dialog renders under parallel load
253 lines
8.2 KiB
TypeScript
253 lines
8.2 KiB
TypeScript
import { fireEvent, screen, waitFor, within } 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,
|
|
};
|
|
}
|
|
|
|
// Role and text queries over the whole board scan every row cell; scoping them
|
|
// to the pagination footer keeps each waitFor poll cheap on a loaded machine.
|
|
function pagination() {
|
|
return within(screen.getByTestId("wo-day-pagination"));
|
|
}
|
|
|
|
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(pagination().getByText("Page 1 of 2")).toBeInTheDocument();
|
|
expect(screen.queryByText("WO-25")).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(pagination().getByRole("button", { name: "Next page" }));
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("harness-state")).toHaveTextContent("page=1");
|
|
expect(screen.getByText("WO-25")).toBeInTheDocument();
|
|
expect(pagination().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(pagination().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(pagination().getByRole("button", { name: "Previous page" })).toBeDisabled();
|
|
expect(pagination().getByRole("button", { name: "Next page" })).toBeDisabled();
|
|
}, 15000);
|
|
});
|