diff --git a/src/main/index.ts b/src/main/index.ts index 3f076c2e908..f91a531ce7b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -119,6 +119,7 @@ import { configureElectronNetworkCompatibility, configureDevUserDataPath, configureOrcaUserDataPathEnv, + disableUnsupportedChromiumFeatures, enableMainProcessGpuFeatures, installDevParentDisconnectQuit, installDevParentSignalQuit, @@ -846,6 +847,7 @@ if (hasSingleInstanceLock) { platform: process.platform, ...getMainProcessLifecycleIdentity() }) + disableUnsupportedChromiumFeatures() configureElectronNetworkCompatibility() enableRendererHeapHeadroom() maybeApplyGpuFallbackForThisLaunch() diff --git a/src/main/startup/configure-process.test.ts b/src/main/startup/configure-process.test.ts index eaf9f42aeec..54ecdc61424 100644 --- a/src/main/startup/configure-process.test.ts +++ b/src/main/startup/configure-process.test.ts @@ -396,6 +396,46 @@ describe('configureElectronNetworkCompatibility', () => { }) }) +describe('disableUnsupportedChromiumFeatures', () => { + it('disables FedCM before Chromium sessions are created', async () => { + const { app } = await import('electron') + const { disableUnsupportedChromiumFeatures } = await import('./configure-process') + + vi.mocked(app.commandLine.appendSwitch).mockClear() + disableUnsupportedChromiumFeatures() + + expect(app.commandLine.appendSwitch).toHaveBeenCalledWith('disable-features', 'FedCm') + }) + + it('preserves existing disabled Chromium features', async () => { + const { app } = await import('electron') + const { disableUnsupportedChromiumFeatures } = await import('./configure-process') + + vi.mocked(app.commandLine.getSwitchValue).mockReturnValueOnce('ExistingFeature') + vi.mocked(app.commandLine.appendSwitch).mockClear() + disableUnsupportedChromiumFeatures() + + expect(app.commandLine.appendSwitch).toHaveBeenCalledWith( + 'disable-features', + 'FedCm,ExistingFeature' + ) + }) + + it('does not duplicate FedCM when disable-features already includes it', async () => { + const { app } = await import('electron') + const { disableUnsupportedChromiumFeatures } = await import('./configure-process') + + vi.mocked(app.commandLine.getSwitchValue).mockReturnValueOnce('FedCm,ExistingFeature') + vi.mocked(app.commandLine.appendSwitch).mockClear() + disableUnsupportedChromiumFeatures() + + expect(app.commandLine.appendSwitch).toHaveBeenCalledWith( + 'disable-features', + 'FedCm,ExistingFeature' + ) + }) +}) + describe('enableMainProcessGpuFeatures', () => { const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') const originalE2EUserDataDir = process.env.ORCA_E2E_USER_DATA_DIR diff --git a/src/main/startup/configure-process.ts b/src/main/startup/configure-process.ts index e545d89da9c..e39963bc9f2 100644 --- a/src/main/startup/configure-process.ts +++ b/src/main/startup/configure-process.ts @@ -66,6 +66,20 @@ export function configureElectronNetworkCompatibility( app.commandLine.appendSwitch('disable-http2') } +export function disableUnsupportedChromiumFeatures(): void { + appendDisabledChromiumFeatures(['FedCm']) +} + +function appendDisabledChromiumFeatures(features: string[]): void { + const existingFeatures = app.commandLine + .getSwitchValue('disable-features') + .split(',') + .map((feature) => feature.trim()) + .filter(Boolean) + const disabledFeatures = Array.from(new Set([...features, ...existingFeatures])).join(',') + app.commandLine.appendSwitch('disable-features', disabledFeatures) +} + function getProcessPathDelimiter(): string { return process.platform === 'win32' ? ';' : ':' } @@ -300,11 +314,7 @@ export function enableMainProcessGpuFeatures(): void { app.commandLine.appendSwitch('enable-features', features) } - const existingDisabledFeatures = app.commandLine.getSwitchValue('disable-features') // Why: IntensiveWakeUpThrottling clamps hidden-page timers to 1/min after 5min, delaying agent-done/bell notifications ~60s. // This opt-out is skipped under GPU fallback (win32-only today); if throttling ever reaches Windows it must move out of this path. - const disabledFeatures = ['IntensiveWakeUpThrottling', existingDisabledFeatures] - .filter(Boolean) - .join(',') - app.commandLine.appendSwitch('disable-features', disabledFeatures) + appendDisabledChromiumFeatures(['IntensiveWakeUpThrottling']) } diff --git a/tests/e2e/browser-fedcm-fallback.spec.ts b/tests/e2e/browser-fedcm-fallback.spec.ts new file mode 100644 index 00000000000..c782bfed5a6 --- /dev/null +++ b/tests/e2e/browser-fedcm-fallback.spec.ts @@ -0,0 +1,139 @@ +import { createServer, type Server } from 'node:http' +import type { AddressInfo } from 'node:net' +import { test, expect } from './helpers/orca-app' +import { ensureTerminalVisible, getActiveWorktreeId } from './helpers/store' + +async function closeServer(server: Server): Promise { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())) + }) +} + +async function startFedCmFallbackServer(): Promise<{ + url: string + close: () => Promise +}> { + const server = createServer((request, response) => { + const origin = `http://127.0.0.1:${(server.address() as AddressInfo).port}` + const pathname = new URL(request.url ?? '/', origin).pathname + response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) + if (pathname === '/popup') { + response.end( + 'Popup fallbackPopup fallback' + ) + return + } + response.end(` + + + FedCM fallback oracle + + + + pending + + + + `) + }) + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + const port = (server.address() as AddressInfo).port + return { url: `http://127.0.0.1:${port}/`, close: () => closeServer(server) } +} + +test('embedded browser omits unusable FedCM and reaches the popup fallback', async ({ + electronApp, + orcaPage +}) => { + const server = await startFedCmFallbackServer() + try { + await ensureTerminalVisible(orcaPage) + const worktreeId = await getActiveWorktreeId(orcaPage) + expect(worktreeId).not.toBeNull() + const browserTabId = await orcaPage.evaluate( + ({ targetWorktreeId, url }) => { + const tab = window.__store!.getState().createBrowserTab(targetWorktreeId!, url, { + title: 'FedCM fallback oracle', + activate: true + }) + return tab.id + }, + { targetWorktreeId: worktreeId, url: server.url } + ) + const readGuest = async (expression: string): Promise => + orcaPage.evaluate( + async ({ targetBrowserTabId, script }) => { + const slot = document.querySelector( + `[data-browser-overlay-tab-id="${targetBrowserTabId}"]` + ) + const webview = slot?.querySelector('webview') as Electron.WebviewTag | null + if (!webview) { + throw new Error(`Missing webview for browser tab ${targetBrowserTabId}`) + } + return (await webview.executeJavaScript(script)) as T + }, + { targetBrowserTabId: browserTabId, script: expression } + ) + + await expect + .poll(() => readGuest('document.title'), { timeout: 10_000 }) + .toBe('FedCM fallback oracle') + const capabilities = await readGuest<{ + hasIdentityCredential: boolean + hasIdentityProvider: boolean + }>('JSON.parse(document.querySelector("#capabilities").textContent)') + expect.soft(capabilities).toEqual({ + hasIdentityCredential: false, + hasIdentityProvider: false + }) + + await readGuest('document.querySelector("#sign-in").click()') + const path = await readGuest('document.querySelector("#path").textContent') + expect.soft(path).toBe('popup-live') + await expect + .poll(() => + electronApp.evaluate(async ({ webContents }) => { + const popup = webContents.getAllWebContents().find((contents) => { + return contents.getURL().endsWith('/popup') + }) + if (!popup) { + return null + } + return { + openerLive: await popup.executeJavaScript('Boolean(window.opener)'), + title: await popup.executeJavaScript('document.title'), + url: popup.getURL() + } + }) + ) + .toEqual({ + openerLive: true, + title: 'Popup fallback', + url: `${server.url}popup` + }) + await expect + .poll(() => readGuest('window.popupMessages')) + .toEqual(['popup-opener-live']) + } finally { + await server.close() + } +})