import { describe, expect, it } from 'vitest'; import { canonicalizeViewerShareSearch, createViewerShareUrl, decodeViewerShareState, encodeViewerShareState, isSafeShareIdentifier, type ViewerShareState, } from '../src/utils/viewStateCodec'; describe('viewer share-link state codec', () => { it('encodes a deterministic compact v1 query string', () => { const state: ViewerShareState = { machineId: 'inline-four-engine', cameraPreset: 'front-cutaway', cameraPosition: [12.34567, 0, -4], cameraTarget: [0, 1.5, 0], renderMode: 'section', exploded: 0.625, playback: 'playing', timeScale: 1.5, selectedComponent: 'piston-1', hiddenComponents: ['valve-cover', 'piston-1', 'valve-cover'], isolatedComponents: ['crankshaft'], componentOpacity: { piston: 1, 'engine-block': 0.35, }, sectionPlane: { axis: 'x', offset: -0.125, }, labels: true, dimensions: false, }; expect(encodeViewerShareState(state)).toBe( '?v=1&m=inline-four-engine&cam=front-cutaway&pos=12.3457,0,-4&target=0,1.5,0&mode=section&explode=0.625&play=1&speed=1.5&select=piston-1&hide=piston-1,valve-cover&iso=crankshaft&opacity=engine-block:0.35;piston:1§ion=x:-0.125&labels=1&dims=0', ); }); it('round-trips canonical links without warnings', () => { const search = '?v=1&m=inline-four-engine&cam=front-cutaway&pos=12.3457,0,-4&target=0,1.5,0&mode=section&explode=0.625&play=1&speed=1.5&select=piston-1&hide=piston-1,valve-cover&iso=crankshaft&opacity=engine-block:0.35;piston:1§ion=x:-0.125&labels=1&dims=0'; const result = decodeViewerShareState(search); expect(result.warnings).toEqual([]); expect(result.migrated).toBe(false); expect(result.canonicalSearch).toBe(search); expect(result.state).toEqual({ schemaVersion: 1, machineId: 'inline-four-engine', cameraPreset: 'front-cutaway', cameraPosition: [12.3457, 0, -4], cameraTarget: [0, 1.5, 0], renderMode: 'section', exploded: 0.625, playback: 'playing', timeScale: 1.5, selectedComponent: 'piston-1', hiddenComponents: ['piston-1', 'valve-cover'], isolatedComponents: ['crankshaft'], componentOpacity: { 'engine-block': 0.35, piston: 1, }, sectionPlane: { axis: 'x', offset: -0.125, }, labels: true, dimensions: false, }); }); it('normalizes legacy aliases into the canonical v1 contract', () => { const result = decodeViewerShareState( '?machine=gear-pump&view=iso&camera=1,2,3&lookAt=0,0,0&display=cross-section&exploded=.4&playing=false&component=rotor&hidden=cover,bolt&isolate=rotor&opacities=cover=.25&clip=z=0.75&showLabels=yes&measurements=no', ); expect(result.migrated).toBe(true); expect(result.warnings).toEqual([]); expect(result.state).toMatchObject({ machineId: 'gear-pump', cameraPreset: 'iso', cameraPosition: [1, 2, 3], cameraTarget: [0, 0, 0], renderMode: 'section', exploded: 0.4, playback: 'paused', selectedComponent: 'rotor', hiddenComponents: ['bolt', 'cover'], isolatedComponents: ['rotor'], componentOpacity: { cover: 0.25, }, sectionPlane: { axis: 'z', offset: 0.75, }, labels: true, dimensions: false, }); expect(result.canonicalSearch).toBe( '?v=1&m=gear-pump&cam=iso&pos=1,2,3&target=0,0,0&mode=section&explode=0.4&play=0&select=rotor&hide=bolt,cover&iso=rotor&opacity=cover:0.25§ion=z:0.75&labels=1&dims=0', ); }); it('clamps unsafe numeric values and drops unsafe identifiers instead of trusting the URL', () => { const result = decodeViewerShareState( '?v=99&m=%3Cscript%3E&explode=5&speed=-1&zoom=NaN&pos=1,Infinity,3&mode=paint&play=maybe§ion=q:9999&hide=valid,%3Cbad%3E,valid&opacity=wheel:2;bad%20id:0.5', { allowedComponentIds: ['valid', 'wheel'], }, ); expect(result.migrated).toBe(true); expect(result.state.machineId).toBeUndefined(); expect(result.state.exploded).toBe(1); expect(result.state.timeScale).toBe(0); expect(result.state.zoom).toBeUndefined(); expect(result.state.cameraPosition).toBeUndefined(); expect(result.state.renderMode).toBeUndefined(); expect(result.state.playback).toBeUndefined(); expect(result.state.sectionPlane).toBeUndefined(); expect(result.state.hiddenComponents).toEqual(['valid']); expect(result.state.componentOpacity).toEqual({ wheel: 1, }); expect(result.warnings.map(({ code }) => code)).toEqual( expect.arrayContaining([ 'unsupported-version', 'invalid-id', 'clamped-number', 'invalid-number', 'invalid-enum', 'invalid-section', ]), ); }); it('can intentionally clear collection defaults with empty canonical parameters', () => { const defaults: ViewerShareState = { renderMode: 'solid', labels: false, hiddenComponents: ['cover'], componentOpacity: { cover: 0.4, }, }; const cleared: ViewerShareState = { renderMode: 'solid', labels: false, hiddenComponents: [], componentOpacity: {}, }; expect(encodeViewerShareState(cleared, { defaultState: defaults })).toBe( '?v=1&hide=&opacity=', ); const result = decodeViewerShareState('?v=1&hide=&opacity=', { defaultState: defaults, }); expect(result.warnings).toEqual([]); expect(result.state.hiddenComponents).toEqual([]); expect(result.state.componentOpacity).toEqual({}); expect(result.state.renderMode).toBe('solid'); expect(result.state.labels).toBe(false); expect(result.canonicalSearch).toBe('?v=1&hide=&opacity='); }); it('extracts query state from hash-router and full-URL share links', () => { const result = decodeViewerShareState( 'https://mechanica.example/#/viewer/inline-four?m=inline-four&mode=wire&labels=on', ); expect(result.migrated).toBe(true); expect(result.state).toMatchObject({ machineId: 'inline-four', renderMode: 'wireframe', labels: true, }); expect(result.canonicalSearch).toBe('?v=1&m=inline-four&mode=wireframe&labels=1'); }); it('builds share URLs without dropping existing route fragments or query params', () => { expect( createViewerShareUrl('/viewer/inline-four#components', { machineId: 'inline-four', exploded: 0.5, }), ).toBe('/viewer/inline-four?v=1&m=inline-four&explode=0.5#components'); expect( createViewerShareUrl('/viewer?foo=bar', { machineId: 'pump', }), ).toBe('/viewer?foo=bar&v=1&m=pump'); }); it('canonicalizes duplicate and unordered component lists', () => { expect(canonicalizeViewerShareSearch('?v=1&hide=z,a,z&m=machine')).toBe( '?v=1&m=machine&hide=a,z', ); }); it('can constrain decoded IDs to the active machine registry', () => { const result = decodeViewerShareState('?v=1&m=unknown&select=known&hide=unknown,known', { allowedMachineIds: ['known-machine'], allowedComponentIds: ['known'], }); expect(result.state.machineId).toBeUndefined(); expect(result.state.selectedComponent).toBe('known'); expect(result.state.hiddenComponents).toEqual(['known']); expect(result.warnings.filter(({ code }) => code === 'unknown-id')).toHaveLength(2); expect(result.canonicalSearch).toBe('?v=1&select=known&hide=known'); }); it('rejects identifiers that could escape the share-link grammar', () => { expect(isSafeShareIdentifier('rotor.stage-1:blade_A')).toBe(true); expect(isSafeShareIdentifier('../rotor')).toBe(false); expect(isSafeShareIdentifier('rotor blade')).toBe(false); expect(isSafeShareIdentifier('')).toBe(false); }); });