UNPKG

@appium/base-driver

Version:

Base driver class for Appium drivers

647 lines (632 loc) 20.3 kB
import type {Driver, DriverMethodDef, HTTPMethod, MethodMap} from '@appium/types'; import _ from 'lodash'; import {DEFAULT_BASE_PATH} from '../constants'; import {match} from 'path-to-regexp'; import {LRUCache} from 'lru-cache'; const COMMAND_NAMES_CACHE = new LRUCache<string, string>({ max: 1024, }); /** * Define the routes: mapping of HTTP methods to particular driver commands, and * any parameters that are expected in a request. Parameters can be `required` or * `optional`. */ export const METHOD_MAP = { // #region W3C WebDriver // https://www.w3.org/TR/webdriver2/ '/session': { POST: { command: 'createSession', payloadParams: { optional: ['capabilities', 'capabilities', 'capabilities'], }, }, }, '/session/:sessionId': { // TODO: JSONWP route, remove in the future GET: {command: 'getSession', deprecated: true}, DELETE: {command: 'deleteSession'}, }, '/status': { GET: {command: 'getStatus'}, }, '/session/:sessionId/timeouts': { GET: {command: 'getTimeouts'}, POST: { command: 'timeouts', payloadParams: { optional: ['type', 'ms', 'script', 'pageLoad', 'implicit'], }, }, }, '/session/:sessionId/url': { GET: {command: 'getUrl'}, POST: {command: 'setUrl', payloadParams: {required: ['url']}}, }, '/session/:sessionId/forward': { POST: {command: 'forward'}, }, '/session/:sessionId/back': { POST: {command: 'back'}, }, '/session/:sessionId/refresh': { POST: {command: 'refresh'}, }, '/session/:sessionId/title': { GET: {command: 'title'}, }, '/session/:sessionId/window': { GET: {command: 'getWindowHandle'}, POST: { command: 'setWindow', payloadParams: { required: ['handle'], }, }, DELETE: {command: 'closeWindow'}, }, '/session/:sessionId/window/handles': { GET: {command: 'getWindowHandles'}, }, '/session/:sessionId/window/new': { POST: {command: 'createNewWindow', payloadParams: {optional: ['type']}}, }, '/session/:sessionId/frame': { POST: {command: 'setFrame', payloadParams: {required: ['id']}}, }, '/session/:sessionId/frame/parent': { POST: {command: 'switchToParentFrame'}, }, '/session/:sessionId/window/rect': { GET: {command: 'getWindowRect'}, POST: { command: 'setWindowRect', payloadParams: {optional: ['x', 'y', 'width', 'height']}, }, }, '/session/:sessionId/window/maximize': { POST: {command: 'maximizeWindow'}, }, '/session/:sessionId/window/minimize': { POST: {command: 'minimizeWindow'}, }, '/session/:sessionId/window/fullscreen': { POST: {command: 'fullScreenWindow'}, }, '/session/:sessionId/element/active': { GET: {command: 'active'}, }, '/session/:sessionId/element/:elementId/shadow': { GET: {command: 'elementShadowRoot'}, }, '/session/:sessionId/element': { POST: { command: 'findElement', payloadParams: {required: ['using', 'value']}, }, }, '/session/:sessionId/elements': { POST: { command: 'findElements', payloadParams: {required: ['using', 'value']}, }, }, '/session/:sessionId/element/:elementId/element': { POST: { command: 'findElementFromElement', payloadParams: {required: ['using', 'value']}, }, }, '/session/:sessionId/element/:elementId/elements': { POST: { command: 'findElementsFromElement', payloadParams: {required: ['using', 'value']}, }, }, '/session/:sessionId/shadow/:shadowId/element': { POST: { command: 'findElementFromShadowRoot', payloadParams: {required: ['using', 'value']}, }, }, '/session/:sessionId/shadow/:shadowId/elements': { POST: { command: 'findElementsFromShadowRoot', payloadParams: {required: ['using', 'value']}, }, }, '/session/:sessionId/element/:elementId/selected': { GET: {command: 'elementSelected'}, }, '/session/:sessionId/element/:elementId/displayed': { GET: {command: 'elementDisplayed'}, }, '/session/:sessionId/element/:elementId/attribute/:name': { GET: {command: 'getAttribute'}, }, '/session/:sessionId/element/:elementId/property/:name': { GET: {command: 'getProperty'}, }, '/session/:sessionId/element/:elementId/css/:propertyName': { GET: {command: 'getCssProperty'}, }, '/session/:sessionId/element/:elementId/text': { GET: {command: 'getText'}, }, '/session/:sessionId/element/:elementId/name': { GET: {command: 'getName'}, }, '/session/:sessionId/element/:elementId/rect': { GET: {command: 'getElementRect'}, }, '/session/:sessionId/element/:elementId/enabled': { GET: {command: 'elementEnabled'}, }, '/session/:sessionId/element/:elementId/computedrole': { GET: {command: 'getComputedRole'}, }, '/session/:sessionId/element/:elementId/computedlabel': { GET: {command: 'getComputedLabel'}, }, '/session/:sessionId/element/:elementId/click': { POST: {command: 'click'}, }, '/session/:sessionId/element/:elementId/clear': { POST: {command: 'clear'}, }, '/session/:sessionId/element/:elementId/value': { POST: { command: 'setValue', payloadParams: { required: ['text'], }, }, }, '/session/:sessionId/source': { GET: {command: 'getPageSource'}, }, '/session/:sessionId/execute/sync': { POST: {command: 'execute', payloadParams: {required: ['script', 'args']}}, }, '/session/:sessionId/execute/async': { POST: { command: 'executeAsync', payloadParams: {required: ['script', 'args']}, }, }, '/session/:sessionId/cookie': { GET: {command: 'getCookies'}, POST: {command: 'setCookie', payloadParams: {required: ['cookie']}}, DELETE: {command: 'deleteCookies'}, }, '/session/:sessionId/cookie/:name': { GET: {command: 'getCookie'}, DELETE: {command: 'deleteCookie'}, }, '/session/:sessionId/actions': { POST: {command: 'performActions', payloadParams: {required: ['actions']}}, DELETE: {command: 'releaseActions'}, }, '/session/:sessionId/alert/dismiss': { POST: {command: 'postDismissAlert'}, }, '/session/:sessionId/alert/accept': { POST: {command: 'postAcceptAlert'}, }, '/session/:sessionId/alert/text': { GET: {command: 'getAlertText'}, POST: { command: 'setAlertText', payloadParams: { required: ['text'], }, }, }, '/session/:sessionId/screenshot': { GET: {command: 'getScreenshot'}, }, '/session/:sessionId/element/:elementId/screenshot': { GET: {command: 'getElementScreenshot'}, }, '/session/:sessionId/print': { POST: { command: 'printPage', payloadParams: { optional: [ 'orientation', 'scale', 'background', 'page', 'margin', 'shrinkToFit', 'pageRanges', ], } } }, // #endregion // #region JSONWP // https://www.selenium.dev/documentation/legacy/json_wire_protocol/ '/session/:sessionId/ime/available_engines': { GET: {command: 'availableIMEEngines', deprecated: true}, }, '/session/:sessionId/ime/active_engine': { GET: {command: 'getActiveIMEEngine', deprecated: true}, }, '/session/:sessionId/ime/activated': { GET: {command: 'isIMEActivated', deprecated: true}, }, '/session/:sessionId/ime/deactivate': { POST: {command: 'deactivateIMEEngine', deprecated: true}, }, '/session/:sessionId/ime/activate': { POST: { command: 'activateIMEEngine', payloadParams: {required: ['engine']}, deprecated: true, }, }, '/session/:sessionId/orientation': { GET: {command: 'getOrientation'}, POST: { command: 'setOrientation', payloadParams: {required: ['orientation']} }, }, '/session/:sessionId/location': { GET: { command: 'getGeoLocation', deprecated: true, }, POST: { command: 'setGeoLocation', payloadParams: {required: ['location']}, deprecated: true, }, }, // #endregion // #region MJSONWP // https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md '/session/:sessionId/rotation': { GET: {command: 'getRotation'}, POST: {command: 'setRotation', payloadParams: {required: ['x', 'y', 'z']}}, }, '/session/:sessionId/context': { GET: {command: 'getCurrentContext'}, POST: {command: 'setContext', payloadParams: {required: ['name']}}, }, '/session/:sessionId/contexts': { GET: {command: 'getContexts'}, }, '/session/:sessionId/network_connection': { GET: {command: 'getNetworkConnection', deprecated: true}, POST: { command: 'setNetworkConnection', payloadParams: {unwrap: 'parameters', required: ['type']}, deprecated: true, }, }, // #endregion // #region Appium '/appium/sessions': { GET: {command: 'getAppiumSessions'}, }, '/session/:sessionId/appium/capabilities': { GET: {command: 'getAppiumSessionCapabilities'} }, '/session/:sessionId/appium/settings': { POST: {command: 'updateSettings', payloadParams: {required: ['settings']}}, GET: {command: 'getSettings'}, }, '/session/:sessionId/appium/commands': { GET: {command: 'listCommands'}, }, '/session/:sessionId/appium/extensions': { GET: {command: 'listExtensions'}, }, '/session/:sessionId/appium/events': { POST: {command: 'getLogEvents', payloadParams: {optional: ['type']}}, }, '/session/:sessionId/appium/log_event': { POST: { command: 'logCustomEvent', payloadParams: {required: ['vendor', 'event']}, }, }, '/session/:sessionId/appium/device/system_time': { GET: {command: 'getDeviceTime'}, POST: {command: 'getDeviceTime', payloadParams: {optional: ['format']}}, }, '/session/:sessionId/appium/device/activate_app': { POST: { command: 'activateApp', payloadParams: { required: [['appId'], ['bundleId']], optional: ['options'], }, }, }, '/session/:sessionId/appium/device/terminate_app': { POST: { command: 'terminateApp', payloadParams: { required: [['appId'], ['bundleId']], optional: ['options'], }, }, }, '/session/:sessionId/appium/device/app_state': { POST: { command: 'queryAppState', payloadParams: { required: [['appId'], ['bundleId']], }, }, }, '/session/:sessionId/appium/device/install_app': { POST: { command: 'installApp', payloadParams: { required: ['appPath'], optional: ['options'], }, }, }, '/session/:sessionId/appium/device/remove_app': { POST: { command: 'removeApp', payloadParams: { required: [['appId'], ['bundleId']], optional: ['options'], }, }, }, '/session/:sessionId/appium/device/app_installed': { POST: { command: 'isAppInstalled', payloadParams: { required: [['appId'], ['bundleId']], }, }, }, '/session/:sessionId/appium/device/hide_keyboard': { POST: { command: 'hideKeyboard', payloadParams: {optional: ['strategy', 'key', 'keyCode', 'keyName']}, }, }, '/session/:sessionId/appium/device/is_keyboard_shown': { GET: {command: 'isKeyboardShown'}, }, '/session/:sessionId/appium/device/push_file': { POST: {command: 'pushFile', payloadParams: {required: ['path', 'data']}}, }, '/session/:sessionId/appium/device/pull_file': { POST: {command: 'pullFile', payloadParams: {required: ['path']}}, }, '/session/:sessionId/appium/device/pull_folder': { POST: {command: 'pullFolder', payloadParams: {required: ['path']}}, }, // #endregion // #region Unknown '/session/:sessionId/receive_async_response': { POST: { command: 'receiveAsyncResponse', payloadParams: {required: ['status', 'value']}, deprecated: true, }, }, '/session/:sessionId/element/:elementId': { GET: {}, }, // #endregion // #region Other Protocols // Selenium/Chromium browsers '/session/:sessionId/se/log': { POST: {command: 'getLog', payloadParams: {required: ['type']}}, }, '/session/:sessionId/se/log/types': { GET: {command: 'getLogTypes'}, }, // Chromium devtools // https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/server/http_handler.cc '/session/:sessionId/:vendor/cdp/execute': { POST: {command: 'executeCdp', payloadParams: {required: ['cmd', 'params']}}, }, // Reporting // https://www.w3.org/TR/reporting-1/ '/session/:sessionId/reporting/generate_test_report': { POST: { command: 'generateTestReport', payloadParams: {required: ['message'], optional: ['group']}, }, }, // Permissions // https://www.w3.org/TR/permissions/ '/session/:sessionId/permissions': { POST: {command: 'setPermissions', payloadParams: {required: ['descriptor', 'state']}}, }, // Device Posture // https://www.w3.org/TR/device-posture/ '/session/:sessionId/deviceposture': { POST: {command: 'setDevicePosture', payloadParams: {required: ['posture']}}, DELETE: {command: 'clearDevicePosture'}, }, // Generic Sensor // https://www.w3.org/TR/generic-sensor/ '/session/:sessionId/sensor': { POST: { command: 'createVirtualSensor', payloadParams: { required: ['type'], optional: ['connected', 'maxSamplingFrequency', 'minSamplingFrequency'], }, }, }, '/session/:sessionId/sensors/:sensorType': { GET: {command: 'getVirtualSensorInfo'}, POST: {command: 'updateVirtualSensorReading', payloadParams: {required: ['reading']}}, DELETE: {command: 'deleteVirtualSensor'}, }, // Custom Handlers // https://html.spec.whatwg.org/multipage/system-state.html#user-agent-automation '/session/:sessionId/custom-handlers/set-mode': { POST: {command: 'setRPHRegistrationMode', payloadParams: {required: ['mode']}}, }, // Webauthn // https://www.w3.org/TR/webauthn-2/#sctn-automation-add-virtual-authenticator '/session/:sessionId/webauthn/authenticator': { POST: { command: 'addVirtualAuthenticator', payloadParams: { required: ['protocol', 'transport'], optional: ['hasResidentKey', 'hasUserVerification', 'isUserConsenting', 'isUserVerified'], }, }, }, '/session/:sessionId/webauthn/authenticator/:authenticatorId': { DELETE: { command: 'removeVirtualAuthenticator', }, }, '/session/:sessionId/webauthn/authenticator/:authenticatorId/credential': { POST: { command: 'addAuthCredential', payloadParams: { required: ['credentialId', 'isResidentCredential', 'rpId', 'privateKey'], optional: ['userHandle', 'signCount'], }, }, }, '/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials': { GET: {command: 'getAuthCredential'}, DELETE: {command: 'removeAllAuthCredentials'}, }, '/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId': { DELETE: {command: 'removeAuthCredential'}, }, '/session/:sessionId/webauthn/authenticator/:authenticatorId/uv': { POST: { command: 'setUserAuthVerified', payloadParams: { required: ['isUserVerified'], }, }, }, // Secure Payment Confirmation // https://www.w3.org/TR/secure-payment-confirmation/ '/session/:sessionId/secure-payment-confirmation/set-mode': { POST: {command: 'setSPCTransactionMode', payloadParams: {required: ['mode']}}, }, // Federated Credential Management // https://www.w3.org/TR/fedcm-1/ '/session/:sessionId/fedcm/canceldialog': { POST: {command: 'fedCMCancelDialog'}, }, '/session/:sessionId/fedcm/selectaccount': { POST: {command: 'fedCMSelectAccount', payloadParams: {required: ['accountIndex']}}, }, '/session/:sessionId/fedcm/clickdialogbutton': { POST: {command: 'fedCMClickDialogButton', payloadParams: {required: ['dialogButton']}}, }, '/session/:sessionId/fedcm/accountlist': { GET: {command: 'fedCMGetAccounts'}, }, '/session/:sessionId/fedcm/gettitle': { GET: {command: 'fedCMGetTitle'}, }, '/session/:sessionId/fedcm/getdialogtype': { GET: {command: 'fedCMGetDialogType'}, }, '/session/:sessionId/fedcm/setdelayenabled': { POST: {command: 'fedCMSetDelayEnabled', payloadParams: {required: ['enabled']}}, }, '/session/:sessionId/fedcm/resetcooldown': { POST: {command: 'fedCMResetCooldown'}, }, // Compute Pressure // https://www.w3.org/TR/compute-pressure/ '/session/:sessionId/pressuresource': { POST: { command: 'createVirtualPressureSource', payloadParams: {required: ['type'], optional: ['supported']}, }, }, '/session/:sessionId/pressuresource/:pressureSourceType': { POST: {command: 'updateVirtualPressureSource', payloadParams: {required: ['sample']}}, DELETE: {command: 'deleteVirtualPressureSource'}, }, // Global Privacy Control (GPC) // https://www.w3.org/TR/gpc/ '/session/:sessionId/privacy': { GET: {command: 'getGlobalPrivacyControl'}, POST: {command: 'setGlobalPrivacyControl', payloadParams: {required: ['gpc']}}, }, // Storage Access // https://privacycg.github.io/storage-access/ '/session/:sessionId/storageaccess': { POST: {command: 'setStorageAccess', payloadParams: {required: ['blocked', 'origin']}}, }, // #endregion } as const satisfies MethodMap<Driver>; // driver command names export const ALL_COMMANDS = _.flatMap(_.values(METHOD_MAP).map(_.values)) .filter((m) => Boolean(m.command)) .map((m) => m.command); /** * Resolve a WebDriver URL path and HTTP method to a driver command name from {@link METHOD_MAP}. * @param endpoint - Request URL or path (may include base path) * @param method - HTTP method (used when one path maps to multiple commands) * @param basePath - Optional base path prefix to strip before matching */ export function routeToCommandName( endpoint: string, method?: HTTPMethod, basePath?: string ): string | undefined { const resolvedBasePath = basePath ?? DEFAULT_BASE_PATH; let normalizedEndpoint = resolvedBasePath ? endpoint.replace(new RegExp(`^${_.escapeRegExp(resolvedBasePath)}`), '') : endpoint; normalizedEndpoint = `${_.startsWith(normalizedEndpoint, '/') ? '' : '/'}${normalizedEndpoint}`; let normalizedPathname: string; try { // we could use any prefix there as we anyway need to only extract the pathname normalizedPathname = new URL(`https://appium.io${normalizedEndpoint}`).pathname; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); throw new Error(`'${endpoint}' cannot be translated to a command name: ${msg}`, {cause: err}); } const normalizedMethod = _.toUpper(method ?? ''); const cacheKey = toCommandNameCacheKey(normalizedPathname, normalizedMethod); const cached = COMMAND_NAMES_CACHE.get(cacheKey); if (cached !== undefined) { return cached || undefined; } const possiblePathnames: string[] = []; if (!normalizedPathname.startsWith('/session/')) { possiblePathnames.push(`/session/any-session-id${normalizedPathname}`); } possiblePathnames.push(normalizedPathname); for (const [routePath, routeSpec] of _.toPairs(METHOD_MAP)) { const routeMatcher = match(routePath); if (possiblePathnames.some((pp) => routeMatcher(pp))) { const spec = routeSpec as Record<string, DriverMethodDef<Driver>>; const commandForAnyMethod = () => _.first(_.keys(spec).map((key) => spec[key]?.command)); const commandName = normalizedMethod ? spec[normalizedMethod]?.command : commandForAnyMethod(); if (commandName) { COMMAND_NAMES_CACHE.set(cacheKey, commandName); return commandName; } } } // storing an empty string means we did not find any match for this set of arguments // and we want to cache this result COMMAND_NAMES_CACHE.set(cacheKey, ''); } function toCommandNameCacheKey(endpoint: string, method?: string): string { return `${endpoint}:${method ?? ''}`; } // driver commands that do not require a session to already exist export const NO_SESSION_ID_COMMANDS = ['createSession', 'getStatus', 'getAppiumSessions'];