313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|