From 6300979e56de0ac74666edeee14a22586036a8d5 Mon Sep 17 00:00:00 2001 From: Sid Sarasvati Date: Mon, 13 Apr 2026 14:26:25 -0400 Subject: [PATCH 1/2] feat(connectOverCDP): add noDefaults option to skip browser overrides Closes #40158 --- docs/src/api/class-browsertype.md | 11 ++++ packages/playwright-client/types/types.d.ts | 10 ++++ .../playwright-core/src/client/browserType.ts | 1 + .../playwright-core/src/protocol/validator.ts | 1 + .../playwright-core/src/server/browser.ts | 1 + .../playwright-core/src/server/browserType.ts | 2 +- .../src/server/chromium/chromium.ts | 12 ++-- .../src/server/chromium/crPage.ts | 7 ++- packages/playwright-core/types/types.d.ts | 10 ++++ packages/protocol/src/channels.d.ts | 2 + packages/protocol/src/protocol.yml | 1 + .../library/chromium/connect-over-cdp.spec.ts | 58 +++++++++++++++++++ 12 files changed, 109 insertions(+), 7 deletions(-) diff --git a/docs/src/api/class-browsertype.md b/docs/src/api/class-browsertype.md index bc7d51d86521a..14f95594856ce 100644 --- a/docs/src/api/class-browsertype.md +++ b/docs/src/api/class-browsertype.md @@ -224,6 +224,17 @@ can see what is going on. Defaults to 0. Maximum time in milliseconds to wait for the connection to be established. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. +### option: BrowserType.connectOverCDP.noDefaults +* since: v1.53 +- `noDefaults` <[boolean]> + +When true, Playwright will not send default overrides to the browser on the default context. This includes +`Browser.setDownloadBehavior`, `Emulation.setFocusEmulationEnabled`, and `Emulation.setEmulatedMedia`. +Useful when attaching to a user's daily-driver browser where these overrides would interfere with +existing browser state. New contexts created via +[browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) +are not affected. Defaults to `false`. + ## async method: BrowserType.connectToWorker * since: v1.60 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index a8cd0cf0dc87a..a408e74f59b09 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -22597,6 +22597,16 @@ export interface ConnectOverCDPOptions { * `0` to disable timeout. */ timeout?: number; + + /** + * When true, Playwright will not send default overrides to the browser on the default context. This includes + * `Browser.setDownloadBehavior`, `Emulation.setFocusEmulationEnabled`, and `Emulation.setEmulatedMedia`. Useful when + * attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New + * contexts created via + * [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) are not + * affected. Defaults to `false`. + */ + noDefaults?: boolean; } export interface ConnectOptions { diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 0f06b4438d755..026fb9dc77df9 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -156,6 +156,7 @@ export class BrowserType extends ChannelOwner imple slowMo: params.slowMo, timeout: new TimeoutSettings(this._platform).timeout(params), isLocal: params.isLocal, + noDefaults: params.noDefaults, }); const browser = Browser.from(result.browser); browser._connectToBrowserType(this, {}, undefined); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index d090e28ad459f..aa891b0f66085 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -632,6 +632,7 @@ scheme.BrowserTypeConnectOverCDPParams = tObject({ slowMo: tOptional(tFloat), timeout: tFloat, isLocal: tOptional(tBoolean), + noDefaults: tOptional(tBoolean), }); scheme.BrowserTypeConnectOverCDPResult = tObject({ browser: tChannel(['Browser']), diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index bdd7dfe8e7aad..60fb4313c64ea 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -63,6 +63,7 @@ export type BrowserOptions = { sdkLanguage?: Language; originalLaunchOptions: types.LaunchOptions; userDataDir?: string; + noDefaults?: boolean; }; export abstract class Browser extends SdkObject { diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index c5126c0f76b14..3515c8b934925 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -286,7 +286,7 @@ export abstract class BrowserType extends SdkObject { } } - async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, timeout?: number, headers?: types.HeadersArray, isLocal?: boolean }): Promise { + async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, timeout?: number, headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean }): Promise { throw new Error('CDP connections are only supported by Chromium'); } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index 15aa8f02bff88..e9dc0416d3fc5 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -80,11 +80,11 @@ export class Chromium extends BrowserType { return super.launchPersistentContext(progress, userDataDir, options); } - override async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray, isLocal?: boolean }) { + override async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean }) { return await this._connectOverCDPInternal(progress, endpointURL, options); } - async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray, isLocal?: boolean }, onClose?: () => Promise) { + async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean }, onClose?: () => Promise) { let headersMap: { [key: string]: string; } | undefined; if (options.headers) headersMap = headersArrayToObject(options.headers, false); @@ -112,7 +112,7 @@ export class Chromium extends BrowserType { return this._connectOverCDPImpl(progress, chromeTransport, closeAndWait, options, onClose); } - private async _connectOverCDPImpl(progress: Progress, transport: ConnectionTransport, closeAndWait: () => Promise, options: types.LaunchOptions & { isLocal?: boolean }, onClose?: () => Promise) { + private async _connectOverCDPImpl(progress: Progress, transport: ConnectionTransport, closeAndWait: () => Promise, options: types.LaunchOptions & { isLocal?: boolean, noDefaults?: boolean }, onClose?: () => Promise) { const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER)); const doCleanup = async () => { await removeFolders([artifactsDir]); @@ -128,7 +128,10 @@ export class Chromium extends BrowserType { try { const browserProcess: BrowserProcess = { close: doClose, kill: doClose }; - const persistent: types.BrowserContextOptions = { noDefaultViewport: true }; + const persistent: types.BrowserContextOptions = { + noDefaultViewport: true, + ...(options.noDefaults ? { acceptDownloads: 'internal-browser-default' as const } : {}), + }; const browserOptions: BrowserOptions = { slowMo: options.slowMo, name: 'chromium', @@ -141,6 +144,7 @@ export class Chromium extends BrowserType { downloadsPath: options.downloadsPath || artifactsDir, tracesDir: options.tracesDir || artifactsDir, originalLaunchOptions: {}, + noDefaults: options.noDefaults, }; validateBrowserContextOptions(persistent, browserOptions); const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, transport, browserOptions)); diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index f82592d62b422..3693957aa51f8 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -512,9 +512,11 @@ class FrameSession { this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }), ]; if (!this._page.isStorageStatePage) { + const skipDefaultOverrides = this._crPage._browserContext._browser.options.noDefaults && + this._crPage._browserContext === this._crPage._browserContext._browser._defaultContext; if (this._crPage._browserContext.needsPlaywrightBinding()) promises.push(this.exposePlaywrightBinding()); - if (this._isMainFrame()) + if (this._isMainFrame() && !skipDefaultOverrides) promises.push(this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true })); const options = this._crPage._browserContext._options; if (options.bypassCSP) @@ -536,7 +538,8 @@ class FrameSession { if (!this._crPage._browserContext._browser.options.headful) promises.push(this._setDefaultFontFamilies(this._client)); promises.push(this._updateGeolocation(true)); - promises.push(this._updateEmulateMedia()); + if (!skipDefaultOverrides) + promises.push(this._updateEmulateMedia()); promises.push(this._updateFileChooserInterception(true)); for (const initScript of this._crPage._page.allInitScripts()) promises.push(this._evaluateOnNewDocument(initScript, 'main', true /* runImmediately */)); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index a8cd0cf0dc87a..a408e74f59b09 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -22597,6 +22597,16 @@ export interface ConnectOverCDPOptions { * `0` to disable timeout. */ timeout?: number; + + /** + * When true, Playwright will not send default overrides to the browser on the default context. This includes + * `Browser.setDownloadBehavior`, `Emulation.setFocusEmulationEnabled`, and `Emulation.setEmulatedMedia`. Useful when + * attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New + * contexts created via + * [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) are not + * affected. Defaults to `false`. + */ + noDefaults?: boolean; } export interface ConnectOptions { diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 4f0613005cfa1..1a05f1375fce0 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1133,11 +1133,13 @@ export type BrowserTypeConnectOverCDPParams = { slowMo?: number, timeout: number, isLocal?: boolean, + noDefaults?: boolean, }; export type BrowserTypeConnectOverCDPOptions = { headers?: NameValue[], slowMo?: number, isLocal?: boolean, + noDefaults?: boolean, }; export type BrowserTypeConnectOverCDPResult = { browser: BrowserChannel, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 0cdaf38f6dcf6..798ce86c1c449 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1030,6 +1030,7 @@ BrowserType: slowMo: float? timeout: float isLocal: boolean? + noDefaults: boolean? returns: browser: Browser defaultContext: BrowserContext? diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts index 2becc9d0fdf85..77a18ca6520c6 100644 --- a/tests/library/chromium/connect-over-cdp.spec.ts +++ b/tests/library/chromium/connect-over-cdp.spec.ts @@ -618,3 +618,61 @@ test('should get title and URL of existing page', async ({ browserType, mode, se await browserServer.close(); } }); + +test('should skip default overrides with noDefaults', async ({ browserType, mode, server }, testInfo) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/40158' }); + const port = 9339 + testInfo.workerIndex; + const browserServer = await browserType.launch({ + args: ['--remote-debugging-port=' + port] + }); + try { + const browser = await browserType.connectOverCDP({ + endpointURL: `http://127.0.0.1:${port}/`, + noDefaults: true, + }); + const defaultContext = browser.contexts()[0]; + const page = await defaultContext.newPage(); + + // Browser.setDownloadBehavior was not sent, so no Playwright download event fires. + server.setRoute('/download', (req, res) => { + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Disposition', 'attachment; filename=file.txt'); + res.end('Hello world'); + }); + await page.setContent(`download`); + let sawDownload = false; + page.on('download', () => { sawDownload = true; }); + await page.click('a'); + await page.waitForTimeout(500); + expect(sawDownload).toBe(false); + + await browser.close(); + } finally { + await browserServer.close(); + } +}); + +test('noDefaults should not affect new contexts', async ({ browserType, mode, server }, testInfo) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/40158' }); + const port = 9339 + testInfo.workerIndex; + const browserServer = await browserType.launch({ + args: ['--remote-debugging-port=' + port] + }); + try { + const browser = await browserType.connectOverCDP({ + endpointURL: `http://127.0.0.1:${port}/`, + noDefaults: true, + }); + + // New contexts should still get normal Playwright defaults. + const newContext = await browser.newContext(); + const page = await newContext.newPage(); + const hasFocus = await page.evaluate(() => document.hasFocus()); + expect(hasFocus).toBe(true); + + await newContext.close(); + await browser.close(); + } finally { + await browserServer.close(); + } +}); From e6aafce0260b0fbd0ad64df202645e18053af182 Mon Sep 17 00:00:00 2001 From: Sid Sarasvati Date: Mon, 20 Apr 2026 10:56:24 -0400 Subject: [PATCH 2/2] chore: regenerate types.d.ts --- packages/playwright-client/types/types.d.ts | 20 ++++++++++---------- packages/playwright-core/types/types.d.ts | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index a408e74f59b09..48fc69eb713ca 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -22586,6 +22586,16 @@ export interface ConnectOverCDPOptions { */ isLocal?: boolean; + /** + * When true, Playwright will not send default overrides to the browser on the default context. This includes + * `Browser.setDownloadBehavior`, `Emulation.setFocusEmulationEnabled`, and `Emulation.setEmulatedMedia`. Useful when + * attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New + * contexts created via + * [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) are not + * affected. Defaults to `false`. + */ + noDefaults?: boolean; + /** * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going * on. Defaults to 0. @@ -22597,16 +22607,6 @@ export interface ConnectOverCDPOptions { * `0` to disable timeout. */ timeout?: number; - - /** - * When true, Playwright will not send default overrides to the browser on the default context. This includes - * `Browser.setDownloadBehavior`, `Emulation.setFocusEmulationEnabled`, and `Emulation.setEmulatedMedia`. Useful when - * attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New - * contexts created via - * [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) are not - * affected. Defaults to `false`. - */ - noDefaults?: boolean; } export interface ConnectOptions { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index a408e74f59b09..48fc69eb713ca 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -22586,6 +22586,16 @@ export interface ConnectOverCDPOptions { */ isLocal?: boolean; + /** + * When true, Playwright will not send default overrides to the browser on the default context. This includes + * `Browser.setDownloadBehavior`, `Emulation.setFocusEmulationEnabled`, and `Emulation.setEmulatedMedia`. Useful when + * attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New + * contexts created via + * [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) are not + * affected. Defaults to `false`. + */ + noDefaults?: boolean; + /** * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going * on. Defaults to 0. @@ -22597,16 +22607,6 @@ export interface ConnectOverCDPOptions { * `0` to disable timeout. */ timeout?: number; - - /** - * When true, Playwright will not send default overrides to the browser on the default context. This includes - * `Browser.setDownloadBehavior`, `Emulation.setFocusEmulationEnabled`, and `Emulation.setEmulatedMedia`. Useful when - * attaching to a user's daily-driver browser where these overrides would interfere with existing browser state. New - * contexts created via - * [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) are not - * affected. Defaults to `false`. - */ - noDefaults?: boolean; } export interface ConnectOptions {