From abcbea6f9ed6bd6ba4f431b7faf9f0166e06be21 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 12 Oct 2026 16:56:02 +0200 Subject: [PATCH 1/4] refactor: attach commands via prototype to allow overrides in descendants Command functions were assigned as instance fields, which made them properties: subclasses could not redefine them as methods or call `super`. Mirror the base-driver layout instead: each command module augments `AndroidDriver` with method signatures via `declare module`, and `Object.assign(AndroidDriver.prototype, ...)` attaches them. `getLogTypes` and `getLog` now live in the class as real overrides using `super`. Also fix `mobileResetGeolocation`, which bound `resetMockLocation` without ever calling it. Co-Authored-By: Claude Sonnet 5.5 --- lib/commands/app-management.ts | 29 ++ lib/commands/appearance.ts | 8 + lib/commands/bluetooth.ts | 7 + lib/commands/context/exports.ts | 36 ++ lib/commands/device/common.ts | 10 + lib/commands/device/emulator-actions.ts | 23 ++ lib/commands/device/emulator-console.ts | 9 + lib/commands/deviceidle.ts | 7 + lib/commands/element.ts | 21 + lib/commands/execute.ts | 7 + lib/commands/file-actions.ts | 10 + lib/commands/find.ts | 9 + lib/commands/geolocation.ts | 22 +- lib/commands/gestures.ts | 7 + lib/commands/image-injection.ts | 9 + lib/commands/ime.ts | 12 + lib/commands/intent.ts | 21 + lib/commands/keyboard.ts | 15 + lib/commands/legacy.ts | 9 + lib/commands/lock/exports.ts | 10 + lib/commands/log.ts | 49 +-- lib/commands/media-projection.ts | 15 + lib/commands/memory.ts | 7 + lib/commands/misc.ts | 15 + lib/commands/network.ts | 16 + lib/commands/nfc.ts | 7 + lib/commands/performance.ts | 13 + lib/commands/permissions.ts | 10 + lib/commands/recordscreen.ts | 8 + lib/commands/resources.ts | 8 + lib/commands/shell.ts | 12 + lib/commands/streamscreen.ts | 12 + lib/commands/system-bars.ts | 10 + lib/commands/time.ts | 8 + lib/driver.ts | 504 ++++++++++++++---------- 35 files changed, 730 insertions(+), 245 deletions(-) diff --git a/lib/commands/app-management.ts b/lib/commands/app-management.ts index 8af017bf..483f97bd 100644 --- a/lib/commands/app-management.ts +++ b/lib/commands/app-management.ts @@ -4,9 +4,38 @@ import {util} from '@appium/support'; import type {UninstallOptions, InstallOptions, StartAppOptions, ListInstalledPackagesOptions} from 'appium-adb'; import {waitForCondition, longSleep} from 'asyncbox'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver, AndroidDriverOpts} from '../driver.js'; import type {AppInfoMap, AppState, IsAppInstalledOptions, TerminateAppOpts} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + getThirdPartyPackages(...args: Parameters): ReturnType; + uninstallOtherPackages( + ...args: Parameters + ): ReturnType; + installOtherApks(...args: Parameters): ReturnType; + installAUT(...args: Parameters): ReturnType; + resetAUT(...args: Parameters): ReturnType; + background(...args: Parameters): ReturnType; + getCurrentActivity(...args: Parameters): ReturnType; + getCurrentPackage(...args: Parameters): ReturnType; + mobileClearApp(...args: Parameters): ReturnType; + mobileInstallApp(...args: Parameters): ReturnType; + installApp(...args: Parameters): ReturnType; + mobileIsAppInstalled(...args: Parameters): ReturnType; + mobileRemoveApp(...args: Parameters): ReturnType; + mobileTerminateApp(...args: Parameters): ReturnType; + mobileListApps(...args: Parameters): ReturnType; + terminateApp(...args: Parameters): ReturnType; + removeApp(...args: Parameters): ReturnType; + activateApp(...args: Parameters): ReturnType; + queryAppState(...args: Parameters): ReturnType; + isAppInstalled(...args: Parameters): ReturnType; + mobileBackgroundApp(...args: Parameters): ReturnType; + } +} + const APP_EXTENSIONS = ['.apk', '.apks'] as const; const PACKAGE_INSTALL_TIMEOUT_MS = 90000; // These constants are in sync with diff --git a/lib/commands/appearance.ts b/lib/commands/appearance.ts index b7a70096..a4db098e 100644 --- a/lib/commands/appearance.ts +++ b/lib/commands/appearance.ts @@ -1,5 +1,13 @@ +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileGetUiMode(...args: Parameters): ReturnType; + mobileSetUiMode(...args: Parameters): ReturnType; + } +} + const RESPONSE_PATTERN = /:\s+(\w+)/; /** diff --git a/lib/commands/bluetooth.ts b/lib/commands/bluetooth.ts index 458fc2bc..ea1e0925 100644 --- a/lib/commands/bluetooth.ts +++ b/lib/commands/bluetooth.ts @@ -1,7 +1,14 @@ import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileBluetooth(...args: Parameters): ReturnType; + } +} + const SUPPORTED_ACTIONS = { ENABLE: 'enable', DISABLE: 'disable', diff --git a/lib/commands/context/exports.ts b/lib/commands/context/exports.ts index ad50bac7..cf1b3588 100644 --- a/lib/commands/context/exports.ts +++ b/lib/commands/context/exports.ts @@ -3,6 +3,7 @@ import type {StringRecord} from '@appium/types'; import {Chromedriver} from 'appium-chromedriver'; import {errors, PROTOCOLS} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../../constraints.js'; import type {AndroidDriver} from '../../driver.js'; import {APP_STATE} from '../app-management.js'; import {BIDI_EVENT_NAME} from '../bidi/constants.js'; @@ -22,6 +23,41 @@ import { shouldDismissChromeWelcome, } from './helpers.js'; +declare module '../../driver.js' { + interface AndroidDriver { + getContexts(...args: Parameters): ReturnType; + getCurrentContext(...args: Parameters): ReturnType; + defaultContextName(...args: Parameters): ReturnType; + assignContexts(...args: Parameters): ReturnType; + switchContext(...args: Parameters): ReturnType; + defaultWebviewName(...args: Parameters): ReturnType; + isChromedriverContext(...args: Parameters): ReturnType; + startChromedriverProxy( + ...args: Parameters + ): ReturnType; + stopChromedriverProxies( + ...args: Parameters + ): ReturnType; + suspendChromedriverProxy( + ...args: Parameters + ): ReturnType; + startChromeSession(...args: Parameters): ReturnType; + onChromedriverStop(...args: Parameters): ReturnType; + isWebContext(...args: Parameters): ReturnType; + mobileGetContexts(...args: Parameters): ReturnType; + mobileGetChromeCapabilities( + ...args: Parameters + ): ReturnType; + setContext(name?: string): Promise; + setWindow(...args: Parameters): ReturnType; + getWindowHandle(...args: Parameters): ReturnType; + getWindowHandles(...args: Parameters): ReturnType; + notifyBiDiContextChange( + ...args: Parameters + ): ReturnType; + } +} + // https://github.com/appium/appium/issues/20710 const DEFAULT_NATIVE_WINDOW_HANDLE = '1'; diff --git a/lib/commands/device/common.ts b/lib/commands/device/common.ts index 1b780b92..7e1c1f36 100644 --- a/lib/commands/device/common.ts +++ b/lib/commands/device/common.ts @@ -5,6 +5,7 @@ import {retryInterval} from 'asyncbox'; import {SETTINGS_HELPER_ID} from 'io.appium.settings'; import * as semver from 'semver'; +import type {AndroidDriverConstraints} from '../../constraints.js'; import type {AndroidDriver} from '../../driver.js'; import {GET_SERVER_LOGS_FEATURE, nativeLogEntryToSeleniumEntry} from '../../utils.js'; import {setMockLocationApp} from '../geolocation.js'; @@ -13,6 +14,15 @@ import {adjustTimeZone} from '../time.js'; import type {ADBDeviceInfo, ADBLaunchInfo} from '../types.js'; import {createBaseADB, prepareEmulator, pushSettingsApp} from './utils.js'; +declare module '../../driver.js' { + interface AndroidDriver { + getDeviceInfoFromCaps(...args: Parameters): ReturnType; + createADB(...args: Parameters): ReturnType; + getLaunchInfo(...args: Parameters): ReturnType; + initDevice(...args: Parameters): ReturnType; + } +} + /** * Gets device information from capabilities, including UDID and emulator port. * diff --git a/lib/commands/device/emulator-actions.ts b/lib/commands/device/emulator-actions.ts index e10fe2f8..dea03f68 100644 --- a/lib/commands/device/emulator-actions.ts +++ b/lib/commands/device/emulator-actions.ts @@ -1,10 +1,33 @@ import {util} from '@appium/support'; import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../../constraints.js'; import type {AndroidDriver} from '../../driver.js'; import type {GsmAction, GsmSignalStrength, GsmVoiceState, PowerACState, NetworkSpeed} from '../types.js'; import {requireEmulator} from './utils.js'; +declare module '../../driver.js' { + interface AndroidDriver { + fingerprint(...args: Parameters): ReturnType; + mobileFingerprint(...args: Parameters): ReturnType; + sendSMS(...args: Parameters): ReturnType; + mobileSendSms(...args: Parameters): ReturnType; + gsmCall(...args: Parameters): ReturnType; + mobileGsmCall(...args: Parameters): ReturnType; + gsmSignal(...args: Parameters): ReturnType; + mobileGsmSignal(...args: Parameters): ReturnType; + gsmVoice(...args: Parameters): ReturnType; + mobileGsmVoice(...args: Parameters): ReturnType; + powerAC(...args: Parameters): ReturnType; + mobilePowerAc(...args: Parameters): ReturnType; + powerCapacity(...args: Parameters): ReturnType; + mobilePowerCapacity(...args: Parameters): ReturnType; + networkSpeed(...args: Parameters): ReturnType; + mobileNetworkSpeed(...args: Parameters): ReturnType; + sensorSet(...args: Parameters): ReturnType; + } +} + /** * @deprecated Use mobile: extension */ diff --git a/lib/commands/device/emulator-console.ts b/lib/commands/device/emulator-console.ts index 54643702..0f2634d9 100644 --- a/lib/commands/device/emulator-console.ts +++ b/lib/commands/device/emulator-console.ts @@ -1,7 +1,16 @@ import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../../constraints.js'; import type {AndroidDriver} from '../../driver.js'; +declare module '../../driver.js' { + interface AndroidDriver { + mobileExecEmuConsoleCommand( + ...args: Parameters + ): ReturnType; + } +} + const EMU_CONSOLE_FEATURE = 'emulator_console'; /** diff --git a/lib/commands/deviceidle.ts b/lib/commands/deviceidle.ts index 983cffc0..2d0099d2 100644 --- a/lib/commands/deviceidle.ts +++ b/lib/commands/deviceidle.ts @@ -1,7 +1,14 @@ import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileDeviceidle(...args: Parameters): ReturnType; + } +} + const SUPPORTED_ACTIONS = ['whitelistAdd', 'whitelistRemove'] as const; /** diff --git a/lib/commands/element.ts b/lib/commands/element.ts index ad9f0b5d..ad5f4477 100644 --- a/lib/commands/element.ts +++ b/lib/commands/element.ts @@ -1,3 +1,24 @@ +import type {AndroidDriverConstraints} from '../constraints.js'; + +declare module '../driver.js' { + interface AndroidDriver { + getAttribute(...args: Parameters): ReturnType; + getName(...args: Parameters): ReturnType; + elementDisplayed(...args: Parameters): ReturnType; + elementEnabled(...args: Parameters): ReturnType; + elementSelected(...args: Parameters): ReturnType; + setElementValue(...args: Parameters): ReturnType; + doSetElementValue(...args: Parameters): ReturnType; + replaceValue(...args: Parameters): ReturnType; + setValueImmediate(...args: Parameters): ReturnType; + setValue(...args: Parameters): ReturnType; + click(...args: Parameters): ReturnType; + getLocationInView(...args: Parameters): ReturnType; + getText(...args: Parameters): ReturnType; + getLocation(...args: Parameters): ReturnType; + getSize(...args: Parameters): ReturnType; + } +} /* eslint-disable @typescript-eslint/no-unused-vars */ import type {Position, Size} from '@appium/types'; diff --git a/lib/commands/execute.ts b/lib/commands/execute.ts index 9735fdbe..c570f25c 100644 --- a/lib/commands/execute.ts +++ b/lib/commands/execute.ts @@ -3,8 +3,15 @@ import type {StringRecord, Element} from '@appium/types'; import type {Chromedriver} from 'appium-chromedriver'; import {errors, PROTOCOLS} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; +declare module '../driver.js' { + interface AndroidDriver { + execute(...args: Parameters): ReturnType; + } +} + const EXECUTE_SCRIPT_PREFIX = 'mobile:'; type ExecuteMethodArgs = readonly any[] | readonly [StringRecord] | Readonly; diff --git a/lib/commands/file-actions.ts b/lib/commands/file-actions.ts index 0424e108..a588196a 100644 --- a/lib/commands/file-actions.ts +++ b/lib/commands/file-actions.ts @@ -4,8 +4,18 @@ import {fs, util, zip, tempDir} from '@appium/support'; import type {ADB} from 'appium-adb'; import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; +declare module '../driver.js' { + interface AndroidDriver { + pullFile(...args: Parameters): ReturnType; + pullFolder(...args: Parameters): ReturnType; + pushFile(...args: Parameters): ReturnType; + mobileDeleteFile(...args: Parameters): ReturnType; + } +} + const CONTAINER_PATH_MARKER = '@'; const CONTAINER_PATH_PATTERN = new RegExp(`^${CONTAINER_PATH_MARKER}([^/]+)/(.+)`, 's'); const ANDROID_MEDIA_RESCAN_INTENT = 'android.intent.action.MEDIA_SCANNER_SCAN_FILE'; diff --git a/lib/commands/find.ts b/lib/commands/find.ts index b28e3054..215eee30 100644 --- a/lib/commands/find.ts +++ b/lib/commands/find.ts @@ -2,9 +2,18 @@ import {util} from '@appium/support'; import type {Element} from '@appium/types'; import {errors, isErrorType} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {FindElementOpts} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + findElOrEls(strategy: string, selector: string, mult: true, context?: string): Promise; + findElOrEls(strategy: string, selector: string, mult: false, context?: string): Promise; + doFindElementOrEls(...args: Parameters): ReturnType; + } +} + /** * @param strategy The element location strategy to use (e.g., 'id', 'xpath', 'class name'). * @param selector The selector value to search for. diff --git a/lib/commands/geolocation.ts b/lib/commands/geolocation.ts index ff458de0..8b04e29a 100644 --- a/lib/commands/geolocation.ts +++ b/lib/commands/geolocation.ts @@ -4,10 +4,30 @@ import {fs, tempDir} from '@appium/support'; import {asyncmap} from 'asyncbox'; import {SETTINGS_HELPER_ID} from 'io.appium.settings'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import {getThirdPartyPackages} from './app-management.js'; import type {GeoLocation} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + setGeoLocation(...args: Parameters): ReturnType; + getGeoLocation(...args: Parameters): ReturnType; + mobileRefreshGpsCache(...args: Parameters): ReturnType; + toggleLocationServices( + ...args: Parameters + ): ReturnType; + isLocationServicesEnabled( + ...args: Parameters + ): ReturnType; + mobileGetGeolocation(...args: Parameters): ReturnType; + mobileSetGeolocation(...args: Parameters): ReturnType; + mobileResetGeolocation( + ...args: Parameters + ): ReturnType; + } +} + // The value close to zero, but not zero, is needed // to trick JSON generation and send a float value instead of an integer, // This allows strictly-typed clients, like Java, to properly @@ -149,7 +169,7 @@ export async function mobileResetGeolocation(this: AndroidDriver): Promise if (this.isEmulator()) { throw new Error('Geolocation reset does not work on emulators'); } - await resetMockLocation.bind(this); + await resetMockLocation.bind(this)(); } // #region Internal helpers diff --git a/lib/commands/gestures.ts b/lib/commands/gestures.ts index 930ff72f..8c617134 100644 --- a/lib/commands/gestures.ts +++ b/lib/commands/gestures.ts @@ -1,8 +1,15 @@ import type {StringRecord} from '@appium/types'; import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; +declare module '../driver.js' { + interface AndroidDriver { + performActions(...args: Parameters): ReturnType; + } +} + /** * Performs a series of actions (gestures) on the device. * diff --git a/lib/commands/image-injection.ts b/lib/commands/image-injection.ts index 50ee5f3d..bb950d59 100644 --- a/lib/commands/image-injection.ts +++ b/lib/commands/image-injection.ts @@ -4,8 +4,17 @@ import path from 'node:path'; import {fs, tempDir} from '@appium/support'; import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileInjectEmulatorCameraImage( + ...args: Parameters + ): ReturnType; + } +} + const EMULATOR_RESOURCES_ROOT = ['emulator', 'resources']; const CONFIG_NAME = 'Toren1BD.posters'; const DEFAULT_CONFIG_PAYLOAD_PREFIX = `poster wall diff --git a/lib/commands/ime.ts b/lib/commands/ime.ts index 7112588e..a7bc627f 100644 --- a/lib/commands/ime.ts +++ b/lib/commands/ime.ts @@ -1,8 +1,20 @@ import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import {SET_STYLUS_HANDWRITING_FEATURE} from '../utils.js'; +declare module '../driver.js' { + interface AndroidDriver { + isIMEActivated(...args: Parameters): ReturnType; + availableIMEEngines(...args: Parameters): ReturnType; + getActiveIMEEngine(...args: Parameters): ReturnType; + activateIMEEngine(...args: Parameters): ReturnType; + deactivateIMEEngine(...args: Parameters): ReturnType; + setStylusHandwriting(...args: Parameters): ReturnType; + } +} + /** * Checks if an IME (Input Method Editor) is activated. * diff --git a/lib/commands/intent.ts b/lib/commands/intent.ts index 985c26f1..c66699e3 100644 --- a/lib/commands/intent.ts +++ b/lib/commands/intent.ts @@ -1,9 +1,30 @@ import {util} from '@appium/support'; import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {IntentOpts} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + startActivity( + appPackage: string, + appActivity: string, + appWaitPackage?: string, + appWaitActivity?: string, + intentAction?: string, + intentCategory?: string, + intentFlags?: string, + optionalIntentArguments?: string, + dontStopAppOnReset?: boolean, + ): Promise; + mobileStartActivity(...args: Parameters): ReturnType; + mobileBroadcast(...args: Parameters): ReturnType; + mobileStartService(...args: Parameters): ReturnType; + mobileStopService(...args: Parameters): ReturnType; + } +} + const NO_VALUE_ARG_TYPE = 'sn'; const SUPPORTED_EXTRA_TYPES = [ 's', diff --git a/lib/commands/keyboard.ts b/lib/commands/keyboard.ts index d42e27bb..7811dad2 100644 --- a/lib/commands/keyboard.ts +++ b/lib/commands/keyboard.ts @@ -1,9 +1,24 @@ import {errors} from 'appium/driver.js'; import {UNICODE_IME, EMPTY_IME} from 'io.appium.settings'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {SendKeysOpts} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + hideKeyboard(...args: Parameters): ReturnType; + isKeyboardShown(...args: Parameters): ReturnType; + keys(...args: Parameters): ReturnType; + doSendKeys(...args: Parameters): ReturnType; + pressKeyCode(...args: Parameters): ReturnType; + longPressKeyCode(...args: Parameters): ReturnType; + mobilePerformEditorAction( + ...args: Parameters + ): ReturnType; + } +} + /** * Hides the on-screen keyboard. * diff --git a/lib/commands/legacy.ts b/lib/commands/legacy.ts index 781caa33..31af7a7d 100644 --- a/lib/commands/legacy.ts +++ b/lib/commands/legacy.ts @@ -1,7 +1,16 @@ import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; +declare module '../driver.js' { + interface AndroidDriver { + reset(...args: Parameters): ReturnType; + closeApp(...args: Parameters): ReturnType; + launchApp(...args: Parameters): ReturnType; + } +} + const ISSUE_URL = 'https://github.com/appium/appium/issues/15807'; /** diff --git a/lib/commands/lock/exports.ts b/lib/commands/lock/exports.ts index 32c857dc..68d70a41 100644 --- a/lib/commands/lock/exports.ts +++ b/lib/commands/lock/exports.ts @@ -1,5 +1,6 @@ import {sleep} from 'asyncbox'; +import type {AndroidDriverConstraints} from '../../constraints.js'; import type {AndroidDriver, AndroidDriverCaps} from '../../driver.js'; import type {UnlockType} from '../types.js'; import { @@ -19,6 +20,15 @@ import { verifyUnlock, } from './helpers.js'; +declare module '../../driver.js' { + interface AndroidDriver { + lock(...args: Parameters): ReturnType; + unlock(...args: Parameters): ReturnType; + mobileUnlock(...args: Parameters): ReturnType; + isLocked(...args: Parameters): ReturnType; + } +} + /** * Locks the device and optionally unlocks it after a specified number of seconds. * diff --git a/lib/commands/log.ts b/lib/commands/log.ts index d24023d3..cc9ebcd8 100644 --- a/lib/commands/log.ts +++ b/lib/commands/log.ts @@ -4,16 +4,31 @@ import os from 'node:os'; import {util} from '@appium/support'; import type {AppiumServer, WSServer} from '@appium/types'; import type {ADB} from 'appium-adb'; -import type {Chromedriver} from 'appium-chromedriver'; -import {DEFAULT_WS_PATHNAME_PREFIX, BaseDriver} from 'appium/driver.js'; +import {DEFAULT_WS_PATHNAME_PREFIX} from 'appium/driver.js'; import {WebSocket, WebSocketServer} from 'ws'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import {GET_SERVER_LOGS_FEATURE, toLogRecord, nativeLogEntryToSeleniumEntry, type LogEntry} from '../utils.js'; import {BIDI_EVENT_NAME} from './bidi/constants.js'; import {makeLogEntryAddedEvent} from './bidi/models.js'; import {NATIVE_WIN} from './context/helpers.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileStartLogsBroadcast( + ...args: Parameters + ): ReturnType; + mobileStopLogsBroadcast( + ...args: Parameters + ): ReturnType; + assignBiDiLogListener( + logEmitter: EE, + properties: BiDiListenerProperties, + ): [EE, LogListener]; + } +} + export const supportedLogTypes = { logcat: { description: 'Logs for Android applications on real device and emulators via ADB', @@ -125,21 +140,6 @@ export async function mobileStopLogsBroadcast(this: AndroidDriver): Promise { - // XXX why doesn't `super` work here? - const nativeLogTypes = await BaseDriver.prototype.getLogTypes.call(this); - if (this.isWebContext()) { - const webLogTypes = (await (this.chromedriver as Chromedriver).jwproxy.command('/log/types', 'GET')) as string[]; - return [...nativeLogTypes, ...webLogTypes]; - } - return nativeLogTypes; -} - /** * Assigns a BiDi log listener to an event emitter. * @@ -164,21 +164,6 @@ export function assignBiDiLogListener( return [logEmitter, listener]; } -/** - * Gets logs of a specific type. - * - * @param logType The type of logs to retrieve. - * @returns Promise that resolves to the logs for the specified type. - */ -export async function getLog(this: AndroidDriver, logType: string): Promise { - if (this.isWebContext() && !Object.keys(this.supportedLogTypes).includes(logType)) { - return await (this.chromedriver as Chromedriver).jwproxy.command('/log', 'POST', { - type: logType, - }); - } - return await BaseDriver.prototype.getLog.call(this, logType); -} - // #region Internal helpers /** diff --git a/lib/commands/media-projection.ts b/lib/commands/media-projection.ts index e17716dc..22b6ae3a 100644 --- a/lib/commands/media-projection.ts +++ b/lib/commands/media-projection.ts @@ -6,9 +6,24 @@ import type {HTTPMethod, StringRecord} from '@appium/types'; import type {ADB} from 'appium-adb'; import dayjs from 'dayjs'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {FormFields} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileIsMediaProjectionRecordingRunning( + ...args: Parameters + ): ReturnType; + mobileStartMediaProjectionRecording( + ...args: Parameters + ): ReturnType; + mobileStopMediaProjectionRecording( + ...args: Parameters + ): ReturnType; + } +} + // https://github.com/appium/io.appium.settings#internal-audio--video-recording const DEFAULT_EXT = '.mp4'; const MIN_API_LEVEL = 29; diff --git a/lib/commands/memory.ts b/lib/commands/memory.ts index 2a684dcf..737b2e15 100644 --- a/lib/commands/memory.ts +++ b/lib/commands/memory.ts @@ -1,8 +1,15 @@ import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {TrimMemoryLevel} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileSendTrimMemory(...args: Parameters): ReturnType; + } +} + /** * Simulates the onTrimMemory() event for the given package. * Read https://developer.android.com/topic/performance/memory diff --git a/lib/commands/misc.ts b/lib/commands/misc.ts index 2ee1b72a..3d513253 100644 --- a/lib/commands/misc.ts +++ b/lib/commands/misc.ts @@ -1,9 +1,24 @@ import type {Size, Rect, StringRecord} from '@appium/types'; import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {SmsListResult, ListSmsOpts} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + getWindowRect(...args: Parameters): ReturnType; + getWindowSize(...args: Parameters): ReturnType; + getDisplayDensity(...args: Parameters): ReturnType; + mobileGetNotifications( + ...args: Parameters + ): ReturnType; + mobileListSms(...args: Parameters): ReturnType; + openNotifications(...args: Parameters): ReturnType; + setUrl(...args: Parameters): ReturnType; + } +} + /** * Gets the window size. * diff --git a/lib/commands/network.ts b/lib/commands/network.ts index 25ba73f5..1eafa412 100644 --- a/lib/commands/network.ts +++ b/lib/commands/network.ts @@ -1,9 +1,25 @@ import {util} from '@appium/support'; import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {ServiceType, GetConnectivityResult} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + getNetworkConnection(...args: Parameters): ReturnType; + isWifiOn(...args: Parameters): ReturnType; + mobileGetConnectivity(...args: Parameters): ReturnType; + mobileSetConnectivity(...args: Parameters): ReturnType; + setNetworkConnection(...args: Parameters): ReturnType; + setWifiState(...args: Parameters): ReturnType; + setDataState(...args: Parameters): ReturnType; + toggleData(...args: Parameters): ReturnType; + toggleFlightMode(...args: Parameters): ReturnType; + toggleWiFi(...args: Parameters): ReturnType; + } +} + const AIRPLANE_MODE_MASK = 0b001; const WIFI_MASK = 0b010; const DATA_MASK = 0b100; diff --git a/lib/commands/nfc.ts b/lib/commands/nfc.ts index 410dba38..c10bb31b 100644 --- a/lib/commands/nfc.ts +++ b/lib/commands/nfc.ts @@ -1,8 +1,15 @@ import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {NfcAction} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileNfc(...args: Parameters): ReturnType; + } +} + const SUPPORTED_ACTIONS = { ENABLE: 'enable', DISABLE: 'disable', diff --git a/lib/commands/performance.ts b/lib/commands/performance.ts index 74edd3e4..282e2d2a 100644 --- a/lib/commands/performance.ts +++ b/lib/commands/performance.ts @@ -2,9 +2,22 @@ import {util} from '@appium/support'; import {retryInterval} from 'asyncbox'; import type {ExecError} from 'teen_process'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {PerformanceDataType} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + getPerformanceData(...args: Parameters): ReturnType; + getPerformanceDataTypes( + ...args: Parameters + ): ReturnType; + mobileGetPerformanceData( + ...args: Parameters + ): ReturnType; + } +} + export const NETWORK_KEYS = [ ['bucketStart', 'activeTime', 'rxBytes', 'rxPackets', 'txBytes', 'txPackets', 'operations', 'bucketDuration'], ['st', 'activeTime', 'rb', 'rp', 'tb', 'tp', 'op', 'bucketDuration'], diff --git a/lib/commands/permissions.ts b/lib/commands/permissions.ts index 4ca7d881..b0f139b4 100644 --- a/lib/commands/permissions.ts +++ b/lib/commands/permissions.ts @@ -1,9 +1,19 @@ import {util} from '@appium/support'; import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import {ADB_SHELL_FEATURE} from '../utils.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileChangePermissions( + ...args: Parameters + ): ReturnType; + mobileGetPermissions(...args: Parameters): ReturnType; + } +} + const ALL_PERMISSIONS_MAGIC = 'all'; const PM_ACTION = Object.freeze({ GRANT: 'grant', diff --git a/lib/commands/recordscreen.ts b/lib/commands/recordscreen.ts index e2a713a1..f3832dd6 100644 --- a/lib/commands/recordscreen.ts +++ b/lib/commands/recordscreen.ts @@ -6,9 +6,17 @@ import type {ADB} from 'appium-adb'; import {waitForCondition} from 'asyncbox'; import {exec} from 'teen_process'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {StartScreenRecordingOpts, StopScreenRecordingOpts, ScreenRecordingProperties} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + startRecordingScreen(...args: Parameters): ReturnType; + stopRecordingScreen(...args: Parameters): ReturnType; + } +} + const RETRY_PAUSE = 300; const RETRY_TIMEOUT = 5000; const MAX_RECORDING_TIME_SEC = 60 * 3; diff --git a/lib/commands/resources.ts b/lib/commands/resources.ts index 759ac2ea..a6f44dfa 100644 --- a/lib/commands/resources.ts +++ b/lib/commands/resources.ts @@ -1,9 +1,17 @@ import {fs, tempDir, util} from '@appium/support'; import type {StringRecord} from '@appium/types'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver, AndroidDriverOpts} from '../driver.js'; import type {Locale} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + getStrings(...args: Parameters): ReturnType; + ensureDeviceLocale(...args: Parameters): ReturnType; + } +} + /** * Gets the localized strings from the application. * diff --git a/lib/commands/shell.ts b/lib/commands/shell.ts index 27bdbc15..e742421d 100644 --- a/lib/commands/shell.ts +++ b/lib/commands/shell.ts @@ -3,9 +3,21 @@ import {errors} from 'appium/driver.js'; import {exec} from 'teen_process'; import type {ExecError} from 'teen_process'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import {ADB_SHELL_FEATURE} from '../utils.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileShell( + command: string, + args?: string[], + timeout?: number, + includeStderr?: T, + ): Promise; + } +} + /** * Executes a shell command on the device via ADB. * diff --git a/lib/commands/streamscreen.ts b/lib/commands/streamscreen.ts index 66eb2977..c5f79f1f 100644 --- a/lib/commands/streamscreen.ts +++ b/lib/commands/streamscreen.ts @@ -10,9 +10,21 @@ import {waitForCondition} from 'asyncbox'; import {checkPortStatus} from 'portscanner'; import {SubProcess, exec} from 'teen_process'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {DeviceInfo, InitGStreamerPipelineOpts} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + mobileStartScreenStreaming( + ...args: Parameters + ): ReturnType; + mobileStopScreenStreaming( + ...args: Parameters + ): ReturnType; + } +} + const RECORDING_INTERVAL_SEC = 5; const STREAMING_STARTUP_TIMEOUT_MS = 5000; const GSTREAMER_BINARY = `gst-launch-1.0${system.isWindows() ? '.exe' : ''}`; diff --git a/lib/commands/system-bars.ts b/lib/commands/system-bars.ts index af899222..c0728fda 100644 --- a/lib/commands/system-bars.ts +++ b/lib/commands/system-bars.ts @@ -2,9 +2,19 @@ import {util} from '@appium/support'; import type {StringRecord} from '@appium/types'; import {errors} from 'appium/driver.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; import type {StatusBarCommand, WindowProperties} from './types.js'; +declare module '../driver.js' { + interface AndroidDriver { + getSystemBars(...args: Parameters): ReturnType; + mobilePerformStatusBarCommand( + ...args: Parameters + ): ReturnType; + } +} + const WINDOW_TITLE_PATTERN = /^\s+Window\s#\d+\sWindow\{[0-9a-f]+\s\w+\s([\w-]+)\}:$/; const FRAME_PATTERN = /\bm?[Ff]rame=\[([0-9.-]+),([0-9.-]+)\]\[([0-9.-]+),([0-9.-]+)\]/; // https://github.com/appium/appium-uiautomator2-driver/issues/904 diff --git a/lib/commands/time.ts b/lib/commands/time.ts index 31ba6128..181d2770 100644 --- a/lib/commands/time.ts +++ b/lib/commands/time.ts @@ -2,8 +2,16 @@ import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat.js'; import utc from 'dayjs/plugin/utc.js'; +import type {AndroidDriverConstraints} from '../constraints.js'; import type {AndroidDriver} from '../driver.js'; +declare module '../driver.js' { + interface AndroidDriver { + getDeviceTime(...args: Parameters): ReturnType; + mobileGetDeviceTime(...args: Parameters): ReturnType; + } +} + dayjs.extend(utc); dayjs.extend(customParseFormat); diff --git a/lib/driver.ts b/lib/driver.ts index 771a4bf9..7a90e403 100644 --- a/lib/driver.ts +++ b/lib/driver.ts @@ -13,6 +13,7 @@ import {BaseDriver} from 'appium/driver.js'; import {SettingsApp} from 'io.appium.settings'; import {LRUCache} from 'lru-cache'; +// oxlint-disable import/no-duplicates -- see the bare imports below import { getThirdPartyPackages, uninstallOtherPackages, @@ -145,8 +146,6 @@ import { supportedLogTypes, mobileStartLogsBroadcast, mobileStopLogsBroadcast, - getLogTypes, - getLog, assignBiDiLogListener, } from './commands/log.js'; import { @@ -191,6 +190,43 @@ import type {AndroidDriverConstraints} from './constraints.js'; import {executeMethodMap} from './execute-method-map.js'; import {newMethodMap} from './method-map.js'; import {parseArray, removeAllSessionWebSocketHandlers} from './utils.js'; +// oxlint-enable import/no-duplicates +// Bare imports make sure `declare module` augmentations in the command modules +// reach the emitted `driver.d.ts` import graph of downstream drivers. +import './commands/app-management.js'; +import './commands/appearance.js'; +import './commands/bluetooth.js'; +import './commands/context/exports.js'; +import './commands/device/common.js'; +import './commands/device/emulator-actions.js'; +import './commands/device/emulator-console.js'; +import './commands/deviceidle.js'; +import './commands/element.js'; +import './commands/execute.js'; +import './commands/file-actions.js'; +import './commands/find.js'; +import './commands/geolocation.js'; +import './commands/gestures.js'; +import './commands/image-injection.js'; +import './commands/ime.js'; +import './commands/intent.js'; +import './commands/keyboard.js'; +import './commands/legacy.js'; +import './commands/lock/exports.js'; +import './commands/log.js'; +import './commands/media-projection.js'; +import './commands/memory.js'; +import './commands/misc.js'; +import './commands/network.js'; +import './commands/nfc.js'; +import './commands/performance.js'; +import './commands/permissions.js'; +import './commands/recordscreen.js'; +import './commands/resources.js'; +import './commands/shell.js'; +import './commands/streamscreen.js'; +import './commands/system-bars.js'; +import './commands/time.js'; export type AndroidDriverCaps = DriverCaps; export type W3CAndroidDriverCaps = W3CDriverCaps; @@ -231,217 +267,7 @@ class AndroidDriver; - getContexts = getContexts; - getCurrentContext = getCurrentContext; - defaultContextName = defaultContextName; - assignContexts = assignContexts; - switchContext = switchContext; - defaultWebviewName = defaultWebviewName; - isChromedriverContext = isChromedriverContext; - startChromedriverProxy = startChromedriverProxy; - stopChromedriverProxies = stopChromedriverProxies; - suspendChromedriverProxy = suspendChromedriverProxy; - startChromeSession = startChromeSession; - onChromedriverStop = onChromedriverStop; - isWebContext = isWebContext; - mobileGetContexts = mobileGetContexts; - mobileGetChromeCapabilities = mobileGetChromeCapabilities; - setContext = setContext as any as (this: AndroidDriver, name?: string) => Promise; - setWindow = setWindow; - getWindowHandle = getWindowHandle; - getWindowHandles = getWindowHandles; - notifyBiDiContextChange = notifyBiDiContextChange; - - getDeviceInfoFromCaps = getDeviceInfoFromCaps; - createADB = createADB; - getLaunchInfo = getLaunchInfo; - initDevice = initDevice; - - fingerprint = fingerprint; - mobileFingerprint = mobileFingerprint; - sendSMS = sendSMS; - mobileSendSms = mobileSendSms; - gsmCall = gsmCall; - mobileGsmCall = mobileGsmCall; - gsmSignal = gsmSignal; - mobileGsmSignal = mobileGsmSignal; - gsmVoice = gsmVoice; - mobileGsmVoice = mobileGsmVoice; - powerAC = powerAC; - mobilePowerAc = mobilePowerAc; - powerCapacity = powerCapacity; - mobilePowerCapacity = mobilePowerCapacity; - networkSpeed = networkSpeed; - mobileNetworkSpeed = mobileNetworkSpeed; - sensorSet = sensorSet; - - mobileExecEmuConsoleCommand = mobileExecEmuConsoleCommand; - - getThirdPartyPackages = getThirdPartyPackages; - uninstallOtherPackages = uninstallOtherPackages; - installOtherApks = installOtherApks; - installAUT = installAUT; - resetAUT = resetAUT; - background = background; - getCurrentActivity = getCurrentActivity; - getCurrentPackage = getCurrentPackage; - mobileClearApp = mobileClearApp; - mobileInstallApp = mobileInstallApp; - installApp = installApp; - mobileIsAppInstalled = mobileIsAppInstalled; - mobileRemoveApp = mobileRemoveApp; - mobileTerminateApp = mobileTerminateApp; - mobileListApps = mobileListApps; - terminateApp = terminateApp; - removeApp = removeApp; - activateApp = activateApp; - queryAppState = queryAppState; - isAppInstalled = isAppInstalled; - mobileBackgroundApp = mobileBackgroundApp; - - mobileGetUiMode = mobileGetUiMode; - mobileSetUiMode = mobileSetUiMode; - - mobileDeviceidle = mobileDeviceidle; - - mobileBluetooth = mobileBluetooth; - - getAttribute = getAttribute; - getName = getName; - elementDisplayed = elementDisplayed; - elementEnabled = elementEnabled; - elementSelected = elementSelected; - setElementValue = setElementValue; - doSetElementValue = doSetElementValue; - replaceValue = replaceValue; - setValueImmediate = setValueImmediate; - setValue = setValue; - click = click; - getLocationInView = getLocationInView; - getText = getText; - getLocation = getLocation; - getSize = getSize; - - execute = execute; - - pullFile = pullFile; - pullFolder = pullFolder; - pushFile = pushFile; - mobileDeleteFile = mobileDeleteFile; - - findElOrEls = findElOrEls; - doFindElementOrEls = doFindElementOrEls; - - setGeoLocation = setGeoLocation; - getGeoLocation = getGeoLocation; - mobileRefreshGpsCache = mobileRefreshGpsCache; - toggleLocationServices = toggleLocationServices; - isLocationServicesEnabled = isLocationServicesEnabled; - mobileGetGeolocation = mobileGetGeolocation; - mobileSetGeolocation = mobileSetGeolocation; - mobileResetGeolocation = mobileResetGeolocation; - - performActions = performActions; - - isIMEActivated = isIMEActivated; - availableIMEEngines = availableIMEEngines; - getActiveIMEEngine = getActiveIMEEngine; - activateIMEEngine = activateIMEEngine; - deactivateIMEEngine = deactivateIMEEngine; - setStylusHandwriting = setStylusHandwriting; - - startActivity = startActivity as unknown as ( - appPackage: string, - appActivity: string, - appWaitPackage?: string, - appWaitActivity?: string, - intentAction?: string, - intentCategory?: string, - intentFlags?: string, - optionalIntentArguments?: string, - dontStopAppOnReset?: boolean, - ) => Promise; - mobileStartActivity = mobileStartActivity; - mobileBroadcast = mobileBroadcast; - mobileStartService = mobileStartService; - mobileStopService = mobileStopService; - - hideKeyboard = hideKeyboard; - isKeyboardShown = isKeyboardShown; - keys = keys; - doSendKeys = doSendKeys; - pressKeyCode = pressKeyCode; - longPressKeyCode = longPressKeyCode; - mobilePerformEditorAction = mobilePerformEditorAction; - - lock = lock; - unlock = unlock; - mobileUnlock = mobileUnlock; - isLocked = isLocked; - supportedLogTypes = supportedLogTypes; - mobileStartLogsBroadcast = mobileStartLogsBroadcast; - mobileStopLogsBroadcast = mobileStopLogsBroadcast; - getLogTypes = getLogTypes; - getLog = getLog; - assignBiDiLogListener = assignBiDiLogListener; - - mobileIsMediaProjectionRecordingRunning = mobileIsMediaProjectionRecordingRunning; - mobileStartMediaProjectionRecording = mobileStartMediaProjectionRecording; - mobileStopMediaProjectionRecording = mobileStopMediaProjectionRecording; - - mobileSendTrimMemory = mobileSendTrimMemory; - - mobileInjectEmulatorCameraImage = mobileInjectEmulatorCameraImage; - - getWindowRect = getWindowRect; - getWindowSize = getWindowSize; - getDisplayDensity = getDisplayDensity; - mobileGetNotifications = mobileGetNotifications; - mobileListSms = mobileListSms; - openNotifications = openNotifications; - setUrl = setUrl; - - getNetworkConnection = getNetworkConnection; - isWifiOn = isWifiOn; - mobileGetConnectivity = mobileGetConnectivity; - mobileSetConnectivity = mobileSetConnectivity; - setNetworkConnection = setNetworkConnection; - setWifiState = setWifiState; - setDataState = setDataState; - toggleData = toggleData; - toggleFlightMode = toggleFlightMode; - toggleWiFi = toggleWiFi; - - getPerformanceData = getPerformanceData; - getPerformanceDataTypes = getPerformanceDataTypes; - mobileGetPerformanceData = mobileGetPerformanceData; - - mobileChangePermissions = mobileChangePermissions; - mobileGetPermissions = mobileGetPermissions; - - startRecordingScreen = startRecordingScreen; - stopRecordingScreen = stopRecordingScreen; - - getStrings = getStrings; - ensureDeviceLocale = ensureDeviceLocale; - - mobileShell = mobileShell; - - mobileNfc = mobileNfc; - - mobileStartScreenStreaming = mobileStartScreenStreaming; - mobileStopScreenStreaming = mobileStopScreenStreaming; - - getSystemBars = getSystemBars; - mobilePerformStatusBarCommand = mobilePerformStatusBarCommand; - - getDeviceTime = getDeviceTime; - mobileGetDeviceTime = mobileGetDeviceTime; - - reset = reset; - closeApp = closeApp; - launchApp = launchApp; constructor(opts: InitialOpts = {} as InitialOpts, shouldValidateCaps = true) { super(opts, shouldValidateCaps); @@ -506,6 +332,25 @@ class AndroidDriver { + const nativeLogTypes = await super.getLogTypes(); + if (this.isWebContext() && this.chromedriver) { + const webLogTypes = (await this.chromedriver.jwproxy.command( + '/log/types', + 'GET', + )) as string[]; + return [...nativeLogTypes, ...webLogTypes]; + } + return nativeLogTypes; + } + + override async getLog(logType: string): Promise { + if (this.isWebContext() && this.chromedriver && !Object.keys(this.supportedLogTypes).includes(logType)) { + return await this.chromedriver.jwproxy.command('/log', 'POST', {type: logType}); + } + return await super.getLog(logType); + } + override async deleteSession(sessionId?: string | null) { await removeAllSessionWebSocketHandlers.bind(this)(); @@ -524,4 +369,239 @@ class AndroidDriver Date: Mon, 12 Oct 2026 16:59:24 +0200 Subject: [PATCH 2/4] style: fix formatting in driver.ts Co-Authored-By: Claude Sonnet 5.5 --- lib/driver.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/driver.ts b/lib/driver.ts index 7a90e403..9fbcd86c 100644 --- a/lib/driver.ts +++ b/lib/driver.ts @@ -335,10 +335,7 @@ class AndroidDriver { const nativeLogTypes = await super.getLogTypes(); if (this.isWebContext() && this.chromedriver) { - const webLogTypes = (await this.chromedriver.jwproxy.command( - '/log/types', - 'GET', - )) as string[]; + const webLogTypes = (await this.chromedriver.jwproxy.command('/log/types', 'GET')) as string[]; return [...nativeLogTypes, ...webLogTypes]; } return nativeLogTypes; From c8e3bd64280dd183fceb4e75141bde7cb9c3c5eb Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 12 Oct 2026 21:17:29 +0200 Subject: [PATCH 3/4] chore: fix lint warnings and add inheritance regression test Stringify arrays/unknown values explicitly in log messages, extend the no-duplicates suppression over the bare imports in driver.ts, and cover subclass overrides with `super` in a new unit test. Co-Authored-By: Claude Sonnet 5.5 --- lib/commands/app-management.ts | 4 ++-- lib/commands/bluetooth.ts | 2 +- lib/commands/device/common.ts | 2 +- lib/commands/device/utils.ts | 6 +++--- lib/commands/deviceidle.ts | 2 +- lib/commands/geolocation.ts | 2 +- lib/commands/intent.ts | 2 +- lib/commands/lock/helpers.ts | 2 +- lib/commands/log.ts | 2 +- lib/commands/network.ts | 4 ++-- lib/commands/nfc.ts | 2 +- lib/commands/permissions.ts | 4 +++- lib/commands/resources.ts | 2 +- lib/commands/system-bars.ts | 4 ++-- lib/driver.ts | 2 +- lib/utils.ts | 2 +- test/unit/driver.spec.ts | 33 +++++++++++++++++++++++++++++++++ 17 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 test/unit/driver.spec.ts diff --git a/lib/commands/app-management.ts b/lib/commands/app-management.ts index 483f97bd..0bb721ba 100644 --- a/lib/commands/app-management.ts +++ b/lib/commands/app-management.ts @@ -637,7 +637,7 @@ export async function uninstallOtherPackages( appPackages = await getThirdPartyPackages.bind(this)(filterPackages); } - this.log.debug(`Uninstalling packages: ${appPackages}`); + this.log.debug(`Uninstalling packages: ${appPackages.join(', ')}`); await Promise.all(appPackages.map((appPackage) => this.adb.uninstallApk(appPackage))); } @@ -659,7 +659,7 @@ export async function getThirdPartyPackages(this: AndroidDriver, filterPackages: .trim() .replace(/package:/g, '') .split(EOL); - this.log.debug(`'${appPackagesArray}' filtered with '${filterPackages}'`); + this.log.debug(`'${appPackagesArray.join(', ')}' filtered with '${filterPackages.join(', ')}'`); return appPackagesArray.filter((pkg) => !filterPackagesSet.has(pkg)); } catch (err) { const error = err as Error; diff --git a/lib/commands/bluetooth.ts b/lib/commands/bluetooth.ts index ea1e0925..6c315fd8 100644 --- a/lib/commands/bluetooth.ts +++ b/lib/commands/bluetooth.ts @@ -39,7 +39,7 @@ export async function mobileBluetooth(this: AndroidDriver, action: BluetoothActi break; default: throw new errors.InvalidArgumentError( - `You must provide a valid 'action' argument. Supported actions are: ${Object.values(SUPPORTED_ACTIONS)}`, + `You must provide a valid 'action' argument. Supported actions are: ${Object.values(SUPPORTED_ACTIONS).join(', ')}`, ); } } diff --git a/lib/commands/device/common.ts b/lib/commands/device/common.ts index 7e1c1f36..ace8873a 100644 --- a/lib/commands/device/common.ts +++ b/lib/commands/device/common.ts @@ -58,7 +58,7 @@ export async function getDeviceInfoFromCaps(this: AndroidDriver): Promise typeof entry === 'string')); + } else if (typeof avdArgs === 'string') { + result.push(...util.shellParse(avdArgs).filter((entry): entry is string => typeof entry === 'string')); } } if (networkSpeed) { diff --git a/lib/commands/deviceidle.ts b/lib/commands/deviceidle.ts index 2d0099d2..941e263b 100644 --- a/lib/commands/deviceidle.ts +++ b/lib/commands/deviceidle.ts @@ -43,7 +43,7 @@ export async function mobileDeviceidle( break; default: throw new errors.InvalidArgumentError( - `action must be one of ${JSON.stringify(SUPPORTED_ACTIONS)}. Got '${action}' instead`, + `action must be one of ${JSON.stringify(SUPPORTED_ACTIONS)}. Got '${String(action)}' instead`, ); } await this.adb.shellChunks(argsGenerator, packagesArr); diff --git a/lib/commands/geolocation.ts b/lib/commands/geolocation.ts index 8b04e29a..de44b59a 100644 --- a/lib/commands/geolocation.ts +++ b/lib/commands/geolocation.ts @@ -233,7 +233,7 @@ async function resetMockLocation(this: AndroidDriver): Promise { return; } - this.log.debug(`Resetting mock_location permission for the following apps: ${resultPkgs}`); + this.log.debug(`Resetting mock_location permission for the following apps: ${resultPkgs.join(', ')}`); await asyncmap(resultPkgs, async (pkgId) => { try { await this.adb.shell(['appops', 'set', pkgId, 'android:mock_location', 'deny']); diff --git a/lib/commands/intent.ts b/lib/commands/intent.ts index c66699e3..458d4611 100644 --- a/lib/commands/intent.ts +++ b/lib/commands/intent.ts @@ -409,7 +409,7 @@ function parseIntentSpec(opts: IntentOpts = {}): string[] { } for (const item of extras) { if (!Array.isArray(item)) { - throw new errors.InvalidArgumentError(`Extra argument '${item}' must be an array`); + throw new errors.InvalidArgumentError(`Extra argument '${String(item)}' must be an array`); } const [type, key, value] = item; if (!SUPPORTED_EXTRA_TYPES.includes(type as any)) { diff --git a/lib/commands/lock/helpers.ts b/lib/commands/lock/helpers.ts index f53c0a1f..8cef29f5 100644 --- a/lib/commands/lock/helpers.ts +++ b/lib/commands/lock/helpers.ts @@ -70,7 +70,7 @@ export function validateUnlockCapabilities(caps: T) } } else { throw new Error( - `Invalid unlock type '${unlockType}'. Only the following unlock types are supported: ${UNLOCK_TYPES}`, + `Invalid unlock type '${unlockType}'. Only the following unlock types are supported: ${UNLOCK_TYPES.join(', ')}`, ); } return caps; diff --git a/lib/commands/log.ts b/lib/commands/log.ts index cc9ebcd8..b470b945 100644 --- a/lib/commands/log.ts +++ b/lib/commands/log.ts @@ -82,7 +82,7 @@ export async function mobileStartLogsBroadcast(this: AndroidDriver): Promise !SUPPORTED_SERVICE_NAMES.includes(svc)); if (unsupportedServices.length > 0) { throw new errors.InvalidArgumentError( - `${util.pluralize('Service name', unsupportedServices.length, false)} ${unsupportedServices} ` + + `${util.pluralize('Service name', unsupportedServices.length, false)} ${unsupportedServices.join(', ')} ` + `${unsupportedServices.length === 1 ? 'is' : 'are'} not known. Only the following services are ` + - `suported: ${SUPPORTED_SERVICE_NAMES}`, + `suported: ${SUPPORTED_SERVICE_NAMES.join(', ')}`, ); } diff --git a/lib/commands/nfc.ts b/lib/commands/nfc.ts index c10bb31b..b18d5c9f 100644 --- a/lib/commands/nfc.ts +++ b/lib/commands/nfc.ts @@ -34,7 +34,7 @@ export async function mobileNfc(this: AndroidDriver, action: NfcAction): Promise break; default: throw new errors.InvalidArgumentError( - `You must provide a valid 'action' argument. Supported actions are: ${Object.values(SUPPORTED_ACTIONS)}`, + `You must provide a valid 'action' argument. Supported actions are: ${Object.values(SUPPORTED_ACTIONS).join(', ')}`, ); } } diff --git a/lib/commands/permissions.ts b/lib/commands/permissions.ts index b0f139b4..2ea1babb 100644 --- a/lib/commands/permissions.ts +++ b/lib/commands/permissions.ts @@ -97,7 +97,9 @@ export async function mobileChangePermissions( action?.toLowerCase() as AppOpsAction, ); default: - throw new errors.InvalidArgumentError(`'target' argument must be one of: ${Object.values(PERMISSION_TARGET)}`); + throw new errors.InvalidArgumentError( + `'target' argument must be one of: ${Object.values(PERMISSION_TARGET).join(', ')}`, + ); } } /** diff --git a/lib/commands/resources.ts b/lib/commands/resources.ts index a6f44dfa..7e6d3108 100644 --- a/lib/commands/resources.ts +++ b/lib/commands/resources.ts @@ -76,7 +76,7 @@ export async function ensureDeviceLocale( this.log.debug((e1 as Error).stack); } if (suggestions.length > 0) { - errMsg += ` You may want to apply one of the following locales instead: ${suggestions}`; + errMsg += ` You may want to apply one of the following locales instead: ${suggestions.join(', ')}`; } throw new Error(errMsg, {cause: e}); } diff --git a/lib/commands/system-bars.ts b/lib/commands/system-bars.ts index c0728fda..d1e4f9aa 100644 --- a/lib/commands/system-bars.ts +++ b/lib/commands/system-bars.ts @@ -98,7 +98,7 @@ export async function mobilePerformStatusBarCommand( if (!action) { throw new errors.InvalidArgumentError( `The '${command}' status bar command is unknown. Only the following commands ` + - `are supported: ${Object.keys(statusBarCommands)}`, + `are supported: ${Object.keys(statusBarCommands).join(', ')}`, ); } return await action(); @@ -191,7 +191,7 @@ export function parseWindows(this: AndroidDriver, lines: string): SystemBarsResu this.log.info( `No windows have been found whose title matches to ` + `'${namePrefix}'. Assuming it is invisible. ` + - `Only the following windows are available: ${Object.keys(windows)}`, + `Only the following windows are available: ${Object.keys(windows).join(', ')}`, ); result[window as keyof SystemBarsResult] = structuredClone(DEFAULT_WINDOW_PROPERTIES); } diff --git a/lib/driver.ts b/lib/driver.ts index 9fbcd86c..880966b5 100644 --- a/lib/driver.ts +++ b/lib/driver.ts @@ -190,7 +190,6 @@ import type {AndroidDriverConstraints} from './constraints.js'; import {executeMethodMap} from './execute-method-map.js'; import {newMethodMap} from './method-map.js'; import {parseArray, removeAllSessionWebSocketHandlers} from './utils.js'; -// oxlint-enable import/no-duplicates // Bare imports make sure `declare module` augmentations in the command modules // reach the emitted `driver.d.ts` import graph of downstream drivers. import './commands/app-management.js'; @@ -227,6 +226,7 @@ import './commands/shell.js'; import './commands/streamscreen.js'; import './commands/system-bars.js'; import './commands/time.js'; +// oxlint-enable import/no-duplicates export type AndroidDriverCaps = DriverCaps; export type W3CAndroidDriverCaps = W3CDriverCaps; diff --git a/lib/utils.ts b/lib/utils.ts index c461f9ce..8dd28b59 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -53,7 +53,7 @@ export function parseArray(cap: string | string[]): string[] { return [cap]; } - throw new Error(`must provide a string or JSON Array; received ${cap}`); + throw new Error(`must provide a string or JSON Array; received ${String(cap)}`); } /** diff --git a/test/unit/driver.spec.ts b/test/unit/driver.spec.ts new file mode 100644 index 00000000..4703e5cd --- /dev/null +++ b/test/unit/driver.spec.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import {describe, it} from 'node:test'; + +import sinon from 'sinon'; + +import {AndroidDriver} from '../../lib/driver.js'; + +describe('AndroidDriver inheritance', function () { + it('should attach commands to the prototype', function () { + assert.ok(Object.hasOwn(AndroidDriver.prototype, 'mobileSetUiMode')); + assert.ok(!Object.hasOwn(new AndroidDriver(), 'mobileSetUiMode')); + }); + + it('should allow descendants to override a command and call super', async function () { + const baseStub = sinon.stub(AndroidDriver.prototype, 'getDeviceTime').resolves('base'); + try { + class ChildDriver extends AndroidDriver { + override async getDeviceTime(...args: Parameters) { + return `child:${await super.getDeviceTime(...args)}`; + } + } + assert.strictEqual(await new ChildDriver().getDeviceTime(), 'child:base'); + assert.ok(baseStub.calledOnce); + } finally { + baseStub.restore(); + } + }); + + it('should call the parent implementation in the log overrides', async function () { + const driver = new AndroidDriver(); + assert.ok((await driver.getLogTypes()).includes('logcat')); + }); +}); From dd4071b3ef4720be6586fd6c97fcb4f7c5f0cf6e Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 14 Oct 2026 18:04:36 +0200 Subject: [PATCH 4/4] test: cover mobileResetGeolocation Co-Authored-By: Claude Sonnet 5.5 --- test/unit/commands/geolocation.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/unit/commands/geolocation.spec.ts b/test/unit/commands/geolocation.spec.ts index c76d8440..a3fde505 100644 --- a/test/unit/commands/geolocation.spec.ts +++ b/test/unit/commands/geolocation.spec.ts @@ -1,3 +1,4 @@ +import assert from 'node:assert/strict'; import {describe, it, beforeEach, afterEach} from 'node:test'; import {ADB} from 'appium-adb'; @@ -30,4 +31,19 @@ describe('Geolocation', function () { await setMockLocationApp.bind(driver)('io.appium.settings'); }); }); + + describe('mobileResetGeolocation', function () { + it('should deny mock location for the settings app', async function () { + const shellStub = sandbox + .stub(driver.adb, 'shell') + .callsFake(async (args) => ((args as string[]).includes('packages') ? 'package:com.example.app' : '')); + sandbox.stub(driver.adb, 'fileExists').resolves(false); + await driver.mobileResetGeolocation(); + sinon.assert.calledWith(shellStub, ['appops', 'set', 'io.appium.settings', 'android:mock_location', 'deny']); + }); + it('should throw on emulators', async function () { + driver.opts.avd = 'Pixel'; + await assert.rejects(() => driver.mobileResetGeolocation(), /emulators/); + }); + }); });