import { describe, expect, it } from 'vitest';
import { prepareInteractiveHtmlScenes } from '@/lib/video-export-app/prepare-interactive-html';
import type { Scene } from '@/lib/types/stage';
function scene(html?: string): Scene {
return {
id: 'widget',
stageId: 'stage',
title: 'Widget',
order: 0,
type: 'interactive',
content: { type: 'interactive', url: '', html },
actions: [],
} as Scene;
}
describe('prepareInteractiveHtmlScenes', () => {
it('produces a bounded packaged page with CSP, iframe shims, and freeze runtime', async () => {
const prepared = await prepareInteractiveHtmlScenes([
scene('
Ready
'),
]);
const meta = prepared.html(scene());
const html = prepared.content('interactive:widget');
expect(meta).toMatchObject({
id: 'interactive:widget',
present: true,
contentHash: expect.stringMatching(/^[a-f0-9]{64}$/),
});
expect(html).toContain('data-openmaic-static-csp');
expect(html).toContain('data-iframe-storage-shim');
expect(html).toContain('data-iframe-error-shim');
expect(html).toContain('data-openmaic-static-capture');
expect(html).toContain(
"document.documentElement.setAttribute('data-openmaic-static-state', 'frozen')",
);
expect(html).toContain("connect-src 'none'");
expect(html).toContain("worker-src 'none'");
expect(html).toContain('interactive-worker-disabled');
});
it('keeps the doctype first when authored HTML omits an explicit head', async () => {
const prepared = await prepareInteractiveHtmlScenes([
scene('Standards mode
'),
]);
const html = prepared.content('interactive:widget')!;
expect(html.toLowerCase().startsWith('')).toBe(true);
expect(html.indexOf('data-openmaic-static-csp')).toBeGreaterThan(html.indexOf(''));
expect(html.indexOf('')).toBeLessThan(html.indexOf(''));
});
it('does not inject into head-like text in comments or authored scripts', async () => {
const authoredScript = 'const template = ""; window.__template = template;';
const prepared = await prepareInteractiveHtmlScenes([
scene(
``,
),
]);
const html = prepared.content('interactive:widget')!;
expect(prepared.html(scene())?.present).toBe(true);
expect(html).toContain(``);
expect(html.indexOf('data-openmaic-static-csp')).toBeLessThan(html.indexOf(''));
});
it('ignores embedded-document markup inside authored script text', async () => {
const authoredScript =
'const template = ``; window.__template = template;';
const prepared = await prepareInteractiveHtmlScenes([
scene(`Ready`),
]);
expect(prepared.html(scene())).toMatchObject({ present: true });
expect(prepared.content('interactive:widget')).toContain(``);
});
it('inlines supported remote assets and removes the network URL', async () => {
const prepared = await prepareInteractiveHtmlScenes(
[scene('
')],
{
fetcher: async (url) =>
url === 'https://cdn.test/pixel.png'
? { bytes: new Uint8Array([1, 2, 3]), contentType: 'image/png' }
: null,
},
);
expect(prepared.html(scene())?.present).toBe(true);
expect(prepared.content('interactive:widget')).toContain('src="data:image/png;base64,');
expect(prepared.content('interactive:widget')).not.toContain('https://cdn.test/pixel.png');
});
it('accepts bare module imports resolved by the final offline import map', async () => {
const prepared = await prepareInteractiveHtmlScenes(
[
scene(
'' +
'',
),
],
{
fetcher: async (url) =>
url === 'https://cdn.test/dep.js'
? {
bytes: new TextEncoder().encode('export const value = 42;'),
contentType: 'text/javascript',
}
: null,
},
);
expect(prepared.html(scene())).toMatchObject({ present: true });
expect(prepared.content('interactive:widget')).toContain('"dep":"data:text/javascript;base64,');
});
it('explicitly rejects scoped import maps instead of changing their semantics', async () => {
const authored =
'';
const calls: string[] = [];
const prepared = await prepareInteractiveHtmlScenes([scene(authored)], {
fetcher: async (url) => {
calls.push(url);
return url === 'https://app.test/main.js'
? {
bytes: new TextEncoder().encode('import { value } from "dep";'),
contentType: 'text/javascript',
}
: null;
},
});
expect(prepared.html(scene())).toMatchObject({
present: false,
failure: 'unresolved-resource',
message: expect.stringContaining('unsupported-importmap-scopes'),
});
expect(prepared.content('interactive:widget')).toBeUndefined();
expect(calls).toEqual([]);
});
it('rejects unresolved remote or relative resources with an explicit failure', async () => {
const remote = await prepareInteractiveHtmlScenes(
[scene('
')],
{ fetcher: async () => null },
);
expect(remote.html(scene())).toMatchObject({
present: false,
failure: 'unresolved-resource',
message: expect.stringContaining('https://cdn.test/missing.png'),
});
expect(remote.content('interactive:widget')).toBeUndefined();
const relative = await prepareInteractiveHtmlScenes([
scene(''),
]);
expect(relative.html(scene())).toMatchObject({
present: false,
failure: 'unresolved-resource',
message: expect.stringContaining('./app.js'),
});
const responsive = await prepareInteractiveHtmlScenes(
[scene('
')],
{ fetcher: async () => null },
);
expect(responsive.html(scene())).toMatchObject({
present: false,
failure: 'unresolved-resource',
message: expect.stringContaining('https://cdn.test/missing.png'),
});
const poster = await prepareInteractiveHtmlScenes(
[scene('')],
{ fetcher: async () => null },
);
expect(poster.html(scene())).toMatchObject({
present: false,
failure: 'unresolved-resource',
message: expect.stringContaining('https://cdn.test/missing-poster.jpg'),
});
for (const embedded of [
'',
'',
'