Files
2026-08-16 14:58:47 +08:00

155 lines
5.4 KiB
TypeScript
Raw Permalink 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 { afterAll, afterEach, describe, expect, it, vi } from 'vitest';
import { act, cleanup, render } from '@testing-library/react';
import { BaseLatexElement } from '../../../src/elements/latex/BaseLatexElement';
import type { PPTLatexElement } from '@openmaic/dsl';
/**
* KaTeX lays out large delimiters (\left\{ … \begin{cases}) using the metrics of
* its KaTeX_Size faces, which load asynchronously. The shrink-to-fit measurement
* in BaseLatexElement must re-run once those fonts settle, or a cold export bakes
* the stale fallback-metric scale and the brace desyncs from the piecewise body
* (bug: "大括号后面的分段函数与大括号错位"). These tests drive that re-measure
* with a fake `document.fonts` and stubbed layout metrics.
*/
// jsdom has no layout, so scrollWidth/scrollHeight are 0. Make them return a
// controllable value that changes after "fonts load" to simulate the metric
// shift a real KaTeX_Size face causes.
let naturalW = 0;
let naturalH = 0;
const origW = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'scrollWidth');
const origH = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'scrollHeight');
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
configurable: true,
get: () => naturalW,
});
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
get: () => naturalH,
});
/** A controllable fake of `document.fonts` (FontFaceSet-ish). */
function installFakeFonts() {
let resolveReady: () => void = () => {};
const ready = new Promise<void>((r) => {
resolveReady = r;
});
const listeners = new Map<string, Set<() => void>>();
const fonts = {
ready,
load: vi.fn(() => Promise.resolve([])),
addEventListener: (type: string, cb: () => void) => {
(listeners.get(type) ?? listeners.set(type, new Set()).get(type)!).add(cb);
},
removeEventListener: (type: string, cb: () => void) => {
listeners.get(type)?.delete(cb);
},
};
Object.defineProperty(document, 'fonts', { configurable: true, value: fonts });
return {
resolveReady,
emitLoadingDone: () => listeners.get('loadingdone')?.forEach((cb) => cb()),
};
}
function latex(html: string, overrides: Partial<PPTLatexElement> = {}): PPTLatexElement {
return {
id: 'l1',
type: 'latex',
left: 0,
top: 0,
width: 100,
height: 100,
rotate: 0,
html,
...overrides,
} as unknown as PPTLatexElement;
}
/** Read the scale multiplier off the inner prose div's transform. */
function readScale(container: HTMLElement): number {
const inner = container.querySelector('.slide-renderer-prose') as HTMLElement | null;
const m = inner?.style.transform.match(/scale\(([-\d.]+)\)/);
return m ? Number(m[1]) : NaN;
}
afterEach(() => {
cleanup();
naturalW = 0;
naturalH = 0;
});
describe('BaseLatexElement — fit-scale re-measures after fonts settle', () => {
it('recomputes scale on document.fonts.ready', async () => {
const { resolveReady } = installFakeFonts();
// Cold metrics: fallback font measures the formula as 100×100 → scale 1.
naturalW = 100;
naturalH = 100;
const { container } = render(<BaseLatexElement elementInfo={latex('\\left\\{ x \\right.')} />);
expect(readScale(container)).toBeCloseTo(1);
// KaTeX_Size faces load: the real brace is wider (200px) → must shrink to 0.5.
naturalW = 200;
await act(async () => {
resolveReady();
await Promise.resolve();
});
expect(readScale(container)).toBeCloseTo(0.5);
});
it('recomputes scale on a loadingdone event too', async () => {
const { emitLoadingDone } = installFakeFonts();
naturalW = 100;
naturalH = 100;
const { container } = render(<BaseLatexElement elementInfo={latex('E=mc^2')} />);
expect(readScale(container)).toBeCloseTo(1);
naturalH = 400; // taller than the box → shrink to 0.25
await act(async () => {
emitLoadingDone();
await Promise.resolve();
});
expect(readScale(container)).toBeCloseTo(0.25);
});
it('never enlarges past scale 1', async () => {
const { resolveReady } = installFakeFonts();
naturalW = 50; // smaller than the 100px box
naturalH = 50;
const { container } = render(<BaseLatexElement elementInfo={latex('x')} />);
await act(async () => {
resolveReady();
await Promise.resolve();
});
expect(readScale(container)).toBeCloseTo(1);
});
it('keeps KaTeX HTML, formula color, and horizontal alignment in the shared render path', () => {
naturalW = 100;
naturalH = 100;
const { container } = render(
<BaseLatexElement
elementInfo={latex('<span class="katex-display">E = mc<sup>2</sup></span>', {
color: '#2563eb',
align: 'right',
})}
/>,
);
const content = container.querySelector('.element-content') as HTMLElement;
const formula = container.querySelector('.slide-renderer-prose') as HTMLElement;
expect(content.style.color).toBe('rgb(37, 99, 235)');
expect(formula.innerHTML).toContain('katex-display');
expect(formula.style.transformOrigin).toBe('right center');
expect(formula.parentElement?.style.justifyContent).toBe('flex-end');
});
});
// Restore the prototype descriptors so other suites see real jsdom behavior.
afterAll(() => {
if (origW) Object.defineProperty(HTMLElement.prototype, 'scrollWidth', origW);
if (origH) Object.defineProperty(HTMLElement.prototype, 'scrollHeight', origH);
});