Files
edr-platform/apps/edr-freight-api/src/modules/exports/tabular-export.service.spec.ts
Nathnael fc2b5ee0e3 refactor(reports): export through the shared tabular writer
Completes the writer extraction whose other half landed in fb21ad154.
reports.controller now builds a TabularDoc and calls TabularExportService,
so report-export.service.ts and report-export-request.util.ts are dead and
removed — HEAD was carrying both copies with the controller still on the old
one.

Reports gain CSV for free, and the PDF path now passes buildTabularFallbackPdf
as its fallback: previously it passed none, so a box without Chromium silently
returned PdfRenderService's ~900-character generic text dump instead of a
table. Adds a spec covering the CSV writer's quoting of embedded commas and
double quotes — the reason this uses ExcelJS's csv writer rather than a
hand-rolled join.
2026-08-20 05:16:15 +00:00

66 lines
2.6 KiB
TypeScript

import { PdfRenderService } from '../billing/documents/pdf-render.service';
import { TabularDoc, TabularExportService } from './tabular-export.service';
/** The PDF path is puppeteer-backed; these specs only cover the sheet writers. */
const service = new TabularExportService(null as unknown as PdfRenderService);
const doc: TabularDoc = {
title: 'Bookings',
description: 'every booking',
label: 'test',
columns: [
{ key: 'ref', label: 'Reference', type: 'string' },
{ key: 'customer', label: 'Customer', type: 'string' },
{ key: 'amount', label: 'Amount', type: 'money' },
{ key: 'gov', label: 'Government', type: 'boolean' },
],
rows: [
{ ref: 'BK-1', customer: 'Acme, Inc.', amount: 1234.5, gov: true },
{ ref: 'BK-2', customer: 'Quote "Q" Ltd', amount: null, gov: false },
],
kpis: [{ label: 'Bookings', value: 2 }],
};
describe('TabularExportService.toCsv', () => {
it('quotes a value containing the delimiter — the reason we do not hand-roll join(",")', async () => {
const csv = (await service.toCsv(doc)).toString('utf8');
expect(csv).toContain('"Acme, Inc."');
});
it('escapes embedded double quotes by doubling them', async () => {
const csv = (await service.toCsv(doc)).toString('utf8');
expect(csv).toContain('"Quote ""Q"" Ltd"');
});
it('starts at the header row — no KPI preamble, so the file parses as a plain table', async () => {
const csv = (await service.toCsv(doc)).toString('utf8');
expect(csv.split('\n')[0]).toBe('Reference,Customer,Amount,Government');
expect(csv).not.toContain('Bookings: 2');
});
it('emits one line per row plus the header', async () => {
const csv = (await service.toCsv(doc)).toString('utf8');
expect(csv.trim().split('\n').filter(Boolean)).toHaveLength(3);
});
it('only the selected columns are written, in the order given', async () => {
const csv = (
await service.toCsv({ ...doc, columns: [doc.columns[2], doc.columns[0]] })
).toString('utf8');
expect(csv.split('\n')[0]).toBe('Amount,Reference');
});
});
describe('TabularExportService.toXlsx', () => {
it('writes a real xlsx (a zip, so it starts with the PK magic bytes)', async () => {
const buffer = await service.toXlsx(doc);
expect(buffer.subarray(0, 2).toString('utf8')).toBe('PK');
expect(buffer.length).toBeGreaterThan(1000);
});
it('a title longer than Excel\'s 31-char sheet-name limit does not throw', async () => {
const longTitle = 'A'.repeat(60);
await expect(service.toXlsx({ ...doc, title: longTitle })).resolves.toBeInstanceOf(Buffer);
});
});