mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 11:52:03 +00:00
210 lines
7.1 KiB
TypeScript
210 lines
7.1 KiB
TypeScript
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { RefuseDispatchDialog } from "@/app/v/[token]/dispatch/_components/refuse-dispatch-dialog";
|
|
import {
|
|
VendorPortalContext,
|
|
type VendorPortalContextValue,
|
|
} from "@/app/v/_components/vendor-portal-context";
|
|
import { vendorPortalApi } from "@/domain/vendor-portal/api/vendor-portal-api";
|
|
import { REFUSE_REASON_MAX_LENGTH } from "@/domain/vendor-portal/schemas/refuse-dispatch-schema";
|
|
import { queryKeys } from "@/infra/query-key/query-key";
|
|
import { createTestQueryClient, renderWithProviders } from "@/test/test-utils";
|
|
|
|
function renderDialog({
|
|
token = "portal-token",
|
|
dispatchId = 9,
|
|
onClose = vi.fn(),
|
|
onRefused = vi.fn(),
|
|
}: Partial<{
|
|
token: string;
|
|
dispatchId: number;
|
|
onClose: () => void;
|
|
onRefused: () => void;
|
|
}> = {}) {
|
|
const queryClient = createTestQueryClient();
|
|
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
|
|
|
|
const portalValue: VendorPortalContextValue = {
|
|
token,
|
|
vendor: { companyName: "Gateway Plumbing", vendorId: 101 },
|
|
status: "ready",
|
|
error: null,
|
|
};
|
|
|
|
const utils = renderWithProviders(
|
|
<VendorPortalContext.Provider value={portalValue}>
|
|
<RefuseDispatchDialog open dispatchId={dispatchId} onClose={onClose} onRefused={onRefused} />
|
|
</VendorPortalContext.Provider>,
|
|
{ withAuth: false, queryClient },
|
|
);
|
|
|
|
return { ...utils, invalidateSpy, queryClient };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.spyOn(vendorPortalApi, "refuse").mockResolvedValue({ id: 9, status: "Refused" });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("RefuseDispatchDialog", () => {
|
|
it("submits without a reason when the field is left empty", async () => {
|
|
renderDialog();
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole("button", { name: "Refuse dispatch" }));
|
|
});
|
|
|
|
await waitFor(() =>
|
|
expect(vendorPortalApi.refuse).toHaveBeenCalledWith("portal-token", 9, undefined),
|
|
);
|
|
});
|
|
|
|
it("trims a supplied reason before sending", async () => {
|
|
renderDialog();
|
|
|
|
fireEvent.change(screen.getByLabelText(/Reason/i), { target: { value: " too busy " } });
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole("button", { name: "Refuse dispatch" }));
|
|
});
|
|
|
|
await waitFor(() =>
|
|
expect(vendorPortalApi.refuse).toHaveBeenCalledWith("portal-token", 9, "too busy"),
|
|
);
|
|
});
|
|
|
|
it("blocks submission when the reason exceeds the length cap", async () => {
|
|
renderDialog();
|
|
|
|
const overLimit = "a".repeat(REFUSE_REASON_MAX_LENGTH + 1);
|
|
fireEvent.change(screen.getByLabelText(/Reason/i), { target: { value: overLimit } });
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByRole("button", { name: "Refuse dispatch" })).toBeDisabled(),
|
|
);
|
|
expect(vendorPortalApi.refuse).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows a live character counter bounded by the limit", () => {
|
|
renderDialog();
|
|
|
|
fireEvent.change(screen.getByLabelText(/Reason/i), { target: { value: "abc" } });
|
|
|
|
expect(screen.getByText(`3/${REFUSE_REASON_MAX_LENGTH}`)).toBeInTheDocument();
|
|
});
|
|
|
|
it("invalidates the detail and list cache and closes on success", async () => {
|
|
const onClose = vi.fn();
|
|
const onRefused = vi.fn();
|
|
const { invalidateSpy } = renderDialog({ onClose, onRefused });
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole("button", { name: "Refuse dispatch" }));
|
|
});
|
|
|
|
await waitFor(() => expect(onClose).toHaveBeenCalled());
|
|
expect(onRefused).toHaveBeenCalled();
|
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
|
queryKey: queryKeys.vendorPortal.dispatch("portal-token", 9),
|
|
});
|
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
|
queryKey: queryKeys.vendorPortal.dispatches("portal-token"),
|
|
});
|
|
});
|
|
|
|
it("surfaces a safe inline error and keeps the dialog open on failure", async () => {
|
|
vi.spyOn(vendorPortalApi, "refuse").mockRejectedValueOnce(new Error("nope"));
|
|
const onClose = vi.fn();
|
|
renderDialog({ onClose });
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole("button", { name: "Refuse dispatch" }));
|
|
});
|
|
|
|
await waitFor(() => expect(screen.getByRole("alert")).toHaveTextContent(/Unable to refuse/i));
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("closes without refusing when cancel is pressed", async () => {
|
|
const onClose = vi.fn();
|
|
renderDialog({ onClose });
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
|
|
await waitFor(() => expect(onClose).toHaveBeenCalled());
|
|
expect(vendorPortalApi.refuse).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("blocks Escape and backdrop close while a refusal is pending, then closes on success", async () => {
|
|
let resolveRefusal!: (value: { id: number; status: string }) => void;
|
|
vi.spyOn(vendorPortalApi, "refuse").mockImplementation(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveRefusal = resolve;
|
|
}),
|
|
);
|
|
const onClose = vi.fn();
|
|
renderDialog({ onClose });
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole("button", { name: "Refuse dispatch" }));
|
|
});
|
|
|
|
await waitFor(() => expect(screen.getByRole("button", { name: "Refusing…" })).toBeDisabled());
|
|
expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled();
|
|
|
|
const backdrop = document.querySelector(".MuiBackdrop-root");
|
|
expect(backdrop).not.toBeNull();
|
|
fireEvent.click(backdrop as Element);
|
|
fireEvent.keyDown(document.body, { key: "Escape" });
|
|
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
|
|
await act(async () => {
|
|
resolveRefusal({ id: 9, status: "Refused" });
|
|
});
|
|
|
|
await waitFor(() => expect(onClose).toHaveBeenCalled());
|
|
});
|
|
|
|
it("clears the stale error and reason after closing and reopening", async () => {
|
|
vi.spyOn(vendorPortalApi, "refuse").mockRejectedValueOnce(new Error("nope"));
|
|
const onClose = vi.fn();
|
|
const onRefused = vi.fn();
|
|
|
|
const portalValue: VendorPortalContextValue = {
|
|
token: "portal-token",
|
|
vendor: { companyName: "Gateway Plumbing", vendorId: 101 },
|
|
status: "ready",
|
|
error: null,
|
|
};
|
|
const renderEl = (open: boolean) => (
|
|
<VendorPortalContext.Provider value={portalValue}>
|
|
<RefuseDispatchDialog open={open} dispatchId={9} onClose={onClose} onRefused={onRefused} />
|
|
</VendorPortalContext.Provider>
|
|
);
|
|
|
|
const { rerender } = renderWithProviders(renderEl(true), { withAuth: false });
|
|
|
|
fireEvent.change(screen.getByLabelText(/Reason/i), { target: { value: "too busy" } });
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole("button", { name: "Refuse dispatch" }));
|
|
});
|
|
await waitFor(() => expect(screen.getByRole("alert")).toHaveTextContent(/Unable to refuse/i));
|
|
expect(screen.getByText(`8/${REFUSE_REASON_MAX_LENGTH}`)).toBeInTheDocument();
|
|
|
|
await act(async () => {
|
|
rerender(renderEl(false));
|
|
});
|
|
|
|
await act(async () => {
|
|
rerender(renderEl(true));
|
|
});
|
|
|
|
await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument());
|
|
expect(screen.getByText(`0/${REFUSE_REASON_MAX_LENGTH}`)).toBeInTheDocument();
|
|
});
|
|
});
|