summaryrefslogtreecommitdiff
path: root/apps/web/src/test-utils/mock-opencut-wasm.ts
blob: 782fac022d54a52fbaf61a4d3257ad1857a143c7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
 * Test mock for the `opencut-wasm` package.
 *
 * The published package is wasm-pack bundler-target glue that imports
 * `opencut_wasm_bg.wasm` as an ESM module with instance exports. Bun (1.3.x)
 * does not instantiate `.wasm` imports that way, so any test transitively
 * importing `@/wasm` fails at module evaluation. This file is registered as a
 * bun test preload (root `bunfig.toml` → `[test] preload`) so a faithful
 * pure-TS stand-in for the `opencut-wasm` functions used by
 * `@/wasm/media-time.ts` is in place before any test module evaluates.
 *
 * Semantics mirror rust/crates/time/src/media_time.rs (TICKS_PER_SECOND =
 * 120_000, i64 tick lattice, round-half-away-from-zero).
 */
import { mock } from "bun:test";

const TICKS_PER_SECOND = 120_000;

function ticksPerFrame({
	rate,
}: {
	rate: { numerator: number; denominator: number };
}): number | null {
	if (rate.numerator <= 0 || rate.denominator <= 0) {
		return null;
	}
	return Math.trunc((TICKS_PER_SECOND * rate.denominator) / rate.numerator);
}

function toFrameRound({
	time,
	rate,
}: {
	time: number;
	rate: { numerator: number; denominator: number };
}): number | null {
	const perFrame = ticksPerFrame({ rate });
	if (perFrame === null || perFrame === 0) {
		return null;
	}
	const remainder = ((time % perFrame) + perFrame) % perFrame;
	const floor = Math.floor(time / perFrame);
	return remainder * 2 >= perFrame ? floor + 1 : floor;
}

function fromFrame({
	frame,
	rate,
}: {
	frame: number;
	rate: { numerator: number; denominator: number };
}): number | null {
	const perFrame = ticksPerFrame({ rate });
	if (perFrame === null) {
		return null;
	}
	return frame * perFrame;
}

function floorToFrame({
	time,
	rate,
}: {
	time: number;
	rate: { numerator: number; denominator: number };
}): number | null {
	const perFrame = ticksPerFrame({ rate });
	if (perFrame === null || perFrame === 0) {
		return null;
	}
	return Math.floor(time / perFrame) * perFrame;
}

mock.module("opencut-wasm", () => ({
	TICKS_PER_SECOND: () => TICKS_PER_SECOND,
	mediaTimeFromSeconds: ({ seconds }: { seconds: number }): number | null => {
		if (!Number.isFinite(seconds)) {
			return null;
		}
		// Rust f64::round is half away from zero; Math.round is half up.
		const ticks = seconds * TICKS_PER_SECOND;
		return Math.sign(ticks) * Math.round(Math.abs(ticks));
	},
	mediaTimeToSeconds: ({ time }: { time: number }): number =>
		time / TICKS_PER_SECOND,
	roundToFrame: (options: {
		time: number;
		rate: { numerator: number; denominator: number };
	}): number | null => {
		const frame = toFrameRound(options);
		return frame === null ? null : fromFrame({ frame, rate: options.rate });
	},
	lastFrameTime: (options: {
		duration: number;
		rate: { numerator: number; denominator: number };
	}): number | null => {
		if (options.duration <= 0) {
			return 0;
		}
		return floorToFrame({ time: options.duration - 1, rate: options.rate });
	},
	snappedSeekTime: (options: {
		time: number;
		duration: number;
		rate: { numerator: number; denominator: number };
	}): number | null => {
		const frame = toFrameRound({ time: options.time, rate: options.rate });
		if (frame === null) {
			return null;
		}
		const snapped = fromFrame({ frame, rate: options.rate });
		if (snapped === null) {
			return null;
		}
		return Math.min(Math.max(snapped, 0), options.duration);
	},
	// Timecode parsing is not exercised by command tests; fail loudly rather
	// than silently returning a plausible-but-wrong value.
	parseTimecode: (): null => {
		throw new Error("parseTimecode is not implemented in the test mock");
	},
	// GPU/compositor functions are statically linked through `@/core` →
	// renderer-manager but never called by command tests.
	formatTimecode: (): string => {
		throw new Error("formatTimecode is not implemented in the test mock");
	},
	initializeGpu: (): Promise<void> => {
		throw new Error("initializeGpu is not implemented in the test mock");
	},
	applyEffectPasses: (): never => {
		throw new Error("applyEffectPasses is not implemented in the test mock");
	},
	applyMaskFeather: (): never => {
		throw new Error("applyMaskFeather is not implemented in the test mock");
	},
	initCompositor: (): never => {
		throw new Error("initCompositor is not implemented in the test mock");
	},
	getCompositorCanvas: (): never => {
		throw new Error(
			"getCompositorCanvas is not implemented in the test mock",
		);
	},
	getLastFrameProfile: (): never => {
		throw new Error(
			"getLastFrameProfile is not implemented in the test mock",
		);
	},
	releaseTexture: (): never => {
		throw new Error("releaseTexture is not implemented in the test mock");
	},
	renderFrame: (): never => {
		throw new Error("renderFrame is not implemented in the test mock");
	},
	resizeCompositor: (): never => {
		throw new Error("resizeCompositor is not implemented in the test mock");
	},
	uploadTexture: (): never => {
		throw new Error("uploadTexture is not implemented in the test mock");
	},
}));