appium-chromedriver
Version:
Node.js wrapper around chromedriver.
314 lines • 12.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Chromedriver = void 0;
const node_events_1 = __importDefault(require("node:events"));
const base_driver_1 = require("@appium/base-driver");
const support_1 = require("@appium/support");
const teen_process_1 = require("teen_process");
const utils_1 = require("./utils");
const storage_client_1 = require("./storage-client/storage-client");
const constants_1 = require("./constants");
const binary_1 = require("./commands/binary");
const version_1 = require("./commands/version");
const process_1 = require("./commands/process");
const session_1 = require("./commands/session");
// Keep this import marked as used at runtime when it is otherwise only referenced in type positions.
void base_driver_1.PROTOCOLS;
const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_PORT = 9515;
// consider chromedriver ready once startup banner appears
const chromedriverStdoutStartDetector = (stdout) => stdout.startsWith('Starting ');
class Chromedriver extends node_events_1.default.EventEmitter {
static EVENT_ERROR = constants_1.CHROMEDRIVER_EVENTS.ERROR;
static EVENT_CHANGED = constants_1.CHROMEDRIVER_EVENTS.CHANGED;
static STATE_STOPPED = constants_1.CHROMEDRIVER_STATES.STOPPED;
static STATE_STARTING = constants_1.CHROMEDRIVER_STATES.STARTING;
static STATE_ONLINE = constants_1.CHROMEDRIVER_STATES.ONLINE;
static STATE_STOPPING = constants_1.CHROMEDRIVER_STATES.STOPPING;
static STATE_RESTARTING = constants_1.CHROMEDRIVER_STATES.RESTARTING;
proxyPort;
adb;
cmdArgs;
proc;
useSystemExecutable;
chromedriver;
executableDir;
mappingPath;
bundleId;
executableVerified;
state;
_execFunc;
jwproxy;
isCustomExecutableDir;
verbose;
logPath;
disableBuildCheck;
storageClient;
details;
capabilities;
_desiredProtocol;
_driverVersion;
_onlineStatus;
_log;
proxyHost;
buildChromedriverArgs = process_1.buildChromedriverArgs;
getDriversMapping = binary_1.getDriversMapping;
getChromedrivers = binary_1.getChromedrivers;
updateDriversMapping = binary_1.updateDriversMapping;
getCompatibleChromedriver = binary_1.getCompatibleChromedriver;
initChromedriverPath = binary_1.initChromedriverPath;
getChromeVersion = version_1.getChromeVersionForAutodetection;
syncProtocol = session_1.syncProtocol;
waitForOnline = process_1.waitForOnline;
getStatus = process_1.getStatus;
killAll = process_1.killAll;
changeState = session_1.changeState;
startSession = session_1.startSession;
constructor(args = {}) {
super();
const { host = DEFAULT_HOST, port = DEFAULT_PORT, useSystemExecutable = false, executable, executableDir, bundleId, mappingPath, cmdArgs, adb, verbose, logPath, disableBuildCheck, details, isAutodownloadEnabled = false, reqBasePath, } = args;
this._log = support_1.logger.getLogger((0, utils_1.generateLogPrefix)(this));
this.proxyHost = host;
this.proxyPort = parseInt(String(port), 10);
this.adb = adb;
this.cmdArgs = cmdArgs;
this.proc = null;
this.useSystemExecutable = useSystemExecutable;
this.chromedriver = executable;
this.mappingPath = mappingPath;
this.bundleId = bundleId;
this.executableVerified = false;
this.state = Chromedriver.STATE_STOPPED;
this._execFunc = teen_process_1.exec;
const proxyOpts = { server: this.proxyHost, port: this.proxyPort, log: this._log };
if (reqBasePath) {
proxyOpts.reqBasePath = reqBasePath;
}
this.jwproxy = new base_driver_1.JWProxy(proxyOpts);
if (executableDir) {
this.executableDir = executableDir;
this.isCustomExecutableDir = true;
}
else {
this.executableDir = (0, utils_1.getChromedriverDir)();
this.isCustomExecutableDir = false;
}
this.verbose = verbose;
this.logPath = logPath;
this.disableBuildCheck = !!disableBuildCheck;
this.storageClient = isAutodownloadEnabled
? new storage_client_1.ChromedriverStorageClient({ chromedriverDir: this.executableDir })
: null;
this.details = details;
this.capabilities = {};
this._desiredProtocol = null;
this._driverVersion = null;
this._onlineStatus = null;
}
get log() {
return this._log;
}
get driverVersion() {
return this._driverVersion;
}
/**
* Starts a new Chromedriver session with the given capabilities.
*
* @param caps - Capabilities passed to Chromedriver session creation.
* @param emitStartingState - Whether to emit the `starting` state transition.
* @returns Session capabilities returned by Chromedriver.
*/
async start(caps, emitStartingState = true) {
this.capabilities = this.prepareCapabilitiesForSessionStart(caps);
if (emitStartingState) {
this.changeState(Chromedriver.STATE_STARTING);
}
const args = this.buildChromedriverArgs();
const webviewVersionHolder = {};
try {
await this.launchChromedriverProcess(args, webviewVersionHolder);
this.syncProtocol();
return await this.startSession();
}
catch (e) {
return await this.handleChromedriverStartFailure(e, webviewVersionHolder.version);
}
}
/**
* Gets active Chromedriver session id if the driver is online.
*
* @returns The session id or `null` when driver is not online.
*/
sessionId() {
return this.state === Chromedriver.STATE_ONLINE ? this.jwproxy.sessionId : null;
}
/**
* Restarts current Chromedriver session with previously stored capabilities.
*
* @returns Session capabilities returned by the restarted session.
*/
async restart() {
this.log.info('Restarting chromedriver');
if (this.state !== Chromedriver.STATE_ONLINE) {
throw new Error("Can't restart when we're not online");
}
this.changeState(Chromedriver.STATE_RESTARTING);
await this.stop(false);
return await this.start(this.capabilities, false);
}
/**
* Stops the current Chromedriver session and underlying subprocess.
*
* @param emitStates - Whether to emit stopping/stopped state transitions.
*/
async stop(emitStates = true) {
if (emitStates) {
this.changeState(Chromedriver.STATE_STOPPING);
}
const runSafeStep = async (f) => {
try {
return await f();
}
catch (e) {
const err = e;
this.log.warn(err.message);
this.log.debug(err.stack);
}
};
await runSafeStep(() => this.jwproxy.command('', 'DELETE'));
await runSafeStep(async () => {
await this.proc?.stop('SIGTERM', 20000);
this.proc?.removeAllListeners();
this.proc = null;
});
this.log.prefix = (0, utils_1.generateLogPrefix)(this);
if (emitStates) {
this.changeState(Chromedriver.STATE_STOPPED);
}
}
/**
* Sends a direct command to Chromedriver through the JSONWP/W3C proxy.
*
* @param url - Chromedriver endpoint path.
* @param method - HTTP method used for the command.
* @param body - Optional request payload.
* @returns Command response payload.
*/
async sendCommand(url, method, body = null) {
return await this.jwproxy.command(url, method, body);
}
/**
* Proxies an incoming Express request/response pair to Chromedriver.
*
* @param req - Incoming request object.
* @param res - Outgoing response object.
*/
async proxyReq(req, res) {
await this.jwproxy.proxyReqRes(req, res);
}
/**
* Checks whether the active webview connection is currently responsive.
*
* @returns `true` if `/url` command succeeds, otherwise `false`.
*/
async hasWorkingWebview() {
try {
await this.jwproxy.command('/url', 'GET');
return true;
}
catch {
return false;
}
}
prepareCapabilitiesForSessionStart(caps) {
const capabilities = structuredClone(caps);
// set the logging preferences to ALL browser console logs by default
capabilities.loggingPrefs = structuredClone((0, session_1.getCapValue)(caps, 'loggingPrefs', {}));
if (support_1.util.isEmpty(capabilities.loggingPrefs.browser)) {
capabilities.loggingPrefs.browser = 'ALL';
}
return capabilities;
}
attachChromedriverProcessListeners(webviewVersionHolder) {
const proc = this.proc;
if (!proc) {
throw new Error('Chromedriver subprocess must be assigned before attaching listeners');
}
for (const streamName of ['stderr', 'stdout']) {
proc.on(`line-${streamName}`, (line) => {
// if chromedriver does not print explicit Chrome version support,
// infer webview version from DevTools banner for better errors
if (!webviewVersionHolder.version) {
const match = /"Browser": "([^"]+)"/.exec(line);
if (match) {
webviewVersionHolder.version = match[1];
this.log.debug(`Webview version: '${webviewVersionHolder.version}'`);
}
}
if (this.verbose) {
this.log.debug(`[${streamName.toUpperCase()}] ${line}`);
}
});
}
proc.once('exit', (code, signal) => {
this._driverVersion = null;
this._desiredProtocol = null;
this._onlineStatus = null;
if (this.state !== Chromedriver.STATE_STOPPED &&
this.state !== Chromedriver.STATE_STOPPING &&
this.state !== Chromedriver.STATE_RESTARTING) {
this.log.error(`Chromedriver exited unexpectedly with code ${code}, signal ${signal}`);
this.changeState(Chromedriver.STATE_STOPPED);
}
this.proc?.removeAllListeners();
this.proc = null;
});
}
async launchChromedriverProcess(args, webviewVersionHolder) {
const chromedriverPath = await this.initChromedriverPath();
// remove stale chromedriver/adb-forward leftovers before launching
await this.killAll();
this.proc = new teen_process_1.SubProcess(chromedriverPath, args);
this.attachChromedriverProcessListeners(webviewVersionHolder);
this.log.info(`Spawning Chromedriver with: ${this.chromedriver} ${args.join(' ')}`);
await this.proc.start(chromedriverStdoutStartDetector);
// wait until /status says ready, then negotiate protocol and start session
await this.waitForOnline();
}
formatChromeVersionMismatchHint(err, webviewVersion) {
if (!err.message.includes('Chrome version must be')) {
return '';
}
// enrich the common version-mismatch error with actionable context
let message = 'Unable to automate Chrome version because it is not supported by this version of Chromedriver.\n';
if (webviewVersion) {
message += `Chrome version on the device: ${webviewVersion}\n`;
}
const versionsSupportedByDriver = /Chrome version must be (.+)/.exec(err.message)?.[1] || '';
if (versionsSupportedByDriver) {
message += `Chromedriver supports Chrome version(s): ${versionsSupportedByDriver}\n`;
}
message += 'Check the driver tutorial for troubleshooting.\n';
return message;
}
async handleChromedriverStartFailure(err, webviewVersion) {
this.log.debug(err);
this.emit(Chromedriver.EVENT_ERROR, err);
// an error does not always mean subprocess has already exited
if (this.proc) {
try {
await this.proc.stop();
}
catch { }
}
this.proc?.removeAllListeners();
this.proc = null;
const message = this.formatChromeVersionMismatchHint(err, webviewVersion) + err.message;
throw new Error(message);
}
}
exports.Chromedriver = Chromedriver;
//# sourceMappingURL=chromedriver.js.map