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