From eea6d43c82e1fd09d94d73001090dad205b1f1d2 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 26 Apr 2026 02:50:04 +0200 Subject: fix: model editor time as wasm MediaTime ticks end-to-end --- .../migrations/__tests__/v27-to-v28.test.ts | 257 +++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 apps/web/src/services/storage/migrations/__tests__/v27-to-v28.test.ts (limited to 'apps/web/src/services/storage/migrations/__tests__') diff --git a/apps/web/src/services/storage/migrations/__tests__/v27-to-v28.test.ts b/apps/web/src/services/storage/migrations/__tests__/v27-to-v28.test.ts new file mode 100644 index 00000000..d467c8a1 --- /dev/null +++ b/apps/web/src/services/storage/migrations/__tests__/v27-to-v28.test.ts @@ -0,0 +1,257 @@ +import { describe, expect, test } from "bun:test"; +import { transformProjectV27ToV28 } from "../transformers/v27-to-v28"; + +describe("V27 to V28 Migration", () => { + test("rounds persisted media-time floats back to integer ticks", () => { + const result = transformProjectV27ToV28({ + project: { + id: "project-v27-float-time", + version: 27, + metadata: { + id: "project-v27-float-time", + name: "Project", + duration: 2_152_466.3677130044, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, + settings: { + fps: { numerator: 30, denominator: 1 }, + canvasSize: { width: 1920, height: 1080 }, + background: { type: "color", color: "#000000" }, + }, + timelineViewState: { + zoomLevel: 1.25, + scrollLeft: 120, + playheadTime: 301_234.8, + }, + scenes: [ + { + id: "scene-1", + name: "Scene 1", + isMain: true, + bookmarks: [ + { + time: 120_000.4, + duration: 60_000.6, + note: "Marker", + color: "#ff0000", + }, + ], + tracks: { + main: { + id: "main-track", + type: "video", + name: "Main", + muted: false, + hidden: false, + elements: [ + { + id: "element-1", + type: "video", + name: "Clip", + startTime: 300_000.49, + duration: 2_152_466.3677130044, + trimStart: 30_000.2, + trimEnd: 15_000.7, + sourceDuration: 2_197_467.6, + mediaId: "media-1", + transform: { + scaleX: 1, + scaleY: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }, + opacity: 1, + animations: { + channels: { + opacity: { + kind: "scalar", + keys: [ + { + id: "key-1", + time: 1_000.6, + value: 1, + segmentToNext: "bezier", + tangentMode: "flat", + leftHandle: { dt: -120.6, dv: 0 }, + rightHandle: { dt: 60.4, dv: 0 }, + }, + ], + }, + }, + }, + }, + ], + }, + overlay: [], + audio: [], + }, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, + ], + }, + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(28); + + const metadata = result.project.metadata as Record; + expect(metadata.duration).toBe(2_152_466); + + const timelineViewState = result.project.timelineViewState as Record< + string, + unknown + >; + expect(timelineViewState.playheadTime).toBe(301_235); + expect(timelineViewState.zoomLevel).toBe(1.25); + expect(timelineViewState.scrollLeft).toBe(120); + + const scenes = result.project.scenes as Array>; + const scene = scenes[0]; + expect(scene.bookmarks).toEqual([ + { + time: 120_000, + duration: 60_001, + note: "Marker", + color: "#ff0000", + }, + ]); + + const tracks = scene.tracks as Record; + const mainTrack = tracks.main as Record; + const elements = mainTrack.elements as Array>; + const element = elements[0]; + expect(element.startTime).toBe(300_000); + expect(element.duration).toBe(2_152_466); + expect(element.trimStart).toBe(30_000); + expect(element.trimEnd).toBe(15_001); + expect(element.sourceDuration).toBe(2_197_468); + + const animations = element.animations as Record; + const channels = animations.channels as Record>; + const opacityChannel = channels.opacity; + expect(opacityChannel.keys).toEqual([ + { + id: "key-1", + time: 1_001, + value: 1, + segmentToNext: "bezier", + tangentMode: "flat", + leftHandle: { dt: -121, dv: 0 }, + rightHandle: { dt: 60, dv: 0 }, + }, + ]); + }); + + test("keeps already-integer media-time values unchanged", () => { + const result = transformProjectV27ToV28({ + project: { + id: "project-v27-integer-time", + version: 27, + metadata: { + id: "project-v27-integer-time", + name: "Project", + duration: 120_000, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, + timelineViewState: { + zoomLevel: 2, + scrollLeft: 300, + playheadTime: 30_000, + }, + scenes: [ + { + id: "scene-1", + bookmarks: [{ time: 60_000, duration: 15_000 }], + tracks: { + main: { + id: "main-track", + type: "video", + name: "Main", + muted: false, + hidden: false, + elements: [ + { + id: "element-1", + type: "video", + name: "Clip", + startTime: 10_000, + duration: 20_000, + trimStart: 1_000, + trimEnd: 2_000, + sourceDuration: 23_000, + mediaId: "media-1", + transform: { + scaleX: 1, + scaleY: 1, + position: { x: 0, y: 0 }, + rotate: 0, + }, + opacity: 1, + }, + ], + }, + overlay: [], + audio: [], + }, + }, + ], + }, + }); + + expect(result.skipped).toBe(false); + expect(result.project.version).toBe(28); + + const metadata = result.project.metadata as Record; + expect(metadata.duration).toBe(120_000); + + const timelineViewState = result.project.timelineViewState as Record< + string, + unknown + >; + expect(timelineViewState).toEqual({ + zoomLevel: 2, + scrollLeft: 300, + playheadTime: 30_000, + }); + + const scenes = result.project.scenes as Array>; + const scene = scenes[0]; + expect(scene.bookmarks).toEqual([{ time: 60_000, duration: 15_000 }]); + + const tracks = scene.tracks as Record; + const mainTrack = tracks.main as Record; + const element = (mainTrack.elements as Array>)[0]; + expect(element.startTime).toBe(10_000); + expect(element.duration).toBe(20_000); + expect(element.trimStart).toBe(1_000); + expect(element.trimEnd).toBe(2_000); + expect(element.sourceDuration).toBe(23_000); + }); + + test("skips projects already on v28", () => { + const result = transformProjectV27ToV28({ + project: { + id: "project-v28", + version: 28, + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("already v28"); + }); + + test("skips projects not on v27", () => { + const result = transformProjectV27ToV28({ + project: { + id: "project-v26", + version: 26, + }, + }); + + expect(result.skipped).toBe(true); + expect(result.reason).toBe("not v27"); + }); +}); -- cgit v1.3.1