mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-03 17:53:14 +00:00
Picking a video as uplift evidence reads its movie header before the file is stored in form state. A submit that landed inside that window validated the previous (usually empty) evidence, showed the missing-evidence error and dropped the request, forcing a second click. The submit button is now disabled and reads "Checking evidence…" while the latest pick is being checked, and the submit handler ignores submissions in that window.
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { UpliftRequestForm } from "@/app/v/[token]/dispatch/_components/uplift-request-form";
|
|
import { probeVideoDurationSeconds } from "@/lib/probe-video-duration";
|
|
|
|
vi.mock("@/lib/probe-video-duration", () => ({ probeVideoDurationSeconds: vi.fn() }));
|
|
|
|
function renderForm(overrides: Partial<React.ComponentProps<typeof UpliftRequestForm>> = {}) {
|
|
return render(
|
|
<UpliftRequestForm
|
|
mode="revise"
|
|
currentNte={1000}
|
|
initialRequested="1000"
|
|
initialReason=""
|
|
hasExistingEvidence={false}
|
|
submitting={false}
|
|
uploadingPercent={undefined}
|
|
errorMessage=""
|
|
onSubmit={vi.fn()}
|
|
onCancel={vi.fn()}
|
|
{...overrides}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
describe("UpliftRequestForm revise mode", () => {
|
|
it("shows reason and evidence fields regardless of amount", () => {
|
|
renderForm({ initialRequested: "900" });
|
|
|
|
expect(screen.getByLabelText(/reason/i)).toBeInTheDocument();
|
|
expect(screen.getByLabelText(/evidence/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("blocks submit and does not call onSubmit when revised NTE is at or below current NTE", () => {
|
|
const onSubmit = vi.fn();
|
|
renderForm({ initialRequested: "1000", onSubmit });
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /submit revision/i }));
|
|
|
|
expect(onSubmit).not.toHaveBeenCalled();
|
|
expect(screen.getByText(/must be greater than the current NTE/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("refuses photo evidence when the work order already has 10 photos", async () => {
|
|
renderForm({ mediaUsage: { photos: 10, videos: 0, maxPhotos: 10, maxVideos: 3 } });
|
|
|
|
fireEvent.change(screen.getByLabelText(/evidence/i), {
|
|
target: { files: [new File(["jpg"], "receipt.jpg", { type: "image/jpeg" })] },
|
|
});
|
|
|
|
expect(await screen.findByText("A work order can have at most 10 photos.")).toBeInTheDocument();
|
|
});
|
|
|
|
it("offers and accepts the same phone photo and video types as the upload allowlist", async () => {
|
|
renderForm();
|
|
|
|
const input = screen.getByLabelText("Evidence (PDF, JPG, PNG, HEIC, MP4, MOV)");
|
|
expect(input.getAttribute("accept")?.split(",")).toEqual(
|
|
expect.arrayContaining([".pdf", ".jpg", ".png", ".heic", ".mp4", ".mov"]),
|
|
);
|
|
|
|
fireEvent.change(input, {
|
|
target: { files: [new File(["gif"], "clip.gif", { type: "image/gif" })] },
|
|
});
|
|
expect(
|
|
await screen.findByText("Only PDF, JPG, PNG, HEIC, MP4, and MOV files are allowed."),
|
|
).toBeInTheDocument();
|
|
|
|
fireEvent.change(input, {
|
|
target: { files: [new File(["heic"], "site.heic", { type: "" })] },
|
|
});
|
|
await waitFor(() =>
|
|
expect(
|
|
screen.queryByText("Only PDF, JPG, PNG, HEIC, MP4, and MOV files are allowed."),
|
|
).not.toBeInTheDocument(),
|
|
);
|
|
});
|
|
|
|
it("holds submit while a picked video is still being checked, then submits that video", async () => {
|
|
let finishProbe: (seconds: number | null) => void = () => {};
|
|
vi.mocked(probeVideoDurationSeconds).mockImplementationOnce(
|
|
() => new Promise((resolve) => (finishProbe = resolve)),
|
|
);
|
|
const onSubmit = vi.fn();
|
|
renderForm({ initialRequested: "1200", initialReason: "Extra parts", onSubmit });
|
|
|
|
const video = new File(["mov"], "IMG_0001.MOV", { type: "video/quicktime" });
|
|
fireEvent.change(screen.getByLabelText(/evidence/i), { target: { files: [video] } });
|
|
|
|
const checking = await screen.findByRole("button", { name: "Checking evidence…" });
|
|
expect(checking).toBeDisabled();
|
|
fireEvent.submit(checking.closest("form")!);
|
|
expect(onSubmit).not.toHaveBeenCalled();
|
|
expect(screen.queryByText(/approval-required requests need evidence/i)).not.toBeInTheDocument();
|
|
|
|
finishProbe(30);
|
|
const submitButton = await screen.findByRole("button", { name: /submit revision/i });
|
|
expect(submitButton).toBeEnabled();
|
|
fireEvent.click(submitButton);
|
|
|
|
expect(onSubmit).toHaveBeenCalledWith({
|
|
amount: 1200,
|
|
reason: "Extra parts",
|
|
evidenceFile: video,
|
|
});
|
|
});
|
|
|
|
it("reports the current NTE threshold in the actionable error", () => {
|
|
renderForm({ initialRequested: "900", currentNte: 1000 });
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /submit revision/i }));
|
|
|
|
expect(screen.getByText(/\$1000\.00/)).toBeInTheDocument();
|
|
});
|
|
});
|