summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts')
-rw-r--r--apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts661
1 files changed, 0 insertions, 661 deletions
diff --git a/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts b/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts
deleted file mode 100644
index 7cadb9b7..00000000
--- a/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts
+++ /dev/null
@@ -1,661 +0,0 @@
-import { describe, expect, test } from "bun:test";
-import type {
- AudioElement,
- AudioTrack,
- GraphicElement,
- GraphicTrack,
- OverlayTrack,
- SceneTracks,
- TextElement,
- TextTrack,
- TimelineTrack,
- VideoElement,
- VideoTrack,
-} from "@/lib/timeline";
-import type { Transform } from "@/lib/rendering";
-import { resolveTrackPlacement } from "@/lib/timeline/placement";
-
-function buildTransform(): Transform {
- return {
- scaleX: 1,
- scaleY: 1,
- position: { x: 0, y: 0 },
- rotate: 0,
- };
-}
-
-type TestElement = AudioElement | GraphicElement | TextElement | VideoElement;
-
-function buildElement(params: {
- id: string;
- type: "audio";
- startTime: number;
- duration: number;
-}): AudioElement;
-function buildElement(params: {
- id: string;
- type: "graphic";
- startTime: number;
- duration: number;
-}): GraphicElement;
-function buildElement(params: {
- id: string;
- type: "text";
- startTime: number;
- duration: number;
-}): TextElement;
-function buildElement(params: {
- id: string;
- type: "video";
- startTime: number;
- duration: number;
-}): VideoElement;
-function buildElement({
- id,
- type,
- startTime,
- duration,
-}: {
- id: string;
- type: TestElement["type"];
- startTime: number;
- duration: number;
-}): TestElement {
- switch (type) {
- case "audio":
- return {
- id,
- type: "audio",
- name: id,
- startTime,
- duration,
- trimStart: 0,
- trimEnd: 0,
- volume: 1,
- sourceType: "upload",
- mediaId: `media-${id}`,
- } satisfies AudioElement;
- case "graphic":
- return {
- id,
- type: "graphic",
- name: id,
- startTime,
- duration,
- trimStart: 0,
- trimEnd: 0,
- definitionId: `graphic-${id}`,
- params: {},
- transform: buildTransform(),
- opacity: 1,
- } satisfies GraphicElement;
- case "text":
- return {
- id,
- type: "text",
- name: id,
- startTime,
- duration,
- trimStart: 0,
- trimEnd: 0,
- content: id,
- fontSize: 32,
- fontFamily: "sans-serif",
- color: "#ffffff",
- background: {
- enabled: false,
- color: "#000000",
- },
- textAlign: "left",
- fontWeight: "normal",
- fontStyle: "normal",
- textDecoration: "none",
- transform: buildTransform(),
- opacity: 1,
- } satisfies TextElement;
- case "video":
- return {
- id,
- type: "video",
- name: id,
- startTime,
- duration,
- trimStart: 0,
- trimEnd: 0,
- mediaId: `media-${id}`,
- transform: buildTransform(),
- opacity: 1,
- } satisfies VideoElement;
- }
-
- throw new Error(`Unsupported test element type: ${type}`);
-}
-
-type BuildTrackParams =
- | {
- id: string;
- type: "audio";
- elements?: AudioTrack["elements"];
- }
- | {
- id: string;
- type: "graphic";
- elements?: GraphicTrack["elements"];
- }
- | {
- id: string;
- type: "text";
- elements?: TextTrack["elements"];
- }
- | {
- id: string;
- type: "video";
- elements?: VideoTrack["elements"];
- };
-
-function buildTrack(params: {
- id: string;
- type: "audio";
- elements?: AudioTrack["elements"];
-}): AudioTrack;
-function buildTrack(params: {
- id: string;
- type: "graphic";
- elements?: GraphicTrack["elements"];
-}): GraphicTrack;
-function buildTrack(params: {
- id: string;
- type: "text";
- elements?: TextTrack["elements"];
-}): TextTrack;
-function buildTrack(params: {
- id: string;
- type: "video";
- elements?: VideoTrack["elements"];
-}): VideoTrack;
-function buildTrack(params: BuildTrackParams): TimelineTrack {
- const { id, type } = params;
-
- switch (type) {
- case "audio":
- return {
- id,
- type: "audio",
- name: id,
- elements: params.elements ?? [],
- muted: false,
- };
- case "graphic":
- return {
- id,
- type: "graphic",
- name: id,
- elements: params.elements ?? [],
- hidden: false,
- };
- case "text":
- return {
- id,
- type: "text",
- name: id,
- elements: params.elements ?? [],
- hidden: false,
- };
- case "video":
- return {
- id,
- type: "video",
- name: id,
- elements: params.elements ?? [],
- muted: false,
- hidden: false,
- };
- }
-
- throw new Error(`Unsupported test track type: ${type}`);
-}
-
-function buildTimeSpan({
- startTime,
- duration,
- excludeElementId,
-}: {
- startTime: number;
- duration: number;
- excludeElementId?: string;
-}) {
- return { startTime, duration, excludeElementId };
-}
-
-function buildSceneTracks({
- overlay = [],
- main,
- audio = [],
-}: {
- overlay?: Array<OverlayTrack>;
- main?: VideoTrack;
- audio?: Array<AudioTrack>;
-}): SceneTracks {
- return {
- overlay,
- main:
- main ??
- buildTrack({
- id: "video-main",
- type: "video",
- }),
- audio,
- };
-}
-
-describe("resolveTrackPlacement", () => {
- test("explicit returns the requested compatible track", () => {
- const tracks = buildSceneTracks({
- overlay: [buildTrack({ id: "text-1", type: "text" })],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "text",
- timeSpans: [buildTimeSpan({ startTime: 2, duration: 3 })],
- strategy: { type: "explicit", trackId: "text-1" },
- }),
- ).toEqual({
- kind: "existingTrack",
- trackId: "text-1",
- trackIndex: 0,
- trackType: "text",
- });
- });
-
- test("explicit rejects missing and incompatible tracks", () => {
- const tracks = buildSceneTracks({
- main: buildTrack({ id: "video-1", type: "video" }),
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "text",
- timeSpans: [buildTimeSpan({ startTime: 0, duration: 1 })],
- strategy: { type: "explicit", trackId: "missing" },
- }),
- ).toBeNull();
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "text",
- timeSpans: [buildTimeSpan({ startTime: 0, duration: 1 })],
- strategy: { type: "explicit", trackId: "video-1" },
- }),
- ).toBeNull();
- });
-
- test("firstAvailable picks the first compatible track without overlap", () => {
- const tracks = buildSceneTracks({
- overlay: [
- buildTrack({
- id: "text-1",
- type: "text",
- elements: [
- buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }),
- ],
- }),
- buildTrack({ id: "text-2", type: "text" }),
- ],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "text",
- timeSpans: [buildTimeSpan({ startTime: 2, duration: 1 })],
- strategy: { type: "firstAvailable" },
- }),
- ).toEqual({
- kind: "existingTrack",
- trackId: "text-2",
- trackIndex: 1,
- trackType: "text",
- });
- });
-
- test("firstAvailable creates a new track when all compatible tracks are full", () => {
- const tracks = buildSceneTracks({
- overlay: [
- buildTrack({
- id: "graphic-1",
- type: "graphic",
- elements: [
- buildElement({
- id: "a",
- type: "graphic",
- startTime: 0,
- duration: 5,
- }),
- ],
- }),
- ],
- main: buildTrack({
- id: "video-main",
- type: "video",
- }),
- audio: [buildTrack({ id: "audio-1", type: "audio" })],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "graphic",
- timeSpans: [buildTimeSpan({ startTime: 1, duration: 1 })],
- strategy: { type: "firstAvailable" },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "graphic",
- insertIndex: 0,
- insertPosition: null,
- });
- });
-
- test("preferIndex uses the preferred track when it fits", () => {
- const tracks = buildSceneTracks({
- audio: [buildTrack({ id: "audio-1", type: "audio" })],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "audio",
- timeSpans: [buildTimeSpan({ startTime: 3, duration: 2 })],
- strategy: {
- type: "preferIndex",
- trackIndex: 0,
- hoverDirection: "below",
- },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "audio",
- insertIndex: 1,
- insertPosition: "below",
- });
- });
-
- test("preferIndex creates a new overlay track above the main track", () => {
- const tracks = buildSceneTracks({
- main: buildTrack({ id: "video-main", type: "video" }),
- audio: [buildTrack({ id: "audio-1", type: "audio" })],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "graphic",
- timeSpans: [buildTimeSpan({ startTime: 1, duration: 2 })],
- strategy: {
- type: "preferIndex",
- trackIndex: 1,
- hoverDirection: "below",
- },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "graphic",
- insertIndex: 0,
- insertPosition: "above",
- });
- });
-
- test("preferIndex keeps audio tracks below the main track", () => {
- const tracks = buildSceneTracks({
- overlay: [buildTrack({ id: "text-1", type: "text" })],
- main: buildTrack({ id: "video-main", type: "video" }),
- audio: [buildTrack({ id: "audio-1", type: "audio" })],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "audio",
- timeSpans: [buildTimeSpan({ startTime: 0, duration: 1 })],
- strategy: {
- type: "preferIndex",
- trackIndex: 0,
- hoverDirection: "above",
- createNewTrackOnly: true,
- },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "audio",
- insertIndex: 2,
- insertPosition: "below",
- });
- });
-
- test("aboveSource tries the track above source, then any compatible track", () => {
- const tracks = buildSceneTracks({
- overlay: [
- buildTrack({ id: "text-top", type: "text" }),
- buildTrack({
- id: "text-middle",
- type: "text",
- elements: [
- buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }),
- ],
- }),
- buildTrack({ id: "text-source", type: "text" }),
- ],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "text",
- timeSpans: [buildTimeSpan({ startTime: 1, duration: 1 })],
- strategy: { type: "aboveSource", sourceTrackIndex: 2 },
- }),
- ).toEqual({
- kind: "existingTrack",
- trackId: "text-top",
- trackIndex: 0,
- trackType: "text",
- });
- });
-
- test("aboveSource creates a new overlay track in the overlay zone when none fit", () => {
- const tracks = buildSceneTracks({
- overlay: [
- buildTrack({
- id: "text-top",
- type: "text",
- elements: [
- buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }),
- ],
- }),
- buildTrack({
- id: "text-source",
- type: "text",
- elements: [
- buildElement({ id: "b", type: "text", startTime: 0, duration: 5 }),
- ],
- }),
- ],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "text",
- timeSpans: [buildTimeSpan({ startTime: 1, duration: 1 })],
- strategy: { type: "aboveSource", sourceTrackIndex: 1 },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "text",
- insertIndex: 0,
- insertPosition: null,
- });
- });
-
- test("alwaysNew honors highest and default insertion rules", () => {
- const tracks = buildSceneTracks({
- main: buildTrack({ id: "video-main", type: "video" }),
- audio: [buildTrack({ id: "audio-1", type: "audio" })],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "audio",
- timeSpans: [],
- strategy: { type: "alwaysNew", position: "highest" },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "audio",
- insertIndex: 1,
- insertPosition: null,
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "audio",
- timeSpans: [],
- strategy: { type: "alwaysNew", position: "default" },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "audio",
- insertIndex: 2,
- insertPosition: null,
- });
- });
-
- test("batch time spans reject tracks when any span overlaps", () => {
- const tracks = buildSceneTracks({
- audio: [
- buildTrack({
- id: "audio-1",
- type: "audio",
- elements: [
- buildElement({ id: "a", type: "audio", startTime: 0, duration: 2 }),
- buildElement({ id: "b", type: "audio", startTime: 5, duration: 2 }),
- ],
- }),
- ],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "audio",
- timeSpans: [
- buildTimeSpan({ startTime: 2.5, duration: 1 }),
- buildTimeSpan({ startTime: 5.5, duration: 1 }),
- ],
- strategy: { type: "firstAvailable" },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "audio",
- insertIndex: 1,
- insertPosition: null,
- });
- });
-
- test("handles main-only timelines, single tracks, and track-type derivation", () => {
- expect(
- resolveTrackPlacement({
- tracks: buildSceneTracks({}),
- elementType: "video",
- timeSpans: [buildTimeSpan({ startTime: 0, duration: 3 })],
- strategy: {
- type: "preferIndex",
- trackIndex: 0,
- hoverDirection: "below",
- createNewTrackOnly: true,
- },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "video",
- insertIndex: 0,
- insertPosition: "above",
- });
-
- expect(
- resolveTrackPlacement({
- tracks: buildSceneTracks({
- audio: [buildTrack({ id: "audio-1", type: "audio" })],
- }),
- elementType: "audio",
- timeSpans: [],
- strategy: { type: "alwaysNew", position: "default" },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "audio",
- insertIndex: 2,
- insertPosition: null,
- });
- });
-
- test("existingTrack on main video includes adjustedStartTime when start snaps", () => {
- const tracks = buildSceneTracks({
- main: buildTrack({
- id: "video-main",
- type: "video",
- elements: [
- buildElement({ id: "a", type: "video", startTime: 5, duration: 5 }),
- ],
- }),
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "video",
- timeSpans: [buildTimeSpan({ startTime: 2, duration: 2 })],
- strategy: { type: "explicit", trackId: "video-main" },
- }),
- ).toEqual({
- kind: "existingTrack",
- trackId: "video-main",
- trackIndex: 0,
- trackType: "video",
- adjustedStartTime: 0,
- });
- });
-
- test("preferIndex uses vertical drag direction when hovered track is incompatible", () => {
- const tracks = buildSceneTracks({
- overlay: [buildTrack({ id: "text-1", type: "text" })],
- main: buildTrack({ id: "video-main", type: "video" }),
- audio: [buildTrack({ id: "audio-1", type: "audio" })],
- });
-
- expect(
- resolveTrackPlacement({
- tracks,
- elementType: "audio",
- timeSpans: [buildTimeSpan({ startTime: 0, duration: 1 })],
- strategy: {
- type: "preferIndex",
- trackIndex: 0,
- hoverDirection: "above",
- verticalDragDirection: "down",
- },
- }),
- ).toEqual({
- kind: "newTrack",
- trackType: "audio",
- insertIndex: 2,
- insertPosition: "below",
- });
- });
-});