mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Add distributed settings and workspace import/export (M5)
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// The browser file adapter is mocked: export drives downloadJson, import reads
|
||||
// via readTextFile. The service logic (merge, notify) is what we exercise.
|
||||
vi.mock('../infrastructure/file-transfer', () => ({
|
||||
downloadJson: vi.fn(),
|
||||
readTextFile: vi.fn(),
|
||||
}));
|
||||
|
||||
import { createDataset } from '@core/dataset';
|
||||
import { createSnippet } from '@core/snippet';
|
||||
import { downloadJson, readTextFile } from '../infrastructure/file-transfer';
|
||||
import { useDatasetStore } from '../stores/DatasetStore';
|
||||
import { useNotificationStore } from '../stores/NotificationStore';
|
||||
import { useSnippetStore } from '../stores/SnippetStore';
|
||||
import { exportWorkspace, importWorkspace } from './transfer';
|
||||
|
||||
const mockedDownload = vi.mocked(downloadJson);
|
||||
const mockedRead = vi.mocked(readTextFile);
|
||||
|
||||
/** The most recent notification raised. */
|
||||
function lastNote() {
|
||||
const notes = useNotificationStore.getState().notifications;
|
||||
return notes[notes.length - 1];
|
||||
}
|
||||
|
||||
/** Drive an import from a JSON string (readTextFile is mocked to return it). */
|
||||
async function importJson(text: string) {
|
||||
mockedRead.mockResolvedValueOnce(text);
|
||||
await importWorkspace({} as File);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useSnippetStore.getState().reset();
|
||||
useDatasetStore.getState().reset();
|
||||
useNotificationStore.getState().clear();
|
||||
});
|
||||
|
||||
describe('exportWorkspace', () => {
|
||||
it('reports and downloads nothing for an empty library', () => {
|
||||
exportWorkspace(new Date('2026-06-07T00:00:00.000Z'));
|
||||
expect(mockedDownload).not.toHaveBeenCalled();
|
||||
expect(lastNote()).toMatchObject({ kind: 'info' });
|
||||
expect(lastNote().message).toContain('No snippets to export');
|
||||
});
|
||||
|
||||
it('downloads an envelope and reports counts', () => {
|
||||
useSnippetStore.getState().hydrate([createSnippet({ id: 's1', name: 'A' })]);
|
||||
useDatasetStore
|
||||
.getState()
|
||||
.addDatasets([
|
||||
createDataset({ name: 'Sales', data: [{ a: 1 }], format: 'json', source: 'inline' }),
|
||||
]);
|
||||
|
||||
exportWorkspace(new Date('2026-06-07T12:00:00.000Z'));
|
||||
|
||||
expect(mockedDownload).toHaveBeenCalledTimes(1);
|
||||
const [filename, json] = mockedDownload.mock.calls[0];
|
||||
expect(filename).toBe('astrolabe-project-2026-06-07.json');
|
||||
const env = JSON.parse(json) as { version: string; snippets: unknown[]; datasets: unknown[] };
|
||||
expect(env.version).toBe('1.0');
|
||||
expect(env.snippets).toHaveLength(1);
|
||||
expect(env.datasets).toHaveLength(1);
|
||||
expect(lastNote()).toMatchObject({ kind: 'success' });
|
||||
expect(lastNote().message).toBe('Exported 1 snippet and 1 dataset');
|
||||
});
|
||||
});
|
||||
|
||||
describe('importWorkspace', () => {
|
||||
it('reports an invalid JSON file and leaves the workspace untouched', async () => {
|
||||
useSnippetStore.getState().hydrate([createSnippet({ id: 's1', name: 'A' })]);
|
||||
await importJson('{ not json');
|
||||
expect(useSnippetStore.getState().snippets).toHaveLength(1);
|
||||
expect(lastNote()).toMatchObject({ kind: 'error' });
|
||||
expect(lastNote().message).toContain('valid JSON');
|
||||
});
|
||||
|
||||
it('reports an empty file (no snippets found)', async () => {
|
||||
await importJson(JSON.stringify({ version: '1.0', snippets: [] }));
|
||||
expect(useSnippetStore.getState().snippets).toHaveLength(0);
|
||||
expect(lastNote()).toMatchObject({ kind: 'info' });
|
||||
expect(lastNote().message).toBe('No snippets found in file.');
|
||||
});
|
||||
|
||||
it('merges an envelope: datasets and snippets are appended', async () => {
|
||||
await importJson(
|
||||
JSON.stringify({
|
||||
version: '1.0',
|
||||
snippets: [
|
||||
{ id: 's1', created: '2026-01-01T00:00:00.000Z', name: 'A', spec: '{"mark":"bar"}' },
|
||||
],
|
||||
datasets: [{ id: 1, name: 'Sales', data: [{ a: 1 }], format: 'json', source: 'inline' }],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(useSnippetStore.getState().snippets).toHaveLength(1);
|
||||
expect(useDatasetStore.getState().datasets).toHaveLength(1);
|
||||
expect(useDatasetStore.getState().datasets[0].name).toBe('Sales');
|
||||
expect(lastNote()).toMatchObject({ kind: 'success' });
|
||||
expect(lastNote().message).toBe('Imported 1 snippet and 1 dataset');
|
||||
});
|
||||
|
||||
it('auto-suffixes a clashing dataset name and rewrites the importing snippet ref', async () => {
|
||||
useDatasetStore
|
||||
.getState()
|
||||
.addDatasets([
|
||||
createDataset({ name: 'Sales', data: [{ a: 1 }], format: 'json', source: 'inline' }),
|
||||
]);
|
||||
|
||||
await importJson(
|
||||
JSON.stringify({
|
||||
version: '1.0',
|
||||
snippets: [
|
||||
{
|
||||
id: 's9',
|
||||
created: '2026-01-01T00:00:00.000Z',
|
||||
name: 'Ref',
|
||||
spec: '{"data":{"name":"Sales"},"mark":"bar"}',
|
||||
},
|
||||
],
|
||||
datasets: [{ id: 1, name: 'Sales', data: [{ b: 2 }], format: 'json', source: 'inline' }],
|
||||
}),
|
||||
);
|
||||
|
||||
const names = useDatasetStore
|
||||
.getState()
|
||||
.datasets.map((d) => d.name)
|
||||
.sort();
|
||||
expect(names).toEqual(['Sales', 'Sales 2']);
|
||||
|
||||
const imported = useSnippetStore.getState().snippets.find((s) => s.name === 'Ref')!;
|
||||
expect(imported.datasetRefs).toContain('Sales 2');
|
||||
expect(imported.spec).toContain('Sales 2');
|
||||
|
||||
expect(lastNote()).toMatchObject({ kind: 'warning' });
|
||||
expect(lastNote().message).toContain('Sales → Sales 2');
|
||||
});
|
||||
|
||||
it('reassigns a colliding snippet id, keeping the existing one', async () => {
|
||||
useSnippetStore.getState().hydrate([createSnippet({ id: 's1', name: 'Existing' })]);
|
||||
|
||||
await importJson(
|
||||
JSON.stringify({
|
||||
version: '1.0',
|
||||
snippets: [{ id: 's1', created: '2026-01-01T00:00:00.000Z', name: 'Imported', spec: '{}' }],
|
||||
}),
|
||||
);
|
||||
|
||||
const snippets = useSnippetStore.getState().snippets;
|
||||
expect(snippets).toHaveLength(2);
|
||||
const imported = snippets.find((s) => s.name === 'Imported')!;
|
||||
expect(imported.id).not.toBe('s1');
|
||||
expect(snippets.find((s) => s.name === 'Existing')!.id).toBe('s1');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user