diff --git a/src/utils/aspectRatioUtils.test.ts b/src/utils/aspectRatioUtils.test.ts new file mode 100644 index 000000000..ab7162961 --- /dev/null +++ b/src/utils/aspectRatioUtils.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; +import { getNativeAspectRatioValue } from "./aspectRatioUtils"; + +const FALLBACK = 16 / 9; + +describe("getNativeAspectRatioValue", () => { + it("returns the natural video ratio for a standard HD frame", () => { + expect(getNativeAspectRatioValue(1920, 1080)).toBeCloseTo(16 / 9); + }); + + it("applies the crop region when provided", () => { + // Use non-proportional crop dimensions so the ratio actually changes; + // equal width/height would cancel out and silently pass even if the + // crop were ignored. + const crop = { x: 0, y: 0, width: 0.75, height: 0.5 }; + expect(getNativeAspectRatioValue(1920, 1080, crop)).toBeCloseTo((1920 * 0.75) / (1080 * 0.5)); + }); + + it("falls back to 16/9 when video metadata is not yet loaded (height = 0)", () => { + expect(getNativeAspectRatioValue(1920, 0)).toBe(FALLBACK); + }); + + it("falls back to 16/9 when both dimensions are zero", () => { + expect(getNativeAspectRatioValue(0, 0)).toBe(FALLBACK); + }); + + it("falls back to 16/9 when the crop height collapses to zero", () => { + const crop = { x: 0, y: 0, width: 0.5, height: 0 }; + expect(getNativeAspectRatioValue(1920, 1080, crop)).toBe(FALLBACK); + }); + + it("falls back to 16/9 when inputs are NaN", () => { + expect(getNativeAspectRatioValue(Number.NaN, 1080)).toBe(FALLBACK); + expect(getNativeAspectRatioValue(1920, Number.NaN)).toBe(FALLBACK); + }); + + it("falls back to 16/9 for non-positive dimensions", () => { + expect(getNativeAspectRatioValue(-1920, 1080)).toBe(FALLBACK); + expect(getNativeAspectRatioValue(1920, -1080)).toBe(FALLBACK); + }); + + it("never returns Infinity, NaN, or a non-positive ratio", () => { + const pathologicalInputs: Array< + [number, number, { x: number; y: number; width: number; height: number }?] + > = [ + [0, 0], + [1920, 0], + [0, 1080], + [Number.POSITIVE_INFINITY, 1080], + [1920, Number.POSITIVE_INFINITY], + [Number.NaN, Number.NaN], + // Same idea, but exercising the crop-region branch so a future + // regression there can't slip past the dimension-only cases above. + [1920, 1080, { x: 0, y: 0, width: 0.5, height: 0 }], + [1920, 1080, { x: 0, y: 0, width: 0, height: 0.5 }], + [1920, 1080, { x: 0, y: 0, width: Number.NaN, height: 0.5 }], + [1920, 1080, { x: 0, y: 0, width: 0.5, height: Number.NaN }], + [1920, 1080, { x: 0, y: 0, width: Number.POSITIVE_INFINITY, height: 0.5 }], + [1920, 1080, { x: 0, y: 0, width: 0.5, height: -1 }], + ]; + for (const [w, h, crop] of pathologicalInputs) { + const ratio = getNativeAspectRatioValue(w, h, crop); + expect(Number.isFinite(ratio)).toBe(true); + expect(ratio).toBeGreaterThan(0); + } + }); +}); diff --git a/src/utils/aspectRatioUtils.ts b/src/utils/aspectRatioUtils.ts index 887b543f7..dc0e08bbe 100644 --- a/src/utils/aspectRatioUtils.ts +++ b/src/utils/aspectRatioUtils.ts @@ -11,6 +11,8 @@ export const ASPECT_RATIOS = [ export type AspectRatio = (typeof ASPECT_RATIOS)[number]; +const NATIVE_ASPECT_FALLBACK = 16 / 9; + /** * Returns the numeric value of an aspect ratio. * For "native", returns a fallback ratio of 16/9. @@ -33,7 +35,7 @@ export function getAspectRatioValue(aspectRatio: AspectRatio): number { case "10:16": return 10 / 16; case "native": - return 16 / 9; + return NATIVE_ASPECT_FALLBACK; default: { const _exhaustiveCheck: never = aspectRatio; return _exhaustiveCheck; @@ -48,7 +50,13 @@ export function getNativeAspectRatioValue( ): number { const cropW = cropRegion?.width ?? 1; const cropH = cropRegion?.height ?? 1; - return (videoWidth * cropW) / (videoHeight * cropH); + const numerator = videoWidth * cropW; + const denominator = videoHeight * cropH; + if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) { + return NATIVE_ASPECT_FALLBACK; + } + const ratio = numerator / denominator; + return ratio > 0 && Number.isFinite(ratio) ? ratio : NATIVE_ASPECT_FALLBACK; } export function getAspectRatioDimensions( @@ -72,6 +80,6 @@ export function isPortraitAspectRatio(aspectRatio: AspectRatio): boolean { } export function formatAspectRatioForCSS(aspectRatio: AspectRatio, nativeRatio?: number): string { - if (aspectRatio === "native") return String(nativeRatio ?? 16 / 9); + if (aspectRatio === "native") return String(nativeRatio ?? NATIVE_ASPECT_FALLBACK); return aspectRatio.replace(":", "/"); }