Bug 1590467 - Updated vendored puppeteer with proposed browser selection; r=remote-protocol-reviewers,ato
authorMaja Frydrychowicz <mjzffr@gmail.com>
Fri, 08 Nov 2019 11:52:18 +0000
changeset 501241 278f81464fc09a3b91654c9720b08997f614fcfe
parent 501240 dacb2051e47e4664bfa38a5015f37121627725d9
child 501242 e2ceb42ddbbff20868afb21dfee7be3ea89ea134
push id114168
push userdluca@mozilla.com
push dateSun, 10 Nov 2019 03:08:55 +0000
treeherdermozilla-inbound@33f64c1ef3e4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersremote-protocol-reviewers, ato
bugs1590467
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1590467 - Updated vendored puppeteer with proposed browser selection; r=remote-protocol-reviewers,ato Differential Revision: https://phabricator.services.mozilla.com/D52313
remote/test/puppeteer/.cirrus.yml
remote/test/puppeteer/.npmignore
remote/test/puppeteer/.travis.yml
remote/test/puppeteer/CONTRIBUTING.md
remote/test/puppeteer/docs/api.md
remote/test/puppeteer/experimental/puppeteer-firefox/.cirrus.yml
remote/test/puppeteer/lib/Launcher.js
remote/test/puppeteer/lib/Puppeteer.js
remote/test/puppeteer/lib/helper.js
remote/test/puppeteer/moz.yaml
remote/test/puppeteer/package.json
remote/test/puppeteer/test/puppeteer.spec.js
remote/test/puppeteer/test/test.js
remote/test/puppeteer/utils/protocol-types-generator/index.js
--- a/remote/test/puppeteer/.cirrus.yml
+++ b/remote/test/puppeteer/.cirrus.yml
@@ -19,22 +19,22 @@ task:
   install_script: npm install --unsafe-perm
   lint_script: npm run lint
   coverage_script: npm run coverage
   test_doclint_script: npm run test-doclint
   test_types_script: npm run test-types
 
 task:
   matrix:
-    - name: Firefox (node8 + linux)
+    - name: Firefox Juggler (node8 + linux)
       container:
         dockerfile: .ci/node8/Dockerfile.linux
       xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24
   install_script: npm install --unsafe-perm && cd experimental/puppeteer-firefox && npm install --unsafe-perm
-  test_script: npm run funit
+  test_script: npm run fjunit
 
 task:
   osx_instance:
     image: high-sierra-base
   name: Chromium (node8 + macOS)
   env:
     HOMEBREW_NO_AUTO_UPDATE: 1
   node_install_script:
--- a/remote/test/puppeteer/.npmignore
+++ b/remote/test/puppeteer/.npmignore
@@ -1,18 +1,20 @@
 .appveyor.yml
 .gitattributes
 
