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(); }