feat(base-plugin): add ability for plugins to implement execute methods
MODIFY
packages/base-driver/lib/basedriver/commands/execute.js
packages/base-driver/lib/basedriver/commands/execute.js
import _ from 'lodash';
-import {errors, makeArgs, checkParams} from '../../protocol';
+import {errors, validateExecuteMethodParams} from '../../protocol';
/**
* @template {Constraints} C
@@ -16,23 +16,6 @@ export function ExecuteMixin(Base) {
* @param {[Record<string, any>]|[]} protoArgs
*/
async executeMethod(script, protoArgs) {
- // the w3c protocol will give us an array of arguments to apply to a javascript function.
- // that's not what we're doing. we're going to look for a JS object as the first arg, so we
- // can perform validation on it. we'll ignore everything else.
- if (!protoArgs || !_.isArray(protoArgs) || protoArgs.length > 1) {
- throw new errors.InvalidArgumentError(
- `Did not get correct format of arguments for execute method. Expected zero or one ` +
- `arguments to execute script and instead received: ${JSON.stringify(protoArgs)}`
- );
- }
- let args = protoArgs[0] ?? {};
- if (!_.isPlainObject(args)) {
- throw new errors.InvalidArgumentError(
- `Did not receive an appropriate execute method parameters object. It needs to be ` +
- `deserializable as a plain JS object`
- );
- }
-
const Driver = /** @type {DriverClass} */ (this.constructor);
const commandMetadata = {...Driver.executeMethodMap?.[script]};
if (!commandMetadata.command) {
@@ -42,16 +25,8 @@ export function ExecuteMixin(Base) {
`are: ${availableScripts.join(', ')}`
);
}
- let argsToApply = [];
- if (!commandMetadata.params) {
- commandMetadata.params = {required: [], optional: []};
- } else {
- commandMetadata.params.required ??= [];
- commandMetadata.params.optional ??= [];
- checkParams(commandMetadata.params, args, null);
- }
- argsToApply = makeArgs({}, args, commandMetadata.params, null);
- return await this[commandMetadata.command](...argsToApply);
+ const args = validateExecuteMethodParams(protoArgs, commandMetadata.params);
+ return await this[commandMetadata.command](...args);
}
}
return ExecuteCommands;