+# no longer generated, but old checkouts might still have it
+node6
+
 # exclude all tests
 test
 utils/node6-transform
 
 # exclude internal type definition files
 /lib/*.d.ts
-/node6/lib/*.d.ts
 
 # repeats from .gitignore
 node_modules
 .local-chromium
 .dev_profile*
 .DS_Store
 *.swp
 *.pyc
--- a/remote/test/puppeteer/.travis.yml
+++ b/remote/test/puppeteer/.travis.yml
@@ -14,17 +14,17 @@ cache:
 before_install:
   - "sysctl kernel.unprivileged_userns_clone=1"
   - "export DISPLAY=:99.0"
   - "sh -e /etc/init.d/xvfb start"
 script:
   - 'if [ "$NODE8" = "true" ]; then npm run lint; fi'
   - 'if [ "$NODE8" = "true" ]; then npm run coverage; fi'
   - 'if [ "$FIREFOX" = "true" ]; then cd experimental/puppeteer-firefox && npm i && cd ../..; fi'
-  - 'if [ "$FIREFOX" = "true" ]; then npm run funit; fi'
+  - 'if [ "$FIREFOX" = "true" ]; then npm run fjunit; fi'
   - 'if [ "$NODE8" = "true" ]; then npm run test-doclint; fi'
   - 'if [ "$NODE8" = "true" ]; then npm run test-types; fi'
   - 'if [ "$NODE8" = "true" ]; then npm run bundle; fi'
   - 'if [ "$NODE8" = "true" ]; then npm run unit-bundle; fi'
 jobs:
   include:
     - node_js: "8.16.0"
       env:
--- a/remote/test/puppeteer/CONTRIBUTING.md
+++ b/remote/test/puppeteer/CONTRIBUTING.md
@@ -194,28 +194,35 @@ npm run unit -- --break-on-failure
 ```
 
 - To run tests in non-headless mode:
 
 ```bash
 HEADLESS=false npm run unit
 ```
 
-- To run tests with custom Chromium executable:
+- To run tests with custom browser executable:
 
 ```bash
-CHROME=<path-to-executable> npm run unit
+BINARY=<path-to-executable> npm run unit
 ```
 
 - To run tests in slow-mode:
 
 ```bash
 HEADLESS=false SLOW_MO=500 npm run unit
 ```
 
+- To run tests with additional Launcher options:
+
+```bash
+EXTRA_LAUNCH_OPTIONS='{"args": ["--user-data-dir=some/path"], "handleSIGINT": true}' npm run unit
+```
+
+
 - To debug a test, "focus" a test first and then run:
 
 ```bash
 node --inspect-brk test/test.js
 ```
 
 ## Public API Coverage
 
@@ -233,37 +240,38 @@ See [Debugging Tips](README.md#debugging
 
 # For Project Maintainers
 
 ## Releasing to npm
 
 Releasing to npm consists of the following phases:
 
 1. Source Code: mark a release.
-    1. Bump `package.json` version following the SEMVER rules, run `npm run doc` to update the docs accordingly, and send a PR titled `'chore: mark version vXXX.YYY.ZZZ'` ([example](https://github.com/GoogleChrome/puppeteer/commit/808bf8e5582482a1d849ff22a51e52024810905c)).
-    2. Make sure the PR passes **all checks**.
+    1. Bump `package.json` version following the SEMVER rules.
+    2. Run `npm run doc` to update the docs accordingly.
+    3. Update the “Releases per Chromium Version” list in [`docs/api.md`](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md) to include the new version.
+    4. Send a PR titled `'chore: mark version vXXX.YYY.ZZZ'` ([example](https://github.com/GoogleChrome/puppeteer/pull/5078)).
+    5. Make sure the PR passes **all checks**.
         - **WHY**: there are linters in place that help to avoid unnecessary errors, e.g. [like this](https://github.com/GoogleChrome/puppeteer/pull/2446)
-    3. Merge the PR.
-    4. Once merged, publish the release notes using [GitHub's "draft new release tag" option](https://github.com/GoogleChrome/puppeteer/releases/new).
+    6. Merge the PR.
+    7. Once merged, publish the release notes using [GitHub's “draft new release tag” option](https://github.com/GoogleChrome/puppeteer/releases/new).
         - **NOTE**: tag names are prefixed with `'v'`, e.g. for version `1.4.0` the tag is `v1.4.0`.
-        - For the "raw notes" section, use `git log --pretty="%h - %s" v1.19.0..HEAD`.
-    5. Update the “Releases per Chromium Version” list in [`docs/api.md`](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md) to include the new version.
+        - For the “raw notes” section, use `git log --pretty="%h - %s" v2.0.0..HEAD`.
 2. Publish `puppeteer` to npm.
     1. On your local machine, pull from [upstream](https://github.com/GoogleChrome/puppeteer) and make sure the last commit is the one just merged.
     2. Run `git status` and make sure there are no untracked files.
         - **WHY**: this is to avoid adding unnecessary files to the npm package.
     3. Run [`npx pkgfiles`](https://www.npmjs.com/package/pkgfiles) to make sure you don't publish anything unnecessary.
     4. Run `npm publish`. This publishes the `puppeteer` package.
 3. Publish `puppeteer-core` to npm.
     1. Run `./utils/prepare_puppeteer_core.js`. The script changes the name inside `package.json` to `puppeteer-core`.
     2. Run `npm publish`. This publishes the `puppeteer-core` package.
     3. Run `git reset --hard` to reset the changes to `package.json`.
 4. Source Code: mark post-release.
-    1. Bump `package.json` version to `-post` version and send a PR titled `'chore: bump version to vXXX.YYY.ZZZ-post'` ([example](https://github.com/GoogleChrome/puppeteer/commit/d02440d1eac98028e29f4e1cf55413062a259156))
-        - **NOTE**: make sure to update the "released APIs" section in the top of `docs/api.md` by running `npm run doc`.
+    1. Bump `package.json` version to `-post` version, run `npm run doc` to update the “released APIs” section at the top of `docs/api.md` accordingly, and send a PR titled `'chore: bump version to vXXX.YYY.ZZZ-post'` ([example](https://github.com/GoogleChrome/puppeteer/commit/d02440d1eac98028e29f4e1cf55413062a259156))
         - **NOTE**: no other commits should be landed in-between release commit and bump commit.
 
 ## Updating npm dist tags
 
 For both `puppeteer` and `puppeteer-core` we maintain the following npm tags:
 
 - `chrome-*` tags, e.g. `chrome-75` and so on. These tags match the Puppeteer version that corresponds to the `chrome-*` release.
 - `chrome-stable` tag. This tag points to the Puppeteer version that works with the current Chrome stable release.
--- a/remote/test/puppeteer/docs/api.md
+++ b/remote/test/puppeteer/docs/api.md
@@ -26,16 +26,17 @@
 - [class: Puppeteer](#class-puppeteer)
   * [puppeteer.connect(options)](#puppeteerconnectoptions)
   * [puppeteer.createBrowserFetcher([options])](#puppeteercreatebrowserfetcheroptions)
   * [puppeteer.defaultArgs([options])](#puppeteerdefaultargsoptions)
   * [puppeteer.devices](#puppeteerdevices)
   * [puppeteer.errors](#puppeteererrors)
   * [puppeteer.executablePath()](#puppeteerexecutablepath)
   * [puppeteer.launch([options])](#puppeteerlaunchoptions)
+  * [puppeteer.product](#puppeteerproduct)
 - [class: BrowserFetcher](#class-browserfetcher)
   * [browserFetcher.canDownload(revision)](#browserfetchercandownloadrevision)
   * [browserFetcher.download(revision[, progressCallback])](#browserfetcherdownloadrevision-progresscallback)
   * [browserFetcher.localRevisions()](#browserfetcherlocalrevisions)
   * [browserFetcher.platform()](#browserfetcherplatform)
   * [browserFetcher.remove(revision)](#browserfetcherremoverevision)
   * [browserFetcher.revisionInfo(revision)](#browserfetcherrevisioninforevision)
 - [class: Browser](#class-browser)
@@ -383,16 +384,17 @@ You will then need to call [`puppeteer.c
 Puppeteer looks for certain [environment variables](https://en.wikipedia.org/wiki/Environment_variable) to aid its operations.
 If Puppeteer doesn't find them in the environment during the installation step, a lowercased variant of these variables will be used from the [npm config](https://docs.npmjs.com/cli/config).
 
 - `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` - defines HTTP proxy settings that are used to download and run Chromium.
 - `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD` - do not download bundled Chromium during installation step.
 - `PUPPETEER_DOWNLOAD_HOST` - overwrite URL prefix that is used to download Chromium. Note: this includes protocol and might even include path prefix. Defaults to `https://storage.googleapis.com`.
 - `PUPPETEER_CHROMIUM_REVISION` - specify a certain version of Chromium you'd like Puppeteer to use. See [puppeteer.launch([options])](#puppeteerlaunchoptions) on how executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/GoogleChrome/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk.
 - `PUPPETEER_EXECUTABLE_PATH` - specify an executable path to be used in `puppeteer.launch`. See [puppeteer.launch([options])](#puppeteerlaunchoptions) on how the executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/GoogleChrome/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk.
+- `PUPPETEER_PRODUCT` - specify which browser you'd like Puppeteer to use. Must be one of `chrome` or `firefox`. This is exposed in [`puppeteer.product`](#puppeteerproduct)
 
 > **NOTE** PUPPETEER_* env variables are not accounted for in the [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) package.
 
 
 ### Working with Chrome Extensions
 
 Puppeteer can be used for testing Chrome Extensions.
 
@@ -423,22 +425,23 @@ const puppeteer = require('puppeteer');
 
 ### class: Puppeteer
 
 Puppeteer module provides a method to launch a Chromium instance.
 The following is a typical example of using Puppeteer to drive automation:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.goto('https://www.google.com');
   // other actions...
   await browser.close();
-});
+})();
 ```
 
 #### puppeteer.connect(options)
 - `options` <[Object]>
   - `browserWSEndpoint` <?[string]> a [browser websocket endpoint](#browserwsendpoint) to connect to.
   - `browserURL` <?[string]> a browser url to connect to, in format `http://${host}:${port}`. Use interchangeably with `browserWSEndpoint` to let Puppeteer fetch it from [metadata endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target).
   - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
   - `defaultViewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport.
@@ -476,23 +479,24 @@ The default flags that Chromium will be 
 
 Returns a list of devices to be used with [`page.emulate(options)`](#pageemulateoptions). Actual list of
 devices can be found in [lib/DeviceDescriptors.js](https://github.com/GoogleChrome/puppeteer/blob/master/lib/DeviceDescriptors.js).
 
 ```js
 const puppeteer = require('puppeteer');
 const iPhone = puppeteer.devices['iPhone 6'];
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.emulate(iPhone);
   await page.goto('https://www.google.com');
   // other actions...
   await browser.close();
-});
+})();
 ```
 
 > **NOTE** The old way (Puppeteer versions <= v1.14.0) devices can be obtained with `require('puppeteer/DeviceDescriptors')`.
 
 #### puppeteer.errors
 - returns: <[Object]>
   - `TimeoutError` <[function]> A class of [TimeoutError].
 
@@ -520,36 +524,37 @@ try {
 
 > **NOTE** `puppeteer.executablePath()` is affected by the `PUPPETEER_EXECUTABLE_PATH` and `PUPPETEER_CHROMIUM_REVISION` env variables. See [Environment Variables](#environment-variables) for details.
 
 
 #### puppeteer.launch([options])
 - `options` <[Object]>  Set of configurable options to set on the browser. Can have the following fields:
   - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
   - `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`.
-  - `executablePath` <[string]> Path to a Chromium or Chrome executable to run instead of the bundled Chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/GoogleChrome/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk.
+  - `executablePath` <[string]> Path to a browser executable to run instead of the bundled Chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/GoogleChrome/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk.
   - `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on.
   - `defaultViewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport.
     - `width` <[number]> page width in pixels.
     - `height` <[number]> page height in pixels.
     - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`.
     - `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account. Defaults to `false`.
     - `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]|[Array]<[string]>> If `true`, then do not use [`puppeteer.defaultArgs()`](#puppeteerdefaultargs-options). If an array is given, then filter out the given default arguments. Dangerous option; use with care. 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/), and here is the list of [Firefox flags](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options).
+  - `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, then do not use [`puppeteer.defaultArgs()`](#puppeteerdefaultargsoptions). If an array is given, then filter out the given default arguments. 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`.
   - `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
   - `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
   - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md).
   - `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
   - `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`.
+  - `extraPrefs` <[Object]> Additional [preferences](https://developer.mozilla.org/en-US/docs/Mozilla/Preferences/Preference_reference) that can be passed to Firefox (see `PUPPETEER_PRODUCT`)
 - returns: <[Promise]<[Browser]>> Promise which resolves to browser instance.
 
 
 You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments:
 ```js
 const browser = await puppeteer.launch({
   ignoreDefaultArgs: ['--mute-audio']
 });
@@ -558,16 +563,22 @@ const browser = await puppeteer.launch({
 > **NOTE** Puppeteer can also be used to control the Chrome browser, but it works best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use `executablePath` option with extreme caution.
 >
 > If Google Chrome (rather than Chromium) is preferred, a [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build is suggested.
 >
 > In [puppeteer.launch([options])](#puppeteerlaunchoptions) above, any mention of Chromium also applies to Chrome.
 >
 > See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users.
 
+#### puppeteer.product
+- returns: <[string]> returns the name of the browser that is under automation ("chrome" or "firefox")
+
+The product is set by the `PUPPETEER_PRODUCT` environment variable and defaults to `chrome`. Firefox support is experimental.
+
+
 ### class: BrowserFetcher
 
 BrowserFetcher can download and manage different versions of Chromium.
 
 BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from [omahaproxy.appspot.com](http://omahaproxy.appspot.com/).
 
 An example of using BrowserFetcher to download a specific version of Chromium and running
 Puppeteer against it:
@@ -625,38 +636,40 @@ The method initiates a GET request to do
 * extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
 
 A Browser is created when Puppeteer connects to a Chromium instance, either through [`puppeteer.launch`](#puppeteerlaunchoptions) or [`puppeteer.connect`](#puppeteerconnectoptions).
 
 An example of using a [Browser] to create a [Page]:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.goto('https://example.com');
   await browser.close();
-});
+})();
 ```
 
 An example of disconnecting from and reconnecting to a [Browser]:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   // Store the endpoint to be able to reconnect to Chromium
   const browserWSEndpoint = browser.wsEndpoint();
   // Disconnect puppeteer from Chromium
   browser.disconnect();
 
   // Use the endpoint to reestablish a connection
   const browser2 = await puppeteer.connect({browserWSEndpoint});
   // Close Chromium
   await browser2.close();
-});
+})();
 ```
 #### event: 'disconnected'
 Emitted when Puppeteer gets disconnected from the Chromium instance. This might happen because of one of the following:
 - Chromium is closed or crashed
 - The [`browser.disconnect`](#browserdisconnect) method was called
 
 #### event: 'targetchanged'
 - <[Target]>
@@ -692,23 +705,25 @@ a single instance of [BrowserContext].
 Closes Chromium and all of its pages (if any were opened). The [Browser] object itself is considered to be disposed and cannot be used anymore.
 
 #### browser.createIncognitoBrowserContext()
 - returns: <[Promise]<[BrowserContext]>>
 
 Creates a new incognito browser context. This won't share cookies/cache with other browser contexts.
 
 ```js
-const browser = await puppeteer.launch();
-// Create a new incognito browser context.
-const context = await browser.createIncognitoBrowserContext();
-// Create a new page in a pristine context.
-const page = await context.newPage();
-// Do stuff
-await page.goto('https://example.com');
+(async () => {
+  const browser = await puppeteer.launch();
+  // Create a new incognito browser context.
+  const context = await browser.createIncognitoBrowserContext();
+  // Create a new page in a pristine context.
+  const page = await context.newPage();
+  // Do stuff
+  await page.goto('https://example.com');
+})();
 ```
 
 #### browser.defaultBrowserContext()
 - returns: <[BrowserContext]>
 
 Returns the default browser context. The default browser context can not be closed.
 
 #### browser.disconnect()
@@ -913,22 +928,23 @@ const newWindowTarget = await browserCon
 * extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
 
 Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium. One [Browser] instance might have multiple [Page] instances.
 
 This example creates a page, navigates it to a URL, and then saves a screenshot:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.goto('https://example.com');
   await page.screenshot({path: 'screenshot.png'});
   await browser.close();
-});
+})();
 ```
 
 The Page class emits various events (described below) which can be handled using any of Node's native [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) methods, such as `on`, `once` or `removeListener`.
 
 This example logs a message for a single page `load` event:
 ```js
 page.once('load', () => console.log('Page loaded!'));
 ```
@@ -1264,23 +1280,24 @@ Emulates given device metrics and user a
 To aid emulation, puppeteer provides a list of device descriptors which can be obtained via the [`puppeteer.devices`](#puppeteerdevices).
 
 `page.emulate` will resize the page. A lot of websites don't expect phones to change size, so you should emulate before navigating to the page.
 
 ```js
 const puppeteer = require('puppeteer');
 const iPhone = puppeteer.devices['iPhone 6'];
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.emulate(iPhone);
   await page.goto('https://www.google.com');
   // other actions...
   await browser.close();
-});
+})();
 ```
 
 List of all available devices is available in the source code: [DeviceDescriptors.js](https://github.com/GoogleChrome/puppeteer/blob/master/lib/DeviceDescriptors.js).
 
 #### page.emulateMedia(type)
 - `type` <?[string]> Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` disables CSS media emulation.
 - returns: <[Promise]>
 
@@ -1447,39 +1464,41 @@ If the `puppeteerFunction` returns a [Pr
 
 > **NOTE** Functions installed via `page.exposeFunction` survive navigations.
 
 An example of adding an `md5` function into the page:
 ```js
 const puppeteer = require('puppeteer');
 const crypto = require('crypto');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   page.on('console', msg => console.log(msg.text()));
   await page.exposeFunction('md5', text =>
     crypto.createHash('md5').update(text).digest('hex')
   );
   await page.evaluate(async () => {
     // use window.md5 to compute hashes
     const myString = 'PUPPETEER';
     const myHash = await window.md5(myString);
     console.log(`md5 of ${myString} is ${myHash}`);
   });
   await browser.close();
-});
+})();
 ```
 
 An example of adding a `window.readfile` function into the page:
 
 ```js
 const puppeteer = require('puppeteer');
 const fs = require('fs');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   page.on('console', msg => console.log(msg.text()));
   await page.exposeFunction('readfile', async filePath => {
     return new Promise((resolve, reject) => {
       fs.readFile(filePath, 'utf8', (err, text) => {
         if (err)
           reject(err);
         else
@@ -1488,18 +1507,17 @@ puppeteer.launch().then(async browser =>
     });
   });
   await page.evaluate(async () => {
     // use window.readfile to read contents of a file
     const content = await window.readfile('/etc/hosts');
     console.log(content);
   });
   await browser.close();
-});
-
+})();
 ```
 
 #### page.focus(selector)
 - `selector` <[string]> A [selector] of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
 - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully focused. The promise will be rejected if there is no element matching `selector`.
 
 This method fetches an element with `selector` and focuses it.
 If there's no element matching `selector`, the method throws an error.
@@ -1857,28 +1875,29 @@ Activating request interception enables 
 `request.respond` methods.  This provides the capability to modify network requests that are made by a page.
 
 Once request interception is enabled, every request will stall unless it's continued, responded or aborted.
 An example of a naïve request interceptor that aborts all image requests:
 
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.setRequestInterception(true);
   page.on('request', interceptedRequest => {
     if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg'))
       interceptedRequest.abort();
     else
       interceptedRequest.continue();
   });
   await page.goto('https://example.com');
   await browser.close();
-});
+})();
 ```
 
 > **NOTE** Enabling request interception disables page caching.
 
 #### page.setUserAgent(userAgent)
 - `userAgent` <[string]> Specific user agent to use in this page
 - returns: <[Promise]> Promise which resolves when the user agent is set.
 
@@ -2031,23 +2050,24 @@ await fileChooser.accept(['/tmp/myfile.p
   - `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
 - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to  `pageFunction`
 - returns: <[Promise]<[JSHandle]>> Promise which resolves when the `pageFunction` returns a truthy value. It resolves to a JSHandle of the truthy value.
 
 The `waitForFunction` can be used to observe viewport size change:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   const watchDog = page.waitForFunction('window.innerWidth < 100');
   await page.setViewport({width: 50, height: 50});
   await watchDog;
   await browser.close();
-});
+})();
 ```
 
 To pass arguments from node.js to the predicate of `page.waitForFunction` function:
 
 ```js
 const selector = '.foo';
 await page.waitForFunction(selector => !!document.querySelector(selector), {}, selector);
 ```
@@ -2113,26 +2133,28 @@ return finalResponse.ok();
 Wait for the `selector` to appear in page. If at the moment of calling
 the method the `selector` already exists, the method will return
 immediately. If the selector doesn't appear after the `timeout` milliseconds of waiting, the function will throw.
 
 This method works across navigations:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   let currentURL;
   page
     .waitForSelector('img')
     .then(() => console.log('First URL with image: ' + currentURL));
-  for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
+  for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) {
     await page.goto(currentURL);
+  }
   await browser.close();
-});
+})();
 ```
 Shortcut for [page.mainFrame().waitForSelector(selector[, options])](#framewaitforselectorselector-options).
 
 #### page.waitForXPath(xpath[, options])
 - `xpath` <[string]> A [xpath] of an element to wait for
 - `options` <[Object]> Optional waiting parameters
   - `visible` <[boolean]> wait for element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
   - `hidden` <[boolean]> wait for element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
@@ -2142,26 +2164,28 @@ Shortcut for [page.mainFrame().waitForSe
 Wait for the `xpath` to appear in page. If at the moment of calling
 the method the `xpath` already exists, the method will return
 immediately. If the xpath doesn't appear after the `timeout` milliseconds of waiting, the function will throw.
 
 This method works across navigations:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   let currentURL;
   page
     .waitForXPath('//img')
     .then(() => console.log('First URL with image: ' + currentURL));
-  for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
+  for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) {
     await page.goto(currentURL);
+  }
   await browser.close();
-});
+})();
 ```
 Shortcut for [page.mainFrame().waitForXPath(xpath[, options])](#framewaitforxpathxpath-options).
 
 #### page.workers()
 - returns: <[Array]<[Worker]>>
 This method returns all of the dedicated [WebWorkers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) associated with the page.
 
 > **NOTE** This does not contain ServiceWorkers
@@ -2209,17 +2233,17 @@ Shortcut for [(await worker.executionCon
 - returns: <[string]>
 
 ### class: Accessibility
 
 The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by assistive technology such as [screen readers](https://en.wikipedia.org/wiki/Screen_reader) or [switches](https://en.wikipedia.org/wiki/Switch_access).
 
 Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might have wildly different output.
 
-Blink - Chrome's rendering engine - has a concept of "accessibility tree", which is than translated into different platform-specific APIs. Accessibility namespace gives users
+Blink - Chrome's rendering engine - has a concept of "accessibility tree", which is then translated into different platform-specific APIs. Accessibility namespace gives users
 access to the Blink Accessibility Tree.
 
 Most of the accessibility tree gets filtered out when converting from Blink AX Tree to Platform-specific AX-Tree or by assistive technologies themselves. By default, Puppeteer tries to approximate this filtering, exposing only the "interesting" nodes of the tree.
 
 
 
 #### accessibility.snapshot([options])
 - `options` <[Object]>
@@ -2493,25 +2517,26 @@ Closes the file chooser without selectin
 ### class: Dialog
 
 [Dialog] objects are dispatched by page via the ['dialog'](#event-dialog) event.
 
 An example of using `Dialog` class:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   page.on('dialog', async dialog => {
     console.log(dialog.message());
     await dialog.dismiss();
     await browser.close();
   });
   page.evaluate(() => alert('1'));
-});
+})();
 ```
 
 #### dialog.accept([promptText])
 - `promptText` <[string]> A text to enter in prompt. Does not cause any effects if the dialog's `type` is not prompt.
 - returns: <[Promise]> Promise which resolves when the dialog has been accepted.
 
 #### dialog.defaultValue()
 - returns: <[string]> If dialog is prompt, returns default prompt value. Otherwise, returns empty string.
@@ -2555,28 +2580,30 @@ At every point of time, page exposes its
 - ['framenavigated'](#event-framenavigated) - fired when the frame commits navigation to a different URL.
 - ['framedetached'](#event-framedetached) - fired when the frame gets detached from the page.  A Frame can be detached from the page only once.
 
 An example of dumping frame tree:
 
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.goto('https://www.google.com/chrome/browser/canary.html');
   dumpFrameTree(page.mainFrame(), '');
   await browser.close();
 
   function dumpFrameTree(frame, indent) {
     console.log(indent + frame.url());
-    for (let child of frame.childFrames())
+    for (const child of frame.childFrames()) {
       dumpFrameTree(child, indent + '  ');
+    }
   }
-});
+})();
 ```
 
 An example of getting text from an iframe element:
 
 ```js
   const frame = page.frames().find(frame => frame.name() === 'myframe');
   const text = await frame.$eval('.selector', element => element.textContent);
   console.log(text);
@@ -2892,23 +2919,24 @@ await page.waitFor(selector => !!documen
   - `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
 - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to  `pageFunction`
 - returns: <[Promise]<[JSHandle]>> Promise which resolves when the `pageFunction` returns a truthy value. It resolves to a JSHandle of the truthy value.
 
 The `waitForFunction` can be used to observe viewport size change:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   const watchDog = page.mainFrame().waitForFunction('window.innerWidth < 100');
   page.setViewport({width: 50, height: 50});
   await watchDog;
   await browser.close();
-});
+})();
 ```
 
 To pass arguments from node.js to the predicate of `page.waitForFunction` function:
 
 ```js
 const selector = '.foo';
 await page.waitForFunction(selector => !!document.querySelector(selector), {}, selector);
 ```
@@ -2947,26 +2975,28 @@ const [response] = await Promise.all([
 Wait for the `selector` to appear in page. If at the moment of calling
 the method the `selector` already exists, the method will return
 immediately. If the selector doesn't appear after the `timeout` milliseconds of waiting, the function will throw.
 
 This method works across navigations:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   let currentURL;
   page.mainFrame()
     .waitForSelector('img')
     .then(() => console.log('First URL with image: ' + currentURL));
-  for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
+  for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) {
     await page.goto(currentURL);
+  }
   await browser.close();
-});
+})();
 ```
 
 #### frame.waitForXPath(xpath[, options])
 - `xpath` <[string]> A [xpath] of an element to wait for
 - `options` <[Object]> Optional waiting parameters
   - `visible` <[boolean]> wait for element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
   - `hidden` <[boolean]> wait for element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
   - `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
@@ -2975,26 +3005,28 @@ puppeteer.launch().then(async browser =>
 Wait for the `xpath` to appear in page. If at the moment of calling
 the method the `xpath` already exists, the method will return
 immediately. If the xpath doesn't appear after the `timeout` milliseconds of waiting, the function will throw.
 
 This method works across navigations:
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   let currentURL;
   page.mainFrame()
     .waitForXPath('//img')
     .then(() => console.log('First URL with image: ' + currentURL));
-  for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com'])
+  for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) {
     await page.goto(currentURL);
+  }
   await browser.close();
-});
+})();
 ```
 
 ### class: ExecutionContext
 
 The class represents a context for JavaScript execution. A [Page] might have many execution contexts:
 - each [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) has "default" execution context that is
   always created after frame is attached to DOM. This context is returned by the [`frame.executionContext()`](#frameexecutioncontext) method.
 - [Extensions](https://developer.chrome.com/extensions)'s content scripts create additional execution contexts.
@@ -3174,23 +3206,24 @@ function, it **will not be called**.
 ### class: ElementHandle
 * extends: [JSHandle]
 
 ElementHandle represents an in-page DOM element. ElementHandles can be created with the [page.$](#pageselector) method.
 
 ```js
 const puppeteer = require('puppeteer');
 
-puppeteer.launch().then(async browser => {
+(async () => {
+  const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.goto('https://example.com');
   const hrefElement = await page.$('a');
   await hrefElement.click();
   // ...
-});
+})();
 ```
 
 ElementHandle prevents DOM element from garbage collection unless the handle is [disposed](#elementhandledispose). ElementHandles are auto-disposed when their origin frame gets navigated.
 
 ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalselector-pagefunction-args) and [`page.evaluate()`](#pageevaluatepagefunction-args) methods.
 
 #### elementHandle.$(selector)
 - `selector` <[string]> A [selector] to query element for
--- a/remote/test/puppeteer/experimental/puppeteer-firefox/.cirrus.yml
+++ b/remote/test/puppeteer/experimental/puppeteer-firefox/.cirrus.yml
@@ -2,30 +2,30 @@ env:
   DISPLAY: :99.0
 
 task:
   name: node8 (linux)
   container:
     dockerfile: .ci/node8/Dockerfile.linux
   xvfb_start_background_script: Xvfb :99 -ac -screen 0 1024x768x24
   install_script: npm install
-  test_script: npm run funit
+  test_script: npm run fjunit
 
 task:
   name: node8 (macOS)
   osx_instance:
     image: high-sierra-base
   env:
     HOMEBREW_NO_AUTO_UPDATE: 1
   node_install_script:
     - brew install node@8
     - brew link --force node@8
   install_script: npm install
-  test_script: npm run funit
+  test_script: npm run fjunit
 
 # task:
 #   allow_failures: true
 #  windows_container:
 #    dockerfile: .ci/node8/Dockerfile.windows
 #    os_version: 2016
 #  name: node8 (windows)
 #  install_script: npm install --unsafe-perm
-#  test_script: npm run funit
+#  test_script: npm run fjunit
--- a/remote/test/puppeteer/lib/Launcher.js
+++ b/remote/test/puppeteer/lib/Launcher.js
@@ -21,53 +21,166 @@ const URL = require('url');
 const removeFolder = require('rimraf');
 const childProcess = require('child_process');
 const BrowserFetcher = require('./BrowserFetcher');
 const {Connection} = require('./Connection');
 const {Browser} = require('./Browser');
 const readline = require('readline');
 const fs = require('fs');
 const {helper, assert, debugError} = require('./helper');
+const debugLauncher = require('debug')(`puppeteer:launcher`);
 const {TimeoutError} = require('./Errors');
 const WebSocketTransport = require('./WebSocketTransport');
 const PipeTransport = require('./PipeTransport');
 
 const mkdtempAsync = helper.promisify(fs.mkdtemp);
 const removeFolderAsync = helper.promisify(removeFolder);
+const writeFileAsync = helper.promisify(fs.writeFile);
 
-const CHROME_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_dev_profile-');
+class BrowserRunner {
+
+  /**
+   * @param {string} executablePath
+   * @param {!Array<string>} processArguments
+   * @param {string=} tempDirectory
+   */
+  constructor(executablePath, processArguments, tempDirectory) {
+    this._executablePath = executablePath;
+    this._processArguments = processArguments;
+    this._tempDirectory = tempDirectory;
+    this.proc = null;
+    this.connection = null;
+    this._closed = true;
+    this._listeners = [];
+  }
 
