From 20bdd85de5b801098a23b1f95dd46f72d97a9506 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 21 Apr 2024 18:44:53 +0200 Subject: [PATCH] feat: Improve the typing system (#298) --- lib/commands/app-management.js | 75 ++-- lib/commands/applescript.js | 37 +- lib/commands/execute.js | 27 +- lib/commands/find.js | 26 +- lib/commands/gestures.js | 360 ++++------------- lib/commands/index.js | 27 -- lib/commands/navigation.js | 21 +- lib/commands/record-screen.js | 75 +--- lib/commands/screenshots.js | 37 +- lib/commands/source.js | 27 +- lib/driver.js | 68 +++- lib/types.ts | 688 +++++++++++++++++++++++++++++++++ lib/wda-mac.js | 8 +- 13 files changed, 924 insertions(+), 552 deletions(-) delete mode 100644 lib/commands/index.js create mode 100644 lib/types.ts diff --git a/lib/commands/app-management.js b/lib/commands/app-management.js index 6b3458a..bf756cd 100644 --- a/lib/commands/app-management.js +++ b/lib/commands/app-management.js @@ -1,27 +1,13 @@ -const commands = {}; - -/** - * @typedef {Object} LaunchAppOptions - * @property {string} [bundleId] Bundle identifier of the app to be launched - * or activated. Either this property or `path` must be provided - * @property {string} [path] Full path to the app bundle. Either this property - * or `bundleId` must be provided - * @property {string[]} arguments the list of command line arguments - * for the app to be be launched with. This parameter is ignored if the app - * is already running. - * @property {Object} environment environment variables mapping. Custom - * variables are added to the default process environment. - */ - /** * Start an app with given bundle identifier or activates it * if the app is already running. An exception is thrown if the * app with the given identifier cannot be found. * - * @param {LaunchAppOptions} opts + * @this {Mac2Driver} + * @param {import('../types').LaunchAppOptions} [opts={}] */ -commands.macosLaunchApp = async function macosLaunchApp (opts) { - const { bundleId, environment, path } = opts ?? {}; +export async function macosLaunchApp (opts = {}) { + const { bundleId, environment, path } = opts; return await this.wda.proxy.command('/wda/apps/launch', 'POST', { arguments: opts.arguments, environment, @@ -30,66 +16,51 @@ commands.macosLaunchApp = async function macosLaunchApp (opts) { }); }; -/** - * @typedef {Object} ActivateAppOptions - * @property {string} [bundleId] Bundle identifier of the app to be activated. - * Either this property or `path` must be provided - * @property {string} [path] Full path to the app bundle. Either this property - * or `bundleId` must be provided - */ - /** * Activate an app with given bundle identifier. An exception is thrown if the * app cannot be found or is not running. * - * @param {ActivateAppOptions} opts + * @this {Mac2Driver} + * @param {import('../types').ActivateAppOptions} [opts={}] */ -commands.macosActivateApp = async function macosActivateApp (opts) { - const { bundleId, path } = opts ?? {}; +export async function macosActivateApp (opts = {}) { + const { bundleId, path } = opts; return await this.wda.proxy.command('/wda/apps/activate', 'POST', { bundleId, path }); }; -/** - * @typedef {Object} TerminateAppOptions - * @property {string} [bundleId] Bundle identifier of the app to be terminated. - * Either this property or `path` must be provided - * @property {string} [path] Full path to the app bundle. Either this property - * or `bundleId` must be provided - */ - /** * Terminate an app with given bundle identifier. An exception is thrown if the * app cannot be found. * - * @param {TerminateAppOptions} opts + * @this {Mac2Driver} + * @param {import('../types').TerminateAppOptions} opts * @returns {Promise} `true` if the app was running and has been successfully terminated. * `false` if the app was not running before. */ -commands.macosTerminateApp = async function macosTerminateApp (opts) { +export async function macosTerminateApp (opts) { const { bundleId, path } = opts ?? {}; - return await this.wda.proxy.command('/wda/apps/terminate', 'POST', { bundleId, path }); + return /** @type {boolean} */ ( + await this.wda.proxy.command('/wda/apps/terminate', 'POST', { bundleId, path }) + ); }; -/** - * @typedef {Object} QueryAppStateOptions - * @property {string} [bundleId] Bundle identifier of the app whose state should be queried. - * Either this property or `path` must be provided - * @property {string} [path] Full path to the app bundle. Either this property - * or `bundleId` must be provided - */ - /** * Query an app state with given bundle identifier. An exception is thrown if the * app cannot be found. * - * @param {QueryAppStateOptions} opts + * @this {Mac2Driver} + * @param {import('../types').QueryAppStateOptions} opts * @returns {Promise} The application state code. See * https://developer.apple.com/documentation/xctest/xcuiapplicationstate?language=objc * for more details */ -commands.macosQueryAppState = async function macosQueryAppState (opts) { +export async function macosQueryAppState (opts) { const { bundleId, path } = opts ?? {}; - return await this.wda.proxy.command('/wda/apps/state', 'POST', { bundleId, path }); + return /** @type {number} */ ( + await this.wda.proxy.command('/wda/apps/state', 'POST', { bundleId, path }) + ); }; -export default commands; +/** + * @typedef {import('../driver').Mac2Driver} Mac2Driver + */ diff --git a/lib/commands/applescript.js b/lib/commands/applescript.js index dcf32d4..d28f9d9 100644 --- a/lib/commands/applescript.js +++ b/lib/commands/applescript.js @@ -1,25 +1,10 @@ import { fs, tempDir, util } from 'appium/support'; import { exec } from 'teen_process'; -import log from '../logger'; import path from 'path'; const OSASCRIPT = 'osascript'; const APPLE_SCRIPT_FEATURE = 'apple_script'; -const commands = {}; - -/** - * @typedef {Object} ExecAppleScriptOptions - * @property {string} script A valid AppleScript to execute - * @property {string} language Overrides the scripting language. Basically, sets the value of `-l` command - * line argument of `osascript` tool. If unset the AppleScript language is assumed. - * @property {string} command A valid AppleScript as a single command (no line breaks) to execute - * @property {number} timeout [20000] The number of seconds to wait until a long-running command is - * finished. An error is thrown if the command is still running after this timeout expires. - * @property {string} cwd The path to an existing folder, which is going to be set as the - * working directory for the command/script being executed. - */ - /** * Executes the given AppleScript command or a whole script based on the * given options. Either of these options must be provided. If both are provided @@ -31,12 +16,13 @@ const commands = {}; * and no permissions to do it are given to the parent (for example, Appium or Terminal) * process in System Preferences -> Privacy list. * - * @param {ExecAppleScriptOptions} opts + * @this {Mac2Driver} + * @param {import('../types').ExecAppleScriptOptions} opts * @returns {Promise} The actual stdout of the given command/script * @throws {Error} If the exit code of the given command/script is not zero. * The actual stderr output is set to the error message value. */ -commands.macosExecAppleScript = async function macosExecAppleScript (opts) { +export async function macosExecAppleScript (opts = {}) { this.ensureFeatureEnabled(APPLE_SCRIPT_FEATURE); const { @@ -45,12 +31,12 @@ commands.macosExecAppleScript = async function macosExecAppleScript (opts) { command, cwd, timeout, - } = opts ?? {}; + } = opts; if (!script && !command) { - log.errorAndThrow('AppleScript script/command must not be empty'); + this.log.errorAndThrow('AppleScript script/command must not be empty'); } - if (/\n/.test(command)) { - log.errorAndThrow('AppleScript commands cannot contain line breaks'); + if (/\n/.test(/** @type {string} */(command))) { + this.log.errorAndThrow('AppleScript commands cannot contain line breaks'); } // 'command' has priority over 'script' const shouldRunScript = !command; @@ -64,12 +50,12 @@ commands.macosExecAppleScript = async function macosExecAppleScript (opts) { if (shouldRunScript) { tmpRoot = await tempDir.openDir(); const tmpScriptPath = path.resolve(tmpRoot, 'appium_script.scpt'); - await fs.writeFile(tmpScriptPath, script, 'utf8'); + await fs.writeFile(tmpScriptPath, /** @type {string} */(script), 'utf8'); args.push(tmpScriptPath); } else { args.push('-e', command); } - log.info(`Running ${OSASCRIPT} with arguments: ${util.quote(args)}`); + this.log.info(`Running ${OSASCRIPT} with arguments: ${util.quote(args)}`); try { const {stdout} = await exec(OSASCRIPT, args, {cwd, timeout}); return stdout; @@ -83,5 +69,6 @@ commands.macosExecAppleScript = async function macosExecAppleScript (opts) { } }; -export { commands }; -export default commands; +/** + * @typedef {import('../driver').Mac2Driver} Mac2Driver + */ diff --git a/lib/commands/execute.js b/lib/commands/execute.js index 222bcdb..91ce471 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -1,8 +1,5 @@ import _ from 'lodash'; import { errors } from 'appium/driver'; -import log from '../logger'; - -const commands = {}; const EXTENSION_COMMANDS_MAPPING = { setValue: 'macosSetValue', @@ -39,16 +36,30 @@ const EXTENSION_COMMANDS_MAPPING = { deepLink: 'macosDeepLink', }; -commands.execute = async function execute (script, args) { +/** + * + * @this {Mac2Driver} + * @param {string} script + * @param {any[]|import('@appium/types').StringRecord} [args] + * @returns {Promise} + */ +export async function execute (script, args) { if (script.match(/^macos:/)) { - log.info(`Executing extension command '${script}'`); + this.log.info(`Executing extension command '${script}'`); script = script.replace(/^macos:/, '').trim(); return await this.executeMacosCommand(script, _.isArray(args) ? args[0] : args); } throw new errors.NotImplementedError(); }; -commands.executeMacosCommand = async function executeMacosCommand (command, opts = {}) { +/** + * + * @this {Mac2Driver} + * @param {string} command + * @param {import('@appium/types').StringRecord} [opts={}] + * @returns {Promise} + */ +export async function executeMacosCommand (command, opts = {}) { if (!_.has(EXTENSION_COMMANDS_MAPPING, command)) { throw new errors.UnknownCommandError(`Unknown extension command "${command}". ` + `Only ${_.keys(EXTENSION_COMMANDS_MAPPING)} commands are supported.`); @@ -56,4 +67,6 @@ commands.executeMacosCommand = async function executeMacosCommand (command, opts return await this[EXTENSION_COMMANDS_MAPPING[command]](opts); }; -export default commands; +/** + * @typedef {import('../driver').Mac2Driver} Mac2Driver + */ diff --git a/lib/commands/find.js b/lib/commands/find.js index 2e30aac..d762c50 100644 --- a/lib/commands/find.js +++ b/lib/commands/find.js @@ -1,12 +1,18 @@ import { util } from 'appium/support'; - -const commands = {}; - -// This is needed to make lookup by image working -commands.findElOrEls = async function findElOrEls (strategy, selector, mult, context) { - context = util.unwrapElement(context); - const endpoint = `/element${context ? `/${context}/element` : ''}${mult ? 's' : ''}`; +/** + * This is needed to make lookup by image working + * + * @this {Mac2Driver} + * @param {string} strategy + * @param {string} selector + * @param {boolean} mult + * @param {string} [context] + * @returns {Promise} + */ +export async function findElOrEls (strategy, selector, mult, context) { + const contextId = context ? util.unwrapElement(context) : context; + const endpoint = `/element${contextId ? `/${contextId}/element` : ''}${mult ? 's' : ''}`; if (strategy === '-ios predicate string') { strategy = 'predicate string'; @@ -20,6 +26,6 @@ commands.findElOrEls = async function findElOrEls (strategy, selector, mult, con }); }; - -export { commands }; -export default commands; +/** + * @typedef {import('../driver').Mac2Driver} Mac2Driver + */ diff --git a/lib/commands/gestures.js b/lib/commands/gestures.js index 25a24a9..54999d6 100644 --- a/lib/commands/gestures.js +++ b/lib/commands/gestures.js @@ -1,8 +1,12 @@ import { util } from 'appium/support'; import { errors } from 'appium/driver'; -const commands = {}; - +/** + * + * @param {import('@appium/types').StringRecord} [options={}] + * @param {string[]} [keyNames] + * @returns {string|null} + */ function extractUuid (options = {}, keyNames = ['elementId', 'element']) { for (const name of keyNames) { if (options[name]) { @@ -15,6 +19,12 @@ function extractUuid (options = {}, keyNames = ['elementId', 'element']) { return null; } +/** + * + * @param {import('@appium/types').StringRecord} [options={}] + * @param {string[]} [keyNames] + * @returns {string|null} + */ function requireUuid (options = {}, keyNames = ['elementId', 'element']) { const result = extractUuid(options, keyNames); if (!result) { @@ -23,25 +33,13 @@ function requireUuid (options = {}, keyNames = ['elementId', 'element']) { return result; } - -/** - * @typedef {Object} SetValueOptions - * @property {string} elementId uuid of the element to set value for - * @property {any} value value to set. Could also be an array - * @property {string} text text to set. If both value and text are set - * then `value` is preferred - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while the element value is being set. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Set value to the given element * - * @param {SetValueOptions} opts + * @this {Mac2Driver} + * @param {import('../types').SetValueOptions} opts */ -commands.macosSetValue = async function macosSetValue (opts) { +export async function macosSetValue (opts) { const uuid = requireUuid(opts); const { value, text, keyModifierFlags } = opts ?? {}; return await this.wda.proxy.command(`/element/${uuid}/value`, 'POST', { @@ -50,28 +48,15 @@ commands.macosSetValue = async function macosSetValue (opts) { }); }; -/** - * @typedef {Object} ClickOptions - * @property {string} elementId uuid of the element to click. Either this property - * or/and x and y must be set. If both are set then x and y are considered as relative - * element coordinates. If only x and y are set then these are parsed as - * absolute coordinates. - * @property {number} x click X coordinate - * @property {number} y click Y coordinate - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while click is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform click gesture on an element or by relative/absolute coordinates * - * @param {ClickOptions} opts + * @this {Mac2Driver} + * @param {import('../types').ClickOptions} opts */ -commands.macosClick = async function macosClick (opts) { +export async function macosClick (opts = {}) { const uuid = extractUuid(opts); - const { x, y, keyModifierFlags } = opts ?? {}; + const { x, y, keyModifierFlags } = opts; const url = uuid ? `/element/${uuid}/click` : '/wda/click'; return await this.wda.proxy.command(url, 'POST', { x, y, @@ -79,34 +64,19 @@ commands.macosClick = async function macosClick (opts) { }); }; -/** - * @typedef {Object} ScrollOptions - * @property {string} elementId uuid of the element to be scrolled. Either this property - * or/and x and y must be set. If both are set then x and y are considered as relative - * element coordinates. If only x and y are set then these are parsed as - * absolute coordinates. - * @property {number} x scroll X coordinate - * @property {number} y scroll Y coordinate - * @property {number} deltaX horizontal delta as float number - * @property {number} deltaY vertical delta as float number - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while scroll is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform scroll gesture on an element or by relative/absolute coordinates * - * @param {ScrollOptions} opts + * @this {Mac2Driver} + * @param {import('../types').ScrollOptions} opts */ -commands.macosScroll = async function macosScroll (opts) { +export async function macosScroll (opts = {}) { const uuid = extractUuid(opts); const { x, y, deltaX, deltaY, keyModifierFlags, - } = opts ?? {}; + } = opts; const url = uuid ? `/wda/element/${uuid}/scroll` : '/wda/scroll'; return await this.wda.proxy.command(url, 'POST', { deltaX, deltaY, @@ -115,32 +85,13 @@ commands.macosScroll = async function macosScroll (opts) { }); }; -/** - * @typedef {Object} SwipeOptions - * @property {string} elementId uuid of the element to be swiped. Either this property - * or/and x and y must be set. If both are set then x and y are considered as relative - * element coordinates. If only x and y are set then these are parsed as - * absolute coordinates. - * @property {number} x swipe X coordinate - * @property {number} y swipe Y coordinate - * @property {string} direction either 'up', 'down', 'left' or 'right' - * @property {number} velocity The value is measured in pixels per second and same - * values could behave differently on different devices depending on their display - * density. Higher values make swipe gesture faster (which usually scrolls larger - * areas if we apply it to a list) and lower values slow it down. - * Only values greater than zero have effect. - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while scroll is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform swipe gesture on an element * - * @param {SwipeOptions} opts + * @this {Mac2Driver} + * @param {import('../types').SwipeOptions} opts */ -commands.macosSwipe = async function macosSwipe (opts) { +export async function macosSwipe (opts) { const uuid = extractUuid(opts); const { x, y, @@ -157,28 +108,15 @@ commands.macosSwipe = async function macosSwipe (opts) { }); }; -/** - * @typedef {Object} RightClickOptions - * @property {string} elementId uuid of the element to click. Either this property - * or/and x and y must be set. If both are set then x and y are considered as relative - * element coordinates. If only x and y are set then these are parsed as - * absolute coordinates. - * @property {number} x click X coordinate - * @property {number} y click Y coordinate - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while click is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform right click gesture on an element or by relative/absolute coordinates * - * @param {RightClickOptions} opts + * @this {Mac2Driver} + * @param {import('../types').RightClickOptions} opts */ -commands.macosRightClick = async function macosRightClick (opts) { +export async function macosRightClick (opts = {}) { const uuid = extractUuid(opts); - const { x, y, keyModifierFlags } = opts ?? {}; + const { x, y, keyModifierFlags } = opts; const url = uuid ? `/wda/element/${uuid}/rightClick` : '/wda/rightClick'; return await this.wda.proxy.command(url, 'POST', { x, y, @@ -186,28 +124,15 @@ commands.macosRightClick = async function macosRightClick (opts) { }); }; -/** - * @typedef {Object} HoverOptions - * @property {string} elementId uuid of the element to hover. Either this property - * or/and x and y must be set. If both are set then x and y are considered as relative - * element coordinates. If only x and y are set then these are parsed as - * absolute coordinates. - * @property {number} x click X coordinate - * @property {number} y click Y coordinate - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while hover is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform hover gesture on an element or by relative/absolute coordinates * - * @param {HoverOptions} opts + * @this {Mac2Driver} + * @param {import('../types').HoverOptions} opts */ -commands.macosHover = async function macosHover (opts) { +export async function macosHover (opts = {}) { const uuid = extractUuid(opts); - const { x, y, keyModifierFlags } = opts ?? {}; + const { x, y, keyModifierFlags } = opts; const url = uuid ? `/wda/element/${uuid}/hover` : '/wda/hover'; return await this.wda.proxy.command(url, 'POST', { x, y, @@ -215,28 +140,15 @@ commands.macosHover = async function macosHover (opts) { }); }; -/** - * @typedef {Object} DoubleClickOptions - * @property {string} elementId uuid of the element to double click. Either this property - * or/and x and y must be set. If both are set then x and y are considered as relative - * element coordinates. If only x and y are set then these are parsed as - * absolute coordinates. - * @property {number} x click X coordinate - * @property {number} y click Y coordinate - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while double click is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform double click gesture on an element or by relative/absolute coordinates * - * @param {DoubleClickOptions} opts + * @this {Mac2Driver} + * @param {import('../types').DoubleClickOptions} opts */ -commands.macosDoubleClick = async function macosDoubleClick (opts) { +export async function macosDoubleClick (opts = {}) { const uuid = extractUuid(opts); - const { x, y, keyModifierFlags } = opts ?? {}; + const { x, y, keyModifierFlags } = opts; const url = uuid ? `/wda/element/${uuid}/doubleClick` : '/wda/doubleClick'; return await this.wda.proxy.command(url, 'POST', { x, y, @@ -244,31 +156,13 @@ commands.macosDoubleClick = async function macosDoubleClick (opts) { }); }; -/** - * @typedef {Object} ClickAndDragOptions - * @property {string} sourceElementId uuid of the element to start the drag from. Either this property - * and `destinationElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates - * must be set. - * @property {string} destinationElementId uuid of the element to end the drag on. Either this property - * and `sourceElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates - * must be set. - * @property {number} startX starting X coordinate - * @property {number} startY starting Y coordinate - * @property {number} endX ending X coordinate - * @property {number} endY ending Y coordinate - * @property {number} duration long click duration in float seconds - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while drag is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform long click and drag gesture on an element or by absolute coordinates * - * @param {ClickAndDragOptions} opts + * @this {Mac2Driver} + * @param {import('../types').ClickAndDragOptions} opts */ -commands.macosClickAndDrag = async function macosClickAndDrag (opts) { +export async function macosClickAndDrag (opts) { const sourceUuid = extractUuid(opts, ['sourceElementId', 'sourceElement']); const destUuid = extractUuid(opts, ['destinationElementId', 'destinationElement']); const { @@ -290,36 +184,13 @@ commands.macosClickAndDrag = async function macosClickAndDrag (opts) { }); }; -/** - * @typedef {Object} ClickAndDragAndHoldOptions - * @property {string} sourceElementId uuid of the element to start the drag from. Either this property - * and `destinationElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates - * must be set. - * @property {string} destinationElementId uuid of the element to end the drag on. Either this property - * and `sourceElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates - * must be set. - * @property {number} startX starting X coordinate - * @property {number} startY starting Y coordinate - * @property {number} endX ending X coordinate - * @property {number} endY ending Y coordinate - * @property {number} duration long click duration in float seconds - * @property {number} holdDuration touch hold duration in float seconds - * @property {number} velocity dragging velocity in pixels per second. - * If not provided then the default velocity is used. See - * https://developer.apple.com/documentation/xctest/xcuigesturevelocity - * for more details - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while drag is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform long click, drag and hold gesture on an element or by absolute coordinates * - * @param {ClickAndDragAndHoldOptions} opts + * @this {Mac2Driver} + * @param {import('../types').ClickAndDragAndHoldOptions} opts */ -commands.macosClickAndDragAndHold = async function macosClickAndDragAndHold (opts) { +export async function macosClickAndDragAndHold (opts) { const sourceUuid = extractUuid(opts, ['sourceElementId', 'sourceElement']); const destUuid = extractUuid(opts, ['destinationElementId', 'destinationElement']); const { @@ -343,60 +214,26 @@ commands.macosClickAndDragAndHold = async function macosClickAndDragAndHold (opt }); }; -/** - * @typedef {Object} KeyOptions - * @property {string} key a string, that represents a key to type (see - * https://developer.apple.com/documentation/xctest/xcuielement/1500604-typekey?language=objc - * and https://developer.apple.com/documentation/xctest/xcuikeyboardkey?language=objc) - * @property {number} modifierFlags a set of modifier flags - * (https://developer.apple.com/documentation/xctest/xcuikeymodifierflags?language=objc) - * to use when typing the key. - */ - -/** - * @typedef {Object} KeysOptions - * @property {string} elementId uuid of the element to send keys to. - * If the element is not provided then the keys will be sent to the current application. - * @property {(KeyOptions|string)[]} keys Array of keys to type. - * Each item could either be a string, that represents a key itself (see - * https://developer.apple.com/documentation/xctest/xcuielement/1500604-typekey?language=objc - * and https://developer.apple.com/documentation/xctest/xcuikeyboardkey?language=objc) - * or a dictionary, if the key should also be entered with modifiers. - */ - /** * Send keys to the given element or to the application under test * - * @param {KeysOptions} opts + * @this {Mac2Driver} + * @param {import('../types').KeysOptions} opts */ -commands.macosKeys = async function macosKeys (opts) { +export async function macosKeys (opts) { const uuid = extractUuid(opts); const { keys } = opts ?? {}; const url = uuid ? `/wda/element/${uuid}/keys` : '/wda/keys'; return await this.wda.proxy.command(url, 'POST', { keys }); }; -/** - * @typedef {Object} PressOptions - * @property {string} elementId uuid of the Touch Bar element to be pressed. Either this property - * or/and x and y must be set. If both are set then x and y are considered as relative - * element coordinates. If only x and y are set then these are parsed as - * absolute Touch Bar coordinates. - * @property {number} x long click X coordinate - * @property {number} y long click Y coordinate - * @property {number} duration the number of float seconds to hold the mouse button - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while click is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform press gesture on a Touch Bar element or by relative/absolute coordinates * - * @param {PressOptions} opts + * @this {Mac2Driver} + * @param {import('../types').PressOptions} opts */ -commands.macosPressAndHold = async function macosPressAndHold (opts) { +export async function macosPressAndHold (opts) { const uuid = extractUuid(opts); const { x, y, duration, keyModifierFlags } = opts ?? {}; const url = uuid ? `/wda/element/${uuid}/press` : '/wda/press'; @@ -407,26 +244,13 @@ commands.macosPressAndHold = async function macosPressAndHold (opts) { }); }; -/** - * @typedef {Object} TapOptions - * @property {string} elementId uuid of the Touch Bar element to tap. Either this property - * or/and x and y must be set. If both are set then x and y are considered as relative - * element coordinates. If only x and y are set then these are parsed as - * absolute Touch Bar coordinates. - * @property {number} x click X coordinate - * @property {number} y click Y coordinate - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while click is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform tap gesture on a Touch Bar element or by relative/absolute coordinates * - * @param {TapOptions} opts + * @this {Mac2Driver} + * @param {import('../types').TapOptions} opts */ -commands.macosTap = async function macosTap (opts) { +export async function macosTap (opts = {}) { const uuid = extractUuid(opts); const { x, y, keyModifierFlags } = opts ?? {}; const url = uuid ? `/wda/element/${uuid}/tap` : '/wda/tap'; @@ -436,28 +260,15 @@ commands.macosTap = async function macosTap (opts) { }); }; -/** - * @typedef {Object} DoubleTapOptions - * @property {string} elementId uuid of the Touch Bar element to tap. Either this property - * or/and x and y must be set. If both are set then x and y are considered as relative - * element coordinates. If only x and y are set then these are parsed as - * absolute Touch Bar coordinates. - * @property {number} x click X coordinate - * @property {number} y click Y coordinate - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while click is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform tap gesture on a Touch Bar element or by relative/absolute coordinates * - * @param {DoubleTapOptions} opts + * @this {Mac2Driver} + * @param {import('../types').DoubleTapOptions} opts */ -commands.macosDoubleTap = async function macosDoubleTap (opts) { +export async function macosDoubleTap (opts = {}) { const uuid = extractUuid(opts); - const { x, y, keyModifierFlags } = opts ?? {}; + const { x, y, keyModifierFlags } = opts; const url = uuid ? `/wda/element/${uuid}/doubleTap` : '/wda/doubleTap'; return await this.wda.proxy.command(url, 'POST', { x, y, @@ -465,31 +276,13 @@ commands.macosDoubleTap = async function macosDoubleTap (opts) { }); }; -/** - * @typedef {Object} PressAndDragOptions - * @property {string} sourceElementId uuid of a Touch Bar element to start the drag from. Either this property - * and `destinationElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates - * must be set. - * @property {string} destinationElementId uuid of a Touch Bar element to end the drag on. Either this property - * and `sourceElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates - * must be set. - * @property {number} startX starting X coordinate - * @property {number} startY starting Y coordinate - * @property {number} endX ending X coordinate - * @property {number} endY ending Y coordinate - * @property {number} duration long click duration in float seconds - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while drag is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform long press and drag gesture on a Touch Bar element or by absolute coordinates * - * @param {PressAndDragOptions} opts + * @this {Mac2Driver} + * @param {import('../types').PressAndDragOptions} opts */ -commands.macosPressAndDrag = async function macosPressAndDrag (opts) { +export async function macosPressAndDrag (opts) { const sourceUuid = extractUuid(opts, ['sourceElementId', 'sourceElement']); const destUuid = extractUuid(opts, ['destinationElementId', 'destinationElement']); const { @@ -511,36 +304,13 @@ commands.macosPressAndDrag = async function macosPressAndDrag (opts) { }); }; -/** - * @typedef {Object} PressAndDragAndHoldOptions - * @property {string} sourceElementId uuid of a Touch Bar element to start the drag from. Either this property - * and `destinationElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates - * must be set. - * @property {string} destinationElementId uuid of a Touch Bar element to end the drag on. Either this property - * and `sourceElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates - * must be set. - * @property {number} startX starting X coordinate - * @property {number} startY starting Y coordinate - * @property {number} endX ending X coordinate - * @property {number} endY ending Y coordinate - * @property {number} duration long click duration in float seconds - * @property {number} holdDuration touch hold duration in float seconds - * @property {number} velocity dragging velocity in pixels per second. - * If not provided then the default velocity is used. See - * https://developer.apple.com/documentation/xctest/xcuigesturevelocity - * for more details - * @property {number} keyModifierFlags if set then the given key modifiers will be - * applied while drag is performed. See - * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags - * for more details - */ - /** * Perform press, drag and hold gesture on a Touch Bar element or by absolute Touch Bar coordinates * - * @param {PressAndDragAndHoldOptions} opts + * @this {Mac2Driver} + * @param {import('../types').PressAndDragAndHoldOptions} opts */ -commands.macosPressAndDragAndHold = async function macosPressAndDragAndHold (opts) { +export async function macosPressAndDragAndHold (opts) { const sourceUuid = extractUuid(opts, ['sourceElementId', 'sourceElement']); const destUuid = extractUuid(opts, ['destinationElementId', 'destinationElement']); const { @@ -564,4 +334,6 @@ commands.macosPressAndDragAndHold = async function macosPressAndDragAndHold (opt }); }; -export default commands; +/** + * @typedef {import('../driver').Mac2Driver} Mac2Driver + */ diff --git a/lib/commands/index.js b/lib/commands/index.js deleted file mode 100644 index 940452d..0000000 --- a/lib/commands/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import findCmds from './find'; -import executeCmds from './execute'; -import gestureCmds from './gestures'; -import sourceCmds from './source'; -import appManagementCmds from './app-management'; -import appleScriptCmds from './applescript'; -import screenRecordingCmds from './record-screen'; -import screenshotsCmds from './screenshots'; -import navigationCmds from './navigation'; - -const commands = {}; -Object.assign( - commands, - findCmds, - executeCmds, - gestureCmds, - sourceCmds, - appManagementCmds, - appleScriptCmds, - screenRecordingCmds, - screenshotsCmds, - navigationCmds, - // add other command types here -); - -export { commands }; -export default commands; diff --git a/lib/commands/navigation.js b/lib/commands/navigation.js index ab94614..182e252 100644 --- a/lib/commands/navigation.js +++ b/lib/commands/navigation.js @@ -1,23 +1,16 @@ -const commands = {}; - -/** - * @typedef {Object} DeepLinkOpts - * @property {string} url The URL to be opened. This parameter is manadatory. - * @property {string} [bundleId] The bundle identifier of an application to open the given url with. - * If not provided then the default application for the given url scheme is going to be used. - */ - /** * Opens the given URL with the default or the given application. * Xcode must be at version 14.3+. * - * @param {DeepLinkOpts} opts - * @returns {Promise} + * @this {Mac2Driver} + * @param {import('../types').DeepLinkOptions} opts + * @returns {Promise} */ -commands.macosDeepLink = async function macosDeepLink (opts) { +export async function macosDeepLink (opts) { const {url, bundleId} = opts; return await this.wda.proxy.command('/url', 'POST', {url, bundleId}); }; -export { commands }; -export default commands; +/** + * @typedef {import('../driver').Mac2Driver} Mac2Driver + */ diff --git a/lib/commands/record-screen.js b/lib/commands/record-screen.js index 477c812..6d85e02 100644 --- a/lib/commands/record-screen.js +++ b/lib/commands/record-screen.js @@ -6,8 +6,6 @@ import { SubProcess } from 'teen_process'; import B from 'bluebird'; -const commands = {}; - const RETRY_PAUSE = 300; const RETRY_TIMEOUT = 5000; const DEFAULT_TIME_LIMIT = 60 * 10; // 10 minutes @@ -21,7 +19,7 @@ const DEFAULT_PRESET = 'veryfast'; * * @param {string} localFile * @param {string?} remotePath - * @param {any} uploadOptions + * @param {import('@appium/types').StringRecord} [uploadOptions={}] * @returns */ async function uploadRecordedMedia (localFile, remotePath = null, uploadOptions = {}) { @@ -188,43 +186,6 @@ class ScreenRecorder { } } - -/** - * @typedef {Object} StartRecordingOptions - * - * @property {?string} videoFilter - The video filter spec to apply for ffmpeg. - * See https://trac.ffmpeg.org/wiki/FilteringGuide for more details on the possible values. - * Example: Set it to `scale=ifnot(gte(iw\,1024)\,iw\,1024):-2` in order to limit the video width - * to 1024px. The height will be adjusted automatically to match the actual ratio. - * @property {number|string} fps [15] - The count of frames per second in the resulting video. - * The greater fps it has the bigger file size is. - * @property {string} preset [veryfast] - One of the supported encoding presets. Possible values are: - * - ultrafast - * - superfast - * - veryfast - * - faster - * - fast - * - medium - * - slow - * - slower - * - veryslow - * A preset is a collection of options that will provide a certain encoding speed to compression ratio. - * A slower preset will provide better compression (compression is quality per filesize). - * This means that, for example, if you target a certain file size or constant bit rate, you will achieve better - * quality with a slower preset. Read https://trac.ffmpeg.org/wiki/Encode/H.264 for more details. - * @property {boolean} captureCursor [false] - Whether to capture the mouse cursor while recording - * the screen - * @property {boolean} captureClicks [false] - Whether to capture mouse clicks while recording the - * screen - * @property {!string|number} deviceId - Screen device index to use for the recording. - * The list of available devices could be retrieved using - * `ffmpeg -f avfoundation -list_devices true -i` command. - * @property {string|number} timeLimit [600] - The maximum recording time, in seconds. The default - * value is 600 seconds (10 minutes). - * @property {boolean} forceRestart [true] - Whether to ignore the call if a screen recording is currently running - * (`false`) or to start a new recording immediately and terminate the existing one if running (`true`). - */ - /** * Record the display in background while the automated test is running. * This method requires FFMPEG (https://www.ffmpeg.org/download.html) to be installed @@ -232,11 +193,11 @@ class ScreenRecorder { * in System Preferences->Security & Privacy->Screen Recording. * The resulting video uses H264 codec and is ready to be played by media players built-in into web browsers. * - * @param {StartRecordingOptions} options - The available options. - * @this {import('../driver').Mac2Driver} + * @param {import('../types').StartRecordingOptions} options - The available options. + * @this {Mac2Driver} * @throws {Error} If screen recording has failed to start or is not supported on the device under test. */ -commands.startRecordingScreen = async function startRecordingScreen (options) { +export async function startRecordingScreen (options) { const { timeLimit, videoFilter, @@ -285,37 +246,19 @@ commands.startRecordingScreen = async function startRecordingScreen (options) { } }; -/** - * @typedef {Object} StopRecordingOptions - * - * @property {string} remotePath - The path to the remote location, where the resulting video should be uploaded. - * The following protocols are supported: http/https, ftp. - * Null or empty string value (the default setting) means the content of resulting - * file should be encoded as Base64 and passed as the endpoint response value. - * An exception will be thrown if the generated media file is too big to - * fit into the available process memory. - * @property {string} user - The name of the user for the remote authentication. - * @property {string} pass - The password for the remote authentication. - * @property {string} method - The http multipart upload method name. The 'PUT' one is used by default. - * @property {Object} headers - Additional headers mapping for multipart http(s) uploads - * @property {string} fileFieldName [file] - The name of the form field, where the file content BLOB should be stored for - * http(s) uploads - * @property {Object|[string, string][]} formFields - Additional form fields for multipart http(s) uploads - */ - /** * Stop recording the screen. * If no screen recording has been started before then the method returns an empty string. * - * @param {StopRecordingOptions} options - The available options. + * @param {import('../types').StopRecordingOptions} [options={}] - The available options. * @returns {Promise} Base64-encoded content of the recorded media file if 'remotePath' * parameter is falsy or an empty string. - * @this {import('../driver').Mac2Driver} + * @this {Mac2Driver} * @throws {Error} If there was an error while getting the name of a media file * or the file content cannot be uploaded to the remote location * or screen recording is not supported on the device under test. */ -commands.stopRecordingScreen = async function stopRecordingScreen (options) { +export async function stopRecordingScreen (options = {}) { if (!this._screenRecorder) { log.debug('No screen recording has been started. Doing nothing'); return ''; @@ -330,4 +273,6 @@ commands.stopRecordingScreen = async function stopRecordingScreen (options) { return await uploadRecordedMedia(videoPath, options.remotePath, options); }; -export default commands; +/** + * @typedef {import('../driver').Mac2Driver} Mac2Driver + */ diff --git a/lib/commands/screenshots.js b/lib/commands/screenshots.js index d8c4ecd..537f5f3 100644 --- a/lib/commands/screenshots.js +++ b/lib/commands/screenshots.js @@ -1,32 +1,17 @@ -const commands = {}; - -/** - * @typedef {Object} ScreenshotsInfo - * - * A dictionary where each key contains a unique display identifier - * and values are dictionaries with following items: - * - id: Display identifier - * - isMain: Whether this display is the main one - * - payload: The actual PNG screenshot data encoded to base64 string - */ - -/** - * @typedef {Object} ScreenshotsOpts - * @property {number} displayId macOS display identifier to take a screenshot for. - * If not provided then screenshots of all displays are going to be returned. - * If no matches were found then an error is thrown. - */ - /** * Retrieves screenshots of each display available to macOS * - * @param {ScreenshotsOpts} opts - * @returns {Promise} + * @this {Mac2Driver} + * @param {import('../types').ScreenshotsOpts} [opts={}] + * @returns {Promise} */ -commands.macosScreenshots = async function macosScreenshots (opts) { - const {displayId} = opts ?? {}; - return await this.wda.proxy.command('/wda/screenshots', 'POST', {displayId}); +export async function macosScreenshots (opts = {}) { + const {displayId} = opts; + return /** @type {import('../types').ScreenshotsInfo} */ ( + await this.wda.proxy.command('/wda/screenshots', 'POST', {displayId}) + ); }; -export { commands }; -export default commands; +/** + * @typedef {import('../driver').Mac2Driver} Mac2Driver + */ diff --git a/lib/commands/source.js b/lib/commands/source.js index 270f4ec..b8dc622 100644 --- a/lib/commands/source.js +++ b/lib/commands/source.js @@ -1,26 +1,19 @@ -const commands = {}; - -/** - * @typedef {Object} SourceOptions - * @property {string} format [xml] The format of the application source to retrieve. - * Only two formats are supported: - * - xml: Returns the source formatted as XML document (the default setting) - * - description: Returns the source formatted as debugDescription output. - * See https://developer.apple.com/documentation/xctest/xcuielement/1500909-debugdescription?language=objc - * for more details. - */ - /** * Retrieves the string representation of the current application * - * @param {SourceOptions} opts + * @this {Mac2Driver} + * @param {import('../types').SourceOptions} [opts={}] * @returns {Promise} the page source in the requested format */ -commands.macosSource = async function macosSource (opts) { +export async function macosSource (opts = {}) { const { format = 'xml', - } = opts ?? {}; - return await this.wda.proxy.command(`/source?format=${encodeURIComponent(format)}`, 'GET'); + } = opts; + return /** @type {String} */ ( + await this.wda.proxy.command(`/source?format=${encodeURIComponent(format)}`, 'GET') + ); }; -export default commands; +/** + * @typedef {import('../driver').Mac2Driver} Mac2Driver + */ diff --git a/lib/driver.js b/lib/driver.js index caa602c..73f0c11 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -2,7 +2,15 @@ import _ from 'lodash'; import { BaseDriver, DeviceSettings } from 'appium/driver'; import WDA_MAC_SERVER from './wda-mac'; import { desiredCapConstraints } from './desired-caps'; -import commands from './commands/index'; +import * as appManagemenetCommands from './commands/app-management'; +import * as appleScriptCommands from './commands/applescript'; +import * as executeCommands from './commands/execute'; +import * as findCommands from './commands/find'; +import * as gesturesCommands from './commands/gestures'; +import * as navigationCommands from './commands/navigation'; +import * as recordScreenCommands from './commands/record-screen'; +import * as screenshotCommands from './commands/screenshots'; +import * as sourceCommands from './commands/source'; import log from './logger'; import { newMethodMap } from './method-map'; @@ -22,8 +30,8 @@ export class Mac2Driver extends BaseDriver { /** @type {boolean} */ isProxyActive; - /** @type {(toRun: {command?: string; script?: string}) => Promise} */ - macosExecAppleScript; + /** @type {import('./wda-mac').WDAMacServer} */ + wda; static newMethodMap = newMethodMap; @@ -48,19 +56,16 @@ export class Mac2Driver extends BaseDriver { ]; this.resetState(); this.settings = new DeviceSettings({}, this.onSettingsUpdate.bind(this)); - - for (const [cmd, fn] of _.toPairs(commands)) { - Mac2Driver.prototype[cmd] = fn; - } } async onSettingsUpdate (key, value) { - return await this.wda?.proxy?.command('/appium/settings', 'POST', { + return await this.wda.proxy.command('/appium/settings', 'POST', { settings: {[key]: value} }); } resetState () { + // @ts-ignore This is ok this.wda = null; this.proxyReqRes = null; this.isProxyActive = false; @@ -82,17 +87,17 @@ export class Mac2Driver extends BaseDriver { } async proxyCommand (url, method, body = null) { - return await this.wda?.proxy?.command(url, method, body); + return await this.wda.proxy.command(url, method, body); } // @ts-ignore We know WDA response should be ok async getStatus () { - return await this.wda?.proxy?.command('/status', 'GET'); + return await this.wda.proxy.command('/status', 'GET'); } // needed to make image plugin work async getWindowRect () { - return await this.wda?.proxy?.command('/window/rect', 'GET'); + return await this.wda.proxy.command('/window/rect', 'GET'); } // @ts-ignore TODO: make args typed @@ -117,14 +122,14 @@ export class Mac2Driver extends BaseDriver { await this.deleteSession(); throw e; } - this.proxyReqRes = this.wda?.proxy?.proxyReqRes.bind(this.wda.proxy); + this.proxyReqRes = this.wda.proxy.proxyReqRes.bind(this.wda.proxy); this.isProxyActive = true; return [sessionId, caps]; } async deleteSession () { await this._screenRecorder?.stop(true); - await this.wda?.stopSession(); + await this.wda.stopSession(); if (this.opts.postrun) { if (!_.isString(this.opts.postrun.command) && !_.isString(this.opts.postrun.script)) { @@ -147,6 +152,43 @@ export class Mac2Driver extends BaseDriver { await super.deleteSession(); } + + macosLaunchApp = appManagemenetCommands.macosLaunchApp; + macosActivateApp = appManagemenetCommands.macosActivateApp; + macosTerminateApp = appManagemenetCommands.macosTerminateApp; + macosQueryAppState = appManagemenetCommands.macosQueryAppState; + + macosExecAppleScript = appleScriptCommands.macosExecAppleScript; + + executeMacosCommand = executeCommands.executeMacosCommand; + execute = executeCommands.execute; + + findElOrEls = findCommands.findElOrEls; + + macosSetValue = gesturesCommands.macosSetValue; + macosClick = gesturesCommands.macosClick; + macosScroll = gesturesCommands.macosScroll; + macosSwipe = gesturesCommands.macosSwipe; + macosRightClick = gesturesCommands.macosRightClick; + macosHover = gesturesCommands.macosHover; + macosDoubleClick = gesturesCommands.macosDoubleClick; + macosClickAndDrag = gesturesCommands.macosClickAndDrag; + macosClickAndDragAndHold = gesturesCommands.macosClickAndDragAndHold; + macosKeys = gesturesCommands.macosKeys; + macosPressAndHold = gesturesCommands.macosPressAndHold; + macosTap = gesturesCommands.macosTap; + macosDoubleTap = gesturesCommands.macosDoubleTap; + macosPressAndDrag = gesturesCommands.macosPressAndDrag; + macosPressAndDragAndHold = gesturesCommands.macosPressAndDragAndHold; + + macosDeepLink = navigationCommands.macosDeepLink; + + startRecordingScreen = recordScreenCommands.startRecordingScreen; + stopRecordingScreen = recordScreenCommands.stopRecordingScreen; + + macosScreenshots = screenshotCommands.macosScreenshots; + + macosSource = sourceCommands.macosSource; } export default Mac2Driver; diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..3a4f338 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,688 @@ +import type { StringRecord } from '@appium/types'; + +export interface LaunchAppOptions { + /** + * Bundle identifier of the app to be launched + * or activated. Either this property or `path` must be provided + */ + bundleId?: string; + /** + * Full path to the app bundle. Either this property + * or `bundleId` must be provided + */ + path?: string; + /** + * The list of command line arguments + * for the app to be be launched with. This parameter is ignored if the app + * is already running. + */ + arguments?: string[]; + /** + * Environment variables mapping. Custom + * variables are added to the default process environment. + */ + environment?: StringRecord; +} + +export interface ActivateAppOptions { + /** + * Bundle identifier of the app to be activated. + * Either this property or `path` must be provided + */ + bundleId?: string; + /** + * Full path to the app bundle. Either this property + * or `bundleId` must be provided + */ + path?: string; +} + +export interface TerminateAppOptions { + /** + * Bundle identifier of the app to be terminated. + * Either this property or `path` must be provided + */ + bundleId?: string; + /** + * Full path to the app bundle. Either this property + * or `bundleId` must be provided + */ + path?: string; +} + +export interface QueryAppStateOptions { + /** + * Bundle identifier of the app whose state should be queried. + * Either this property or `path` must be provided + */ + bundleId?: string; + /** + * Full path to the app bundle. Either this property + * or `bundleId` must be provided + */ + path?: string; +} + +export interface ExecAppleScriptOptions { + /** + * A valid AppleScript to execute + */ + script?: string; + /** + * Overrides the scripting language. Basically, sets the value of `-l` command + * line argument of `osascript` tool. If unset the AppleScript language is assumed. + */ + language?: string; + /** + * A valid AppleScript as a single command (no line breaks) to execute + */ + command?: string; + /** + * [20000] The number of seconds to wait until a long-running command is + * finished. An error is thrown if the command is still running after this timeout expires. + */ + timeout?: number; + /** + * The path to an existing folder, which is going to be set as the + * working directory for the command/script being executed. + */ + cwd?: string; +} + +export interface SetValueOptions { + /** + * Uuid of the element to set value for + */ + elementId: string; + /** + * Value to set. Could also be an array + */ + value?: any; + /** + * Text to set. If both value and text are set + * then `value` is preferred + */ + text?: string; + /** + * If set then the given key modifiers will be + * applied while the element value is being set. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface ClickOptions { + /** + * Uuid of the element to click. Either this property + * or/and x and y must be set. If both are set then x and y are considered as relative + * element coordinates. If only x and y are set then these are parsed as + * absolute coordinates. + */ + elementId?: string; + /** + * Click X coordinate + */ + x?: number; + /** + * Click Y coordinate + */ + y?: number; + /** + * If set then the given key modifiers will be + * applied while click is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface ScrollOptions { + /** + * Uuid of the element to be scrolled. Either this property + * or/and x and y must be set. If both are set then x and y are considered as relative + * element coordinates. If only x and y are set then these are parsed as + * absolute coordinates. + */ + elementId?: string; + /** + * Scroll X coordinate + */ + x?: number; + /** + * Scroll Y coordinate + */ + y?: number; + /** + * Horizontal delta as float number + */ + deltaX?: number; + /** + * Vertical delta as float number + */ + deltaY?: number; + /** + * If set then the given key modifiers will be + * applied while scroll is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface SwipeOptions { + /** + * Uuid of the element to be swiped. Either this property + * or/and x and y must be set. If both are set then x and y are considered as relative + * element coordinates. If only x and y are set then these are parsed as + * absolute coordinates. + */ + elementId?: string; + /** + * Swipe X coordinate + */ + x?: number; + /** + * Swipe Y coordinate + */ + y?: number; + /** + * Swipe direction + */ + direction: 'up'|'down'|'left'|'right'; + /** + * The value is measured in pixels per second and same + * values could behave differently on different devices depending on their display + * density. Higher values make swipe gesture faster (which usually scrolls larger + * areas if we apply it to a list) and lower values slow it down. + * Only values greater than zero have effect. + */ + velocity?: number; + /** + * If set then the given key modifiers will be + * applied while scroll is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface RightClickOptions { + /** + * Uuid of the element to click. Either this property + * or/and x and y must be set. If both are set then x and y are considered as relative + * element coordinates. If only x and y are set then these are parsed as + * absolute coordinates. + */ + elementId?: string; + /** + * Click X coordinate + */ + x?: number; + /** + * Click Y coordinate + */ + y?: number; + /** + * If set then the given key modifiers will be + * applied while click is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface HoverOptions { + /** + * Uuid of the element to hover. Either this property + * or/and x and y must be set. If both are set then x and y are considered as relative + * element coordinates. If only x and y are set then these are parsed as + * absolute coordinates. + */ + elementId?: string; + /** + * Hover X coordinate + */ + x?: number; + /** + * Hover Y coordinate + */ + y?: number; + /** + * If set then the given key modifiers will be + * applied while hover is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface DoubleClickOptions { + /** + * Uuid of the element to double click. Either this property + * or/and x and y must be set. If both are set then x and y are considered as relative + * element coordinates. If only x and y are set then these are parsed as + * absolute coordinates. + */ + elementId?: string; + /** + * Click X coordinate + */ + x?: number; + /** + * Click Y coordinate + */ + y?: number; + /** + * If set then the given key modifiers will be + * applied while double click is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface ClickAndDragOptions { + /** + * Uuid of the element to start the drag from. Either this property + * and `destinationElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates + * must be set. + */ + sourceElementId?: string; + /** + * Uuid of the element to end the drag on. Either this property + * and `sourceElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates + * must be set. + */ + destinationElementId?: string; + /** + * Starting X coordinate + */ + startX?: number; + /** + * Starting Y coordinate + */ + startY?: number; + /** + * Ending X coordinate + */ + endX?: number; + /** + * Ending Y coordinate + */ + endY?: number; + /** + * Long click duration in float seconds + */ + duration: number; + /** + * If set then the given key modifiers will be + * applied while drag is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface ClickAndDragAndHoldOptions { + /** + * Uuid of the element to start the drag from. Either this property + * and `destinationElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates + * must be set. + */ + sourceElementId?: string; + /** + * Uuid of the element to end the drag on. Either this property + * and `sourceElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates + * must be set. + */ + destinationElementId?: string; + /** + * Starting X coordinate + */ + startX?: number; + /** + * Starting Y coordinate + */ + startY?: number; + /** + * Ending X coordinate + */ + endX?: number; + /** + * Ending Y coordinate + */ + endY?: number; + /** + * Long click duration in float seconds + */ + duration: number; + /** + * Touch hold duration in float seconds + */ + holdDuration: number; + /** + * Dragging velocity in pixels per second. + * If not provided then the default velocity is used. See + * https://developer.apple.com/documentation/xctest/xcuigesturevelocity + * for more details + */ + velocity?: number; + /** + * If set then the given key modifiers will be + * applied while drag is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface KeyOptions { + /** + * A string, that represents a key to type (see + * https://developer.apple.com/documentation/xctest/xcuielement/1500604-typekey?language=objc + * and https://developer.apple.com/documentation/xctest/xcuikeyboardkey?language=objc) + */ + key: string; + /** + * A set of modifier flags + * (https://developer.apple.com/documentation/xctest/xcuikeymodifierflags?language=objc) + * to use when typing the key. + */ + modifierFlags?: number; +} + +export interface KeysOptions { + /** + * Uuid of the element to send keys to. + * If the element is not provided then the keys will be sent to the current application. + */ + elementId?: string; + /** + * Array of keys to type. + * Each item could either be a string, that represents a key itself (see + * https://developer.apple.com/documentation/xctest/xcuielement/1500604-typekey?language=objc + * and https://developer.apple.com/documentation/xctest/xcuikeyboardkey?language=objc) + * or a dictionary, if the key should also be entered with modifiers. + */ + keys: (KeyOptions | string)[]; +} + +export interface PressOptions { + /** + * Uuid of the Touch Bar element to be pressed. Either this property + * or/and x and y must be set. If both are set then x and y are considered as relative + * element coordinates. If only x and y are set then these are parsed as + * absolute Touch Bar coordinates. + */ + elementId?: string; + /** Press X coordinate */ + x?: number; + /** Press Y coordinate */ + y?: number; + /** + * The number of float seconds to hold the mouse button + */ + duration: number; + /** + * If set then the given key modifiers will be + * applied while click is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface TapOptions { + /** + * Uuid of the Touch Bar element to tap. Either this property + * or/and x and y must be set. If both are set then x and y are considered as relative + * element coordinates. If only x and y are set then these are parsed as + * absolute Touch Bar coordinates. + */ + elementId?: string; + /** + * Tap X coordinate + */ + x?: number; + /** + * Tap Y coordinate + */ + y?: number; + /** + * If set then the given key modifiers will be + * applied while click is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface DoubleTapOptions { + /** + * Uuid of the Touch Bar element to tap. Either this property + * or/and x and y must be set. If both are set then x and y are considered as relative + * element coordinates. If only x and y are set then these are parsed as + * absolute Touch Bar coordinates. + */ + elementId?: string; + /** + * Tap X coordinate + */ + x?: number; + /** + * Tap Y coordinate + */ + y?: number; + /** + * If set then the given key modifiers will be + * applied while click is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface PressAndDragOptions { + /** + * Uuid of a Touch Bar element to start the drag from. Either this property + * and `destinationElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates + * must be set. + */ + sourceElementId?: string; + /** + * Uuid of a Touch Bar element to end the drag on. Either this property + * and `sourceElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates + * must be set. + */ + destinationElementId?: string; + /** Starting X coordinate */ + startX?: number; + /** Starting Y coordinate */ + startY?: number; + /** Ending X coordinate */ + endX?: number; + /** Ending Y coordinate */ + endY?: number; + /** Long press duration in float seconds */ + duration: number; + /** + * If set then the given key modifiers will be + * applied while drag is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface PressAndDragAndHoldOptions { + /** + * Uuid of a Touch Bar element to start the drag from. Either this property + * and `destinationElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates + * must be set. + */ + sourceElementId?: string; + /** + * Uuid of a Touch Bar element to end the drag on. Either this property + * and `sourceElement` must be provided or `startX`, `startY`, `endX`, `endY` coordinates + * must be set. + */ + destinationElementId?: string; + /** Starting X coordinate */ + startX?: number; + /** Starting Y coordinate */ + startY?: number; + /** Ending X coordinate */ + endX?: number; + /** Ending Y coordinate */ + endY?: number; + /** Long press duration in float seconds */ + duration: number; + /** Touch hold duration in float seconds */ + holdDuration: number; + /** + * Dragging velocity in pixels per second. + * If not provided then the default velocity is used. See + * https://developer.apple.com/documentation/xctest/xcuigesturevelocity + * for more details + */ + velocity?: number; + /** + * If set then the given key modifiers will be + * applied while drag is performed. See + * https://developer.apple.com/documentation/xctest/xcuikeymodifierflags + * for more details + */ + keyModifierFlags?: number; +} + +export interface DeepLinkOptions { + /** The URL to be opened. This parameter is manadatory. */ + url: string; + /** + * The bundle identifier of an application to open the given url with. + * If not provided then the default application for the given url scheme is going to be used. + */ + bundleId?: string; +} + +export interface StopRecordingOptions { + /** + * The path to the remote location, where the resulting video should be uploaded. + * The following protocols are supported: http/https, ftp. + * Null or empty string value (the default setting) means the content of resulting + * file should be encoded as Base64 and passed as the endpoint response value. + * An exception will be thrown if the generated media file is too big to + * fit into the available process memory. + */ + remotePath?: string; + /** + * The name of the user for the remote authentication. + */ + user?: string; + /** + * The password for the remote authentication. + */ + pass?: string; + /** + * The http multipart upload method name. The 'PUT' one is used by default. + */ + method?: string; + /** + * Additional headers mapping for multipart http(s) uploads + */ + headers?: StringRecord | [string, any][]; + /** + * [file] The name of the form field, where the file content BLOB should be stored for + * http(s) uploads + */ + fileFieldName?: string; + /** + * Additional form fields for multipart http(s) uploads + */ + formFields?: StringRecord | [string, string][]; +} + +export interface StartRecordingOptions { + /** + * The video filter spec to apply for ffmpeg. + * See https://trac.ffmpeg.org/wiki/FilteringGuide for more details on the possible values. + * Example: Set it to `scale=ifnot(gte(iw\,1024)\,iw\,1024):-2` in order to limit the video width + * to 1024px. The height will be adjusted automatically to match the actual ratio. + */ + videoFilter?: string; + /** + * The count of frames per second in the resulting video. + * The greater fps it has the bigger file size is. 15 by default. + */ + fps?: number | string; + /** + * One of the supported encoding presets. A preset is a collection of options that will provide a + * certain encoding speed to compression ratio. + * A slower preset will provide better compression (compression is quality per filesize). + * This means that, for example, if you target a certain file size or constant bit rate, you will achieve better + * quality with a slower preset. Read https://trac.ffmpeg.org/wiki/Encode/H.264 for more details. + * `veryfast` by default + */ + preset?: 'ultrafast'|'superfast'|'veryfast'|'faster'|'fast'|'medium'|'slow'|'slower'|'veryslow'; + /** + * Whether to capture the mouse cursor while recording + * the screen. false by default + */ + captureCursor?: boolean; + /** + * Whether to capture mouse clicks while recording the + * screen. False by default. + */ + captureClicks?: boolean; + /** + * Screen device index to use for the recording. + * The list of available devices could be retrieved using + * `ffmpeg -f avfoundation -list_devices true -i` command. + */ + deviceId: string | number; + /** + * The maximum recording time, in seconds. The default + * value is 600 seconds (10 minutes). + */ + timeLimit?: string | number; + /** + * Whether to ignore the call if a screen recording is currently running + * (`false`) or to start a new recording immediately and terminate the existing one if running (`true`). + * The default value is `true`. + */ + forceRestart?: boolean; +} + +export interface ScreenshotInfo { + /** Display identifier */ + id: number; + /** Whether this display is the main one */ + isMain: boolean; + /** The actual PNG screenshot data encoded to base64 string */ + payload: string; +} + +/** A dictionary where each key contains a unique display identifier */ +export type ScreenshotsInfo = StringRecord; + +export interface ScreenshotsOpts { + /** + * macOS display identifier to take a screenshot for. + * If not provided then screenshots of all displays are going to be returned. + * If no matches were found then an error is thrown. + */ + displayId?: number; +} + +export interface SourceOptions { + /** + * The format of the application source to retrieve. + * Only two formats are supported: + * - xml: Returns the source formatted as XML document (the default setting) + * - description: Returns the source formatted as debugDescription output. + * See https://developer.apple.com/documentation/xctest/xcuielement/1500909-debugdescription?language=objc + * for more details. + */ + format?: 'xml'|'description'; +} diff --git a/lib/wda-mac.js b/lib/wda-mac.js index 4c7a0b6..6de7187 100644 --- a/lib/wda-mac.js +++ b/lib/wda-mac.js @@ -50,7 +50,7 @@ process.once('exit', () => { }); -class WDAMacProxy extends JWProxy { +export class WDAMacProxy extends JWProxy { /** @type {boolean|undefined} */ didProcessExit; @@ -293,10 +293,14 @@ class WDAMacProcess { } } -class WDAMacServer { +export class WDAMacServer { + /** @type {WDAMacProxy} */ + proxy; + constructor () { this.process = null; this.serverStartupTimeoutMs = STARTUP_TIMEOUT_MS; + // @ts-ignore this is ok this.proxy = null; // To handle if the WDAMac server is proxying requests to a remote WDAMac app instance