WFDASH-WEBAPP/src/lib/pdf/render.test.ts

248 lines
7.6 KiB
TypeScript
Raw Normal View History

import { describe, it, expect } from "vitest";
import { renderQuotePdf } from "./render";
import type { Letterhead } from "@/lib/letterhead";
import type { Customer, Quote, QuoteItem } from "@/lib/types";
/**
* Smoke tests for the PDF renderer. We don't render visually — we
* assert the binary is a well-formed PDF (magic bytes, sane size) so
* any regression in the render pipeline (style typo breaking JSX,
* dep misconfig, etc.) trips the test even without eyeballing output.
*/
const baseLetterhead: Letterhead = {
id: "lh1",
collectionId: "pbc_1010367695",
collectionName: "letterhead_settings",
created: "2026-06-22T20:31:27.165Z",
updated: "2026-06-22T20:31:27.165Z",
display_name: "CNL Mining Supplies",
address: "47 Station Street\nCarletonville, Gauteng 2499",
phone: "+27 18 786 2897",
email: "quotes@wfsales.co.za",
vat_number: "4840229449",
registration_number: "2014/004024/07",
bank_name: "Standard Bank - Carletonville",
bank_account_number: "Current Account\n022196552",
bank_branch_code: "016141",
terms:
"Terms and Conditions:\n1. This quotation is valid for 30 days from the date of issue.\n2. Prices are quoted excluding VAT unless otherwise stated.\n3. Lead time will be confirmed upon receipt of order.",
default_markup_percent: 30,
footer_text: "",
logo: "",
is_active: true,
company: "CNL Mining Supplies (Pty) Ltd",
accent_color: "#738224",
};
const baseCustomer: Customer = {
id: "cust1",
collectionId: "pbc_customers",
collectionName: "customers",
created: "2026-06-22T00:00:00Z",
updated: "2026-06-22T00:00:00Z",
customer: "Cash Sales",
email: "james@microntec.co.za",
alternative_email: "",
buyer_email: "",
number: "0615829016",
company: "",
dateadded: "",
lastvisiteddate: "",
rep: "",
coordinates: { lon: 0, lat: 0 },
description: "",
};
const baseQuote: Quote = {
id: "q1",
collectionId: "pbc_quotes",
collectionName: "quotes",
created: "2026-06-22T13:40:00Z",
updated: "2026-06-22T13:40:00Z",
quote_number: "Q009638",
quote_reference: "",
external_number: "",
rfq_number: "",
markup: 0,
customer: "cust1",
company: "",
claimed_by: "",
description: "",
status: "new",
approved_by: "",
notes: "",
email_sender: "",
email_subject: "",
email_body: "",
has_attachments: false,
company_letterhead: "CNL",
email_delivered: false,
email_delivered_at: "",
email_received_at: "",
email_opened: false,
email_opened_at: "",
email_tracking_id: "",
has_new_reply: false,
last_reply_at: "",
email_failed_at: "",
pdf_generated_at: "",
email_message_id: "",
// Backdating / template / follow-up fields — empty defaults so the
// PDF falls back to `created` for the date and the package default
// (30 days) for validity.
quote_date: "",
validity_days: null,
is_template: false,
template_source_id: "",
generated_from_template_id: "",
follow_up_due: "",
email_cc: "",
email_bcc: "",
email_subject_outbound: "",
email_message: "",
attach_auth_letters: true,
// Outbound-send tracking — empty defaults. Tests that exercise
// the failure banner seed these explicitly.
email_outbound_message_id: "",
email_outbound_attachment_ids: "",
email_error: "",
sending_started_at: "",
};
const baseItems: QuoteItem[] = [
{
id: "qi1",
collectionId: "pbc_quote_items",
collectionName: "quote_items",
created: "2026-06-22T13:40:00Z",
updated: "2026-06-22T13:40:00Z",
quote: "q1",
description: "JEICS30 SLUGGER M2-AL HSS",
quote_number: "Q009638",
quote_reference: "",
quantity: 1,
supplier: "",
cost_price: 0,
selling_price: 648.7,
notes: "",
is_processed: true,
markup: 0,
quote_pdf_id: "",
user_edited: true,
sku: "",
pricing_source: "",
matched_price_list_id: "",
pricing_notes: "",
},
{
id: "qi2",
collectionId: "pbc_quote_items",
collectionName: "quote_items",
created: "2026-06-22T13:40:00Z",
updated: "2026-06-22T13:40:00Z",
quote: "q1",
description: "JEIPILOT 1",
quote_number: "Q009638",
quote_reference: "",
quantity: 1,
supplier: "",
cost_price: 0,
selling_price: 88.36,
notes: "",
is_processed: true,
markup: 0,
quote_pdf_id: "",
user_edited: true,
sku: "",
pricing_source: "",
matched_price_list_id: "",
pricing_notes: "",
},
];
describe("renderQuotePdf", () => {
it("produces a Buffer with a PDF magic-number header", async () => {
const buf = await renderQuotePdf({
quote: baseQuote,
items: baseItems,
customer: baseCustomer,
letterhead: baseLetterhead,
logoDataUri: null,
});
expect(Buffer.isBuffer(buf)).toBe(true);
// Every PDF starts with the literal `%PDF-` (5 bytes).
expect(buf.subarray(0, 5).toString("ascii")).toBe("%PDF-");
}, 30_000);
it("produces a non-trivial buffer (> 1 KB) for a simple quote", async () => {
const buf = await renderQuotePdf({
quote: baseQuote,
items: baseItems,
customer: baseCustomer,
letterhead: baseLetterhead,
logoDataUri: null,
});
expect(buf.length).toBeGreaterThan(1024);
}, 30_000);
it("handles missing logo gracefully (logoDataUri=null)", async () => {
const buf = await renderQuotePdf({
quote: baseQuote,
items: baseItems,
customer: baseCustomer,
letterhead: { ...baseLetterhead, logo: "" },
logoDataUri: null,
});
expect(buf.subarray(0, 5).toString("ascii")).toBe("%PDF-");
}, 30_000);
it("renders CNL and Isherwood without throwing (different accent colors)", async () => {
const cnl = await renderQuotePdf({
quote: baseQuote,
items: baseItems,
customer: baseCustomer,
letterhead: baseLetterhead, // accent #738224
logoDataUri: null,
});
const isherwood = await renderQuotePdf({
quote: baseQuote,
items: baseItems,
customer: baseCustomer,
letterhead: { ...baseLetterhead, display_name: "ISHERWOOD MINING SUPPLIES", accent_color: "#7ed957" },
logoDataUri: null,
});
expect(cnl.subarray(0, 5).toString("ascii")).toBe("%PDF-");
expect(isherwood.subarray(0, 5).toString("ascii")).toBe("%PDF-");
// Different accent color → different generated PDF body length.
// (not a strict equality — accent only affects a small region — but
// the two PDFs at least both produce valid output)
}, 30_000);
it("renders with backdated quote_date + extended validity_days without throwing", async () => {
// Office scenario: backdate to 1 March 2026, valid for 2 months.
// We can't easily grep for "1 March 2026" in the PDF byte stream
// because @react-pdf compresses text into FlateDecode streams —
// so we verify the format helper produces the right dates and
// trust the rest from the unit tests in format.test.ts.
const { effectiveQuoteDate, effectiveValidityDays, addDays } = await import("./format");
const d = effectiveQuoteDate({ quote_date: "2026-03-01T00:00:00.000Z", created: baseQuote.created });
expect(d).toBe("2026-03-01T00:00:00.000Z");
expect(effectiveValidityDays({ validity_days: 60 })).toBe(60);
expect(addDays(d, effectiveValidityDays({ validity_days: 60 }))).toBe("2026-04-30T00:00:00.000Z");
const buf = await renderQuotePdf({
quote: {
...baseQuote,
quote_date: "2026-03-01T00:00:00.000Z",
validity_days: 60,
},
items: baseItems,
customer: baseCustomer,
letterhead: baseLetterhead,
logoDataUri: null,
});
expect(buf.subarray(0, 5).toString("ascii")).toBe("%PDF-");
expect(buf.length).toBeGreaterThan(1024);
}, 30_000);
});