Files
suaveplan/packages/communication/emails/email-templates/src/templates/invoice/invoice.test.ts
2026-05-27 23:29:53 +01:00

127 lines
4.3 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import { findEvery, findFirst, textOf } from "../../../tests/setup/html.js";
import { renderToHtml } from "../../renderer/renderer.js";
import { renderVNode } from "../../vnode/vnode.js";
import { InvoiceEmail, invoiceTemplate } from "./invoice.js";
const baseProps = {
recipientName: "Jane",
invoiceNumber: "INV-001",
invoiceDate: "2026-01-01",
items: [
{ description: "Widget", quantity: 2, unitPrice: 10, total: 20 },
{ description: "Gizmo", quantity: 1, unitPrice: 15.5, total: 15.5 },
],
subtotal: 35.5,
total: 35.5,
};
describe("invoiceTemplate", () => {
it("renders line-item rows", { timeout: 5000 }, () => {
const { html } = renderToHtml(invoiceTemplate, baseProps);
const widget = findFirst(
html,
(el) => el.name === "td" && textOf(el).includes("Widget")
);
expect(widget).not.toBeNull();
const gizmo = findFirst(
html,
(el) => el.name === "td" && textOf(el).includes("Gizmo")
);
expect(gizmo).not.toBeNull();
});
it("renders the invoice number in the subject and heading", {
timeout: 5000,
}, () => {
const { html, subject } = renderToHtml(invoiceTemplate, baseProps);
expect(subject).toBe("Invoice #INV-001");
expect(html).toContain("Invoice #INV-001");
});
it("formats currency to two decimal places", { timeout: 5000 }, () => {
const html = renderVNode(InvoiceEmail(baseProps));
expect(html).toContain("$10.00");
expect(html).toContain("$15.50");
expect(html).toContain("$35.50");
});
it("uses custom currency symbol", { timeout: 5000 }, () => {
const html = renderVNode(
InvoiceEmail({
...baseProps,
currency: "€",
})
);
expect(html).toContain("€10.00");
expect(html).not.toContain("$10.00");
});
it("omits dueDate when not provided", { timeout: 5000 }, () => {
const html = renderVNode(InvoiceEmail(baseProps));
expect(html).toContain("Date: 2026-01-01");
expect(html).not.toContain("Due:");
});
it("renders dueDate when provided", { timeout: 5000 }, () => {
const html = renderVNode(
InvoiceEmail({ ...baseProps, dueDate: "2026-01-31" })
);
expect(html).toContain("Due: 2026-01-31");
});
it("renders tax row when tax provided", { timeout: 5000 }, () => {
const html = renderVNode(
InvoiceEmail({ ...baseProps, tax: 3.55, total: 39.05 })
);
expect(html).toContain("Tax: $3.55");
});
it("omits tax row when not provided", { timeout: 5000 }, () => {
const html = renderVNode(InvoiceEmail(baseProps));
expect(html).not.toContain("Tax:");
});
it("renders payment button when paymentUrl provided", {
timeout: 5000,
}, () => {
const html = renderVNode(
InvoiceEmail({ ...baseProps, paymentUrl: "https://x/pay" })
);
const a = findFirst(
html,
(el) => el.name === "a" && el.attribs.href === "https://x/pay"
);
expect(a).not.toBeNull();
});
it("omits payment button when paymentUrl absent", { timeout: 5000 }, () => {
const html = renderVNode(InvoiceEmail(baseProps));
expect(html).not.toContain("Pay Now");
});
it("uses explicit companyName", { timeout: 5000 }, () => {
const html = renderVNode(
InvoiceEmail({ ...baseProps, companyName: "Acme" })
);
expect(html).toContain("from Acme");
});
it("renders the column headers", { timeout: 5000 }, () => {
const html = renderVNode(InvoiceEmail(baseProps));
const headers = findEvery(html, (el) => el.name === "th");
const labels = headers.map((h) => textOf(h));
expect(labels).toEqual(["Item", "Qty", "Price", "Total"]);
});
it("renders an empty line-items section when items=[]", {
timeout: 5000,
}, () => {
const html = renderVNode(
InvoiceEmail({ ...baseProps, items: [], subtotal: 0, total: 0 })
);
const tbody = findFirst(html, (el) => el.name === "tbody");
expect(tbody).not.toBeNull();
});
});