@eyeo/get-browser-binary
Version:
Install browser binaries and matching webdrivers
387 lines (321 loc) • 14.3 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 {expect} from "expect";
import path from "path";
import Jimp from "jimp";
import {By} from "selenium-webdriver";
import {BROWSERS, snapshotsBaseDir, takeFullPageScreenshot} from "../index.js";
import {killDriverProcess} from "../src/utils.js";
import {TEST_SERVER_URL} from "./test-server.js";
const VERSIONS = {
chromium: ["latest", "77.0.3865.0", "beta", "dev"],
firefox: ["latest", "68.0", "beta", "dev"],
edge: ["latest", "114.0.1823.82", "beta", "dev"]
};
const TEST_URL_BASIC = `${TEST_SERVER_URL}/basic.html`;
const PROXY_URL_BASIC = "http://testpages.adblockplus.org/basic.html";
const TEST_URL_LONG = `${TEST_SERVER_URL}/long.html`;
async function switchToHandle(driver, testFn) {
for (let handle of await driver.getAllWindowHandles()) {
let url;
try {
await driver.switchTo().window(handle);
url = await driver.getCurrentUrl();
}
catch (e) {
continue;
}
if (testFn(url))
return handle;
}
}
async function getHandle(driver, page) {
let url;
let handle = await driver.wait(() => switchToHandle(driver, handleUrl => {
if (!handleUrl)
return false;
url = new URL(handleUrl);
return url.pathname == page;
}), 8000, `${page} did not open`);
return handle;
}
function normalize(version) {
// Discards any numbers after the third dot. For example, "103.0.5060.134"
// will return "103.0.5060", and "68.0" will return "68.0"
let normalized = version.split(".").slice(0, 3).join(".");
// On Windows, Firefox beta versions look like "103.0b9", but the installed
// version returned by the browser is actually "103.0"
return normalized.split("b")[0];
}
function getWindowSize(driver) {
return driver.executeScript(() => {
return {height: window.innerHeight, width: window.innerWidth};
});
}
expect.extend({
toMeasureLessThan(small, big) {
let pass = small.width < big.width || small.height < big.height;
let message = () =>
`expected small sizes (w: ${small.width}, h: ${small.height}) ` +
`to be smaller than big sizes (w: ${big.width}, h: ${big.height})`;
if (pass)
return {message, pass: true};
return {message, pass: false};
}
});
function getExtension(browser, version) {
let manifest = "mv2";
// The latest Edge installed on the Gitlab Windows Shared Runners is Edge 79,
// which does not support manifest v3. More info:
// https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/29
let windowsSharedRunners =
browser == "edge" && process.platform == "win32" && process.env.CI_JOB_ID;
if (["chromium", "edge"].includes(browser) &&
["latest", "beta", "dev"].includes(version) && !windowsSharedRunners)
manifest = "mv3";
let extensionPaths =
[path.resolve(process.cwd(), "test", "extension", manifest)];
return {extensionPaths, manifest};
}
async function getInstallFileCTime(browser) {
// Browsers installed at OS level are not tested
if (browser == "edge" && (process.platform == "win32" ||
process.platform == "darwin"))
return null;
const installTypes = [".zip", ".dmg", ".bz2", ".xz", ".deb", ".exe"];
let cacheDir = path.join(snapshotsBaseDir, browser, "cache");
let cacheFiles = await fs.promises.readdir(cacheDir);
let browserZip =
cacheFiles.find(elem => installTypes.some(type => elem.includes(type)));
if (!browserZip)
throw new Error(`Files in ${cacheDir} don't belong to any known install file types: ${installTypes}`);
return (await fs.promises.stat(path.join(cacheDir, browserZip))).ctimeMs;
}
const browserNames = {
chromium: "chrome",
firefox: "firefox",
edge: /(MicrosoftEdge|msedge)/
};
async function basicUrlTest(driver, browser) {
await driver.navigate().to(TEST_URL_BASIC);
expect((await driver.getCapabilities()).getBrowserName())
.toEqual(expect.stringMatching(browserNames[browser]));
let text = await driver.findElement(By.id("basic")).getText();
expect(text).toEqual("Test server basic page");
}
// Adding the browser version to the test title for logging purposes
function addVersionToTitle(ctx, version) {
ctx.test.title = `${ctx.test.title} [v${version}]`;
}
function isOldHeadlessMode(browser, version) {
// Chromium's old headless mode doesn't support loading extensions
return browser != "firefox" && (
!["latest", "beta", "dev"].includes(version) ||
// Edge on the Windows CI is v79, that is old headless
(browser == "edge" && process.platform == "win32"));
}
for (let browser of Object.keys(BROWSERS)) {
describe(`Browser: ${browser}`, () => {
before(async() => {
if (process.env.TEST_KEEP_SNAPSHOTS == "true")
return;
await fs.promises.rm(snapshotsBaseDir, {force: true, recursive: true});
});
for (let version of VERSIONS[browser]) {
describe(`Version: ${version}`, () => {
let driver = null;
let firstInstallFileCTime = null;
let customBrowserBinary = null;
let failedInstall = false;
async function quitDriver() {
if (!driver)
return;
await driver.quit();
driver = null;
// Some platforms don't immediately kill the driver process
if (browser == "chromium")
await killDriverProcess("chromedriver");
else if (browser == "firefox")
await killDriverProcess("geckodriver");
// https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/77
if (process.platform == "win32" && browser == "chromium") {
await killDriverProcess("chrome");
}
else if (process.platform == "win32" && browser == "edge") {
await killDriverProcess("msedgedriver");
await killDriverProcess("msedge");
}
}
beforeEach(function() {
if (browser == "edge" && process.platform != "linux" &&
version == "114.0.1823.82")
this.skip();
if (failedInstall)
this.skip();
});
afterEach(quitDriver);
it("installs", async function() {
// installing browsers may take a while on the CI
this.timeout(150000);
let binary;
let versionNumber;
try {
({binary, versionNumber} =
await BROWSERS[browser].installBrowser(version, this.timeout()));
}
catch (err) {
failedInstall = true;
throw err;
}
let browserName = browser == "edge" ? /(edge|Edge)/ : browser;
expect(binary).toEqual(expect.stringMatching(browserName));
let installedVersion =
await BROWSERS[browser].getInstalledVersion(binary);
expect(installedVersion).toEqual(
expect.stringContaining(normalize(versionNumber)));
addVersionToTitle(this, versionNumber);
// Data used in further tests
customBrowserBinary = binary;
firstInstallFileCTime = await getInstallFileCTime(browser);
});
it("runs", async function() {
driver = await BROWSERS[browser].getDriver(version);
const url = await driver.getCurrentUrl();
// check that default automation tab is in focus:
// data;, (chromium) and about:blank (firefox)
expect(url).toEqual(expect.stringMatching(/^(data:,|about:blank)$/));
await basicUrlTest(driver, browser);
let browserVersion =
(await driver.getCapabilities()).getBrowserVersion();
addVersionToTitle(this, browserVersion);
});
// This test depends on running the "installs" test
it("uses cached install files", async function() {
if (!firstInstallFileCTime)
this.skip();
// assigning `driver` to allow the afterEach hook quit the driver
driver = await BROWSERS[browser].getDriver(version);
let secondInstallFileCTime = await getInstallFileCTime(browser);
expect(secondInstallFileCTime).toEqual(firstInstallFileCTime);
});
// This test depends on running the "installs" test
it("runs a custom browser binary", async function() {
if (!customBrowserBinary)
this.skip();
driver =
await BROWSERS[browser].getDriver(version, {customBrowserBinary});
await basicUrlTest(driver, browser);
expect((await driver.getCapabilities()).getBrowserName())
.toEqual(expect.stringMatching(browserNames[browser]));
});
it("supports extra args", async() => {
let headless = false;
let extraArgs = browser == "firefox" ?
["-width=600", "-height=400"] : ["window-size=600,400"];
driver = await BROWSERS[browser].getDriver(
version, {headless, extraArgs});
await driver.navigate().to(TEST_URL_BASIC);
let sizeSmall = await getWindowSize(driver);
await quitDriver();
driver = await BROWSERS[browser].getDriver(version, {headless});
await driver.navigate().to(TEST_URL_BASIC);
let sizeDefault = await getWindowSize(driver);
expect(sizeSmall).toMeasureLessThan(sizeDefault);
});
it("supports proxy", async function() {
if (browser != "firefox")
this.skip();
let headless = true;
let proxy = "http://localhost:3000/proxy-config.pac";
let extraArgs = [];
driver = await BROWSERS[browser].getDriver(
version, {headless, extraArgs, proxy});
await driver.navigate().to(PROXY_URL_BASIC);
let text = await driver.findElement(By.id("basic")).getText();
expect(text).toEqual("Test server basic page");
await quitDriver();
});
it("takes a full page screenshot", async() => {
driver = await BROWSERS[browser].getDriver(version);
await driver.navigate().to(TEST_URL_LONG);
let fullImg = await takeFullPageScreenshot(driver);
// Taking a regular webdriver screenshot, which should be shorter
let data = await driver.takeScreenshot();
let partImg = await Jimp.read(Buffer.from(data, "base64"));
expect(fullImg.bitmap.width).toBeGreaterThan(0);
expect(fullImg.bitmap.height).toBeGreaterThan(partImg.bitmap.height);
});
// The only reason the geckodriver package is required in package.json
// is to install a specific version needed by Firefox 68.0, otherwise it
// fails to load extensions. When the oldest Firefox version is bumped,
// Selenium's automated driver download should be able to manage it.
// https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/44
it("loads an extension", async() => {
let headless = !isOldHeadlessMode(browser, version);
let {extensionPaths} = getExtension(browser, version);
driver = await BROWSERS[browser].getDriver(
version, {headless, extensionPaths});
await getHandle(driver, "/index.html");
});
it("loads an extension in incognito mode", async function() {
if (browser == "firefox" && version == "68.0")
this.skip();
let {extensionPaths, manifest} = getExtension(browser, version);
driver = await BROWSERS[browser].getDriver(
version, {headless: false, extensionPaths, incognito: true});
await BROWSERS[browser].enableExtensionInIncognito(
driver, `Browser test extension - ${manifest}`
);
await getHandle(driver, "/index.html");
});
it("updates an extension", async() => {
let headless = !isOldHeadlessMode(browser, version);
let {extensionPaths, manifest} = getExtension(browser, version);
let tmpExtensionDir = path.join(snapshotsBaseDir, "extension");
await fs.promises.rm(tmpExtensionDir, {recursive: true, force: true});
await fs.promises.cp(extensionPaths[0], tmpExtensionDir,
{recursive: true});
driver = await BROWSERS[browser].getDriver(
version, {headless, extensionPaths: [tmpExtensionDir]});
await getHandle(driver, "/index.html");
let text = await driver.findElement(By.id("title")).getText();
expect(text).toEqual(`Browser test extension - ${manifest}`);
// The page is modified and reloaded to emulate an extension update
await fs.promises.cp(path.join(tmpExtensionDir, "update.html"),
path.join(tmpExtensionDir, "index.html"));
await driver.navigate().refresh();
text = await driver.findElement(By.id("title")).getText();
expect(text).toEqual(`Updated test extension - ${manifest}`);
});
});
}
describe("Any version", async() => {
it("does not install unsupported versions", async() => {
for (let unsupported of ["0.0", "invalid"]) {
await expect(BROWSERS[browser].installBrowser(unsupported))
.rejects.toThrow(`Unsupported browser version: ${unsupported}`);
}
});
it("does not load an invalid extension", async() => {
let extensionPaths = [process.cwd()];
await expect(BROWSERS[browser].getDriver("latest", {extensionPaths}))
.rejects.toThrow(`Extension manifest file not found: ${extensionPaths[0]}`);
});
});
});
}