diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts
index 75cdfc39d89e6..7568299f36494 100644
--- a/packages/playwright-core/src/server/chromium/chromium.ts
+++ b/packages/playwright-core/src/server/chromium/chromium.ts
@@ -112,7 +112,17 @@ 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,
+ // When attaching to an externally-launched browser via CDP, the user
+ // (not Playwright) owns the browser. Do not send
+ // Browser.setDownloadBehavior, which would hijack the real browser's
+ // download folder globally — including for tabs the user opened
+ // manually outside of automation. Callers that need automation-owned
+ // downloads can still opt in by passing acceptDownloads explicitly
+ // on a new BrowserContext.
+ acceptDownloads: 'internal-browser-default',
+ };
const browserOptions: BrowserOptions = {
slowMo: options.slowMo,
name: 'chromium',
diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts
index ffad00656f89b..b2f17284821d5 100644
--- a/tests/library/chromium/connect-over-cdp.spec.ts
+++ b/tests/library/chromium/connect-over-cdp.spec.ts
@@ -89,7 +89,11 @@ test('should cleanup artifacts dir after connectOverCDP disconnects due to ws cl
expect(exists2).toBe(false);
});
-test('should connectOverCDP and manage downloads in default context', async ({ browserType, mode, server }, testInfo) => {
+test('should connectOverCDP and manage downloads in an opted-in context', async ({ browserType, mode, server }, testInfo) => {
+ // Downloads from Playwright-created contexts still work normally when the caller opts
+ // in via `newContext({ acceptDownloads: true })`. The default context (shared with the
+ // attached browser) no longer intercepts downloads by default, to preserve the real
+ // user's download behavior — see test below for that guarantee.
server.setRoute('/downloadWithFilename', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment; filename=file.txt');
@@ -105,7 +109,8 @@ test('should connectOverCDP and manage downloads in default context', async ({ b
const browser = await browserType.connectOverCDP({
endpointURL: `http://127.0.0.1:${port}/`,
});
- const page = await browser.contexts()[0].newPage();
+ const context = await browser.newContext({ acceptDownloads: true });
+ const page = await context.newPage();
await page.setContent(`download`);
const [download] = await Promise.all([
@@ -120,6 +125,47 @@ test('should connectOverCDP and manage downloads in default context', async ({ b
await download.saveAs(userPath);
expect(fs.existsSync(userPath)).toBeTruthy();
expect(fs.readFileSync(userPath).toString()).toBe('Hello world');
+ await context.close();
+ } finally {
+ await browserServer.close();
+ }
+});
+
+test('should not override browser-default download behavior on the default context', async ({ browserType, mode, server }, testInfo) => {
+ // Regression guard: connectOverCDP must NOT send Browser.setDownloadBehavior for the
+ // default context it inherits from the attached browser. Previously it did, which
+ // globally hijacked the real user's download folder (downloads landed in a Playwright
+ // temp dir with UUID filenames — see chromium.ts comment in _connectOverCDPImpl).
+ server.setRoute('/downloadWithFilename', (req, res) => {
+ res.setHeader('Content-Type', 'application/octet-stream');
+ res.setHeader('Content-Disposition', 'attachment; filename=file.txt');
+ res.end(`Hello world`);
+ });
+
+ 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}/`,
+ });
+ const defaultContext = browser.contexts()[0];
+ const page = await defaultContext.newPage();
+ await page.setContent(`download`);
+
+ // Playwright must NOT emit a 'download' event on the default context after the fix,
+ // because the default context's acceptDownloads is 'internal-browser-default' and
+ // Browser.setDownloadBehavior is never sent with eventsEnabled=true for it.
+ let sawDownloadEvent = false;
+ page.on('download', () => { sawDownloadEvent = true; });
+
+ await page.click('a');
+ // Give Chrome and Playwright a moment to deliver the event if it were going to fire.
+ await page.waitForTimeout(500);
+
+ expect(sawDownloadEvent).toBe(false);
} finally {
await browserServer.close();
}