Merge pull request #624 from webadderallorg/fix/main-baseline-test-drift

test: restore main baseline verification
This commit is contained in:
Phạm Thị Minh Hồng
2026-05-29 23:03:32 +07:00
committed by GitHub
5 changed files with 128 additions and 59 deletions
@@ -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;
+5 -3
View File
@@ -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");
+53 -31
View File
@@ -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(