@appium/base-driver
Version:
Base driver class for Appium drivers
129 lines (115 loc) • 4.43 kB
text/typescript
import {logger} from '@appium/support';
import {EventEmitter} from 'node:events';
import type {
AppiumLogger,
BidiModuleMap,
BiDiResultData,
StringRecord,
} from '@appium/types';
import {
MAX_LOG_BODY_LENGTH,
} from '../constants';
import {errors} from '../protocol';
import {BIDI_COMMANDS} from '../protocol/bidi-commands';
import _ from 'lodash';
import {generateDriverLogPrefix} from './helpers';
export class ExtensionCore {
bidiEventSubs: Record<string, string[]>;
bidiCommands: BidiModuleMap = BIDI_COMMANDS as BidiModuleMap;
_logPrefix?: string;
protected _log: AppiumLogger;
// used to handle driver events
readonly eventEmitter: NodeJS.EventEmitter;
constructor(logPrefix?: string) {
this._logPrefix = logPrefix;
this.bidiEventSubs = {};
this.eventEmitter = new EventEmitter();
}
get log(): AppiumLogger {
if (!this._log) {
this.updateLogPrefix(this._logPrefix ?? generateDriverLogPrefix(this));
}
return this._log;
}
updateLogPrefix(logPrefix: string) {
this._log = logger.getLogger(logPrefix);
}
updateBidiCommands(cmds: BidiModuleMap): void {
const overlappingKeys = _.intersection(Object.keys(cmds), Object.keys(this.bidiCommands));
if (overlappingKeys.length) {
this.log.warn(`Overwriting existing bidi modules: ${JSON.stringify(overlappingKeys)}. This may not be intended!`);
}
this.bidiCommands = {
...this.bidiCommands,
...cmds,
};
}
doesBidiCommandExist(moduleName: string, methodName: string): boolean {
try {
this.ensureBidiCommandExists(moduleName, methodName);
} catch {
return false;
}
return true;
}
ensureBidiCommandExists(moduleName: string, methodName: string): void {
// if we don't get a valid format for bidi command name, reject
if (!moduleName || !methodName) {
throw new errors.UnknownCommandError(
`Did not receive a valid BiDi module and method name ` +
`of the form moduleName.methodName. Instead received ` +
`'${moduleName}.${methodName}'`,
);
}
// if the command module or method isn't part of our spec, reject
if (!this.bidiCommands[moduleName] || !this.bidiCommands[moduleName][methodName]) {
throw new errors.UnknownCommandError();
}
const {command} = this.bidiCommands[moduleName][methodName];
// if the command method isn't part of our spec, also reject
if (!command) {
throw new errors.UnknownCommandError();
}
// If the driver doesn't have this command, it must not be implemented
if (!this[command]) {
throw new errors.NotYetImplementedError();
}
}
async executeBidiCommand(bidiCmd: string, bidiParams: StringRecord, next?: () => Promise<any>, driver?: ExtensionCore): Promise<BiDiResultData> {
const handlerType = (next && driver) ? 'plugin' : 'driver';
const [moduleName, methodName] = bidiCmd.split('.');
this.ensureBidiCommandExists(moduleName, methodName);
const {command, params} = this.bidiCommands[moduleName][methodName];
// TODO improve param parsing and error messages along the lines of what we have in the http
// handlers
const args: any[] = [];
if (params?.required?.length) {
for (const requiredParam of params.required) {
if (_.isUndefined(bidiParams[requiredParam])) {
throw new errors.InvalidArgumentError(
`The ${requiredParam} parameter was required but you omitted it`,
);
}
args.push(bidiParams[requiredParam]);
}
}
if (params?.optional?.length) {
for (const optionalParam of params.optional) {
args.push(bidiParams[optionalParam]);
}
}
const logParams = _.truncate(JSON.stringify(bidiParams), {length: MAX_LOG_BODY_LENGTH});
this.log.debug(
`Executing bidi command '${bidiCmd}' with params ${logParams} by passing to ${handlerType} ` +
`method '${command}'`,
);
// call the handler with the signature appropriate to extension type (plugin or driver)
const response = (next && driver) ? await this[command](next, driver, ...args) : await this[command](...args);
const finalResponse = _.isUndefined(response) ? {} : response;
this.log.debug(
`Responding to bidi command '${bidiCmd}' with ` +
`${_.truncate(JSON.stringify(finalResponse), {length: MAX_LOG_BODY_LENGTH})}`
);
return finalResponse;
}
}