import { beforeEach, describe, expect, it, vi, } from 'vitest'; import { createCharacterComparison } from './createCharacterComparison.svelte'; type Font = { name: string; id: string }; const fontA: Font = { name: 'Roboto', id: 'roboto' }; const fontB: Font = { name: 'Open Sans', id: 'open-sans' }; function createMockCanvas(charWidth = 10): HTMLCanvasElement { return { getContext: () => ({ font: '', measureText: (text: string) => ({ width: text.length * charWidth }), }), } as unknown as HTMLCanvasElement; } function createMockContainer(offsetWidth = 500): HTMLElement { return { offsetWidth, getBoundingClientRect: () => ({ left: 0, width: offsetWidth, top: 0, right: offsetWidth, bottom: 0, height: 0, }), } as unknown as HTMLElement; } describe('createCharacterComparison', () => { beforeEach(() => { // Mock window.innerWidth for getFontSize and padding calculations Object.defineProperty(globalThis, 'window', { value: { innerWidth: 1024 }, writable: true, configurable: true, }); }); describe('Initial State', () => { it('should initialize with empty lines and zero container width', () => { const comparison = createCharacterComparison( () => 'test', () => fontA, () => fontB, () => 400, () => 48, ); expect(comparison.lines).toEqual([]); expect(comparison.containerWidth).toBe(0); }); }); describe('breakIntoLines', () => { it('should not break lines when container or canvas is undefined', () => { const comparison = createCharacterComparison( () => 'Hello world', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(undefined, undefined); expect(comparison.lines).toEqual([]); comparison.breakIntoLines(createMockContainer(), undefined); expect(comparison.lines).toEqual([]); }); it('should not break lines when fonts are undefined', () => { const comparison = createCharacterComparison( () => 'Hello world', () => undefined, () => undefined, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(), createMockCanvas()); expect(comparison.lines).toEqual([]); }); it('should produce a single line when text fits within container', () => { // charWidth=10, padding=96 (innerWidth>=640), availableWidth=500-96=404 // "Hello" = 5 chars * 10 = 50px, fits easily const comparison = createCharacterComparison( () => 'Hello', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(500), createMockCanvas(10)); expect(comparison.lines).toHaveLength(1); expect(comparison.lines[0].text).toBe('Hello'); }); it('should break text into multiple lines when it overflows', () => { // charWidth=10, container=200, padding=96, availableWidth=104 // "Hello world test" => "Hello" (50px), "Hello world" (110px > 104) // So "Hello" goes on line 1, "world" (50px) fits, "world test" (100px) fits const comparison = createCharacterComparison( () => 'Hello world test', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(200), createMockCanvas(10)); expect(comparison.lines.length).toBeGreaterThan(1); // All original text should be preserved across lines const reconstructed = comparison.lines.map(l => l.text).join(' '); expect(reconstructed).toBe('Hello world test'); }); it('should update containerWidth after breaking lines', () => { const comparison = createCharacterComparison( () => 'Hi', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(750), createMockCanvas(10)); expect(comparison.containerWidth).toBe(750); }); it('should use smaller padding on narrow viewports', () => { Object.defineProperty(globalThis, 'window', { value: { innerWidth: 500 }, writable: true, configurable: true, }); // container=150, padding=48 (innerWidth<640), availableWidth=102 // "ABCDEFGHIJ" = 10 chars * 10 = 100px, fits in 102 const comparison = createCharacterComparison( () => 'ABCDEFGHIJ', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(150), createMockCanvas(10)); expect(comparison.lines).toHaveLength(1); expect(comparison.lines[0].text).toBe('ABCDEFGHIJ'); }); it('should break a single long word using binary search', () => { // container=150, padding=96, availableWidth=54 // "ABCDEFGHIJ" = 10 chars * 10 = 100px, doesn't fit as single word // Binary search should split it const comparison = createCharacterComparison( () => 'ABCDEFGHIJ', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(150), createMockCanvas(10)); expect(comparison.lines.length).toBeGreaterThan(1); const reconstructed = comparison.lines.map(l => l.text).join(''); expect(reconstructed).toBe('ABCDEFGHIJ'); }); it('should store max width between both fonts for each line', () => { // Use a canvas where measureText returns text.length * charWidth // Both fonts measure the same, so width = text.length * charWidth const comparison = createCharacterComparison( () => 'Hi', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(500), createMockCanvas(10)); expect(comparison.lines[0].width).toBe(20); // "Hi" = 2 chars * 10 }); }); describe('getCharState', () => { it('should return zero proximity and isPast=false when containerWidth is 0', () => { const comparison = createCharacterComparison( () => 'test', () => fontA, () => fontB, () => 400, () => 48, ); const state = comparison.getCharState(0, 50, undefined, undefined); expect(state.proximity).toBe(0); expect(state.isPast).toBe(false); }); it('should return zero proximity when charElement is not found', () => { const comparison = createCharacterComparison( () => 'test', () => fontA, () => fontB, () => 400, () => 48, ); // First break lines to set containerWidth comparison.breakIntoLines(createMockContainer(500), createMockCanvas(10)); const lineEl = { children: [] } as unknown as HTMLElement; const container = createMockContainer(500); const state = comparison.getCharState(0, 50, lineEl, container); expect(state.proximity).toBe(0); expect(state.isPast).toBe(false); }); it('should calculate proximity based on distance from slider', () => { const comparison = createCharacterComparison( () => 'test', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(500), createMockCanvas(10)); // Character centered at 250px in a 500px container = 50% const charEl = { getBoundingClientRect: () => ({ left: 240, width: 20 }), }; const lineEl = { children: [charEl] } as unknown as HTMLElement; const container = createMockContainer(500); // Slider at 50% => charCenter at 250px => charGlobalPercent = 50% // distance = |50 - 50| = 0, proximity = max(0, 1 - 0/5) = 1 const state = comparison.getCharState(0, 50, lineEl, container); expect(state.proximity).toBe(1); expect(state.isPast).toBe(false); }); it('should return isPast=true when slider is past the character', () => { const comparison = createCharacterComparison( () => 'test', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(500), createMockCanvas(10)); // Character centered at 100px => 20% of 500px const charEl = { getBoundingClientRect: () => ({ left: 90, width: 20 }), }; const lineEl = { children: [charEl] } as unknown as HTMLElement; const container = createMockContainer(500); // Slider at 80% => past the character at 20% const state = comparison.getCharState(0, 80, lineEl, container); expect(state.isPast).toBe(true); }); it('should return zero proximity when character is far from slider', () => { const comparison = createCharacterComparison( () => 'test', () => fontA, () => fontB, () => 400, () => 48, ); comparison.breakIntoLines(createMockContainer(500), createMockCanvas(10)); // Character at 10% of container, slider at 90% => distance = 80%, range = 5% const charEl = { getBoundingClientRect: () => ({ left: 45, width: 10 }), }; const lineEl = { children: [charEl] } as unknown as HTMLElement; const container = createMockContainer(500); const state = comparison.getCharState(0, 90, lineEl, container); expect(state.proximity).toBe(0); }); }); });