');
});
it('keeps the whole card at 1280x720', () => {
const hd = at(1280, 720);
expect(hd).toContain('构建一个确定性');
expect(hd).toContain('
');
expect(hd).toContain("What you'll gain");
expect(hd).toContain('导出教练');
expect(hd).toContain('发布评审员');
});
it('drops the people row at 854x480, where its caption type stops being legible', () => {
const sd = at(854, 480);
expect(sd).toContain('构建一个确定性');
expect(sd).toContain('
');
expect(sd).not.toContain('导出教练');
expect(sd).not.toContain('发布评审员');
});
it('sheds gains before the title and counts when the panel runs out of room', () => {
// A 1280x480 frame keeps the full-size type but halves the vertical room,
// so the budget forces more than the people row out.
const squat = at(1280, 480);
expect(squat).toContain('构建一个确定性');
expect(squat).toContain('
3Stages');
expect(squat).not.toContain('
');
expect(squat).not.toContain("What you'll gain");
});
});
describe('emitHyperframes multi-cue subtitle positioning (regression)', () => {
// A scene with several narration cues; the earlier cut left inactive cues in
// layout (visibility:hidden + inline-block), so the band grew multiple rows
// tall and the active cue drifted up into the slide/title area.
const ir = compileVideoTimeline(
{
stage: { id: 'stage', name: 'Multi Cue' },
scenes: [
slide(
'intro',
[
speech('sp1', 'First caption line', { audioId: 'a1' }),
speech('sp2', 'Second caption line', { audioId: 'a2' }),
speech('sp3', 'Third caption line', { audioId: 'a3' }),
],
{ title: 'Intro', elements: [] },
),
],
},
{
timing: stubProbe({ sp1: 2000, sp2: 2000, sp3: 2000 }, {}),
assets: stubAssets({ sp1: audioMeta('a1'), sp2: audioMeta('a2'), sp3: audioMeta('a3') }, {}),
},
);
const html = emitHyperframes(ir, { width: 1920, height: 1080, burnInSubtitles: true }).files.find(
(f) => f.path === 'index.html',
)!.content;
it('stacks every cue in one grid cell so the active cue never shifts', () => {
// All cues share grid-area 1/1 — one slot, not one row each.
const cueCount = (html.match(/id="subtitle-cue-\d+"/g) ?? []).length;
expect(cueCount).toBe(3);
expect(html.match(/grid-area:1\/1/g)?.length).toBe(3);
expect(html).toContain('id="subtitles" style="position:absolute');
// The container is a grid so the single occupied cell owns the whole band.
expect(html).toMatch(/id="subtitles"[^>]*display:grid/);
});
it('removes inactive cues from layout (display:none, never visibility:hidden)', () => {
// Every cue starts display:none; toggled cues use display, not visibility —
// a visibility-hidden cue would keep its box and push the active one out of slot.
for (let i = 0; i < 3; i++) {
expect(html).toMatch(new RegExp(`id="subtitle-cue-${i}"[^>]*display:none`));
expect(html).toContain(`tl.set('#subtitle-cue-${i}',{display:'-webkit-box'}`);
expect(html).toContain(`tl.set('#subtitle-cue-${i}',{display:'none'}`);
}
expect(html).not.toContain('visibility:hidden');
expect(html).not.toContain("visibility:'hidden'");
});
});
describe('emitHyperframes determinism red-lines (hyperframes lint proxy)', () => {
const project = emitHyperframes(compileSample());
const html = project.files.find((f) => f.path === 'index.html')!.content;
it('loads no script or asset from an http(s) origin (no CDN)', () => {
expect(html).not.toMatch(/src="https?:\/\//);
});
it('embeds its primary font so Hyperframes never resolves Google Fonts', () => {
expect(html).toContain('@font-face');
expect(html).toContain('font-family: "Inter"');
expect(html).toMatch(/src:\s*url\("data:font\/woff2;base64,[A-Za-z0-9+/=]+"\)/);
expect(html).not.toContain('fonts.googleapis.com');
expect(html).not.toContain('fonts.gstatic.com');
});
it('uses no non-deterministic runtime APIs', () => {
expect(html).not.toContain('Date.now');
expect(html).not.toContain('Math.random');
expect(html).not.toContain('requestAnimationFrame');
expect(html).not.toContain('setTimeout');
expect(html).not.toContain('setInterval');
});
it('uses no infinite repeats (finite ring pulse only)', () => {
expect(html).not.toContain('repeat:-1');
expect(html).not.toContain('repeat: -1');
expect(html).not.toContain('Infinity');
});
it('declares an explicit root duration and extends the timeline to it', () => {
expect(html).toMatch(/data-duration="[\d.]+"/);
expect(html).toMatch(/tl\.set\(\{\}, \{\}, [\d.]+\);/);
});
});