yslootahrobotics/src/hooks/useUrlSync.test.ts
Najjar\NajjarV02 03dbc4ac98 feat(configure): add Basic vs EDU robot body selector under Persona Attire
- New `activeBody` field in config store with `setBody` action and URL sync (`b` key, defaults to `basic` for backward compat)
- Robot Body section in ConfigPanel between Persona Attire and Pricing
- RobotModel base mesh extracted into BaseBodyMesh subcomponent wrapped in error boundary so a missing EDU GLB silently falls back to basic
- Tests cover defaults, setBody, reset, and round-trip serialization

Note: drop `/public/Unitree_G1_EDU.glb` to enable the EDU variant — until then EDU selection falls back to basic with a console warning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:11:14 +04:00

115 lines
4.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { configStore, serializeConfig, deserializeConfig } from '@/store/useConfigStore';
// Mock window.history
const mockReplaceState = vi.fn();
const mockPushState = vi.fn();
const mockAddEventListener = vi.fn();
const mockRemoveEventListener = vi.fn();
vi.stubGlobal('window', {
location: {
href: 'http://localhost:3000',
search: '',
},
history: {
replaceState: mockReplaceState,
pushState: mockPushState,
state: {},
},
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
});
describe('URL Serialization', () => {
describe('serializeConfig', () => {
it('should serialize colors, persona, and payloads to base64', () => {
const encoded = serializeConfig({
activeColors: { primary: '#ff0000', secondary: '#00ff00', accent: '#0000ff' },
activePersonaAttire: 'Emarati Kandura',
activeBody: 'basic',
activePayloads: [{ id: 'cam1', type: 'camera', position: 'head' }],
isHydrated: true,
});
expect(typeof encoded).toBe('string');
// Verify it decodes correctly
const decoded = JSON.parse(atob(encoded));
expect(decoded.c.primary).toBe('#ff0000');
expect(decoded.p).toBe('Emarati Kandura');
expect(decoded.y[0].id).toBe('cam1');
});
});
describe('deserializeConfig', () => {
it('should decode base64 and return valid state', () => {
const original = {
activeColors: { primary: '#ff0000', secondary: '#00ff00', accent: '#0000ff' },
activePersonaAttire: 'Industrial Vest',
activeBody: 'edu' as const,
activePayloads: [
{ id: 'cam1', type: 'camera', position: 'head' },
{ id: 'light1', type: 'light', position: 'arm' },
],
isHydrated: true,
};
const encoded = serializeConfig(original);
const decoded = deserializeConfig(encoded);
expect(decoded).not.toBeNull();
expect(decoded?.activeColors).toEqual(original.activeColors);
expect(decoded?.activePersonaAttire).toBe(original.activePersonaAttire);
expect(decoded?.activePayloads).toEqual(original.activePayloads);
});
it('should return null for malformed base64', () => {
expect(deserializeConfig('!!!invalid-base64!!!')).toBeNull();
});
it('should return null for valid base64 but invalid JSON', () => {
expect(deserializeConfig(btoa('not json'))).toBeNull();
});
it('should return null for valid JSON but wrong schema', () => {
expect(deserializeConfig(btoa(JSON.stringify({ wrong: 'schema' })))).toBeNull();
});
});
});
describe('Store integration with URL', () => {
beforeEach(() => {
vi.clearAllMocks();
configStore.getState().reset();
});
it('should create encoded URL with config', () => {
configStore.getState().setColors({ primary: '#ff0000' });
configStore.getState().setPersonaAttire('Test');
configStore.getState().addPayload({ id: 'p1', type: 'camera', position: 'head' });
const state = configStore.getState();
const encoded = serializeConfig(state);
expect(encoded).toBeTruthy();
expect(encoded.length).toBeGreaterThan(0);
});
it('should preserve state through serialize/deserialize cycle', () => {
configStore.getState().setColors({ primary: '#abc123', secondary: '#def456', accent: '#789abc' });
configStore.getState().setPersonaAttire('Emarati Kandura');
configStore.getState().addPayload({ id: 'cam1', type: 'PTZ Camera', position: 'head' });
configStore.getState().addPayload({ id: 'light1', type: 'LED Panel', position: 'chest' });
configStore.getState().setHydrated(true);
const state = configStore.getState();
const encoded = serializeConfig(state);
const decoded = deserializeConfig(encoded);
expect(decoded).not.toBeNull();
expect(decoded?.activeColors).toEqual(state.activeColors);
expect(decoded?.activePersonaAttire).toBe(state.activePersonaAttire);
expect(decoded?.activePayloads).toHaveLength(2);
});
});