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:
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user