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
96 lines
3.9 KiB
TypeScript
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();
|
|
});
|
|
});
|