summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/timeline/group-move/__tests__
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-15 05:43:29 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-15 05:43:29 +0200
commitac43a5a73fa905cdac905bc956763b6093d6738e (patch)
tree8d9ff8c3a9ae55bac8860215bec69cc5b6094a1c /apps/web/src/lib/timeline/group-move/__tests__
parent6312fc5b078689bc82c52fda9b7f92ee0a21d311 (diff)
downloadopencut-ac43a5a73fa905cdac905bc956763b6093d6738e.tar.gz
feat: move/resize multiple elements
Diffstat (limited to 'apps/web/src/lib/timeline/group-move/__tests__')
-rw-r--r--apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts287
1 files changed, 287 insertions, 0 deletions
diff --git a/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts b/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts
new file mode 100644
index 00000000..e6c86e41
--- /dev/null
+++ b/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts
@@ -0,0 +1,287 @@
+import { describe, expect, test } from "bun:test";
+import type {
+ AudioTrack,
+ OverlayTrack,
+ SceneTracks,
+ TextElement,
+ TextTrack,
+ VideoElement,
+ VideoTrack,
+} from "@/lib/timeline/types";
+import type { Transform } from "@/lib/rendering";
+import { buildMoveGroup } from "@/lib/timeline/group-move/build-group";
+import { resolveGroupMove } from "@/lib/timeline/group-move/resolve-move";
+
+function buildTransform(): Transform {
+ return {
+ scaleX: 1,
+ scaleY: 1,
+ position: { x: 0, y: 0 },
+ rotate: 0,
+ };
+}
+
+function buildVideoElement({
+ id,
+ startTime,
+ duration,
+}: {
+ id: string;
+ startTime: number;
+ duration: number;
+}): VideoElement {
+ return {
+ id,
+ type: "video",
+ name: id,
+ startTime,
+ duration,
+ trimStart: 0,
+ trimEnd: 0,
+ mediaId: `${id}-media`,
+ transform: buildTransform(),
+ opacity: 1,
+ };
+}
+
+function buildTextElement({
+ id,
+ startTime,
+ duration,
+}: {
+ id: string;
+ startTime: number;
+ duration: number;
+}): TextElement {
+ return {
+ id,
+ type: "text",
+ name: id,
+ startTime,
+ duration,
+ trimStart: 0,
+ trimEnd: 0,
+ content: id,
+ fontSize: 32,
+ fontFamily: "Inter",
+ color: "#ffffff",
+ background: {
+ enabled: false,
+ color: "#000000",
+ },
+ textAlign: "left",
+ fontWeight: "normal",
+ fontStyle: "normal",
+ textDecoration: "none",
+ transform: buildTransform(),
+ opacity: 1,
+ };
+}
+
+function buildVideoTrack({
+ id,
+ elements = [],
+}: {
+ id: string;
+ elements?: VideoTrack["elements"];
+}): VideoTrack {
+ return {
+ id,
+ type: "video",
+ name: id,
+ elements,
+ muted: false,
+ hidden: false,
+ };
+}
+
+function buildTextTrack({
+ id,
+ elements = [],
+}: {
+ id: string;
+ elements?: TextTrack["elements"];
+}): TextTrack {
+ return {
+ id,
+ type: "text",
+ name: id,
+ elements,
+ hidden: false,
+ };
+}
+
+function buildTracks({
+ overlay,
+ main,
+ audio = [],
+}: {
+ overlay: OverlayTrack[];
+ main: VideoTrack;
+ audio?: AudioTrack[];
+}): SceneTracks {
+ return {
+ overlay,
+ main,
+ audio,
+ };
+}
+
+describe("group move", () => {
+ test("buildMoveGroup keeps rigid time offsets", () => {
+ const tracks = buildTracks({
+ overlay: [
+ buildVideoTrack({
+ id: "overlay-video",
+ elements: [
+ buildVideoElement({ id: "video-1", startTime: 5, duration: 4 }),
+ ],
+ }),
+ buildTextTrack({
+ id: "overlay-text",
+ elements: [
+ buildTextElement({ id: "text-1", startTime: 8, duration: 3 }),
+ ],
+ }),
+ ],
+ main: buildVideoTrack({ id: "main" }),
+ });
+
+ const group = buildMoveGroup({
+ anchorRef: { trackId: "overlay-video", elementId: "video-1" },
+ selectedElements: [
+ { trackId: "overlay-video", elementId: "video-1" },
+ { trackId: "overlay-text", elementId: "text-1" },
+ ],
+ tracks,
+ });
+
+ expect(group).not.toBeNull();
+ expect(group?.members.map((member) => member.timeOffset)).toEqual([0, 3]);
+ });
+
+ test("resolveGroupMove preserves order and clamps rigidly at time zero", () => {
+ const tracks = buildTracks({
+ overlay: [
+ buildTextTrack({
+ id: "overlay-text",
+ elements: [
+ buildTextElement({ id: "text-1", startTime: 0, duration: 3 }),
+ ],
+ }),
+ buildVideoTrack({
+ id: "overlay-video",
+ elements: [
+ buildVideoElement({ id: "video-1", startTime: 5, duration: 4 }),
+ ],
+ }),
+ ],
+ main: buildVideoTrack({ id: "main" }),
+ });
+ const group = buildMoveGroup({
+ anchorRef: { trackId: "overlay-video", elementId: "video-1" },
+ selectedElements: [
+ { trackId: "overlay-video", elementId: "video-1" },
+ { trackId: "overlay-text", elementId: "text-1" },
+ ],
+ tracks,
+ });
+ if (!group) {
+ throw new Error("Expected group");
+ }
+
+ const result = resolveGroupMove({
+ group,
+ tracks,
+ anchorStartTime: 7,
+ target: {
+ kind: "existingTrack",
+ anchorTargetTrackId: "main",
+ },
+ });
+
+ expect(result?.moves).toEqual([
+ {
+ sourceTrackId: "overlay-video",
+ elementId: "video-1",
+ targetTrackId: "main",
+ newStartTime: 5,
+ },
+ {
+ sourceTrackId: "overlay-text",
+ elementId: "text-1",
+ targetTrackId: "overlay-text",
+ newStartTime: 0,
+ },
+ ]);
+ });
+
+ test("resolveGroupMove falls back to new overlay tracks when the group cannot cross main", () => {
+ const tracks = buildTracks({
+ overlay: [
+ buildVideoTrack({
+ id: "overlay-video",
+ elements: [
+ buildVideoElement({ id: "video-1", startTime: 5, duration: 4 }),
+ ],
+ }),
+ buildTextTrack({
+ id: "overlay-text",
+ elements: [
+ buildTextElement({ id: "text-1", startTime: 5, duration: 3 }),
+ ],
+ }),
+ ],
+ main: buildVideoTrack({ id: "main" }),
+ });
+ const group = buildMoveGroup({
+ anchorRef: { trackId: "overlay-video", elementId: "video-1" },
+ selectedElements: [
+ { trackId: "overlay-video", elementId: "video-1" },
+ { trackId: "overlay-text", elementId: "text-1" },
+ ],
+ tracks,
+ });
+ if (!group) {
+ throw new Error("Expected group");
+ }
+
+ const result = resolveGroupMove({
+ group,
+ tracks,
+ anchorStartTime: 6,
+ target: {
+ kind: "newTracks",
+ anchorInsertIndex: 2,
+ newTrackIds: ["new-video-track", "new-text-track"],
+ },
+ });
+
+ expect(result?.createTracks).toEqual([
+ {
+ id: "new-video-track",
+ type: "video",
+ index: 2,
+ },
+ {
+ id: "new-text-track",
+ type: "text",
+ index: 3,
+ },
+ ]);
+ expect(result?.moves).toEqual([
+ {
+ sourceTrackId: "overlay-video",
+ elementId: "video-1",
+ targetTrackId: "new-video-track",
+ newStartTime: 6,
+ },
+ {
+ sourceTrackId: "overlay-text",
+ elementId: "text-1",
+ targetTrackId: "new-text-track",
+ newStartTime: 6,
+ },
+ ]);
+ });
+});