mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 18:12:05 +00:00
141 lines
4.6 KiB
TypeScript
141 lines
4.6 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();
|
||
|
|
});
|
||
|
|
});
|