From 58fc323ea254477d2a40531aa10f577debddf07f Mon Sep 17 00:00:00 2001 From: Codex Review Integration Date: Wed, 16 Sep 2026 22:06:33 -0300 Subject: [PATCH 1/2] fix(work-orders): use local calendar day for Past Due and stabilize slow suites 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 --- .../_hooks/use-work-orders-table-day-view.ts | 3 +- .../work-orders/utils/table-filter-utils.ts | 7 +++- .../pm-schedules-list-actions.test.tsx | 7 ++-- .../vendors/vendor-create-modal.test.tsx | 28 ++++++++++---- .../vendors/vendor-detail-drawer.test.tsx | 24 ++++++++---- ...-adv-search-page-sync.integration.test.tsx | 20 ++++++---- .../utils/table-filter-utils.test.ts | 37 ++++++++++++++++++- 7 files changed, 96 insertions(+), 30 deletions(-) diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts index 3f15b3cb..07e2ce06 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts @@ -4,6 +4,7 @@ import { useWorkOrdersDayPagination } from "@/app/(protected)/workorders/_hooks/ import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; +import { todayIso } from "@/domain/work-orders/utils/wizard-date-utils"; import { pageSelectedDayGroups } from "@/domain/work-orders/utils/day-pagination"; import { buildSelectedDayGroup, @@ -71,7 +72,7 @@ export function useWorkOrdersTableDayView({ const isEmptyWeek = !advActive && scheduled.length === 0 && !searchActive; const todayKey = filters.weekMonday === currentWeekMonday() - ? deriveDayFields(new Date().toISOString().slice(0, 10)).dayGroup + ? deriveDayFields(todayIso()).dayGroup : null; return { diff --git a/src/domain/work-orders/utils/table-filter-utils.ts b/src/domain/work-orders/utils/table-filter-utils.ts index 4f2a045b..3d8eb132 100644 --- a/src/domain/work-orders/utils/table-filter-utils.ts +++ b/src/domain/work-orders/utils/table-filter-utils.ts @@ -10,6 +10,7 @@ import { ADVANCED_FILTER_PREDICATES } from "@/domain/work-orders/utils/advanced- import { isCarryOverDisplayRow } from "@/domain/work-orders/utils/carry-over-display"; import { isDateInWeek } from "@/domain/work-orders/utils/table-day-utils"; import { weekRange } from "@/domain/work-orders/utils/list-week-utils"; +import { todayIso } from "@/domain/work-orders/utils/wizard-date-utils"; const TERMINAL_STATUSES = new Set(["Completed", "Canceled", "Done", "Cancelled"]); @@ -17,8 +18,10 @@ export function computeIsPastDue(row: WorkOrderTableRow): boolean { if (!row.scheduledOn || TERMINAL_STATUSES.has(row.status)) { return false; } - const today = new Date().toISOString().slice(0, 10); - return row.scheduledOn.slice(0, 10) < today; + // scheduledOn is a calendar date, so compare it to the user's local calendar + // day. A UTC "today" rolls over hours early west of UTC and flags work + // scheduled for today as Past Due every evening. + return row.scheduledOn.slice(0, 10) < todayIso(); } export function enrichTableRow(row: WorkOrderTableRow): WorkOrderTableRow { diff --git a/src/test/app/(protected)/pmschedules/pm-schedules-list-actions.test.tsx b/src/test/app/(protected)/pmschedules/pm-schedules-list-actions.test.tsx index f26b9ea5..efb56316 100644 --- a/src/test/app/(protected)/pmschedules/pm-schedules-list-actions.test.tsx +++ b/src/test/app/(protected)/pmschedules/pm-schedules-list-actions.test.tsx @@ -1,6 +1,9 @@ import { screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import PmSchedulesListPage from "@/app/(protected)/pmschedules/index"; +// Static import: a dynamic import inside the test transforms the edit page's +// module graph against the 5s test budget, which times out under load. +import EditPage from "@/app/(protected)/pmschedules/edit/[id]"; import { renderWithProviders } from "@/test/test-utils"; const usePmSchedulesList = vi.fn(); @@ -75,9 +78,7 @@ describe("PmSchedulesListPage actions", () => { }); describe("PmScheduleEditUnavailablePage", () => { - it("shows unavailable message instead of edit form when canEdit is false", async () => { - const EditPage = (await import("@/app/(protected)/pmschedules/edit/[id]")).default; - + it("shows unavailable message instead of edit form when canEdit is false", () => { renderWithProviders(, { route: "/pmschedules/edit/1", routerProps: { initialEntries: ["/pmschedules/edit/1"] }, diff --git a/src/test/app/(protected)/vendors/vendor-create-modal.test.tsx b/src/test/app/(protected)/vendors/vendor-create-modal.test.tsx index 355f99f1..644fc329 100644 --- a/src/test/app/(protected)/vendors/vendor-create-modal.test.tsx +++ b/src/test/app/(protected)/vendors/vendor-create-modal.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { VendorCreateModal } from "@/app/(protected)/vendors/_components/vendor-create-modal"; import { VendorRosterFormFields } from "@/app/(protected)/vendors/_components/vendor-roster-form-fields"; import { @@ -49,6 +49,10 @@ function renderCreateModal() { }); } +// delay: null skips user-event's per-keystroke setTimeout yields, which only +// add real-timer latency here. +let user = userEvent.setup({ delay: null }); + async function fillCompanyNameAndSubmit(name = "Gateway Plumbing") { fireEvent.change(screen.getByRole("combobox", { name: "Company (required)" }), { target: { value: name }, @@ -57,7 +61,7 @@ async function fillCompanyNameAndSubmit(name = "Gateway Plumbing") { // phone-or-email rule, so the address is filled to keep the form's only // outstanding problem the one under test. fillRequiredAddress(); - await userEvent.click(screen.getByRole("button", { name: "Add Vendor" })); + await user.click(screen.getByRole("button", { name: "Add Vendor" })); } function fillRequiredAddress({ @@ -74,7 +78,15 @@ function fillRequiredAddress({ pickVendorState(state); } -describe("VendorCreateModal validation", () => { +// Each test mounts the full MUI dialog (Autocompletes, 50-option State list) +// and costs ~1s of CPU when idle; the first also pays JIT warm-up. Under +// parallel agent load the 5s default is exceeded by CPU starvation alone, +// matching the 10s budgets already used by vendors-list.test.tsx. +describe("VendorCreateModal validation", { timeout: 15_000 }, () => { + beforeEach(() => { + user = userEvent.setup({ delay: null }); + }); + it("explains the company phone-or-email requirement after submission", async () => { renderCreateModal(); await fillCompanyNameAndSubmit(); @@ -138,8 +150,8 @@ describe("VendorCreateModal validation", () => { const phone = screen.getByRole("textbox", { name: "Company Phone (optional)" }); const email = screen.getByRole("textbox", { name: "Email (optional)" }); - await userEvent.type(phone, "314"); - await userEvent.click(screen.getByRole("button", { name: "Add Vendor" })); + await user.type(phone, "314"); + await user.click(screen.getByRole("button", { name: "Add Vendor" })); expect(await screen.findByText("Enter a 10-digit phone number")).toBeInTheDocument(); expect(phone).toHaveAttribute("aria-invalid", "true"); @@ -153,8 +165,8 @@ describe("VendorCreateModal validation", () => { const phone = screen.getByRole("textbox", { name: "Company Phone (optional)" }); const email = screen.getByRole("textbox", { name: "Email (optional)" }); - await userEvent.type(email, "not-an-email"); - await userEvent.click(screen.getByRole("button", { name: "Add Vendor" })); + await user.type(email, "not-an-email"); + await user.click(screen.getByRole("button", { name: "Add Vendor" })); expect(await screen.findByText("Invalid email")).toBeInTheDocument(); expect(email).toHaveAttribute("aria-invalid", "true"); @@ -169,7 +181,7 @@ describe("VendorCreateModal validation", () => { expect(await screen.findByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).toBeInTheDocument(); - await userEvent.type(phone, "3145550100"); + await user.type(phone, "3145550100"); await waitFor(() => expect(screen.queryByText(VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE)).not.toBeInTheDocument(), ); diff --git a/src/test/app/(protected)/vendors/vendor-detail-drawer.test.tsx b/src/test/app/(protected)/vendors/vendor-detail-drawer.test.tsx index 0da2e959..0e5ea3eb 100644 --- a/src/test/app/(protected)/vendors/vendor-detail-drawer.test.tsx +++ b/src/test/app/(protected)/vendors/vendor-detail-drawer.test.tsx @@ -68,11 +68,19 @@ function rosterWith(technicians: Array>) { }; } +// delay: null skips user-event's per-keystroke setTimeout yields, which only +// add real-timer latency here. +let user = userEvent.setup({ delay: null }); + beforeEach(() => { saveMutate.mockReset(); + user = userEvent.setup({ delay: null }); }); -describe("VendorDetailDrawer selected-technician display", () => { +// The edit-mode drawer is a heavy MUI render and the first test pays JIT +// warm-up; under parallel agent load the 5s default is exceeded by CPU +// starvation alone, matching the 10s budgets in vendors-list.test.tsx. +describe("VendorDetailDrawer selected-technician display", { timeout: 15_000 }, () => { it.each([ { removedPosition: 1, remainingId: 2, remainingName: "Beth" }, { removedPosition: 2, remainingId: 1, remainingName: "Adam" }, @@ -107,10 +115,10 @@ describe("VendorDetailDrawer selected-technician display", () => { { route: "/vendors", withAuth: false }, ); - await userEvent.click( + await user.click( screen.getByRole("button", { name: `Remove technician ${removedPosition}` }), ); - await userEvent.click(screen.getByRole("button", { name: "Save changes" })); + await user.click(screen.getByRole("button", { name: "Save changes" })); await waitFor(() => expect(saveMutate).toHaveBeenCalledTimes(1)); expect(saveMutate).toHaveBeenCalledWith( @@ -161,12 +169,12 @@ describe("VendorDetailDrawer selected-technician display", () => { { route: "/vendors", withAuth: false }, ); - await userEvent.click(screen.getByRole("button", { name: "Remove technician 2" })); + await user.click(screen.getByRole("button", { name: "Remove technician 2" })); const save = screen.getByRole("button", { name: "Save changes" }); expect(save).toBeEnabled(); - await userEvent.click(save); + await user.click(save); expect( await screen.findByText("Provide a company phone or email (at least one required)"), @@ -444,10 +452,10 @@ describe("VendorDetailDrawer deactivation prompt", () => { ); const toggle = screen.getByRole("switch", { name: "Active status" }); - await userEvent.click(toggle); + await user.click(toggle); expect(toggle).toBeChecked(); - await userEvent.click(toggle); + await user.click(toggle); expect(toggle).not.toBeChecked(); expect(onRequestDeactivation).not.toHaveBeenCalled(); @@ -472,7 +480,7 @@ describe("VendorDetailDrawer deactivation prompt", () => { { route: "/vendors", withAuth: false }, ); - await userEvent.click(screen.getByRole("switch", { name: "Active status" })); + await user.click(screen.getByRole("switch", { name: "Active status" })); expect(onRequestDeactivation).toHaveBeenCalledWith(vendor); }); diff --git a/src/test/app/(protected)/workorders/wo-table-adv-search-page-sync.integration.test.tsx b/src/test/app/(protected)/workorders/wo-table-adv-search-page-sync.integration.test.tsx index 1ef0734c..4ca3e8d8 100644 --- a/src/test/app/(protected)/workorders/wo-table-adv-search-page-sync.integration.test.tsx +++ b/src/test/app/(protected)/workorders/wo-table-adv-search-page-sync.integration.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, screen, waitFor } from "@testing-library/react"; +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"; @@ -120,6 +120,12 @@ function pagedResult(rows: WorkOrderTableRow[], page: number, pageSize: number) }; } +// 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; @@ -213,14 +219,14 @@ describe("advanced search page sync after mutation", () => { ); }); expect(screen.getByText("WO-0")).toBeInTheDocument(); - expect(screen.getByText("Page 1 of 2")).toBeInTheDocument(); + expect(pagination().getByText("Page 1 of 2")).toBeInTheDocument(); expect(screen.queryByText("WO-25")).not.toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "Next page" })); + 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(screen.getByText("Page 2 of 2")).toBeInTheDocument(); + expect(pagination().getByText("Page 2 of 2")).toBeInTheDocument(); }); const callsBeforeMutation = searchBoard.mock.calls.length; @@ -236,12 +242,12 @@ describe("advanced search page sync after mutation", () => { expect(screen.getByTestId("harness-state")).toHaveTextContent( "adv=true rows=20 total=20 page=0", ); - expect(screen.getByText("Page 1 of 1")).toBeInTheDocument(); + 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(screen.getByRole("button", { name: "Previous page" })).toBeDisabled(); - expect(screen.getByRole("button", { name: "Next page" })).toBeDisabled(); + expect(pagination().getByRole("button", { name: "Previous page" })).toBeDisabled(); + expect(pagination().getByRole("button", { name: "Next page" })).toBeDisabled(); }, 15000); }); diff --git a/src/test/domain/work-orders/utils/table-filter-utils.test.ts b/src/test/domain/work-orders/utils/table-filter-utils.test.ts index a2e1f2eb..022d36fe 100644 --- a/src/test/domain/work-orders/utils/table-filter-utils.test.ts +++ b/src/test/domain/work-orders/utils/table-filter-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { EMPTY_UPLIFT_SUMMARY } from "@/domain/work-orders/types/work-order-uplift"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { EMPTY_ADVANCED_FILTERS } from "@/domain/work-orders/types/work-order-advanced-filters"; @@ -526,6 +526,41 @@ describe("computeIsPastDue", () => { }); }); +describe("computeIsPastDue across the local/UTC day boundary", () => { + beforeEach(() => { + // UTC-3 with no DST: local evening is already the next day in UTC. + vi.stubEnv("TZ", "America/Sao_Paulo"); + vi.useFakeTimers({ toFake: ["Date"] }); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllEnvs(); + }); + + it("does not flag today's work as Past Due at 23:30 local (02:30 UTC next day)", () => { + vi.setSystemTime(new Date("2026-09-17T02:30:00.000Z")); + + expect(computeIsPastDue(makeRow({ scheduledOn: "2026-09-16", status: "Scheduled" }))).toBe( + false, + ); + expect(computeIsPastDue(makeRow({ scheduledOn: "2026-09-15", status: "Scheduled" }))).toBe( + true, + ); + }); + + it("flags yesterday's work as Past Due at 00:30 local", () => { + vi.setSystemTime(new Date("2026-09-17T03:30:00.000Z")); + + expect(computeIsPastDue(makeRow({ scheduledOn: "2026-09-16", status: "Scheduled" }))).toBe( + true, + ); + expect(computeIsPastDue(makeRow({ scheduledOn: "2026-09-17", status: "Scheduled" }))).toBe( + false, + ); + }); +}); + describe("enrichTableRow", () => { it("recomputes isPastDue from scheduledOn and ignores sticky API true", () => { const enriched = enrichTableRow( From d1b36ed1c02815bbd34157ba1272062050e90917 Mon Sep 17 00:00:00 2001 From: Codex Review Integration Date: Wed, 16 Sep 2026 22:21:17 -0300 Subject: [PATCH 2/2] style(work-orders): apply prettier to day-view hook --- .../workorders/_hooks/use-work-orders-table-day-view.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts index 07e2ce06..42b3bde9 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-table-day-view.ts @@ -71,9 +71,7 @@ export function useWorkOrdersTableDayView({ }); const isEmptyWeek = !advActive && scheduled.length === 0 && !searchActive; const todayKey = - filters.weekMonday === currentWeekMonday() - ? deriveDayFields(todayIso()).dayGroup - : null; + filters.weekMonday === currentWeekMonday() ? deriveDayFields(todayIso()).dayGroup : null; return { scheduled,