mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
133 lines
4.9 KiB
TypeScript
133 lines
4.9 KiB
TypeScript
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"g<h>i|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);
|
||
});
|
||
});
|