/** Isolated Chromium proof for packaged interactive HTML readiness, freeze, and fallback. */ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { chromium, type Browser, type Page } from '@playwright/test'; import { compileVideoTimeline, emitHyperframes } from '@/lib/video-export'; import { prepareInteractiveHtmlScenes } from '@/lib/video-export-app/prepare-interactive-html'; import type { Scene } from '@/lib/types/stage'; import { NO_ASSETS, NO_PROBE } from './helpers'; const REQUIRED = process.env.INTERACTIVE_STATIC_BROWSER === '1'; const BASE_URL = 'http://interactive-static.test'; const GSAP = readFileSync(join(process.cwd(), 'public/vendor/gsap.min.js'), 'utf8'); function interactiveScene(html: string): Scene { return { id: 'widget', stageId: 'stage', title: 'Static interactive fixture', order: 0, type: 'interactive', content: { type: 'interactive', url: '', html }, actions: [], } as Scene; } async function emittedFixture(html: string): Promise<{ index: string; child: string }> { const scene = interactiveScene(html); const prepared = await prepareInteractiveHtmlScenes([scene]); const ir = compileVideoTimeline( { stage: { id: 'stage', name: 'Interactive smoke' }, scenes: [scene] }, { timing: NO_PROBE, assets: NO_ASSETS, interactive: prepared }, ); const project = emitHyperframes(ir, { width: 1280, height: 720 }); return { index: project.files.find((file) => file.path === 'index.html')!.content, child: prepared.content('interactive:widget')!, }; } async function load(page: Page, fixture: { index: string; child: string }): Promise { await page.route(`${BASE_URL}/**`, async (route) => { const path = new URL(route.request().url()).pathname; if (path === '/index.html' || path === '/') { await route.fulfill({ status: 200, contentType: 'text/html', body: fixture.index }); } else if (path === '/assets/vendor/gsap.min.js') { await route.fulfill({ status: 200, contentType: 'text/javascript', body: GSAP }); } else if (path === '/assets/interactive/001-static-interactive-fixture.html') { await route.fulfill({ status: 200, contentType: 'text/html', body: fixture.child }); } else { await route.fulfill({ status: 404, body: 'not found' }); } }); await page.goto(`${BASE_URL}/index.html`, { waitUntil: 'load' }); } describe.skipIf(!REQUIRED)('frozen interactive HTML in Chromium', () => { let browser: Browser; beforeAll(async () => { browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, args: ['--disable-crash-reporter', '--disable-crashpad'], }); }); afterAll(async () => { await browser?.close(); }); it('freezes the settled initial state before exposing the Hyperframes timeline', async () => { const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); try { await load( page, await emittedFixture(`
0pending`), ); await page.waitForFunction(() => { const host = document.querySelector('[data-interactive-static-host]'); const timelines = (window as typeof window & { __timelines?: Record }) .__timelines; return ( host?.getAttribute('data-interactive-static-state') === 'frozen' && !!timelines?.openmaic ); }); const child = page.frames().find((frame) => frame !== page.mainFrame())!; const before = await child.evaluate(() => ({ counter: document.querySelector('#counter')?.textContent, workerStatus: document.querySelector('#worker-status')?.textContent, state: document.documentElement.getAttribute('data-openmaic-static-state'), animations: document.getAnimations().map((animation) => animation.playState), })); await page.waitForTimeout(350); const after = await child.evaluate(() => ({ counter: document.querySelector('#counter')?.textContent, workerStatus: document.querySelector('#worker-status')?.textContent, state: document.documentElement.getAttribute('data-openmaic-static-state'), animations: document.getAnimations().map((animation) => animation.playState), })); expect(before.state).toBe('frozen'); expect(before.workerStatus).toContain('interactive-worker-disabled'); expect(after).toEqual(before); expect(after.animations.every((state) => state === 'paused')).toBe(true); } finally { await page.close(); } }); it('creates a standards-mode head without corrupting head-like script text', async () => { const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); try { await load( page, await emittedFixture( '
Standards mode
', ), ); await page.waitForFunction(() => { return ( document .querySelector('[data-interactive-static-host]') ?.getAttribute('data-interactive-static-state') === 'frozen' ); }); const child = page.frames().find((frame) => frame !== page.mainFrame())!; expect(await child.evaluate(() => document.compatMode)).toBe('CSS1Compat'); expect( await child.evaluate(() => (window as typeof window & { __template?: string }).__template), ).toBe(''); } finally { await page.close(); } }); it('switches a runtime failure to the visible placeholder without failing the composition', async () => { const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); try { await load( page, await emittedFixture( '

Broken

', ), ); await page.waitForFunction(() => { const host = document.querySelector('[data-interactive-static-host]'); const timelines = (window as typeof window & { __timelines?: Record }) .__timelines; return ( host?.getAttribute('data-interactive-static-state') === 'fallback' && !!timelines?.openmaic ); }); const result = await page.evaluate(() => { const host = document.querySelector('[data-interactive-static-host]')!; const fallback = host.querySelector('[data-interactive-fallback]')!; const diagnostics = ( window as typeof window & { __openmaicVideoDiagnostics?: Array<{ code: string; message: string }>; } ).__openmaicVideoDiagnostics; const manifest = ( window as typeof window & { __openmaicVideoManifest?: { runtimeDiagnostics?: Array<{ code: string }> }; } ).__openmaicVideoManifest; const runtimeReport = document.querySelector( '[data-openmaic-runtime-diagnostics]', ); return { state: host.getAttribute('data-interactive-static-state'), code: host.getAttribute('data-interactive-diagnostic'), fallbackDisplay: getComputedStyle(fallback).display, fallbackText: fallback.textContent, diagnostics, manifestDiagnostics: manifest?.runtimeDiagnostics, runtimeReport: runtimeReport?.textContent, }; }); expect(result).toMatchObject({ state: 'fallback', code: 'interactive-runtime-failure', fallbackDisplay: 'block', fallbackText: expect.stringContaining('fixture boom'), diagnostics: [ expect.objectContaining({ code: 'interactive-runtime-failure', message: expect.stringContaining('fixture boom'), }), ], manifestDiagnostics: [expect.objectContaining({ code: 'interactive-runtime-failure' })], runtimeReport: expect.stringContaining('interactive-runtime-failure'), }); expect(page.frames()).toHaveLength(1); } finally { await page.close(); } }); it('unloads the child document after a readiness failure', async () => { const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); try { await load( page, await emittedFixture( '', ), ); await page.waitForFunction(() => { return ( document .querySelector('[data-interactive-static-host]') ?.getAttribute('data-interactive-static-state') === 'fallback' ); }); await page.waitForTimeout(100); expect(page.frames()).toHaveLength(1); expect( await page .locator('[data-interactive-static-host]') .getAttribute('data-interactive-diagnostic'), ).toBe('interactive-ready-failure'); } finally { await page.close(); } }); });