diff --git a/electron/ai-edition/document-service.test.ts b/electron/ai-edition/document-service.test.ts index 1a2915751..2d2ecd705 100644 --- a/electron/ai-edition/document-service.test.ts +++ b/electron/ai-edition/document-service.test.ts @@ -24,9 +24,9 @@ describe("DocumentService", () => { }); describe("createProject", () => { - it("creates a v5 doc with the given title and writes it to disk", async () => { + it("creates a v6 doc with the given title and writes it to disk", async () => { const doc = await service.createProject("Demo Project"); - expect(doc.schemaVersion).toBe(5); + expect(doc.schemaVersion).toBe(6); expect(doc.project.title).toBe("Demo Project"); expect(doc.project.id).toMatch(/^proj_/); expect(doc.assets).toEqual([]); @@ -34,7 +34,7 @@ describe("DocumentService", () => { const filePath = path.join(tempDir, `${doc.project.id}.openscreen`); const raw = await fs.readFile(filePath, "utf8"); expect(JSON.parse(raw)).toMatchObject({ - schemaVersion: 5, + schemaVersion: 6, project: { title: "Demo Project" }, }); }); diff --git a/src/components/ai-edition/WebcamOverlay.test.tsx b/src/components/ai-edition/WebcamOverlay.test.tsx index 08e573f03..64386fab7 100644 --- a/src/components/ai-edition/WebcamOverlay.test.tsx +++ b/src/components/ai-edition/WebcamOverlay.test.tsx @@ -36,7 +36,7 @@ const CLIP_WITHOUT_CAMERA: AxcutClip = { function makeDocument(): AxcutDocument { return { - schemaVersion: 5, + schemaVersion: 6, project: { id: "proj_test", title: "Test", diff --git a/src/components/ai-edition/v4/V4Timeline.tsx b/src/components/ai-edition/v4/V4Timeline.tsx index d75da33a9..1cbab2988 100644 --- a/src/components/ai-edition/v4/V4Timeline.tsx +++ b/src/components/ai-edition/v4/V4Timeline.tsx @@ -30,7 +30,7 @@ import { ZOOM_DEPTH_SCALES } from "@/components/video-editor/types"; import { useScopedT } from "@/contexts/I18nContext"; import { useAudioPeaks } from "@/hooks/useAudioPeaks"; import { createId } from "@/lib/ai-edition/document/ids"; -import { collectNativeFormats, referenceClipDims } from "@/lib/ai-edition/document/outputFormat"; +import { collectNativeFormats } from "@/lib/ai-edition/document/outputFormat"; import type { AxcutClip } from "@/lib/ai-edition/schema"; import { useProjectStore } from "@/lib/ai-edition/store/projectStore"; import { useChatPromptBus } from "@/lib/ai-edition/store/useChatPromptBus"; @@ -46,11 +46,7 @@ import { } from "@/lib/ai-edition/timeline/trim-mapping"; import { buildAutoZoomSuggestions } from "@/lib/ai-edition/timeline/zoom-suggestions"; import { nativeBridgeClient } from "@/native/client"; -import { - ASPECT_RATIO_PRESETS, - getAspectRatioLabel, - toAspectRatioToken, -} from "@/utils/aspectRatioUtils"; +import { ASPECT_RATIO_PRESETS, getAspectRatioLabel } from "@/utils/aspectRatioUtils"; import { TransportBar } from "../TransportBar"; import type { VideoSource } from "../VirtualPreview"; import styles from "./EditorShellV4.module.css"; @@ -271,16 +267,6 @@ export function V4Timeline({ // stays a pure list of fixed choices, and "which shapes are my clips" lives solely in ORIGINAL // (no more per-preset badge, which split that one answer across two places). const timelineIsMixed = nativeFormats.length > 1; - // A project saved before the shapes were enumerated still stores "native". Resolve it to the - // shape it currently means so the menu highlights a real row (and the button names a real - // ratio) instead of showing a selection that matches nothing. Picking that row rewrites the - // document to the concrete token — which is how those projects self-migrate off the value - // that silently moved with the clip list. - const activeToken = useMemo(() => { - if (settings.aspectRatio !== "native" || !document) return settings.aspectRatio; - const reference = referenceClipDims(document); - return toAspectRatioToken(reference.width, reference.height) ?? settings.aspectRatio; - }, [settings.aspectRatio, document]); const [aspectMenuOpen, setAspectMenuOpen] = useState(false); const [autoEnhanceOpen, setAutoEnhanceOpen] = useState(false); @@ -1115,7 +1101,7 @@ export function V4Timeline({ title={t("toolbar.aspectRatio")} aria-label={t("toolbar.aspectRatio")} > - {getAspectRatioLabel(activeToken)} + {getAspectRatioLabel(settings.aspectRatio)} @@ -1134,7 +1120,7 @@ export function V4Timeline({ type="button" key={ratio} className={`${styles.recMenuRow}${ - ratio === activeToken ? ` ${styles.active}` : "" + ratio === settings.aspectRatio ? ` ${styles.active}` : "" }`} onClick={() => { void setSettings({ aspectRatio: ratio }); @@ -1152,7 +1138,7 @@ export function V4Timeline({ type="button" key={format.token} className={`${styles.recMenuRow}${ - format.token === activeToken ? ` ${styles.active}` : "" + format.token === settings.aspectRatio ? ` ${styles.active}` : "" }`} onClick={() => { void setSettings({ aspectRatio: format.token }); diff --git a/src/lib/ai-edition/document/migrate.test.ts b/src/lib/ai-edition/document/migrate.test.ts index 2b801de15..9b9b0bfcd 100644 --- a/src/lib/ai-edition/document/migrate.test.ts +++ b/src/lib/ai-edition/document/migrate.test.ts @@ -43,7 +43,7 @@ describe("migrateProjectDataToAxcutDocument", () => { it("produces a v3 document with one asset and one clip from a v2 single-recording project", () => { const doc = migrateProjectDataToAxcutDocument(makeV2Project()); - expect(doc.schemaVersion).toBe(5); + expect(doc.schemaVersion).toBe(6); expect(doc.assets).toHaveLength(1); const asset = doc.assets[0]; expect(asset.kind).toBe("video"); diff --git a/src/lib/ai-edition/schema/index.test.ts b/src/lib/ai-edition/schema/index.test.ts index 4b416a8b2..9b3767c6e 100644 --- a/src/lib/ai-edition/schema/index.test.ts +++ b/src/lib/ai-edition/schema/index.test.ts @@ -14,9 +14,9 @@ import { zoomRegionSchema, } from "./index"; -describe("axcut-schema v5", () => { - it("uses schema version 4", () => { - expect(axcutSchemaVersion).toBe(5); +describe("axcut-schema v6", () => { + it("uses schema version 6", () => { + expect(axcutSchemaVersion).toBe(6); }); it("rejects unknown schema versions", () => { @@ -28,9 +28,9 @@ describe("axcut-schema v5", () => { ).toThrow(); }); - it("createEmptyDocument returns a valid v5 doc with empty collections", () => { + it("createEmptyDocument returns a valid v6 doc with empty collections", () => { const doc = createEmptyDocument({ projectId: "proj_1", title: "Demo" }); - expect(doc.schemaVersion).toBe(5); + expect(doc.schemaVersion).toBe(6); expect(doc.assets).toEqual([]); expect(doc.timeline.clips).toEqual([]); expect(doc.timeline.trimRanges).toEqual([]); @@ -255,7 +255,7 @@ describe("axcut-schema v5", () => { const doc = documentSchema.parse( v3Doc({ project: { ...v3Doc().project, primaryAssetId: "asset_2" } }), ); - expect(doc.schemaVersion).toBe(5); + expect(doc.schemaVersion).toBe(6); expect((doc as Record).cameraTrack).toBeUndefined(); expect(doc.assets.find((a) => a.id === "asset_1")?.cameraTrack).toBeNull(); expect(doc.assets.find((a) => a.id === "asset_2")?.cameraTrack?.sourcePath).toBe("/cam.mp4"); @@ -269,7 +269,7 @@ describe("axcut-schema v5", () => { it("is a no-op when the v3 document has no legacy cameraTrack", () => { const doc = documentSchema.parse(v3Doc({ cameraTrack: null })); - expect(doc.schemaVersion).toBe(5); + expect(doc.schemaVersion).toBe(6); for (const asset of doc.assets) { expect(asset.cameraTrack).toBeNull(); } @@ -407,7 +407,7 @@ describe("v4 -> v5 clip-anchored modifier migration", () => { ], }), ); - expect(doc.schemaVersion).toBe(5); + expect(doc.schemaVersion).toBe(6); expect(doc.zoomRanges).toHaveLength(1); const z = doc.zoomRanges[0]; expect(z).toMatchObject({ id: "z1", clipId: "clip_a", depth: 3 }); @@ -469,7 +469,7 @@ describe("v4 -> v5 clip-anchored modifier migration", () => { expect(doc.zoomRanges[0].clipId).toBeUndefined(); }); - it("is idempotent — re-parsing an already-v5 document changes nothing", () => { + it("is idempotent — re-parsing the migrated document changes nothing", () => { const once = documentSchema.parse( makeV4Doc({ zoomRanges: [ @@ -481,3 +481,274 @@ describe("v4 -> v5 clip-anchored modifier migration", () => { expect(twice).toEqual(once); }); }); + +// --- v5 -> v6 native AspectRatio migration ----------------------------------- +// `"native"` used to be a runtime-only sentinel that resolved to the timeline's largest +// clip. v6 makes that resolution permanent by baking the concrete `"W:H"` token into the +// document. After this upgrader runs, no document ever contains `"native"` again — the +// union arm in `AspectRatio` is dropped, and the runtime bridge in +// `lib/ai-edition/document/outputFormat` is no longer needed. + +describe("v5 -> v6 native AspectRatio migration", () => { + function makeV5Doc(overrides: Record = {}) { + const createdAt = "2024-01-01T00:00:00.000Z"; + return { + schemaVersion: 5, + project: { id: "p1", title: "v5-aspect", createdAt, updatedAt: createdAt }, + assets: [ + { id: "asset_f", kind: "video", label: "A", originalPath: "/a.mp4", cameraTrack: null }, + ], + timeline: { + clips: [ + { + id: "clip_a", + assetId: "asset_f", + sourceStartSec: 0, + sourceEndSec: 30, + timelineStartSec: 0, + timelineEndSec: 30, + origin: "user", + }, + ], + }, + ...overrides, + }; + } + + it("rewrites legacy aspectRatio === 'native' to the largest clip's concrete token", () => { + const doc = documentSchema.parse( + makeV5Doc({ + legacyEditor: { aspectRatio: "native" }, + assets: [ + { + id: "asset_f", + kind: "video", + label: "A", + originalPath: "/a.mp4", + cameraTrack: null, + video: { width: 1920, height: 1080 }, + }, + ], + }), + ); + expect(doc.schemaVersion).toBe(6); + expect((doc.legacyEditor as Record).aspectRatio).toBe("16:9"); + }); + + it("picks the largest clip when the timeline is mixed-shape", () => { + const doc = documentSchema.parse({ + schemaVersion: 5, + project: { + id: "p1", + title: "mixed", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + assets: [ + { + id: "asset_f", + kind: "video", + label: "A", + originalPath: "/a.mp4", + cameraTrack: null, + video: { width: 1920, height: 1080 }, + }, + { + id: "asset_g", + kind: "video", + label: "B", + originalPath: "/b.mp4", + cameraTrack: null, + video: { width: 2160, height: 3840 }, + }, + ], + timeline: { + clips: [ + { + id: "clip_a", + assetId: "asset_f", + sourceStartSec: 0, + sourceEndSec: 10, + timelineStartSec: 0, + timelineEndSec: 10, + origin: "user", + }, + { + id: "clip_b", + assetId: "asset_g", + sourceStartSec: 0, + sourceEndSec: 10, + timelineStartSec: 10, + timelineEndSec: 20, + origin: "user", + }, + ], + }, + legacyEditor: { aspectRatio: "native" }, + }); + expect(doc.schemaVersion).toBe(6); + expect((doc.legacyEditor as Record).aspectRatio).toBe("9:16"); + }); + + it("leaves 'native' alone when the timeline has no clips with known dimensions", () => { + // Deliberately NOT a 16:9 fallback: an empty/unprobed timeline gives no basis + // for a concrete token, and guessing one persists a wrong frame. See the v1.7 + // import case below. + const doc = documentSchema.parse(makeV5Doc({ legacyEditor: { aspectRatio: "native" } })); + expect(doc.schemaVersion).toBe(6); + expect((doc.legacyEditor as Record).aspectRatio).toBe("native"); + }); + + it("passes through a concrete aspectRatio unchanged", () => { + const doc = documentSchema.parse(makeV5Doc({ legacyEditor: { aspectRatio: "4:5" } })); + expect(doc.schemaVersion).toBe(6); + expect((doc.legacyEditor as Record).aspectRatio).toBe("4:5"); + }); + + it("passes through a legacyEditor without aspectRatio unchanged", () => { + const doc = documentSchema.parse(makeV5Doc({ legacyEditor: { someOtherField: "preserved" } })); + expect(doc.schemaVersion).toBe(6); + const legacy = doc.legacyEditor as Record; + expect(legacy.someOtherField).toBe("preserved"); + expect(legacy.aspectRatio).toBeUndefined(); + }); + + it("passes through a v5 doc with no legacyEditor at all (only the version bumps)", () => { + const v5 = makeV5Doc(); + const doc = documentSchema.parse(v5); + expect(doc.schemaVersion).toBe(6); + expect(doc.legacyEditor).toBeNull(); + }); + + it("is idempotent — re-parsing an already-v6 document changes nothing", () => { + const once = documentSchema.parse(makeV5Doc({ legacyEditor: { aspectRatio: "16:9" } })); + const twice = documentSchema.parse(once); + expect(twice).toEqual(once); + }); + + it("keeps 'native' when the source dimensions are not known yet (v1.7 import)", () => { + // The v1.7 -> v1.8 path: `{version:2, media, editor}` carries only file paths, + // so `migrateProjectDataToAxcutDocument` produces assets with no `video` block. + // Baking here would stamp a hardcoded 16:9 and persist it — permanently + // reframing every portrait v1.7 project saved with "Native". Leave the + // sentinel; it resolves dynamically at runtime and converts on a later load, + // once useTimeline's probe has written `asset.video` back. + const doc = documentSchema.parse({ + schemaVersion: 5, + project: { + id: "p1", + title: "from v1.7", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + assets: [ + { + id: "asset_u", + kind: "video", + label: "A", + originalPath: "/a.mp4", + cameraTrack: null, + // no `video` — exactly what the v2 import produces + }, + ], + timeline: { + clips: [ + { + id: "clip_a", + assetId: "asset_u", + sourceStartSec: 0, + sourceEndSec: 10, + timelineStartSec: 0, + timelineEndSec: 10, + origin: "user", + }, + ], + }, + legacyEditor: { aspectRatio: "native" }, + }); + expect(doc.schemaVersion).toBe(6); + expect((doc.legacyEditor as Record).aspectRatio).toBe("native"); + }); + + it("converts 'native' once the probe has persisted dimensions", () => { + // Second load of the same project, after useTimeline probed a PORTRAIT source. + // This is the case that must not become 16:9. + const doc = documentSchema.parse({ + schemaVersion: 5, + project: { + id: "p1", + title: "from v1.7, probed", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + assets: [ + { + id: "asset_u", + kind: "video", + label: "A", + originalPath: "/a.mp4", + cameraTrack: null, + video: { width: 1080, height: 1920 }, + }, + ], + timeline: { + clips: [ + { + id: "clip_a", + assetId: "asset_u", + sourceStartSec: 0, + sourceEndSec: 10, + timelineStartSec: 0, + timelineEndSec: 10, + origin: "user", + }, + ], + }, + legacyEditor: { aspectRatio: "native" }, + }); + expect(doc.schemaVersion).toBe(6); + expect((doc.legacyEditor as Record).aspectRatio).toBe("9:16"); + }); + + it("bakes the CROPPED dimensions, not the raw ones", () => { + // "native" resolved to the cropped clip at runtime. A 3840x2160 asset cropped + // to its left half is effectively 1920x2160 → 8:9. Reading the raw dims would + // wrongly yield 16:9 and silently reframe the project. + const doc = documentSchema.parse({ + schemaVersion: 5, + project: { + id: "p1", + title: "cropped", + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, + assets: [ + { + id: "asset_c", + kind: "video", + label: "A", + originalPath: "/a.mp4", + cameraTrack: null, + video: { width: 3840, height: 2160 }, + }, + ], + timeline: { + clips: [ + { + id: "clip_a", + assetId: "asset_c", + sourceStartSec: 0, + sourceEndSec: 10, + timelineStartSec: 0, + timelineEndSec: 10, + origin: "user", + cropRegion: { x: 0, y: 0, width: 0.5, height: 1 }, + }, + ], + }, + legacyEditor: { aspectRatio: "native" }, + }); + expect(doc.schemaVersion).toBe(6); + expect((doc.legacyEditor as Record).aspectRatio).toBe("8:9"); + }); +}); diff --git a/src/lib/ai-edition/schema/index.ts b/src/lib/ai-edition/schema/index.ts index 990b32557..a10b718ab 100644 --- a/src/lib/ai-edition/schema/index.ts +++ b/src/lib/ai-edition/schema/index.ts @@ -11,6 +11,12 @@ // shape — runtime ops, IPC, and exporter integration land in Phase 1+. import { z } from "zod"; +// Cycle-safe: `aspectRatioUtils` has no schema dependency, and `document/outputFormat` +// is downstream of schema. We import the leaf tokeniser directly so the v5→v6 upgrader +// can rewrite `"native"` without dragging in the whole output-format module. +// Relative, not `@/`: the Electron main bundle imports this module and +// vite-plugin-electron builds it without the root resolve.alias. +import { toAspectRatioToken } from "../../../utils/aspectRatioUtils"; // Cycle-safe: `document/ids` only pulls `uuid`, and `timeline/timelineMap`'s // transitive value-imports (region-ventilation, virtual-preview) import from this // module TYPE-ONLY, so requiring them here never re-enters schema at runtime. @@ -21,7 +27,11 @@ import { anchorRegionsWithDerivedMs } from "../timeline/timelineMap"; // CLIP-ANCHORED fragments: `{clipId, sourceStartSec, sourceEndSec}` is the // source of truth, `startMs`/`endMs` stay as a derived cache for the // transition. See technical-documentation/architecture/timeline-model.md -export const axcutSchemaVersion = 5; +// 5. v6 — `"native"` AspectRatio retires. The v5→v6 upgrader rewrites every +// stored `"native"` to a concrete `"W:H"` token (the timeline's largest +// clip, falling back to "16:9"), so the value can be dropped from the +// `AspectRatio` union without a runtime bridge. +export const axcutSchemaVersion = 6; export const isoDateSchema = z.string().datetime({ offset: true }); @@ -520,8 +530,113 @@ function upgradeV4DocumentToV5(raw: unknown): unknown { }; } +/** + * Largest raw asset footprint among the timeline's clips. A local copy of the + * `referenceClipDims` pick from `document/outputFormat` — duplicated here so the schema + * package doesn't import from a module that itself imports from this one (cycle), and so + * the upgrader doesn't need the runtime `probedAssetDims` map (it runs at load time, before + * any asset is probed). Crop IS applied: `"native"` resolved to the *cropped* clip + * dimensions at runtime (`clipEffectiveDims` → `calculateEffectiveSourceDimensions`), + * so ignoring it here would silently reframe every cropped project. + */ +/** Mirrors `atLeastEven` in mp4ExportSettings — H.264 4:2:0 rejects odd dims. */ +function atLeastEven(value: number): number { + return Math.max(2, Math.floor(value / 2) * 2); +} + +function largestClipDims(doc: Record): { width: number; height: number } | null { + const timeline = (doc.timeline ?? {}) as Record; + const clips = Array.isArray(timeline.clips) ? timeline.clips : []; + const assets = Array.isArray(doc.assets) ? doc.assets : []; + const assetById = new Map>(); + for (const a of assets) { + if (a && typeof a === "object" && typeof (a as { id?: unknown }).id === "string") { + assetById.set((a as { id: string }).id, a as Record); + } + } + let best: { width: number; height: number } | null = null; + let bestArea = 0; + for (const clip of clips) { + if (!clip || typeof clip !== "object") continue; + const c = clip as Record; + const assetId = c.assetId; + if (typeof assetId !== "string") continue; + const asset = assetById.get(assetId); + if (!asset) continue; + const video = (asset.video ?? {}) as Record; + const rawW = typeof video.width === "number" ? video.width : 0; + const rawH = typeof video.height === "number" ? video.height : 0; + if (rawW <= 0 || rawH <= 0) continue; + // Apply the clip's crop, exactly as `calculateEffectiveSourceDimensions` + // (mp4ExportSettings) does at runtime — including the snap to even pixels, + // so the reduced token matches the size the encoder is actually handed. + // "native" meant "the cropped source's shape"; ignoring crop here would + // bake the wrong ratio into every cropped project. + const crop = (c.cropRegion ?? {}) as Record; + const cropW = typeof crop.width === "number" && crop.width > 0 ? crop.width : 1; + const cropH = typeof crop.height === "number" && crop.height > 0 ? crop.height : 1; + const w = atLeastEven(Math.round(rawW * cropW)); + const h = atLeastEven(Math.round(rawH * cropH)); + const area = w * h; + if (area > bestArea) { + bestArea = area; + best = { width: w, height: h }; + } + } + return best; +} + +/** + * v5 → v6 — retire the `"native"` AspectRatio. `"native"` was a runtime-only sentinel + * that resolved to the timeline's largest clip; v6 makes that resolution permanent by + * baking the concrete `"W:H"` token into the document. The new value matches the OLD + * runtime answer (largest clip), so projects migrate losslessly — preview and export + * will keep framing the same shape they did yesterday. + * + * Falls back to `"16:9"` when the timeline has no clips with known dimensions (the same + * value the runtime bridge in `resolveAspectRatioValue` returned when no document was + * available). A document without a `legacyEditor` envelope is unchanged apart from the + * version bump. + */ +function upgradeV5DocumentToV6(raw: unknown): unknown { + if (!raw || typeof raw !== "object") return raw; + const doc = raw as Record; + if (doc.schemaVersion !== 5) return raw; + + const legacy = + doc.legacyEditor && typeof doc.legacyEditor === "object" && !Array.isArray(doc.legacyEditor) + ? (doc.legacyEditor as Record) + : null; + + if (!legacy || legacy.aspectRatio !== "native") { + return { ...doc, schemaVersion: 6 }; + } + + // OPPORTUNISTIC, never forced. Baking requires real source dimensions, and a + // project imported from v1.7 has none yet: `migrateProjectDataToAxcutDocument` + // builds its asset from `{version:2, media, editor}`, which carries only file + // paths — dimensions arrive later, when `useTimeline`'s probe effect writes + // `asset.video` back to disk. + // + // So when dimensions are unknown we leave `"native"` in place rather than + // stamping a hardcoded ratio. `"native"` keeps resolving dynamically at runtime + // (`resolveAspectRatioValue`), which IS the v1.7 behaviour, and the next load + // after the probe has persisted dimensions converts it for real. Forcing + // `"16:9"` here would permanently reframe every portrait v1.7 project that used + // "Native", on its first open in v1.8, with no way back. + const dims = largestClipDims(doc); + if (!dims) { + return { ...doc, schemaVersion: 6 }; + } + return { + ...doc, + schemaVersion: 6, + legacyEditor: { ...legacy, aspectRatio: toAspectRatioToken(dims.width, dims.height) }, + }; +} + export const documentSchema = z.preprocess( - (raw) => upgradeV4DocumentToV5(upgradeV3DocumentToV4(raw)), + (raw) => upgradeV5DocumentToV6(upgradeV4DocumentToV5(upgradeV3DocumentToV4(raw))), documentSchemaShape, ); diff --git a/src/native/sceneDescription.test.ts b/src/native/sceneDescription.test.ts index 7262356e0..7fbb49129 100644 --- a/src/native/sceneDescription.test.ts +++ b/src/native/sceneDescription.test.ts @@ -64,7 +64,7 @@ function makeDoc( const createdAt = "2024-01-01T00:00:00.000Z"; const baseProject = { id: "p1", title: "Test", createdAt, updatedAt: createdAt }; return { - schemaVersion: 5, + schemaVersion: 6, project: { ...baseProject, ...(overrides.project ?? {}), diff --git a/src/utils/aspectRatioUtils.test.ts b/src/utils/aspectRatioUtils.test.ts index 06d54a664..e1d60181d 100644 --- a/src/utils/aspectRatioUtils.test.ts +++ b/src/utils/aspectRatioUtils.test.ts @@ -15,8 +15,7 @@ describe("parseAspectRatio", () => { expect(parseAspectRatio(" 64 : 27 ")).toEqual({ width: 64, height: 27 }); }); - it('rejects the legacy "native" sentinel and malformed input', () => { - expect(parseAspectRatio("native")).toBeNull(); + it("rejects malformed input", () => { expect(parseAspectRatio("16/9")).toBeNull(); expect(parseAspectRatio("16:")).toBeNull(); expect(parseAspectRatio("0:9")).toBeNull(); @@ -26,10 +25,9 @@ describe("parseAspectRatio", () => { }); describe("isAspectRatio", () => { - it("accepts presets, free-form shapes and the legacy sentinel", () => { + it("accepts presets and free-form shapes", () => { expect(isAspectRatio("16:9")).toBe(true); expect(isAspectRatio("64:27")).toBe(true); - expect(isAspectRatio("native")).toBe(true); }); it("rejects anything a project file could hold that isn't a ratio", () => { @@ -62,10 +60,6 @@ describe("getAspectRatioValue", () => { expect(getAspectRatioValue("9:16")).toBeCloseTo(9 / 16, 6); expect(getAspectRatioValue("64:27")).toBeCloseTo(64 / 27, 6); }); - - it("falls back to 16/9 for the legacy sentinel, which has no document context here", () => { - expect(getAspectRatioValue("native")).toBeCloseTo(FALLBACK_RATIO, 6); - }); }); describe("getNativeAspectRatioValue", () => {