From 5e9d4eb8b7b3dca8f135045d5b2c693137f27b4d Mon Sep 17 00:00:00 2001 From: mhavelant Date: Sat, 9 Mar 2019 15:12:02 +0100 Subject: [PATCH] Allow puppeteer to connect to a remote chrome instance. --- README.md | 57 +++++++++++++++++++++++++++++++++-- core/util/runPuppet.js | 67 ++++++++++++++++++++++++++++++++++-------- package-lock.json | 11 +++++++ package.json | 1 + 4 files changed, 122 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 596ecd45..cb50ef41 100644 --- a/README.md +++ b/README.md @@ -747,11 +747,12 @@ The [storageState](https://playwright.dev/docs/api/class-browsercontext#browser- ### Setting Puppeteer And Playwright Option Flags -Backstop sets two defaults for both Puppeteer and Playwright: +Backstop sets three defaults for both Puppeteer and Playwright: ```json ignoreHTTPSErrors: true, -headless: +headless: , +remote: false ``` You can add more settings (or override the defaults) with the `engineOptions` property. (properties are merged). This is where headless mode can also be set to 'new', until "new headless mode" is less hacky and more supported by Playwright. @@ -772,6 +773,53 @@ More info here: * [Puppeteer on github](https://github.com/GoogleChrome/puppeteer). * [Playwright on github](https://github.com/microsoft/playwright). + +#### Running Puppeteer on an already running Chrome instance +```json +"engineOptions": { + "remote": true, + "remoteOptions": { + // Note: This ignoreHTTPSErrors is separate from the one in engineOptions, and no default is set for it by BackstopJS. + "ignoreHTTPSErrors": false, + "browserWSEndpoint": "ws://myremotechrome:9222/devtools/browser/e6447a74-d83f-4f4e-a8e9-388b5216e0c2" + } +} +``` + +For more available remoteOptions, check BrowserOptions and ConnectOptions from puppeteer. + +Notes for remote mode: +- Anything outside of remoteOptions is ignored. +- Chrome has to be launched separately, and BackstopJS has to be able to connect to it. + - Args, headless, etc. have to be set for chrome when launching chrome. + + +#### Example setup with docker + +1. Start a dockerized chrome instance + 1. E.g `docker run -d -p 9222:9222 --cap-add=SYS_ADMIN justinribeiro/chrome-headless` + 2. Note the output, it's the docker container ID, e.g `8d203d9598f89028f98389f50a92aab33e18699e502f3813ab584ee654a8ca8a` +2. Get the devtools web socket + 1. Use `docker logs ` (list these with `docker ps`) + 1. E.g `docker logs 8d203d9598f89028f98389f50a92aab33e18699e502f3813ab584ee654a8ca8a` + 2. E.g `docker logs loving_dijkstra` + 2. Copy the web socket URL from the logs + 1. E.g, if the logs say `DevTools listening on ws://0.0.0.0:9222/devtools/browser/e6447a74-d83f-4f4e-a8e9-388b5216e0c2`, then the URl is `ws://0.0.0.0:9222/devtools/browser/e6447a74-d83f-4f4e-a8e9-388b5216e0c2` +3. Create a new or edit an existing BackstopJS config and add these: +```json + "engineOptions": { + "remote": true, + "remoteOptions": { + "browserWSEndpoint": "ws://localhost:9222/devtools/browser/e6447a74-d83f-4f4e-a8e9-388b5216e0c2" + } + }, +``` + +If everything went OK, now you should be able to use backstop commands as usual. +Notes: +- Since in this example chrome is running in a docker container, it won't see any files on your system, unless they are mounted to it. + This means `url: path/to/index.html` scenarios won't work. But making these work is more of a docker topic, not a BackstopJS one. + ### Using Docker For Testing Across Different Environments @@ -1195,6 +1243,11 @@ For all engines there is also the `debug` setting. This enables verbose console "debug": true ``` +Note, this does not work when using +```json +"remote": true +``` + ### Issues with Chrome-Headless in Docker diff --git a/core/util/runPuppet.js b/core/util/runPuppet.js index 5c5ef7f8..87de791a 100644 --- a/core/util/runPuppet.js +++ b/core/util/runPuppet.js @@ -49,6 +49,54 @@ function loggerAction (action, color, message, ...rest) { console[action](chalk[color](message), ...rest); } +/** + * + * Launch the browser, or connect to it. + * + * @param {Object} puppeteerArgs + * @returns {Promise} + */ +async function obtainBrowser (puppeteerArgs) { + if (puppeteerArgs.remote === true) { + return puppeteer.connect(puppeteerArgs.remoteOptions); + } + + return puppeteer.launch(puppeteerArgs); +} + +/** + * Close the browser, or disconnect from it. + * + * @param {Puppeteer.Browser} browser + * @param {Object} puppeteerArgs + * @returns {Promise<*>} + */ +async function releaseBrowser (browser, puppeteerArgs) { + if (puppeteerArgs.remote === true) { + return browser.disconnect(); + } + + return browser.close(); +} + +/** + * Build the puppeteer args object. + * + * @param {Object} config + * @returns {Object} + */ +function buildPuppeteerArgs (config) { + return Object.assign( + {}, + { + ignoreHTTPSErrors: true, + headless: config.debugWindow ? false : config?.engineOptions?.headless || 'new', + remote: false + }, + config.engineOptions + ); +} + async function processScenarioView (scenario, variantOrScenarioLabelSafe, scenarioLabelSafe, viewport, config, logger) { const { scenarioDefaults = {} } = config; @@ -76,16 +124,9 @@ async function processScenarioView (scenario, variantOrScenarioLabelSafe, scenar const VP_W = viewport.width || viewport.viewport.width; const VP_H = viewport.height || viewport.viewport.height; - const puppeteerArgs = Object.assign( - {}, - { - ignoreHTTPSErrors: true, - headless: config.debugWindow ? false : config?.engineOptions?.headless || 'new' - }, - config.engineOptions - ); + const puppeteerArgs = buildPuppeteerArgs(config); - const browser = await puppeteer.launch(puppeteerArgs); + const browser = await obtainBrowser(puppeteerArgs); const page = await browser.newPage(); await page.setViewport({ width: VP_W, height: VP_H }); @@ -286,7 +327,7 @@ async function processScenarioView (scenario, variantOrScenarioLabelSafe, scenar error = e; } } else { - await browser.close(); + await releaseBrowser(browser, puppeteerArgs); } if (error) { @@ -355,6 +396,8 @@ async function delegateSelectors ( captureJobs.push(function () { return captureScreenshot(page, browser, null, selectorMap, config, captureList, viewport, logger); }); } + const puppeteerArgs = buildPuppeteerArgs(config); + return new Promise(function (resolve, reject) { let job = null; const errors = []; @@ -378,10 +421,10 @@ async function delegateSelectors ( next(); }).then(async () => { logger.log('green', 'x Close Browser'); - await browser.close(); + await releaseBrowser(browser, puppeteerArgs); }).catch(async (err) => { logger.log('red', err); - await browser.close(); + await releaseBrowser(browser, puppeteerArgs); }).then(_ => compareConfig); } diff --git a/package-lock.json b/package-lock.json index 664ca2a4..6ade1203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@babel/core": "^7.23.6", "@babel/preset-env": "^7.23.6", "@babel/preset-react": "^7.23.3", + "@types/puppeteer": "^7.0.4", "assert": "^2.1.0", "babel-loader": "^9.1.3", "backstop-twentytwenty": "^1.1.0", @@ -2751,6 +2752,16 @@ "@types/node": "*" } }, + "node_modules/@types/puppeteer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-7.0.4.tgz", + "integrity": "sha512-ja78vquZc8y+GM2al07GZqWDKQskQXygCDiu0e3uO0DMRKqE0MjrFBFmTulfPYzLB6WnL7Kl2tFPy0WXSpPomg==", + "deprecated": "This is a stub types definition. puppeteer provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "puppeteer": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.11", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", diff --git a/package.json b/package.json index 59f2c578..0a45cffa 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@babel/core": "^7.23.6", "@babel/preset-env": "^7.23.6", "@babel/preset-react": "^7.23.3", + "@types/puppeteer": "^7.0.4", "assert": "^2.1.0", "babel-loader": "^9.1.3", "backstop-twentytwenty": "^1.1.0",