-const DEFAULT_ARGS = [
-  '--disable-background-networking',
-  '--enable-features=NetworkService,NetworkServiceInProcess',
-  '--disable-background-timer-throttling',
-  '--disable-backgrounding-occluded-windows',
-  '--disable-breakpad',
-  '--disable-client-side-phishing-detection',
-  '--disable-component-extensions-with-background-pages',
-  '--disable-default-apps',
-  '--disable-dev-shm-usage',
-  '--disable-extensions',
-  // BlinkGenPropertyTrees disabled due to crbug.com/937609
-  '--disable-features=TranslateUI,BlinkGenPropertyTrees',
-  '--disable-hang-monitor',
-  '--disable-ipc-flooding-protection',
-  '--disable-popup-blocking',
-  '--disable-prompt-on-repost',
-  '--disable-renderer-backgrounding',
-  '--disable-sync',
-  '--force-color-profile=srgb',
-  '--metrics-recording-only',
-  '--no-first-run',
-  '--enable-automation',
-  '--password-store=basic',
-  '--use-mock-keychain',
-];
+  /**
+   * @param {!(Launcher.LaunchOptions)=} options
+   */
+  start(options = {}) {
+    const {
+      handleSIGINT,
+      handleSIGTERM,
+      handleSIGHUP,
+      dumpio,
+      env,
+      pipe
+    } = options;
+    /** @type {!Array<"ignore"|"pipe">} */
+    let stdio = ['pipe', 'pipe', 'pipe'];
+    if (pipe) {
+      if (dumpio)
+        stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
+      else
+        stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
+    }
+    assert(!this.proc, 'This process has previously been started.');
+    debugLauncher(`Calling ${this._executablePath} ${this._processArguments.join(' ')}`);
+    this.proc = childProcess.spawn(
+        this._executablePath,
+        this._processArguments,
+        {
+          // On non-windows platforms, `detached: true` makes child process a leader of a new
+          // 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,
+          stdio
+        }
+    );
+    if (dumpio) {
+      this.proc.stderr.pipe(process.stderr);
+      this.proc.stdout.pipe(process.stdout);
+    }
+    this._closed = false;
+    this._processClosing = new Promise((fulfill, reject) => {
+      this.proc.once('exit', () => {
+        this._closed = true;
+        // Cleanup as processes exit.
+        if (this._tempDirectory) {
+          removeFolderAsync(this._tempDirectory)
+              .then(() => fulfill())
+              .catch(err => console.error(err));
+        } else {
+          fulfill();
+        }
+      });
+    });
+    this._listeners = [ helper.addEventListener(process, 'exit', this.kill.bind(this)) ];
+    if (handleSIGINT)
+      this._listeners.push(helper.addEventListener(process, 'SIGINT', () => { this.kill(); process.exit(130); }));
+    if (handleSIGTERM)
+      this._listeners.push(helper.addEventListener(process, 'SIGTERM', this.close.bind(this)));
+    if (handleSIGHUP)
+      this._listeners.push(helper.addEventListener(process, 'SIGHUP', this.close.bind(this)));
+  }
 
