import { describe, expect, test } from "bun:test"; import type { AudioTrack, EffectTrack, ImageElement, SceneTracks, TextTrack, VideoElement, VideoTrack, } from "@/timeline"; import { computeTransitionCutDropTarget } from "@/timeline/components/drop-target"; import { mediaTime, TICKS_PER_SECOND, ZERO_MEDIA_TIME } from "@/wasm"; const PIXELS_PER_SECOND = 50; function buildImage({ id, start, duration, }: { id: string; start: number; duration: number; }): ImageElement { return { id, type: "image", name: id, mediaId: `media-${id}`, startTime: mediaTime({ ticks: start }), duration: mediaTime({ ticks: duration }), trimStart: ZERO_MEDIA_TIME, trimEnd: ZERO_MEDIA_TIME, params: { opacity: 0.75 }, }; } function buildVideo({ id, start, duration, }: { id: string; start: number; duration: number; }): VideoElement { return { id, type: "video", name: id, mediaId: `media-${id}`, startTime: mediaTime({ ticks: start }), duration: mediaTime({ ticks: duration }), trimStart: ZERO_MEDIA_TIME, trimEnd: ZERO_MEDIA_TIME, params: { opacity: 1 }, }; } function buildVideoTrack({ elements, }: { elements: VideoTrack["elements"]; }): VideoTrack { return { id: "visual", type: "video", name: "Visual", elements, muted: false, hidden: false, }; } function buildTracks({ main, overlay = [], audio = [], }: { main: VideoTrack; overlay?: SceneTracks["overlay"]; audio?: AudioTrack[]; }): SceneTracks { return { main, overlay, audio }; } function resolve({ tracks, mouseX, mouseY = 10, }: { tracks: SceneTracks; mouseX: number; mouseY?: number; }) { return computeTransitionCutDropTarget({ mouseX, mouseY, tracks, pixelsPerSecond: PIXELS_PER_SECOND, zoomLevel: 1, }); } const oneSecond = TICKS_PER_SECOND; function adjacentTrack(): VideoTrack { return buildVideoTrack({ elements: [ buildImage({ id: "outgoing", start: 0, duration: oneSecond }), buildVideo({ id: "incoming", start: oneSecond, duration: oneSecond }), ], }); } describe("computeTransitionCutDropTarget", () => { test("resolves an adjacent image and video cut within 12 screen pixels", () => { const target = resolve({ tracks: buildTracks({ main: adjacentTrack() }), mouseX: 55, }); expect(target).toEqual({ trackIndex: 0, isNewTrack: false, insertPosition: null, xPosition: mediaTime({ ticks: oneSecond }), targetElement: null, targetCut: { trackId: "visual", outgoingElementId: "outgoing", incomingElementId: "incoming", time: mediaTime({ ticks: oneSecond }), }, }); }); test("rejects a pointer beyond 12 screen pixels", () => { expect( resolve({ tracks: buildTracks({ main: adjacentTrack() }), mouseX: 63 }), ).toBeNull(); }); test("rejects gaps and overlaps", () => { const incoming = buildVideo({ id: "incoming", start: oneSecond, duration: oneSecond, }); const gap = buildVideoTrack({ elements: [ buildImage({ id: "outgoing", start: 0, duration: oneSecond - 1 }), incoming, ], }); const overlap = buildVideoTrack({ elements: [ buildImage({ id: "outgoing", start: 0, duration: oneSecond + 1 }), incoming, ], }); expect( resolve({ tracks: buildTracks({ main: gap }), mouseX: 50 }), ).toBeNull(); expect( resolve({ tracks: buildTracks({ main: overlap }), mouseX: 50 }), ).toBeNull(); }); test("rejects the first visual clip boundary", () => { expect( resolve({ tracks: buildTracks({ main: adjacentTrack() }), mouseX: 0 }), ).toBeNull(); }); test("rejects audio, text, and effect tracks", () => { const textTrack: TextTrack = { id: "text", type: "text", name: "Text", elements: [], hidden: false, }; const effectTrack: EffectTrack = { id: "effect", type: "effect", name: "Effect", elements: [], hidden: false, }; const audioTrack: AudioTrack = { id: "audio", type: "audio", name: "Audio", elements: [], muted: false, }; const main = adjacentTrack(); expect( resolve({ tracks: buildTracks({ main, overlay: [textTrack] }), mouseX: 50, }), ).toBeNull(); expect( resolve({ tracks: buildTracks({ main, overlay: [effectTrack] }), mouseX: 50, }), ).toBeNull(); expect( resolve({ tracks: buildTracks({ main, audio: [audioTrack] }), mouseX: 50, mouseY: 72, }), ).toBeNull(); }); });