diff --git a/src/components/collection-confirm/confirm-invoice-dialog.test.tsx b/src/components/collection-confirm/confirm-invoice-dialog.test.tsx
new file mode 100644
index 0000000..14ea609
--- /dev/null
+++ b/src/components/collection-confirm/confirm-invoice-dialog.test.tsx
@@ -0,0 +1,210 @@
+import { render, screen, waitFor, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { ConfirmInvoiceDialog } from "./confirm-invoice-dialog";
+
+const costOverrides = { "row-1": 50 };
+
+function getDialog() {
+ return screen.getByRole("dialog");
+}
+
+function getDateInput(dialog = getDialog()) {
+ return within(dialog).getByLabelText(/Supplier invoice date/i);
+}
+
+function getNumberInput(dialog = getDialog()) {
+ return within(dialog).getByLabelText(/Supplier invoice number/i);
+}
+
+function getSubmitButton(dialog = getDialog()) {
+ return within(dialog).getByRole("button", { name: /^Confirm(?: \d+ items)?$/i });
+}
+
+afterEach(() => {
+ vi.restoreAllMocks();
+});
+
+describe("ConfirmInvoiceDialog", () => {
+ it("renders the row and cost override summary", () => {
+ render(
+ ,
+ );
+
+ expect(
+ within(getDialog()).getByText(/Confirming 2 rows/i),
+ ).toHaveTextContent("Confirming 2 rows · 1 cost override applied");
+ });
+
+ it("calls onSubmit with the invoice details and cost overrides", async () => {
+ const user = userEvent.setup();
+ const onSubmit = vi.fn();
+ render(
+ ,
+ );
+
+ await user.type(getDateInput(), "2026-08-03");
+ await user.type(getNumberInput(), "INV-001");
+ await user.click(getSubmitButton());
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ expect(onSubmit).toHaveBeenCalledWith({
+ invoice: { date: "2026-08-03", number: "INV-001" },
+ costOverrides,
+ });
+ });
+
+ it("disables submit when the date is empty", async () => {
+ const user = userEvent.setup();
+ render(
+ ,
+ );
+
+ await user.type(getNumberInput(), "INV-001");
+
+ expect(getSubmitButton()).toBeDisabled();
+ });
+
+ it("disables submit when the invoice number is empty", async () => {
+ const user = userEvent.setup();
+ render(
+ ,
+ );
+
+ await user.type(getDateInput(), "2026-08-03");
+
+ expect(getSubmitButton()).toBeDisabled();
+ });
+
+ it("disables submit when both invoice fields are empty", () => {
+ render(
+ ,
+ );
+
+ expect(getSubmitButton()).toBeDisabled();
+ });
+
+ it("trims the invoice number before submitting", async () => {
+ const user = userEvent.setup();
+ const onSubmit = vi.fn();
+ render(
+ ,
+ );
+
+ await user.type(getNumberInput(), " INV-001 ");
+ await user.click(getSubmitButton());
+
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ expect(onSubmit.mock.calls[0][0].invoice.number).toBe("INV-001");
+ });
+
+ it("uses singular row and plural zero-overrides labels", () => {
+ render(
+ ,
+ );
+
+ expect(
+ within(getDialog()).getByText(/Confirming 1 row/i),
+ ).toHaveTextContent("Confirming 1 row · 0 cost overrides applied");
+ });
+
+ it("re-applies updated defaults when reopened", async () => {
+ const onOpenChange = vi.fn();
+ const onSubmit = vi.fn();
+ const { rerender } = render(
+ ,
+ );
+
+ expect(getDateInput()).toHaveValue("2026-08-01");
+ expect(getNumberInput()).toHaveValue("INV-OLD");
+
+ rerender(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
+ );
+
+ rerender(
+ ,
+ );
+
+ const reopenedDialog = await screen.findByRole("dialog");
+ await waitFor(() => {
+ expect(getDateInput(reopenedDialog)).toHaveValue("2026-08-02");
+ expect(getNumberInput(reopenedDialog)).toHaveValue("INV-NEW");
+ });
+ });
+
+ it("closes the dialog when Cancel is clicked", async () => {
+ const user = userEvent.setup();
+ const onOpenChange = vi.fn();
+ render(
+ ,
+ );
+
+ await user.click(within(getDialog()).getByRole("button", { name: /Cancel/i }));
+
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+});