-class Launcher {
+  /**
+   * @return {Promise}
+   */
+  close() {
+    if (this._closed)
+      return Promise.resolve();
+    helper.removeEventListeners(this._listeners);
+    if (this._tempDirectory) {
+      this.kill();
+    } else if (this.connection) {
+      // Attempt to close the browser gracefully
+      this.connection.send('Browser.close').catch(error => {
+        debugError(error);
+        this.kill();
+      });
+    }
+    return this._processClosing;
+  }
+
+  // This function has to be sync to be used as 'exit' event handler.
+  kill() {
+    helper.removeEventListeners(this._listeners);
+    if (this.proc && this.proc.pid && !this.proc.killed && !this._closed) {
+      try {
+        if (process.platform === 'win32')
+          childProcess.execSync(`taskkill /pid ${this.proc.pid} /T /F`);
+        else
+          process.kill(-this.proc.pid, 'SIGKILL');
+      } catch (e) {
+        // the process might have already stopped
+      }
+    }
+    // Attempt to remove temporary profile directory to avoid littering.
+    try {
+      removeFolder.sync(this._tempDirectory);
+    } catch (e) { }
+  }
+
+  /**
+   * @param {!({usePipe?: boolean, timeout: number, slowMo: number, preferredRevision: string})} options
+   *
+   * @return {!Promise<!Connection>}
+   */
+  async setupConnection(options) {
+    const {
+      usePipe,
+      timeout,
+      slowMo,
+      preferredRevision
+    } = options;
+    if (!usePipe) {
+      const browserWSEndpoint = await waitForWSEndpoint(this.proc, timeout, preferredRevision);
+      const transport = await WebSocketTransport.create(browserWSEndpoint);
+      this.connection = new Connection(browserWSEndpoint, transport, slowMo);
+    } else {
+      const transport = new PipeTransport(/** @type {!NodeJS.WritableStream} */(this.proc.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (this.proc.stdio[4]));
+      this.connection = new Connection('', transport, slowMo);
+    }
+    return this.connection;
+  }
+}
+
+class ChromeLauncher {
   /**
    * @param {string} projectRoot
    * @param {string} preferredRevision
    * @param {boolean} isPuppeteerCore
    */
   constructor(projectRoot, preferredRevision, isPuppeteerCore) {
     this._projectRoot = projectRoot;
     this._preferredRevision = preferredRevision;
@@ -90,164 +203,94 @@ class Launcher {
       handleSIGTERM = true,
       handleSIGHUP = true,
       ignoreHTTPSErrors = false,
       defaultViewport = {width: 800, height: 600},
       slowMo = 0,
       timeout = 30000
     } = options;
 
+    const profilePath = path.join(os.tmpdir(), 'puppeteer_dev_chrome_profile-');
     const chromeArguments = [];
     if (!ignoreDefaultArgs)
       chromeArguments.push(...this.defaultArgs(options));
     else if (Array.isArray(ignoreDefaultArgs))
-      chromeArguments.push(...this.defaultArgs(options).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
+      chromeArguments.push(...this.defaultArgs(options).filter(arg => !ignoreDefaultArgs.includes(arg)));
     else
       chromeArguments.push(...args);
 
     let temporaryUserDataDir = null;
 
     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);
+      temporaryUserDataDir = await mkdtempAsync(profilePath);
       chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
     }
-    if (!chromeArguments.some(arg => arg.startsWith('--profile')) &&
-        env.PROFILE) {
-      chromeArguments.push("--profile");
-      chromeArguments.push(env.PROFILE);
-    }
 
     let chromeExecutable = executablePath;
     if (!executablePath) {
-      const {missingText, executablePath} = this._resolveExecutablePath();
+      const {missingText, executablePath} = resolveExecutablePath(this);
       if (missingText)
         throw new Error(missingText);
       chromeExecutable = executablePath;
     }
 
     const usePipe = chromeArguments.includes('--remote-debugging-pipe');
-    /** @type {!Array<"ignore"|"pipe">} */
-    let stdio = ['pipe', 'pipe', 'pipe'];
-    if (usePipe) {
-      if (dumpio)
-        stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
-      else
-        stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
-    }
-    const chromeProcess = childProcess.spawn(
-        chromeExecutable,
-        chromeArguments,
-        {
-          // On non-windows platforms, `detached: true` makes child process a leader of a new
-          // 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,
-          stdio
-        }
-    );
-
-    if (dumpio) {
-      chromeProcess.stderr.pipe(process.stderr);
-      chromeProcess.stdout.pipe(process.stdout);
-    }
+    const runner = new BrowserRunner(chromeExecutable, chromeArguments, temporaryUserDataDir);
+    runner.start({handleSIGHUP, handleSIGTERM, handleSIGINT, dumpio, env, pipe: usePipe});
 
-    let chromeClosed = false;
-    const waitForChromeToClose = new Promise((fulfill, reject) => {
-      chromeProcess.once('exit', () => {
-        chromeClosed = true;
-        // Cleanup as processes exit.
-        if (temporaryUserDataDir) {
-          removeFolderAsync(temporaryUserDataDir)
-              .then(() => fulfill())
-              .catch(err => console.error(err));
-        } else {
-          fulfill();
-        }
-      });
-    });
-
-    const listeners = [ helper.addEventListener(process, 'exit', killChrome) ];
-    if (handleSIGINT)
-      listeners.push(helper.addEventListener(process, 'SIGINT', () => { killChrome(); process.exit(130); }));
-    if (handleSIGTERM)
-      listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyCloseChrome));
-    if (handleSIGHUP)
-      listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyCloseChrome));
-    /** @type {?Connection} */
-    let connection = null;
     try {
-      if (!usePipe) {
-        const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, timeout, this._preferredRevision);
-        const transport = await WebSocketTransport.create(browserWSEndpoint);
-        connection = new Connection(browserWSEndpoint, transport, slowMo);
-      } else {
-        const transport = new PipeTransport(/** @type {!NodeJS.WritableStream} */(chromeProcess.stdio[3]), /** @type {!NodeJS.ReadableStream} */ (chromeProcess.stdio[4]));
-        connection = new Connection('', transport, slowMo);
-      }
-      const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, chromeProcess, gracefullyCloseChrome);
+      const connection = await runner.setupConnection({usePipe, timeout, slowMo, preferredRevision: this._preferredRevision});
+      const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
       await browser.waitForTarget(t => t.type() === 'page');
       return browser;
     } catch (e) {
-      killChrome();
+      runner.kill();
       throw e;
     }
