feat: Add BiDi commands to the listCommands API output (#20925)

This commit is contained in:
Mykola Mokhnach
2025-01-24 19:48:54 +01:00
committed by GitHub
parent e7541f88fb
commit 2635dcb457
6 changed files with 178 additions and 15 deletions

View File

@@ -13,6 +13,7 @@ import {
promoteAppiumOptionsForObject,
generateDriverLogPrefix,
METHOD_MAP as BASE_METHOD_MAP,
BIDI_COMMANDS as BASE_BIDI_COMMANDS,
} from '@appium/base-driver';
import AsyncLock from 'async-lock';
import {
@@ -21,6 +22,7 @@ import {
makeNonW3cCapsError,
validateFeatures,
toRestCommandsMap,
toBiDiCommandsMap,
} from './utils';
import {util} from '@appium/support';
import {getDefaultsForExtension} from './schema';
@@ -574,22 +576,27 @@ class AppiumDriver extends DriverCore {
*/
listCommands(sessionId) {
/** @type {import('@appium/types').MethodMap<any>} */
let driverMethodMap = {};
let driverRestMethodMap = {};
/** @type {import('@appium/types').BidiModuleMap} */
let driverBiDiCommands = {};
/** @type {Record<string, import('@appium/types').MethodMap<any>>} */
let pluginMethodMaps = {};
let pluginRestMethodMaps = {};
/** @type {Record<string, import('@appium/types').BidiModuleMap>} */
let pluginBiDiCommands = {};
if (sessionId) {
// @ts-ignore It's ok if the newMethodMap property is not there
driverMethodMap = this.driverForSession(sessionId)?.constructor?.newMethodMap ?? {};
// @ts-ignore It's ok if the newMethodMap property is not there
pluginMethodMaps = _.fromPairs(
this.pluginsForSession(sessionId)
.map((p) => p.constructor)
// @ts-ignore It's ok if the newMethodMap property is not there
.map((c) => [c.name, c.newMethodMap ?? {}])
const driverClass = /** @type {import('@appium/types').DriverClass | undefined} */ (
this.driverForSession(sessionId)?.constructor
);
driverRestMethodMap = driverClass?.newMethodMap ?? {};
driverBiDiCommands = driverClass?.newBidiCommands ?? {};
const pluginClasses = this.pluginsForSession(sessionId)
.map((p) => /** @type {import('@appium/types').PluginClass} */ (p.constructor));
pluginRestMethodMaps = _.fromPairs(pluginClasses.map((c) => [c.name, c.newMethodMap ?? {}]));
pluginBiDiCommands = _.fromPairs(pluginClasses.map((c) => [c.name, c.newBidiCommands ?? {}]));
}
return {
rest: toRestCommandsMap(BASE_METHOD_MAP, driverMethodMap, pluginMethodMaps),
rest: toRestCommandsMap(BASE_METHOD_MAP, driverRestMethodMap, pluginRestMethodMaps),
bidi: toBiDiCommandsMap(BASE_BIDI_COMMANDS, driverBiDiCommands, pluginBiDiCommands),
};
}

View File

@@ -578,6 +578,90 @@ export function toRestCommandsMap(baseMethodMap, driverMethodMap, pluginMethodMa
};
}
/**
*
* @param {import('@appium/types').BidiModuleMap} baseModuleMap
* @param {import('@appium/types').BidiModuleMap} driverModuleMap
* @param {Record<string, import('@appium/types').BidiModuleMap>} [pluginModuleMaps]
* @returns {import('@appium/types').BiDiCommandsMap}
*/
export function toBiDiCommandsMap(baseModuleMap, driverModuleMap, pluginModuleMaps) {
/**
* @param {import("@appium/types").BidiMethodParams | undefined} params
* @returns {import("@appium/types").BiDiCommandItemParam[] | undefined}
*/
const toBiDiCommandParams = (params) => {
if (!params) {
return;
}
/**
*
* @param {any} x
* @param {boolean} isRequired
* @returns {import("@appium/types").BiDiCommandItemParam | undefined}
*/
const toBiDiCommandItemParam = (x, isRequired) => {
const isNameAnArray = _.isArray(x);
const name = isNameAnArray ? x[0] : x;
if (!_.isString(name)) {
return;
}
// If parameter names are arrays then this means
// either of them is required.
// Not sure we could reflect that in here.
const required = isRequired && !isNameAnArray;
return {
name,
required,
};
};
/** @type {import("@appium/types").BiDiCommandItemParam[]} */
const requiredParams = (params.required ?? [])
.map((name) => toBiDiCommandItemParam(name, true))
.filter((x) => !_.isUndefined(x));
/** @type {import("@appium/types").BiDiCommandItemParam[]} */
const optionalParams = (params.optional ?? [])
.map((name) => toBiDiCommandItemParam(name, false))
.filter((x) => !_.isUndefined(x));
return requiredParams.length || optionalParams.length
? [...requiredParams, ...optionalParams]
: undefined;
};
/**
*
* @param {import('@appium/types').BidiModuleMap} mm
* @returns {Record<string, import('@appium/types').BiDiCommandNamesToInfosMap>}
*/
const moduleMapToBiDiCommandsInfo = (mm) => {
/** @type {Record<string, import('@appium/types').BiDiCommandNamesToInfosMap>} */
const res = {};
for (const [domain, commands] of _.toPairs(mm)) {
const commandsMap = {};
for (const [name, spec] of _.toPairs(commands)) {
commandsMap[name] = {
command: spec.command,
deprecated: spec.deprecated,
info: spec.info,
params: toBiDiCommandParams(spec.params),
};
}
// @ts-ignore this is OK
res[domain] = commandsMap;
}
return res;
};
return {
base: moduleMapToBiDiCommandsInfo(baseModuleMap),
driver: moduleMapToBiDiCommandsInfo(driverModuleMap),
plugins: pluginModuleMaps ? _.mapValues(pluginModuleMaps, moduleMapToBiDiCommandsInfo) : undefined,
};
}
/**
* @typedef {import('@appium/types').StringRecord} StringRecord
* @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints

View File

@@ -231,11 +231,30 @@ describe('FakeDriver via HTTP', function () {
);
const commands = await driver.listCommands();
JSON.stringify(commands.rest.base['/session/:sessionId/frame'])
.should.eql(JSON.stringify({POST: {command: 'setFrame', params: [
{name: 'id', required: true}
]}}));
_.size(commands.rest.driver).should.be.greaterThan(1);
JSON.stringify(commands.bidi.base.session.subscribe).should.eql(
JSON.stringify({
command: 'bidiSubscribe',
'params': [
{
name: 'events',
required: true
},
{
name: 'contexts',
required: false
}
]
})
);
_.size(commands.bidi.base).should.be.greaterThan(1);
_.size(commands.bidi.driver).should.be.greaterThan(0);
});
});

View File

@@ -49,6 +49,9 @@ export {
// Web socket helpers
export {DEFAULT_WS_PATHNAME_PREFIX} from './express/websocket';
// BiDi exports
export {BIDI_COMMANDS} from './protocol/bidi-commands';
export {generateDriverLogPrefix} from './basedriver/helpers';
/**

View File

@@ -3,7 +3,7 @@ const SUBSCRIPTION_REQUEST_PARAMS = /** @type {const} */ ({
optional: ['contexts'],
});
const BIDI_COMMANDS = /** @type {const} */ ({
export const BIDI_COMMANDS = /** @type {const} */ ({
session: {
subscribe: {
command: 'bidiSubscribe',
@@ -31,5 +31,3 @@ const BIDI_COMMANDS = /** @type {const} */ ({
// TODO add definitions for all bidi commands.
// spec link: https://w3c.github.io/webdriver-bidi/
export {BIDI_COMMANDS};

View File

@@ -243,10 +243,62 @@ export interface RestCommandsMap {
plugins?: Record<string, Record<string, RestMethodsToCommandsMap>>;
}
export interface BiDiCommandItemParam {
/**
* Command paremeter name
*/
name: string;
/**
* True if the paramter is required for the given command
*/
required: boolean;
}
export interface BiDiCommandItem {
/**
* Command name
*/
command?: string;
/**
* Whether the command is marked for deprecation
*/
deprecated?: boolean;
/**
* Optinal infostring about the command's purpose or a comment
*/
info?: string;
/**
* List of command parameters
*/
params?: BiDiCommandItemParam[];
}
export interface BiDiCommandNamesToInfosMap {
[name: string]: Record<string, BiDiCommandItem>;
}
export interface BiDiCommandsMap {
/**
* Domains to BiDi commands mapping in the base driver
*/
base: Record<string, BiDiCommandNamesToInfosMap>;
/**
* Domains to BiDi commands mapping in the session-specific driver
*/
driver: Record<string, BiDiCommandNamesToInfosMap>;
/**
* Plugin name to domains to BiDi commands mapping
*/
plugins?: Record<string, Record<string, BiDiCommandNamesToInfosMap>>;
}
export interface ListCommandsResponse {
/**
* REST APIs mapping
*/
rest?: RestCommandsMap;
bidi?: any;
/**
* BiDi APIs mapping
*/
bidi?: BiDiCommandsMap;
}