127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
|
|
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 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: 120,
|
||
|
|
available: 80,
|
||
|
|
underOffer: 10,
|
||
|
|
maintenance: 5,
|
||
|
|
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('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);
|
||
|
|
});
|