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> = {}) { return render( , ); } 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(); }); });