From c7d65c6156c902b9b0d11c7513a5f36edd166853 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 9 Oct 2026 08:06:15 -0700 Subject: [PATCH 1/2] fix(chromium): do not drop windows that have not navigated while connecting When connecting, pages without a renderer are probed with Inspector.enable and dropped as crashed or discarded. An Electron window that has not navigated yet has no renderer either, so a packaged app, which creates its windows while Playwright is still connecting, lost its first window. Only probe pages that have a url. Fixes: https://github.com/microsoft/playwright/issues/43247 --- .../playwright-core/src/server/chromium/crBrowser.ts | 5 ++++- packages/playwright-core/src/server/chromium/crPage.ts | 10 +++++----- tests/electron/electron-app.spec.ts | 10 ++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 2bf9475749369..91d631a4b6393 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -197,7 +197,10 @@ export class CRBrowser extends Browser { if (targetInfo.type === 'page' || treatOtherAsPage) { const opener = targetInfo.openerId ? this._crPages.get(targetInfo.openerId) || null : null; - const crPage = new CRPage(session, targetInfo.targetId, context, opener, { hasUIWindow: targetInfo.type === 'page' }); + // A page that has never navigated, e.g. an Electron window before its first loadURL(), + // has no renderer yet either, but it is not crashed and initializes once it navigates. + const mayHaveLostRenderer = this._isConnecting && !!targetInfo.url; + const crPage = new CRPage(session, targetInfo.targetId, context, opener, { hasUIWindow: targetInfo.type === 'page', mayHaveLostRenderer }); this._crPages.set(targetInfo.targetId, crPage); return; } diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 95c650e37da1f..8f13c080304ca 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -92,7 +92,7 @@ export class CRPage implements PageDelegate { return crPage._mainFrameSession; } - constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, bits: { hasUIWindow: boolean }) { + constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, bits: { hasUIWindow: boolean, mayHaveLostRenderer: boolean }) { this._targetId = targetId; this._opener = opener; const dragManager = new DragManager(this); @@ -122,7 +122,7 @@ export class CRPage implements PageDelegate { this._page.setEmulatedSizeFromWindowOpen({ viewport: viewportSize, screen: viewportSize }); } - this._mainFrameSession._initialize(bits.hasUIWindow).then( + this._mainFrameSession._initialize(bits.hasUIWindow, bits.mayHaveLostRenderer).then( () => this._page.reportAsNew(this._opener?._page, undefined), error => this._page.reportAsNew(this._opener?._page, error)); } @@ -469,7 +469,7 @@ class FrameSession { ]); } - async _initialize(hasUIWindow: boolean) { + async _initialize(hasUIWindow: boolean, mayHaveLostRenderer: boolean) { const browserOptions = this._crPage._browserContext._browser.options; if (!this._page.isStorageStatePage && hasUIWindow && !this._crPage._browserContext._browser.isClank() && @@ -576,7 +576,7 @@ class FrameSession { for (const initScript of this._crPage._page.allInitScripts()) promises.push(this._evaluateOnNewDocument(initScript, 'main', true /* runImmediately */)); } - if (this._isMainFrame() && this._crPage._browserContext._browser._isConnecting) { + if (mayHaveLostRenderer) { // An existing page without a renderer, e.g. crashed or discarded, never responds to the commands above. // Get notified with Inspector.targetCrashed right away, so that such a page is reported as closed, // and we do not stall while connecting to the browser. @@ -783,7 +783,7 @@ class FrameSession { } const frameSession = new FrameSession(this._crPage, session, targetId, this); this._crPage._sessions.set(targetId, frameSession); - frameSession._initialize(false).catch(e => e); + frameSession._initialize(false, false).catch(e => e); return; } diff --git a/tests/electron/electron-app.spec.ts b/tests/electron/electron-app.spec.ts index adbca5e7fe162..05749b971ea2d 100644 --- a/tests/electron/electron-app.spec.ts +++ b/tests/electron/electron-app.spec.ts @@ -334,6 +334,16 @@ test('should be able to get the first window when with a delayed navigation', as await expect(page.locator('h1')).toHaveText('Foobar'); }); +test('should be able to get the first window with a delayed navigation when launched via executablePath', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/43247' }, +}, async ({ launchElectronApp }) => { + // Like a packaged app: without our loader, the window is created while Playwright is still connecting. + const app = await launchElectronApp('electron-window-app-delayed-loadURL.js', [], { executablePath: require('electron/index.js') }); + const page = await app.firstWindow(); + await expect(page).toHaveURL('data:text/html,

Foobar

