dodil/erp-ui
tests/forms-and-dialogs.test.tsx
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Button } from "@radix-ui/themes";
import { ConfirmDialog, FormDialog, MoneyInput, parseMoneyInput, validate } from "../src";
import { a11y, renderInTheme } from "./helpers";
test("an amount typed by a person is read as exact text, never through a float", () => {
expect(parseMoneyInput("12,000.5")).toBe("12000.50");
expect(parseMoneyInput(" 820 ")).toBe("820.00");
expect(parseMoneyInput("9007199254740993.01")).toBe("9007199254740993.01"); // a float would say ...992
expect(parseMoneyInput("007.5")).toBe("7.50");
expect(parseMoneyInput("1.234")).toBeNull(); // never rounded
expect(parseMoneyInput("-5")).toBeNull();
expect(parseMoneyInput("12e3")).toBeNull();
expect(parseMoneyInput("")).toBe("");
});
test("the money input hands back the decimal string and says what is wrong", async () => {
const seen: (string | null)[] = [];
const { container } = renderInTheme(<MoneyInput label="Unit price" value="" currency="EUR" onChange={(v) => seen.push(v)} />);
const input = screen.getByLabelText("Unit price");
await userEvent.type(input, "1,250.5");
expect(seen.at(-1)).toBe("1250.50");
await userEvent.type(input, "55");
expect(seen.at(-1)).toBeNull();
expect(input).toHaveAttribute("aria-invalid", "true");
expect(screen.getByText(/at most 2 decimals/)).toBeInTheDocument();
expect(await a11y(container)).toEqual([]);
});
test("required fields stop a form before anything is sent", () => {
const fields = [{ name: "subject", label: "Subject", required: true }, { name: "qty", label: "Quantity", kind: "integer" as const }];
expect(validate(fields, { subject: " ", qty: "2.5" })).toEqual({ subject: "Subject is required", qty: "Quantity must be a whole number" });
expect(validate(fields, { subject: "Call", qty: "3" })).toEqual({});
});
test("a form dialog keeps what was typed and shows the API's refusal in place", async () => {
const sent: Record<string, string>[] = [];
let refuse = true;
const { baseElement } = renderInTheme(
<FormDialog trigger={<Button>New deal</Button>} title="New deal" submitLabel="Open deal"
fields={[{ name: "name", label: "Name", required: true },
{ name: "amount", label: "Amount", kind: "money", currency: "EUR" }]}
onSubmit={async (v) => { sent.push(v); if (refuse) throw new Error("the customer is blocked"); }} />,
);
await userEvent.click(screen.getByRole("button", { name: "New deal" }));
await userEvent.click(screen.getByRole("button", { name: "Open deal" }));
expect(sent).toEqual([]); // required: nothing sent
expect(screen.getByText("Name is required")).toBeInTheDocument();
await userEvent.type(screen.getByLabelText("Name *"), "Acme 2027");
await userEvent.type(screen.getByLabelText("Amount"), "48,000");
await userEvent.click(screen.getByRole("button", { name: "Open deal" }));
expect(await screen.findByRole("alert")).toHaveTextContent("the customer is blocked");
expect(sent).toEqual([{ name: "Acme 2027", amount: "48000.00" }]); // normalised, exact
expect(screen.getByLabelText("Name *")).toHaveValue("Acme 2027"); // kept
expect(await a11y(baseElement)).toEqual([]);
refuse = false;
await userEvent.click(screen.getByRole("button", { name: "Open deal" }));
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
});
test("a confirm dialog that needs a reason will not confirm without one", async () => {
const done: string[] = [];
const { baseElement } = renderInTheme(
<ConfirmDialog trigger={<Button>Disqualify</Button>} title="Disqualify Karin Ek?" confirmLabel="Disqualify" tone="danger"
reason={{ label: "Why?" }} onConfirm={(r) => { done.push(r); }} />,
);
await userEvent.click(screen.getByRole("button", { name: "Disqualify" }));
const confirm = screen.getAllByRole("button", { name: "Disqualify" }).at(-1)!;
expect(confirm).toBeDisabled();
await userEvent.type(screen.getByLabelText("Why?"), "uses an in-house agency");
expect(confirm).toBeEnabled();
expect(await a11y(baseElement)).toEqual([]);
await userEvent.click(confirm);
expect(done).toEqual(["uses an in-house agency"]);
});
test("a refused confirmation stays open with the reason", async () => {
renderInTheme(
<ConfirmDialog trigger={<Button>Send</Button>} title="Send the quote?" confirmLabel="Send it"
onConfirm={() => { throw new Error("needs the deal desk's approval"); }} />,
);
await userEvent.click(screen.getByRole("button", { name: "Send" }));
await userEvent.click(screen.getByRole("button", { name: "Send it" }));
expect(await screen.findByRole("alert")).toHaveTextContent("needs the deal desk's approval");
expect(screen.getByRole("alertdialog")).toBeInTheDocument();
});
test("a form dialog can be opened from code — a card dropped on a column that needs a reason", async () => {
const sent: Record<string, string>[] = [];
const closed: boolean[] = [];
renderInTheme(
<FormDialog open title="Lose the deal?" submitLabel="Mark lost" onOpenChange={(o) => closed.push(o)}
fields={[{ name: "why", label: "Why", required: true }]} onSubmit={(v) => { sent.push(v); }} />,
);
await userEvent.type(screen.getByLabelText("Why *"), "price");
await userEvent.click(screen.getByRole("button", { name: "Mark lost" }));
expect(sent).toEqual([{ why: "price" }]);
expect(closed).toEqual([false]);
});