193 lines
6.6 KiB
TypeScript
193 lines
6.6 KiB
TypeScript
// @vitest-environment happy-dom
|
|
// AckDrawer wires `useAckDetail` (TanStack Query) and renders a Radix
|
|
// Dialog portal — both need an act-aware, DOM-backed environment or
|
|
// React logs warnings and the portal can't mount.
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
import { cleanup, render } from "@testing-library/react";
|
|
import { ApiError } from "@/lib/api";
|
|
import { AckDrawer } from "@/components/AckDrawer";
|
|
import type { Ack } from "@/types";
|
|
|
|
// Mock the hook BEFORE the import above is resolved (vitest hoists
|
|
// `vi.mock` to the top of the file regardless of where it appears
|
|
// syntactically). Mocking the hook directly — rather than mocking
|
|
// `api.getAck` — lets each test pin the hook's exact return shape
|
|
// without standing up a real `QueryClient`.
|
|
const { useAckDetail } = vi.hoisted(() => ({
|
|
useAckDetail: vi.fn(),
|
|
}));
|
|
vi.mock("@/hooks/useAckDetail", () => ({
|
|
useAckDetail,
|
|
}));
|
|
|
|
/**
|
|
* Minimal valid `Ack` fixture — every required key present so the
|
|
* component typechecks. The wire shape extends with `raw_999_text`
|
|
* (and `rawJson`), per `useAckDetail`'s `AckDetail` type — populated
|
|
* when the backend serves it; absent on older rows.
|
|
*/
|
|
const SAMPLE_ACK: Ack & { raw_999_text: string } = {
|
|
id: 42,
|
|
sourceBatchId: "b-uuid-1",
|
|
acceptedCount: 3,
|
|
rejectedCount: 1,
|
|
receivedCount: 4,
|
|
ackCode: "P",
|
|
parsedAt: "2026-06-20T12:00:00Z",
|
|
raw_999_text: "ISA*...*~\nGS*...*~\nST*999*0001~",
|
|
};
|
|
|
|
/**
|
|
* Configure the mocked hook's return value for a single test. The
|
|
* `refetch` default is a fresh `vi.fn()` — tests that need to assert
|
|
* on it can override via `overrides.refetch`.
|
|
*/
|
|
function mockDetail(
|
|
overrides: Partial<{
|
|
data: (Ack & { raw_999_text?: string }) | null;
|
|
isLoading: boolean;
|
|
isError: boolean;
|
|
error: Error | null;
|
|
refetch: () => void;
|
|
}> = {}
|
|
) {
|
|
useAckDetail.mockReturnValue({
|
|
data: null,
|
|
isLoading: false,
|
|
isError: false,
|
|
error: null,
|
|
refetch: vi.fn(),
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
// happy-dom keeps `document.body` between tests; without cleanup,
|
|
// `screen.getByText(...)` would find nodes from earlier renders.
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("AckDrawer", () => {
|
|
it("test_renders_nothing_when_ackId_is_null", () => {
|
|
mockDetail({ data: null });
|
|
render(<AckDrawer ackId={null} onClose={() => {}} />);
|
|
|
|
// No ack content should be in the document when the drawer is
|
|
// closed — Radix's Dialog gates the portal on `open`.
|
|
expect(document.body.textContent).not.toContain("b-uuid-1");
|
|
});
|
|
|
|
it("test_calls_useAckDetail_with_ackId", () => {
|
|
mockDetail({ data: SAMPLE_ACK });
|
|
render(<AckDrawer ackId="42" onClose={() => {}} />);
|
|
|
|
expect(useAckDetail).toHaveBeenCalledWith("42");
|
|
});
|
|
|
|
it("test_renders_ack_summary_on_success", () => {
|
|
mockDetail({ data: SAMPLE_ACK });
|
|
render(<AckDrawer ackId="42" onClose={() => {}} />);
|
|
|
|
// Header shows the source batch id as the title.
|
|
expect(document.body.textContent).toContain("b-uuid-1");
|
|
// Counts (3 accepted, 1 rejected, 4 received) are surfaced as
|
|
// StatTile values.
|
|
expect(document.body.textContent).toContain("3");
|
|
expect(document.body.textContent).toContain("1");
|
|
expect(document.body.textContent).toContain("4");
|
|
});
|
|
|
|
it("test_renders_ack_code_pill_with_human_label", () => {
|
|
mockDetail({ data: { ...SAMPLE_ACK, ackCode: "P" } });
|
|
render(<AckDrawer ackId="42" onClose={() => {}} />);
|
|
|
|
// The pill renders a human label, not just the bare code letter.
|
|
expect(document.body.textContent).toContain("Partially accepted");
|
|
});
|
|
|
|
it("test_renders_skeleton_while_loading", () => {
|
|
mockDetail({ isLoading: true });
|
|
render(<AckDrawer ackId="42" onClose={() => {}} />);
|
|
|
|
// The `Skeleton` primitive sets `aria-busy="true"` — a stable
|
|
// hook for the loading state.
|
|
expect(document.querySelectorAll('[aria-busy="true"]').length).toBeGreaterThan(0);
|
|
// And the ack id should NOT have leaked in yet.
|
|
expect(document.body.textContent).not.toContain("b-uuid-1");
|
|
});
|
|
|
|
it("test_renders_not_found_error_on_404", () => {
|
|
mockDetail({
|
|
isError: true,
|
|
error: new ApiError(404, "Ack ghost not found"),
|
|
});
|
|
render(<AckDrawer ackId="9999" onClose={() => {}} />);
|
|
|
|
const errEl = document.querySelector('[data-testid="ack-drawer-error-not_found"]');
|
|
expect(errEl).not.toBeNull();
|
|
// Body should mention "doesn't exist" — the not_found COPY key.
|
|
expect(errEl?.textContent).toContain("doesn't exist");
|
|
// And the retry button should NOT be present (not_found has no
|
|
// retry affordance — retrying a 404 won't help).
|
|
expect(document.querySelector('[data-testid="error-retry"]')).toBeNull();
|
|
});
|
|
|
|
it("test_renders_network_error_with_retry", () => {
|
|
mockDetail({ isError: true, error: new Error("network down") });
|
|
render(<AckDrawer ackId="42" onClose={() => {}} />);
|
|
|
|
const errEl = document.querySelector('[data-testid="ack-drawer-error-network"]');
|
|
expect(errEl).not.toBeNull();
|
|
expect(errEl?.textContent).toContain("Couldn't reach the server");
|
|
|
|
// Network variant shows a Retry button.
|
|
const retryBtn = document.querySelector(
|
|
'[data-testid="error-retry"]'
|
|
) as HTMLButtonElement | null;
|
|
expect(retryBtn).not.toBeNull();
|
|
});
|
|
|
|
it("test_close_button_calls_onClose", () => {
|
|
const onClose = vi.fn<() => void>();
|
|
mockDetail({ isError: true, error: new Error("network down") });
|
|
render(<AckDrawer ackId="42" onClose={onClose} />);
|
|
|
|
const closeBtn = document.querySelector(
|
|
'[data-testid="error-close"]'
|
|
) as HTMLButtonElement | null;
|
|
expect(closeBtn).not.toBeNull();
|
|
closeBtn!.click();
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("test_renders_download_button_when_raw_999_text_present", () => {
|
|
mockDetail({ data: SAMPLE_ACK });
|
|
render(<AckDrawer ackId="42" onClose={() => {}} />);
|
|
|
|
// The header action slot populates with the Download 999 button
|
|
// only when the ack detail carries raw_999_text.
|
|
const dlBtn = document.querySelector('[data-testid="ack-drawer-download"]');
|
|
expect(dlBtn).not.toBeNull();
|
|
});
|
|
|
|
it("test_omits_download_button_when_raw_999_text_absent", () => {
|
|
const data: Ack = {
|
|
id: 42,
|
|
sourceBatchId: "b-uuid-1",
|
|
acceptedCount: 3,
|
|
rejectedCount: 1,
|
|
receivedCount: 4,
|
|
ackCode: "A",
|
|
parsedAt: "2026-06-20T12:00:00Z",
|
|
};
|
|
mockDetail({ data });
|
|
render(<AckDrawer ackId="42" onClose={() => {}} />);
|
|
|
|
// No raw_999_text → no Download button.
|
|
expect(document.querySelector('[data-testid="ack-drawer-download"]')).toBeNull();
|
|
});
|
|
});
|