UNPKG

appium-remote-debugger

Version:
227 lines (209 loc) 8.33 kB
import _ from 'lodash'; import {errorFromMJSONWPStatusCode} from '@appium/base-driver'; import {util, node} from '@appium/support'; import nodeFs from 'node:fs'; import path from 'node:path'; import type {StringRecord} from '@appium/types'; import type {AppInfo, AppDict, Page} from './types'; const MODULE_NAME = 'appium-remote-debugger'; export const WEB_CONTENT_BUNDLE_ID = 'com.apple.WebKit.WebContent'; const INACTIVE_APP_CODE = 0; // values for the page `WIRTypeKey` entry const ACCEPTED_PAGE_TYPES = [ 'WIRTypeWeb', // up to iOS 11.3 'WIRTypeWebPage', // iOS 11.4 'WIRTypePage', // iOS 11.4 webview ]; export const RESPONSE_LOG_LENGTH = 100; /** * Takes a dictionary from the remote debugger and converts it into a more * manageable AppInfo object with understandable keys. * * @param dict - Dictionary from the remote debugger containing application information. * @returns A tuple containing the application ID and the AppInfo object. */ export function appInfoFromDict(dict: Record<string, any>): [string, AppInfo] { const id = dict.WIRApplicationIdentifierKey; const isProxy = _.isString(dict.WIRIsApplicationProxyKey) ? dict.WIRIsApplicationProxyKey.toLowerCase() === 'true' : dict.WIRIsApplicationProxyKey; // automation enabled can be either from the keys // - WIRRemoteAutomationEnabledKey (boolean) // - WIRAutomationAvailabilityKey (string or boolean) let isAutomationEnabled: boolean | string = !!dict.WIRRemoteAutomationEnabledKey; if (_.has(dict, 'WIRAutomationAvailabilityKey')) { if (_.isString(dict.WIRAutomationAvailabilityKey)) { isAutomationEnabled = dict.WIRAutomationAvailabilityKey === 'WIRAutomationAvailabilityUnknown' ? 'Unknown' : dict.WIRAutomationAvailabilityKey === 'WIRAutomationAvailabilityAvailable'; } else { isAutomationEnabled = !!dict.WIRAutomationAvailabilityKey; } } const entry: AppInfo = { id, isProxy, name: dict.WIRApplicationNameKey, bundleId: dict.WIRApplicationBundleIdentifierKey, hostId: dict.WIRHostApplicationIdentifierKey, isActive: dict.WIRIsApplicationActiveKey !== INACTIVE_APP_CODE, isAutomationEnabled, }; return [id, entry]; } /** * Takes a dictionary from the remote debugger and converts it into an array * of Page objects with understandable keys. Filters out non-web pages. * * @param pageDict - Dictionary from the remote debugger containing page information. * @returns An array of Page objects representing the available pages. */ export function pageArrayFromDict(pageDict: StringRecord): Page[] { return ( _.values(pageDict) // count only WIRTypeWeb pages and ignore all others (WIRTypeJavaScript etc) .filter( (dict) => _.isUndefined(dict.WIRTypeKey) || ACCEPTED_PAGE_TYPES.includes(dict.WIRTypeKey), ) .map((dict) => ({ id: dict.WIRPageIdentifierKey, title: dict.WIRTitleKey, url: dict.WIRURLKey, isKey: !_.isUndefined(dict.WIRConnectionIdentifierKey), })) ); } /** * Finds all application identifier keys that match the given bundle ID. * If no matches are found and the bundle ID is not WEB_CONTENT_BUNDLE_ID, * falls back to searching for WEB_CONTENT_BUNDLE_ID. * * @param bundleId - The bundle identifier to search for. * @param appDict - The application dictionary to search in. * @returns An array of unique application identifier keys matching the bundle ID. */ export function appIdsForBundle(bundleId: string, appDict: AppDict): string[] { const appIds: string[] = _.toPairs(appDict) .filter(([, data]) => data.bundleId === bundleId) .map(([key]) => key); // if nothing is found, try to get the generic app if (appIds.length === 0 && bundleId !== WEB_CONTENT_BUNDLE_ID) { return appIdsForBundle(WEB_CONTENT_BUNDLE_ID, appDict); } return _.uniq(appIds); } /** * Validates that all parameters in the provided object have non-nil values. * Throws an error if any parameters are missing (null or undefined). * * @template T - The type of the parameters object. * @param params - An object containing parameters to validate. * @returns The same parameters object if all values are valid. * @throws Error if any parameters are missing, listing all missing parameter names. */ export function checkParams<T extends StringRecord>(params: T): T { // check if all parameters have a value const errors = _.toPairs(params) .filter(([, value]) => _.isNil(value)) .map(([param]) => param); if (errors.length) { throw new Error(`Missing ${util.pluralize('parameter', errors.length)}: ${errors.join(', ')}`); } return params; } /** * Converts a value to a JSON string, removing noisy function properties * that can muddy the logs. * * @param value - The value to stringify. * @param multiline - If true, formats the JSON with indentation. Defaults to false. * @returns A JSON string representation of the value with noisy properties removed. */ export function simpleStringify(value: any, multiline: boolean = false): string { if (!value) { return JSON.stringify(value); } const cleanValue = removeNoisyProperties(_.clone(value)); return multiline ? JSON.stringify(cleanValue, null, 2) : JSON.stringify(cleanValue); } /** * Converts the result from a JavaScript evaluation in the remote debugger * into a usable format. Handles errors, serialization, and cleans up noisy * function properties. * * @param res - The raw result from the remote debugger's JavaScript evaluation. * @returns The cleaned and converted result value. * @throws Error if the result is undefined, has an unexpected type, or contains * an error status code. */ export function convertJavascriptEvaluationResult(res: any): any { if (_.isUndefined(res)) { throw new Error( `Did not get OK result from remote debugger. Result was: ${_.truncate(simpleStringify(res), {length: RESPONSE_LOG_LENGTH})}`, ); } else if (_.isString(res)) { try { res = JSON.parse(res); } catch { // we might get a serialized object, but we might not // if we get here, it is just a value } } else if (!_.isObject(res)) { throw new Error(`Result has unexpected type: (${typeof res}).`); } if (res.status && res.status !== 0) { // we got some form of error. throw errorFromMJSONWPStatusCode(res.status, res.value.message || res.value); } // with either have an object with a `value` property (even if `null`), // or a plain object const value = _.has(res, 'value') ? res.value : res; return removeNoisyProperties(value); } /** * Calculates the path to the current module's root folder. * The result is memoized for performance. * * @returns The full path to the module root directory. * @throws Error if the module root folder cannot be determined. */ export const getModuleRoot = _.memoize(function getModuleRoot(): string { const root = node.getModuleRootSync(MODULE_NAME, __filename); if (!root) { throw new Error(`Cannot find the root folder of the ${MODULE_NAME} Node.js module`); } return root; }); /** * Reads and parses the package.json file from the module root. * * @returns The parsed package.json contents as a StringRecord. */ export function getModuleProperties(): StringRecord { const fullPath = path.resolve(getModuleRoot(), 'package.json'); return JSON.parse(nodeFs.readFileSync(fullPath, 'utf8')); } /** * Determines if the WebInspector shim can be used based on the provided iOS platform version. * @param platformVersion - The iOS platform version string (e.g., "18.0", "17.5.1") * @returns true if the WebInspector shim can be used, false otherwise */ export function canUseWebInspectorShim(platformVersion: string): boolean { return !!platformVersion && util.compareVersions(platformVersion, '>=', '18.0'); } /** * Removes noisy function properties from an object that can muddy the logs. * These properties are often added by JavaScript number objects and similar. * * @param obj - The object to clean. * @returns The cleaned object. */ function removeNoisyProperties<T>(obj: T): T { if (_.isObject(obj)) { for (const property of ['ceil', 'clone', 'floor', 'round', 'scale', 'toString']) { delete obj[property]; } } return obj; }