taiko
Version:
Taiko is a Node.js library for automating Chromium based browsers
231 lines (207 loc) • 6.3 kB
JavaScript
/**
* Copyright 2018 Thoughtworks Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This module is imported from Puppeteer(https://github.com/GoogleChrome/puppeteer)
* Few modifications are done on the file.
*/
const fs = require("fs-extra");
const { readdir, access, removeSync, existsSync } = fs;
const { join, basename } = require("node:path");
const { promisify } = require("node:util");
const { helper, assert } = require("../helper");
const { createInterface } = require("node:readline");
const { parse } = require("node:url");
const BrowserMetadata = require("./metadata");
const metadata = new BrowserMetadata();
const supportedPlatforms = ["mac-arm64", "mac-x64", "linux", "win32", "win64"];
const readdirAsync = promisify(readdir.bind(fs));
function existsAsync(filePath) {
let fulfill = null;
const promise = new Promise((x) => {
fulfill = x;
});
access(filePath, (err) => fulfill(!err));
return promise;
}
class Browser {
/**
* @param {!Browser.Options=} options
*/
constructor() {
this._downloadsFolder = join(helper.projectRoot(), ".local-chromium");
this._platform = metadata.platform();
this.revisionInfo = metadata.revisionInfo();
}
/**
* @return {string}
*/
platform() {
return this._platform;
}
/**
* @return {!Promise<!Array<string>>}
*/
async localRevisions() {
if (!(await existsAsync(this._downloadsFolder))) {
return [];
}
const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames
.map((fileName) => parseFolderPath(fileName))
.filter((entry) => entry && entry.platform === this._platform)
.map((entry) => entry.revision);
}
/**
* @param {string} revision
* @return {!Promise}
*/
async remove(revision) {
const folderPath = this._getFolderPath(revision);
assert(
await existsAsync(folderPath),
`Failed to remove: revision ${revision} is not downloaded`,
);
removeSync(folderPath);
}
_resolveExecutablePath() {
const executablePath = process.env.TAIKO_BROWSER_PATH;
if (executablePath) {
const missingText = !existsSync(executablePath)
? `Tried to use TAIKO_BROWSER_PATH env variable to launch browser but did not find any executable at: ${executablePath}`
: null;
return { executablePath, missingText };
}
const metadata = require(join(helper.projectRoot(), "package.json"));
if (!metadata.taiko) {
return {
executablePath,
missingText:
"Cannot find browser executable information in package.json, please set TAIKO_BROWSER_PATH env variable",
};
}
const missingText = !this.revisionInfo.local
? "Chromium revision is not downloaded. Provide TAIKO_BROWSER_PATH or Install taiko again to download bundled chromium."
: null;
return {
executablePath: this.revisionInfo.executablePath,
missingText,
};
}
getExecutablePath() {
const { missingText, executablePath } = this._resolveExecutablePath();
if (missingText) {
throw new Error(missingText);
}
return executablePath;
}
waitForWSEndpoint(browserProcess, timeout) {
return new Promise((resolve, reject) => {
const rl = createInterface({
input: browserProcess.stderr,
});
let stderr = "";
const listeners = [
helper.addEventListener(rl, "line", onLine),
helper.addEventListener(rl, "close", () => onClose()),
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 browser!${error ? ` ${error.message}` : ""}\n${stderr}`,
),
);
}
function onTimeout() {
cleanup();
reject(
new Error(
`Timed out after ${timeout} ms while trying to connect to Browser!`,
),
);
}
/**
* @param {string} line
*/
function onLine(line) {
stderr += `${line}\n`;
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match) {
return;
}
cleanup();
const endpoint = {
host: parse(match[1]).hostname,
port: parse(match[1]).port,
browser: parse(match[1]).href,
};
resolve(endpoint);
}
function cleanup() {
if (timeoutId) {
clearTimeout(timeoutId);
}
helper.removeEventListeners(listeners);
}
});
}
/**
* @param {string} revision
* @return {string}
*/
_getFolderPath(revision) {
return join(this._downloadsFolder, `${this._platform}-${revision}`);
}
}
module.exports = Browser;
/**
* @param {string} folderPath
* @return {?{platform: string, revision: string}}
*/
function parseFolderPath(folderPath) {
const name = basename(folderPath);
const splits = name.split("-");
if (splits.length !== 2) {
return null;
}
const [platform, revision] = splits;
if (!supportedPlatforms.includes(platform)) {
return null;
}
return { platform, revision };
}
/**
* @typedef {Object} BrowserFetcher.Options
* @property {string=} platform
* @property {string=} path
* @property {string=} host
*/
/**
* @typedef {Object} BrowserFetcher.RevisionInfo
* @property {string} folderPath
* @property {string} executablePath
* @property {string} url
* @property {boolean} local
* @property {string} revision
*/