chore: establish learning module baseline

This commit is contained in:
inman committed 2026-08-16 14:58:47 +08:00
commit 2d04197f3f
2409 files changed
+533050

No files matched your search

@@ -0,0 +1,154 @@
// @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);
});