'); + await expect(page.locator('h1')).toHaveText('Foobar'); +}); + test('should detach debugger on app-initiated exit', async ({ launchElectronApp }) => { const electronApp = await launchElectronApp('electron-app.js'); const closePromise = new Promise(f => electronApp.process().on('close', f)); From a6ef516ddfc560b5fbceed7be44a6c0c27a210e3 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 9 Oct 2026 09:24:48 -0700 Subject: [PATCH 2/2] fix(electron): do not probe windows for a renderer while connecting Instead of checking the target url, do not send Inspector.enable to Electron windows. An Electron window that has not navigated yet has no renderer, but it is not crashed. --- packages/playwright-core/src/server/browser.ts | 2 ++ .../playwright-core/src/server/chromium/crBrowser.ts | 5 +---- packages/playwright-core/src/server/chromium/crPage.ts | 10 +++++----- .../playwright-core/src/server/electron/electron.ts | 2 ++ 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index b2446893519c1..a1fea9f2dfc78 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -66,6 +66,8 @@ export type BrowserOptions = { userDataDir?: string; noDefaults?: boolean; isWebView?: boolean; + // Do not send Inspector.enable to detect pages without a renderer while connecting. + noInspectorEnableOnConnect?: boolean; }; export abstract class Browser extends SdkObject { diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 91d631a4b6393..2bf9475749369 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -197,10 +197,7 @@ export class CRBrowser extends Browser { if (targetInfo.type === 'page' || treatOtherAsPage) { const opener = targetInfo.openerId ? this._crPages.get(targetInfo.openerId) || null : null; - // A page that has never navigated, e.g. an Electron window before its first loadURL(), - // has no renderer yet either, but it is not crashed and initializes once it navigates. - const mayHaveLostRenderer = this._isConnecting && !!targetInfo.url; - const crPage = new CRPage(session, targetInfo.targetId, context, opener, { hasUIWindow: targetInfo.type === 'page', mayHaveLostRenderer }); + const crPage = new CRPage(session, targetInfo.targetId, context, opener, { hasUIWindow: targetInfo.type === 'page' }); this._crPages.set(targetInfo.targetId, crPage); return; } diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 8f13c080304ca..1510bd07df3a0 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -92,7 +92,7 @@ export class CRPage implements PageDelegate { return crPage._mainFrameSession; } - constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, bits: { hasUIWindow: boolean, mayHaveLostRenderer: boolean }) { + constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, bits: { hasUIWindow: boolean }) { this._targetId = targetId; this._opener = opener; const dragManager = new DragManager(this); @@ -122,7 +122,7 @@ export class CRPage implements PageDelegate { this._page.setEmulatedSizeFromWindowOpen({ viewport: viewportSize, screen: viewportSize }); } - this._mainFrameSession._initialize(bits.hasUIWindow, bits.mayHaveLostRenderer).then( + this._mainFrameSession._initialize(bits.hasUIWindow).then( () => this._page.reportAsNew(this._opener?._page, undefined), error => this._page.reportAsNew(this._opener?._page, error)); } @@ -469,7 +469,7 @@ class FrameSession { ]); } - async _initialize(hasUIWindow: boolean, mayHaveLostRenderer: boolean) { + async _initialize(hasUIWindow: boolean) { const browserOptions = this._crPage._browserContext._browser.options; if (!this._page.isStorageStatePage && hasUIWindow && !this._crPage._browserContext._browser.isClank() && @@ -576,7 +576,7 @@ class FrameSession { for (const initScript of this._crPage._page.allInitScripts()) promises.push(this._evaluateOnNewDocument(initScript, 'main', true /* runImmediately */)); } - if (mayHaveLostRenderer) { + if (this._isMainFrame() && this._crPage._browserContext._browser._isConnecting && !browserOptions.noInspectorEnableOnConnect) { // An existing page without a renderer, e.g. crashed or discarded, never responds to the commands above. // Get notified with Inspector.targetCrashed right away, so that such a page is reported as closed, // and we do not stall while connecting to the browser. @@ -783,7 +783,7 @@ class FrameSession { } const frameSession = new FrameSession(this._crPage, session, targetId, this); this._crPage._sessions.set(targetId, frameSession); - frameSession._initialize(false, false).catch(e => e); + frameSession._initialize(false).catch(e => e); return; } diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index 7e3d3b01b15bf..a13e4f42f2287 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -307,6 +307,8 @@ export class Electron extends SdkObject { downloadsPath: artifactsDir, tracesDir: options.tracesDir || artifactsDir, originalLaunchOptions: {}, + // A window that has not navigated yet has no renderer, but it is not crashed. + noInspectorEnableOnConnect: true, }; validateBrowserContextOptions(contextOptions, browserOptions); const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions));