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]);
});