Add dataset library, extract-to-dataset, and render-time reference resolution

This commit is contained in:
2026-06-05 15:49:40 +03:00
parent 25849461e0
commit a4e4d96d3b
41 changed files with 3909 additions and 19 deletions
+144
View File
@@ -0,0 +1,144 @@
import { beforeEach, describe, expect, test } from 'vitest';
import { createDataset } from '@core/dataset';
import { createSnippet } from '@core/snippet';
import { selectSelectedDataset, useDatasetStore, type DatasetForm } from './DatasetStore';
import { useSnippetStore } from './SnippetStore';
const store = () => useDatasetStore.getState();
const T = new Date('2026-06-01T00:00:00Z');
beforeEach(() => {
store().reset();
useSnippetStore.getState().reset();
});
describe('save — create', () => {
test('valid inline JSON is added, profiled, and selected', () => {
store().startCreate();
store().updateForm({ name: 'Sales', input: '[{"a":1,"b":2},{"a":3,"b":4}]' });
expect(store().save(T)).toBe(true);
const ds = selectSelectedDataset(store());
expect(ds?.name).toBe('Sales');
expect(ds?.format).toBe('json');
expect(ds?.rowCount).toBe(2);
expect(ds?.columns).toEqual(['a', 'b']);
expect(store().view).toBe('detail');
});
test('CSV input is stored as raw text and profiled', () => {
store().startCreate();
store().updateForm({ name: 'Regions', input: 'city,pop\nA,10\nB,20' });
expect(store().save(T)).toBe(true);
const ds = selectSelectedDataset(store());
expect(ds?.format).toBe('csv');
expect(ds?.data).toBe('city,pop\nA,10\nB,20');
expect(ds?.rowCount).toBe(2);
});
test('a valid URL infers format from the extension', () => {
store().startCreate();
store().updateForm({ name: 'Remote', source: 'url', input: 'https://example.com/data.csv' });
expect(store().save(T)).toBe(true);
const ds = selectSelectedDataset(store());
expect(ds?.source).toBe('url');
expect(ds?.format).toBe('csv');
expect(ds?.rowCount).toBeNull(); // URL datasets aren't profiled
});
});
describe('save — validation', () => {
const submit = (patch: Partial<DatasetForm>) => {
store().startCreate();
store().updateForm(patch);
return store().save(T);
};
test('blank name is rejected', () => {
expect(submit({ name: ' ', input: '[{"a":1}]' })).toBe(false);
expect(store().formError).toMatch(/name/i);
expect(store().datasets).toHaveLength(0);
});
test('duplicate name (case-insensitive) is rejected', () => {
store().add(
createDataset({ name: 'Sales', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
);
expect(submit({ name: 'sales', input: '[{"a":1}]' })).toBe(false);
expect(store().formError).toMatch(/already exists/i);
});
test('empty and unrecognized inline data are rejected', () => {
expect(submit({ name: 'Empty', input: ' ' })).toBe(false);
expect(submit({ name: 'Junk', input: 'this is not data' })).toBe(false);
expect(store().formError).toMatch(/valid JSON/i);
});
test('a non-http URL is rejected', () => {
expect(submit({ name: 'Bad', source: 'url', input: 'ftp://x/y.csv' })).toBe(false);
expect(store().formError).toMatch(/url/i);
});
});
describe('save — edit', () => {
test('updating inline data re-profiles and advances modified', () => {
store().add(
createDataset({ name: 'D', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
);
const id = store().selectedId!;
store().startEdit();
store().updateForm({ input: '[{"a":1,"b":2},{"a":3,"b":4}]' });
const later = new Date('2026-07-01T00:00:00Z');
expect(store().save(later)).toBe(true);
const ds = store().datasets.find((d) => d.id === id)!;
expect(ds.columnCount).toBe(2);
expect(ds.modified).toBe(later.toISOString());
});
test('renaming a referenced dataset propagates into referencing snippets', () => {
store().add(
createDataset({ name: 'Sales', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
);
const snippet = createSnippet({
id: 's1',
spec: JSON.stringify({ data: { name: 'Sales' }, mark: 'bar' }),
now: T,
});
useSnippetStore.getState().hydrate([snippet], 's1');
useSnippetStore.getState().publish(T); // seed datasetRefs = ['Sales']
store().startEdit();
store().updateForm({ name: 'Revenue' });
expect(store().save(new Date('2026-08-01T00:00:00Z'))).toBe(true);
expect(store().datasets[0].name).toBe('Revenue');
const s = useSnippetStore.getState().snippets.find((x) => x.id === 's1')!;
expect(s.datasetRefs).toEqual(['Revenue']);
expect(s.spec).toContain('"Revenue"');
});
});
describe('remove & view transitions', () => {
test('removing the selected dataset clears the selection and returns to the list', () => {
store().add(
createDataset({ name: 'D', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
);
const id = store().selectedId!;
store().remove(id);
expect(store().selectedId).toBeNull();
expect(store().view).toBe('list');
});
test('cancelForm returns to detail when a dataset is selected', () => {
store().add(
createDataset({ name: 'D', data: [{ a: 1 }], format: 'json', source: 'inline', now: T }),
);
store().startEdit();
expect(store().view).toBe('edit');
store().cancelForm();
expect(store().view).toBe('detail');
});
});