// @vitest-environment jsdom import { clearCache } from '@chenglou/pretext'; import { beforeEach, describe, expect, it, } from 'vitest'; import { installCanvasMock } from '../__mocks__/canvas'; import { TextLayoutEngine } from './TextLayoutEngine.svelte'; // Fixed-width mock: every segment is measured as (text.length * 10) px. // This is font-independent so we can reason about wrapping precisely. const CHAR_WIDTH = 10; describe('TextLayoutEngine', () => { let engine: TextLayoutEngine; beforeEach(() => { // Install mock BEFORE any prepareWithSegments call. // clearMeasurementCaches resets pretext's cached canvas context // and segment metric caches so each test gets a clean slate. installCanvasMock((_font, text) => text.length * CHAR_WIDTH); clearCache(); engine = new TextLayoutEngine(); }); it('returns empty result for empty string', () => { const result = engine.layout('', '400 16px "Inter"', 500, 20); expect(result.lines).toHaveLength(0); expect(result.totalHeight).toBe(0); }); it('returns a single line when text fits within width', () => { // 'ABC' = 3 chars × 10px = 30px, fits in 500px const result = engine.layout('ABC', '400 16px "Inter"', 500, 20); expect(result.lines).toHaveLength(1); expect(result.lines[0].text).toBe('ABC'); }); it('breaks text into multiple lines when it exceeds width', () => { // 'Hello World' — pretext will split at the space. // 'Hello' = 50px, ' ' hangs, 'World' = 50px. Width = 60px forces wrap after 'Hello '. const result = engine.layout('Hello World', '400 16px "Inter"', 60, 20); expect(result.lines.length).toBeGreaterThan(1); // First line must not exceed the container width. expect(result.lines[0].width).toBeLessThanOrEqual(60); }); it('assigns correct x positions to characters on a single line', () => { // 'ABC': A=10px, B=10px, C=10px; all on one line in 500px container. const result = engine.layout('ABC', '400 16px "Inter"', 500, 20); const chars = result.lines[0].chars; expect(chars).toHaveLength(3); expect(chars[0].char).toBe('A'); expect(chars[0].x).toBe(0); expect(chars[0].width).toBe(CHAR_WIDTH); expect(chars[1].char).toBe('B'); expect(chars[1].x).toBe(CHAR_WIDTH); expect(chars[1].width).toBe(CHAR_WIDTH); expect(chars[2].char).toBe('C'); expect(chars[2].x).toBe(CHAR_WIDTH * 2); expect(chars[2].width).toBe(CHAR_WIDTH); }); it('x positions are monotonically increasing across a line', () => { const result = engine.layout('ABCDE', '400 16px "Inter"', 500, 20); const chars = result.lines[0].chars; for (let i = 1; i < chars.length; i++) { expect(chars[i].x).toBeGreaterThan(chars[i - 1].x); } }); it('each line has at least one char', () => { const result = engine.layout('Hello World', '400 16px "Inter"', 60, 20); for (const line of result.lines) { expect(line.chars.length).toBeGreaterThan(0); } }); it('totalHeight equals lineCount * lineHeight', () => { const lineHeight = 24; const result = engine.layout('Hello World', '400 16px "Inter"', 60, lineHeight); expect(result.totalHeight).toBe(result.lines.length * lineHeight); }); });