import { describe, expect, mock, test } from "bun:test"; import type { DragEvent } from "react"; import type { SceneTracks, VideoElement, VideoTrack } from "@/timeline"; import { DragDropController, type DragDropConfig, type DragDropConfigRef, } from "@/timeline/controllers/drag-drop-controller"; import { TimelineDragSource } from "@/timeline/drag-source"; import { mediaTime, TICKS_PER_SECOND, ZERO_MEDIA_TIME } from "@/wasm"; function buildVideo({ id, start, }: { id: string; start: number; }): VideoElement { return { id, type: "video", name: id, mediaId: `media-${id}`, startTime: mediaTime({ ticks: start }), duration: mediaTime({ ticks: TICKS_PER_SECOND }), trimStart: ZERO_MEDIA_TIME, trimEnd: ZERO_MEDIA_TIME, params: { opacity: id === "incoming" ? 0.8 : 1 }, }; } function buildTracks(): SceneTracks { const main: VideoTrack = { id: "visual", type: "video", name: "Visual", elements: [ buildVideo({ id: "outgoing", start: 0 }), buildVideo({ id: "incoming", start: TICKS_PER_SECOND }), ], muted: false, hidden: false, }; return { overlay: [], main, audio: [] }; } function buildDataTransfer(): DataTransfer { return { types: [], files: [], dropEffect: "none", effectAllowed: "none", setData: mock(() => {}), } as unknown as DataTransfer; } function buildDragEvent({ clientX, dataTransfer, }: { clientX: number; dataTransfer: DataTransfer; }): DragEvent { return { clientX, clientY: 10, dataTransfer, preventDefault: mock(() => {}), } as unknown as DragEvent; } function setup() { const dragSource = new TimelineDragSource(); const updateElements = mock(() => {}); const insertElement = mock(() => {}); const executeCommand = mock(() => {}); const tracksElement = { scrollLeft: 0, scrollTop: 0, getBoundingClientRect: () => ({ left: 0, top: 0 }), } as unknown as HTMLDivElement; const config: DragDropConfig = { zoomLevel: 1, getContainerEl: () => tracksElement, getHeaderEl: () => null, getTracksScrollEl: () => tracksElement, getActiveProjectFps: () => null, getActiveProjectId: () => null, getSceneTracks: buildTracks, getCurrentPlayheadTime: () => ZERO_MEDIA_TIME, getMediaAssets: () => [], dragSource, addMediaAsset: () => Promise.resolve(null), executeCommand, insertElement, updateElements, addClipEffect: mock(() => {}), }; const configRef: DragDropConfigRef = { current: config }; return { controller: new DragDropController({ configRef }), dragSource, updateElements, insertElement, executeCommand, }; } function beginTransitionDrag( dragSource: TimelineDragSource, dataTransfer: DataTransfer, ) { dragSource.begin({ dataTransfer, dragData: { type: "transition", id: "push-left", name: "Slide left", transitionKind: "push-left", inDuration: 0.45, outDuration: 0, }, }); } describe("DragDropController transition drops", () => { test("hovers and applies a transition only to the incoming clip", () => { const { controller, dragSource, updateElements, insertElement, executeCommand, } = setup(); const dataTransfer = buildDataTransfer(); beginTransitionDrag(dragSource, dataTransfer); controller.onDragOver(buildDragEvent({ clientX: 50, dataTransfer })); expect(dataTransfer.dropEffect).toBe("copy"); expect(controller.dropTarget?.targetCut).toEqual({ trackId: "visual", outgoingElementId: "outgoing", incomingElementId: "incoming", time: mediaTime({ ticks: TICKS_PER_SECOND }), }); controller.onDrop(buildDragEvent({ clientX: 50, dataTransfer })); expect(updateElements).toHaveBeenCalledTimes(1); expect(updateElements).toHaveBeenCalledWith({ updates: [ { trackId: "visual", elementId: "incoming", patch: { params: { opacity: 0.8, "transition.kind": "push-left", "transition.inDuration": 0.45, "transition.outDuration": 0, }, }, }, ], }); expect(insertElement).not.toHaveBeenCalled(); expect(executeCommand).not.toHaveBeenCalled(); }); test("does nothing when the transition has no cut target", () => { const { controller, dragSource, updateElements, insertElement, executeCommand, } = setup(); const dataTransfer = buildDataTransfer(); beginTransitionDrag(dragSource, dataTransfer); controller.onDragOver(buildDragEvent({ clientX: 80, dataTransfer })); expect(controller.dropTarget).toBeNull(); controller.onDrop(buildDragEvent({ clientX: 80, dataTransfer })); expect(updateElements).not.toHaveBeenCalled(); expect(insertElement).not.toHaveBeenCalled(); expect(executeCommand).not.toHaveBeenCalled(); }); });