pelagia-portal/App/tests/unit/vendor-form-captcha.test.tsx
Claude (auto-fix) 55ae1d46d0
All checks were successful
PR checks / checks (pull_request) Successful in 46s
PR checks / integration (pull_request) Successful in 31s
fix(vendors): move GSTIN CAPTCHA into a popup
The GSTIN lookup rendered its CAPTCHA (image + 6-digit input + Verify /
New image) inline inside the Add/Edit Vendor dialog. AdminDialog has no
internal scroll and is vertically centred, so the taller form pushed its
footer (Cancel / Create Vendor / Save) off-screen and out of reach.

Extract the CAPTCHA into a dedicated popup (CaptchaPopup) overlaid on the
vendor form at z-[60] with an explicit Cancel button and a ✕ close
control. It handles Escape on the capture phase so dismissing the CAPTCHA
does not also close the underlying form. In-flight CAPTCHA errors now
show inside the popup (it stays open so the user can retry / get a new
image); the success line still lands on the main form. The form footer is
never displaced.

Adds a unit test covering popup open on Look up, Cancel closing only the
popup, and a successful verify populating the fields.

Fixes #114
2026-06-24 06:37:29 +05:30

96 lines
3.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { AddVendorButton } from "@/app/(portal)/admin/vendors/vendor-form";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), refresh: vi.fn() }),
}));
// The form imports server actions; stub them so the client component renders in jsdom.
vi.mock("@/app/(portal)/admin/vendors/actions", () => ({
createVendor: vi.fn(),
updateVendor: vi.fn(),
toggleVendorActive: vi.fn(),
}));
const GSTIN = "27AAHCP5787B1Z6"; // 15 chars
describe("VendorForm — GSTIN CAPTCHA popup (issue #114)", () => {
beforeEach(() => {
global.fetch = vi.fn(async () =>
new Response(JSON.stringify({ captchaBase64: "ABC123", sessionId: "sess-1" }), {
headers: { "Content-Type": "application/json" },
}),
) as unknown as typeof fetch;
});
afterEach(() => {
vi.restoreAllMocks();
});
async function openFormAndLookup() {
render(<AddVendorButton simple />);
fireEvent.click(screen.getByText("+ Add Vendor"));
const gstinInput = screen.getByPlaceholderText(/27AAHCP5787B1Z6/);
fireEvent.change(gstinInput, { target: { value: GSTIN } });
fireEvent.click(screen.getByText("Look up"));
// Popup renders the CAPTCHA prompt once the fetch resolves.
await screen.findByText(/Enter the code shown in the image/i);
}
it("opens the CAPTCHA in a popup with a Cancel/Close control, leaving the form footer reachable", async () => {
await openFormAndLookup();
// The CAPTCHA lives in its own popup …
expect(screen.getByRole("heading", { name: /GSTIN CAPTCHA/i })).toBeTruthy();
// Both the form's ✕ and the popup's ✕/Cancel close controls are present.
expect(screen.getAllByLabelText("Close").length).toBeGreaterThanOrEqual(2);
expect(screen.getAllByText("Cancel").length).toBeGreaterThanOrEqual(2);
// … and the underlying vendor form's submit button is still rendered (never displaced).
expect(screen.getByText("Create Vendor")).toBeTruthy();
});
it("closes the popup on Cancel without closing the vendor form", async () => {
await openFormAndLookup();
// The popup's Cancel is the first one in the DOM (the CAPTCHA section precedes the footer).
fireEvent.click(screen.getAllByText("Cancel")[0]);
await waitFor(() => {
expect(screen.queryByText(/Enter the code shown in the image/i)).toBeNull();
});
// The vendor form itself stays open.
expect(screen.getByText("Create Vendor")).toBeTruthy();
expect(screen.queryByRole("heading", { name: /GSTIN CAPTCHA/i })).toBeNull();
});
it("verifies the CAPTCHA, fills the form fields, and closes the popup on success", async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockImplementation(async (url: string) => {
if (String(url).includes("/api/gst/captcha")) {
return new Response(JSON.stringify({ captchaBase64: "ABC123", sessionId: "sess-1" }), {
headers: { "Content-Type": "application/json" },
});
}
return new Response(
JSON.stringify({
legalName: "Acme Pvt Ltd",
tradeName: "Acme",
address: "1 Dock Rd",
pincode: "400001",
gstin: GSTIN,
status: "Active",
registrationDate: "2020-01-01",
}),
{ headers: { "Content-Type": "application/json" } },
);
});
await openFormAndLookup();
fireEvent.change(screen.getByPlaceholderText("6 digits"), { target: { value: "123456" } });
fireEvent.click(screen.getByText("Verify"));
// Popup closes; success line + populated fields appear on the form.
await waitFor(() => {
expect(screen.queryByText(/Enter the code shown in the image/i)).toBeNull();
});
expect((screen.getByDisplayValue("Acme") as HTMLInputElement)).toBeTruthy();
expect(screen.getByText(/Acme Pvt Ltd — Active/)).toBeTruthy();
});
});