Files
frontend-svelte/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.test.ts
Ilia Mashkov a526a51af8 test: fix TextLayoutEngine tests — correct jsdom directive placement and canvas mock setup
fix: correct grapheme-width fallback in TextLayoutEngine for null breakableFitAdvances
2026-04-11 15:48:52 +03:00

90 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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);
});
});