summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/controllers/__tests__
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/timeline/controllers/__tests__')
-rw-r--r--apps/web/src/timeline/controllers/__tests__/drag-drop-controller-transition.test.ts190
1 files changed, 190 insertions, 0 deletions
diff --git a/apps/web/src/timeline/controllers/__tests__/drag-drop-controller-transition.test.ts b/apps/web/src/timeline/controllers/__tests__/drag-drop-controller-transition.test.ts
new file mode 100644
index 00000000..b24f9ad9
--- /dev/null
+++ b/apps/web/src/timeline/controllers/__tests__/drag-drop-controller-transition.test.ts
@@ -0,0 +1,190 @@
+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<DragDropConfig["updateElements"]>(() => {});
+ const insertElement = mock<DragDropConfig["insertElement"]>(() => {});
+ const executeCommand = mock<DragDropConfig["executeCommand"]>(() => {});
+ 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();
+ });
+});