diff --git a/apps/web/src/components/preview/PreviewView.browser.test.tsx b/apps/web/src/components/preview/PreviewView.browser.test.tsx index 115aec44c5ab..f3c86edcddd1 100644 --- a/apps/web/src/components/preview/PreviewView.browser.test.tsx +++ b/apps/web/src/components/preview/PreviewView.browser.test.tsx @@ -34,12 +34,14 @@ const mocks = vi.hoisted(() => ({ idleDesktopTab: false, idleDesktopOverlay: false, loading: false, + loadFailed: false, nativeServerTab: false, streamedServerTab: false, serverEpoch: null as string | null, servers: [] as PreviewableServer[], recordingTabIds: new Set(), recordingRuntimeTabId: null as string | null, + stopBrowserRecording: vi.fn(async (_runtimeTabId: string) => null), recordVisitForThread: vi.fn(), })); @@ -51,6 +53,13 @@ const STUB_BROWSER_DEFAULTS = { profiles: BUILT_IN_BROWSER_PROFILES, profileId: DEFAULT_BROWSER_PROFILE_ID, }; +const LOAD_FAILED = { + _tag: "LoadFailed", + url: "http://example.com/", + title: "", + code: -102, + description: "ERR_CONNECTION_REFUSED", +} as const; // The desktop bridge only exists inside Electron; the view reads it once at import. vi.mock(import("./previewBridge"), async (original) => ({ @@ -105,9 +114,11 @@ vi.mock(import("~/previewStateStore"), async (original) => ({ desktopByTabId: { "tab-1": { hasWebContents: true, - navStatus: mocks.idleDesktopOverlay - ? { _tag: "Idle" } - : { _tag: "Success", url: "http://example.com/", title: "Example" }, + navStatus: mocks.loadFailed + ? LOAD_FAILED + : mocks.idleDesktopOverlay + ? { _tag: "Idle" } + : { _tag: "Success", url: "http://example.com/", title: "Example" }, canGoBack: false, canGoForward: false, loading: mocks.loading, @@ -139,7 +150,9 @@ vi.mock(import("~/previewStateStore"), async (original) => ({ threadId: "thread-1", tabId: "tab-1", runtime: mocks.nativeServerTab || mocks.streamedServerTab ? "server" : "desktop", - navStatus: { _tag: "Success", url: "http://example.com/", title: "Example" }, + navStatus: mocks.loadFailed + ? LOAD_FAILED + : { _tag: "Success", url: "http://example.com/", title: "Example" }, canGoBack: false, canGoForward: false, updatedAt: "2026-07-13T00:00:00.000Z", @@ -157,6 +170,7 @@ vi.mock(import("~/browser/browserRecording"), async (original) => ({ ...(await original()), findActiveBrowserRecordingRuntimeTabId: () => mocks.recordingRuntimeTabId, useActiveBrowserRecordingTabIds: () => mocks.recordingTabIds, + stopBrowserRecording: mocks.stopBrowserRecording, })); vi.mock(import("./useDiscoveredLocalServers"), async (original) => ({ ...(await original()), @@ -236,6 +250,7 @@ function setup() { mocks.idleDesktopTab = false; mocks.idleDesktopOverlay = false; mocks.loading = false; + mocks.loadFailed = false; mocks.nativeServerTab = false; mocks.streamedServerTab = false; mocks.serverEpoch = null; @@ -451,6 +466,19 @@ describe("PreviewView", () => { expect(listeners.size).toBe(0); }); + it("stops an active recording after the page fails to load", async () => { + setup(); + mocks.loadFailed = true; + mocks.recordingTabIds = new Set([TEST_RUNTIME_TAB_ID]); + await using _screen = await render(view()); + + const stop = page.getByRole("button", { name: "Stop recording" }); + await expect.element(stop).toBeEnabled(); + await userEvent.click(stop); + + expect(mocks.stopBrowserRecording).toHaveBeenCalledWith(TEST_RUNTIME_TAB_ID); + }); + it("focuses the address bar each time the focus-url shortcut fires", async () => { setup(); await using _screen = await render(view()); diff --git a/apps/web/src/components/preview/PreviewView.tsx b/apps/web/src/components/preview/PreviewView.tsx index 02d856d78ca6..03332df6d8fa 100644 --- a/apps/web/src/components/preview/PreviewView.tsx +++ b/apps/web/src/components/preview/PreviewView.tsx @@ -997,7 +997,7 @@ export function PreviewView({ onSubmit={(next) => void handleSubmitUrl(next)} onOpenInBrowser={tabId ? handleOpenInBrowser : undefined} onCapture={previewBridge && tabId && !isServerTab ? handleCapture : undefined} - captureDisabled={!desktopOverlay || isUnreachable} + captureDisabled={recordingRuntimeTabId === null && (!desktopOverlay || isUnreachable)} recording={recordingRuntimeTabId !== null} onPictureInPicture={ tabId && (isServerTab || previewBridge) ? handlePictureInPicture : undefined