mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
f365258fcc
The hash becomes the single serialized view (snippet / Datasets list / a dataset / new-dataset form / Chart Builder). url-hash is the sole reader/writer of location+history (total, never-throwing parse; hash-only canonical URLs). UrlStateSync grows from the modal↔URL seam into the full router: state→hash is derived from the stores (so in-modal dataset sub-views reach the hash), hash→state restores on load and on Back/Forward, validating ids and cleaning stale links. startRouting() runs after store hydrate.
149 lines
5.7 KiB
TypeScript
149 lines
5.7 KiB
TypeScript
import { beforeEach, describe, expect, test } from 'vitest';
|
|
import { createDataset } from '@core/dataset';
|
|
import { createSnippet } from '@core/snippet';
|
|
import { useAppStore } from '../stores/AppStore';
|
|
import { useChartBuilderStore } from '../stores/ChartBuilderStore';
|
|
import { useDatasetStore } from '../stores/DatasetStore';
|
|
import { useSnippetStore } from '../stores/SnippetStore';
|
|
import { applyView, deriveViewState } from './UrlStateSync';
|
|
|
|
const T = new Date('2026-06-01T00:00:00Z');
|
|
|
|
const seedDataset = (id: number, name: string) =>
|
|
createDataset({ id, name, data: [{ a: 1 }], format: 'json', source: 'inline', now: T });
|
|
|
|
beforeEach(() => {
|
|
useSnippetStore.getState().reset();
|
|
useDatasetStore.getState().reset();
|
|
useChartBuilderStore.setState({ datasetId: null });
|
|
useAppStore.getState().setActiveModal(null);
|
|
window.location.hash = '';
|
|
});
|
|
|
|
describe('deriveViewState — the URL is derived from store state', () => {
|
|
test('no snippet, no modal → default snippets view', () => {
|
|
useSnippetStore.getState().hydrate([], null);
|
|
expect(deriveViewState()).toEqual({ kind: 'snippets' });
|
|
});
|
|
|
|
test('an active snippet → #snippet-<id>', () => {
|
|
useSnippetStore.getState().hydrate([createSnippet({ id: 's1', now: T })], 's1');
|
|
expect(deriveViewState()).toEqual({ kind: 'snippet', snippetId: 's1' });
|
|
});
|
|
|
|
test('Datasets manager (list) → #datasets', () => {
|
|
useAppStore.getState().setActiveModal('datasets');
|
|
expect(deriveViewState()).toEqual({ kind: 'datasets' });
|
|
});
|
|
|
|
test('a selected dataset → #datasets/dataset-<id>', () => {
|
|
useDatasetStore.getState().hydrate([seedDataset(7, 'Sales')]);
|
|
useDatasetStore.getState().select(7);
|
|
useAppStore.getState().setActiveModal('datasets');
|
|
expect(deriveViewState()).toEqual({ kind: 'dataset', datasetId: 7 });
|
|
});
|
|
|
|
test('the new-dataset form → #datasets/new', () => {
|
|
useDatasetStore.getState().startCreate();
|
|
useAppStore.getState().setActiveModal('datasets');
|
|
expect(deriveViewState()).toEqual({ kind: 'dataset-new' });
|
|
});
|
|
|
|
test('the Chart Builder → #datasets/dataset-<id>/build', () => {
|
|
useChartBuilderStore.setState({ datasetId: 42 });
|
|
useAppStore.getState().setActiveModal('chartBuilder');
|
|
expect(deriveViewState()).toEqual({ kind: 'dataset-build', datasetId: 42 });
|
|
});
|
|
|
|
test('a non-navigable modal (extract) leaves the underlying snippet view in the URL', () => {
|
|
useSnippetStore.getState().hydrate([createSnippet({ id: 's1', now: T })], 's1');
|
|
useAppStore.getState().setActiveModal('extract');
|
|
expect(deriveViewState()).toEqual({ kind: 'snippet', snippetId: 's1' });
|
|
});
|
|
});
|
|
|
|
describe('applyView — the hash drives the stores (restore / Back-Forward)', () => {
|
|
test('restoring a snippet selects it and closes any modal', () => {
|
|
useSnippetStore.getState().hydrate([createSnippet({ id: 's1', now: T })], null);
|
|
useAppStore.getState().setActiveModal('datasets');
|
|
|
|
applyView({ kind: 'snippet', snippetId: 's1' });
|
|
|
|
expect(useAppStore.getState().activeModal).toBeNull();
|
|
expect(useSnippetStore.getState().activeSnippetId).toBe('s1');
|
|
});
|
|
|
|
test('a dead snippet id falls back to the default view and cleans the URL', () => {
|
|
useSnippetStore.getState().hydrate([], null);
|
|
window.location.hash = '#snippet-gone';
|
|
|
|
applyView({ kind: 'snippet', snippetId: 'gone' });
|
|
|
|
expect(useSnippetStore.getState().activeSnippetId).toBeNull();
|
|
expect(window.location.hash).toBe('');
|
|
});
|
|
|
|
test('restoring the Datasets list opens the manager on its list view', () => {
|
|
applyView({ kind: 'datasets' });
|
|
expect(useAppStore.getState().activeModal).toBe('datasets');
|
|
expect(useDatasetStore.getState().view).toBe('list');
|
|
});
|
|
|
|
test('restoring a specific dataset selects it in the open manager', () => {
|
|
useDatasetStore.getState().hydrate([seedDataset(7, 'Sales')]);
|
|
|
|
applyView({ kind: 'dataset', datasetId: 7 });
|
|
|
|
expect(useAppStore.getState().activeModal).toBe('datasets');
|
|
expect(useDatasetStore.getState().selectedId).toBe(7);
|
|
expect(useDatasetStore.getState().view).toBe('detail');
|
|
});
|
|
|
|
test('a dead dataset id degrades to the Datasets list and cleans the URL', () => {
|
|
useDatasetStore.getState().hydrate([]);
|
|
window.location.hash = '#datasets/dataset-99';
|
|
|
|
applyView({ kind: 'dataset', datasetId: 99 });
|
|
|
|
expect(useAppStore.getState().activeModal).toBe('datasets');
|
|
expect(useDatasetStore.getState().selectedId).toBeNull();
|
|
expect(window.location.hash).toBe('#datasets');
|
|
});
|
|
|
|
test('restoring the new-dataset form opens the manager on its create view', () => {
|
|
applyView({ kind: 'dataset-new' });
|
|
expect(useAppStore.getState().activeModal).toBe('datasets');
|
|
expect(useDatasetStore.getState().view).toBe('new');
|
|
});
|
|
|
|
test('restoring the Chart Builder loads its dataset and opens the builder', () => {
|
|
useDatasetStore.getState().hydrate([seedDataset(7, 'Sales')]);
|
|
|
|
applyView({ kind: 'dataset-build', datasetId: 7 });
|
|
|
|
expect(useAppStore.getState().activeModal).toBe('chartBuilder');
|
|
expect(useChartBuilderStore.getState().datasetId).toBe(7);
|
|
});
|
|
});
|
|
|
|
describe('round-trip: applyView then deriveViewState is identity', () => {
|
|
test('every store-representable view survives the round trip', () => {
|
|
useSnippetStore.getState().hydrate([createSnippet({ id: 's1', now: T })], null);
|
|
useDatasetStore.getState().hydrate([seedDataset(7, 'Sales')]);
|
|
|
|
const views = [
|
|
{ kind: 'snippets' as const },
|
|
{ kind: 'snippet' as const, snippetId: 's1' },
|
|
{ kind: 'datasets' as const },
|
|
{ kind: 'dataset' as const, datasetId: 7 },
|
|
{ kind: 'dataset-new' as const },
|
|
{ kind: 'dataset-build' as const, datasetId: 7 },
|
|
];
|
|
|
|
for (const view of views) {
|
|
applyView(view);
|
|
expect(deriveViewState()).toEqual(view);
|
|
}
|
|
});
|
|
});
|