-
-    /**
-     * @return {Promise}
-     */
-    function gracefullyCloseChrome() {
-      helper.removeEventListeners(listeners);
-      if (temporaryUserDataDir) {
-        killChrome();
-      } else if (connection) {
-        // Attempt to close chrome gracefully
-        connection.send('Browser.close').catch(error => {
-          debugError(error);
-          killChrome();
-        });
-      }
-      return waitForChromeToClose;
-    }
-
-    // This method has to be sync to be used as 'exit' event handler.
-    function killChrome() {
-      helper.removeEventListeners(listeners);
-      if (chromeProcess.pid && !chromeProcess.killed && !chromeClosed) {
-        // Force kill chrome.
-        try {
-          if (process.platform === 'win32')
-            childProcess.execSync(`taskkill /pid ${chromeProcess.pid} /T /F`);
-          else
-            process.kill(-chromeProcess.pid, 'SIGKILL');
-        } catch (e) {
-          // the process might have already stopped
-        }
-      }
-      // Attempt to remove temporary profile directory to avoid littering.
-      try {
-        removeFolder.sync(temporaryUserDataDir);
-      } catch (e) { }
-    }
   }
 
   /**
    * @param {!Launcher.ChromeArgOptions=} options
    * @return {!Array<string>}
    */
   defaultArgs(options = {}) {
+    const chromeArguments = [
+      '--disable-background-networking',
+      '--enable-features=NetworkService,NetworkServiceInProcess',
+      '--disable-background-timer-throttling',
+      '--disable-backgrounding-occluded-windows',
+      '--disable-breakpad',
+      '--disable-client-side-phishing-detection',
+      '--disable-component-extensions-with-background-pages',
+      '--disable-default-apps',
+      '--disable-dev-shm-usage',
+      '--disable-extensions',
+      // BlinkGenPropertyTrees disabled due to crbug.com/937609
+      '--disable-features=TranslateUI,BlinkGenPropertyTrees',
+      '--disable-hang-monitor',
+      '--disable-ipc-flooding-protection',
+      '--disable-popup-blocking',
+      '--disable-prompt-on-repost',
+      '--disable-renderer-backgrounding',
+      '--disable-sync',
+      '--force-color-profile=srgb',
+      '--metrics-recording-only',
+      '--no-first-run',
+      '--enable-automation',
+      '--password-store=basic',
+      '--use-mock-keychain',
+    ];
     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',
@@ -259,17 +302,124 @@ class Launcher {
     chromeArguments.push(...args);
     return chromeArguments;
   }
 
   /**
    * @return {string}
    */
   executablePath() {
-    return this._resolveExecutablePath().executablePath;
+    return resolveExecutablePath(this).executablePath;
+  }
+
+  /**
+   * @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserURL?: string, transport?: !Puppeteer.ConnectionTransport})} options
+   * @return {!Promise<!Browser>}
+   */
+  async connect(options) {
+    const {
+      browserWSEndpoint,
+      browserURL,
+      ignoreHTTPSErrors = false,
+      defaultViewport = {width: 800, height: 600},
+      transport,
+      slowMo = 0,
+    } = options;
+
+    assert(Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) === 1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect');
+
+    let connection = null;
+    if (transport) {
+      connection = new Connection('', transport, slowMo);
+    } else if (browserWSEndpoint) {
+      const connectionTransport = await WebSocketTransport.create(browserWSEndpoint);
+      connection = new Connection(browserWSEndpoint, connectionTransport, slowMo);
+    } else if (browserURL) {
+      const connectionURL = await getWSEndpoint(browserURL);
+      const connectionTransport = await WebSocketTransport.create(connectionURL);
+      connection = new Connection(connectionURL, connectionTransport, slowMo);
+    }
+
+    const {browserContextIds} = await connection.send('Target.getBrowserContexts');
+    return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
+  }
+
+}
+
+
+class FirefoxLauncher {
+  /**
+   * @param {string} projectRoot
+   * @param {string} preferredRevision
+   * @param {boolean} isPuppeteerCore
+   */
+  constructor(projectRoot, preferredRevision, isPuppeteerCore) {
+    this._projectRoot = projectRoot;
+    this._preferredRevision = preferredRevision;
+    this._isPuppeteerCore = isPuppeteerCore;
+  }
+
+  /**
+   * @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions & {extraPrefs?: !object})=} options
+   * @return {!Promise<!Browser>}
+   */
+  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,
+      extraPrefs = {}
+    } = options;
+
+    const firefoxArguments = [];
+    if (!ignoreDefaultArgs)
+      firefoxArguments.push(...this.defaultArgs(options));
+    else if (Array.isArray(ignoreDefaultArgs))
+      firefoxArguments.push(...this.defaultArgs(options).filter(arg => !ignoreDefaultArgs.includes(arg)));
+    else
+      firefoxArguments.push(...args);
+
+    let temporaryUserDataDir = null;
+
+    if (!firefoxArguments.includes('-profile') && !firefoxArguments.includes('--profile')) {
+      temporaryUserDataDir = await this._createProfile(extraPrefs);
+      firefoxArguments.push('--profile');
+      firefoxArguments.push(temporaryUserDataDir);
+    }
+
+    let executable = executablePath;
+    if (!executablePath) {
+      const {missingText, executablePath} = resolveExecutablePath(this);
+      if (missingText)
+        throw new Error(missingText);
+      executable = executablePath;
+    }
+
+    const runner = new BrowserRunner(executable, firefoxArguments, temporaryUserDataDir);
+    runner.start({handleSIGHUP, handleSIGTERM, handleSIGINT, dumpio, env, pipe});
+
+    try {
+      const connection = await runner.setupConnection({usePipe: pipe, timeout, slowMo, preferredRevision: this._preferredRevision});
+      const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, runner.proc, runner.close.bind(runner));
+      await browser.waitForTarget(t => t.type() === 'page');
+      return browser;
+    } catch (e) {
+      runner.kill();
+      throw e;
+    }
   }
 
   /**
    * @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserURL?: string, transport?: !Puppeteer.ConnectionTransport})} options
    * @return {!Promise<!Browser>}
    */
   async connect(options) {
     const {
@@ -295,78 +445,115 @@ class Launcher {
       connection = new Connection(connectionURL, connectionTransport, slowMo);
     }
 
     const {browserContextIds} = await connection.send('Target.getBrowserContexts');
     return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, () => connection.send('Browser.close').catch(debugError));
   }
 
   /**
-   * @return {{executablePath: string, missingText: ?string}}
+   * @return {string}
    */
