UNPKG

@eyeo/get-browser-binary

Version:

Install browser binaries and matching webdrivers

287 lines (257 loc) 9.08 kB
/* * Copyright (c) 2006-present eyeo GmbH * * This module is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import fs from "fs"; import path from "path"; import {pipeline} from "stream"; import {promisify} from "util"; import {exec} from "child_process"; import got from "got"; import dmg from "dmg"; import Jimp from "jimp"; export const errMsg = { unsupportedVersion: "Unsupported browser version", unsupportedPlatform: "Unsupported platform", driverDownload: "Driver download failed", driverStart: "Unable to start driver", extensionNotFound: "Extension not found", browserDownload: "Browser download failed", browserNotInstalled: "Browser is not installed", browserVersionCheck: "Checking the browser version failed", elemNotFound: "HTML element not found", manifestNotFound: "Extension manifest file not found" }; export const platformArch = `${process.platform}-${process.arch}`; /** * Root folder where browser and webdriver files get downloaded and extracted. * @type {string} */ export let snapshotsBaseDir = path.join(process.cwd(), "browser-snapshots"); /** * Downloads url resources. * @param {string} url The url of the resource to be downloaded. * @param {string} destFile The destination file path. * @param {number} [timeout=0] Allowed time in ms for the download to complete. * When set to 0 there is no time limit. * @throws {TypeError} Invalid URL, Download timeout. */ export async function download(url, destFile, timeout = 0) { if (process.env.VERBOSE == "true") // eslint-disable-next-line no-console console.log(`Downloading from ${url} ...`); let cacheDir = path.dirname(destFile); await fs.promises.mkdir(cacheDir, {recursive: true}); let tempDest = `${destFile}-${process.pid}`; let writable = fs.createWriteStream(tempDest); let timeoutID; try { let downloading = promisify(pipeline)(got.stream(url), writable); if (timeout == 0) { await downloading; } else { let timeoutPromise = new Promise((resolve, reject) => { timeoutID = setTimeout( () => reject(`Download timeout after ${timeout}ms`), timeout); }); await Promise.race([downloading, timeoutPromise]); } } catch (error) { await fs.promises.rm(tempDest, {force: true}); throw error; } finally { if (timeoutID) clearTimeout(timeoutID); } await fs.promises.rename(tempDest, destFile); } export async function extractTar(archive, dir) { await fs.promises.mkdir(dir); let command; if (archive.endsWith(".xz")) command = `tar -xf ${archive} -C ${dir}`; else command = `tar -jxf ${archive} -C ${dir}`; await promisify(exec)(command); } export async function extractDmg(archive, dir) { let mpath = await promisify(dmg.mount)(archive); let files = await fs.promises.readdir(mpath); let target = files.find(file => path.extname(file) == ".app"); let source = path.join(mpath, target); await fs.promises.mkdir(dir); try { await fs.promises.cp(source, path.join(dir, target), {recursive: true}); } finally { try { await promisify(dmg.unmount)(mpath); } catch (err) { console.error(`Error unmounting DMG: ${err}`); } } } // Useful to unlock the driver file before replacing it or executing it export async function killDriverProcess(driverName) { let cmd = `kill $(pgrep ${driverName})`; let shell; if (process.platform == "win32") { cmd = `Get-Process -Name ${driverName} | Stop-Process; Start-Sleep 1`; shell = "powershell.exe"; } try { await promisify(exec)(cmd, {shell}); } catch (err) { // Command will fail when the driver process is not found if (!err.toString().includes("Command failed")) throw err; } } export function wait(condition, timeout = 0, message, pollTimeout = 100) { if (typeof condition !== "function") throw TypeError("Wait condition must be a function"); function evaluateCondition() { return new Promise((resolve, reject) => { try { resolve(condition(this)); } catch (ex) { reject(ex); } }); } let result = new Promise((resolve, reject) => { let startTime = Date.now(); let pollCondition = async() => { evaluateCondition().then(value => { let elapsed = Date.now() - startTime; if (value) { resolve(value); } else if (timeout && elapsed >= timeout) { try { let timeoutMessage = message ? `${typeof message === "function" ? message() : message}\n` : ""; reject( new Error(`${timeoutMessage}Wait timed out after ${elapsed}ms`) ); } catch (ex) { reject( new Error(`${ex.message}\nWait timed out after ${elapsed}ms`) ); } } else { setTimeout(pollCondition, pollTimeout); } }, reject); }; pollCondition(); }); return result; } /** * @typedef {Object} Jimp * @see https://github.com/oliver-moran/jimp/tree/master/packages/jimp */ /** * Takes a screenshot of the full page by scrolling from top to bottom. * @param {webdriver} driver The driver controlling the browser. * @param {boolean} [hideScrollbars=true] Hides any scrollbars before taking * the screenshot, or not. * @return {Jimp} A Jimp image object containing the screenshot. * @example * // Getting a base-64 encoded PNG from the returned Jimp image * let image = await takeFullPageScreenshot(driver); * let encodedPNG = await image.getBase64Async("image/png"); */ export async function takeFullPageScreenshot(driver, hideScrollbars = true) { // On macOS scrollbars appear and disappear overlapping the content as // scrolling occurs. Hiding the scrollbars helps getting reproducible // screenshots. if (hideScrollbars) { await driver.executeScript(() => { if (!document.head) return; let style = document.createElement("style"); style.textContent = "html { overflow-y: scroll; }"; document.head.appendChild(style); if (document.documentElement.clientWidth == window.innerWidth) style.textContent = "html::-webkit-scrollbar { display: none; }"; else document.head.removeChild(style); }); } let fullScreenshot = new Jimp(0, 0); while (true) { let [width, height, offset] = await driver.executeScript((...args) => { window.scrollTo(0, args[0]); // Math.ceil rounds up potential decimal values on window.scrollY, // ensuring the loop will not hang due to never reaching enough // fullScreenshot's height. return [document.documentElement.clientWidth, document.documentElement.scrollHeight, Math.ceil(window.scrollY)]; }, fullScreenshot.bitmap.height); let data = await driver.takeScreenshot(); let partialScreenshot = await Jimp.read(Buffer.from(data, "base64")); let combinedScreenshot = new Jimp(width, offset + partialScreenshot.bitmap.height); combinedScreenshot.composite(fullScreenshot, 0, 0); combinedScreenshot.composite(partialScreenshot, 0, offset); fullScreenshot = combinedScreenshot; if (fullScreenshot.bitmap.height >= height) break; } return fullScreenshot; } /** * Returns the major version of a browser version number. * @param {string} versionNumber Full browser version number. * @return {number} Major version number. * @throws {Error} Unsupported browser version. */ export function getMajorVersion(versionNumber) { let majorVersion = parseInt(versionNumber && versionNumber.split(".")[0], 10); if (isNaN(majorVersion)) throw new Error(`${errMsg.unsupportedVersion}: ${versionNumber}`); return majorVersion; } export function checkVersion(version, minVersion, channels = []) { if (channels.includes(version)) return; if (getMajorVersion(version) < minVersion) throw new Error(`${errMsg.unsupportedVersion}: ${version}`); } export function checkPlatform() { if (!["win32", "linux", "darwin"].includes(process.platform)) throw new Error(`${errMsg.unsupportedPlatform}: ${process.platform}`); } export async function checkExtensionPaths(extensionPaths) { for (let extensionPath of extensionPaths) { try { await fs.promises.access(path.join(extensionPath, "manifest.json")); } catch (err) { throw new Error(`${errMsg.manifestNotFound}: ${extensionPath}`); } } }