2026-09-25 01:43:34 -03:00
|
|
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
2026-08-11 13:49:03 -03:00
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
|
import { UpliftRequestForm } from "@/app/v/[token]/dispatch/_components/uplift-request-form";
|
2026-09-25 01:50:27 -03:00
|
|
|
import { probeVideoDurationSeconds } from "@/lib/probe-video-duration";
|
|
|
|
|
|
|
|
|
|
vi.mock("@/lib/probe-video-duration", () => ({ probeVideoDurationSeconds: vi.fn() }));
|
2026-08-11 13:49:03 -03:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-24 22:29:20 -03:00
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-25 01:43:34 -03:00
|
|
|
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(),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-25 01:50:27 -03:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-11 13:49:03 -03:00
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
});
|