mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
Merge pull request #624 from webadderallorg/fix/main-baseline-test-drift
test: restore main baseline verification
This commit is contained in:
@@ -87,8 +87,8 @@ describe("editorPreferences", () => {
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.exportQuality).toBe("source");
|
||||
});
|
||||
|
||||
it("defaults cursor preferences to macOS at 2.5x with gentler sway", () => {
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.cursorStyle).toBe("macos");
|
||||
it("defaults cursor preferences to Tahoe at 2.5x with gentler sway", () => {
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.cursorStyle).toBe("tahoe");
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.cursorSize).toBe(2.5);
|
||||
expect(DEFAULT_EDITOR_PREFERENCES.cursorSway).toBe(0.4);
|
||||
});
|
||||
@@ -126,7 +126,9 @@ describe("editorPreferences", () => {
|
||||
|
||||
const loaded = loadEditorPreferences();
|
||||
|
||||
expect(loaded.zoomMotionBlurTuning).toEqual(DEFAULT_EDITOR_PREFERENCES.zoomMotionBlurTuning);
|
||||
expect(loaded.zoomMotionBlurTuning).toEqual(
|
||||
DEFAULT_EDITOR_PREFERENCES.zoomMotionBlurTuning,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not save dev-only split blur tuning overrides to editor preferences", () => {
|
||||
@@ -401,6 +403,38 @@ describe("editorPreferences", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not let localStorage failures mask Electron preset persistence", () => {
|
||||
const settingsStore = stubElectronSettings();
|
||||
const localStorage = createStorageMock();
|
||||
localStorage.setItem = () => {
|
||||
throw new Error("localStorage unavailable");
|
||||
};
|
||||
vi.stubGlobal("localStorage", localStorage);
|
||||
|
||||
expect(
|
||||
saveEditorPresets([
|
||||
{
|
||||
id: "preset-1",
|
||||
name: "Demo Preset",
|
||||
createdAt: "2026-05-01T00:00:00.000Z",
|
||||
updatedAt: "2026-05-02T00:00:00.000Z",
|
||||
snapshot: {
|
||||
...DEFAULT_EDITOR_PREFERENCES,
|
||||
cropRegion: DEFAULT_CROP_REGION,
|
||||
autoCaptionSettings: DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
},
|
||||
},
|
||||
]),
|
||||
).toBe(true);
|
||||
|
||||
expect(settingsStore.get(EDITOR_PRESETS_STORAGE_KEY)).toMatchObject([
|
||||
{
|
||||
id: "preset-1",
|
||||
name: "Demo Preset",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves crop region in editor preset snapshots", () => {
|
||||
const localStorage = createStorageMock();
|
||||
vi.stubGlobal("localStorage", localStorage);
|
||||
|
||||
@@ -285,14 +285,12 @@ function normalizeEditorControls(
|
||||
zoomInDurationMs: sanitizedRaw.zoomInDurationMs ?? fallback.zoomInDurationMs,
|
||||
zoomInOverlapMs: sanitizedRaw.zoomInOverlapMs ?? fallback.zoomInOverlapMs,
|
||||
zoomOutDurationMs: sanitizedRaw.zoomOutDurationMs ?? fallback.zoomOutDurationMs,
|
||||
connectedZoomGapMs:
|
||||
sanitizedRaw.connectedZoomGapMs ?? fallback.connectedZoomGapMs,
|
||||
connectedZoomGapMs: sanitizedRaw.connectedZoomGapMs ?? fallback.connectedZoomGapMs,
|
||||
connectedZoomDurationMs:
|
||||
sanitizedRaw.connectedZoomDurationMs ?? fallback.connectedZoomDurationMs,
|
||||
zoomInEasing: sanitizedRaw.zoomInEasing ?? fallback.zoomInEasing,
|
||||
zoomOutEasing: sanitizedRaw.zoomOutEasing ?? fallback.zoomOutEasing,
|
||||
connectedZoomEasing:
|
||||
sanitizedRaw.connectedZoomEasing ?? fallback.connectedZoomEasing,
|
||||
connectedZoomEasing: sanitizedRaw.connectedZoomEasing ?? fallback.connectedZoomEasing,
|
||||
showCursor: sanitizedRaw.showCursor ?? fallback.showCursor,
|
||||
loopCursor: sanitizedRaw.loopCursor ?? fallback.loopCursor,
|
||||
cursorStyle: sanitizedRaw.cursorStyle ?? fallback.cursorStyle,
|
||||
@@ -302,16 +300,14 @@ function normalizeEditorControls(
|
||||
sanitizedRaw.cursorSpringStiffnessMultiplier ??
|
||||
fallback.cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier:
|
||||
sanitizedRaw.cursorSpringDampingMultiplier ??
|
||||
fallback.cursorSpringDampingMultiplier,
|
||||
sanitizedRaw.cursorSpringDampingMultiplier ?? fallback.cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier:
|
||||
sanitizedRaw.cursorSpringMassMultiplier ?? fallback.cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier:
|
||||
sanitizedRaw.cameraSpringStiffnessMultiplier ??
|
||||
fallback.cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier:
|
||||
sanitizedRaw.cameraSpringDampingMultiplier ??
|
||||
fallback.cameraSpringDampingMultiplier,
|
||||
sanitizedRaw.cameraSpringDampingMultiplier ?? fallback.cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier:
|
||||
sanitizedRaw.cameraSpringMassMultiplier ?? fallback.cameraSpringMassMultiplier,
|
||||
cursorMotionBlur: sanitizedRaw.cursorMotionBlur ?? fallback.cursorMotionBlur,
|
||||
@@ -324,8 +320,7 @@ function normalizeEditorControls(
|
||||
frame: sanitizedRaw.frame !== undefined ? sanitizedRaw.frame : fallback.frame,
|
||||
webcam: sanitizedRaw.webcam ?? fallback.webcam,
|
||||
aspectRatio: sanitizedRaw.aspectRatio ?? fallback.aspectRatio,
|
||||
exportEncodingMode:
|
||||
sanitizedRaw.exportEncodingMode ?? fallback.exportEncodingMode,
|
||||
exportEncodingMode: sanitizedRaw.exportEncodingMode ?? fallback.exportEncodingMode,
|
||||
exportBackendPreference:
|
||||
sanitizedRaw.exportBackendPreference === undefined
|
||||
? fallback.exportBackendPreference
|
||||
@@ -452,15 +447,25 @@ export function saveEditorPreferences(preferences: Partial<EditorPreferences>):
|
||||
const merged = normalizeEditorPreferences({ ...current, ...preferences }, current);
|
||||
const persisted = stripPersistedDevMotionBlurSettings(merged);
|
||||
saveAppSetting(EDITOR_PREFERENCES_STORAGE_KEY, persisted);
|
||||
globalThis.localStorage?.setItem(
|
||||
EDITOR_PREFERENCES_STORAGE_KEY,
|
||||
JSON.stringify(persisted),
|
||||
);
|
||||
saveLocalStorageJson(EDITOR_PREFERENCES_STORAGE_KEY, persisted);
|
||||
} catch {
|
||||
// Ignore storage failures so editor controls still work.
|
||||
}
|
||||
}
|
||||
|
||||
function saveLocalStorageJson(key: string, value: unknown): boolean {
|
||||
try {
|
||||
if (typeof globalThis.localStorage === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
globalThis.localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadEditorPresets(): EditorPreset[] {
|
||||
const persisted = loadAppSetting<unknown>(EDITOR_PRESETS_STORAGE_KEY);
|
||||
if (persisted !== null) {
|
||||
@@ -482,9 +487,12 @@ export function loadEditorPresets(): EditorPreset[] {
|
||||
export function saveEditorPresets(presets: EditorPreset[]): boolean {
|
||||
try {
|
||||
const normalized = normalizeEditorPresets(presets);
|
||||
const persisted = saveAppSetting(EDITOR_PRESETS_STORAGE_KEY, normalized);
|
||||
globalThis.localStorage?.setItem(EDITOR_PRESETS_STORAGE_KEY, JSON.stringify(normalized));
|
||||
return persisted || typeof globalThis.localStorage !== "undefined";
|
||||
const persistedToAppSettings = saveAppSetting(EDITOR_PRESETS_STORAGE_KEY, normalized);
|
||||
const persistedToLocalStorage = saveLocalStorageJson(
|
||||
EDITOR_PRESETS_STORAGE_KEY,
|
||||
normalized,
|
||||
);
|
||||
return persistedToAppSettings || persistedToLocalStorage;
|
||||
} catch {
|
||||
// Ignore storage failures so editor controls still work.
|
||||
return false;
|
||||
|
||||
@@ -14,8 +14,8 @@ type OfflineRenderTestHarness = AudioProcessor & {
|
||||
sourceAudioFallbackPaths: string[],
|
||||
sourceAudioFallbackStartDelayMsByPath?: Record<string, number>,
|
||||
): Promise<{
|
||||
mainBuffer: AudioBuffer | null;
|
||||
companionEntries: Array<{ buffer: AudioBuffer; startDelaySec: number }>;
|
||||
mainBufferEntry: { buffer: AudioBuffer; gain: number } | null;
|
||||
companionEntries: Array<{ buffer: AudioBuffer; startDelaySec: number; gain: number }>;
|
||||
}>;
|
||||
renderAndMuxOfflineAudio(
|
||||
videoUrl: string,
|
||||
@@ -55,9 +55,11 @@ describe("AudioProcessor offline render preparation", () => {
|
||||
["/tmp/recording.mp4", "/tmp/recording.mic.wav"],
|
||||
);
|
||||
|
||||
expect(prepared.mainBuffer).toBe(mainBuffer);
|
||||
expect(prepared.mainBufferEntry?.buffer).toBe(mainBuffer);
|
||||
expect(prepared.mainBufferEntry?.gain).toBe(1);
|
||||
expect(prepared.companionEntries).toHaveLength(1);
|
||||
expect(prepared.companionEntries[0]?.buffer).toBe(micBuffer);
|
||||
expect(prepared.companionEntries[0]?.gain).toBe(1);
|
||||
expect(decodeAudioFromUrl).toHaveBeenCalledWith("file:///tmp/recording.mp4");
|
||||
expect(decodeAudioFromUrl).toHaveBeenCalledWith("/tmp/recording.mic.wav");
|
||||
expect(decodeAudioFromUrl).not.toHaveBeenCalledWith("/tmp/recording.mp4");
|
||||
|
||||
@@ -363,19 +363,30 @@ describe("ModernFrameRenderer webcam frame cache", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the refresh throttle for default crop regions", () => {
|
||||
it("renders non-video webcam frames directly for default crop regions", () => {
|
||||
const renderer = createRenderer() as any;
|
||||
const liveSource = {};
|
||||
|
||||
renderer.config.webcam.cropRegion = { x: 0, y: 0, width: 1, height: 1 };
|
||||
renderer.webcamFrameCacheCanvas = { width: 1280, height: 720 };
|
||||
renderer.lastWebcamCacheRefreshTime = 10;
|
||||
renderer.currentVideoTime = 10.1;
|
||||
const renderableSource = renderer.resolveRenderableWebcamSource(
|
||||
liveSource,
|
||||
1280,
|
||||
720,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(renderer.shouldRefreshWebcamFrameCache(1280, 720)).toBe(false);
|
||||
expect(renderableSource).toMatchObject({
|
||||
source: liveSource,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
mode: "live",
|
||||
});
|
||||
expect(renderer.webcamFrameCacheCanvas).toBeNull();
|
||||
});
|
||||
|
||||
it("bypasses the refresh throttle for cropped webcam regions", () => {
|
||||
it("uses a cache-backed source for cropped webcam regions", () => {
|
||||
const renderer = createRenderer() as any;
|
||||
const liveSource = {};
|
||||
|
||||
renderer.config.webcam.cropRegion = {
|
||||
x: 0.25,
|
||||
@@ -383,11 +394,30 @@ describe("ModernFrameRenderer webcam frame cache", () => {
|
||||
width: 0.5,
|
||||
height: 1,
|
||||
};
|
||||
renderer.webcamFrameCacheCanvas = { width: 640, height: 720 };
|
||||
renderer.lastWebcamCacheRefreshTime = 10;
|
||||
renderer.currentVideoTime = 10.1;
|
||||
const renderableSource = renderer.resolveRenderableWebcamSource(
|
||||
liveSource,
|
||||
1280,
|
||||
720,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(renderer.shouldRefreshWebcamFrameCache(1280, 720)).toBe(true);
|
||||
expect(renderableSource?.source).toBe(renderer.webcamFrameCacheCanvas);
|
||||
expect(renderableSource).toMatchObject({
|
||||
width: 640,
|
||||
height: 720,
|
||||
mode: "cached",
|
||||
});
|
||||
expect(renderer.webcamFrameCacheCtx.drawImage).toHaveBeenCalledWith(
|
||||
liveSource,
|
||||
320,
|
||||
0,
|
||||
640,
|
||||
720,
|
||||
0,
|
||||
0,
|
||||
640,
|
||||
720,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -720,31 +750,23 @@ describe("ModernFrameRenderer temporal webcam sync", () => {
|
||||
zoom: { scale: 1, focusX: 0.5, focusY: 0.5, progress: 0 },
|
||||
}));
|
||||
|
||||
await renderer.renderTemporalMotionBlurFrame(
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
33_333,
|
||||
{
|
||||
stageSize: { width: 1920, height: 1080 },
|
||||
videoSize: { width: 1920, height: 1080 },
|
||||
baseScale: 1,
|
||||
baseOffset: { x: 0, y: 0 },
|
||||
maskRect: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
sourceCrop: { x: 0, y: 0, width: 1, height: 1 },
|
||||
},
|
||||
await renderer.renderTemporalMotionBlurFrame(1_000_000, 1_000_000, 1_000_000, 33_333, {
|
||||
stageSize: { width: 1920, height: 1080 },
|
||||
videoSize: { width: 1920, height: 1080 },
|
||||
baseScale: 1,
|
||||
baseOffset: { x: 0, y: 0 },
|
||||
maskRect: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
sourceCrop: { x: 0, y: 0, width: 1, height: 1 },
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(renderer.renderSceneSample).toHaveBeenCalledTimes(3);
|
||||
expect(renderer.renderSceneSample.mock.calls.map((call: unknown[]) => call[6])).toEqual([
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1, 1, 1,
|
||||
]);
|
||||
expect(
|
||||
new Set(renderer.renderSceneSample.mock.calls.map((call: unknown[]) => call[0])).size,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { ModernVideoExporter as ModernVideoExporterClass } from "./modernVideoExporter";
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const videoInfo = {
|
||||
@@ -67,13 +68,18 @@ vi.mock("./muxer", () => ({
|
||||
}));
|
||||
|
||||
describe("ModernVideoExporter native fallback routing", () => {
|
||||
let ModernVideoExporter: typeof ModernVideoExporterClass;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ ModernVideoExporter } = await import("./modernVideoExporter"));
|
||||
}, 30_000);
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("falls back to WebCodecs instead of surfacing a native error when Breeze is unavailable", async () => {
|
||||
const { ModernVideoExporter } = await import("./modernVideoExporter");
|
||||
const exporter = new ModernVideoExporter({
|
||||
videoUrl: "file:///recording.mp4",
|
||||
width: 1920,
|
||||
@@ -124,7 +130,6 @@ describe("ModernVideoExporter native fallback routing", () => {
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||||
});
|
||||
|
||||
const { ModernVideoExporter } = await import("./modernVideoExporter");
|
||||
const nativeResult = {
|
||||
success: true,
|
||||
blob: new Blob([], { type: "video/mp4" }),
|
||||
@@ -181,7 +186,6 @@ describe("ModernVideoExporter native fallback routing", () => {
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||||
});
|
||||
|
||||
const { ModernVideoExporter } = await import("./modernVideoExporter");
|
||||
const staticLayoutResult = {
|
||||
success: true,
|
||||
blob: new Blob([], { type: "video/mp4" }),
|
||||
@@ -235,7 +239,6 @@ describe("ModernVideoExporter native fallback routing", () => {
|
||||
}, 15_000);
|
||||
|
||||
it("retries the main decode path once with a readable file-backed source", async () => {
|
||||
const { ModernVideoExporter } = await import("./modernVideoExporter");
|
||||
mocks.streamingDecoderGetEffectiveDuration.mockReturnValue(1);
|
||||
mocks.streamingDecoderDecodeAll
|
||||
.mockRejectedValueOnce(
|
||||
|
||||
Reference in New Issue
Block a user