90 lines
3.4 KiB
TypeScript
90 lines
3.4 KiB
TypeScript
// @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);
|
||
});
|
||
});
|