import { describe, it, expect } from 'vitest'; import { renderToBuffer } from '@react-pdf/renderer'; import { createElement } from 'react'; import { DashboardReport } from '@/lib/pdf/reports/dashboard-report'; import { ClientListReport } from '@/lib/pdf/reports/client-list-report'; import { BerthListReport } from '@/lib/pdf/reports/berth-list-report'; import { InterestListReport } from '@/lib/pdf/reports/interest-list-report'; import type { ReportBranding } from '@/lib/pdf/reports/types'; const branding: ReportBranding = { logoUrl: null, primaryColor: '#0F4C81', portName: 'Port Nimara', }; describe('PDF report renderer', () => { it('renders a dashboard report with all sections to a non-empty PDF buffer', async () => { const element = createElement(DashboardReport, { title: 'Test report', subtitle: 'Unit-test fixture', branding, generatedAt: '2026-05-21T12:00:00.000Z', config: { kind: 'dashboard', widgetIds: [ 'kpi_overview', 'pipeline_funnel', 'berth_status', 'source_conversion', 'hot_deals', ], }, data: { kpis: { totalClients: 142, activeInterests: 27, pipelineValue: 1250000, pipelineValueCurrency: 'USD', occupancyRate: 64.3, }, pipelineCounts: [ { stage: 'enquiry', count: 12 }, { stage: 'qualified', count: 8 }, { stage: 'eoi', count: 4 }, { stage: 'reservation', count: 2 }, { stage: 'deposit_paid', count: 1 }, ], berthStatus: { total: 115, available: 80, underOffer: 10, sold: 25, }, sourceConversion: [ { source: 'website', total: 60, won: 12, lost: 30, conversionRate: 0.2 }, { source: 'referral', total: 25, won: 8, lost: 10, conversionRate: 0.32 }, ], hotDeals: [ { id: 'i1', clientName: 'Acme Corp', mooringNumber: 'A3', stage: 'reservation', lastContact: '2026-05-18T09:00:00.000Z', }, ], }, }); const buf = await renderToBuffer(element as any); expect(buf.byteLength).toBeGreaterThan(2_000); // PDF files start with `%PDF-` magic bytes - sanity-check that // the renderer produced an actual PDF, not an error blob or // empty buffer. const head = buf.subarray(0, 5).toString('utf-8'); expect(head).toBe('%PDF-'); }, 30_000); it('skips sections whose widget id is absent from widgetIds', async () => { const element = createElement(DashboardReport, { title: 'Sparse report', branding, generatedAt: '2026-05-21T12:00:00.000Z', config: { kind: 'dashboard', widgetIds: ['kpi_overview'], }, data: { kpis: { totalClients: 5, activeInterests: 1, pipelineValue: 0, pipelineValueCurrency: 'USD', occupancyRate: 0, }, // Provide pipelineCounts even though widgetIds didn't ask for // it - the renderer should still skip the section since it's // gated on widgetIds, not data presence. pipelineCounts: [{ stage: 'enquiry', count: 1 }], }, }); const buf = await renderToBuffer(element as any); expect(buf.byteLength).toBeGreaterThan(1_000); }, 30_000); it('renders a client list report to a non-empty PDF buffer', async () => { const element = createElement(ClientListReport, { title: 'Clients', branding, generatedAt: '2026-05-21T12:00:00.000Z', config: { kind: 'clients' }, data: { rows: [ { id: 'c1', fullName: 'Acme Corp', source: 'website', nationality: 'GB', primaryEmail: 'ops@acme.example', primaryPhone: '+44 20 7946 0000', createdAt: '2026-04-15T10:00:00Z', }, { id: 'c2', fullName: 'Beta Industries', source: 'referral', nationality: null, primaryEmail: null, primaryPhone: null, createdAt: '2026-05-01T10:00:00Z', }, ], total: 2, capHit: false, }, }); const buf = await renderToBuffer(element as any); expect(buf.byteLength).toBeGreaterThan(1_500); expect(buf.subarray(0, 5).toString('utf-8')).toBe('%PDF-'); }, 30_000); it('renders a berth list report', async () => { const element = createElement(BerthListReport, { title: 'Berths', branding, generatedAt: '2026-05-21T12:00:00.000Z', config: { kind: 'berths' }, data: { rows: [ { id: 'b1', mooringNumber: 'A1', area: 'A', status: 'available', lengthFt: '40', widthFt: '14', draftFt: '6', price: '120000', priceCurrency: 'USD', tenureType: 'permanent', }, ], total: 1, capHit: false, }, }); const buf = await renderToBuffer(element as any); expect(buf.byteLength).toBeGreaterThan(1_500); expect(buf.subarray(0, 5).toString('utf-8')).toBe('%PDF-'); }, 30_000); it('renders an interest pipeline report', async () => { const element = createElement(InterestListReport, { title: 'Pipeline', branding, generatedAt: '2026-05-21T12:00:00.000Z', config: { kind: 'interests' }, data: { rows: [ { id: 'i1', clientName: 'Acme Corp', primaryMooring: 'A1', pipelineStage: 'reservation', source: 'website', outcome: null, createdAt: '2026-04-20T10:00:00Z', }, ], total: 1, capHit: false, }, }); const buf = await renderToBuffer(element as any); expect(buf.byteLength).toBeGreaterThan(1_500); expect(buf.subarray(0, 5).toString('utf-8')).toBe('%PDF-'); }, 30_000); it('falls back to a stable layout when no logo URL is supplied', async () => { const element = createElement(DashboardReport, { title: 'Logoless', branding: { ...branding, logoUrl: null }, generatedAt: '2026-05-21T12:00:00.000Z', config: { kind: 'dashboard', widgetIds: ['kpi_overview'] }, data: { kpis: { totalClients: 0, activeInterests: 0, pipelineValue: 0, pipelineValueCurrency: 'USD', occupancyRate: 0, }, }, }); const buf = await renderToBuffer(element as any); expect(buf.byteLength).toBeGreaterThan(1_000); }, 30_000); });