import { afterEach, describe, expect, it, vi } from 'vitest'; /** * Mock the renderer snapshot so the frame path is testable in plain Node (the * real `@openmaic/renderer/snapshot` needs a build + DOM). `slideToPng` records * the slide it was handed so tests can assert which media the frame captured. */ const capturedSlides: Array<{ elements: Array>; background?: { type: string; image?: { src: string } }; }> = []; const mediaOwnerMocks = vi.hoisted(() => ({ withAssetUrl: vi.fn(async (_ref: string, fn: (url: string | null) => Promise) => fn(null), ), })); vi.mock('@openmaic/renderer/snapshot', () => ({ slideToPng: vi.fn( async (slide: { elements: Array>; background?: { type: string; image?: { src: string } }; }) => { capturedSlides.push(structuredClone(slide)); return new Blob(['png'], { type: 'image/png' }); }, ), })); vi.mock('@/lib/media/use-asset-url', () => ({ withAssetUrl: mediaOwnerMocks.withAssetUrl, })); import { collectVideoAssets } from '@/lib/video-export-app/collect'; import type { VideoTimeline } from '@/lib/video-export'; import type { VideoTimelineRecords } from '@/lib/video-export-app/timeline-deps'; import type { Scene } from '@/lib/types/stage'; import type { AudioFileRecord, MediaFileRecord } from '@/lib/utils/database'; /** Minimal IR carrying only an asset plan — collectVideoAssets reads `ir.assets.entries`. */ function irWith(entries: VideoTimeline['assets']['entries']): VideoTimeline { return { assets: { entries } } as unknown as VideoTimeline; } function audioRecord(over: Partial): AudioFileRecord { return { id: 'aud-1', blob: new Blob([], { type: 'audio/mpeg' }), format: 'mp3', createdAt: 0, ...over, }; } function videoRecord(over: Partial): MediaFileRecord { return { id: 'stage:el-1', stageId: 'stage', type: 'video', blob: new Blob([], { type: 'video/mp4' }), mimeType: 'video/mp4', size: 0, prompt: '', params: '', createdAt: 0, ...over, }; } function imageRecord(over: Partial): MediaFileRecord { return { id: 'stage:img-1', stageId: 'stage', type: 'image', blob: new Blob([], { type: 'image/png' }), mimeType: 'image/png', size: 0, prompt: '', params: '', createdAt: 0, ...over, }; } function records(over: Partial = {}): VideoTimelineRecords { return { audioById: new Map(), mediaByElementId: new Map(), videoDurationMsByElementId: new Map(), interactiveHtml: { html: () => null, content: () => undefined }, ...over, }; } /** A slide scene whose single element points at a generated-media placeholder. */ function slideScene(element: Record): Scene { return { id: 's1', content: { type: 'slide', canvas: { elements: [element] } }, } as unknown as Scene; } let objectUrlSeq = 0; const revoked: string[] = []; afterEach(() => { vi.unstubAllGlobals(); capturedSlides.length = 0; revoked.length = 0; objectUrlSeq = 0; mediaOwnerMocks.withAssetUrl.mockReset().mockImplementation(async (_ref, fn) => fn(null)); }); /** Stub URL object-URL lifecycle (absent in Node) so frame tests can run. */ function stubObjectUrls() { vi.stubGlobal('URL', { createObjectURL: () => `blob:mock/${++objectUrlSeq}`, revokeObjectURL: (url: string) => revoked.push(url), }); } /** * Stub the first-frame decode path (`document.createElement('video'|'canvas')`) * used by `decodeFirstFramePosterUrl`. The fake