From 6825088644b26ac1f2dec0230047c0e7314fc4df Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Tue, 7 Aug 2018 13:22:04 -0700 Subject: [PATCH] feat(launcher): allow options to be passed into puppeteer.defaultArgs (#2950) --- docs/api.md | 17 ++++-- lib/Browser.js | 6 --- lib/Launcher.js | 120 ++++++++++++++++++++++------------------- lib/Puppeteer.js | 4 +- test/puppeteer.spec.js | 8 +-- 5 files changed, 85 insertions(+), 70 deletions(-) diff --git a/docs/api.md b/docs/api.md index 77bc2695..09dff1a8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -16,7 +16,7 @@ Next Release: **Aug 9, 2018** - [class: Puppeteer](#class-puppeteer) * [puppeteer.connect(options)](#puppeteerconnectoptions) * [puppeteer.createBrowserFetcher([options])](#puppeteercreatebrowserfetcheroptions) - * [puppeteer.defaultArgs()](#puppeteerdefaultargs) + * [puppeteer.defaultArgs([options])](#puppeteerdefaultargsoptions) * [puppeteer.executablePath()](#puppeteerexecutablepath) * [puppeteer.launch([options])](#puppeteerlaunchoptions) - [class: BrowserFetcher](#class-browserfetcher) @@ -378,9 +378,20 @@ This methods attaches Puppeteer to an existing Chromium instance. - `platform` <[string]> Possible values are: `mac`, `win32`, `win64`, `linux`. Defaults to the current platform. - returns: <[BrowserFetcher]> -#### puppeteer.defaultArgs() +#### puppeteer.defaultArgs([options]) +- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: + - `headless` <[boolean]> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. + - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). + - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md). + - `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`. - returns: <[Array]<[string]>> The default flags that Chromium will be launched with. +`puppeteer.defaultArgs` can be used to turn off some flags that Puppeteer usually launches Chromium with: +```js +const customArgs = puppeteer.defaultArgs().filter(arg => arg !== '--mute-audio'); +const browser = await puppeteer.launch({ignoreDefaultArgs: true, args: customArgs}); +``` + #### puppeteer.executablePath() - returns: <[string]> A path where Puppeteer expects to find bundled Chromium. Chromium might not exist there if the download was skipped with [`PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`](#environment-variables). @@ -398,7 +409,7 @@ This methods attaches Puppeteer to an existing Chromium instance. - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). - - `ignoreDefaultArgs` <[boolean]> Do not use [`puppeteer.defaultArgs()`](#puppeteerdefaultargs). Dangerous option; use with care. Defaults to `false`. + - `ignoreDefaultArgs` <[boolean]> Do not use [`puppeteer.defaultArgs()`](#puppeteerdefaultargs-options). Dangerous option; use with care. Defaults to `false`. - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. - `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`. diff --git a/lib/Browser.js b/lib/Browser.js index 3977ff30..8ef68c6a 100644 --- a/lib/Browser.js +++ b/lib/Browser.js @@ -297,9 +297,3 @@ helper.tracePublicAPI(BrowserContext); helper.tracePublicAPI(Browser); module.exports = {Browser, BrowserContext}; - -/** - * @typedef {Object} BrowserOptions - * @property {boolean=} setDefaultViewport - * @property {boolean=} ignoreHTTPSErrors - */ diff --git a/lib/Launcher.js b/lib/Launcher.js index a9ecbc5b..6f491597 100644 --- a/lib/Launcher.js +++ b/lib/Launcher.js @@ -48,9 +48,6 @@ const DEFAULT_ARGS = [ '--metrics-recording-only', '--no-first-run', '--safebrowsing-disable-auto-update', -]; - -const AUTOMATION_ARGS = [ '--enable-automation', '--password-store=basic', '--use-mock-keychain', @@ -58,52 +55,43 @@ const AUTOMATION_ARGS = [ class Launcher { /** - * @param {!(LaunchOptions & BrowserOptions)=} options + * @param {!(LaunchOptions & ChromeArgOptions & BrowserOptions)=} options * @return {!Promise} */ - static async launch(options) { - options = Object.assign({}, options || {}); + static async launch(options = {}) { + const { + ignoreDefaultArgs = false, + args = [], + dumpio = false, + executablePath = null, + pipe = false, + env = process.env, + handleSIGINT = true, + handleSIGTERM = true, + handleSIGHUP = true, + ignoreHTTPSErrors = false, + defaultViewport = {width: 800, height: 600}, + slowMo = 0, + timeout = 30000 + } = options; + + const chromeArguments = !ignoreDefaultArgs ? this.defaultArgs(options) : args; let temporaryUserDataDir = null; - const chromeArguments = []; - if (!options.ignoreDefaultArgs) - chromeArguments.push(...DEFAULT_ARGS); - if (!options.ignoreDefaultArgs) - chromeArguments.push(...AUTOMATION_ARGS); - if (!options.ignoreDefaultArgs || !chromeArguments.some(argument => argument.startsWith('--remote-debugging-'))) - chromeArguments.push(options.pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0'); - - if (!options.args || !options.args.some(arg => arg.startsWith('--user-data-dir'))) { - if (!options.userDataDir) - temporaryUserDataDir = await mkdtempAsync(CHROME_PROFILE_PATH); - - chromeArguments.push(`--user-data-dir=${options.userDataDir || temporaryUserDataDir}`); + if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-'))) + chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0'); + if (!chromeArguments.some(arg => arg.startsWith('--user-data-dir'))) { + temporaryUserDataDir = await mkdtempAsync(CHROME_PROFILE_PATH); + chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`); } - if (options.devtools === true) { - chromeArguments.push('--auto-open-devtools-for-tabs'); - options.headless = false; - } - if (typeof options.headless !== 'boolean' || options.headless) { - chromeArguments.push( - '--headless', - '--hide-scrollbars', - '--mute-audio' - ); - if (os.platform() === 'win32') - chromeArguments.push('--disable-gpu'); - } - if (Array.isArray(options.args) && options.args.every(arg => arg.startsWith('-'))) - chromeArguments.push('about:blank'); - let chromeExecutable = options.executablePath; - if (typeof chromeExecutable !== 'string') { + let chromeExecutable = executablePath; + if (!executablePath) { const browserFetcher = new BrowserFetcher(); const revisionInfo = browserFetcher.revisionInfo(ChromiumRevision); assert(revisionInfo.local, `Chromium revision is not downloaded. Run "npm install" or "yarn install"`); chromeExecutable = revisionInfo.executablePath; } - if (Array.isArray(options.args)) - chromeArguments.push(...options.args); const usePipe = chromeArguments.includes('--remote-debugging-pipe'); const stdio = usePipe ? ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe']; @@ -115,12 +103,12 @@ class Launcher { // process group, making it possible to kill child process tree with `.kill(-pid)` command. // @see https://nodejs.org/api/child_process.html#child_process_options_detached detached: process.platform !== 'win32', - env: options.env || process.env, + env, stdio } ); - if (options.dumpio) { + if (dumpio) { chromeProcess.stderr.pipe(process.stderr); chromeProcess.stdout.pipe(process.stdout); } @@ -141,22 +129,16 @@ class Launcher { }); const listeners = [ helper.addEventListener(process, 'exit', killChrome) ]; - if (options.handleSIGINT !== false) + if (handleSIGINT) listeners.push(helper.addEventListener(process, 'SIGINT', () => { killChrome(); process.exit(130); })); - if (options.handleSIGTERM !== false) + if (handleSIGTERM) listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyCloseChrome)); - if (options.handleSIGHUP !== false) + if (handleSIGHUP) listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyCloseChrome)); /** @type {?Connection} */ let connection = null; try { - const { - ignoreHTTPSErrors = false, - defaultViewport = {width: 800, height: 600}, - slowMo = 0 - } = options; if (!usePipe) { - const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000; const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout); connection = await Connection.createForWebSocket(browserWSEndpoint, slowMo); } else { @@ -228,10 +210,34 @@ class Launcher { } /** + * @param {!ChromeArgOptions=} options * @return {!Array} */ - static defaultArgs() { - return DEFAULT_ARGS.concat(AUTOMATION_ARGS); + static defaultArgs(options = {}) { + const { + devtools = false, + headless = !devtools, + args = [], + userDataDir = null + } = options; + const chromeArguments = [...DEFAULT_ARGS]; + if (userDataDir) + chromeArguments.push(`--user-data-dir=${userDataDir}`); + if (devtools) + chromeArguments.push('--auto-open-devtools-for-tabs'); + if (headless) { + chromeArguments.push( + '--headless', + '--hide-scrollbars', + '--mute-audio' + ); + if (os.platform() === 'win32') + chromeArguments.push('--disable-gpu'); + } + if (args.every(arg => arg.startsWith('-'))) + chromeArguments.push('about:blank'); + chromeArguments.push(...args); + return chromeArguments; } /** @@ -317,19 +323,23 @@ function waitForWSEndpoint(chromeProcess, timeout) { } /** - * @typedef {Object} LaunchOptions + * @typedef {Object} ChromeArgOptions * @property {boolean=} headless + * @property {Array=} args + * @property {string=} userDataDir + * @property {boolean=} devtools + */ + +/** + * @typedef {Object} LaunchOptions * @property {string=} executablePath - * @property {!Array=} args * @property {boolean=} ignoreDefaultArgs * @property {boolean=} handleSIGINT * @property {boolean=} handleSIGTERM * @property {boolean=} handleSIGHUP * @property {number=} timeout * @property {boolean=} dumpio - * @property {string=} userDataDir * @property {!Object=} env - * @property {boolean=} devtools * @property {boolean=} pipe */ diff --git a/lib/Puppeteer.js b/lib/Puppeteer.js index 9f673495..b84abcb4 100644 --- a/lib/Puppeteer.js +++ b/lib/Puppeteer.js @@ -44,8 +44,8 @@ module.exports = class { /** * @return {!Array} */ - static defaultArgs() { - return Launcher.defaultArgs(); + static defaultArgs(options) { + return Launcher.defaultArgs(options); } /** diff --git a/test/puppeteer.spec.js b/test/puppeteer.spec.js index dffd1514..8095deaa 100644 --- a/test/puppeteer.spec.js +++ b/test/puppeteer.spec.js @@ -60,7 +60,6 @@ module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBro await rmAsync(downloadsFolder); }); }); - describe('Puppeteer.launch', function() { it('should reject all promises when browser is closed', async() => { const browser = await puppeteer.launch(defaultBrowserOptions); @@ -135,8 +134,10 @@ module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBro await rmAsync(userDataDir).catch(e => {}); }); it('should return the default chrome arguments', async() => { - const args = puppeteer.defaultArgs(); - expect(args).toContain('--no-first-run'); + expect(puppeteer.defaultArgs()).toContain('--no-first-run'); + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs({headless: false})).not.toContain('--headless'); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('--user-data-dir=foo'); }); it('should dump browser process stderr', async({server}) => { const dumpioTextToLog = 'MAGIC_DUMPIO_TEST'; @@ -183,7 +184,6 @@ module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBro }); it('should support the pipe argument', async() => { const options = Object.assign({}, defaultBrowserOptions); - options.ignoreDefaultArgs = true; options.args = ['--remote-debugging-pipe'].concat(options.args); const browser = await puppeteer.launch(options); expect(browser.wsEndpoint()).toBe('');