import { describe, expect, test } from 'vitest'; import { extractDatasetRefs, recomputeDatasetRefs, renameDatasetInSpec } from './spec-refs'; describe('extractDatasetRefs', () => { test('collects a top-level named-data reference', () => { expect(extractDatasetRefs({ data: { name: 'Sales' }, mark: 'bar' })).toEqual(['Sales']); }); test('collects per-layer references', () => { const spec = { layer: [ { data: { name: 'A' }, mark: 'bar' }, { data: { name: 'B' }, mark: 'line' }, ], }; expect(extractDatasetRefs(spec).sort()).toEqual(['A', 'B']); }); test('collects references inside concat and a child spec (facet/repeat)', () => { const spec = { facet: { field: 'g' }, spec: { hconcat: [{ data: { name: 'C' } }, { vconcat: [{ data: { name: 'D' } }] }] }, }; expect(extractDatasetRefs(spec).sort()).toEqual(['C', 'D']); }); test('accepts a JSON-text spec', () => { const spec = JSON.stringify({ data: { name: 'FromString' } }); expect(extractDatasetRefs(spec)).toEqual(['FromString']); }); test('unparseable string yields no refs', () => { expect(extractDatasetRefs('{ not valid json')).toEqual([]); }); test('inline-data and url-data references (no name) are ignored', () => { expect(extractDatasetRefs({ data: { values: [{ a: 1 }] } })).toEqual([]); expect(extractDatasetRefs({ data: { url: 'http://x/y.csv' } })).toEqual([]); }); test('excludes names the spec defines for itself via top-level datasets', () => { const spec = { datasets: { foo: [{ a: 1 }] }, data: { name: 'foo' }, layer: [{ data: { name: 'Library' } }], }; // "foo" is self-defined and not a library dependency; "Library" is. expect(extractDatasetRefs(spec)).toEqual(['Library']); }); test('a data row carrying a field literally named "data" is not a reference', () => { // The inline rows happen to have a column called `data` whose value looks // like a reference object — it is payload, not a library dependency. const spec = { data: { values: [{ data: { name: 'NotARef' } }, { data: { name: 'AlsoNot' } }] }, mark: 'bar', }; expect(extractDatasetRefs(spec)).toEqual([]); }); test('does not descend into a self-defined datasets payload', () => { const spec = { datasets: { local: [{ data: { name: 'Buried' } }] }, mark: 'bar' }; expect(extractDatasetRefs(spec)).toEqual([]); }); test('collects a reference from a lookup transform (from.data)', () => { const spec = { transform: [{ lookup: 'id', from: { data: { name: 'Lookup' }, key: 'id', fields: ['x'] } }], }; expect(extractDatasetRefs(spec)).toEqual(['Lookup']); }); }); describe('recomputeDatasetRefs', () => { test('returns sorted, de-duped names', () => { const spec = { layer: [{ data: { name: 'B' } }, { data: { name: 'A' } }, { data: { name: 'B' } }], }; expect(recomputeDatasetRefs(spec)).toEqual(['A', 'B']); }); }); describe('renameDatasetInSpec', () => { test('rewrites every occurrence of the old name', () => { const spec = { data: { name: 'Old' }, layer: [{ data: { name: 'Old' } }, { data: { name: 'Other' } }], }; const out = renameDatasetInSpec(spec, 'Old', 'New'); expect(out.data.name).toBe('New'); expect(out.layer[0].data.name).toBe('New'); expect(out.layer[1].data.name).toBe('Other'); }); test('preserves other keys on the data object', () => { const spec = { data: { name: 'Old', format: { type: 'csv' } } }; const out = renameDatasetInSpec(spec, 'Old', 'New'); expect(out.data).toEqual({ name: 'New', format: { type: 'csv' } }); }); test('does not mutate the input', () => { const spec = { data: { name: 'Old' } }; const before = structuredClone(spec); renameDatasetInSpec(spec, 'Old', 'New'); expect(spec).toEqual(before); }); test('preserves object shape for an object spec', () => { const out = renameDatasetInSpec({ data: { name: 'Old' } }, 'Old', 'New'); expect(typeof out).toBe('object'); }); test('preserves string shape for a string spec (returns pretty JSON text)', () => { const out = renameDatasetInSpec(JSON.stringify({ data: { name: 'Old' } }), 'Old', 'New'); expect(typeof out).toBe('string'); expect(JSON.parse(out)).toEqual({ data: { name: 'New' } }); }); test('returns an unparseable string spec unchanged', () => { const bad = '{ not json'; expect(renameDatasetInSpec(bad, 'Old', 'New')).toBe(bad); }); test("does not rename a name defined by the spec's own top-level datasets", () => { const spec = { datasets: { Old: [{ a: 1 }] }, data: { name: 'Old' } }; const out = renameDatasetInSpec(spec, 'Old', 'New'); expect(out.data.name).toBe('Old'); // self-defined — left untouched expect(Object.keys(out.datasets)).toEqual(['Old']); }); test('does not rewrite a "data" field buried in inline data rows', () => { const spec = { data: { name: 'Old', values: [{ data: { name: 'Old' } }] }, mark: 'bar', }; const out = renameDatasetInSpec(spec, 'Old', 'New'); expect(out.data.name).toBe('New'); // the real reference is renamed expect(out.data.values[0].data.name).toBe('Old'); // the row payload is left alone }); test('renames a reference inside a lookup transform (from.data)', () => { const spec = { transform: [{ lookup: 'id', from: { data: { name: 'Old' }, key: 'id' } }], }; const out = renameDatasetInSpec(spec, 'Old', 'New'); expect(out.transform[0].from.data.name).toBe('New'); }); });