import { beforeEach, describe, expect, it, vi } from 'vitest'; /** * Guards the elementId → mediaRef bridge in the app-side compiler deps. * * A `play_video` action targets a slide element by its `.id` (e.g. `video_abc`), * but generated-media records are keyed by the element's media ref (`gen_vid_…`). * Without the bridge, `assets.media()` / `timing.videoDurationMs()` miss for every * generated video, which cascades to a dropped clip in the export (regression the * user reported as "视频元素丢失"). These tests mock Dexie (no Dexie-in-node * harness, per the repo pattern) and run the real factory. */ const mediaRows = new Map(); const mediaGet = vi.fn((id: string) => Promise.resolve(mediaRows.get(id))); const audioGet = vi.fn<(id: string) => Promise>((_id) => Promise.resolve(undefined)); vi.mock('@/lib/utils/database', () => ({ mediaFileKey: (stageId: string, ref: string) => `${stageId}:${ref}`, db: { audioFiles: { get: (id: string) => audioGet(id) }, mediaFiles: { get: (...args: [string]) => mediaGet(...args), }, }, })); // Stub the off-screen geometry measurement (needs a real browser/layout). It // records the calls so tests can assert which elements were measured, and // returns a canned content-box geometry. const measureCalls: Array<{ elementIds: string[] }> = []; vi.mock('@openmaic/renderer/snapshot', () => ({ measureSlideElementGeometry: vi.fn(async (_slide: unknown, elementIds: string[]) => { measureCalls.push({ elementIds: [...elementIds] }); return new Map( elementIds.map((id) => [id, { x: 11, y: 19, w: 18, h: 25, centerX: 20, centerY: 31.5 }]), ); }), })); // The proxy-mediated URL fetch and the pool read are module-level seams; the // legacy-URL fallback tests drive them directly. const fetchMediaUrlMock = vi.fn(); vi.mock('@/lib/media/fetch-media-url', () => ({ fetchMediaUrl: (...args: unknown[]) => fetchMediaUrlMock(...args), })); const resolveAudioBlobMock = vi.fn<(id: string) => Promise>((_id) => Promise.resolve(null), ); vi.mock('@/lib/media/resolve-audio-bytes', () => ({ resolveAudioBlob: (id: string) => resolveAudioBlobMock(id), })); import { createVideoTimelineDeps } from '@/lib/video-export-app/timeline-deps'; import { collectVideoAssets } from '@/lib/video-export-app/collect'; import { compileVideoTimeline } from '@/lib/video-export'; import { resolveVideoMediaForElement } from '@/lib/media/media-task-resolution'; import { useMediaGenerationStore } from '@/lib/store/media-generation'; import type { MediaFileRecord } from '@/lib/utils/database'; import type { Scene } from '@/lib/types/stage'; const STAGE_ID = 'stage-1'; const MEDIA_REF = 'gen_vid_abc123'; const ELEMENT_ID = 'video_element_1'; /** A media record present via ossKey (no local bytes → no DOM probe in Node). */ function videoRecord(over: Partial = {}): MediaFileRecord { return { id: `${STAGE_ID}:${MEDIA_REF}`, stageId: STAGE_ID, type: 'video', blob: new Blob([], { type: 'video/mp4' }), mimeType: 'video/mp4', ossKey: 'https://cdn.example/v.mp4', size: 0, prompt: '', params: '', createdAt: 0, ...over, } as MediaFileRecord; } /** A media record for an arbitrary ref (id is `stageId:ref`). */ function videoRecordFor(ref: string, over: Partial = {}): MediaFileRecord { return videoRecord({ id: `${STAGE_ID}:${ref}`, ...over }); } /** A slide scene whose element points at the media ref via `mediaRef`. */ function slideScene(el: Record, actions: unknown[] = []): Scene { return { id: 'scene-1', stageId: STAGE_ID, title: 'Scene', order: 0, type: 'slide', content: { type: 'slide', canvas: { elements: [el] } }, actions, } as unknown as Scene; } /** A slide scene with an explicit id, one element, and optional actions. */ function slideSceneWithId( sceneId: string, el: Record, actions: unknown[] = [], ): Scene { return { id: sceneId, stageId: STAGE_ID, title: sceneId, order: 0, type: 'slide', content: { type: 'slide', canvas: { elements: [el] } }, actions, } as unknown as Scene; } const spotlight = (elementId: string) => ({ type: 'spotlight', elementId }); /** Seed the mocked media table; rows are keyed by their compound id. */ function seedMedia(...records: MediaFileRecord[]) { for (const record of records) mediaRows.set(record.id, record); } beforeEach(() => { mediaRows.clear(); mediaGet.mockClear(); audioGet.mockReset().mockImplementation(() => Promise.resolve(undefined)); measureCalls.length = 0; fetchMediaUrlMock.mockReset(); resolveAudioBlobMock.mockReset().mockImplementation(() => Promise.resolve(null)); useMediaGenerationStore.setState({ tasks: {} }); }); /** A fake `