-  _resolveExecutablePath() {
-    // puppeteer-core doesn't take into account PUPPETEER_* env variables.
-    if (!this._isPuppeteerCore) {
-      const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.npm_config_puppeteer_executable_path || process.env.npm_package_config_puppeteer_executable_path;
-      if (executablePath) {
-        const missingText = !fs.existsSync(executablePath) ? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' + executablePath : null;
-        return { executablePath, missingText };
-      }
-    }
-    const browserFetcher = new BrowserFetcher(this._projectRoot);
-    if (!this._isPuppeteerCore) {
-      const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
-      if (revision) {
-        const revisionInfo = browserFetcher.revisionInfo(revision);
-        const missingText = !revisionInfo.local ? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' + revisionInfo.executablePath : null;
-        return {executablePath: revisionInfo.executablePath, missingText};
-      }
-    }
-    const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision);
-    const missingText = !revisionInfo.local ? `Chromium revision is not downloaded. Run "npm install" or "yarn install"` : null;
-    return {executablePath: revisionInfo.executablePath, missingText};
+  executablePath() {
+    const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.npm_config_puppeteer_executable_path || process.env.npm_package_config_puppeteer_executable_path;
+    // TODO get resolveExecutablePath working for Firefox
+    if (!executablePath)
+      throw new Error('Please set PUPPETEER_EXECUTABLE_PATH to a Firefox binary.');
+    return executablePath;
   }
 
