@appium/base-driver
Version:
Base driver class for Appium drivers
363 lines • 14.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DriverCore = void 0;
const async_lock_1 = __importDefault(require("async-lock"));
const lodash_1 = __importDefault(require("lodash"));
const node_os_1 = __importDefault(require("node:os"));
const constants_1 = require("../constants");
const protocol_1 = require("../protocol");
const device_settings_1 = require("./device-settings");
const helpers = __importStar(require("./helpers"));
const extension_core_1 = require("./extension-core");
const NEW_COMMAND_TIMEOUT_MS = 60 * 1000;
const ON_UNEXPECTED_SHUTDOWN_EVENT = 'onUnexpectedShutdown';
const ALL_DRIVERS_MATCH = '*';
const FEATURE_NAME_SEPARATOR = ':';
class DriverCore extends extension_core_1.ExtensionCore {
/**
* Make the basedriver version available so for any driver which inherits from this package, we
* know which version of basedriver it inherited from
*/
static baseVersion = helpers.BASEDRIVER_VER;
sessionId;
sessionCreationTimestampMs;
opts;
initialOpts;
helpers;
/**
* basePath is used for several purposes, for example in setting up
* proxying to other drivers, since we need to know what the base path
* of any incoming request might look like. We set it to the default
* initially but it is automatically updated during any actual program
* execution by the routeConfiguringFunction, which is necessarily run as
* the entrypoint for any Appium server
*/
basePath;
relaxedSecurityEnabled;
allowInsecure;
denyInsecure;
newCommandTimeoutMs;
implicitWaitMs;
locatorStrategies;
webLocatorStrategies;
managedDrivers;
noCommandTimer;
shutdownUnexpectedly;
shouldValidateCaps;
/**
* settings should be instantiated by drivers which extend BaseDriver, but
* we set it to an empty DeviceSettings instance here to make sure that the
* default settings are applied even if an extending driver doesn't utilize
* the settings functionality itself
*/
settings;
protocol;
_eventHistory;
/**
* TODO: remove this._log and use this.log instead
*/
_log;
commandsQueueGuard;
constructor(opts = {}, shouldValidateCaps = true) {
super();
this._log = this.log; // TODO: remove references to this._log and use this.log instead
// setup state
this.opts = opts;
// use a custom tmp dir to avoid losing data and app when computer is
// restarted
this.opts.tmpDir = this.opts.tmpDir || process.env.APPIUM_TMP_DIR || node_os_1.default.tmpdir();
// base-driver internals
this.shouldValidateCaps = shouldValidateCaps;
// keeping track of initial opts
this.initialOpts = lodash_1.default.cloneDeep(opts);
this.sessionId = null;
this.helpers = helpers;
this.basePath = constants_1.DEFAULT_BASE_PATH;
this.relaxedSecurityEnabled = false;
this.allowInsecure = [];
this.denyInsecure = [];
this.newCommandTimeoutMs = NEW_COMMAND_TIMEOUT_MS;
this.implicitWaitMs = 0;
this.locatorStrategies = [];
this.webLocatorStrategies = [];
this.managedDrivers = [];
this.noCommandTimer = null;
this._eventHistory = { commands: [] };
this.shutdownUnexpectedly = false;
this.commandsQueueGuard = new async_lock_1.default();
this.settings = new device_settings_1.DeviceSettings();
}
/**
* This property is used by AppiumDriver to store the data of the
* specific driver sessions. This data can be later used to adjust
* properties for driver instances running in parallel.
* Override it in inherited driver classes if necessary.
*/
get driverData() {
return {};
}
/**
* This property controls the way the `executeCommand` method
* handles new driver commands received from the client.
* Override it for inherited classes only in special cases.
*
* @return If the returned value is true (default) then all the commands
* received by the particular driver instance are going to be put into the queue,
* so each following command will not be executed until the previous command
* execution is completed. False value disables that queue, so each driver command
* is executed independently and does not wait for anything.
*/
get isCommandsQueueEnabled() {
return true;
}
/*
* make eventHistory a property and return a cloned object so a consumer can't
* inadvertently change data outside of logEvent
*/
get eventHistory() {
return lodash_1.default.cloneDeep(this._eventHistory);
}
/**
* If this driver has requested proxying of bidi connections to an upstream bidi endpoint, this
* method should be overridden to return the URL of that websocket, to indicate that bidi
* proxying is enabled. Otherwise, a null return will indicate that bidi proxying should not be
* active and bidi commands will be handled by this driver.
*
* @returns {string | null}
*/
get bidiProxyUrl() {
return null;
}
/**
* Set a callback handler if needed to execute a custom piece of code
* when the driver is shut down unexpectedly. Multiple calls to this method
* will cause the handler to be executed multiple times
*
* @param handler The code to be executed on unexpected shutdown.
* The function may accept one argument, which is the actual error instance, which
* caused the driver to shut down.
*/
onUnexpectedShutdown(handler) {
this.eventEmitter.on(ON_UNEXPECTED_SHUTDOWN_EVENT, handler);
}
/**
* API method for driver developers to log timings for important events
*/
logEvent(eventName) {
if (eventName === 'commands') {
throw new Error('Cannot log commands directly');
}
if (typeof eventName !== 'string') {
throw new Error(`Invalid eventName ${eventName}`);
}
if (!this._eventHistory[eventName]) {
this._eventHistory[eventName] = [];
}
const ts = Date.now();
const logTime = new Date(ts).toTimeString();
this._eventHistory[eventName].push(ts);
this.log.debug(`Event '${eventName}' logged at ${ts} (${logTime})`);
}
/**
* @privateRemarks Overridden in appium driver, but here so that individual drivers can be
* tested with clients that poll
*/
async getStatus() {
return {};
}
/**
* method required by MJSONWP in order to determine whether it should
* respond with an invalid session response
*/
sessionExists(sessionId) {
if (!sessionId)
return false; // eslint-disable-line curly
return sessionId === this.sessionId;
}
/**
* method required by MJSONWP in order to determine if the command should
* be proxied directly to the driver
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
driverForSession(sessionId) {
return this;
}
isMjsonwpProtocol() {
return this.protocol === constants_1.PROTOCOLS.MJSONWP;
}
isW3CProtocol() {
return this.protocol === constants_1.PROTOCOLS.W3C;
}
setProtocolMJSONWP() {
this.protocol = constants_1.PROTOCOLS.MJSONWP;
}
setProtocolW3C() {
this.protocol = constants_1.PROTOCOLS.W3C;
}
/**
* Check whether a given feature is enabled via its name
*
* @param name - name of feature/command
*/
isFeatureEnabled(name) {
// automationName comparison is case-insensitive,
// while feature name is case-sensitive
const currentAutomationName = lodash_1.default.toLower(this.opts.automationName);
const parseFullName = (fullName) => {
const separatorPos = fullName.indexOf(FEATURE_NAME_SEPARATOR);
if (separatorPos <= 0) {
// we do not expect this to happen as
// arguments are validated upon server startup,
// but better be safe than sorry
throw new Error(`The full feature name must include both the automation name ` +
`'${this.opts.automationName}' or the '${ALL_DRIVERS_MATCH}' ` +
`wildcard to apply the feature to all installed drivers, and ` +
`the feature name split by a colon. Got '${fullName}' instead`);
}
return [
lodash_1.default.toLower(fullName.substring(0, separatorPos)),
fullName.substring(separatorPos + 1)
];
};
const parseFullNames = (fullNames) => fullNames.map(parseFullName);
const matches = ([automationName, featureName]) => [currentAutomationName, ALL_DRIVERS_MATCH].includes(automationName) && featureName === name;
// if we have explicitly denied this feature, return false immediately
if (!lodash_1.default.isEmpty(this.denyInsecure) && parseFullNames(this.denyInsecure).some(matches)) {
return false;
}
// if we specifically have allowed the feature, return true
if (!lodash_1.default.isEmpty(this.allowInsecure) && parseFullNames(this.allowInsecure).some(matches)) {
return true;
}
// otherwise, if we've globally allowed insecure features and not denied
// this one, return true
if (this.relaxedSecurityEnabled) {
return true;
}
// if we haven't allowed anything insecure, then reject
return false;
}
/**
* Assert that a given feature is enabled and throw a helpful error if it's
* not
*
* @param name - name of feature/command
*/
assertFeatureEnabled(name) {
if (!this.isFeatureEnabled(name)) {
throw new Error(`Potentially insecure feature '${name}' has not been ` +
`enabled. If you want to enable this feature and accept ` +
`the security ramifications, please do so by following ` +
`the documented instructions at http://appium.io/docs/en/latest/guides/security/`);
}
}
validateLocatorStrategy(strategy, webContext = false) {
let validStrategies = this.locatorStrategies;
this.log.debug(`Valid locator strategies for this request: ${validStrategies.join(', ')}`);
if (webContext) {
validStrategies = validStrategies.concat(this.webLocatorStrategies);
}
if (!lodash_1.default.includes(validStrategies, strategy)) {
throw new protocol_1.errors.InvalidSelectorError(`Locator Strategy '${strategy}' is not supported for this session`);
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
proxyActive(sessionId) {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getProxyAvoidList(sessionId) {
return [];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canProxy(sessionId) {
return false;
}
/**
* Whether a given command route (expressed as method and url) should not be
* proxied according to this driver
*
* @param sessionId - the current sessionId (in case the driver runs
* multiple session ids and requires it). This is not used in this method but
* should be made available to overridden methods.
* @param method - HTTP method of the route
* @param url - url of the route
* @param [body] - webdriver request body
*
* @returns whether the route should be avoided
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
proxyRouteIsAvoided(sessionId, method, url, body) {
for (const avoidSchema of this.getProxyAvoidList(sessionId)) {
if (!lodash_1.default.isArray(avoidSchema) || avoidSchema.length !== 2) {
throw new Error('Proxy avoidance must be a list of pairs');
}
const [avoidMethod, avoidPathRegex] = avoidSchema;
if (!lodash_1.default.includes(['GET', 'POST', 'DELETE'], avoidMethod)) {
throw new Error(`Unrecognized proxy avoidance method '${avoidMethod}'`);
}
if (!lodash_1.default.isRegExp(avoidPathRegex)) {
throw new Error('Proxy avoidance path must be a regular expression');
}
const normalizedUrl = url.replace(new RegExp(`^${lodash_1.default.escapeRegExp(this.basePath)}`), '');
if (avoidMethod === method && avoidPathRegex.test(normalizedUrl)) {
return true;
}
}
return false;
}
/**
*
* @param {Driver} driver
*/
addManagedDriver(driver) {
this.managedDrivers.push(driver);
}
getManagedDrivers() {
return this.managedDrivers;
}
async clearNewCommandTimeout() {
if (this.noCommandTimer) {
clearTimeout(this.noCommandTimer);
this.noCommandTimer = null;
}
}
}
exports.DriverCore = DriverCore;
//# sourceMappingURL=core.js.map