mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Add dataset library, extract-to-dataset, and render-time reference resolution
This commit is contained in:
+122
-1
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { escapeVegaField, prepareSpecForRender } from './rendering';
|
||||
import {
|
||||
DatasetNotFoundError,
|
||||
escapeVegaField,
|
||||
prepareSpecForRender,
|
||||
type ResolvableDataset,
|
||||
} from './rendering';
|
||||
|
||||
describe('prepareSpecForRender', () => {
|
||||
test('returns a deep copy, never the same reference', () => {
|
||||
@@ -95,6 +100,122 @@ describe('prepareSpecForRender — fit modes (spec §04 Rendering Contract step
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareSpecForRender — dataset resolution (spec §04 Rendering Contract step 1)', () => {
|
||||
const datasets: ResolvableDataset[] = [
|
||||
{ name: 'JsonDs', data: [{ a: 1 }], format: 'json', source: 'inline' },
|
||||
{ name: 'CsvDs', data: 'a,b\n1,2', format: 'csv', source: 'inline' },
|
||||
{ name: 'TsvDs', data: 'a\tb\n1\t2', format: 'tsv', source: 'inline' },
|
||||
{
|
||||
name: 'TopoDs',
|
||||
data: { type: 'Topology', objects: {} },
|
||||
format: 'topojson',
|
||||
source: 'inline',
|
||||
},
|
||||
{ name: 'UrlDs', data: 'https://x/y.csv', format: 'csv', source: 'url' },
|
||||
];
|
||||
|
||||
test('inline JSON → values inlined', () => {
|
||||
const out = prepareSpecForRender({ data: { name: 'JsonDs' }, mark: 'bar' }, { datasets });
|
||||
expect(out.data).toEqual({ values: [{ a: 1 }] });
|
||||
});
|
||||
|
||||
test('inline CSV → raw text inlined, tagged csv', () => {
|
||||
const out = prepareSpecForRender({ data: { name: 'CsvDs' } }, { datasets });
|
||||
expect(out.data).toEqual({ values: 'a,b\n1,2', format: { type: 'csv' } });
|
||||
});
|
||||
|
||||
test('inline TSV → raw text inlined, tagged tsv', () => {
|
||||
const out = prepareSpecForRender({ data: { name: 'TsvDs' } }, { datasets });
|
||||
expect(out.data).toEqual({ values: 'a\tb\n1\t2', format: { type: 'tsv' } });
|
||||
});
|
||||
|
||||
test('inline TopoJSON → value inlined, tagged topojson (preserving feature)', () => {
|
||||
const out = prepareSpecForRender(
|
||||
{ data: { name: 'TopoDs', format: { feature: 'counties' } } },
|
||||
{ datasets },
|
||||
);
|
||||
expect(out.data).toEqual({
|
||||
values: { type: 'Topology', objects: {} },
|
||||
format: { feature: 'counties', type: 'topojson' },
|
||||
});
|
||||
});
|
||||
|
||||
test('URL → url reference tagged with the dataset format', () => {
|
||||
const out = prepareSpecForRender({ data: { name: 'UrlDs' } }, { datasets });
|
||||
expect(out.data).toEqual({ url: 'https://x/y.csv', format: { type: 'csv' } });
|
||||
});
|
||||
|
||||
test('resolves references in nested layer / concat / child sub-specs', () => {
|
||||
const spec = {
|
||||
layer: [{ data: { name: 'JsonDs' } }],
|
||||
spec: { hconcat: [{ data: { name: 'CsvDs' } }] },
|
||||
};
|
||||
const out = prepareSpecForRender(spec, { datasets }) as unknown as {
|
||||
layer: Array<{ data: unknown }>;
|
||||
spec: { hconcat: Array<{ data: unknown }> };
|
||||
};
|
||||
expect(out.layer[0].data).toEqual({ values: [{ a: 1 }] });
|
||||
expect(out.spec.hconcat[0].data).toEqual({ values: 'a,b\n1,2', format: { type: 'csv' } });
|
||||
});
|
||||
|
||||
test('matches dataset names case-insensitively', () => {
|
||||
const out = prepareSpecForRender({ data: { name: 'jsonds' } }, { datasets });
|
||||
expect(out.data).toEqual({ values: [{ a: 1 }] });
|
||||
});
|
||||
|
||||
test('throws DatasetNotFoundError naming the missing dataset', () => {
|
||||
expect(() => prepareSpecForRender({ data: { name: 'Missing' } }, { datasets })).toThrow(
|
||||
DatasetNotFoundError,
|
||||
);
|
||||
expect(() => prepareSpecForRender({ data: { name: 'Missing' } }, { datasets })).toThrow(
|
||||
/Missing/,
|
||||
);
|
||||
});
|
||||
|
||||
test('a library reference with no datasets provided is still not-found', () => {
|
||||
expect(() => prepareSpecForRender({ data: { name: 'Anything' } })).toThrow(
|
||||
DatasetNotFoundError,
|
||||
);
|
||||
});
|
||||
|
||||
test('a spec with no named refs passes through untouched even with no datasets', () => {
|
||||
const spec = { data: { values: [{ a: 1 }] }, mark: 'bar' };
|
||||
expect(prepareSpecForRender(spec)).toEqual(spec);
|
||||
});
|
||||
|
||||
test('a self-defined top-level datasets name is left untouched and does not throw', () => {
|
||||
const spec = { datasets: { local: [{ a: 1 }] }, data: { name: 'local' }, mark: 'bar' };
|
||||
const out = prepareSpecForRender(spec, { datasets });
|
||||
expect(out.data).toEqual({ name: 'local' });
|
||||
});
|
||||
|
||||
test('resolution runs before fit-mode: a ref + fit mode produce both transforms', () => {
|
||||
const spec = { data: { name: 'JsonDs' }, mark: 'bar' };
|
||||
const out = prepareSpecForRender(spec, { datasets, fitMode: 'full' }) as unknown as {
|
||||
data: unknown;
|
||||
width: string;
|
||||
height: string;
|
||||
};
|
||||
expect(out.data).toEqual({ values: [{ a: 1 }] });
|
||||
expect(out.width).toBe('container');
|
||||
expect(out.height).toBe('container');
|
||||
});
|
||||
|
||||
test('copy-not-mutate: the input spec is untouched during resolution', () => {
|
||||
const spec = { data: { name: 'JsonDs' }, mark: 'bar' };
|
||||
const before = structuredClone(spec);
|
||||
prepareSpecForRender(spec, { datasets });
|
||||
expect(spec).toEqual(before);
|
||||
});
|
||||
|
||||
test('preserves pre-existing reference keys other than name', () => {
|
||||
const out = prepareSpecForRender({ data: { name: 'JsonDs', foo: 1 } }, { datasets }) as {
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
expect(out.data).toEqual({ values: [{ a: 1 }], foo: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeVegaField', () => {
|
||||
test('escapes dots and brackets that VL treats as accessors', () => {
|
||||
expect(escapeVegaField('user.age')).toBe('user\\.age');
|
||||
|
||||
Reference in New Issue
Block a user