import { describe, expect, it } from 'vitest'; import { chartExportFilename, inlineReferencedDatasets, MAX_BASENAME_LEN, referencedDatasetNames, snippetFileBasename, } from './chart-export'; import { DatasetNotFoundError, type ResolvableDataset } from './rendering'; describe('snippetFileBasename', () => { it('keeps the user’s words and case', () => { expect(snippetFileBasename('Sales by Region')).toBe('Sales-by-Region'); }); it('swaps whitespace runs for a single dash', () => { expect(snippetFileBasename(' a b c ')).toBe('a-b-c'); }); it('folds dots into the separator so the extension stays unambiguous', () => { expect(snippetFileBasename('data.v1.final')).toBe('data-v1-final'); }); it('strips characters illegal on Windows / awkward in URLs', () => { expect(snippetFileBasename('a/b\\c:d*e?f"gi|j')).toBe('abcdefghij'); }); it('keeps non-latin letters (no ASCII folding — full-script support)', () => { expect(snippetFileBasename('Продажи по региону')).toBe('Продажи-по-региону'); expect(snippetFileBasename('売上 グラフ')).toBe('売上-グラフ'); }); it('collapses and trims dashes', () => { expect(snippetFileBasename('--a -- b--')).toBe('a-b'); }); it('falls back to "chart" when nothing usable remains', () => { expect(snippetFileBasename('')).toBe('chart'); expect(snippetFileBasename(' ')).toBe('chart'); expect(snippetFileBasename('/// \\\\\\')).toBe('chart'); expect(snippetFileBasename('...')).toBe('chart'); }); it('caps the length and never leaves a trailing dash from the cut', () => { const long = snippetFileBasename('x'.repeat(200)); expect(long.length).toBe(MAX_BASENAME_LEN); // A name whose cap boundary lands on a dash must not end in one. const dashy = snippetFileBasename('a'.repeat(MAX_BASENAME_LEN - 1) + ' bbbb'); expect(dashy.endsWith('-')).toBe(false); }); it('drops control characters', () => { // Build the control byte from a code point so no literal control char is in source. expect(snippetFileBasename(`a${String.fromCharCode(1)}bcd`)).toBe('abcd'); }); }); describe('chartExportFilename', () => { it('appends the format as the extension', () => { expect(chartExportFilename('Sales by Region', 'png')).toBe('Sales-by-Region.png'); expect(chartExportFilename('Sales by Region', 'svg')).toBe('Sales-by-Region.svg'); expect(chartExportFilename('Sales by Region', 'vl.json')).toBe('Sales-by-Region.vl.json'); }); it('uses the "chart" fallback for an unusable name', () => { expect(chartExportFilename('', 'png')).toBe('chart.png'); }); }); describe('referencedDatasetNames', () => { it('returns the saved-dataset names a spec references, deduped', () => { const spec = JSON.stringify({ layer: [ { data: { name: 'sales' } }, { data: { name: 'sales' } }, { data: { name: 'costs' } }, ], }); expect(referencedDatasetNames(spec).sort()).toEqual(['costs', 'sales']); }); it('excludes names the spec defines for itself via top-level datasets', () => { const spec = JSON.stringify({ datasets: { local: [{ a: 1 }] }, data: { name: 'local' }, }); expect(referencedDatasetNames(spec)).toEqual([]); }); it('returns [] for an inline-data spec (no references)', () => { const spec = JSON.stringify({ data: { values: [{ a: 1 }] }, mark: 'bar' }); expect(referencedDatasetNames(spec)).toEqual([]); }); it('returns [] for unparseable text rather than throwing', () => { expect(referencedDatasetNames('{ not json')).toEqual([]); }); }); describe('inlineReferencedDatasets', () => { const sales: ResolvableDataset = { name: 'sales', data: [{ region: 'N', value: 10 }], format: 'json', source: 'inline', }; const parse = (text: string) => JSON.parse(text) as { data?: unknown; width?: unknown }; it('replaces a named reference with the dataset’s inline values', () => { const spec = JSON.stringify({ data: { name: 'sales' }, mark: 'bar' }); const out = parse(inlineReferencedDatasets(spec, [sales])); expect(out.data).toEqual({ values: [{ region: 'N', value: 10 }] }); }); it('resolves the name case-insensitively (mirrors reference resolution)', () => { const spec = JSON.stringify({ data: { name: 'Sales' }, mark: 'bar' }); const out = parse(inlineReferencedDatasets(spec, [sales])); expect(out.data).toEqual({ values: [{ region: 'N', value: 10 }] }); }); it('leaves sizing as authored — no fit-mode applied', () => { const spec = JSON.stringify({ data: { name: 'sales' }, width: 300, mark: 'bar' }); const out = parse(inlineReferencedDatasets(spec, [sales])); expect(out.width).toBe(300); }); it('throws DatasetNotFoundError when a referenced dataset is missing', () => { const spec = JSON.stringify({ data: { name: 'missing' }, mark: 'bar' }); expect(() => inlineReferencedDatasets(spec, [sales])).toThrow(DatasetNotFoundError); }); });