From 1f968b92c91c4ffc57353e9d72f824285cfb8cb9 Mon Sep 17 00:00:00 2001 From: Wolfie Date: Fri, 26 Jun 2026 20:05:47 -0700 Subject: [PATCH] Fix browser pane stuck in HTML fullscreen (#6442) (#6451) * Fix browser pane stuck in HTML fullscreen (#6442) Guest content in a browser pane could enter HTML fullscreen but not exit it: requestFullscreen() resized the host BrowserWindow into native fullscreen, and exitFullscreen() had nothing to restore, leaving the pane stuck. Set the webview's disableHtmlFullscreenWindowResize preference so HTML fullscreen is contained to the element. Exit then works and fullscreenchange fires as guests expect. * Apply browser guest fullscreen policy consistently Co-authored-by: Orca --------- Co-authored-by: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Co-authored-by: Jinwoo-H Co-authored-by: Orca --- src/main/browser/offscreen-browser-backend.ts | 4 ++ ...en-browser-backend.web-preferences.test.ts | 27 +++++++++++++ src/main/window/createMainWindow.test.ts | 5 +++ src/main/window/createMainWindow.ts | 4 ++ .../components/browser-pane/BrowserPane.tsx | 11 ++++++ .../BrowserPane.webview-preferences.test.ts | 38 +++++++++++++++++++ src/shared/browser-guest-web-preferences.ts | 5 +++ 7 files changed, 94 insertions(+) create mode 100644 src/main/browser/offscreen-browser-backend.web-preferences.test.ts create mode 100644 src/renderer/src/components/browser-pane/BrowserPane.webview-preferences.test.ts create mode 100644 src/shared/browser-guest-web-preferences.ts diff --git a/src/main/browser/offscreen-browser-backend.ts b/src/main/browser/offscreen-browser-backend.ts index 7eeed2863..c8ad921c4 100644 --- a/src/main/browser/offscreen-browser-backend.ts +++ b/src/main/browser/offscreen-browser-backend.ts @@ -1,6 +1,7 @@ import { randomUUID } from 'crypto' import { BrowserWindow } from 'electron' import { ORCA_BROWSER_PARTITION } from '../../shared/constants' +import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser-guest-web-preferences' import type { BrowserBackend, BrowserBackendCreateTab } from './browser-backend' import type { BrowserManager } from './browser-manager' import { browserSessionRegistry } from './browser-session-registry' @@ -35,6 +36,9 @@ export class OffscreenBrowserBackend implements BrowserBackend { width: DEFAULT_VIEWPORT_WIDTH, height: DEFAULT_VIEWPORT_HEIGHT, webPreferences: { + // Why: offscreen pages are the SSH/headless browser backend; keep their + // HTML fullscreen behavior aligned with desktop guests. + ...ORCA_BROWSER_GUEST_WEB_PREFERENCES, partition, sandbox: true, contextIsolation: true, diff --git a/src/main/browser/offscreen-browser-backend.web-preferences.test.ts b/src/main/browser/offscreen-browser-backend.web-preferences.test.ts new file mode 100644 index 000000000..7f558e2d0 --- /dev/null +++ b/src/main/browser/offscreen-browser-backend.web-preferences.test.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { describe, expect, it } from 'vitest' + +const OFFSCREEN_BACKEND_SOURCE = resolve(__dirname, 'offscreen-browser-backend.ts') + +function sourceBetween(source: string, start: string, end: string): string { + const startIndex = source.indexOf(start) + const endIndex = source.indexOf(end, startIndex + start.length) + + expect(startIndex).toBeGreaterThanOrEqual(0) + expect(endIndex).toBeGreaterThan(startIndex) + + return source.slice(startIndex, endIndex) +} + +describe('OffscreenBrowserBackend web preferences', () => { + it('uses the shared browser guest fullscreen policy', () => { + const source = readFileSync(OFFSCREEN_BACKEND_SOURCE, 'utf8') + const webPreferencesBlock = sourceBetween(source, 'webPreferences: {', 'partition,') + + expect(source).toContain( + "import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser-guest-web-preferences'" + ) + expect(webPreferencesBlock).toContain('...ORCA_BROWSER_GUEST_WEB_PREFERENCES') + }) +}) diff --git a/src/main/window/createMainWindow.test.ts b/src/main/window/createMainWindow.test.ts index 85a0e805c..4b4693ed5 100644 --- a/src/main/window/createMainWindow.test.ts +++ b/src/main/window/createMainWindow.test.ts @@ -225,6 +225,11 @@ describe('createMainWindow', () => { { src: 'data:text/html,' } as never ) expect(allowBlankEvent.preventDefault).not.toHaveBeenCalled() + expect(allowBlankPrefs).toMatchObject({ + disableHtmlFullscreenWindowResize: true, + partition: 'persist:orca-browser', + sandbox: true + }) const denyInlineHtmlEvent = { preventDefault: vi.fn() } windowHandlers['will-attach-webview']( diff --git a/src/main/window/createMainWindow.ts b/src/main/window/createMainWindow.ts index dec78fea5..863392bf1 100644 --- a/src/main/window/createMainWindow.ts +++ b/src/main/window/createMainWindow.ts @@ -20,6 +20,7 @@ import { normalizeBrowserNavigationUrl, normalizeExternalBrowserUrl } from '../../shared/browser-url' +import { ORCA_BROWSER_GUEST_WEB_PREFERENCES } from '../../shared/browser-guest-web-preferences' import { isCrashReportReason } from '../../shared/crash-reporting' import { getWindowShortcutActionId, @@ -485,6 +486,9 @@ export function createMainWindow( webPreferences.allowRunningInsecureContent = false webPreferences.contextIsolation = true webPreferences.sandbox = true + // Why: keep renderer-created webviews aligned with the browser guest policy + // even if the host markup omits or misspells a preference. + Object.assign(webPreferences, ORCA_BROWSER_GUEST_WEB_PREFERENCES) // Why: preserve the registry-validated partition instead of forcing the // legacy constant. This lets imported/isolated session profiles use their // own cookie/storage partition while keeping all other hardening intact. diff --git a/src/renderer/src/components/browser-pane/BrowserPane.tsx b/src/renderer/src/components/browser-pane/BrowserPane.tsx index b1e304dc3..84e084d76 100644 --- a/src/renderer/src/components/browser-pane/BrowserPane.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPane.tsx @@ -77,6 +77,7 @@ import { browserViewportPresetToOverride, getBrowserViewportPreset } from '../../../../shared/browser-viewport-presets' +import { ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE } from '../../../../shared/browser-guest-web-preferences' import { rememberLiveBrowserUrl } from './browser-runtime' import { destroyPersistentWebview, @@ -3612,6 +3613,16 @@ function BrowserPagePane({ webview = document.createElement('webview') as Electron.WebviewTag webview.setAttribute('partition', webviewPartition) webview.setAttribute('allowpopups', '') + // Why: keep HTML fullscreen contained to the element instead of + // letting guest content resize the host BrowserWindow into native macOS + // fullscreen. Without this, requestFullscreen() resizes the OS window but + // exitFullscreen() has nothing to restore, leaving the pane stuck + // fullscreen (issue #6442). Containing fullscreen to the element makes + // exit reliable and fires fullscreenchange as guests expect. + // Why: the key must be camelCase — Electron's webpreferences + // parser spreads keys verbatim into WebPreferences, so a lowercase key is + // silently ignored. + webview.setAttribute('webpreferences', ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE) webview.style.display = 'flex' webview.style.flex = '1' webview.style.width = '100%' diff --git a/src/renderer/src/components/browser-pane/BrowserPane.webview-preferences.test.ts b/src/renderer/src/components/browser-pane/BrowserPane.webview-preferences.test.ts new file mode 100644 index 000000000..da62f3939 --- /dev/null +++ b/src/renderer/src/components/browser-pane/BrowserPane.webview-preferences.test.ts @@ -0,0 +1,38 @@ +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { describe, expect, it } from 'vitest' + +const BROWSER_PANE_SOURCE = resolve(__dirname, 'BrowserPane.tsx') + +function browserPaneSource(): string { + return readFileSync(BROWSER_PANE_SOURCE, 'utf8') +} + +function sourceBetween(source: string, start: string, end: string): string { + const startIndex = source.indexOf(start) + const endIndex = source.indexOf(end, startIndex + start.length) + + expect(startIndex).toBeGreaterThanOrEqual(0) + expect(endIndex).toBeGreaterThan(startIndex) + + return source.slice(startIndex, endIndex) +} + +describe('BrowserPane webview preferences', () => { + it('keeps HTML fullscreen contained inside the browser pane webview', () => { + const source = browserPaneSource() + const creationBlock = sourceBetween( + source, + "webview = document.createElement('webview')", + 'registerPersistentWebview' + ) + + expect(source).toContain( + "import { ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE } from '../../../../shared/browser-guest-web-preferences'" + ) + expect(creationBlock).toContain( + "webview.setAttribute('webpreferences', ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE)" + ) + expect(creationBlock).not.toContain('disablehtmlfullscreenwindowresize') + }) +}) diff --git a/src/shared/browser-guest-web-preferences.ts b/src/shared/browser-guest-web-preferences.ts new file mode 100644 index 000000000..2e7cd0973 --- /dev/null +++ b/src/shared/browser-guest-web-preferences.ts @@ -0,0 +1,5 @@ +export const ORCA_BROWSER_GUEST_WEB_PREFERENCES = { + disableHtmlFullscreenWindowResize: true +} as const + +export const ORCA_BROWSER_GUEST_WEB_PREFERENCES_ATTRIBUTE = 'disableHtmlFullscreenWindowResize=true'