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'