summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/migrations/__tests__
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-27 00:55:15 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:56:48 +0200
commit4d127a038bf768eded68aae3cfa5c1b12241bdac (patch)
tree16bbb801caf6770546cff4fdb37b6408ec9ead98 /apps/web/src/services/storage/migrations/__tests__
parent82817cb50cb0487350573d6129130aec705f924b (diff)
downloadopencut-4d127a038bf768eded68aae3cfa5c1b12241bdac.tar.gz
chore: normalize line endings
Diffstat (limited to 'apps/web/src/services/storage/migrations/__tests__')
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts20
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts108
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts186
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts264
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts126
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts280
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts274
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts184
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts350
9 files changed, 896 insertions, 896 deletions
diff --git a/apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts b/apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts
index 5f82e14a..a97e42d7 100644
--- a/apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/fixtures/index.ts
@@ -1,10 +1,10 @@
-/**
- * Test fixtures for storage migrations.
- * These represent real project data shapes from each version.
- */
-
-export * from "./v0";
-export * from "./v1";
-export * from "./v2";
-export * from "./v3";
-export * from "./v5";
+/**
+ * Test fixtures for storage migrations.
+ * These represent real project data shapes from each version.
+ */
+
+export * from "./v0";
+export * from "./v1";
+export * from "./v2";
+export * from "./v3";
+export * from "./v5";
diff --git a/apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts b/apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts
index 5e181c76..89bb83c3 100644
--- a/apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/fixtures/v5.ts
@@ -1,54 +1,54 @@
-export const v5Project = {
- id: "project-v5-456",
- version: 5,
- metadata: {
- id: "project-v5-456",
- name: "My V5 Project",
- thumbnail: "data:image/png;base64,abc123",
- duration: 30,
- createdAt: "2024-06-01T10:00:00.000Z",
- updatedAt: "2024-06-01T14:00:00.000Z",
- },
- settings: {
- fps: 30,
- canvasSize: { width: 1920, height: 1080 },
- background: { type: "color", color: "#000000" },
- },
- currentSceneId: "scene-main",
- scenes: [
- {
- id: "scene-main",
- name: "Main scene",
- isMain: true,
- tracks: [
- {
- id: "track-1",
- type: "video",
- name: "Video Track",
- isMain: true,
- elements: [],
- },
- ],
- bookmarks: [2.0, 5.5, 12.0],
- createdAt: "2024-06-01T10:00:00.000Z",
- updatedAt: "2024-06-01T14:00:00.000Z",
- },
- {
- id: "scene-intro",
- name: "Intro",
- isMain: false,
- tracks: [
- {
- id: "track-2",
- type: "video",
- name: "Video Track",
- isMain: true,
- elements: [],
- },
- ],
- bookmarks: [],
- createdAt: "2024-06-01T10:00:00.000Z",
- updatedAt: "2024-06-01T14:00:00.000Z",
- },
- ],
-};
+export const v5Project = {
+ id: "project-v5-456",
+ version: 5,
+ metadata: {
+ id: "project-v5-456",
+ name: "My V5 Project",
+ thumbnail: "data:image/png;base64,abc123",
+ duration: 30,
+ createdAt: "2024-06-01T10:00:00.000Z",
+ updatedAt: "2024-06-01T14:00:00.000Z",
+ },
+ settings: {
+ fps: 30,
+ canvasSize: { width: 1920, height: 1080 },
+ background: { type: "color", color: "#000000" },
+ },
+ currentSceneId: "scene-main",
+ scenes: [
+ {
+ id: "scene-main",
+ name: "Main scene",
+ isMain: true,
+ tracks: [
+ {
+ id: "track-1",
+ type: "video",
+ name: "Video Track",
+ isMain: true,
+ elements: [],
+ },
+ ],
+ bookmarks: [2.0, 5.5, 12.0],
+ createdAt: "2024-06-01T10:00:00.000Z",
+ updatedAt: "2024-06-01T14:00:00.000Z",
+ },
+ {
+ id: "scene-intro",
+ name: "Intro",
+ isMain: false,
+ tracks: [
+ {
+ id: "track-2",
+ type: "video",
+ name: "Video Track",
+ isMain: true,
+ elements: [],
+ },
+ ],
+ bookmarks: [],
+ createdAt: "2024-06-01T10:00:00.000Z",
+ updatedAt: "2024-06-01T14:00:00.000Z",
+ },
+ ],
+};
diff --git a/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts b/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts
index f2a4908f..122af2cb 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v15-to-v16.test.ts
@@ -1,93 +1,93 @@
-import { describe, expect, test } from "bun:test";
-import { transformProjectV15ToV16 } from "../transformers/v15-to-v16";
-
-describe("V15 to V16 Migration", () => {
- test("renames sticker tracks to graphic tracks", () => {
- const result = transformProjectV15ToV16({
- project: {
- id: "project-v15",
- version: 15,
- metadata: {
- id: "project-v15",
- name: "Project",
- createdAt: "2024-01-01T00:00:00.000Z",
- updatedAt: "2024-01-01T00:00:00.000Z",
- },
- settings: {
- fps: 30,
- canvasSize: { width: 1920, height: 1080 },
- background: { type: "color", color: "#000000" },
- },
- currentSceneId: "scene-main",
- scenes: [
- {
- id: "scene-main",
- name: "Main scene",
- isMain: true,
- tracks: [
- {
- id: "track-graphic",
- type: "sticker",
- name: "Sticker Track",
- hidden: false,
- elements: [
- {
- id: "sticker-1",
- type: "sticker",
- name: "Logo",
- stickerId: "icons:mdi:home",
- duration: 5,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- transform: {
- scaleX: 1,
- scaleY: 1,
- position: { x: 0, y: 0 },
- rotate: 0,
- },
- opacity: 1,
- },
- ],
- },
- ],
- bookmarks: [],
- createdAt: "2024-01-01T00:00:00.000Z",
- updatedAt: "2024-01-01T00:00:00.000Z",
- },
- ],
- },
- });
-
- expect(result.skipped).toBe(false);
- expect(result.project.version).toBe(16);
- expect(
- (result.project.scenes as Array<{ tracks: Array<{ type: string }> }>)[0]
- .tracks[0].type,
- ).toBe("graphic");
- });
-
- test("skips projects already on v16", () => {
- const result = transformProjectV15ToV16({
- project: {
- id: "project-v16",
- version: 16,
- },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("already v16");
- });
-
- test("skips projects with no id", () => {
- const result = transformProjectV15ToV16({
- project: {
- version: 15,
- scenes: [],
- },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("no project id");
- });
-});
+import { describe, expect, test } from "bun:test";
+import { transformProjectV15ToV16 } from "../transformers/v15-to-v16";
+
+describe("V15 to V16 Migration", () => {
+ test("renames sticker tracks to graphic tracks", () => {
+ const result = transformProjectV15ToV16({
+ project: {
+ id: "project-v15",
+ version: 15,
+ metadata: {
+ id: "project-v15",
+ name: "Project",
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ },
+ settings: {
+ fps: 30,
+ canvasSize: { width: 1920, height: 1080 },
+ background: { type: "color", color: "#000000" },
+ },
+ currentSceneId: "scene-main",
+ scenes: [
+ {
+ id: "scene-main",
+ name: "Main scene",
+ isMain: true,
+ tracks: [
+ {
+ id: "track-graphic",
+ type: "sticker",
+ name: "Sticker Track",
+ hidden: false,
+ elements: [
+ {
+ id: "sticker-1",
+ type: "sticker",
+ name: "Logo",
+ stickerId: "icons:mdi:home",
+ duration: 5,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: {
+ scaleX: 1,
+ scaleY: 1,
+ position: { x: 0, y: 0 },
+ rotate: 0,
+ },
+ opacity: 1,
+ },
+ ],
+ },
+ ],
+ bookmarks: [],
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ },
+ ],
+ },
+ });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(16);
+ expect(
+ (result.project.scenes as Array<{ tracks: Array<{ type: string }> }>)[0]
+ .tracks[0].type,
+ ).toBe("graphic");
+ });
+
+ test("skips projects already on v16", () => {
+ const result = transformProjectV15ToV16({
+ project: {
+ id: "project-v16",
+ version: 16,
+ },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("already v16");
+ });
+
+ test("skips projects with no id", () => {
+ const result = transformProjectV15ToV16({
+ project: {
+ version: 15,
+ scenes: [],
+ },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("no project id");
+ });
+});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts b/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts
index e246943e..352ca762 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v16-to-v17.test.ts
@@ -1,132 +1,132 @@
-import { describe, expect, test } from "bun:test";
-import { transformProjectV16ToV17 } from "../transformers/v16-to-v17";
-
-describe("V16 to V17 Migration", () => {
- test("adds center stroke alignment to masks that do not have it", () => {
- const result = transformProjectV16ToV17({
- project: {
- id: "project-v16",
- version: 16,
- metadata: {
- id: "project-v16",
- name: "Project",
- createdAt: "2024-01-01T00:00:00.000Z",
- updatedAt: "2024-01-01T00:00:00.000Z",
- },
- settings: {
- fps: 30,
- canvasSize: { width: 1920, height: 1080 },
- background: { type: "color", color: "#000000" },
- },
- currentSceneId: "scene-main",
- scenes: [
- {
- id: "scene-main",
- name: "Main scene",
- isMain: true,
- tracks: [
- {
- id: "track-video",
- type: "video",
- name: "Video Track",
- hidden: false,
- elements: [
- {
- id: "video-1",
- type: "video",
- name: "Clip",
- mediaId: "media-1",
- duration: 5,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- transform: {
- scaleX: 1,
- scaleY: 1,
- position: { x: 0, y: 0 },
- rotate: 0,
- },
- opacity: 1,
- masks: [
- {
- id: "mask-1",
- type: "rectangle",
- params: {
- feather: 0,
- inverted: false,
- strokeColor: "#ffffff",
- strokeWidth: 8,
- centerX: 0,
- centerY: 0,
- width: 0.6,
- height: 0.6,
- rotation: 0,
- scale: 1,
- },
- },
- {
- id: "mask-2",
- type: "ellipse",
- params: {
- feather: 0,
- inverted: false,
- strokeColor: "#ffffff",
- strokeWidth: 8,
- strokeAlign: "outside",
- centerX: 0,
- centerY: 0,
- width: 0.6,
- height: 0.6,
- rotation: 0,
- scale: 1,
- },
- },
- ],
- },
- ],
- },
- ],
- bookmarks: [],
- createdAt: "2024-01-01T00:00:00.000Z",
- updatedAt: "2024-01-01T00:00:00.000Z",
- },
- ],
- },
- });
-
- expect(result.skipped).toBe(false);
- expect(result.project.version).toBe(17);
-
- const migratedMasks = (
- ((result.project.scenes as Array<{ tracks: Array<{ elements: Array<{ masks: Array<{ params: Record<string, unknown> }> }> }> }>)[0]
- .tracks[0].elements[0].masks)
- );
-
- expect(migratedMasks[0].params.strokeAlign).toBe("center");
- expect(migratedMasks[1].params.strokeAlign).toBe("outside");
- });
-
- test("skips projects already on v17", () => {
- const result = transformProjectV16ToV17({
- project: {
- id: "project-v17",
- version: 17,
- },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("already v17");
- });
-
- test("skips projects with no id", () => {
- const result = transformProjectV16ToV17({
- project: {
- version: 16,
- scenes: [],
- },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("no project id");
- });
-});
+import { describe, expect, test } from "bun:test";
+import { transformProjectV16ToV17 } from "../transformers/v16-to-v17";
+
+describe("V16 to V17 Migration", () => {
+ test("adds center stroke alignment to masks that do not have it", () => {
+ const result = transformProjectV16ToV17({
+ project: {
+ id: "project-v16",
+ version: 16,
+ metadata: {
+ id: "project-v16",
+ name: "Project",
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ },
+ settings: {
+ fps: 30,
+ canvasSize: { width: 1920, height: 1080 },
+ background: { type: "color", color: "#000000" },
+ },
+ currentSceneId: "scene-main",
+ scenes: [
+ {
+ id: "scene-main",
+ name: "Main scene",
+ isMain: true,
+ tracks: [
+ {
+ id: "track-video",
+ type: "video",
+ name: "Video Track",
+ hidden: false,
+ elements: [
+ {
+ id: "video-1",
+ type: "video",
+ name: "Clip",
+ mediaId: "media-1",
+ duration: 5,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: {
+ scaleX: 1,
+ scaleY: 1,
+ position: { x: 0, y: 0 },
+ rotate: 0,
+ },
+ opacity: 1,
+ masks: [
+ {
+ id: "mask-1",
+ type: "rectangle",
+ params: {
+ feather: 0,
+ inverted: false,
+ strokeColor: "#ffffff",
+ strokeWidth: 8,
+ centerX: 0,
+ centerY: 0,
+ width: 0.6,
+ height: 0.6,
+ rotation: 0,
+ scale: 1,
+ },
+ },
+ {
+ id: "mask-2",
+ type: "ellipse",
+ params: {
+ feather: 0,
+ inverted: false,
+ strokeColor: "#ffffff",
+ strokeWidth: 8,
+ strokeAlign: "outside",
+ centerX: 0,
+ centerY: 0,
+ width: 0.6,
+ height: 0.6,
+ rotation: 0,
+ scale: 1,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ bookmarks: [],
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ },
+ ],
+ },
+ });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(17);
+
+ const migratedMasks = (
+ ((result.project.scenes as Array<{ tracks: Array<{ elements: Array<{ masks: Array<{ params: Record<string, unknown> }> }> }> }>)[0]
+ .tracks[0].elements[0].masks)
+ );
+
+ expect(migratedMasks[0].params.strokeAlign).toBe("center");
+ expect(migratedMasks[1].params.strokeAlign).toBe("outside");
+ });
+
+ test("skips projects already on v17", () => {
+ const result = transformProjectV16ToV17({
+ project: {
+ id: "project-v17",
+ version: 17,
+ },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("already v17");
+ });
+
+ test("skips projects with no id", () => {
+ const result = transformProjectV16ToV17({
+ project: {
+ version: 16,
+ scenes: [],
+ },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("no project id");
+ });
+});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts b/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts
index 7524a8d4..0f679b6d 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v18-to-v19.test.ts
@@ -1,63 +1,63 @@
-import { describe, expect, test } from "bun:test";
-import { transformProjectV18ToV19 } from "../transformers/v18-to-v19";
-
-describe("V18 to V19 Migration", () => {
- test("adds canvas size mode and empty remembered custom size defaults", () => {
- const result = transformProjectV18ToV19({
- project: {
- id: "project-v18-defaults",
- version: 18,
- metadata: {
- id: "project-v18-defaults",
- name: "Project",
- createdAt: "2024-01-01T00:00:00.000Z",
- updatedAt: "2024-01-01T00:00:00.000Z",
- },
- settings: {
- fps: 30,
- canvasSize: { width: 1920, height: 1080 },
- originalCanvasSize: { width: 1920, height: 1080 },
- background: { type: "color", color: "#000000" },
- },
- currentSceneId: "scene-main",
- scenes: [],
- },
- });
-
- expect(result.skipped).toBe(false);
- expect(result.project.version).toBe(19);
- expect(
- (result.project.settings as Record<string, unknown>).canvasSizeMode,
- ).toBe("preset");
- expect(
- (result.project.settings as Record<string, unknown>).lastCustomCanvasSize,
- ).toBeNull();
- expect(
- (result.project.settings as Record<string, unknown>).originalCanvasSize,
- ).toEqual({ width: 1920, height: 1080 });
- });
-
- test("skips projects already on v19", () => {
- const result = transformProjectV18ToV19({
- project: {
- id: "project-v19",
- version: 19,
- },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("already v19");
- });
-
- test("skips projects with no id", () => {
- const result = transformProjectV18ToV19({
- project: {
- version: 18,
- scenes: [],
- },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("no project id");
- });
-});
+import { describe, expect, test } from "bun:test";
+import { transformProjectV18ToV19 } from "../transformers/v18-to-v19";
+
+describe("V18 to V19 Migration", () => {
+ test("adds canvas size mode and empty remembered custom size defaults", () => {
+ const result = transformProjectV18ToV19({
+ project: {
+ id: "project-v18-defaults",
+ version: 18,
+ metadata: {
+ id: "project-v18-defaults",
+ name: "Project",
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ },
+ settings: {
+ fps: 30,
+ canvasSize: { width: 1920, height: 1080 },
+ originalCanvasSize: { width: 1920, height: 1080 },
+ background: { type: "color", color: "#000000" },
+ },
+ currentSceneId: "scene-main",
+ scenes: [],
+ },
+ });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(19);
+ expect(
+ (result.project.settings as Record<string, unknown>).canvasSizeMode,
+ ).toBe("preset");
+ expect(
+ (result.project.settings as Record<string, unknown>).lastCustomCanvasSize,
+ ).toBeNull();
+ expect(
+ (result.project.settings as Record<string, unknown>).originalCanvasSize,
+ ).toEqual({ width: 1920, height: 1080 });
+ });
+
+ test("skips projects already on v19", () => {
+ const result = transformProjectV18ToV19({
+ project: {
+ id: "project-v19",
+ version: 19,
+ },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("already v19");
+ });
+
+ test("skips projects with no id", () => {
+ const result = transformProjectV18ToV19({
+ project: {
+ version: 18,
+ scenes: [],
+ },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("no project id");
+ });
+});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts b/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts
index 81d03147..6c2d6597 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v3-to-v4.test.ts
@@ -1,140 +1,140 @@
-import { describe, expect, test } from "bun:test";
-import { transformProjectV3ToV4 } from "../transformers/v3-to-v4";
-import { v3Project } from "./fixtures";
-
-describe("V3 to V4 Migration", () => {
- test("normalizes legacy text fontWeight values", () => {
- const projectWithLegacyTextWeight = {
- ...v3Project,
- scenes: [
- {
- ...v3Project.scenes[0],
- tracks: [
- {
- id: "track-text",
- type: "text",
- name: "Text Track",
- hidden: false,
- elements: [
- {
- id: "text-1",
- type: "text",
- name: "Title",
- content: "Hello",
- duration: 5,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- fontSize: 64,
- fontFamily: "Inter",
- color: "#ffffff",
- backgroundColor: "transparent",
- textAlign: "center",
- fontWeight: "bold",
- fontStyle: "normal",
- textDecoration: "none",
- transform: {
- scale: 1,
- position: { x: 0, y: 0 },
- rotate: 0,
- },
- opacity: 1,
- },
- ],
- },
- ],
- },
- ],
- };
-
- const result = transformProjectV3ToV4({
- project: projectWithLegacyTextWeight,
- });
-
- expect(result.skipped).toBe(false);
- expect(result.project.version).toBe(4);
-
- const migratedScene = (
- result.project.scenes as Array<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[0];
-
- expect(migratedElement.fontWeight).toBe("700");
- });
-
- test("does not mutate non-text tracks", () => {
- const projectWithoutTextTrack = {
- ...v3Project,
- scenes: [
- {
- ...v3Project.scenes[0],
- tracks: [
- {
- id: "track-sticker",
- type: "sticker",
- name: "Sticker Track",
- hidden: false,
- elements: [
- {
- id: "sticker-1",
- type: "sticker",
- name: "Flag",
- iconName: "mdi:home",
- duration: 5,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- transform: {
- scale: 1,
- position: { x: 0, y: 0 },
- rotate: 0,
- },
- opacity: 1,
- },
- ],
- },
- ],
- },
- ],
- };
-
- const result = transformProjectV3ToV4({ project: projectWithoutTextTrack });
- const migratedScene = (
- result.project.scenes as Array<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[0];
-
- expect(migratedElement.iconName).toBe("mdi:home");
- });
-
- test("skips projects that are already v4", () => {
- const result = transformProjectV3ToV4({
- project: { ...v3Project, version: 4 },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("already v4");
- });
-
- test("skips projects with no id", () => {
- const result = transformProjectV3ToV4({
- project: {
- version: 3,
- scenes: [],
- },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("no project id");
- });
-});
+import { describe, expect, test } from "bun:test";
+import { transformProjectV3ToV4 } from "../transformers/v3-to-v4";
+import { v3Project } from "./fixtures";
+
+describe("V3 to V4 Migration", () => {
+ test("normalizes legacy text fontWeight values", () => {
+ const projectWithLegacyTextWeight = {
+ ...v3Project,
+ scenes: [
+ {
+ ...v3Project.scenes[0],
+ tracks: [
+ {
+ id: "track-text",
+ type: "text",
+ name: "Text Track",
+ hidden: false,
+ elements: [
+ {
+ id: "text-1",
+ type: "text",
+ name: "Title",
+ content: "Hello",
+ duration: 5,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ fontSize: 64,
+ fontFamily: "Inter",
+ color: "#ffffff",
+ backgroundColor: "transparent",
+ textAlign: "center",
+ fontWeight: "bold",
+ fontStyle: "normal",
+ textDecoration: "none",
+ transform: {
+ scale: 1,
+ position: { x: 0, y: 0 },
+ rotate: 0,
+ },
+ opacity: 1,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ const result = transformProjectV3ToV4({
+ project: projectWithLegacyTextWeight,
+ });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(4);
+
+ const migratedScene = (
+ result.project.scenes as Array<Record<string, unknown>>
+ )[0];
+ const migratedTrack = (
+ migratedScene.tracks as Array<Record<string, unknown>>
+ )[0];
+ const migratedElement = (
+ migratedTrack.elements as Array<Record<string, unknown>>
+ )[0];
+
+ expect(migratedElement.fontWeight).toBe("700");
+ });
+
+ test("does not mutate non-text tracks", () => {
+ const projectWithoutTextTrack = {
+ ...v3Project,
+ scenes: [
+ {
+ ...v3Project.scenes[0],
+ tracks: [
+ {
+ id: "track-sticker",
+ type: "sticker",
+ name: "Sticker Track",
+ hidden: false,
+ elements: [
+ {
+ id: "sticker-1",
+ type: "sticker",
+ name: "Flag",
+ iconName: "mdi:home",
+ duration: 5,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: {
+ scale: 1,
+ position: { x: 0, y: 0 },
+ rotate: 0,
+ },
+ opacity: 1,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ const result = transformProjectV3ToV4({ project: projectWithoutTextTrack });
+ const migratedScene = (
+ result.project.scenes as Array<Record<string, unknown>>
+ )[0];
+ const migratedTrack = (
+ migratedScene.tracks as Array<Record<string, unknown>>
+ )[0];
+ const migratedElement = (
+ migratedTrack.elements as Array<Record<string, unknown>>
+ )[0];
+
+ expect(migratedElement.iconName).toBe("mdi:home");
+ });
+
+ test("skips projects that are already v4", () => {
+ const result = transformProjectV3ToV4({
+ project: { ...v3Project, version: 4 },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("already v4");
+ });
+
+ test("skips projects with no id", () => {
+ const result = transformProjectV3ToV4({
+ project: {
+ version: 3,
+ scenes: [],
+ },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("no project id");
+ });
+});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts b/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts
index ce0c3019..95386137 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v4-to-v5.test.ts
@@ -1,137 +1,137 @@
-import { describe, expect, test } from "bun:test";
-import { transformProjectV4ToV5 } from "../transformers/v4-to-v5";
-import { v3Project } from "./fixtures";
-
-describe("V4 to V5 Migration", () => {
- test("migrates sticker iconName to stickerId and removes legacy color", () => {
- const projectWithLegacySticker = {
- ...v3Project,
- version: 4,
- scenes: [
- {
- ...v3Project.scenes[0],
- tracks: [
- {
- id: "track-sticker",
- type: "sticker",
- name: "Sticker Track",
- hidden: false,
- elements: [
- {
- id: "sticker-1",
- type: "sticker",
- name: "Home",
- iconName: "mdi:home",
- color: "#ff0000",
- duration: 5,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- transform: {
- scale: 1,
- position: { x: 0, y: 0 },
- rotate: 0,
- },
- opacity: 1,
- },
- ],
- },
- ],
- },
- ],
- };
-
- const result = transformProjectV4ToV5({
- project: projectWithLegacySticker,
- });
-
- expect(result.skipped).toBe(false);
- expect(result.project.version).toBe(5);
-
- const migratedScene = (
- result.project.scenes as Array<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[0];
-
- expect(migratedElement.stickerId).toBe("icons:mdi:home");
- expect("iconName" in migratedElement).toBe(false);
- expect("color" in migratedElement).toBe(false);
- });
-
- test("keeps provider-prefixed stickerId values unchanged", () => {
- const projectWithStickerId = {
- ...v3Project,
- version: 4,
- scenes: [
- {
- ...v3Project.scenes[0],
- tracks: [
- {
- id: "track-sticker",
- type: "sticker",
- name: "Sticker Track",
- hidden: false,
- elements: [
- {
- id: "sticker-1",
- type: "sticker",
- name: "Flag",
- stickerId: "flags:AD",
- duration: 5,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- transform: {
- scale: 1,
- position: { x: 0, y: 0 },
- rotate: 0,
- },
- opacity: 1,
- },
- ],
- },
- ],
- },
- ],
- };
-
- const result = transformProjectV4ToV5({ project: projectWithStickerId });
- const migratedScene = (
- result.project.scenes as Array<Record<string, unknown>>
- )[0];
- const migratedTrack = (
- migratedScene.tracks as Array<Record<string, unknown>>
- )[0];
- const migratedElement = (
- migratedTrack.elements as Array<Record<string, unknown>>
- )[0];
-
- expect(migratedElement.stickerId).toBe("flags:AD");
- });
-
- test("skips projects that are already v5", () => {
- const result = transformProjectV4ToV5({
- project: { ...v3Project, version: 5 },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("already v5");
- });
-
- test("skips projects with no id", () => {
- const result = transformProjectV4ToV5({
- project: {
- version: 4,
- scenes: [],
- },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("no project id");
- });
-});
+import { describe, expect, test } from "bun:test";
+import { transformProjectV4ToV5 } from "../transformers/v4-to-v5";
+import { v3Project } from "./fixtures";
+
+describe("V4 to V5 Migration", () => {
+ test("migrates sticker iconName to stickerId and removes legacy color", () => {
+ const projectWithLegacySticker = {
+ ...v3Project,
+ version: 4,
+ scenes: [
+ {
+ ...v3Project.scenes[0],
+ tracks: [
+ {
+ id: "track-sticker",
+ type: "sticker",
+ name: "Sticker Track",
+ hidden: false,
+ elements: [
+ {
+ id: "sticker-1",
+ type: "sticker",
+ name: "Home",
+ iconName: "mdi:home",
+ color: "#ff0000",
+ duration: 5,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: {
+ scale: 1,
+ position: { x: 0, y: 0 },
+ rotate: 0,
+ },
+ opacity: 1,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ const result = transformProjectV4ToV5({
+ project: projectWithLegacySticker,
+ });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(5);
+
+ const migratedScene = (
+ result.project.scenes as Array<Record<string, unknown>>
+ )[0];
+ const migratedTrack = (
+ migratedScene.tracks as Array<Record<string, unknown>>
+ )[0];
+ const migratedElement = (
+ migratedTrack.elements as Array<Record<string, unknown>>
+ )[0];
+
+ expect(migratedElement.stickerId).toBe("icons:mdi:home");
+ expect("iconName" in migratedElement).toBe(false);
+ expect("color" in migratedElement).toBe(false);
+ });
+
+ test("keeps provider-prefixed stickerId values unchanged", () => {
+ const projectWithStickerId = {
+ ...v3Project,
+ version: 4,
+ scenes: [
+ {
+ ...v3Project.scenes[0],
+ tracks: [
+ {
+ id: "track-sticker",
+ type: "sticker",
+ name: "Sticker Track",
+ hidden: false,
+ elements: [
+ {
+ id: "sticker-1",
+ type: "sticker",
+ name: "Flag",
+ stickerId: "flags:AD",
+ duration: 5,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: {
+ scale: 1,
+ position: { x: 0, y: 0 },
+ rotate: 0,
+ },
+ opacity: 1,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ const result = transformProjectV4ToV5({ project: projectWithStickerId });
+ const migratedScene = (
+ result.project.scenes as Array<Record<string, unknown>>
+ )[0];
+ const migratedTrack = (
+ migratedScene.tracks as Array<Record<string, unknown>>
+ )[0];
+ const migratedElement = (
+ migratedTrack.elements as Array<Record<string, unknown>>
+ )[0];
+
+ expect(migratedElement.stickerId).toBe("flags:AD");
+ });
+
+ test("skips projects that are already v5", () => {
+ const result = transformProjectV4ToV5({
+ project: { ...v3Project, version: 5 },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("already v5");
+ });
+
+ test("skips projects with no id", () => {
+ const result = transformProjectV4ToV5({
+ project: {
+ version: 4,
+ scenes: [],
+ },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("no project id");
+ });
+});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts b/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts
index 6bbd5634..66e87713 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v5-to-v6.test.ts
@@ -1,92 +1,92 @@
-import { describe, expect, test } from "bun:test";
-import { transformProjectV5ToV6 } from "../transformers/v5-to-v6";
-import { v5Project } from "./fixtures";
-
-describe("V5 to V6 Migration", () => {
- test("converts number bookmarks to Bookmark objects", async () => {
- const result = transformProjectV5ToV6({
- project: v5Project as Parameters<
- typeof transformProjectV5ToV6
- >[0]["project"],
- });
-
- expect(result.skipped).toBe(false);
- expect(result.project.version).toBe(6);
-
- const mainScene = (
- result.project.scenes as Array<{ bookmarks: unknown[] }>
- )[0];
- expect(mainScene.bookmarks).toEqual([
- { time: 2.0 },
- { time: 5.5 },
- { time: 12.0 },
- ]);
-
- const introScene = (
- result.project.scenes as Array<{ bookmarks: unknown[] }>
- )[1];
- expect(introScene.bookmarks).toEqual([]);
- });
-
- test("skips projects that are already v6", () => {
- const result = transformProjectV5ToV6({
- project: {
- ...v5Project,
- version: 6,
- scenes: [
- {
- ...(v5Project as { scenes: unknown[] }).scenes[0],
- bookmarks: [{ time: 2 }, { time: 5 }],
- },
- ],
- } as Parameters<typeof transformProjectV5ToV6>[0]["project"],
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("already v6");
- });
-
- test("skips projects with no id", () => {
- const result = transformProjectV5ToV6({
- project: {
- version: 5,
- scenes: [],
- } as Parameters<typeof transformProjectV5ToV6>[0]["project"],
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("no project id");
- });
-
- test("preserves existing Bookmark objects with note, color, duration", () => {
- const projectWithRichBookmarks = {
- ...v5Project,
- version: 5,
- scenes: [
- {
- ...(v5Project as { scenes: Array<Record<string, unknown>> })
- .scenes[0],
- bookmarks: [
- { time: 1, note: "Intro", color: "#ef4444" },
- { time: 5.5, duration: 2 },
- ],
- },
- ],
- };
-
- const result = transformProjectV5ToV6({
- project: projectWithRichBookmarks as Parameters<
- typeof transformProjectV5ToV6
- >[0]["project"],
- });
-
- expect(result.skipped).toBe(false);
- const mainScene = (
- result.project.scenes as Array<{ bookmarks: unknown[] }>
- )[0];
- expect(mainScene.bookmarks).toEqual([
- { time: 1, note: "Intro", color: "#ef4444" },
- { time: 5.5, duration: 2 },
- ]);
- });
-});
+import { describe, expect, test } from "bun:test";
+import { transformProjectV5ToV6 } from "../transformers/v5-to-v6";
+import { v5Project } from "./fixtures";
+
+describe("V5 to V6 Migration", () => {
+ test("converts number bookmarks to Bookmark objects", async () => {
+ const result = transformProjectV5ToV6({
+ project: v5Project as Parameters<
+ typeof transformProjectV5ToV6
+ >[0]["project"],
+ });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(6);
+
+ const mainScene = (
+ result.project.scenes as Array<{ bookmarks: unknown[] }>
+ )[0];
+ expect(mainScene.bookmarks).toEqual([
+ { time: 2.0 },
+ { time: 5.5 },
+ { time: 12.0 },
+ ]);
+
+ const introScene = (
+ result.project.scenes as Array<{ bookmarks: unknown[] }>
+ )[1];
+ expect(introScene.bookmarks).toEqual([]);
+ });
+
+ test("skips projects that are already v6", () => {
+ const result = transformProjectV5ToV6({
+ project: {
+ ...v5Project,
+ version: 6,
+ scenes: [
+ {
+ ...(v5Project as { scenes: unknown[] }).scenes[0],
+ bookmarks: [{ time: 2 }, { time: 5 }],
+ },
+ ],
+ } as Parameters<typeof transformProjectV5ToV6>[0]["project"],
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("already v6");
+ });
+
+ test("skips projects with no id", () => {
+ const result = transformProjectV5ToV6({
+ project: {
+ version: 5,
+ scenes: [],
+ } as Parameters<typeof transformProjectV5ToV6>[0]["project"],
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("no project id");
+ });
+
+ test("preserves existing Bookmark objects with note, color, duration", () => {
+ const projectWithRichBookmarks = {
+ ...v5Project,
+ version: 5,
+ scenes: [
+ {
+ ...(v5Project as { scenes: Array<Record<string, unknown>> })
+ .scenes[0],
+ bookmarks: [
+ { time: 1, note: "Intro", color: "#ef4444" },
+ { time: 5.5, duration: 2 },
+ ],
+ },
+ ],
+ };
+
+ const result = transformProjectV5ToV6({
+ project: projectWithRichBookmarks as Parameters<
+ typeof transformProjectV5ToV6
+ >[0]["project"],
+ });
+
+ expect(result.skipped).toBe(false);
+ const mainScene = (
+ result.project.scenes as Array<{ bookmarks: unknown[] }>
+ )[0];
+ expect(mainScene.bookmarks).toEqual([
+ { time: 1, note: "Intro", color: "#ef4444" },
+ { time: 5.5, duration: 2 },
+ ]);
+ });
+});
diff --git a/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts b/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts
index 51e91f78..aa79a6ea 100644
--- a/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts
+++ b/apps/web/src/services/storage/migrations/__tests__/v8-to-v9.test.ts
@@ -1,175 +1,175 @@
-import { describe, expect, test } from "bun:test";
-import { transformProjectV8ToV9 } from "../transformers/v8-to-v9";
-
-const v8ProjectWithText = {
- id: "project-v8-text",
- version: 8,
- metadata: {
- id: "project-v8-text",
- name: "V8 Project with Text",
- createdAt: "2024-01-01T00:00:00.000Z",
- updatedAt: "2024-01-01T00:00:00.000Z",
- },
- settings: {
- fps: 30,
- canvasSize: { width: 1920, height: 1080 },
- background: { type: "color", color: "#000000" },
- },
- currentSceneId: "scene-main",
- scenes: [
- {
- id: "scene-main",
- name: "Main scene",
- isMain: true,
- tracks: [
- {
- id: "track-text",
- type: "text",
- name: "Text Track",
- hidden: false,
- elements: [
- {
- id: "el-1",
- type: "text",
- content: "With color",
- startTime: 0,
- duration: 5,
- background: {
- color: "#ff0000",
- cornerRadius: 0,
- paddingX: 8,
- paddingY: 4,
- },
- },
- {
- id: "el-2",
- type: "text",
- content: "Transparent",
- startTime: 5,
- duration: 5,
- background: {
- color: "transparent",
- paddingX: 30,
- paddingY: 42,
- },
- },
- ],
- },
- ],
- bookmarks: [],
- createdAt: "2024-01-01T00:00:00.000Z",
- updatedAt: "2024-01-01T00:00:00.000Z",
- },
- ],
-} as Parameters<typeof transformProjectV8ToV9>[0]["project"];
-
-describe("V8 to V9 Migration", () => {
- test("adds background.enabled from color (transparent => false, otherwise true)", () => {
- const result = transformProjectV8ToV9({ project: v8ProjectWithText });
-
- expect(result.skipped).toBe(false);
- expect(result.project.version).toBe(9);
-
- const track = (
- result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }>
- )[0].tracks[0];
- const elements = track.elements as Array<{ background: { enabled: boolean; color: string } }>;
-
- expect(elements[0].background.enabled).toBe(true);
- expect(elements[0].background.color).toBe("#ff0000");
-
- expect(elements[1].background.enabled).toBe(false);
- expect(elements[1].background.color).toBe("transparent");
- });
-
- test("preserves existing background.enabled if already present", () => {
- const projectWithEnabled = {
- ...v8ProjectWithText,
- scenes: [
- {
- ...(v8ProjectWithText.scenes as Record<string, unknown>[])[0],
- tracks: [
- {
- id: "track-text",
- type: "text",
- name: "Text Track",
- hidden: false,
- elements: [
- {
- id: "el-1",
- type: "text",
- content: "Already has enabled",
- startTime: 0,
- duration: 5,
- background: {
- enabled: false,
- color: "#00ff00",
- },
- },
- ],
- },
- ],
- },
- ],
- } as Parameters<typeof transformProjectV8ToV9>[0]["project"];
-
- const result = transformProjectV8ToV9({ project: projectWithEnabled });
-
- expect(result.skipped).toBe(false);
- const elements = (
- result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }>
- )[0].tracks[0].elements as Array<{ background: { enabled: boolean } }>;
- expect(elements[0].background.enabled).toBe(false);
- });
-
- test("skips non-text elements and tracks", () => {
- const projectWithVideoOnly = {
- ...v8ProjectWithText,
- scenes: [
- {
- id: "scene-main",
- name: "Main scene",
- isMain: true,
- tracks: [
- {
- id: "track-video",
- type: "video",
- name: "Video Track",
- isMain: true,
- elements: [],
- },
- ],
- bookmarks: [],
- createdAt: "2024-01-01T00:00:00.000Z",
- updatedAt: "2024-01-01T00:00:00.000Z",
- },
- ],
- } as Parameters<typeof transformProjectV8ToV9>[0]["project"];
-
- const result = transformProjectV8ToV9({ project: projectWithVideoOnly });
-
- expect(result.skipped).toBe(false);
- expect(result.project.version).toBe(9);
- });
-
- test("skips projects that are already v9", () => {
- const result = transformProjectV8ToV9({
- project: { ...v8ProjectWithText, version: 9 },
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("already v9");
- });
-
- test("skips projects with no id", () => {
- const result = transformProjectV8ToV9({
- project: {
- version: 8,
- scenes: [],
- } as Parameters<typeof transformProjectV8ToV9>[0]["project"],
- });
-
- expect(result.skipped).toBe(true);
- expect(result.reason).toBe("no project id");
- });
-});
+import { describe, expect, test } from "bun:test";
+import { transformProjectV8ToV9 } from "../transformers/v8-to-v9";
+
+const v8ProjectWithText = {
+ id: "project-v8-text",
+ version: 8,
+ metadata: {
+ id: "project-v8-text",
+ name: "V8 Project with Text",
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ },
+ settings: {
+ fps: 30,
+ canvasSize: { width: 1920, height: 1080 },
+ background: { type: "color", color: "#000000" },
+ },
+ currentSceneId: "scene-main",
+ scenes: [
+ {
+ id: "scene-main",
+ name: "Main scene",
+ isMain: true,
+ tracks: [
+ {
+ id: "track-text",
+ type: "text",
+ name: "Text Track",
+ hidden: false,
+ elements: [
+ {
+ id: "el-1",
+ type: "text",
+ content: "With color",
+ startTime: 0,
+ duration: 5,
+ background: {
+ color: "#ff0000",
+ cornerRadius: 0,
+ paddingX: 8,
+ paddingY: 4,
+ },
+ },
+ {
+ id: "el-2",
+ type: "text",
+ content: "Transparent",
+ startTime: 5,
+ duration: 5,
+ background: {
+ color: "transparent",
+ paddingX: 30,
+ paddingY: 42,
+ },
+ },
+ ],
+ },
+ ],
+ bookmarks: [],
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ },
+ ],
+} as Parameters<typeof transformProjectV8ToV9>[0]["project"];
+
+describe("V8 to V9 Migration", () => {
+ test("adds background.enabled from color (transparent => false, otherwise true)", () => {
+ const result = transformProjectV8ToV9({ project: v8ProjectWithText });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(9);
+
+ const track = (
+ result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }>
+ )[0].tracks[0];
+ const elements = track.elements as Array<{ background: { enabled: boolean; color: string } }>;
+
+ expect(elements[0].background.enabled).toBe(true);
+ expect(elements[0].background.color).toBe("#ff0000");
+
+ expect(elements[1].background.enabled).toBe(false);
+ expect(elements[1].background.color).toBe("transparent");
+ });
+
+ test("preserves existing background.enabled if already present", () => {
+ const projectWithEnabled = {
+ ...v8ProjectWithText,
+ scenes: [
+ {
+ ...(v8ProjectWithText.scenes as Record<string, unknown>[])[0],
+ tracks: [
+ {
+ id: "track-text",
+ type: "text",
+ name: "Text Track",
+ hidden: false,
+ elements: [
+ {
+ id: "el-1",
+ type: "text",
+ content: "Already has enabled",
+ startTime: 0,
+ duration: 5,
+ background: {
+ enabled: false,
+ color: "#00ff00",
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ } as Parameters<typeof transformProjectV8ToV9>[0]["project"];
+
+ const result = transformProjectV8ToV9({ project: projectWithEnabled });
+
+ expect(result.skipped).toBe(false);
+ const elements = (
+ result.project.scenes as Array<{ tracks: Array<{ elements: unknown[] }> }>
+ )[0].tracks[0].elements as Array<{ background: { enabled: boolean } }>;
+ expect(elements[0].background.enabled).toBe(false);
+ });
+
+ test("skips non-text elements and tracks", () => {
+ const projectWithVideoOnly = {
+ ...v8ProjectWithText,
+ scenes: [
+ {
+ id: "scene-main",
+ name: "Main scene",
+ isMain: true,
+ tracks: [
+ {
+ id: "track-video",
+ type: "video",
+ name: "Video Track",
+ isMain: true,
+ elements: [],
+ },
+ ],
+ bookmarks: [],
+ createdAt: "2024-01-01T00:00:00.000Z",
+ updatedAt: "2024-01-01T00:00:00.000Z",
+ },
+ ],
+ } as Parameters<typeof transformProjectV8ToV9>[0]["project"];
+
+ const result = transformProjectV8ToV9({ project: projectWithVideoOnly });
+
+ expect(result.skipped).toBe(false);
+ expect(result.project.version).toBe(9);
+ });
+
+ test("skips projects that are already v9", () => {
+ const result = transformProjectV8ToV9({
+ project: { ...v8ProjectWithText, version: 9 },
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("already v9");
+ });
+
+ test("skips projects with no id", () => {
+ const result = transformProjectV8ToV9({
+ project: {
+ version: 8,
+ scenes: [],
+ } as Parameters<typeof transformProjectV8ToV9>[0]["project"],
+ });
+
+ expect(result.skipped).toBe(true);
+ expect(result.reason).toBe("no project id");
+ });
+});