Files
astrolabe/src/core/spec-refs.test.ts
T
oleh 693f5d7073 Fix dataset-ref walk false positive and wire spec-mandated success toasts
- Prune the data payload and top-level datasets keys in all three ref walks
  (extractDatasetRefs, renameDatasetInSpec, resolveDatasetRefs) so a data row
  carrying a field named "data" is no longer misread as a library reference,
  spuriously rewritten, or made to throw DatasetNotFoundError. Adds tests,
  including a guard that lookup-transform refs (from.data) still resolve.
- Wire the deferred success toasts now the Toaster has landed: publish, revert,
  extract-to-dataset, and snippet/dataset delete. Copy follows the council
  title-vs-message rule (title states the action, message adds the consequence).
- Reconcile the spec's blanket toast mandate to "toast only what the user can't
  already see": no toast on visible-result creates (snippet, dataset form);
  Copy Reference stays inline and gains an aria-live announcement (new shared
  .visually-hidden utility) instead of a toast-per-copy.
- Move the toast region to bottom-right so it stops covering the header action
  cluster (Publish/Revert, theme/datasets).
- Update docs/spec 01F/02/05 and docs/architecture/07 + 10 to match.
2026-06-05 16:34:58 +03:00

149 lines
5.5 KiB
TypeScript

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');
});
});