diff --git a/packages/base-driver/lib/basedriver/driver.ts b/packages/base-driver/lib/basedriver/driver.ts index 99d2f58ab..07258f35b 100644 --- a/packages/base-driver/lib/basedriver/driver.ts +++ b/packages/base-driver/lib/basedriver/driver.ts @@ -25,6 +25,7 @@ import {DELETE_SESSION_COMMAND, determineProtocol, errors} from '../protocol'; import {processCapabilities, validateCaps} from './capabilities'; import {DriverCore} from './core'; import * as helpers from './helpers'; +import {resolveExecuteExtensionName} from '../helpers/extension-command-name'; const EVENT_SESSION_INIT = 'newSessionRequested'; const EVENT_SESSION_START = 'newSessionStarted'; @@ -161,6 +162,11 @@ export class BaseDriver< // log timing information about this command const endTime = Date.now(); + + if (this.clarifyCommandName) { + cmd = this.clarifyCommandName(cmd, args); + } + this._eventHistory.commands.push({cmd, startTime, endTime}); if (cmd === 'createSession') { this.logEvent(EVENT_SESSION_START); @@ -171,6 +177,17 @@ export class BaseDriver< return res; } + clarifyCommandName(cmd: string, args: string[]): string { + if (cmd === 'execute') { + const firstArg = args?.[0]; + if (_.isString(firstArg) && firstArg.trim().length > 0) { + return resolveExecuteExtensionName.call(this, firstArg); + } + } + + return cmd; + } + async startUnexpectedShutdown( err: Error = new errors.NoSuchDriverError('The driver was unexpectedly shut down!'), ) { diff --git a/packages/base-driver/lib/helpers/extension-command-name.ts b/packages/base-driver/lib/helpers/extension-command-name.ts new file mode 100644 index 000000000..6b3889b36 --- /dev/null +++ b/packages/base-driver/lib/helpers/extension-command-name.ts @@ -0,0 +1,27 @@ +import _ from 'lodash'; +import type {Constraints, Driver, DriverClass} from '@appium/types'; +import type {BaseDriver} from '../basedriver/driver'; + +/** + * Resolves the name of extension method corresponding to an `execute` command string + * based on the driver's `executeMethodMap`. + * + * @param commandName - The command name to resolve. + * @returns The resolved extension command name if a mapping exists. Otherwise, the original command name. + */ +export function resolveExecuteExtensionName( + this: BaseDriver, + commandName: string +): string { + const Driver = this.constructor as DriverClass>; + const methodMap = Driver.executeMethodMap; + + if (methodMap && _.isPlainObject(methodMap) && commandName in methodMap) { + const command = methodMap[commandName]?.command; + if (typeof command === 'string') { + return command; + } + } + + return commandName; +} diff --git a/packages/base-driver/test/e2e/basedriver/logevents.e2e.spec.js b/packages/base-driver/test/e2e/basedriver/logevents.e2e.spec.js new file mode 100644 index 000000000..b1e4bf6ad --- /dev/null +++ b/packages/base-driver/test/e2e/basedriver/logevents.e2e.spec.js @@ -0,0 +1,55 @@ +import {server, routeConfiguringFunction} from '../../../lib'; +import axios from 'axios'; +// eslint-disable-next-line import/named +import {createSandbox} from 'sinon'; +import {getTestPort, TEST_HOST} from '@appium/driver-test-support'; +import {MockExecuteDriver} from '../protocol/mock-execute-driver'; + +let port, baseUrl; + +describe('Execute Command Test', function () { + let sandbox; + let driver; + let httpServer; + + beforeEach(async function () { + const chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + chai.use(chaiAsPromised.default); + chai.should(); + sandbox = createSandbox(); + port = await getTestPort(); + baseUrl = `http://${TEST_HOST}:${port}`; + driver = new MockExecuteDriver(); + driver.sessionId = 'foo'; + + httpServer = await server({ + routeConfiguringFunction: routeConfiguringFunction(driver), + port, + }); + }); + + afterEach(async function () { + sandbox.restore(); + await httpServer.close(); + }); + + it('should rename extended command and log it in event history', async function () { + const script = 'mobile: activateApp'; + const args = [{appId: 'io.appium.TestApp'}]; + + const res = await axios.post(`${baseUrl}/session/foo/execute/sync`, { + script, + args, + }); + + res.status.should.eql(200); + res.data.should.have.property('value'); + res.data.value.should.deep.equal({executed: script, args}); + + const events = await driver.getLogEvents(); + const command = events.commands[0]; + + command.should.have.property('cmd', 'mobileActivateApp'); + }); +}); diff --git a/packages/base-driver/test/e2e/protocol/mock-execute-driver.js b/packages/base-driver/test/e2e/protocol/mock-execute-driver.js new file mode 100644 index 000000000..8092dcdfe --- /dev/null +++ b/packages/base-driver/test/e2e/protocol/mock-execute-driver.js @@ -0,0 +1,25 @@ + +import {BaseDriver} from '../../../lib'; +import {PROTOCOLS} from '../../../lib/constants'; + +class MockExecuteDriver extends BaseDriver { + + static executeMethodMap = { + 'mobile: activateApp': { + command: 'mobileActivateApp', + } + }; + + constructor() { + super(); + this.protocol = PROTOCOLS.W3C; + this.sessionId = null; + this.jwpProxyActive = false; + } + + async execute(script, args) { + return {executed: script, args}; + } +} + +export {MockExecuteDriver}; diff --git a/packages/types/lib/driver.ts b/packages/types/lib/driver.ts index 4e84b8862..334f8e46d 100644 --- a/packages/types/lib/driver.ts +++ b/packages/types/lib/driver.ts @@ -692,6 +692,19 @@ export interface Driver< */ executeCommand(cmd: string, ...args: any[]): Promise; + + /** + * A helper method to modify the command name before it's logged. + * + * Useful for resolving generic commands like 'execute' to a more specific + * name based on arguments (e.g., identifying custom extensions). + * + * @param cmd - The original command name + * @param args - Arguments passed to the command + * @returns A potentially updated command name + */ + clarifyCommandName?(cmd: string, args: string[]): string; + /** Execute a driver (WebDriver Bidi protocol) command by its name as defined in the bidi commands file * @param bidiCmd - the name of the command in the bidi spec * @param args - arguments to pass to the command