diff --git a/src/components/launch/hooks/useWebcamPreviewOverlay.ts b/src/components/launch/hooks/useWebcamPreviewOverlay.ts index ed527e9a..2cd69806 100644 --- a/src/components/launch/hooks/useWebcamPreviewOverlay.ts +++ b/src/components/launch/hooks/useWebcamPreviewOverlay.ts @@ -1,6 +1,7 @@ import { type PointerEvent, useCallback, useEffect, useRef, useState } from "react"; import { canShowFloatingWebcamPreview, + DEFAULT_WEBCAM_PREVIEW_SIZE, parseWebcamPreviewSize, resizeWebcamPreview, WEBCAM_PREVIEW_SIZE_STORAGE_KEY, @@ -48,9 +49,13 @@ export function useWebcamPreviewOverlay({ const [showFloatingWebcamPreview, setShowFloatingWebcamPreview] = useState(true); const [webcamPreviewOffset, setWebcamPreviewOffset] = useState(DEFAULT_WEBCAM_PREVIEW_OFFSET); const webcamPreviewOffsetRef = useRef(DEFAULT_WEBCAM_PREVIEW_OFFSET); - const [webcamPreviewSize, setWebcamPreviewSize] = useState(() => - parseWebcamPreviewSize(localStorage.getItem(WEBCAM_PREVIEW_SIZE_STORAGE_KEY)), - ); + const [webcamPreviewSize, setWebcamPreviewSize] = useState(() => { + try { + return parseWebcamPreviewSize(localStorage.getItem(WEBCAM_PREVIEW_SIZE_STORAGE_KEY)); + } catch { + return DEFAULT_WEBCAM_PREVIEW_SIZE; + } + }); const webcamPreviewResizeRef = useRef<{ pointerId: number; handle: WebcamPreviewResizeHandle; @@ -273,7 +278,14 @@ export function useWebcamPreviewOverlay({ isWebcamPreviewDraggingRef.current = false; setWebcamPreviewSize(resizeState.size); setWebcamPreviewOffset({ ...webcamPreviewOffsetRef.current }); - localStorage.setItem(WEBCAM_PREVIEW_SIZE_STORAGE_KEY, JSON.stringify(resizeState.size)); + try { + localStorage.setItem( + WEBCAM_PREVIEW_SIZE_STORAGE_KEY, + JSON.stringify(resizeState.size), + ); + } catch { + // Keep resizing and pointer cleanup working when preferences cannot be saved. + } if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } diff --git a/tests/ui/hud-layout.spec.ts b/tests/ui/hud-layout.spec.ts index 178dd73d..40e24754 100644 --- a/tests/ui/hud-layout.spec.ts +++ b/tests/ui/hud-layout.spec.ts @@ -109,56 +109,82 @@ test("New recording mode starts the webcam with an editor still open and release .toBe(true); }); -test("floating webcam preview resizes from its corners and remembers the size", async ({ - page, -}) => { - await installDesktopBridge(page); - await page.addInitScript(() => { - window.electronAPI.getRecordingPreferences = async () => ({ - success: true, - microphoneEnabled: false, - webcamEnabled: true, - systemAudioEnabled: false, +for (const storageBlocked of [false, true]) { + test(`floating webcam preview resizes from its corners ${storageBlocked ? "when storage is unavailable" : "and remembers the size"}`, async ({ + page, + }) => { + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + await installDesktopBridge(page); + if (storageBlocked) { + await page.addInitScript(() => { + const getItem = Storage.prototype.getItem; + const setItem = Storage.prototype.setItem; + Storage.prototype.getItem = function (key) { + if (key === "recordly.webcam-preview-size") + throw new Error("Storage unavailable"); + return getItem.call(this, key); + }; + Storage.prototype.setItem = function (key, value) { + if (key === "recordly.webcam-preview-size") + throw new Error("Storage unavailable"); + return setItem.call(this, key, value); + }; + }); + } + await page.addInitScript(() => { + window.electronAPI.hudOverlaySetIgnoreMouse = async (ignore) => { + document.documentElement.dataset.webcamPassthrough = String(ignore); + return { success: true }; + }; + window.electronAPI.getRecordingPreferences = async () => ({ + success: true, + microphoneEnabled: false, + webcamEnabled: true, + systemAudioEnabled: false, + }); + navigator.mediaDevices.getUserMedia = async () => { + const canvas = document.createElement("canvas"); + canvas.getContext("2d")!.fillRect(0, 0, 320, 320); + return canvas.captureStream(24); + }; + navigator.mediaDevices.enumerateDevices = async () => []; }); - navigator.mediaDevices.getUserMedia = async () => { - const canvas = document.createElement("canvas"); - canvas.getContext("2d")!.fillRect(0, 0, 320, 320); - return canvas.captureStream(24); + await page.goto("/?windowType=hud-overlay"); + const preview = page.locator("[data-resize-handle]").first().locator(".."); + await expect(preview).toBeVisible(); + const before = (await preview.boundingBox())!; + expect([before.width, before.height]).toEqual([288, 288]); + + const dragHandle = async (handle: string, dx: number, dy: number) => { + const box = (await preview.locator(`[data-resize-handle="${handle}"]`).boundingBox())!; + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + dx, box.y + box.height / 2 + dy, { + steps: 5, + }); + await page.mouse.up(); }; - navigator.mediaDevices.enumerateDevices = async () => []; + + // Top-left grows the preview while the bottom-right corner stays anchored. + await dragHandle("nw", -60, -40); + const grown = (await preview.boundingBox())!; + expect([grown.width, grown.height]).toEqual([348, 328]); + expect(grown.x + grown.width).toBeCloseTo(before.x + before.width); + expect(grown.y + grown.height).toBeCloseTo(before.y + before.height); + + // Bottom-right shrinks the preview while the top-left corner stays put, down to the minimum. + await dragHandle("se", -500, -500); + const shrunk = (await preview.boundingBox())!; + expect([shrunk.width, shrunk.height]).toEqual([120, 120]); + expect(shrunk.x).toBeCloseTo(grown.x); + expect(shrunk.y).toBeCloseTo(grown.y); + await expect(page.locator("html")).toHaveAttribute("data-webcam-passthrough", "true"); + + await page.reload(); + await expect(preview).toBeVisible(); + const restored = (await preview.boundingBox())!; + expect([restored.width, restored.height]).toEqual(storageBlocked ? [288, 288] : [120, 120]); + expect(errors).toEqual([]); }); - await page.goto("/?windowType=hud-overlay"); - const preview = page.locator("[data-resize-handle]").first().locator(".."); - await expect(preview).toBeVisible(); - const before = (await preview.boundingBox())!; - expect([before.width, before.height]).toEqual([288, 288]); - - const dragHandle = async (handle: string, dx: number, dy: number) => { - const box = (await preview.locator(`[data-resize-handle="${handle}"]`).boundingBox())!; - await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); - await page.mouse.down(); - await page.mouse.move(box.x + box.width / 2 + dx, box.y + box.height / 2 + dy, { - steps: 5, - }); - await page.mouse.up(); - }; - - // Top-left grows the preview while the bottom-right corner stays anchored. - await dragHandle("nw", -60, -40); - const grown = (await preview.boundingBox())!; - expect([grown.width, grown.height]).toEqual([348, 328]); - expect(grown.x + grown.width).toBeCloseTo(before.x + before.width); - expect(grown.y + grown.height).toBeCloseTo(before.y + before.height); - - // Bottom-right shrinks the preview while the top-left corner stays put, down to the minimum. - await dragHandle("se", -500, -500); - const shrunk = (await preview.boundingBox())!; - expect([shrunk.width, shrunk.height]).toEqual([120, 120]); - expect(shrunk.x).toBeCloseTo(grown.x); - expect(shrunk.y).toBeCloseTo(grown.y); - - await page.reload(); - await expect(preview).toBeVisible(); - const restored = (await preview.boundingBox())!; - expect([restored.width, restored.height]).toEqual([120, 120]); -}); +}