From 01b682a4618c577e17f7158c9c50fc8601d90c95 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:25:51 +1100 Subject: [PATCH] Preserve edited timelines and stabilize webcam crop controls --- .../video-editor/WebcamCropControl.tsx | 2 +- .../hooks/useAutoApplyZooms.test.ts | 19 +++++++ .../video-editor/hooks/useAutoApplyZooms.ts | 12 +++++ .../video-editor/settings/CursorSettings.tsx | 5 +- src/i18n/locales/de/settings.json | 1 + src/i18n/locales/en/settings.json | 1 + src/i18n/locales/es/settings.json | 1 + src/i18n/locales/fr/settings.json | 1 + src/i18n/locales/it/settings.json | 1 + src/i18n/locales/ja/settings.json | 1 + src/i18n/locales/ko/settings.json | 1 + src/i18n/locales/nl/settings.json | 1 + src/i18n/locales/pt-BR/settings.json | 1 + src/i18n/locales/zh-CN/settings.json | 1 + src/i18n/locales/zh-TW/settings.json | 1 + tests/ui/cursor-idle-settings.spec.ts | 4 ++ tests/ui/scene-preferences.spec.ts | 52 +++++++++++-------- tests/ui/videos-library.spec.ts | 21 +++++++- tests/ui/webcam-preview-frame.spec.ts | 1 + 19 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/components/video-editor/WebcamCropControl.tsx b/src/components/video-editor/WebcamCropControl.tsx index 12eb45a1..cdea4681 100644 --- a/src/components/video-editor/WebcamCropControl.tsx +++ b/src/components/video-editor/WebcamCropControl.tsx @@ -388,7 +388,7 @@ export function WebcamCropControl({ ref={containerRef} variant="secondary" data-webcam-crop-control - className="relative w-full touch-none select-none overflow-hidden rounded-lg border border-foreground/10" + className="relative w-full touch-none select-none overflow-clip rounded-lg border border-foreground/10" style={{ aspectRatio: previewAspectRatio }} onPointerMove={handlePointerMove} onPointerUp={endDrag} diff --git a/src/components/video-editor/hooks/useAutoApplyZooms.test.ts b/src/components/video-editor/hooks/useAutoApplyZooms.test.ts index d583b7e8..45d81e2f 100644 --- a/src/components/video-editor/hooks/useAutoApplyZooms.test.ts +++ b/src/components/video-editor/hooks/useAutoApplyZooms.test.ts @@ -37,6 +37,7 @@ it("waits for ready media and matching cursor data before applying", () => { { ready: false }, { totalMs: 0 }, { cursorTelemetry: [] }, + { clipRegions: [] }, { cursorTelemetrySourcePath: "previous.mp4" }, ]) { const params = input(patch); @@ -56,3 +57,21 @@ it("keeps automatic zooms off when disabled and preserves reopened projects and expect(params.onZoomSuggested).not.toHaveBeenCalled(); } }); + +it("preserves imported sequences and clip edits made while cursor data was loading", () => { + const original = input().clipRegions[0]; + for (const clipRegions of [ + [ + original, + { ...original, id: "imported", startMs: 6000, endMs: 7000, sourceStartMs: 6000 }, + ], + [{ ...original, sourceStartMs: 1000 }], + [{ ...original, startMs: 500 }], + [{ ...original, endMs: 5000 }], + [{ ...original, speed: 2 }], + ]) { + const params = input({ clipRegions }); + expect(applyAutomaticZooms(params)).toBe(true); + expect(params.onZoomSuggested).not.toHaveBeenCalled(); + } +}); diff --git a/src/components/video-editor/hooks/useAutoApplyZooms.ts b/src/components/video-editor/hooks/useAutoApplyZooms.ts index acdf1fbe..16331f86 100644 --- a/src/components/video-editor/hooks/useAutoApplyZooms.ts +++ b/src/components/video-editor/hooks/useAutoApplyZooms.ts @@ -22,6 +22,18 @@ export function applyAutomaticZooms(input: Input): boolean { if (!input.ready || !input.sourcePath || !Number.isFinite(input.totalMs) || input.totalMs <= 0) return false; if (!input.enabled || input.isSavedProject || input.zoomRegions.length > 0) return true; + // Imported sequences get new media paths too. Only start on an unedited take; + // adding/trimming clips while telemetry loads must preserve the user's timeline. + const clip = input.clipRegions[0]; + if (!clip) return false; + if ( + input.clipRegions.length !== 1 || + clip.startMs !== 0 || + Math.abs(clip.endMs - input.totalMs) > 1 || + (clip.sourceStartMs ?? 0) !== 0 || + (clip.speed ?? 1) !== 1 + ) + return true; if (input.cursorTelemetrySourcePath !== input.sourcePath || input.cursorTelemetry.length < 2) return false; const result = buildInteractionZoomSuggestions({ diff --git a/src/components/video-editor/settings/CursorSettings.tsx b/src/components/video-editor/settings/CursorSettings.tsx index 4cfefd6d..d2970e08 100644 --- a/src/components/video-editor/settings/CursorSettings.tsx +++ b/src/components/video-editor/settings/CursorSettings.tsx @@ -254,7 +254,10 @@ export function CursorSettings(props: Props) { {advanced && ( JSON.parse(localStorage.getItem("recordly.editor.preferences") ?? "{}"), @@ -61,8 +66,7 @@ test("Scene controls carry to new projects; loading an older look does not repla page, }) => { await installSceneBridge(page); - await page.goto("/?windowType=editor&project=first"); - await expect(page.locator('[data-variant="clip"]')).toHaveCount(1, { timeout: 20_000 }); + await openScene(page, "/?windowType=editor&project=first"); await setSlider(page, "Padding", 8); const blur = await setSlider(page, "Blur", 3); const shadow = await setSlider(page, "Shadow", 4); @@ -91,7 +95,7 @@ test("Scene controls carry to new projects; loading an older look does not repla await expect.poll(() => storedPreferences(page)).toMatchObject(expected); // A second recording reads the controls actually saved by the first editor. - await page.goto("/?windowType=editor&project=second"); + await openScene(page, "/?windowType=editor&project=second"); for (const [index, side] of ["Top", "Bottom", "Left", "Right"].entries()) { await expect(page.getByRole("slider", { name: side, exact: true })).toHaveValue( String(index + 1), @@ -108,7 +112,7 @@ test("Scene controls carry to new projects; loading an older look does not repla "true", ); - await page.goto("/?windowType=editor&older=1"); + await openScene(page, "/?windowType=editor&older=1"); await expect( page.getByRole("button", { name: "Aspect ratio: 16:9", exact: true }), ).toBeVisible(); @@ -123,7 +127,7 @@ test("Scene controls carry to new projects; loading an older look does not repla await expect .poll(() => storedPreferences(page)) .toMatchObject({ ...expected, shadowIntensity: 0.71 }); - await page.goto("/?windowType=editor&project=third"); + await openScene(page, "/?windowType=editor&project=third"); await expect(page.getByRole("slider", { name: "Top", exact: true })).toHaveValue("1"); await expect(page.getByRole("slider", { name: "Shadow", exact: true })).toHaveValue("0.71"); await expect(page.getByRole("button", { name: "Gradient 1", exact: true })).toHaveAttribute( @@ -136,27 +140,29 @@ test("image, video, solid color and gradient selections survive creating another page, }) => { await installSceneBridge(page); - await page.goto("/?windowType=editor&project=initial"); - for (const [index, tab] of ["Image", "Video", "Colors", "Colors"].entries()) { + await openScene(page, "/?windowType=editor&project=initial"); + const cases = [ + { + tab: "Image", + label: "Golden Gate Day", + wallpaper: "/wallpapers/golden-gate-day-no-vignette.png", + }, + { tab: "Video", label: "Wispy Sky", wallpaper: "/wallpapers/wispysky.mp4" }, + { tab: "Colors", label: "Background color #E2E8F0", wallpaper: "#e2e8f0" }, + { + tab: "Colors", + label: "Gradient 2", + wallpaper: "linear-gradient(135deg, #e0e7ff 0%, #c4b5fd 100%)", + }, + ]; + for (const [index, { tab, label, wallpaper }] of cases.entries()) { await page.getByRole("row", { name: tab, exact: true }).click(); - const tile = - index === 2 - ? page.getByRole("button", { name: "Background color #E2E8F0", exact: true }) - : index === 3 - ? page.getByRole("button", { name: "Gradient 2", exact: true }) - : page - .locator( - `[data-background-panel="${tab.toLowerCase()}"] .wallpaper-tile`, - ) - .nth(index === 0 ? 1 : 0); - const label = await tile.getAttribute("aria-label"); - expect(label).toBeTruthy(); + const tile = page.getByRole("button", { name: label, exact: true }); await tile.click(); await expect(tile).toHaveAttribute("aria-pressed", "true"); - const wallpaper = (await storedPreferences(page)).wallpaper; - expect(wallpaper).toBeTruthy(); - await page.goto(`/?windowType=editor&project=background-${index}`); - await expect(page.getByRole("button", { name: label!, exact: true })).toHaveAttribute( + await expect.poll(() => storedPreferences(page)).toMatchObject({ wallpaper }); + await openScene(page, `/?windowType=editor&project=background-${index}`); + await expect(page.getByRole("button", { name: label, exact: true })).toHaveAttribute( "aria-pressed", "true", ); diff --git a/tests/ui/videos-library.spec.ts b/tests/ui/videos-library.spec.ts index 5ce1800a..5d04f6ee 100644 --- a/tests/ui/videos-library.spec.ts +++ b/tests/ui/videos-library.spec.ts @@ -361,10 +361,17 @@ test("Clips shows original filenames and truncates long names beside real still }); for (const enabled of [true, false]) { - test(`imported recordings stay unzoomed with legacy auto zooms ${enabled ? "on" : "off"}`, async ({ + test(`imported recordings stay unzoomed with automatic zooms ${enabled ? "on" : "off"}`, async ({ page, }) => { await setup(page); + await installDesktopBridgeOverrides(page, () => { + const get = window.electronAPI.getAppSetting; + window.electronAPI.getAppSetting = (key) => + key === "recordly.editor.preferences" + ? JSON.parse(localStorage.getItem(key) ?? "{}") + : get(key); + }); await page.evaluate((enabled) => { const prefs = JSON.parse(localStorage.getItem("recordly.editor.preferences") || "{}"); localStorage.setItem( @@ -373,7 +380,17 @@ for (const enabled of [true, false]) { ); }, enabled); await page.reload(); - await expect(page.locator('[data-variant="clip"]')).toHaveCount(1); + await expect(page.locator('[data-variant="clip"]')).toHaveCount(1, { timeout: 20_000 }); + await page.getByRole("button", { name: "Settings", exact: true }).click(); + const settings = page.getByRole("dialog", { name: "Settings", exact: true }); + await settings.getByRole("tab", { name: "Motion", exact: true }).click(); + const automatic = settings.getByRole("switch", { + name: "Automatically add zooms", + exact: true, + }); + if (enabled) await expect(automatic).toBeChecked(); + else await expect(automatic).not.toBeChecked(); + await page.keyboard.press("Escape"); await page.getByRole("button", { name: "Clips", exact: true }).click(); await page.evaluate(() => { let count = 0; diff --git a/tests/ui/webcam-preview-frame.spec.ts b/tests/ui/webcam-preview-frame.spec.ts index cbaf76b2..1c97a9dd 100644 --- a/tests/ui/webcam-preview-frame.spec.ts +++ b/tests/ui/webcam-preview-frame.spec.ts @@ -61,6 +61,7 @@ test("webcam preview frame choices persist and inline crop updates immediately", exact: true, }); await handle.focus(); + await expect.poll(() => crop.evaluate((element) => element.scrollTop)).toBe(0); await handle.press("ArrowLeft"); await expect .poll(() =>