UNPKG

appium

Version:

Automation for Apps.

518 lines (472 loc) 15.3 kB
import _ from 'lodash'; import logger from './logger'; import { processCapabilities, STANDARD_CAPS, errors, isW3cCaps, } from '@appium/base-driver'; import {inspect as dump} from 'util'; import {node, fs} from '@appium/support'; import path from 'path'; import {SERVER_SUBCOMMAND, DRIVER_TYPE, PLUGIN_TYPE, SETUP_SUBCOMMAND} from './constants'; import os from 'node:os'; const W3C_APPIUM_PREFIX = 'appium'; const STANDARD_CAPS_LOWERCASE = new Set([...STANDARD_CAPS].map((cap) => cap.toLowerCase())); export const V4_BROADCAST_IP = '0.0.0.0'; export const V6_BROADCAST_IP = '::'; export const npmPackage = fs.readPackageJsonFrom(__dirname); /** * * If `stdout` is a TTY, this is `true`. * * Used for tighter control over log output. * @type {boolean} */ const isStdoutTTY = process.stdout.isTTY; /** * Creates an error object in case a session gets incompatible capabilities as the input. * * @returns {Error} */ export function makeNonW3cCapsError() { return new errors.SessionNotCreatedError( 'Session capabilities format must comply to the W3C standard. Make sure your client is up to date. ' + 'See https://www.w3.org/TR/webdriver/#new-session for more details.' ); } /** * Dumps to value to the console using `info` logger. * * @todo May want to force color to be `false` if {@link isStdoutTTY} is `false`. */ export const inspect = _.flow( _.partialRight( /** @type {(object: any, options: import('util').InspectOptions) => string} */ (dump), {colors: true, depth: null, compact: !isStdoutTTY} ), (...args) => { logger.info(...args); } ); /** * Takes the caps that were provided in the request and translates them * into caps that can be used by the inner drivers. * * @template {Constraints} C * @param {W3CCapabilities<C>} w3cCapabilities * @param {C} constraints * @param {NSCapabilities<C>} [defaultCapabilities] * @returns {ParsedDriverCaps<C>|InvalidCaps<C>} */ export function parseCapsForInnerDriver( w3cCapabilities, constraints = /** @type {C} */ ({}), defaultCapabilities = {} ) { if (!isW3cCaps(w3cCapabilities)) { return /** @type {InvalidCaps<C>} */ ({ error: makeNonW3cCapsError(), }); } let desiredCaps = /** @type {ParsedDriverCaps<C>['desiredCaps']} */ ({}); /** @type {ParsedDriverCaps<C>['processedW3CCapabilities'] | undefined} */ let processedW3CCapabilities; // Make sure we don't mutate the original arguments w3cCapabilities = _.cloneDeep(w3cCapabilities); defaultCapabilities = _.cloneDeep(defaultCapabilities); if (!_.isEmpty(defaultCapabilities)) { for (const [defaultCapKey, defaultCapValue] of _.toPairs(defaultCapabilities)) { let isCapAlreadySet = false; // Check if the key is already present in firstMatch entries for (const firstMatchEntry of w3cCapabilities.firstMatch ?? []) { if ( _.isPlainObject(firstMatchEntry) && _.has(removeAppiumPrefixes(firstMatchEntry), removeAppiumPrefix(defaultCapKey)) ) { isCapAlreadySet = true; break; } } // Check if the key is already present in alwaysMatch entries isCapAlreadySet = isCapAlreadySet || (_.isPlainObject(w3cCapabilities.alwaysMatch) && _.has( removeAppiumPrefixes(w3cCapabilities.alwaysMatch), removeAppiumPrefix(defaultCapKey) )); if (isCapAlreadySet) { // Skip if the key is already present in the provided caps continue; } // Only add the default capability if it is not overridden if (_.isEmpty(w3cCapabilities.firstMatch)) { w3cCapabilities.firstMatch = /** @type {W3CCapabilities<C>['firstMatch']} */ ([ {[defaultCapKey]: defaultCapValue}, ]); } else { w3cCapabilities.firstMatch[0][defaultCapKey] = defaultCapValue; } } } // Call the process capabilities algorithm to find matching caps on the W3C // (see: https://github.com/jlipps/simple-wd-spec#processing-capabilities) try { desiredCaps = processCapabilities(w3cCapabilities, constraints, true); } catch (error) { logger.info(`Could not parse W3C capabilities: ${error.message}`); return /** @type {InvalidCaps<C>} */ ({ desiredCaps, processedW3CCapabilities, error, }); } // Create a new w3c capabilities payload that contains only the matching caps in `alwaysMatch` processedW3CCapabilities = { alwaysMatch: {...insertAppiumPrefixes(desiredCaps)}, firstMatch: [{}], }; return /** @type {ParsedDriverCaps<C>} */ ({ desiredCaps, processedW3CCapabilities, }); } /** * Takes a capabilities objects and prefixes capabilities with `appium:` * @template {Constraints} [C={}] * @param {Capabilities<C>} caps - Desired capabilities object * @returns {NSCapabilities<C>} */ export function insertAppiumPrefixes(caps) { return /** @type {NSCapabilities<C>} */ ( _.mapKeys(caps, (_, key) => STANDARD_CAPS_LOWERCASE.has(key.toLowerCase()) || key.includes(':') ? key : `${W3C_APPIUM_PREFIX}:${key}` ) ); } /** * @template {Constraints} [C={}] * @param {NSCapabilities<C>} caps * @returns {Capabilities<C>} */ export function removeAppiumPrefixes(caps) { return /** @type {Capabilities<C>} */ (_.mapKeys(caps, (_, key) => removeAppiumPrefix(key))); } /** * @param {string} key * @returns {string} */ function removeAppiumPrefix(key) { const prefix = `${W3C_APPIUM_PREFIX}:`; return _.startsWith(key, prefix) ? key.substring(prefix.length) : key; } /** * * @param {string} pkgName * @returns {string|undefined} */ export function getPackageVersion(pkgName) { const pkgInfo = require(`${pkgName}/package.json`) || {}; return pkgInfo.version; } /** * Returns the root directory of the Appium module. * * @returns {string} - The absolute path to the Appium module root directory. * @throws {Error} - If the Appium module root cannot be determined. */ export const getAppiumModuleRoot = _.memoize(function getAppiumModuleRoot() { const selfRoot = node.getModuleRootSync('appium', __filename); if (!selfRoot) { throw new Error('Cannot find the appium module root. This is likely a bug in Appium.'); } return selfRoot; }); /** * Adjusts NODE_PATH environment variable, * so CJS drivers and plugins could load their peer dependencies. * Read https://nodejs.org/api/modules.html#loading-from-the-global-folders * for more details. * * Unfortunately this hack does not work with ESM modules, * @returns {void} */ export function adjustNodePath() { let appiumModuleSearchRoot; try { appiumModuleSearchRoot = path.dirname(getAppiumModuleRoot()); } catch (error) { logger.warn(error.message); return; } const refreshRequirePaths = () => { try { // ! This hack allows us to avoid modification of import // ! statements in client modules. It uses a private API though, // ! so it could break (maybe, eventually). // See https://gist.github.com/branneman/8048520#7-the-hack // @ts-ignore see above comment require('module').Module._initPaths(); return true; } catch { return false; } }; if (!process.env.NODE_PATH) { process.env.NODE_PATH = appiumModuleSearchRoot; if (refreshRequirePaths()) { process.env.APPIUM_OMIT_PEER_DEPS = '1'; } else { delete process.env.NODE_PATH; } return; } const nodePathParts = process.env.NODE_PATH.split(path.delimiter); if (nodePathParts.includes(appiumModuleSearchRoot)) { process.env.APPIUM_OMIT_PEER_DEPS = '1'; return; } nodePathParts.push(appiumModuleSearchRoot); process.env.NODE_PATH = nodePathParts.join(path.delimiter); if (refreshRequirePaths()) { process.env.APPIUM_OMIT_PEER_DEPS = '1'; } else { process.env.NODE_PATH = _.without(nodePathParts, appiumModuleSearchRoot).join(path.delimiter); } } /** * Pulls the initial values of Appium settings from the given capabilities argument. * Each setting item must satisfy the following format: * `settings[setting_name]: setting_value` * or * ``` * settings = { * setting_name1: 'setting_value1', * setting_name2: 'setting_value2', * } * ``` * The capabilities argument itself gets mutated, so it does not contain parsed * settings anymore to avoid further parsing issues. * Check * https://appium.io/docs/en/latest/guides/settings/ * for more details on the available settings. * * @param {?Object} caps - Capabilities dictionary. It is mutated if * one or more settings have been pulled from it * @return {Object} - An empty dictionary if the given caps contains no * setting items or a dictionary containing parsed Appium setting names along with * their values. */ export function pullSettings(caps) { if (!_.isPlainObject(caps) || _.isEmpty(caps)) { return {}; } const result = {}; const singleSettings = {}; for (const [key, value] of _.toPairs(caps)) { let match; if (/^(s|appium:s)ettings$/.test(key) && _.isPlainObject(value)) { Object.assign(result, value); delete caps[key]; } else if ((match = /^(s|appium:s)ettings\[(\S+)\]$/.exec(key))) { singleSettings[match[2]] = value; delete caps[key]; } } if (!_.isEmpty(singleSettings)) { Object.assign(result, singleSettings); } return result; } /** * @template {CliCommand} [Cmd=ServerCommand] * @template {CliExtensionSubcommand|void} [SubCmd=void] * @param {Args<Cmd, SubCmd>} args * @returns {args is Args<ServerCommand>} */ export function isServerCommandArgs(args) { return args.subcommand === SERVER_SUBCOMMAND; } /** * @template {CliCommand} Cmd * @template {CliExtensionSubcommand|CliCommandSetupSubcommand|void} [SubCmd=void] * @param {Args<Cmd, SubCmd>} args * @returns {args is Args<SetupCommand>} */ export function isSetupCommandArgs(args) { return args.subcommand === SETUP_SUBCOMMAND; } /** * @template {CliCommand} [Cmd=ServerCommand] * @template {CliExtensionSubcommand|void} [SubCmd=void] * @param {Args<Cmd, SubCmd>} args * @returns {args is Args<CliExtensionCommand, SubCmd>} */ export function isExtensionCommandArgs(args) { return args.subcommand === DRIVER_TYPE || args.subcommand === PLUGIN_TYPE; } /** * @template {CliCommand} Cmd * @template {CliExtensionSubcommand} SubCmd * @param {Args<Cmd, SubCmd>} args * @returns {args is Args<DriverCommand, SubCmd>} */ export function isDriverCommandArgs(args) { return args.subcommand === DRIVER_TYPE; } /** * @template {CliCommand} Cmd * @template {CliExtensionSubcommand} SubCmd * @param {Args<Cmd, SubCmd>} args * @returns {args is Args<PluginCommand, SubCmd>} */ export function isPluginCommandArgs(args) { return args.subcommand === PLUGIN_TYPE; } /** * Fetches the list of matched network interfaces of the current host. * * @param {4|6|null} family Either 4 to include ipv4 addresses only, * 6 to include ipv6 addresses only, or null to include all of them * @returns {os.NetworkInterfaceInfo[]} The list of matched interfaces */ export function fetchInterfaces (family = null) { let familyValue = null; // 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6 if (family === 4) { familyValue = [4, 'IPv4']; } else if (family === 6) { familyValue = [6, 'IPv6']; } // @ts-ignore The linter does not understand the below filter return _.flatMap(_.values(os.networkInterfaces()).filter(Boolean)) // @ts-ignore The linter does not understand the above filter .filter(({family}) => !familyValue || familyValue && familyValue.includes(family)); } /** * https://github.com/SheetJS/js-adler32 * * @param {string} str * @param {number?} [seed] * @returns {number} */ export function adler32(str, seed = null) { let a = 1, b = 0, L = str.length, M = 0, c = 0, d = 0; if (typeof seed === 'number') { a = seed & 0xFFFF; b = seed >>> 16; } for (let i = 0; i < L;) { M = Math.min(L - i, 2918); while (M > 0) { c = str.charCodeAt(i++); if (c < 0x80) { a += c; } else if (c < 0x800) { a += 192 | ((c >> 6) & 31); b += a; --M; a += 128 | (c & 63); } else if (c >= 0xD800 && c < 0xE000) { c = (c & 1023) + 64; d = str.charCodeAt(i++) & 1023; a += 240 | ((c >> 8) & 7); b += a; --M; a += 128 | ((c >> 2) & 63); b += a; --M; a += 128 | ((d >> 6) & 15) | ((c & 3) << 4); b += a; --M; a += 128 | (d & 63); } else { a += 224 | ((c >> 12) & 15); b += a; --M; a += 128 | ((c >> 6) & 63); b += a; --M; a += 128 | (c & 63); } b += a; --M; } a = (15 * (a >>> 16) + (a & 65535)); b = (15 * (b >>> 16) + (b & 65535)); } return ((b % 65521) << 16) | (a % 65521); } /** * Checks if the provided address is a broadcast one. * * @param {string} address * @returns {boolean} */ export function isBroadcastIp(address) { return [V4_BROADCAST_IP, V6_BROADCAST_IP, `[${V6_BROADCAST_IP}]`].includes(address); } /** * @typedef {import('@appium/types').StringRecord} StringRecord * @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints */ /** * @template {Constraints} [C=BaseDriverCapConstraints] * @typedef ParsedDriverCaps * @property {Capabilities<C>} desiredCaps * @property {W3CCapabilities<C>} processedW3CCapabilities */ /** * @todo protocol is more specific * @template {Constraints} [C=BaseDriverCapConstraints] * @typedef InvalidCaps * @property {Error} error * @property {Capabilities<C>} [desiredCaps] * @property {W3CCapabilities<C>} [processedW3CCapabilities] */ /** * @template {Constraints} C * @typedef {import('@appium/types').Capabilities<C>} Capabilities */ /** * @template {Constraints} C * @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities */ /** * @template {Constraints} C * @typedef {import('@appium/types').NSCapabilities<C>} NSCapabilities */ /** * @template {Constraints} C * @typedef {import('@appium/types').ConstraintsToCaps<C>} ConstraintsToCaps */ /** * @template T * @typedef {import('type-fest').StringKeyOf<T>} StringKeyOf */ /** * @typedef {import('@appium/types').Constraints} Constraints */ /** * @typedef {import('appium/types').CliCommand} CliCommand * @typedef {import('appium/types').CliExtensionSubcommand} CliExtensionSubcommand * @typedef {import('appium/types').CliExtensionCommand} CliExtensionCommand * @typedef {import('appium/types').CliCommandSetupSubcommand} CliCommandSetupSubcommand * @typedef {import('appium/types').CliCommandServer} ServerCommand * @typedef {import('appium/types').CliCommandDriver} DriverCommand * @typedef {import('appium/types').CliCommandPlugin} PluginCommand * @typedef {import('appium/types').CliCommandSetup} SetupCommand */ /** * @template {CliCommand} [Cmd=ServerCommand] * @template {CliExtensionSubcommand|CliCommandSetupSubcommand|void} [SubCmd=void] * @typedef {import('appium/types').Args<Cmd, SubCmd>} Args */ /** * @template {CliCommand} [Cmd=ServerCommand] * @template {CliExtensionSubcommand|CliCommandSetupSubcommand|void} [SubCmd=void] * @typedef {import('appium/types').ParsedArgs<Cmd, SubCmd>} ParsedArgs */