diff --git a/docs/api.md b/docs/api.md index 63100c3d144b3..52700f0097b1c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -516,6 +516,7 @@ Clears all registered handlers. - `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on. - `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Puppeteer to use. - `product` <[string]> Possible values are: `chrome`, `firefox`. Defaults to `chrome`. + - `targetFilter` |[boolean]> Use this function to decide if Puppeteer should connect to the given target. If a `targetFilter` is provided, Puppeteer only connects to targets for which `targetFilter` returns `true`. By default, Puppeteer connects to all available targets. - returns: <[Promise]<[Browser]>> This methods attaches Puppeteer to an existing browser instance. @@ -623,6 +624,7 @@ try { - `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`. - `pipe` <[boolean]> Connects to the browser over a pipe instead of a WebSocket. Defaults to `false`. - `extraPrefsFirefox` <[Object]> Additional [preferences](https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/Preference_reference) that can be passed to Firefox (see `PUPPETEER_PRODUCT`) + - `targetFilter` |[boolean]> Use this function to decide if Puppeteer should connect to the given target. If a `targetFilter` is provided, Puppeteer only connects to targets for which `targetFilter` returns `true`. By default, Puppeteer connects to all available targets. - returns: <[Promise]<[Browser]>> Promise which resolves to browser instance. You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments: diff --git a/src/common/Browser.ts b/src/common/Browser.ts index 5d1d142400474..d4acb4cf24702 100644 --- a/src/common/Browser.ts +++ b/src/common/Browser.ts @@ -29,6 +29,13 @@ import { Viewport } from './PuppeteerViewport.js'; */ export type BrowserCloseCallback = () => Promise | void; +/** + * @public + */ +export type TargetFilterCallback = ( + target: Protocol.Target.TargetInfo +) => Promise | boolean; + const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map< Permission, Protocol.Browser.PermissionType @@ -189,7 +196,8 @@ export class Browser extends EventEmitter { ignoreHTTPSErrors: boolean, defaultViewport?: Viewport | null, process?: ChildProcess, - closeCallback?: BrowserCloseCallback + closeCallback?: BrowserCloseCallback, + targetFilterCallback?: TargetFilterCallback ): Promise { const browser = new Browser( connection, @@ -197,7 +205,8 @@ export class Browser extends EventEmitter { ignoreHTTPSErrors, defaultViewport, process, - closeCallback + closeCallback, + targetFilterCallback ); await connection.send('Target.setDiscoverTargets', { discover: true }); return browser; @@ -207,6 +216,7 @@ export class Browser extends EventEmitter { private _process?: ChildProcess; private _connection: Connection; private _closeCallback: BrowserCloseCallback; + private _targetFilterCallback: TargetFilterCallback; private _defaultContext: BrowserContext; private _contexts: Map; /** @@ -224,7 +234,8 @@ export class Browser extends EventEmitter { ignoreHTTPSErrors: boolean, defaultViewport?: Viewport | null, process?: ChildProcess, - closeCallback?: BrowserCloseCallback + closeCallback?: BrowserCloseCallback, + targetFilterCallback?: TargetFilterCallback ) { super(); this._ignoreHTTPSErrors = ignoreHTTPSErrors; @@ -232,6 +243,7 @@ export class Browser extends EventEmitter { this._process = process; this._connection = connection; this._closeCallback = closeCallback || function (): void {}; + this._targetFilterCallback = targetFilterCallback || ((): boolean => true); this._defaultContext = new BrowserContext(this._connection, this, null); this._contexts = new Map(); @@ -330,6 +342,11 @@ export class Browser extends EventEmitter { ? this._contexts.get(browserContextId) : this._defaultContext; + const shouldAttachToTarget = await this._targetFilterCallback(targetInfo); + if (!shouldAttachToTarget) { + return; + } + const target = new Target( targetInfo, context, diff --git a/src/common/BrowserConnector.ts b/src/common/BrowserConnector.ts index 1f8963bae95a4..58cdf4ce6ace0 100644 --- a/src/common/BrowserConnector.ts +++ b/src/common/BrowserConnector.ts @@ -15,7 +15,7 @@ */ import { ConnectionTransport } from './ConnectionTransport.js'; -import { Browser } from './Browser.js'; +import { Browser, TargetFilterCallback } from './Browser.js'; import { assert } from './assert.js'; import { debugError } from '../common/helper.js'; import { Connection } from './Connection.js'; @@ -43,6 +43,10 @@ export interface BrowserConnectOptions { * aid debugging. */ slowMo?: number; + /** + * Callback to decide if Puppeteer should connect to a given target or not. + */ + targetFilter?: TargetFilterCallback; } const getWebSocketTransportClass = async () => { @@ -71,6 +75,7 @@ export const connectToBrowser = async ( defaultViewport = { width: 800, height: 600 }, transport, slowMo = 0, + targetFilter, } = options; assert( @@ -106,7 +111,8 @@ export const connectToBrowser = async ( ignoreHTTPSErrors, defaultViewport, null, - () => connection.send('Browser.close').catch(debugError) + () => connection.send('Browser.close').catch(debugError), + targetFilter ); }; diff --git a/test/launcher.spec.ts b/test/launcher.spec.ts index 864cebdaa0c7c..c302db28c6b2f 100644 --- a/test/launcher.spec.ts +++ b/test/launcher.spec.ts @@ -18,6 +18,7 @@ import os from 'os'; import path from 'path'; import sinon from 'sinon'; import { promisify } from 'util'; +import Protocol from 'devtools-protocol'; import { getTestState, itFailsFirefox, @@ -538,6 +539,35 @@ describe('Launcher specs', function () { await page.close(); await browser.close(); }); + it('should support targetFilter option', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const page1 = await originalBrowser.newPage(); + await page1.goto(server.EMPTY_PAGE); + + const page2 = await originalBrowser.newPage(); + await page2.goto(server.EMPTY_PAGE + '?should-be-ignored'); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + targetFilter: (targetInfo: Protocol.Target.TargetInfo) => + !targetInfo.url.includes('should-be-ignored'), + }); + + const pages = await browser.pages(); + + await page2.close(); + await page1.close(); + await browser.close(); + + expect(pages.map((p: Page) => p.url()).sort()).toEqual([ + 'about:blank', + server.EMPTY_PAGE, + ]); + }); itFailsFirefox( 'should be able to reconnect to a disconnected browser', async () => {