fix(test): wire SendQuoteModal stub to props.onSend

The SendQuoteModal test stub had a noop triggerSend placeholder that
never called props.onSend, so the click on the modal's "Submit modal"
button would silently fail. Tests 3-7 (which assert on fetch /
refresh / toast effects from sendApprove) would all hang even after
Task 2 implements the page correctly.

Fix: call props.onSend directly from the button's onClick. Drop the
unused triggerSend hoisted spy. Tighten the stub's props type to
SendQuoteModalProps so Task 2's prop-name typos surface as TS errors.

Also widen the PB mock fallback's getList to return the full envelope
(items + totalItems + totalPages) so Task 2 reading quote_attachments
or any other collection doesn't crash on undefined totalItems.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dave 2026-08-27 00:46:34 +02:00
parent 097ada09f8
commit 27f60e14ec

View file

@ -1,4 +1,5 @@
import { describe, it, expect, beforeEach, vi } from "vitest"; import { describe, it, expect, beforeEach, vi } from "vitest";
import * as React from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import ApprovalsPage from "./page"; import ApprovalsPage from "./page";
@ -9,13 +10,11 @@ const {
refreshMock, refreshMock,
pushMock, pushMock,
captureModalProps, captureModalProps,
triggerSend,
} = vi.hoisted(() => ({ } = vi.hoisted(() => ({
getListMock: vi.fn(), getListMock: vi.fn(),
refreshMock: vi.fn(), refreshMock: vi.fn(),
pushMock: vi.fn(), pushMock: vi.fn(),
captureModalProps: vi.fn(), captureModalProps: vi.fn(),
triggerSend: vi.fn(),
})); }));
// Stub the PocketBase client so we can drive list contents from the test. // Stub the PocketBase client so we can drive list contents from the test.
@ -29,7 +28,14 @@ vi.mock("@/lib/pocketbase", () => ({
if (name === "quote_items") { if (name === "quote_items") {
return { getFullList: async () => [] }; return { getFullList: async () => [] };
} }
return { getFullList: async () => [], getList: async () => ({ items: [] }) }; // Defensive fallback for any other collection the page or
// SendQuoteModal may read in Tasks 2-3 (e.g. quote_attachments,
// users). Match the PB envelope shape so code that consumes
// totalItems / totalPages doesn't crash on undefined.
return {
getFullList: async () => [],
getList: async () => ({ items: [], totalItems: 0, totalPages: 1 }),
};
}, },
}), }),
})); }));
@ -40,15 +46,16 @@ vi.mock("next/navigation", () => ({
})); }));
// Replace SendQuoteModal with a thin stub. The test grabs the props the // Replace SendQuoteModal with a thin stub. The test grabs the props the
// page passed in via captureModalProps; triggerSend becomes the onSend // page passed in via captureModalProps; clicking the test's "Submit
// prop, so tests can fire the modal submit callback directly. // modal" button calls props.onSend directly, which is the page's
// sendApprove callback — so the fetch / refresh / toast assertions
// downstream observe real effects.
type SendQuoteModalProps = React.ComponentProps<
typeof import("@/components/quotes/send-quote-modal").SendQuoteModal
>;
vi.mock("@/components/quotes/send-quote-modal", () => ({ vi.mock("@/components/quotes/send-quote-modal", () => ({
SendQuoteModal: (props: { SendQuoteModal: (props: SendQuoteModalProps) => {
open: boolean;
submitLabel?: string;
onSend: (p: unknown) => Promise<void>;
quote: { id: string };
}) => {
captureModalProps(props); captureModalProps(props);
if (!props.open) return null; if (!props.open) return null;
return ( return (
@ -58,7 +65,7 @@ vi.mock("@/components/quotes/send-quote-modal", () => ({
<button <button
type="button" type="button"
onClick={() => onClick={() =>
triggerSend({ props.onSend({
to: "buyer@example.com", to: "buyer@example.com",
cc: "", cc: "",
bcc: "", bcc: "",