import { describe, expect, it } from 'vitest'; import { createShareUrl, decodeCompactViewState, deserializeViewStateFromSearchParams, encodeCompactViewState, parseViewStateSearchParams, serializeViewStateToSearchParams } from './viewStateUrl'; describe('viewStateUrl', () => { it('serializes and parses a deterministic canonical share state', () => { const params = serializeViewStateToSearchParams( { machineId: 'four-stroke-engine', viewMode: 'wireframe', camera: { position: [8, 3, -6], target: [0, 1, 0], fov: 39.567, zoom: 1 }, selectedPartId: 'crankshaft', hiddenPartIds: ['valves', 'piston', 'piston'], partOpacities: { block: 1.2, crankshaft: 0.3333, invalid: Number.NaN }, isExploded: true, explodeAmount: 1.25, showAnnotations: false, showLabels: true }, { precision: 2 } ); expect(params.get('machine')).toBe('four-stroke-engine'); expect(params.get('mode')).toBe('wireframe'); expect(params.get('cam')).toBe('8,3,-6'); expect(params.get('target')).toBe('0,1,0'); expect(params.get('fov')).toBe('39.57'); expect(params.get('zoom')).toBe('1'); expect(params.get('selected')).toBe('crankshaft'); expect(params.get('hidden')).toBe('piston,valves'); expect(params.get('opacity')).toBe('crankshaft:0.33'); expect(params.get('exploded')).toBe('1'); expect(params.get('explode')).toBe('1.25'); expect(params.get('annotations')).toBe('0'); expect(params.has('labels')).toBe(false); const decoded = deserializeViewStateFromSearchParams(params); expect(decoded.machineId).toBe('four-stroke-engine'); expect(decoded.viewMode).toBe('wireframe'); expect(decoded.camera?.position).toEqual([8, 3, -6]); expect(decoded.camera?.target).toEqual([0, 1, 0]); expect(decoded.camera?.fov).toBe(39.57); expect(decoded.selectedPartId).toBe('crankshaft'); expect(decoded.hiddenPartIds).toEqual(['piston', 'valves']); expect(decoded.partOpacities).toEqual({ crankshaft: 0.33 }); expect(decoded.isExploded).toBe(true); expect(decoded.explodeAmount).toBe(1.25); expect(decoded.showAnnotations).toBe(false); }); it('tolerates corrupt URL values and clamps numeric ranges', () => { const decoded = deserializeViewStateFromSearchParams( '?cam=1,two,3&target=0,0,0&zoom=-9&fov=999&explode=-2&opacity=flywheel:2,bad:not-a-number,cover:-0.4&hidden=a,,a&annotations=maybe' ); expect(decoded.camera?.position).toBeUndefined(); expect(decoded.camera?.target).toEqual([0, 0, 0]); expect(decoded.camera?.zoom).toBe(0.05); expect(decoded.camera?.fov).toBe(90); expect(decoded.explodeAmount).toBe(0); expect(decoded.hiddenPartIds).toEqual(['a']); expect(decoded.partOpacities).toEqual({ cover: 0, flywheel: 1 }); expect(decoded.showAnnotations).toBeUndefined(); const parsed = parseViewStateSearchParams('?cam=1,two,3&target=0,0,0'); expect(parsed.warnings).toContain('Ignoring malformed camera position.'); }); it('parses legacy triplet camera params and wireframe flags', () => { const decoded = deserializeViewStateFromSearchParams( '?m=hydraulic-pump&px=10&py=2&pz=3&tx=0&ty=1&tz=0&wireframe=1&selected=bearing&exploded=true' ); expect(decoded.machineId).toBe('hydraulic-pump'); expect(decoded.viewMode).toBe('wireframe'); expect(decoded.camera?.position).toEqual([10, 2, 3]); expect(decoded.camera?.target).toEqual([0, 1, 0]); expect(decoded.selectedPartId).toBe('bearing'); expect(decoded.isExploded).toBe(true); }); it('creates share URLs without discarding unrelated query params or hashes', () => { const shareUrl = createShareUrl( 'https://docs.mechanica.test/viewer?utm=lesson&machine=old&cam=0,0,0#parts', { machineId: 'four-stroke-engine', selectedPartId: 'crankshaft', camera: { position: [1, 2, 3], target: [0, 0, 0] } } ); const parsed = new URL(shareUrl); expect(parsed.searchParams.get('utm')).toBe('lesson'); expect(parsed.searchParams.get('machine')).toBe('four-stroke-engine'); expect(parsed.searchParams.get('cam')).toBe('1,2,3'); expect(parsed.searchParams.get('target')).toBe('0,0,0'); expect(parsed.searchParams.get('selected')).toBe('crankshaft'); expect(parsed.hash).toBe('#parts'); }); it('round-trips compact versioned view-state payloads', () => { const encoded = encodeCompactViewState({ machineId: 'inline-gearbox', viewMode: 'x-ray', camera: { position: [4.12345, 2, -9], target: [0, 0.25, 0], preset: 'iso' }, isolatedPartIds: ['input-shaft', 'output-shaft'], showLabels: false }); const decoded = decodeCompactViewState(encoded); expect(decoded?.machineId).toBe('inline-gearbox'); expect(decoded?.viewMode).toBe('xray'); expect(decoded?.camera?.position).toEqual([4.123, 2, -9]); expect(decoded?.camera?.target).toEqual([0, 0.25, 0]); expect(decoded?.camera?.preset).toBe('iso'); expect(decoded?.isolatedPartIds).toEqual(['input-shaft', 'output-shaft']); expect(decoded?.showLabels).toBe(false); expect(deserializeViewStateFromSearchParams(`?vs=${encodeURIComponent(encoded)}`)).toEqual(decoded); }); });