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