mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
150 lines
5.7 KiB
TypeScript
150 lines
5.7 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import {
|
|
parseHash,
|
|
parseExampleHash,
|
|
serializeHash,
|
|
readView,
|
|
replaceView,
|
|
pushView,
|
|
type ViewState,
|
|
} from './url-hash';
|
|
|
|
/** One value per `ViewState` variant — the round-trip and serialize coverage set. */
|
|
const ALL_VIEWS: ViewState[] = [
|
|
{ kind: 'snippets' },
|
|
{ kind: 'snippet', snippetId: 'abc123' },
|
|
{ kind: 'datasets' },
|
|
{ kind: 'dataset', datasetId: 7 },
|
|
{ kind: 'dataset-new' },
|
|
{ kind: 'dataset-build', datasetId: 42 },
|
|
{ kind: 'build' },
|
|
];
|
|
|
|
describe('serializeHash', () => {
|
|
it('produces the spec grammar form for each variant', () => {
|
|
expect(serializeHash({ kind: 'snippets' })).toBe('');
|
|
expect(serializeHash({ kind: 'snippet', snippetId: 'abc123' })).toBe('#snippet-abc123');
|
|
expect(serializeHash({ kind: 'datasets' })).toBe('#datasets');
|
|
expect(serializeHash({ kind: 'dataset', datasetId: 7 })).toBe('#datasets/dataset-7');
|
|
expect(serializeHash({ kind: 'dataset-new' })).toBe('#datasets/new');
|
|
expect(serializeHash({ kind: 'dataset-build', datasetId: 42 })).toBe(
|
|
'#datasets/dataset-42/build',
|
|
);
|
|
expect(serializeHash({ kind: 'build' })).toBe('#build');
|
|
});
|
|
});
|
|
|
|
describe('parseHash', () => {
|
|
it('parses each grammar row to the right ViewState', () => {
|
|
expect(parseHash('')).toEqual({ kind: 'snippets' });
|
|
expect(parseHash('#snippet-abc123')).toEqual({ kind: 'snippet', snippetId: 'abc123' });
|
|
expect(parseHash('#datasets')).toEqual({ kind: 'datasets' });
|
|
expect(parseHash('#datasets/dataset-7')).toEqual({ kind: 'dataset', datasetId: 7 });
|
|
expect(parseHash('#datasets/new')).toEqual({ kind: 'dataset-new' });
|
|
expect(parseHash('#datasets/dataset-42/build')).toEqual({
|
|
kind: 'dataset-build',
|
|
datasetId: 42,
|
|
});
|
|
expect(parseHash('#build')).toEqual({ kind: 'build' });
|
|
});
|
|
|
|
it('tolerates a missing leading "#"', () => {
|
|
expect(parseHash('datasets')).toEqual({ kind: 'datasets' });
|
|
expect(parseHash('snippet-x')).toEqual({ kind: 'snippet', snippetId: 'x' });
|
|
});
|
|
|
|
it('treats a snippet id as an opaque string (slashes, dashes, etc.)', () => {
|
|
expect(parseHash('#snippet-a-b-c')).toEqual({ kind: 'snippet', snippetId: 'a-b-c' });
|
|
expect(parseHash('#snippet-2026-06-07')).toEqual({ kind: 'snippet', snippetId: '2026-06-07' });
|
|
});
|
|
|
|
it('falls back to the default snippets view on malformed / unknown hashes', () => {
|
|
const fallback: ViewState = { kind: 'snippets' };
|
|
expect(parseHash('#snippet-')).toEqual(fallback); // empty snippet id
|
|
expect(parseHash('#datasets/dataset-abc')).toEqual(fallback); // non-numeric dataset id
|
|
expect(parseHash('#datasets/dataset-')).toEqual(fallback); // empty dataset id
|
|
expect(parseHash('#datasets/dataset-7/extra')).toEqual(fallback); // unknown trailing segment
|
|
expect(parseHash('#datasets/bogus')).toEqual(fallback); // unknown datasets sub-route
|
|
expect(parseHash('#garbage')).toEqual(fallback); // unknown top-level
|
|
});
|
|
|
|
it('never throws for arbitrary input', () => {
|
|
for (const raw of ['#', '#/', '#//', '#datasets//', '#snippet', '###']) {
|
|
expect(() => parseHash(raw)).not.toThrow();
|
|
expect(parseHash(raw).kind).toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('parseExampleHash', () => {
|
|
it('extracts the example id from the one-shot action link', () => {
|
|
expect(parseExampleHash('#example-brush')).toBe('brush');
|
|
expect(parseExampleHash('example-bar')).toBe('bar'); // leading "#" optional
|
|
});
|
|
|
|
it('returns null for anything that is not an example link', () => {
|
|
expect(parseExampleHash('')).toBeNull();
|
|
expect(parseExampleHash('#example-')).toBeNull(); // empty id
|
|
expect(parseExampleHash('#snippet-abc')).toBeNull();
|
|
expect(parseExampleHash('#build')).toBeNull();
|
|
});
|
|
|
|
it('is not a ViewState: parseHash degrades an example link to the default view', () => {
|
|
expect(parseHash('#example-brush')).toEqual({ kind: 'snippets' });
|
|
});
|
|
});
|
|
|
|
describe('round-trip identity', () => {
|
|
it('parseHash(serializeHash(v)) deep-equals v for every variant', () => {
|
|
for (const v of ALL_VIEWS) {
|
|
expect(parseHash(serializeHash(v))).toEqual(v);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('readView / replaceView / pushView (window + history)', () => {
|
|
beforeEach(() => {
|
|
// Reset to a clean, hash-less location before each case.
|
|
window.history.replaceState({}, '', '/');
|
|
});
|
|
|
|
it('readView reflects the current location.hash', () => {
|
|
window.location.hash = '#datasets/dataset-7';
|
|
expect(readView()).toEqual({ kind: 'dataset', datasetId: 7 });
|
|
});
|
|
|
|
it('readView returns the default for an empty hash', () => {
|
|
expect(readView()).toEqual({ kind: 'snippets' });
|
|
});
|
|
|
|
it('replaceView writes the hash without growing history', () => {
|
|
const before = window.history.length;
|
|
replaceView({ kind: 'snippet', snippetId: 'xyz' });
|
|
expect(window.location.hash).toBe('#snippet-xyz');
|
|
expect(readView()).toEqual({ kind: 'snippet', snippetId: 'xyz' });
|
|
expect(window.history.length).toBe(before);
|
|
});
|
|
|
|
it('replaceView clears the hash for the default view', () => {
|
|
window.location.hash = '#datasets';
|
|
replaceView({ kind: 'snippets' });
|
|
expect(window.location.hash).toBe('');
|
|
expect(readView()).toEqual({ kind: 'snippets' });
|
|
});
|
|
|
|
it('pushView writes the hash and adds a history entry', () => {
|
|
const before = window.history.length;
|
|
pushView({ kind: 'dataset-build', datasetId: 42 });
|
|
expect(window.location.hash).toBe('#datasets/dataset-42/build');
|
|
expect(readView()).toEqual({ kind: 'dataset-build', datasetId: 42 });
|
|
expect(window.history.length).toBe(before + 1);
|
|
});
|
|
|
|
it('round-trips through the real history API for every variant', () => {
|
|
for (const v of ALL_VIEWS) {
|
|
replaceView(v);
|
|
expect(readView()).toEqual(v);
|
|
}
|
|
});
|
|
});
|