+  /**
+   * @param {!Launcher.ChromeArgOptions=} options
+   * @return {!Array<string>}
+   */
+  defaultArgs(options = {}) {
+    const firefoxArguments = [
+      '--remote-debugging-port=0',
+      '--no-remote',
+      '--foreground',
+    ];
+    const {
+      devtools = false,
+      headless = !devtools,
+      args = [],
+      userDataDir = null
+    } = options;
+    if (userDataDir) {
+      firefoxArguments.push('--profile');
+      firefoxArguments.push(userDataDir);
+    }
+    if (headless)
+      firefoxArguments.push('--headless');
+    if (devtools)
+      firefoxArguments.push('--devtools');
+    if (args.every(arg => arg.startsWith('-')))
+      firefoxArguments.push('about:blank');
+    firefoxArguments.push(...args);
+    return firefoxArguments;
+  }
+
+  /**
+   * @param {!Object=} extraPrefs
+   * @return {!Promise<string>}
+   */
+  async _createProfile(extraPrefs) {
+    const profilePath = await mkdtempAsync(path.join(os.tmpdir(), 'puppeteer_dev_firefox_profile-'));
+    const prefs_js = [];
+    const user_js = [];
+    const preferences = {
+      // https://bugzilla.mozilla.org/show_bug.cgi?id=1544393
+      'remote.enabled': true,
+      // https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
+      'browser.dom.window.dump.enabled': true
+    };
+    Object.assign(preferences, extraPrefs);
+    for (const [key, value] of Object.entries(preferences))
+      user_js.push(`user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`);
+    await writeFileAsync(path.join(profilePath, 'user.js'), user_js.join('\n'));
+    await writeFileAsync(path.join(profilePath, 'prefs.js'), prefs_js.join('\n'));
+    return profilePath;
+  }
 }
 
+
 /**
- * @param {!Puppeteer.ChildProcess} chromeProcess
+ * @param {!Puppeteer.ChildProcess} browserProcess
  * @param {number} timeout
  * @param {string} preferredRevision
  * @return {!Promise<string>}
  */
