@eyeo/get-browser-binary
Version:
Install browser binaries and matching webdrivers
287 lines (257 loc) • 9.08 kB
JavaScript
/*
* 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}`);
}
}
}