Files
frontend-svelte/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.test.ts
T

169 lines
6.3 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 { TextLayoutEngine } from '$shared/lib';
import { installCanvasMock } from '$shared/lib/helpers/__mocks__/canvas';
import { clearCache } from '@chenglou/pretext';
import {
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import type { FontLoadStatus } from '../../model/types';
import { mockUnifiedFont } from '../mocks';
import { createFontRowSizeResolver } from './createFontRowSizeResolver';
// Fixed-width canvas mock: every character is 10px wide regardless of font.
// This makes wrapping math predictable: N chars × 10px = N×10 total width.
const CHAR_WIDTH = 10;
const LINE_HEIGHT = 20;
const CONTAINER_WIDTH = 200;
const CONTENT_PADDING_X = 32; // p-4 × 2 sides = 32px
const CHROME_HEIGHT = 56;
const FALLBACK_HEIGHT = 220;
const FONT_SIZE_PX = 16;
describe('createFontRowSizeResolver', () => {
let statusMap: Map<string, FontLoadStatus>;
let getStatus: (key: string) => FontLoadStatus | undefined;
beforeEach(() => {
installCanvasMock((_font, text) => text.length * CHAR_WIDTH);
clearCache();
statusMap = new Map();
getStatus = key => statusMap.get(key);
});
function makeResolver(overrides?: Partial<Parameters<typeof createFontRowSizeResolver>[0]>) {
const font = mockUnifiedFont({ id: 'inter', name: 'Inter' });
return {
font,
resolver: createFontRowSizeResolver({
getFonts: () => [font],
getWeight: () => 400,
getPreviewText: () => 'Hello',
getContainerWidth: () => CONTAINER_WIDTH,
getFontSizePx: () => FONT_SIZE_PX,
getLineHeightPx: () => LINE_HEIGHT,
getStatus,
contentHorizontalPadding: CONTENT_PADDING_X,
chromeHeight: CHROME_HEIGHT,
fallbackHeight: FALLBACK_HEIGHT,
...overrides,
}),
};
}
it('returns fallbackHeight when font status is undefined', () => {
const { resolver } = makeResolver();
expect(resolver(0)).toBe(FALLBACK_HEIGHT);
});
it('returns fallbackHeight when font status is "loading"', () => {
const { resolver } = makeResolver();
statusMap.set('inter@400', 'loading');
expect(resolver(0)).toBe(FALLBACK_HEIGHT);
});
it('returns fallbackHeight when font status is "error"', () => {
const { resolver } = makeResolver();
statusMap.set('inter@400', 'error');
expect(resolver(0)).toBe(FALLBACK_HEIGHT);
});
it('returns fallbackHeight when containerWidth is 0', () => {
const { resolver } = makeResolver({ getContainerWidth: () => 0 });
statusMap.set('inter@400', 'loaded');
expect(resolver(0)).toBe(FALLBACK_HEIGHT);
});
it('returns fallbackHeight when previewText is empty', () => {
const { resolver } = makeResolver({ getPreviewText: () => '' });
statusMap.set('inter@400', 'loaded');
expect(resolver(0)).toBe(FALLBACK_HEIGHT);
});
it('returns fallbackHeight for out-of-bounds rowIndex', () => {
const { resolver } = makeResolver();
statusMap.set('inter@400', 'loaded');
expect(resolver(99)).toBe(FALLBACK_HEIGHT);
});
it('returns computed height (totalHeight + chromeHeight) when font is loaded', () => {
const { resolver } = makeResolver();
statusMap.set('inter@400', 'loaded');
// 'Hello' = 5 chars × 10px = 50px. contentWidth = 200 - 32 = 168px. Fits on one line.
// totalHeight = 1 × LINE_HEIGHT = 20. result = 20 + CHROME_HEIGHT = 76.
const result = resolver(0);
expect(result).toBe(LINE_HEIGHT + CHROME_HEIGHT);
});
it('returns increased height when text wraps due to narrow container', () => {
// contentWidth = 40 - 32 = 8px — 'Hello' (50px) forces wrapping onto many lines
const { resolver } = makeResolver({ getContainerWidth: () => 40 });
statusMap.set('inter@400', 'loaded');
const result = resolver(0);
expect(result).toBeGreaterThan(LINE_HEIGHT + CHROME_HEIGHT);
});
it('does not call layout() again on second call with same arguments', () => {
const { resolver } = makeResolver();
statusMap.set('inter@400', 'loaded');
const layoutSpy = vi.spyOn(TextLayoutEngine.prototype, 'layout');
resolver(0);
resolver(0);
expect(layoutSpy).toHaveBeenCalledTimes(1);
layoutSpy.mockRestore();
});
it('calls layout() again when containerWidth changes (cache miss)', () => {
let width = CONTAINER_WIDTH;
const { resolver } = makeResolver({ getContainerWidth: () => width });
statusMap.set('inter@400', 'loaded');
const layoutSpy = vi.spyOn(TextLayoutEngine.prototype, 'layout');
resolver(0);
width = 100;
resolver(0);
expect(layoutSpy).toHaveBeenCalledTimes(2);
layoutSpy.mockRestore();
});
it('returns greater height when container narrows (more wrapping)', () => {
let width = CONTAINER_WIDTH;
const { resolver } = makeResolver({ getContainerWidth: () => width });
statusMap.set('inter@400', 'loaded');
const h1 = resolver(0);
width = 100; // narrower → more wrapping
const h2 = resolver(0);
expect(h2).toBeGreaterThanOrEqual(h1);
});
it('uses variable font key for variable fonts', () => {
const vfFont = mockUnifiedFont({ id: 'roboto', name: 'Roboto', features: { isVariable: true } });
const { resolver } = makeResolver({ getFonts: () => [vfFont] });
// Variable fonts use '{id}@vf' key, not '{id}@{weight}'
statusMap.set('roboto@vf', 'loaded');
const result = resolver(0);
expect(result).not.toBe(FALLBACK_HEIGHT);
expect(result).toBeGreaterThan(0);
});
it('returns fallbackHeight for variable font when static key is set instead', () => {
const vfFont = mockUnifiedFont({ id: 'roboto', name: 'Roboto', features: { isVariable: true } });
const { resolver } = makeResolver({ getFonts: () => [vfFont] });
// Setting the static key should NOT unlock computed height for variable fonts
statusMap.set('roboto@400', 'loaded');
expect(resolver(0)).toBe(FALLBACK_HEIGHT);
});
});