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