MODIFY
packages/base-driver/lib/protocol/index.js
packages/base-driver/lib/protocol/index.js
@@ -7,6 +7,7 @@ import {
GET_STATUS_COMMAND,
makeArgs,
checkParams,
+ validateExecuteMethodParams,
} from './protocol';
import {NO_SESSION_ID_COMMANDS, ALL_COMMANDS, METHOD_MAP, routeToCommandName} from './routes';
import {errors, isErrorType, errorFromMJSONWPStatusCode, errorFromW3CJsonCode} from './errors';
@@ -17,6 +18,7 @@ export {
isErrorType,
makeArgs,
checkParams,
+ validateExecuteMethodParams,
errorFromMJSONWPStatusCode,
errorFromW3CJsonCode,
ALL_COMMANDS,

MODIFY
packages/base-driver/lib/protocol/protocol.js
packages/base-driver/lib/protocol/protocol.js
@@ -215,6 +215,34 @@ export function makeArgs(requestParams, jsonObj, payloadParams, protocol) {
return args;
}
+function validateExecuteMethodParams(params, paramSpec) {
+ // the w3c protocol will give us an array of arguments to apply to a javascript function.
+ // that's not what we're doing. we're going to look for a JS object as the first arg, so we
+ // can perform validation on it. we'll ignore everything else.
+ if (!params || !_.isArray(params) || params.length > 1) {
+ throw new errors.InvalidArgumentError(
+ `Did not get correct format of arguments for execute method. Expected zero or one ` +
+ `arguments to execute script and instead received: ${JSON.stringify(params)}`
+ );
+ }
+ const args = params[0] ?? {};
+ if (!_.isPlainObject(args)) {
+ throw new errors.InvalidArgumentError(
+ `Did not receive an appropriate execute method parameters object. It needs to be ` +
+ `deserializable as a plain JS object`
+ );
+ }
+ if (!paramSpec) {
+ paramSpec = {required: [], optional: []};
+ } else {
+ paramSpec.required ??= [];
+ paramSpec.optional ??= [];
+ checkParams(paramSpec, args, null);
+ }
+ const argsToApply = makeArgs({}, args, paramSpec, null);
+ return argsToApply;
+}
+
/**
*
* @param {import('@appium/types').Core} driver
@@ -526,4 +554,5 @@ export {
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
deprecatedCommandsLogged,
+ validateExecuteMethodParams,
};

MODIFY
packages/base-plugin/lib/plugin.js
packages/base-plugin/lib/plugin.js
import {logger} from '@appium/support';
+import {validateExecuteMethodParams} from '@appium/base-driver';
/**
* @implements {Plugin}
@@ -21,6 +22,24 @@ class BasePlugin {
*/
static newMethodMap = {};
+ /**
+ * Subclasses should use type `import('@appium/types').ExecuteMethodMap<SubclassName>`.
+ *
+ * Building up this map allows the use of the convenience function `executeMethod`, which
+ * basically does verification of names and parameters for execute methods implemented by this
+ * plugin.
+ *
+ * ```ts
+ * static executeMethodMap = {
+ * 'foo: bar': {
+ * command: 'commandName',
+ * params: {required: ['thing1', 'thing2'], optional: ['thing3']},
+ * },
+ * } as const;
+ * ```
+ */
+ static executeMethodMap = {};
+
/**
* @param {string} name
* @param {Record<string,unknown>} [cliArgs]
@@ -30,6 +49,30 @@ class BasePlugin {
this.cliArgs = cliArgs;
this.logger = logger.getLogger(`Plugin [${name}]`);
}
+
+ /**
+ * A convenience method that can be called by plugins who implement their own `executeMethodMap`.
+ * Only useful if your plugin has defined `executeMethodMap`. This helper requires passing in the
+ * `next` and `driver` objects since naturally we'd want to make sure to trigger the driver's own
+ * `executeMethod` call if an execute method is not found on the plugin itself.
+ *
+ * @param {NextPluginCallback} next
+ * @param {Driver} driver
+ * @param {string} script
+ * @param {[Record<string, any>]|[]} protoArgs
+ */
+ async executeMethod(next, driver, script, protoArgs) {
+ const Plugin = /** @type {PluginClass} */ (this.constructor);
+ const commandMetadata = {...Plugin.executeMethodMap?.[script]};
+ if (!commandMetadata.command) {
+ this.logger.info(
+ `Plugin did not know how to handle method '${script}'. Passing control to next`
+ );
+ return await next();
+ }
+ const args = validateExecuteMethodParams(protoArgs, commandMetadata.params);
+ return await this[commandMetadata.command](next, driver, ...args);
+ }
}
export default BasePlugin;
@@ -37,4 +80,7 @@ export {BasePlugin};
/**
* @typedef {import('@appium/types').Plugin} Plugin
+ * @typedef {import('@appium/types').NextPluginCallback} NextPluginCallback
+ * @typedef {import('@appium/types').Driver} Driver
+ * @typedef {import('@appium/types').PluginClass} PluginClass
*/

MODIFY
packages/base-plugin/package.json
packages/base-plugin/package.json
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
- "@appium/support": "^3.1.3"
+ "@appium/support": "^3.1.3",
+ "@appium/base-driver": "^9.2.3"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",

MODIFY
packages/types/lib/plugin.ts
packages/types/lib/plugin.ts
-import {MethodMap, UpdateServerCallback, Class, AppiumLogger} from '.';
+import {MethodMap, ExecuteMethodMap, UpdateServerCallback, Class, AppiumLogger} from '.';
import {Driver, ExternalDriver} from './driver';
/**
@@ -21,6 +21,7 @@ export interface PluginStatic<P extends Plugin> {
* }
*/
newMethodMap?: MethodMap<P>;
+ executeMethodMap?: ExecuteMethodMap<P>;
}
/**

feat(base-plugin): add ability for plugins to implement execute methods