-function waitForWSEndpoint(chromeProcess, timeout, preferredRevision) {
+function waitForWSEndpoint(browserProcess, timeout, preferredRevision) {
   return new Promise((resolve, reject) => {
-    const rl = readline.createInterface({ input: chromeProcess.stdout });
+    const rl = readline.createInterface({ input: browserProcess.stdout });
     let stderr = '';
     const listeners = [
       helper.addEventListener(rl, 'line', onLine),
       helper.addEventListener(rl, 'close', () => onClose()),
-      helper.addEventListener(chromeProcess, 'exit', () => onClose()),
-      helper.addEventListener(chromeProcess, 'error', error => onClose(error))
+      helper.addEventListener(browserProcess, 'exit', () => onClose()),
+      helper.addEventListener(browserProcess, 'error', error => onClose(error))
     ];
     const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;
 
     /**
      * @param {!Error=} error
      */
     function onClose(error) {
       cleanup();
       reject(new Error([
-        'Failed to launch chrome!' + (error ? ' ' + error.message : ''),
+        'Failed to launch the browser process!' + (error ? ' ' + error.message : ''),
         stderr,
         '',
         'TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md',
         '',
       ].join('\n')));
     }
 
     function onTimeout() {
       cleanup();
-      reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${preferredRevision}`));
+      reject(new TimeoutError(`Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.`));
     }
 
     /**
      * @param {string} line
      */
     function onLine(line) {
       stderr += line + '\n';
       const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
@@ -413,16 +600,62 @@ function getWSEndpoint(browserURL) {
 
   return promise.catch(e => {
     e.message = `Failed to fetch browser webSocket url from ${endpointURL}: ` + e.message;
     throw e;
   });
 }
 
 /**
+ * @param {ChromeLauncher|FirefoxLauncher} launcher
+ *
+ * @return {{executablePath: string, missingText: ?string}}
+ */
+function resolveExecutablePath(launcher) {
+  // puppeteer-core doesn't take into account PUPPETEER_* env variables.
+  if (!launcher._isPuppeteerCore) {
+    const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.npm_config_puppeteer_executable_path || process.env.npm_package_config_puppeteer_executable_path;
+    if (executablePath) {
+      const missingText = !fs.existsSync(executablePath) ? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' + executablePath : null;
+      return { executablePath, missingText };
+    }
+  }
+  const browserFetcher = new BrowserFetcher(launcher._projectRoot);
+  if (!launcher._isPuppeteerCore) {
+    const revision = process.env['PUPPETEER_CHROMIUM_REVISION'];
+    if (revision) {
+      const revisionInfo = browserFetcher.revisionInfo(revision);
+      const missingText = !revisionInfo.local ? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' + revisionInfo.executablePath : null;
+      return {executablePath: revisionInfo.executablePath, missingText};
+    }
+  }
+  const revisionInfo = browserFetcher.revisionInfo(launcher._preferredRevision);
+  const missingText = !revisionInfo.local ? `Browser is not downloaded. Run "npm install" or "yarn install"` : null;
+  return {executablePath: revisionInfo.executablePath, missingText};
+}
+
+/**
+ * @param {string} product
+ * @param {string} projectRoot
+ * @param {string} preferredRevision
+ * @param {boolean} isPuppeteerCore
+ * @return {ChromeLauncher|FirefoxLauncher}
+ */
+function Launcher(product, projectRoot, preferredRevision, isPuppeteerCore) {
+  switch (product) {
+    case 'firefox':
+      return new FirefoxLauncher(projectRoot, preferredRevision, isPuppeteerCore);
+    case 'chrome':
+    default:
+      return new ChromeLauncher(projectRoot, preferredRevision, isPuppeteerCore);
+  }
+}
+
+
+/**
  * @typedef {Object} Launcher.ChromeArgOptions
  * @property {boolean=} headless
  * @property {Array<string>=} args
  * @property {string=} userDataDir
  * @property {boolean=} devtools
  */
 
 /**
--- a/remote/test/puppeteer/lib/Puppeteer.js
+++ b/remote/test/puppeteer/lib/Puppeteer.js
@@ -13,29 +13,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 const Launcher = require('./Launcher');
 const BrowserFetcher = require('./BrowserFetcher');
 const Errors = require('./Errors');
 const DeviceDescriptors = require('./DeviceDescriptors');
 
+const PRODUCT = process.env.PUPPETEER_PRODUCT || process.env.npm_config_puppeteer_product || process.env.npm_package_config_puppeteer_product;
+
 module.exports = class {
   /**
    * @param {string} projectRoot
    * @param {string} preferredRevision
    * @param {boolean} isPuppeteerCore
    */
   constructor(projectRoot, preferredRevision, isPuppeteerCore) {
     this._projectRoot = projectRoot;
-    this._launcher = new Launcher(projectRoot, preferredRevision, isPuppeteerCore);
+    this._launcher = Launcher(PRODUCT, projectRoot, preferredRevision, isPuppeteerCore);
   }
 
   /**
-   * @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions)=} options
+   * @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions & {extraPrefs?: !object})=} options
    * @return {!Promise<!Puppeteer.Browser>}
    */
   launch(options) {
     return this._launcher.launch(options);
   }
 
   /**
    * @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserURL?: string, transport?: !Puppeteer.ConnectionTransport})} options
@@ -48,16 +50,23 @@ module.exports = class {
   /**
    * @return {string}
    */
   executablePath() {
     return this._launcher.executablePath();
   }
 
   /**
+   * @return {string}
+   */
+  get product() {
+    return PRODUCT || 'chrome';
+  }
+
+  /**
    * @return {Object}
    */
   get devices() {
     return DeviceDescriptors;
   }
 
   /**
    * @return {Object}
--- a/remote/test/puppeteer/lib/helper.js
+++ b/remote/test/puppeteer/lib/helper.js
@@ -132,17 +132,17 @@ class Helper {
   }
 
   /**
    * @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: (string|symbol), handler: function(?):void}>} listeners
    */
   static removeEventListeners(listeners) {
     for (const listener of listeners)
       listener.emitter.removeListener(listener.eventName, listener.handler);
-    listeners.splice(0, listeners.length);
+    listeners.length = 0;
   }
 
   /**
    * @param {!Object} obj
    * @return {boolean}
    */
   static isString(obj) {
     return typeof obj === 'string' || obj instanceof String;
--- a/remote/test/puppeteer/moz.yaml
+++ b/remote/test/puppeteer/moz.yaml
@@ -1,10 +1,10 @@
 bugzilla:
   component: Agent
   product: Remote Protocol
 origin:
   description: Headless Chrome Node API
   license: Apache-2.0
   name: puppeteer
-  release: 3daf9766562eaa189866687644b338eba1bef531
+  release: d26ff10
   url: https://github.com/mjzffr/puppeteer.git
 schema: 1
--- a/remote/test/puppeteer/package.json
+++ b/remote/test/puppeteer/package.json
@@ -7,17 +7,18 @@
   "engines": {
     "node": ">=8.16.0"
   },
   "puppeteer": {
     "chromium_revision": "706915"
   },
   "scripts": {
     "unit": "node test/test.js",
-    "funit": "BROWSER=firefox node test/test.js",
+    "fjunit": "PUPPETEER_PRODUCT=juggler node test/test.js",
+    "funit": "PUPPETEER_PRODUCT=firefox node test/test.js",
     "debug-unit": "node --inspect-brk test/test.js",
     "test-doclint": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js",
     "test": "npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-types && node utils/testrunner/test/test.js",
     "install": "node install.js",
     "lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .) && npm run tsc && npm run doc",
     "doc": "node utils/doclint/cli.js",
     "coverage": "cross-env COVERAGE=true npm run unit",
     "tsc": "tsc -p .",
--- a/remote/test/puppeteer/test/puppeteer.spec.js
+++ b/remote/test/puppeteer/test/puppeteer.spec.js
@@ -23,41 +23,50 @@ const YELLOW_COLOR = '\x1b[33m';
 const RESET_COLOR = '\x1b[0m';
 
 module.exports.addTests = ({testRunner, product, puppeteerPath}) => {
   const {describe, xdescribe, fdescribe} = testRunner;
   const {it, fit, xit} = testRunner;
   const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
 
   const CHROME = product === 'Chromium';
-  const FFOX = product === 'Firefox';
+  const FFOX = (product === 'Firefox' || product === 'Juggler');
+  const JUGGLER = product === 'Juggler';
 
   const puppeteer = require(puppeteerPath);
 
   const headless = (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true';
   const slowMo = parseInt((process.env.SLOW_MO || '0').trim(), 10);
+  let extraLaunchOptions = {};
+  try {
+    extraLaunchOptions = JSON.parse(process.env.EXTRA_LAUNCH_OPTIONS || '{}');
+  } catch (e) {
+    console.warn(`${YELLOW_COLOR}Error parsing EXTRA_LAUNCH_OPTIONS: ${e.message}. Skipping.${RESET_COLOR}`);
+  }
 
-  const defaultBrowserOptions = {
+  const defaultBrowserOptions = Object.assign({
     handleSIGINT: false,
-    executablePath: CHROME ? process.env.CHROME : process.env.FFOX,
+    executablePath: process.env.BINARY,
     slowMo,
     headless,
     dumpio: !!process.env.DUMPIO,
-  };
+  }, extraLaunchOptions);
+
 
   if (defaultBrowserOptions.executablePath) {
     console.warn(`${YELLOW_COLOR}WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}${RESET_COLOR}`);
   } else {
-    // Make sure the `npm install` was run after the chromium roll.
-    if (!fs.existsSync(puppeteer.executablePath()))
-      throw new Error(`Browser is not downloaded. Run 'npm install' and try to re-run tests`);
+    const path = puppeteer.executablePath();
+    if (!fs.existsSync(path))
+      throw new Error(`Browser is not downloaded at ${path}. Run 'npm install' and try to re-run tests`);
   }
 
-  const GOLDEN_DIR = path.join(__dirname, 'golden-' + product.toLowerCase());
-  const OUTPUT_DIR = path.join(__dirname, 'output-' + product.toLowerCase());
+  const suffix = JUGGLER ? 'firefox' : product.toLowerCase();
+  const GOLDEN_DIR = path.join(__dirname, 'golden-' + suffix);
+  const OUTPUT_DIR = path.join(__dirname, 'output-' + suffix);
   if (fs.existsSync(OUTPUT_DIR))
     rm(OUTPUT_DIR);
   const {expect} = new Matchers({
     toBeGolden: GoldenUtils.compare.bind(null, GOLDEN_DIR, OUTPUT_DIR)
   });
 
   const testOptions = {
     testRunner,
@@ -67,17 +76,17 @@ module.exports.addTests = ({testRunner, 
     puppeteer,
     expect,
     defaultBrowserOptions,
     puppeteerPath,
     headless: !!defaultBrowserOptions.headless,
   };
 
   beforeAll(async() => {
-    if (FFOX && defaultBrowserOptions.executablePath)
+    if (JUGGLER && defaultBrowserOptions.executablePath)
       await require('../experimental/puppeteer-firefox/misc/install-preferences')(defaultBrowserOptions.executablePath);
   });
 
   describe('Browser', function() {
     beforeAll(async state => {
       state.browser = await puppeteer.launch(defaultBrowserOptions);
     });
 
--- a/remote/test/puppeteer/test/test.js
+++ b/remote/test/puppeteer/test/test.js
@@ -71,38 +71,54 @@ beforeEach(async({server, httpsServer}) 
   server.reset();
   httpsServer.reset();
 });
 
 const CHROMIUM_NO_COVERAGE = new Set([
   'page.emulateMedia', // Legacy alias for `page.emulateMediaType`.
 ]);
 
-if (process.env.BROWSER === 'firefox') {
-  testRunner.addTestDSL('it_fails_ffox', 'skip');
-  testRunner.addSuiteDSL('describe_fails_ffox', 'skip');
-  describe('Firefox', () => {
-    require('./puppeteer.spec.js').addTests({
-      product: 'Firefox',
-      puppeteerPath: path.resolve(__dirname, '../experimental/puppeteer-firefox/'),
-      testRunner,
+switch (process.env.PUPPETEER_PRODUCT) {
+  case 'firefox':
+    testRunner.addTestDSL('it_fails_ffox', 'skip');
+    testRunner.addSuiteDSL('describe_fails_ffox', 'skip');
+    describe('Firefox', () => {
+      require('./puppeteer.spec.js').addTests({
+        product: 'Firefox',
+        puppeteerPath: utils.projectRoot(),
+        testRunner,
+      });
+      if (process.env.COVERAGE)
+        utils.recordAPICoverage(testRunner, require('../lib/api'), require('../lib/Events').Events, CHROMIUM_NO_COVERAGE);
     });
-  });
-} else {
-  testRunner.addTestDSL('it_fails_ffox', 'run');
-  testRunner.addSuiteDSL('describe_fails_ffox', 'run');
-  describe('Chromium', () => {
-    require('./puppeteer.spec.js').addTests({
-      product: 'Chromium',
-      puppeteerPath: utils.projectRoot(),
-      testRunner,
+    break;
+  case 'juggler':
+    testRunner.addTestDSL('it_fails_ffox', 'skip');
+    testRunner.addSuiteDSL('describe_fails_ffox', 'skip');
+    describe('Firefox (Juggler)', () => {
+      require('./puppeteer.spec.js').addTests({
+        product: 'Juggler',
+        puppeteerPath: path.resolve(__dirname, '../experimental/puppeteer-firefox/'),
+        testRunner,
+      });
     });
-    if (process.env.COVERAGE)
-      utils.recordAPICoverage(testRunner, require('../lib/api'), require('../lib/Events').Events, CHROMIUM_NO_COVERAGE);
-  });
+    break;
+  case 'chrome':
+  default:
+    testRunner.addTestDSL('it_fails_ffox', 'run');
+    testRunner.addSuiteDSL('describe_fails_ffox', 'run');
+    describe('Chromium', () => {
+      require('./puppeteer.spec.js').addTests({
+        product: 'Chromium',
+        puppeteerPath: utils.projectRoot(),
+        testRunner,
+      });
+      if (process.env.COVERAGE)
+        utils.recordAPICoverage(testRunner, require('../lib/api'), require('../lib/Events').Events, CHROMIUM_NO_COVERAGE);
+    });
 }
 
 if (process.env.CI && testRunner.hasFocusedTestsOrSuites()) {
   console.error('ERROR: "focused" tests/suites are prohibitted on bots. Remove any "fit"/"fdescribe" declarations.');
   process.exit(1);
 }
 
 new Reporter(testRunner, {
--- a/remote/test/puppeteer/utils/protocol-types-generator/index.js
+++ b/remote/test/puppeteer/utils/protocol-types-generator/index.js
@@ -1,14 +1,14 @@
 // @ts-check
 const path = require('path');
 const puppeteer = require('../..');
 module.exports = puppeteer.launch({
   pipe: false,
-  executablePath: process.env.CHROME,
+  executablePath: process.env.BINARY,
 }).then(async browser => {
   const origin = browser.wsEndpoint().match(/ws:\/\/([0-9A-Za-z:\.]*)\//)[1];
   const page = await browser.newPage();
   await page.goto(`http://${origin}/json/protocol`);
   const json = JSON.parse(await page.evaluate(() => document.documentElement.innerText));
   const version = await browser.version();
   await browser.close();
   const output = `// This is generated from /utils/protocol-types-generator/index.js