From a3c08496cd3a5928fbdcd2aec824b966a4562b35 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 1 Feb 2025 22:02:08 +0100 Subject: [PATCH] feat: Add support of executeMethodMap (#290) BREAKING CHANGE: The following method arguments have been **modified**: - startRecordingScreen - stopRecordingScreen - windowsDeleteFile - windowsDeleteFolder - windowsClick - windowsScroll - windowsClickAndDrag - windowsHover - windowsKeys - windowsSetClipboard - windowsGetClipboard --- lib/commands/app-management.js | 26 ++- lib/commands/clipboard.js | 48 ++--- lib/commands/execute.js | 77 ++++--- lib/commands/file-movement.js | 94 +++++--- lib/commands/find.js | 26 ++- lib/commands/general.js | 95 ++++++--- lib/commands/gestures.js | 212 ++++++++----------- lib/commands/index.js | 30 --- lib/commands/powershell.js | 16 +- lib/commands/record-screen.js | 112 +++++----- lib/commands/touch.js | 20 +- lib/driver.js | 66 +++++- lib/execute-method-map.ts | 144 +++++++++++++ lib/utils.js | 19 -- test/e2e/commands/file-movement-e2e-specs.js | 18 +- test/e2e/commands/winapi-e2e-specs.js | 46 ++-- test/e2e/constants.js | 2 - test/e2e/driver-e2e-specs.js | 18 +- test/e2e/helpers.js | 21 ++ 19 files changed, 638 insertions(+), 452 deletions(-) delete mode 100644 lib/commands/index.js create mode 100644 lib/execute-method-map.ts delete mode 100644 test/e2e/constants.js create mode 100644 test/e2e/helpers.js diff --git a/lib/commands/app-management.js b/lib/commands/app-management.js index 38b87e4..b6715b9 100644 --- a/lib/commands/app-management.js +++ b/lib/commands/app-management.js @@ -1,5 +1,3 @@ -const commands = {}; - // https://github.com/microsoft/WinAppDriver/blob/master/Docs/SupportedAPIs.md /** @@ -11,10 +9,14 @@ const commands = {}; * for more examples. * It is possible to open another window of the same app and then switch between * windows using https://www.selenium.dev/documentation/webdriver/interactions/windows/ API + * + * @this {WindowsDriver} */ -commands.windowsLaunchApp = async function windowsLaunchApp (/* opts = {} */) { - return await this.winAppDriver.sendCommand('/appium/app/launch', 'POST', {}); -}; +export async function windowsLaunchApp () { + return await this.winAppDriver.sendCommand( + '/appium/app/launch', 'POST', {} + ); +} /** * Close the active window of the app under test. Check @@ -25,11 +27,15 @@ commands.windowsLaunchApp = async function windowsLaunchApp (/* opts = {} */) { * After the current app window is closed it is required to use the above API to switch to another * active window if there is any; this API does not perform the switch automatically. * + * @this {WindowsDriver} * @throws {Error} if the app process is not running */ -commands.windowsCloseApp = async function windowsCloseApp (/* opts = {} */) { - return await this.winAppDriver.sendCommand('/appium/app/close', 'POST', {}); -}; +export async function windowsCloseApp () { + return await this.winAppDriver.sendCommand( + '/appium/app/close', 'POST', {} + ); +} -export { commands }; -export default commands; +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + */ diff --git a/lib/commands/clipboard.js b/lib/commands/clipboard.js index 34e28fa..b9e6cae 100644 --- a/lib/commands/clipboard.js +++ b/lib/commands/clipboard.js @@ -1,10 +1,7 @@ import { exec } from 'teen_process'; import { errors } from 'appium/driver'; -import { requireArgs } from '../utils'; import _ from 'lodash'; -const commands = {}; - /** * @typedef {'plaintext' | 'image'} ContentTypeEnum */ @@ -19,22 +16,17 @@ const CONTENT_TYPE = Object.freeze({ // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-clipboard?view=powershell-7.3 -/** - * @typedef {Object} SetClipboardOptions - * @property {string} b64Content base64-encoded clipboard content to set - * @property {ContentTypeEnum} contentType [text] The clipboard content type to set - */ - /** * Sets the Windows clipboard to the given string or PNG-image. * - * @param {Partial} opts + * @this {WindowsDriver} + * @param {string} b64Content base64-encoded clipboard content to set + * @param {ContentTypeEnum} [contentType='text'] The clipboard content type to set */ -commands.windowsSetClipboard = async function windowsSetClipboard (opts = {}) { - const { - b64Content, - contentType = CONTENT_TYPE.plaintext, - } = requireArgs(['b64Content'], opts); +export async function windowsSetClipboard ( + b64Content, + contentType = CONTENT_TYPE.plaintext +) { if (b64Content && Buffer.from(b64Content, 'base64').toString('base64') !== b64Content) { throw new errors.InvalidArgumentError(`The 'b64Content' argument must be a valid base64-encoded string`); } @@ -56,26 +48,21 @@ commands.windowsSetClipboard = async function windowsSetClipboard (opts = {}) { `Only the following content types are supported: ${_.values(CONTENT_TYPE)}` ); } -}; +} // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-clipboard?view=powershell-7.3 -/** - * @typedef {Object} GetClipboardOptions - * @property {ContentTypeEnum} contentType [plaintext] The clipboard content type to get. - * Only PNG images are supported for extraction if set to 'image'. - */ - /** * Returns the Windows clipboard content as base64-encoded string. * - * @param {Partial} opts + * @this {WindowsDriver} + * @property {ContentTypeEnum} [contentType='plaintext'] The clipboard content type to get. + * Only PNG images are supported for extraction if set to 'image'. * @returns {Promise} base64-encoded content of the clipboard */ -commands.windowsGetClipboard = async function windowsGetClipboard (opts = {}) { - const { - contentType = CONTENT_TYPE.plaintext, - } = opts; +export async function windowsGetClipboard ( + contentType = CONTENT_TYPE.plaintext +) { switch (contentType) { case CONTENT_TYPE.plaintext: { const {stdout} = await exec('powershell', ['-command', @@ -98,7 +85,8 @@ commands.windowsGetClipboard = async function windowsGetClipboard (opts = {}) { `Only the following content types are supported: ${_.values(CONTENT_TYPE)}` ); } -}; +} -export { commands }; -export default commands; +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + */ diff --git a/lib/commands/execute.js b/lib/commands/execute.js index 0f8325c..6d2ebea 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -2,48 +2,43 @@ import _ from 'lodash'; import { errors } from 'appium/driver'; import { POWER_SHELL_FEATURE } from '../constants'; -const commands = {}; - -const POWER_SHELL_SCRIPT_PATTERN = /^powerShell$/; -const WINDOWS_EXTENSION_SCRIPT_PATTERN = /^windows:/; -const EXTENSION_COMMANDS_MAPPING = { - startRecordingScreen: 'startRecordingScreen', - stopRecordingScreen: 'stopRecordingScreen', - - launchApp: 'windowsLaunchApp', - closeApp: 'windowsCloseApp', - - deleteFile: 'windowsDeleteFile', - deleteFolder: 'windowsDeleteFolder', - - click: 'windowsClick', - scroll: 'windowsScroll', - clickAndDrag: 'windowsClickAndDrag', - hover: 'windowsHover', - keys: 'windowsKeys', - - setClipboard: 'windowsSetClipboard', - getClipboard: 'windowsGetClipboard', -}; - -commands.execute = async function execute (script, args) { - if (WINDOWS_EXTENSION_SCRIPT_PATTERN.test(script)) { +const POWER_SHELL_SCRIPT = 'powerShell'; +const EXECUTE_SCRIPT_PREFIX = 'windows:'; + +/** + * + * @this {WindowsDriver} + * @param {string} script + * @param {ExecuteMethodArgs} [args] + * @returns {Promise} + */ +export async function execute (script, args) { + if (_.startsWith(script, EXECUTE_SCRIPT_PREFIX)) { this.log.info(`Executing extension command '${script}'`); - script = script.replace(WINDOWS_EXTENSION_SCRIPT_PATTERN, '').trim(); - return await this.executeWindowsCommand(script, _.isArray(args) ? args[0] : args); - } else if (POWER_SHELL_SCRIPT_PATTERN.test(script)) { + const formattedScript = script.trim().replace(/^windows:\s*/, `${EXECUTE_SCRIPT_PREFIX} `); + const preprocessedArgs = preprocessExecuteMethodArgs(args); + return await this.executeMethod(formattedScript, [preprocessedArgs]); + } else if (script === POWER_SHELL_SCRIPT) { this.assertFeatureEnabled(POWER_SHELL_FEATURE); - return await this.execPowerShell(_.isArray(args) ? _.first(args) : args); + return await this.execPowerShell( + /** @type {import('./powershell').ExecPowerShellOptions} */ (preprocessExecuteMethodArgs(args)) + ); } throw new errors.NotImplementedError(); -}; - -commands.executeWindowsCommand = async function executeWindowsCommand (command, opts = {}) { - if (!_.has(EXTENSION_COMMANDS_MAPPING, command)) { - throw new errors.UnknownCommandError(`Unknown windows command '${command}'. ` + - `Only ${_.keys(EXTENSION_COMMANDS_MAPPING)} commands are supported.`); - } - return await this[EXTENSION_COMMANDS_MAPPING[command]](opts); -}; - -export default commands; +} + +/** + * Massages the arguments going into an execute method. + * + * @param {ExecuteMethodArgs} [args] + * @returns {StringRecord} + */ +function preprocessExecuteMethodArgs(args) { + return /** @type {StringRecord} */ ((_.isArray(args) ? _.first(args) : args) ?? {}); +} + +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + * @typedef {import('@appium/types').StringRecord} StringRecord + * @typedef {readonly any[] | readonly [StringRecord] | Readonly} ExecuteMethodArgs + */ diff --git a/lib/commands/file-movement.js b/lib/commands/file-movement.js index 0552eeb..2bd28dc 100644 --- a/lib/commands/file-movement.js +++ b/lib/commands/file-movement.js @@ -12,9 +12,15 @@ const KNOWN_ENV_VARS = [ 'TEMP', 'TMP', 'HOMEPATH', 'USERPROFILE', 'PUBLIC' ]; -const commands = {}; -commands.pushFile = async function pushFile (remotePath, base64Data) { +/** + * + * @this {WindowsDriver} + * @param {string} remotePath + * @param {string} base64Data + * @returns {Promise} + */ +export async function pushFile (remotePath, base64Data) { this.assertFeatureEnabled(MODIFY_FS_FEATURE); if (remotePath.endsWith(path.sep)) { throw new errors.InvalidArgumentError( @@ -32,75 +38,84 @@ commands.pushFile = async function pushFile (remotePath, base64Data) { await mkdirp(path.dirname(fullPath)); const content = Buffer.from(base64Data, 'base64'); await fs.writeFile(fullPath, content); -}; +} -commands.pullFile = async function pullFile (remotePath) { +/** + * + * @this {WindowsDriver} + * @param {string} remotePath + * @returns {Promise} + */ +export async function pullFile (remotePath) { const fullPath = resolveToAbsolutePath(remotePath); await checkFileExists(fullPath); return (await util.toInMemoryBase64(fullPath)).toString(); -}; +} -commands.pullFolder = async function pullFolder (remotePath) { +/** + * + * @this {WindowsDriver} + * @param {string} remotePath + * @returns {Promise} + */ +export async function pullFolder (remotePath) { const fullPath = resolveToAbsolutePath(remotePath); await checkFolderExists(fullPath); return (await zip.toInMemoryZip(fullPath, { encodeToBase64: true, })).toString(); -}; +} /** - * @typedef {Object} DeleteFileOptions - * @property {string} remotePath - The path to a file. + * Remove the file from the file system + * + * @this {WindowsDriver} + * @param {string} remotePath - The path to a file. * The path may contain environment variables that could be expanded on the server side. * Due to security reasons only variables listed below would be expanded: `APPDATA`, * `LOCALAPPDATA`, `PROGRAMFILES`, `PROGRAMFILES(X86)`, `PROGRAMDATA`, `ALLUSERSPROFILE`, * `TEMP`, `TMP`, `HOMEPATH`, `USERPROFILE`, `PUBLIC`. - */ - -/** - * Remove the file from the file system - * - * @param {DeleteFileOptions} opts * @throws {InvalidArgumentError} If the file to be deleted does not exist or * remote path is not an absolute path. */ -commands.windowsDeleteFile = async function windowsDeleteFile (opts) { +export async function windowsDeleteFile (remotePath) { this.assertFeatureEnabled(MODIFY_FS_FEATURE); - const { remotePath } = opts ?? {}; const fullPath = resolveToAbsolutePath(remotePath); await checkFileExists(fullPath); await fs.unlink(fullPath); -}; +} /** - * @typedef {Object} DeleteFolderOptions - * @property {string} remotePath - The path to a folder. + * Remove the folder from the file system + * + * @this {WindowsDriver} + * @param {string} remotePath - The path to a folder. * The path may contain environment variables that could be expanded on the server side. * Due to security reasons only variables listed below would be expanded: `APPDATA`, * `LOCALAPPDATA`, `PROGRAMFILES`, `PROGRAMFILES(X86)`, `PROGRAMDATA`, `ALLUSERSPROFILE`, * `TEMP`, `TMP`, `HOMEPATH`, `USERPROFILE`, `PUBLIC`. - */ - -/** - * Remove the folder from the file system - * - * @param {DeleteFolderOptions} opts * @throws {InvalidArgumentError} If the folder to be deleted does not exist or * remote path is not an absolute path. */ -commands.windowsDeleteFolder = async function windowsDeleteFolder (opts) { +export async function windowsDeleteFolder (remotePath) { this.assertFeatureEnabled(MODIFY_FS_FEATURE); - const { remotePath } = opts ?? {}; const fullPath = resolveToAbsolutePath(remotePath); await checkFolderExists(fullPath); await fs.rimraf(fullPath); -}; +} +/** + * + * @param {string} remotePath + * @returns {string} + */ function resolveToAbsolutePath (remotePath) { - const resolvedPath = remotePath.replace(/%([^%]+)%/g, + const resolvedPath = remotePath.replace( + /%([^%]+)%/g, (_, key) => KNOWN_ENV_VARS.includes(key.toUpperCase()) - ? process.env[key.toUpperCase()] - : `%${key}%`); + ? /** @type {string} */ (process.env[key.toUpperCase()]) + : `%${key}%` + ); if (!path.isAbsolute(resolvedPath)) { throw new errors.InvalidArgumentError('It is expected that remote path is absolute. ' + @@ -109,6 +124,11 @@ function resolveToAbsolutePath (remotePath) { return resolvedPath; } +/** + * + * @param {string} remotePath + * @returns {Promise} + */ async function checkFileExists (remotePath) { if (!await fs.exists(remotePath)) { throw new errors.InvalidArgumentError(`The remote file '${remotePath}' does not exist.`); @@ -121,6 +141,11 @@ async function checkFileExists (remotePath) { } } +/** + * + * @param {string} remotePath + * @returns {Promise} + */ async function checkFolderExists (remotePath) { if (!await fs.exists(remotePath)) { throw new errors.InvalidArgumentError(`The remote folder '${remotePath}' does not exist.`); @@ -133,5 +158,6 @@ async function checkFolderExists (remotePath) { } } -export { commands }; -export default commands; +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + */ \ No newline at end of file diff --git a/lib/commands/find.js b/lib/commands/find.js index bf9d718..1151a1e 100644 --- a/lib/commands/find.js +++ b/lib/commands/find.js @@ -1,19 +1,23 @@ import { util } from 'appium/support'; - -const commands = {}; - -commands.findElOrEls = async function findElOrEls (strategy, selector, mult, context) { - context = util.unwrapElement(context); - const endpoint = `/element${context ? `/${context}/element` : ''}${mult ? 's' : ''}`; - +/** + * + * @this {WindowsDriver} + * @param {string} strategy + * @param {string} selector + * @param {boolean} mult + * @param {string} [context] + * @returns + */ +export async function findElOrEls (strategy, selector, mult, context) { + const endpoint = `/element${context ? `/${util.unwrapElement(context)}/element` : ''}${mult ? 's' : ''}`; // This is either an array if mult is true or an object if mult is false return await this.winAppDriver.sendCommand(endpoint, 'POST', { using: strategy, value: selector, }); -}; - +} -export { commands }; -export default commands; +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + */ diff --git a/lib/commands/general.js b/lib/commands/general.js index bbdab77..20042ab 100644 --- a/lib/commands/general.js +++ b/lib/commands/general.js @@ -8,7 +8,7 @@ import { util } from 'appium/support'; */ /** - * @this {import('../driver').WindowsDriver} + * @this {WindowsDriver} * @returns {Promise} */ async function getScreenSize () { @@ -26,17 +26,18 @@ async function getScreenSize () { }; } -const commands = {}; - // The next two commands are required // for proper `-image` locator functionality /** + * @this {WindowsDriver} * @returns {Promise} */ -commands.getWindowSize = async function getWindowSize () { - const size = await this.winAppDriver.sendCommand('/window/size', 'GET'); +export async function getWindowSize () { + const size = await this.winAppDriver.sendCommand( + '/window/size', 'GET' + ); if (_.isPlainObject(size)) { - return size; + return /** @type {Size} */ (size); } // workaround for https://github.com/microsoft/WinAppDriver/issues/1104 this.log.info('Cannot retrieve window size from WinAppDriver'); @@ -45,51 +46,95 @@ commands.getWindowSize = async function getWindowSize () { }; // a workaround for https://github.com/appium/appium/issues/15923 -commands.getWindowRect = async function getWindowRect () { - const {width, height} = await this.getWindowSize(); +/** + * @this {WindowsDriver} + * @returns {Promise} + */ +export async function getWindowRect () { + const {width, height} = await getWindowSize.bind(this)(); let [x, y] = [0, 0]; try { - const handle = await this.winAppDriver.sendCommand('/window_handle', 'GET'); - ({x, y} = await this.winAppDriver.sendCommand(`/window/${handle}/position`, 'GET')); + const handle = await this.winAppDriver.sendCommand( + '/window_handle', 'GET' + ); + ({x, y} = /** @type {import('@appium/types').Position} */ ( + await this.winAppDriver.sendCommand( + `/window/${handle}/position`, 'GET' + )) + ); } catch (e) { this.log.warn( `Cannot fetch the window position. Defaulting to zeroes. Original error: ${e.message}` ); } return {x, y, width, height}; -}; +} // a workaround for https://github.com/appium/appium/issues/15923 -commands.setWindowRect = async function setWindowRect (x, y, width, height) { +/** + * @this {WindowsDriver} + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + * @returns {Promise} + */ +export async function setWindowRect (x, y, width, height) { let didProcess = false; if (!_.isNil(width) && !_.isNil(height)) { - await this.winAppDriver.sendCommand('/window/size', 'POST', {width, height}); + await this.winAppDriver.sendCommand( + '/window/size', 'POST', {width, height} + ); didProcess = true; } if (!_.isNil(x) && !_.isNil(y)) { - const handle = await this.winAppDriver.sendCommand('/window_handle', 'GET'); - await this.winAppDriver.sendCommand(`/window/${handle}/position`, 'POST', {x, y}); + const handle = await this.winAppDriver.sendCommand( + '/window_handle', 'GET' + ); + await this.winAppDriver.sendCommand( + `/window/${handle}/position`, 'POST', {x, y} + ); didProcess = true; } if (!didProcess) { this.log.info('Either x and y or width and height must be defined. Doing nothing'); } -}; +} -commands.getScreenshot = async function getScreenshot () { +/** + * @this {WindowsDriver} + * @returns {Promise} + */ +export async function getScreenshot () { // TODO: This trick ensures the resulting data is encoded according to RFC4648 standard // TODO: remove it as soon as WAD returns the screenshot data being properly encoded - return Buffer.from(await this.winAppDriver.sendCommand('/screenshot', 'GET'), 'base64') + const originalPayload = await this.winAppDriver.sendCommand( + '/screenshot', 'GET' + ); + return Buffer.from(/** @type {string} */ (originalPayload), 'base64') .toString('base64'); -}; +} // a workaround for https://github.com/appium/appium/issues/16316 -commands.getElementRect = async function getElementRect (el) { +/** + * + * @this {WindowsDriver} + * @param {string} el + * @returns {Promise} + */ +export async function getElementRect (el) { const elId = util.unwrapElement(el); - const {x, y} = await this.winAppDriver.sendCommand(`/element/${elId}/location`, 'GET'); - const {width, height} = await this.winAppDriver.sendCommand(`/element/${elId}/size`, 'GET'); + const {x, y} = /** @type {import('@appium/types').Position} */ ( + await this.winAppDriver.sendCommand(`/element/${elId}/location`, 'GET') + ); + const {width, height} = /** @type {import('@appium/types').Size} */ ( + await this.winAppDriver.sendCommand( + `/element/${elId}/size`, 'GET' + ) + ); return {x, y, width, height}; -}; +} -export { commands }; -export default commands; +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + */ diff --git a/lib/commands/gestures.js b/lib/commands/gestures.js index 769a254..d19cac2 100644 --- a/lib/commands/gestures.js +++ b/lib/commands/gestures.js @@ -250,52 +250,44 @@ function parseKeyActions(actions) { return combinedArray; } - -const commands = {}; - /** - * @typedef {Object} ClickOptions - * @property {string} elementId Hexadecimal identifier of the element to click on. + * Performs single click mouse gesture. + * + * @this {WindowsDriver} + * @param {string} [elementId] Hexadecimal identifier of the element to click on. * If this parameter is missing then given coordinates will be parsed as absolute ones. * Otherwise they are parsed as relative to the top left corner of this element. - * @property {number} x Integer horizontal coordinate of the click point. Both x and y coordinates + * @param {number} [x] Integer horizontal coordinate of the click point. Both x and y coordinates * must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {number} y Integer vertical coordinate of the click point. Both x and y coordinates + * @param {number} [y] Integer vertical coordinate of the click point. Both x and y coordinates * must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {'left' | 'middle' | 'right' | 'back' | 'forward'} button [left] Name of + * @param {'left' | 'middle' | 'right' | 'back' | 'forward'} [button=left] Name of * the mouse button to be clicked. An exception is thrown if an unknown button name * is provided. - * @property {string[]|string} modifierKeys List of possible keys or a single key name to + * @param {string[]|string} [modifierKeys] List of possible keys or a single key name to * depress while the click is being performed. Supported key names are: Shift, Ctrl, Alt, Win. * For example, in order to keep Ctrl+Alt depressed while clicking, provide the value of * ['ctrl', 'alt'] - * @property {number} durationMs The number of milliseconds to wait between pressing + * @param {number} [durationMs] The number of milliseconds to wait between pressing * and releasing the mouse button. By default no delay is applied, which simulates a * regular click. - * @property {number} times [1] How many times the click must be performed. - * @property {number} interClickDelayMs [100] Duration od the pause between each + * @param {number} [times=1] How many times the click must be performed. + * @param {number} [interClickDelayMs=100] Duration od the pause between each * click gesture. Only makes sense if `times` is greater than one. - */ - -/** - * Performs single click mouse gesture. - * - * @param {ClickOptions} opts * @throws {Error} If given options are not acceptable or the gesture has failed. */ -commands.windowsClick = async function windowsClick (opts) { - const { - elementId, - x, y, - button = MOUSE_BUTTON.LEFT, - modifierKeys, - durationMs, - times = 1, - interClickDelayMs = 100, - } = opts ?? {}; - +export async function windowsClick ( + elementId, + x, + y, + button = MOUSE_BUTTON.LEFT, + modifierKeys, + durationMs, + times = 1, + interClickDelayMs = 100, +) { const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys); const [absoluteX, absoluteY] = await toAbsoluteCoordinates.bind(this)(elementId, x, y); let clickDownInput; @@ -318,12 +310,12 @@ commands.windowsClick = async function windowsClick (opts) { await handleInputs(modifierKeyDownInputs); } await handleInputs(moveInput); - const hasDuration = _.isInteger(durationMs) && durationMs > 0; + const hasDuration = _.isInteger(durationMs) && /** @type {number} */ (durationMs) > 0; const hasInterClickDelay = _.isInteger(interClickDelayMs) && interClickDelayMs > 0; for (let i = 0; i < times; ++i) { if (hasDuration) { await handleInputs(clickDownInput); - await B.delay(durationMs); + await B.delay(/** @type {number} */ (durationMs)); await handleInputs(clickUpInput); } else { await handleInputs(clickInput); @@ -340,47 +332,46 @@ commands.windowsClick = async function windowsClick (opts) { }; /** - * @typedef {Object} ScrollOptions - * @property {string} elementId Hexadecimal identifier of the element to scroll. + * Performs horizontal or vertical scrolling with mouse wheel. + * + * @this {WindowsDriver} + * @param {string} [elementId] Hexadecimal identifier of the element to scroll. * If this parameter is missing then given coordinates will be parsed as absolute ones. * Otherwise they are parsed as relative to the top left corner of this element. - * @property {number} x Integer horizontal coordinate of the scroll point. Both x and y coordinates + * @param {number} [x] Integer horizontal coordinate of the scroll point. Both x and y coordinates * must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {number} y Integer vertical coordinate of the scroll point. Both x and y coordinates + * @param {number} [y] Integer vertical coordinate of the scroll point. Both x and y coordinates * must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {number} deltaX Integer horizontal scroll delta. Either this value + * @param {number} [deltaX] Integer horizontal scroll delta. Either this value * or deltaY must be provided, but not both. - * @property {number} deltaY Integer vertical scroll delta. Either this value + * @param {number} [deltaY] Integer vertical scroll delta. Either this value * or deltaX must be provided, but not both. - * @property {string[]|string} modifierKeys List of possible keys or a single key name to + * @param {string[]|string} [modifierKeys] List of possible keys or a single key name to * depress while the scroll is being performed. Supported key names are: Shift, Ctrl, Alt, Win. * For example, in order to keep Ctrl+Alt depressed while clicking, provide the value of * ['ctrl', 'alt'] - */ - -/** - * Performs horizontal or vertical scrolling with mouse wheel. - * - * @param {ScrollOptions} opts * @throws {Error} If given options are not acceptable or the gesture has failed. */ -commands.windowsScroll = async function windowsScroll (opts) { - const { - elementId, - x, y, - deltaX, deltaY, - modifierKeys, - } = opts ?? {}; - +export async function windowsScroll ( + elementId, + x, + y, + deltaX, + deltaY, + modifierKeys, +) { const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys); const [absoluteX, absoluteY] = await toAbsoluteCoordinates.bind(this)(elementId, x, y); let moveInput; let scrollInput; try { moveInput = await toMouseMoveInput({x: absoluteX, y: absoluteY}); - scrollInput = toMouseWheelInput({dx: deltaX, dy: deltaY}); + scrollInput = toMouseWheelInput({ + dx: /** @type {number} */ (deltaX), + dy: /** @type {number} */ (deltaY), + }); } catch (e) { throw preprocessError(e); } @@ -400,52 +391,46 @@ commands.windowsScroll = async function windowsScroll (opts) { await handleInputs(modifierKeyUpInputs); } } -}; +} /** - * @typedef {Object} ClickAndDragOptions - * @property {string} startElementId Hexadecimal identifier of the element to start the drag from. + * Performs drag and drop mouse gesture. + * + * @this {WindowsDriver} + * @param {string} [startElementId] Hexadecimal identifier of the element to start the drag from. * If this parameter is missing then given coordinates will be parsed as absolute ones. * Otherwise they are parsed as relative to the top left corner of this element. - * @property {number} startX Integer horizontal coordinate of the drag start point. Both startX + * @param {number} [startX] Integer horizontal coordinate of the drag start point. Both startX * and startY coordinates must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {number} startY Integer vertical coordinate of the drag start point. Both startX and + * @param {number} [startY] Integer vertical coordinate of the drag start point. Both startX and * startY coordinates must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {string} endElementId Hexadecimal identifier of the element to end the drag on. + * @param {string} [endElementId] Hexadecimal identifier of the element to end the drag on. * If this parameter is missing then given coordinates will be parsed as absolute ones. * Otherwise they are parsed as relative to the top left corner of this element. - * @property {number} endX Integer horizontal coordinate of the drag end point. Both endX and endY coordinates + * @param {number} [endX] Integer horizontal coordinate of the drag end point. Both endX and endY coordinates * must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {number} endY Integer vertical coordinate of the drag end point. Both endX and endY coordinates + * @param {number} [endY] Integer vertical coordinate of the drag end point. Both endX and endY coordinates * must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {string[]|string} modifierKeys List of possible keys or a single key name to + * @param {string[]|string} [modifierKeys] List of possible keys or a single key name to * depress while the drag is being performed. Supported key names are: Shift, Ctrl, Alt, Win. * For example, in order to keep Ctrl+Alt depressed while clicking, provide the value of * ['ctrl', 'alt'] - * @property {number} durationMs [5000] The number of milliseconds to wait between pressing + * @param {number} [durationMs=5000] The number of milliseconds to wait between pressing * the left mouse button and moving the cursor to the ending drag point. - */ - -/** - * Performs drag and drop mouse gesture. - * - * @param {ClickAndDragOptions} opts * @throws {Error} If given options are not acceptable or the gesture has failed. */ -commands.windowsClickAndDrag = async function windowsClickAndDrag (opts) { - const { - startElementId, - startX, startY, - endElementId, - endX, endY, - modifierKeys, - durationMs = 5000, - } = opts ?? {}; - +export async function windowsClickAndDrag ( + startElementId, + startX, startY, + endElementId, + endX, endY, + modifierKeys, + durationMs = 5000, +) { const screenSize = await getVirtualScreenSize(); const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys); const [[startAbsoluteX, startAbsoluteY], [endAbsoluteX, endAbsoluteY]] = await B.all([ @@ -484,52 +469,46 @@ commands.windowsClickAndDrag = async function windowsClickAndDrag (opts) { await handleInputs(modifierKeyUpInputs); } } -}; +} /** - * @typedef {Object} HoverOptions - * @property {string} startElementId Hexadecimal identifier of the element to start the hover from. + * Performs hover mouse gesture. + * + * @this {WindowsDriver} + * @param {string} [startElementId] Hexadecimal identifier of the element to start the hover from. * If this parameter is missing then given coordinates will be parsed as absolute ones. * Otherwise they are parsed as relative to the top left corner of this element. - * @property {number} startX Integer horizontal coordinate of the hover start point. Both startX + * @param {number} [startX] Integer horizontal coordinate of the hover start point. Both startX * and startY coordinates must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {number} startY Integer vertical coordinate of the hover start point. Both startX and + * @param {number} [startY] Integer vertical coordinate of the hover start point. Both startX and * startY coordinates must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {string} endElementId Hexadecimal identifier of the element to end the hover on. + * @param {string} [endElementId] Hexadecimal identifier of the element to end the hover on. * If this parameter is missing then given coordinates will be parsed as absolute ones. * Otherwise they are parsed as relative to the top left corner of this element. - * @property {number} endX Integer horizontal coordinate of the hover end point. Both endX and endY coordinates + * @param {number} [endX] Integer horizontal coordinate of the hover end point. Both endX and endY coordinates * must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {number} endY Integer vertical coordinate of the hover end point. Both endX and endY coordinates + * @param {number} [endY] Integer vertical coordinate of the hover end point. Both endX and endY coordinates * must be provided or none of them if elementId is present. In such case the gesture * will be performed at the center point of the given element. - * @property {string[]|string} modifierKeys List of possible keys or a single key name to + * @param {string[]|string} [modifierKeys] List of possible keys or a single key name to * depress while the hover is being performed. Supported key names are: Shift, Ctrl, Alt, Win. * For example, in order to keep Ctrl+Alt depressed while hovering, provide the value of * ['ctrl', 'alt'] - * @property {number} durationMs [500] The number of milliseconds between + * @param {number} [durationMs=500] The number of milliseconds between * moving the cursor from the starting to the ending hover point. - */ - -/** - * Performs hover mouse gesture. - * - * @param {HoverOptions} opts * @throws {Error} If given options are not acceptable or the gesture has failed. */ -commands.windowsHover = async function windowsHover (opts) { - const { - startElementId, - startX, startY, - endElementId, - endX, endY, - modifierKeys, - durationMs = 500, - } = opts ?? {}; - +export async function windowsHover ( + startElementId, + startX, startY, + endElementId, + endX, endY, + modifierKeys, + durationMs = 500, +) { const screenSize = await getVirtualScreenSize(); const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys); const [[startAbsoluteX, startAbsoluteY], [endAbsoluteX, endAbsoluteY]] = await B.all([ @@ -575,7 +554,7 @@ commands.windowsHover = async function windowsHover (opts) { await handleInputs(modifierKeyUpInputs); } } -}; +} /** * @typedef {Object} KeyAction @@ -592,22 +571,14 @@ commands.windowsHover = async function windowsHover (opts) { * ! Do not forget to release depressed keys in your automated tests. */ -/** - * @typedef {Object} KeysOptions - * @property {KeyAction[] | KeyAction} actions One or more key actions. - */ - /** * Performs customized keyboard input. * - * @param {KeysOptions} opts + * @this {WindowsDriver} + * @param {KeyAction[] | KeyAction} actions One or more key actions. * @throws {Error} If given options are not acceptable or the gesture has failed. */ -commands.windowsKeys = async function windowsKeys (opts) { - const { - actions, - } = opts ?? {}; - +export async function windowsKeys (actions) { const parsedItems = parseKeyActions(_.isArray(actions) ? actions : [actions]); this.log.debug(`Parsed ${util.pluralize('key action', parsedItems.length, true)}`); for (const item of parsedItems) { @@ -617,7 +588,8 @@ commands.windowsKeys = async function windowsKeys (opts) { await B.delay(item); } } -}; +} -export { commands }; -export default commands; +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + */ diff --git a/lib/commands/index.js b/lib/commands/index.js deleted file mode 100644 index 0c199d0..0000000 --- a/lib/commands/index.js +++ /dev/null @@ -1,30 +0,0 @@ -import generalCmds from './general'; -import findCmds from './find'; -import recordScreenCmds from './record-screen'; -import touchCmds from './touch'; -import powerShellCmds from './powershell'; -import executeCmds from './execute'; -import fileMovementExtensions from './file-movement'; -import appManagementExtensions from './app-management'; -import gesturesCmds from './gestures'; -import clipboardCmds from './clipboard'; - - -const commands = {}; -Object.assign( - commands, - executeCmds, - generalCmds, - findCmds, - recordScreenCmds, - touchCmds, - powerShellCmds, - fileMovementExtensions, - appManagementExtensions, - gesturesCmds, - clipboardCmds, - // add other command types here -); - -export { commands }; -export default commands; diff --git a/lib/commands/powershell.js b/lib/commands/powershell.js index 5e361fd..9b09c75 100644 --- a/lib/commands/powershell.js +++ b/lib/commands/powershell.js @@ -4,8 +4,6 @@ import { exec } from 'teen_process'; import path from 'path'; import B from 'bluebird'; -const commands = {}; - const EXECUTION_POLICY = { REMOTE_SIGNED: 'RemoteSigned', UNDEFINED: 'Undefined', @@ -16,8 +14,8 @@ const POWER_SHELL = 'powershell.exe'; /** * @typedef {Object} ExecPowerShellOptions - * @property {string?} script A valid Power Shell script to execute - * @property {string?} command A valid Power Shell command to execute + * @property {string} [script] A valid Power Shell script to execute + * @property {string} [command] A valid Power Shell command to execute */ /** @@ -30,12 +28,13 @@ const POWER_SHELL = 'powershell.exe'; * temporarily switch user execution policy if necessary and restore it afterwards. * This makes scripts slightly less performant, as single commands. * + * @this {WindowsDriver} * @param {ExecPowerShellOptions} 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.execPowerShell = async function execPowerShell (opts) { +export async function execPowerShell (opts) { const { script, command, @@ -101,7 +100,8 @@ commands.execPowerShell = async function execPowerShell (opts) { })() ]); } -}; +} -export { commands }; -export default commands; +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + */ diff --git a/lib/commands/record-screen.js b/lib/commands/record-screen.js index 68deeee..7adc3fa 100644 --- a/lib/commands/record-screen.js +++ b/lib/commands/record-screen.js @@ -4,8 +4,6 @@ import { util, fs, net, system, tempDir } from 'appium/support'; 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 @@ -51,7 +49,7 @@ async function requireFfmpegPath () { } } -class ScreenRecorder { +export class ScreenRecorder { /** * @param {string} videoPath * @param {import('@appium/types').AppiumLogger} log @@ -196,17 +194,20 @@ class ScreenRecorder { } } - /** - * @typedef {Object} StartRecordingOptions + * Record the display in background while the automated test is running. + * This method requires FFMPEG (https://www.ffmpeg.org/download.html) to be installed + * and present in PATH. + * The resulting video uses H264 codec and is ready to be played by media players built-in into web browsers. * - * @property {string} videoFilter - The video filter spec to apply for ffmpeg. + * @this {WindowsDriver} + * @param {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. + * @param {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: + * @param {string} [preset='veryfast'] - One of the supported encoding presets. Possible values are: * - ultrafast * - superfast * - veryfast @@ -220,40 +221,29 @@ class ScreenRecorder { * 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 + * @param {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 + * @param {boolean} [captureClicks=false] - Whether to capture mouse clicks while recording the * screen - * @property {string} audioInput - If set then the given audio input will be used to record the computer audio + * @param {string} [audioInput] - If set then the given audio input will be used to record the computer audio * along with the desktop video. The list of available devices could be retrieved using * `ffmpeg -list_devices true -f dshow -i dummy` command. - * @property {string|number} timeLimit [600] - The maximum recording time, in seconds. The default + * @param {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 + * @param {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 - * and present in PATH. - * 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').WindowsDriver} * @throws {Error} If screen recording has failed to start or is not supported on the device under test. */ -commands.startRecordingScreen = async function startRecordingScreen (options) { - const { - timeLimit, - videoFilter, - fps, - preset, - captureCursor, - captureClicks, - audioInput, - forceRestart = true, - } = options ?? {}; +export async function startRecordingScreen ( + timeLimit, + videoFilter, + fps, + preset, + captureCursor, + captureClicks, + audioInput, + forceRestart = true, +) { if (this._screenRecorder?.isRunning?.()) { this.log.debug('The screen recording is already running'); if (!forceRestart) { @@ -284,31 +274,26 @@ commands.startRecordingScreen = async function startRecordingScreen (options) { this._screenRecorder = null; throw e; } -}; +} /** - * @typedef {Object} StopRecordingOptions + * Stop recording the screen. + * If no screen recording has been started before then the method returns an empty string. * - * @property {string} remotePath - The path to the remote location, where the resulting video should be uploaded. + * @this {WindowsDriver} + * @param {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 + * @param {string} [user] - The name of the user for the remote authentication. + * @param {string} [pass] - The password for the remote authentication. + * @param {string} [method] - The http multipart upload method name. The 'PUT' one is used by default. + * @param {Object} [headers] - Additional headers mapping for multipart http(s) uploads + * @param {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 {Object[]|[string, string][]} [formFields] - Additional form fields for multipart http(s) uploads * @returns {Promise} Base64-encoded content of the recorded media file if 'remotePath' * parameter is falsy or an empty string. * @this {import('../driver').WindowsDriver} @@ -316,7 +301,15 @@ commands.startRecordingScreen = async function startRecordingScreen (options) { * 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 ( + remotePath, + user, + pass, + method, + headers, + fileFieldName, + formFields, +) { if (!this._screenRecorder) { this.log.debug('No screen recording has been started. Doing nothing'); return ''; @@ -328,11 +321,20 @@ commands.stopRecordingScreen = async function stopRecordingScreen (options) { this.log.debug('No video data is found. Returning an empty string'); return ''; } - if (_.isEmpty(options.remotePath)) { + if (_.isEmpty(remotePath)) { const {size} = await fs.stat(videoPath); this.log.debug(`The size of the resulting screen recording is ${util.toReadableSizeString(size)}`); } - return await uploadRecordedMedia(videoPath, options.remotePath, options); -}; + return await uploadRecordedMedia(videoPath, remotePath, { + user, + pass, + method, + headers, + fileFieldName, + formFields, + }); +} -export default commands; +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + */ diff --git a/lib/commands/touch.js b/lib/commands/touch.js index 2de01b7..47ec604 100644 --- a/lib/commands/touch.js +++ b/lib/commands/touch.js @@ -1,9 +1,17 @@ -const commands = {}; //This is needed to make clicks on -image elements work properly -commands.performActions = async function (actions) { - return await this.winAppDriver.sendCommand('/actions', 'POST', {actions}); -}; +/** + * + * @this {WindowsDriver} + * @param {any} actions + * @returns {Promise} + */ +export async function performActions (actions) { + return await this.winAppDriver.sendCommand( + '/actions', 'POST', {actions} + ); +} -Object.assign(commands); -export default commands; +/** + * @typedef {import('../driver').WindowsDriver} WindowsDriver + */ diff --git a/lib/driver.js b/lib/driver.js index 84a7f5a..6448543 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -3,9 +3,19 @@ import { BaseDriver } from 'appium/driver'; import { system } from 'appium/support'; import { WinAppDriver } from './winappdriver'; import { desiredCapConstraints } from './desired-caps'; -import { commands } from './commands/index'; +import * as appManagementCommands from './commands/app-management'; +import * as clipboardCommands from './commands/clipboard'; +import * as executeCommands from './commands/execute'; +import * as fileCommands from './commands/file-movement'; +import * as findCommands from './commands/find'; +import * as generalCommands from './commands/general'; +import * as gestureCommands from './commands/gestures'; +import * as powershellCommands from './commands/powershell'; +import * as recordScreenCommands from './commands/record-screen'; +import * as touchCommands from './commands/touch'; import { POWER_SHELL_FEATURE } from './constants'; import { newMethodMap } from './method-map'; +import { executeMethodMap } from './execute-method-map'; /** @type {import('@appium/types').RouteMatcher[]} */ const NO_PROXY = [ @@ -30,17 +40,21 @@ const NO_PROXY = [ ]; // Appium instantiates this class -class WindowsDriver extends BaseDriver { +export class WindowsDriver extends BaseDriver { /** @type {boolean} */ isProxyActive; - /** @type {(toRun: {command?: string; script?: string}) => Promise} */ - execPowerShell; - /** @type {import('@appium/types').RouteMatcher[]} */ jwpProxyAvoid; + /** @type {WinAppDriver} */ + winAppDriver; + + /** @type {import('./commands/record-screen').ScreenRecorder | null} */ + _screenRecorder; + static newMethodMap = newMethodMap; + static executeMethodMap = executeMethodMap; constructor (opts = {}, shouldValidateCaps = true) { // @ts-ignore TODO: Make opts typed @@ -55,15 +69,12 @@ class WindowsDriver extends BaseDriver { 'accessibility id', ]; this.resetState(); - - for (const [cmd, fn] of _.toPairs(commands)) { - WindowsDriver.prototype[cmd] = fn; - } } resetState () { this.jwpProxyAvoid = NO_PROXY; this.isProxyActive = false; + // @ts-ignore It's ok this.winAppDriver = null; this._screenRecorder = null; } @@ -157,9 +168,44 @@ class WindowsDriver extends BaseDriver { } return await this.winAppDriver.proxy.command(url, method, body); } + + windowsLaunchApp = appManagementCommands.windowsLaunchApp; + windowsCloseApp = appManagementCommands.windowsCloseApp; + + windowsSetClipboard = clipboardCommands.windowsGetClipboard; + windowsGetClipboard = clipboardCommands.windowsGetClipboard; + + execute = executeCommands.execute; + + pushFile = fileCommands.pushFile; + pullFile = fileCommands.pullFile; + pullFolder = fileCommands.pullFolder; + windowsDeleteFile = fileCommands.windowsDeleteFile; + windowsDeleteFolder = fileCommands.windowsDeleteFolder; + + // @ts-ignore This is expected + findElOrEls = findCommands.findElOrEls; + + getWindowSize = generalCommands.getWindowSize; + getWindowRect = generalCommands.getWindowRect; + setWindowRect = generalCommands.setWindowRect; + getScreenshot = generalCommands.getScreenshot; + getElementRect = generalCommands.getElementRect; + + windowsClick = gestureCommands.windowsClick; + windowsScroll = gestureCommands.windowsScroll; + windowsClickAndDrag = gestureCommands.windowsClickAndDrag; + windowsHover = gestureCommands.windowsHover; + windowsKeys = gestureCommands.windowsKeys; + + execPowerShell = powershellCommands.execPowerShell; + + startRecordingScreen = recordScreenCommands.startRecordingScreen; + stopRecordingScreen = recordScreenCommands.stopRecordingScreen; + + performActions = touchCommands.performActions; } -export { WindowsDriver }; export default WindowsDriver; /** diff --git a/lib/execute-method-map.ts b/lib/execute-method-map.ts new file mode 100644 index 0000000..783ad4f --- /dev/null +++ b/lib/execute-method-map.ts @@ -0,0 +1,144 @@ +import { ExecuteMethodMap } from '@appium/types'; + +export const executeMethodMap = { + 'windows: startRecordingScreen': { + command: 'startRecordingScreen', + params: { + optional: [ + 'timeLimit', + 'fps', + 'preset', + 'captureCursor', + 'captureClicks', + 'audioInput', + 'forceRestart', + ], + }, + }, + 'windows: stopRecordingScreen': { + command: 'stopRecordingScreen', + params: { + optional: [ + 'remotePath', + 'user', + 'pass', + 'method', + 'headers', + 'fileFieldName', + 'formFields', + ], + }, + }, + + 'windows: launchApp': { + command: 'windowsLaunchApp', + }, + 'windows: closeApp': { + command: 'windowsCloseApp', + }, + + 'windows: deleteFolder': { + command: 'windowsDeleteFolder', + params: { + required: [ + 'remotePath', + ], + }, + }, + 'windows: deleteFile': { + command: 'windowsDeleteFile', + params: { + required: [ + 'remotePath', + ], + }, + }, + + + 'windows: click': { + command: 'windowsClick', + params: { + optional: [ + 'elementId', + 'x', + 'y', + 'button', + 'modifierKeys', + 'durationMs', + 'times', + 'interClickDelayMs', + ], + }, + }, + 'windows: scroll': { + command: 'windowsScroll', + params: { + optional: [ + 'elementId', + 'x', + 'y', + 'deltaX', + 'deltaY', + 'modifierKeys', + ], + }, + }, + 'windows: clickAndDrag': { + command: 'windowsClickAndDrag', + params: { + optional: [ + 'startElementId', + 'startX', + 'startY', + 'endElementId', + 'endX', + 'endY', + 'modifierKeys', + 'durationMs', + ], + }, + }, + 'windows: hover': { + command: 'windowsHover', + params: { + optional: [ + 'startElementId', + 'startX', + 'startY', + 'endElementId', + 'endX', + 'endY', + 'modifierKeys', + 'durationMs', + ], + }, + }, + 'windows: keys': { + command: 'windowsKeys', + params: { + required: [ + 'actions', + ], + }, + }, + + 'windows: setClipboard': { + command: 'windowsSetClipboard', + params: { + required: [ + 'b64Content', + ], + optional: [ + 'contentType', + ], + }, + }, + 'windows: getClipboard': { + command: 'windowsGetClipboard', + params: { + optional: [ + 'contentType', + ], + }, + }, +} as const satisfies ExecuteMethodMap; diff --git a/lib/utils.js b/lib/utils.js index 9e7c60e..d8d7036 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,8 +2,6 @@ import { util, net } from 'appium/support'; import { promisify } from 'node:util'; import { exec } from 'node:child_process'; import B from 'bluebird'; -import _ from 'lodash'; -import { errors } from 'appium/driver'; const execAsync = promisify(exec); @@ -27,23 +25,6 @@ export async function shellExec(cmd, args = [], opts = {}) { .timeout(timeoutMs, `The command '${fullCmd}' timed out after ${timeoutMs}ms`); } -/** - * Assert the presence of particular keys in the given object - * - * @template {Object} T - * @param {keyof T|(keyof T)[]} argNames one or more key names - * @param {T} opts the object to check - * @returns {T} the same given object - */ -export function requireArgs (argNames, opts) { - for (const argName of (_.isArray(argNames) ? argNames : [argNames])) { - if (!_.has(opts, argName)) { - throw new errors.InvalidArgumentError(`'${String(argName)}' argument must be provided`); - } - } - return opts; -} - /** * * @param {string} srcUrl diff --git a/test/e2e/commands/file-movement-e2e-specs.js b/test/e2e/commands/file-movement-e2e-specs.js index 9038afa..7fede08 100644 --- a/test/e2e/commands/file-movement-e2e-specs.js +++ b/test/e2e/commands/file-movement-e2e-specs.js @@ -2,21 +2,7 @@ import { remote as wdio } from 'webdriverio'; import path from 'path'; import { tempDir, fs } from 'appium/support'; import { isAdmin } from '../../../lib/installer'; -import { TEST_HOST, TEST_PORT } from '../constants'; - - -const TEST_CAPS = { - platformName: 'Windows', - 'appium:automationName': 'windows', - 'appium:app': 'Root', -}; - -const WDIO_OPTS = { - hostname: TEST_HOST, - port: TEST_PORT, - connectionRetryCount: 0, - capabilities: TEST_CAPS -}; +import { buildWdIoOptions } from '../helpers'; describe('file movement', function () { let driver; @@ -36,7 +22,7 @@ describe('file movement', function () { return this.skip(); } - driver = await wdio(WDIO_OPTS); + driver = await wdio(buildWdIoOptions('Root')); }); afterEach(async function () { diff --git a/test/e2e/commands/winapi-e2e-specs.js b/test/e2e/commands/winapi-e2e-specs.js index ff7f1f9..0ded684 100644 --- a/test/e2e/commands/winapi-e2e-specs.js +++ b/test/e2e/commands/winapi-e2e-specs.js @@ -1,9 +1,10 @@ -import { commands } from '../../../lib/commands/gestures'; -import { log } from '../../../lib/logger'; - +import { buildWdIoOptions } from '../helpers'; +import { remote as wdio } from 'webdriverio'; describe('winapi', function () { let chai; + /** @type {import('webdriverio').Browser} */ + let driver; before(async function () { chai = await import('chai'); @@ -11,16 +12,23 @@ describe('winapi', function () { chai.should(); chai.use(chaiAsPromised.default); - commands.log = log; + + driver = await wdio(buildWdIoOptions('Root')); }); - after(function () { - delete commands.log; + after(async function () { + try { + if (driver) { + await driver.deleteSession(); + } + } finally { + driver = null; + } }); describe('mouseClick', function () { it('performs single click with Shift+Ctrl', async function () { - await commands.windowsClick({ + await driver.execute('windows: click', { x: 100, y: 100, modifierKeys: ['shift', 'ctrl'], @@ -28,7 +36,7 @@ describe('winapi', function () { }); it('performs long click', async function () { - await commands.windowsClick({ + await driver.execute('windows: click', { x: 100, y: 100, durationMs: 500, @@ -36,7 +44,7 @@ describe('winapi', function () { }); it('performs double click', async function () { - await commands.windowsClick({ + await driver.execute('windows: click', { x: 100, y: 100, times: 2, @@ -44,7 +52,7 @@ describe('winapi', function () { }); it('performs context click', async function () { - await commands.windowsClick({ + await driver.execute('windows: click', { x: 100, y: 100, button: 'right', @@ -84,14 +92,14 @@ describe('winapi', function () { ]; for (const errData of errDatas) { - await commands.windowsClick(errData).should.be.rejected; + await driver.execute('windows: click', errData).should.be.rejected; } }); }); describe('mouseScroll', function () { it('performs vertical scroll gesture with Ctrl+Alt depressed', async function () { - await commands.windowsScroll({ + await driver.execute('windows: scroll', { x: 600, y: 300, deltaY: 200, @@ -100,7 +108,7 @@ describe('winapi', function () { }); it('performs horizontal scroll gesture', async function () { - await commands.windowsScroll({ + await driver.execute('windows: scroll', { x: 600, y: 300, deltaX: -200, @@ -108,7 +116,7 @@ describe('winapi', function () { }); it('does nothing if zero delta is provided', async function () { - await commands.windowsScroll({ + await driver.execute('windows: scroll', { x: 100, y: 100, deltaY: 0, @@ -136,14 +144,14 @@ describe('winapi', function () { ]; for (const errData of errDatas) { - await commands.windowsScroll(errData).should.be.rejected; + await driver.execute('windows: scroll', errData).should.be.rejected; } }); }); describe('mouseClickAndDrag', function () { it('performs drag gesture with Ctrl+Shift depressed', async function () { - await commands.windowsClickAndDrag({ + await driver.execute('windows: clickAndDrag', { startX: 600, startY: 300, endX: 500, @@ -155,7 +163,7 @@ describe('winapi', function () { describe('windowsHover', function () { it('performs hover gesture with Ctrl+Shift depressed', async function () { - await commands.windowsHover({ + await driver.execute('windows: clickAndDrag', { startX: 600, startY: 300, endX: 500, @@ -167,7 +175,7 @@ describe('winapi', function () { describe('keys', function () { it('performs complex key input', async function () { - await commands.windowsKeys({ + await driver.execute('windows: keys', { actions: [ {virtualKeyCode: 0x10, down: true}, {pause: 100}, @@ -199,7 +207,7 @@ describe('winapi', function () { ]; for (const errData of errDatas) { - await commands.windowsKeys(errData).should.be.rejected; + await driver.execute('windows: keys', {actions: [errData]}).should.be.rejected; } }); }); diff --git a/test/e2e/constants.js b/test/e2e/constants.js deleted file mode 100644 index 0ff0120..0000000 --- a/test/e2e/constants.js +++ /dev/null @@ -1,2 +0,0 @@ -export const TEST_PORT = parseInt(process.env.APPIUM_TEST_SERVER_PORT || 4788, 10); -export const TEST_HOST = process.env.APPIUM_TEST_SERVER_HOST || '127.0.0.1'; diff --git a/test/e2e/driver-e2e-specs.js b/test/e2e/driver-e2e-specs.js index 2a7762a..d2500a1 100644 --- a/test/e2e/driver-e2e-specs.js +++ b/test/e2e/driver-e2e-specs.js @@ -1,20 +1,6 @@ import { remote as wdio } from 'webdriverio'; import { isAdmin } from '../../lib/installer'; -import { TEST_HOST, TEST_PORT } from './constants'; - - -const TEST_CAPS = { - platformName: 'Windows', - 'appium:automationName': 'windows', - 'appium:app': 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App', -}; - -const WDIO_OPTS = { - hostname: TEST_HOST, - port: TEST_PORT, - connectionRetryCount: 0, - capabilities: TEST_CAPS -}; +import { buildWdIoOptions } from './helpers'; describe('Driver', function () { let driver; @@ -33,7 +19,7 @@ describe('Driver', function () { return this.skip(); } - driver = await wdio(WDIO_OPTS); + driver = await wdio(buildWdIoOptions('Microsoft.WindowsCalculator_8wekyb3d8bbwe!App')); }); afterEach(async function () { diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js new file mode 100644 index 0000000..e5b670e --- /dev/null +++ b/test/e2e/helpers.js @@ -0,0 +1,21 @@ +export const TEST_PORT = parseInt(process.env.APPIUM_TEST_SERVER_PORT || 4788, 10); +export const TEST_HOST = process.env.APPIUM_TEST_SERVER_HOST || '127.0.0.1'; + + +/** + * + * @param {string} app + * @returns {Record} + */ +export function buildWdIoOptions(app) { + return { + hostname: TEST_HOST, + port: TEST_PORT, + connectionRetryCount: 0, + capabilities: { + platformName: 'Windows', + 'appium:automationName': 'windows', + 'appium:app': app, + } + }; +}