From e243d223230bb9aff441cbfb96012addf2bcbd11 Mon Sep 17 00:00:00 2001 From: Andrew Chen Wang Date: Fri, 12 Jun 2026 16:44:48 -0400 Subject: [PATCH 1/2] fix(chromium): create headed pages in background to avoid stealing focus Pass `background: true` to Target.createTarget for headed Chromium so that opening a new page does not activate the browser app and steal OS-level focus from the user. Playwright already emulates focus, so pages behave as foreground. The parameter is not supported by the headless shell or Android, where it is omitted. References: https://github.com/microsoft/playwright/issues/4822 --- .../src/server/chromium/crBrowser.ts | 5 +++- tests/library/headful.spec.ts | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 27a3409fb5c28..68fcbed0c8324 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -370,7 +370,10 @@ export class CRBrowserContext extends BrowserContext { } override async doCreateNewPage(): Promise { - const { targetId } = await this._browser._session.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId }); + // Create headful windows/tabs in the background to avoid stealing focus from the user. + // The `background` parameter is not supported by the headless shell or Android. + const background = this._browser.options.headful && this._browser.options.name !== 'clank' ? true : undefined; + const { targetId } = await this._browser._session.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId, background }); return this._browser._crPages.get(targetId)!._page; } diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index bc9707ba69a06..5e32a1d979083 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -232,6 +232,30 @@ it('Page.bringToFront should work', async ({ browser }) => { await page2.close(); }); +it('new pages should be focused and interactable', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/4822' }, +}, async ({ browser }) => { + // Chromium creates headed pages in the background to avoid stealing + // OS focus; emulated focus should make them behave as foreground. + const page1 = await browser.newPage(); + await page1.setContent(''); + const page2 = await browser.newPage(); + await page2.setContent(''); + + expect(await page1.evaluate('document.visibilityState')).toBe('visible'); + expect(await page2.evaluate('document.visibilityState')).toBe('visible'); + expect(await page1.evaluate('document.hasFocus()')).toBe(true); + expect(await page2.evaluate('document.hasFocus()')).toBe(true); + + await page1.fill('#i', 'page1'); + await page2.fill('#i', 'page2'); + expect(await page1.inputValue('#i')).toBe('page1'); + expect(await page2.inputValue('#i')).toBe('page2'); + + await page1.close(); + await page2.close(); +}); + it('should click in OOPIF', async ({ browserName, launchPersistent, server }) => { server.setRoute('/empty.html', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); From c3675b958648ca804f2783a0d5e442730ceb8906 Mon Sep 17 00:00:00 2001 From: Andrew Chen Wang Date: Fri, 12 Jun 2026 16:53:14 -0400 Subject: [PATCH 2/2] fix(chromium): make background page creation opt-in via createPagesInBackground Replace the unconditional behavior change with a new browserType.launch() option. When enabled, headed Chromium creates pages with Target.createTarget background:true so the browser window does not activate and steal OS focus. Defaults to false. --- docs/src/api/class-browsertype.md | 8 ++++++++ packages/playwright-client/types/types.d.ts | 8 ++++++++ packages/playwright-core/src/protocol/validator.ts | 2 ++ .../playwright-core/src/server/chromium/crBrowser.ts | 3 +-- packages/playwright-core/types/types.d.ts | 8 ++++++++ packages/protocol/spec/mixins.yml | 1 + packages/protocol/src/channels.d.ts | 4 ++++ tests/library/headful.spec.ts | 12 ++++++------ 8 files changed, 38 insertions(+), 8 deletions(-) diff --git a/docs/src/api/class-browsertype.md b/docs/src/api/class-browsertype.md index 9f94ff4beb68c..af59fbac3679d 100644 --- a/docs/src/api/class-browsertype.md +++ b/docs/src/api/class-browsertype.md @@ -322,6 +322,14 @@ describes some differences for Linux users. ### option: BrowserType.launch.ignoreAllDefaultArgs = %%-csharp-java-browser-option-ignorealldefaultargs-%% * since: v1.9 +### option: BrowserType.launch.createPagesInBackground +* since: v1.62 +- `createPagesInBackground` <[boolean]> + +When set to `true`, new pages are created in the background, so that the headed browser window does not activate and +steal the OS focus from the user. Pages still behave as focused ones. Use [`method: Page.bringToFront`] to activate +the browser window when needed. Currently only implemented for headed Chromium. Defaults to `false`. + ## async method: BrowserType.launchPersistentContext * since: v1.8 - returns: <[BrowserContext]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 0bff9d367c0b3..95c45e0804693 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -23283,6 +23283,14 @@ export interface LaunchOptions { */ chromiumSandbox?: boolean; + /** + * When set to `true`, new pages are created in the background, so that the headed browser window does not activate + * and steal the OS focus from the user. Pages still behave as focused ones. Use + * [page.bringToFront()](https://playwright.dev/docs/api/class-page#page-bring-to-front) to activate the browser + * window when needed. Currently only implemented for headed Chromium. Defaults to `false`. + */ + createPagesInBackground?: boolean; + /** * If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and * is deleted when browser is closed. In either case, the downloads are deleted when the browser context they were diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 4a9b428e56fc3..f3045939b35dc 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -982,6 +982,7 @@ scheme.BrowserTypeLaunchParams = tObject({ tracesDir: tOptional(tString), artifactsDir: tOptional(tString), chromiumSandbox: tOptional(tBoolean), + createPagesInBackground: tOptional(tBoolean), firefoxUserPrefs: tOptional(tAny), cdpPort: tOptional(tInt), slowMo: tOptional(tFloat), @@ -1011,6 +1012,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({ tracesDir: tOptional(tString), artifactsDir: tOptional(tString), chromiumSandbox: tOptional(tBoolean), + createPagesInBackground: tOptional(tBoolean), firefoxUserPrefs: tOptional(tAny), cdpPort: tOptional(tInt), noDefaultViewport: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 68fcbed0c8324..12c2955552e17 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -370,9 +370,8 @@ export class CRBrowserContext extends BrowserContext { } override async doCreateNewPage(): Promise { - // Create headful windows/tabs in the background to avoid stealing focus from the user. // The `background` parameter is not supported by the headless shell or Android. - const background = this._browser.options.headful && this._browser.options.name !== 'clank' ? true : undefined; + const background = this._browser.options.originalLaunchOptions?.createPagesInBackground && this._browser.options.headful && this._browser.options.name !== 'clank' ? true : undefined; const { targetId } = await this._browser._session.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId, background }); return this._browser._crPages.get(targetId)!._page; } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 0bff9d367c0b3..95c45e0804693 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -23283,6 +23283,14 @@ export interface LaunchOptions { */ chromiumSandbox?: boolean; + /** + * When set to `true`, new pages are created in the background, so that the headed browser window does not activate + * and steal the OS focus from the user. Pages still behave as focused ones. Use + * [page.bringToFront()](https://playwright.dev/docs/api/class-page#page-bring-to-front) to activate the browser + * window when needed. Currently only implemented for headed Chromium. Defaults to `false`. + */ + createPagesInBackground?: boolean; + /** * If specified, accepted downloads are downloaded into this directory. Otherwise, temporary directory is created and * is deleted when browser is closed. In either case, the downloads are deleted when the browser context they were diff --git a/packages/protocol/spec/mixins.yml b/packages/protocol/spec/mixins.yml index c950b7f2b68ac..8acc43f9bdb75 100644 --- a/packages/protocol/spec/mixins.yml +++ b/packages/protocol/spec/mixins.yml @@ -92,6 +92,7 @@ LaunchOptions: tracesDir: string? artifactsDir: string? chromiumSandbox: boolean? + createPagesInBackground: boolean? firefoxUserPrefs: json? cdpPort: int? diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index ddec4af4c34a1..d716810ab5c2b 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1838,6 +1838,7 @@ export type BrowserTypeLaunchParams = { tracesDir?: string, artifactsDir?: string, chromiumSandbox?: boolean, + createPagesInBackground?: boolean, firefoxUserPrefs?: any, cdpPort?: number, slowMo?: number, @@ -1863,6 +1864,7 @@ export type BrowserTypeLaunchOptions = { tracesDir?: string, artifactsDir?: string, chromiumSandbox?: boolean, + createPagesInBackground?: boolean, firefoxUserPrefs?: any, cdpPort?: number, slowMo?: number, @@ -1892,6 +1894,7 @@ export type BrowserTypeLaunchPersistentContextParams = { tracesDir?: string, artifactsDir?: string, chromiumSandbox?: boolean, + createPagesInBackground?: boolean, firefoxUserPrefs?: any, cdpPort?: number, noDefaultViewport?: boolean, @@ -1980,6 +1983,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { tracesDir?: string, artifactsDir?: string, chromiumSandbox?: boolean, + createPagesInBackground?: boolean, firefoxUserPrefs?: any, cdpPort?: number, noDefaultViewport?: boolean, diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index 5e32a1d979083..1efffe7732599 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -232,11 +232,12 @@ it('Page.bringToFront should work', async ({ browser }) => { await page2.close(); }); -it('new pages should be focused and interactable', { +it('pages created in background should be focused and interactable', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/4822' }, -}, async ({ browser }) => { - // Chromium creates headed pages in the background to avoid stealing - // OS focus; emulated focus should make them behave as foreground. +}, async ({ browserType }) => { + // With createPagesInBackground, pages do not activate the browser window; + // emulated focus should make them behave as foreground. + const browser = await browserType.launch({ createPagesInBackground: true }); const page1 = await browser.newPage(); await page1.setContent(''); const page2 = await browser.newPage(); @@ -252,8 +253,7 @@ it('new pages should be focused and interactable', { expect(await page1.inputValue('#i')).toBe('page1'); expect(await page2.inputValue('#i')).toBe('page2'); - await page1.close(); - await page2.close(); + await browser.close(); }); it('should click in OOPIF', async ({ browserName, launchPersistent, server }) => {