import { describe, it, expect, beforeEach, vi } from 'vitest' import { buildCsvDownloadItem } from './csv-item' let csvText = '' let revokeSpy: ReturnType beforeEach(() => { csvText = '' vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:csv') revokeSpy = vi .spyOn(URL, 'revokeObjectURL') .mockImplementation(() => undefined) const RealBlob = global.Blob vi.stubGlobal( 'Blob', class extends RealBlob { constructor(parts: BlobPart[], opts?: BlobPropertyBag) { csvText = typeof parts[0] === 'string' ? parts[0] : '' super(parts, opts) } }, ) }) describe('buildCsvDownloadItem', () => { it('returns an item with the canonical CSV shape (id, label, icon)', () => { const item = buildCsvDownloadItem({ filename: 'demo', getRows: () => [] }) expect(item.id).toBe('csv') expect(item.label).toBe('CSV') expect(item.icon).toBeTruthy() }) it('honours a label override', () => { const item = buildCsvDownloadItem({ filename: 'demo', getRows: () => [], label: 'Export data', }) expect(item.label).toBe('Export data') }) it('getRows path serialises rows via toCsvString and returns a .csv handle', async () => { const item = buildCsvDownloadItem({ filename: 'sales', getRows: () => [ ['name', 'value'], ['a', 1], ], }) const handle = await item.resolve() expect(handle.url).toBe('blob:csv') expect(handle.filename).toBe('sales.csv') expect(csvText).toBe('name,value\na,1') handle.revoke?.() expect(revokeSpy).toHaveBeenCalledWith('blob:csv') }) it('getRows path applies the formula-injection guard', async () => { const item = buildCsvDownloadItem({ filename: 'sales', getRows: () => [['=SUM(A1)']], }) await item.resolve() expect(csvText).toBe("'=SUM(A1)") }) it('getCsv path passes the pre-built CSV through verbatim', async () => { const item = buildCsvDownloadItem({ filename: 'table', getCsv: () => 'col\n=raw', }) const handle = await item.resolve() expect(handle.filename).toBe('table.csv') // getCsv is an escape hatch — content is used as-is, no extra escaping. expect(csvText).toBe('col\n=raw') }) })