// @vitest-environment jsdom import { act, render } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { TextComponent } from '../src/features/ui-editor/types/TextComponent'; import { findLargestFittingFontSize, fontFamilyToCss, fontStyleToCss, SYSTEM_UI_FONT_STACK, } from '../src/features/ui-editor/utils/textStyleToCss'; import { TextComponentView } from '../src/view/ui-editor/components/preview/components/TextComponentView'; function text(overrides: Partial = {}): TextComponent { return { content: '测试文本', font: 'SystemFont', font_style: 'Normal', font_sizing: { Fixed: 14 }, color: [0, 0, 0, 255], alignment: 'UpperLeft', horizontal_overflow: 'Wrap', vertical_overflow: 'Truncate', line_spacing: 1, ...overrides, }; } const resources = { previewUrls: {}, sprites: {}, fontFaces: {}, }; afterEach(() => { vi.unstubAllGlobals(); }); describe('UI 编辑器文本样式算法', () => { it.each([ ['Normal', { fontStyle: 'normal', fontWeight: 400 }], ['Bold', { fontStyle: 'normal', fontWeight: 700 }], ['Italic', { fontStyle: 'italic', fontWeight: 400 }], ['BoldItalic', { fontStyle: 'italic', fontWeight: 700 }], ] as const)('maps %s to browser font properties', (style, expected) => { expect(fontStyleToCss(style)).toEqual(expected); }); it('uses a loaded bound font and falls back for all other states', () => { expect( fontFamilyToCss( { Bound: 'body' }, { body: { cssFamily: 'ui-editor-font-body', status: 'loaded' }, }, ), ).toBe('ui-editor-font-body'); for (const status of ['loading', 'error'] as const) { expect( fontFamilyToCss( { Bound: 'body' }, { body: { cssFamily: 'ui-editor-font-body', status }, }, ), ).toBe(SYSTEM_UI_FONT_STACK); } expect(fontFamilyToCss({ Bound: 'missing' }, {})).toBe( SYSTEM_UI_FONT_STACK, ); }); it('finds the greatest fitting integer and retains min if none fits', () => { expect(findLargestFittingFontSize(8, 20, (size) => size <= 15)).toBe(15); expect(findLargestFittingFontSize(8, 20, () => false)).toBe(8); }); }); describe('TextComponentView', () => { it('applies a loaded bound font and font style to the preview', () => { render( , ); const view = document.querySelector( '[data-component-kind="Text"]', ) as HTMLDivElement; expect(view.style.fontFamily).toBe('ui-editor-font-body'); expect(view.style.fontStyle).toBe('italic'); expect(view.style.fontWeight).toBe('700'); }); it('recalculates BestFit after its container is measured and resized', () => { let observerCallback: ResizeObserverCallback | undefined; class TestResizeObserver { constructor(callback: ResizeObserverCallback) { observerCallback = callback; } observe = vi.fn(); disconnect = vi.fn(); } vi.stubGlobal('ResizeObserver', TestResizeObserver); render( , ); const view = document.querySelector( '[data-component-kind="Text"]', ) as HTMLDivElement; const measurement = document.querySelector( '[data-ui-editor-best-fit-measurement="true"]', ) as HTMLDivElement; let width = 120; Object.defineProperty(view, 'clientWidth', { configurable: true, get: () => width, }); Object.defineProperty(view, 'clientHeight', { configurable: true, get: () => 24, }); Object.defineProperty(measurement, 'scrollWidth', { configurable: true, get: () => Number.parseInt(measurement.style.fontSize, 10) * 8, }); Object.defineProperty(measurement, 'scrollHeight', { configurable: true, get: () => 20, }); act(() => observerCallback?.([], {} as ResizeObserver)); expect(view.style.fontSize).toBe('15px'); width = 72; act(() => observerCallback?.([], {} as ResizeObserver)); expect(view.style.fontSize).toBe('9px'); }); it('does not restart BestFit measurement when an equivalent component is cloned', () => { const observers: Array<{ disconnect: ReturnType }> = []; class TestResizeObserver { readonly disconnect = vi.fn(); constructor(_callback: ResizeObserverCallback) { observers.push(this); } observe = vi.fn(); } vi.stubGlobal('ResizeObserver', TestResizeObserver); const original = text({ font_sizing: { BestFit: { min: 8, max: 20 } } }); const view = render( , ); view.rerender( , ); expect(observers).toHaveLength(1); expect(observers[0]!.disconnect).not.toHaveBeenCalled(); }); });