diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 8c1cb2d..f8ef192 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -26,5 +26,7 @@ jobs: name: Install dev dependencies - run: npm run lint name: Run linter + - run: npm run format:check + name: Run Prettier check - run: npm run test name: Run unit tests diff --git a/lib/commands/app-management.js b/lib/commands/app-management.js index b6715b9..f86a9f3 100644 --- a/lib/commands/app-management.js +++ b/lib/commands/app-management.js @@ -12,10 +12,8 @@ * * @this {WindowsDriver} */ -export async function windowsLaunchApp () { - return await this.winAppDriver.sendCommand( - '/appium/app/launch', 'POST', {} - ); +export async function windowsLaunchApp() { + return await this.winAppDriver.sendCommand('/appium/app/launch', 'POST', {}); } /** @@ -30,10 +28,8 @@ export async function windowsLaunchApp () { * @this {WindowsDriver} * @throws {Error} if the app process is not running */ -export async function windowsCloseApp () { - return await this.winAppDriver.sendCommand( - '/appium/app/close', 'POST', {} - ); +export async function windowsCloseApp() { + return await this.winAppDriver.sendCommand('/appium/app/close', 'POST', {}); } /** diff --git a/lib/commands/clipboard.js b/lib/commands/clipboard.js index b9e6cae..0c0cf52 100644 --- a/lib/commands/clipboard.js +++ b/lib/commands/clipboard.js @@ -1,5 +1,5 @@ -import { exec } from 'teen_process'; -import { errors } from 'appium/driver'; +import {exec} from 'teen_process'; +import {errors} from 'appium/driver'; import _ from 'lodash'; /** @@ -23,29 +23,30 @@ const CONTENT_TYPE = Object.freeze({ * @param {string} b64Content base64-encoded clipboard content to set * @param {ContentTypeEnum} [contentType='text'] The clipboard content type to set */ -export async function windowsSetClipboard ( - b64Content, - contentType = CONTENT_TYPE.plaintext -) { +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`); + throw new errors.InvalidArgumentError( + `The 'b64Content' argument must be a valid base64-encoded string`, + ); } switch (contentType) { case CONTENT_TYPE.plaintext: - return await exec('powershell', ['-command', + return await exec('powershell', [ + '-command', `$str=[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64Content}'));`, - 'Set-Clipboard -Value $str' + 'Set-Clipboard -Value $str', ]); case CONTENT_TYPE.image: - return await exec('powershell', ['-command', + return await exec('powershell', [ + '-command', `$img=[Drawing.Bitmap]::FromStream([IO.MemoryStream][Convert]::FromBase64String('${b64Content}'));`, '[System.Windows.Forms.Clipboard]::SetImage($img);', - '$img.Dispose();' + '$img.Dispose();', ]); default: throw new errors.InvalidArgumentError( `The clipboard content type '${contentType}' is not known. ` + - `Only the following content types are supported: ${_.values(CONTENT_TYPE)}` + `Only the following content types are supported: ${_.values(CONTENT_TYPE)}`, ); } } @@ -60,29 +61,29 @@ export async function windowsSetClipboard ( * Only PNG images are supported for extraction if set to 'image'. * @returns {Promise} base64-encoded content of the clipboard */ -export async function windowsGetClipboard ( - contentType = CONTENT_TYPE.plaintext -) { +export async function windowsGetClipboard(contentType = CONTENT_TYPE.plaintext) { switch (contentType) { case CONTENT_TYPE.plaintext: { - const {stdout} = await exec('powershell', ['-command', + const {stdout} = await exec('powershell', [ + '-command', '$str=Get-Clipboard;', - '[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($str));' + '[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($str));', ]); return _.trim(stdout); } case CONTENT_TYPE.image: { - const {stdout} = await exec('powershell', ['-command', + const {stdout} = await exec('powershell', [ + '-command', '$s=New-Object System.IO.MemoryStream;', '[System.Windows.Forms.Clipboard]::GetImage().Save($s,[System.Drawing.Imaging.ImageFormat]::Png);', - '[System.Convert]::ToBase64String($s.ToArray());' + '[System.Convert]::ToBase64String($s.ToArray());', ]); return _.trim(stdout); } default: throw new errors.InvalidArgumentError( `The clipboard content type '${contentType}' is not known. ` + - `Only the following content types are supported: ${_.values(CONTENT_TYPE)}` + `Only the following content types are supported: ${_.values(CONTENT_TYPE)}`, ); } } diff --git a/lib/commands/context.ts b/lib/commands/context.ts index 3c42ec9..977836f 100644 --- a/lib/commands/context.ts +++ b/lib/commands/context.ts @@ -1,19 +1,19 @@ -import { errors } from 'appium/driver'; +import {errors} from 'appium/driver'; const WINDOWS_CONTEXT = 'NATIVE_APP'; -export async function getContexts (): Promise { +export async function getContexts(): Promise { return [WINDOWS_CONTEXT]; } -export async function getCurrentContext (): Promise { +export async function getCurrentContext(): Promise { return WINDOWS_CONTEXT; } -export async function setContext (context: string): Promise { +export async function setContext(context: string): Promise { if (context !== WINDOWS_CONTEXT) { throw new errors.NoSuchContextError( - `The Windows Driver only supports '${WINDOWS_CONTEXT}' context.` + `The Windows Driver only supports '${WINDOWS_CONTEXT}' context.`, ); } -} \ No newline at end of file +} diff --git a/lib/commands/execute.js b/lib/commands/execute.js index ef00c3c..efd600b 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { POWER_SHELL_FEATURE } from '../constants'; +import {POWER_SHELL_FEATURE} from '../constants'; const POWER_SHELL_SCRIPT = 'powerShell'; const EXECUTE_SCRIPT_PREFIX = 'windows:'; @@ -11,11 +11,13 @@ const EXECUTE_SCRIPT_PREFIX = 'windows:'; * @param {ExecuteMethodArgs} [args] * @returns {Promise} */ -export async function execute (script, args) { +export async function execute(script, args) { if (script === POWER_SHELL_SCRIPT) { this.assertFeatureEnabled(POWER_SHELL_FEATURE); return await this.execPowerShell( - /** @type {import('./powershell').ExecPowerShellOptions} */ (preprocessExecuteMethodArgs(args)) + /** @type {import('./powershell').ExecPowerShellOptions} */ ( + preprocessExecuteMethodArgs(args) + ), ); } diff --git a/lib/commands/file-movement.js b/lib/commands/file-movement.js index 0c54041..d0e16d9 100644 --- a/lib/commands/file-movement.js +++ b/lib/commands/file-movement.js @@ -1,16 +1,22 @@ import _ from 'lodash'; import path from 'node:path'; -import { errors } from 'appium/driver'; -import { fs, mkdirp, util, zip } from 'appium/support'; -import { MODIFY_FS_FEATURE } from '../constants'; +import {errors} from 'appium/driver'; +import {fs, mkdirp, util, zip} from 'appium/support'; +import {MODIFY_FS_FEATURE} from '../constants'; // List of env variables, that can be expanded in path const KNOWN_ENV_VARS = [ - 'APPDATA', 'LOCALAPPDATA', - 'PROGRAMFILES', 'PROGRAMFILES(X86)', - 'PROGRAMDATA', 'ALLUSERSPROFILE', - 'TEMP', 'TMP', - 'HOMEPATH', 'USERPROFILE', 'PUBLIC' + 'APPDATA', + 'LOCALAPPDATA', + 'PROGRAMFILES', + 'PROGRAMFILES(X86)', + 'PROGRAMDATA', + 'ALLUSERSPROFILE', + 'TEMP', + 'TMP', + 'HOMEPATH', + 'USERPROFILE', + 'PUBLIC', ]; /** @@ -20,12 +26,13 @@ const KNOWN_ENV_VARS = [ * @param {string} base64Data * @returns {Promise} */ -export async function pushFile (remotePath, base64Data) { +export async function pushFile(remotePath, base64Data) { this.assertFeatureEnabled(MODIFY_FS_FEATURE); if (remotePath.endsWith(path.sep)) { throw new errors.InvalidArgumentError( 'It is expected that remote path points to a file rather than a folder. ' + - `'${remotePath}' is given instead`); + `'${remotePath}' is given instead`, + ); } if (_.isArray(base64Data)) { @@ -46,7 +53,7 @@ export async function pushFile (remotePath, base64Data) { * @param {string} remotePath * @returns {Promise} */ -export async function pullFile (remotePath) { +export async function pullFile(remotePath) { const fullPath = resolveToAbsolutePath(remotePath); await checkFileExists(fullPath); return (await util.toInMemoryBase64(fullPath)).toString(); @@ -58,12 +65,14 @@ export async function pullFile (remotePath) { * @param {string} remotePath * @returns {Promise} */ -export async function pullFolder (remotePath) { +export async function pullFolder(remotePath) { const fullPath = resolveToAbsolutePath(remotePath); await checkFolderExists(fullPath); - return (await zip.toInMemoryZip(fullPath, { - encodeToBase64: true, - })).toString(); + return ( + await zip.toInMemoryZip(fullPath, { + encodeToBase64: true, + }) + ).toString(); } /** @@ -78,7 +87,7 @@ export async function pullFolder (remotePath) { * @throws {InvalidArgumentError} If the file to be deleted does not exist or * remote path is not an absolute path. */ -export async function windowsDeleteFile (remotePath) { +export async function windowsDeleteFile(remotePath) { this.assertFeatureEnabled(MODIFY_FS_FEATURE); const fullPath = resolveToAbsolutePath(remotePath); await checkFileExists(fullPath); @@ -97,7 +106,7 @@ export async function windowsDeleteFile (remotePath) { * @throws {InvalidArgumentError} If the folder to be deleted does not exist or * remote path is not an absolute path. */ -export async function windowsDeleteFolder (remotePath) { +export async function windowsDeleteFolder(remotePath) { this.assertFeatureEnabled(MODIFY_FS_FEATURE); const fullPath = resolveToAbsolutePath(remotePath); await checkFolderExists(fullPath); @@ -109,17 +118,17 @@ export async function windowsDeleteFolder (remotePath) { * @param {string} remotePath * @returns {string} */ -function resolveToAbsolutePath (remotePath) { - const resolvedPath = remotePath.replace( - /%([^%]+)%/g, - (_, key) => KNOWN_ENV_VARS.includes(key.toUpperCase()) +function resolveToAbsolutePath(remotePath) { + const resolvedPath = remotePath.replace(/%([^%]+)%/g, (_, key) => + KNOWN_ENV_VARS.includes(key.toUpperCase()) ? /** @type {string} */ (process.env[key.toUpperCase()]) - : `%${key}%` + : `%${key}%`, ); if (!path.isAbsolute(resolvedPath)) { - throw new errors.InvalidArgumentError('It is expected that remote path is absolute. ' + - `'${resolvedPath}' is given instead`); + throw new errors.InvalidArgumentError( + 'It is expected that remote path is absolute. ' + `'${resolvedPath}' is given instead`, + ); } return resolvedPath; } @@ -129,15 +138,16 @@ function resolveToAbsolutePath (remotePath) { * @param {string} remotePath * @returns {Promise} */ -async function checkFileExists (remotePath) { - if (!await fs.exists(remotePath)) { +async function checkFileExists(remotePath) { + if (!(await fs.exists(remotePath))) { throw new errors.InvalidArgumentError(`The remote file '${remotePath}' does not exist.`); } const stat = await fs.stat(remotePath); if (!stat.isFile()) { throw new errors.InvalidArgumentError( 'It is expected that remote path points to a file rather than a folder. ' + - `'${remotePath}' is given instead`); + `'${remotePath}' is given instead`, + ); } } @@ -146,18 +156,19 @@ async function checkFileExists (remotePath) { * @param {string} remotePath * @returns {Promise} */ -async function checkFolderExists (remotePath) { - if (!await fs.exists(remotePath)) { +async function checkFolderExists(remotePath) { + if (!(await fs.exists(remotePath))) { throw new errors.InvalidArgumentError(`The remote folder '${remotePath}' does not exist.`); } const stat = await fs.stat(remotePath); if (!stat.isDirectory()) { throw new errors.InvalidArgumentError( 'It is expected that remote path points to a folder rather than a file. ' + - `'${remotePath}' is given instead`); + `'${remotePath}' is given instead`, + ); } } /** * @typedef {import('../driver').WindowsDriver} WindowsDriver - */ \ No newline at end of file + */ diff --git a/lib/commands/find.js b/lib/commands/find.js index 1151a1e..497d353 100644 --- a/lib/commands/find.js +++ b/lib/commands/find.js @@ -1,4 +1,4 @@ -import { util } from 'appium/support'; +import {util} from 'appium/support'; /** * @@ -9,7 +9,7 @@ import { util } from 'appium/support'; * @param {string} [context] * @returns */ -export async function findElOrEls (strategy, selector, mult, context) { +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', { diff --git a/lib/commands/general.js b/lib/commands/general.js index b2a0bd1..ee8ab15 100644 --- a/lib/commands/general.js +++ b/lib/commands/general.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { util } from 'appium/support'; +import {util} from 'appium/support'; /** * @typedef {Object} Size @@ -11,9 +11,10 @@ import { util } from 'appium/support'; * @this {WindowsDriver} * @returns {Promise} */ -async function getScreenSize () { +async function getScreenSize() { const dimensions = await this.execPowerShell({ - command: 'Add-Type -AssemblyName System.Windows.Forms;[System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size', + command: + 'Add-Type -AssemblyName System.Windows.Forms;[System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Size', }); this.log.debug(`Screen size information retrieved: ${dimensions}`); const match = /^\s*(True|False)\s+(\d+)\s+(\d+)/m.exec(dimensions); @@ -32,10 +33,8 @@ async function getScreenSize () { * @this {WindowsDriver} * @returns {Promise} */ -export 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 /** @type {Size} */ (size); } @@ -43,28 +42,24 @@ export async function getWindowSize () { this.log.info('Cannot retrieve window size from WinAppDriver'); this.log.info('Falling back to Windows Forms to calculate dimensions'); return await getScreenSize.bind(this)(); -}; +} // a workaround for https://github.com/appium/appium/issues/15923 /** * @this {WindowsDriver} * @returns {Promise} */ -export async function getWindowRect () { +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' - ); + const handle = await this.winAppDriver.sendCommand('/window_handle', 'GET'); ({x, y} = /** @type {import('@appium/types').Position} */ ( - await this.winAppDriver.sendCommand( - `/window/${handle}/position`, 'GET' - )) - ); + 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}` + `Cannot fetch the window position. Defaulting to zeroes. Original error: ${e.message}`, ); } return {x, y, width, height}; @@ -79,21 +74,15 @@ export async function getWindowRect () { * @param {number} height * @returns {Promise} */ -export async function setWindowRect (x, y, width, height) { +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) { @@ -106,14 +95,11 @@ export async function setWindowRect (x, y, width, height) { * @this {WindowsDriver} * @returns {Promise} */ -export async function getScreenshot () { +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 - const originalPayload = await this.winAppDriver.sendCommand( - '/screenshot', 'GET' - ); - return Buffer.from(/** @type {string} */ (originalPayload), 'base64') - .toString('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 @@ -123,15 +109,13 @@ export async function getScreenshot () { * @param {string} el * @returns {Promise} */ -export async function getElementRect (el) { +export async function getElementRect(el) { const elId = util.unwrapElement(el); 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' - ) + await this.winAppDriver.sendCommand(`/element/${elId}/size`, 'GET') ); return {x, y, width, height}; } diff --git a/lib/commands/gestures.js b/lib/commands/gestures.js index bc36dba..79cec13 100644 --- a/lib/commands/gestures.js +++ b/lib/commands/gestures.js @@ -14,14 +14,13 @@ import { getVirtualScreenSize, ensureDpiAwareness as _ensureDpiAwareness, } from './winapi/user32'; -import { errors } from 'appium/driver'; +import {errors} from 'appium/driver'; import B from 'bluebird'; -import { util } from 'appium/support'; -import { isInvalidArgumentError } from './winapi/errors'; +import {util} from 'appium/support'; +import {isInvalidArgumentError} from './winapi/errors'; const EVENT_INJECTION_DELAY_MS = 5; - function preprocessError(e) { if (!isInvalidArgumentError(e)) { return e; @@ -32,7 +31,6 @@ function preprocessError(e) { return err; } - function modifierKeysToInputs(modifierKeys) { if (_.isEmpty(modifierKeys)) { return [[], []]; @@ -63,31 +61,37 @@ async function toAbsoluteCoordinates(elementId, x, y, msgPrefix = '') { if (!elementId && !hasX && !hasY) { throw new errors.InvalidArgumentError( - `${msgPrefix}Either element identifier or absolute coordinates must be provided` + `${msgPrefix}Either element identifier or absolute coordinates must be provided`, ); } if (!elementId) { if (!hasX || !hasY) { throw new errors.InvalidArgumentError( - `${msgPrefix}Both absolute coordinates must be provided` + `${msgPrefix}Both absolute coordinates must be provided`, ); } this.log.debug(`${msgPrefix}Absolute coordinates: (${x}, ${y})`); return [x, y]; } - if (hasX && !hasY || !hasX && hasY) { + if ((hasX && !hasY) || (!hasX && hasY)) { throw new errors.InvalidArgumentError( - `${msgPrefix}Both relative element coordinates must be provided` + `${msgPrefix}Both relative element coordinates must be provided`, ); } let absoluteX = x; let absoluteY = y; - const {x: left, y: top} = await this.winAppDriver.sendCommand(`/element/${elementId}/location`, 'GET'); + const {x: left, y: top} = await this.winAppDriver.sendCommand( + `/element/${elementId}/location`, + 'GET', + ); if (!hasX && !hasY) { - const {width, height} = await this.winAppDriver.sendCommand(`/element/${elementId}/size`, 'GET'); + const {width, height} = await this.winAppDriver.sendCommand( + `/element/${elementId}/size`, + 'GET', + ); absoluteX = left + Math.trunc(width / 2); absoluteY = top + Math.trunc(height / 2); } else { @@ -107,7 +111,7 @@ function isKeyDown(action) { return true; default: throw new errors.InvalidArgumentError( - `Key action '${action}' is unknown. Only ${_.values(KEY_ACTION)} actions are supported` + `Key action '${action}' is unknown. Only ${_.values(KEY_ACTION)} actions are supported`, ); } } @@ -123,7 +127,7 @@ function isKeyDown(action) { function toModifierInputs(modifierKeys, action) { const events = []; const usedKeys = new Set(); - for (const keyName of (_.isArray(modifierKeys) ? modifierKeys : [modifierKeys])) { + for (const keyName of _.isArray(modifierKeys) ? modifierKeys : [modifierKeys]) { const lowerKeyName = _.toLower(keyName); if (usedKeys.has(lowerKeyName)) { continue; @@ -132,7 +136,7 @@ function toModifierInputs(modifierKeys, action) { const virtualKeyCode = MODIFIER_KEY[lowerKeyName]; if (_.isUndefined(virtualKeyCode)) { throw new errors.InvalidArgumentError( - `Modifier key name '${keyName}' is unknown. Supported key names are: ${_.keys(MODIFIER_KEY)}` + `Modifier key name '${keyName}' is unknown. Supported key names are: ${_.keys(MODIFIER_KEY)}`, ); } events.push({virtualKeyCode, action}); @@ -146,11 +150,7 @@ function toModifierInputs(modifierKeys, action) { .map(createKeyInput); } -const KEY_ACTION_PROPERTIES = [ - 'pause', - 'text', - 'virtualKeyCode', -]; +const KEY_ACTION_PROPERTIES = ['pause', 'text', 'virtualKeyCode']; /** * @param {KeyAction} action @@ -166,26 +166,21 @@ function parseKeyAction(action, index) { const actionPrefix = `Key Action #${index + 1} (${JSON.stringify(action)}): `; if (definedPropertiesCount === 0) { throw new errors.InvalidArgumentError( - `${actionPrefix}Some key action (${KEY_ACTION_PROPERTIES.join(' or ')}) must be defined` + `${actionPrefix}Some key action (${KEY_ACTION_PROPERTIES.join(' or ')}) must be defined`, ); } else if (definedPropertiesCount > 1) { throw new errors.InvalidArgumentError( - `${actionPrefix}Only one key action (${KEY_ACTION_PROPERTIES.join(' or ')}) must be defined` + `${actionPrefix}Only one key action (${KEY_ACTION_PROPERTIES.join(' or ')}) must be defined`, ); } - const { - pause, - text, - virtualKeyCode, - down, - } = action; + const {pause, text, virtualKeyCode, down} = action; if (hasPause) { const durationMs = pause; if (!_.isInteger(durationMs) || durationMs < 0) { throw new errors.InvalidArgumentError( - `${actionPrefix}Pause value must be a valid positive integer number of milliseconds` + `${actionPrefix}Pause value must be a valid positive integer number of milliseconds`, ); } return durationMs; @@ -193,7 +188,7 @@ function parseKeyAction(action, index) { if (hasText) { if (!_.isString(text) || _.isEmpty(text)) { throw new errors.InvalidArgumentError( - `${actionPrefix}Text value must be a valid non-empty string` + `${actionPrefix}Text value must be a valid non-empty string`, ); } return toUnicodeKeyInputs(text); @@ -203,15 +198,17 @@ function parseKeyAction(action, index) { if (_.has(action, 'down')) { if (!_.isBoolean(down)) { throw new errors.InvalidArgumentError( - `${actionPrefix}The down argument must be of type boolean if provided` + `${actionPrefix}The down argument must be of type boolean if provided`, ); } // only depress or release the key if `down` is provided - return [createKeyInput({ - wVk: virtualKeyCode, - dwFlags: down ? 0 : KEYEVENTF_KEYUP, - })]; + return [ + createKeyInput({ + wVk: virtualKeyCode, + dwFlags: down ? 0 : KEYEVENTF_KEYUP, + }), + ]; } // otherwise just press the key return [ @@ -279,7 +276,7 @@ function parseKeyActions(actions) { * click gesture. Only makes sense if `times` is greater than one. * @throws {Error} If given options are not acceptable or the gesture has failed. */ -export async function windowsClick ( +export async function windowsClick( elementId, x, y, @@ -291,7 +288,8 @@ export async function windowsClick ( ) { await ensureDpiAwareness.bind(this)(); - const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys); + const [modifierKeyDownInputs, modifierKeyUpInputs] = + modifierKeysToInputs.bind(this)(modifierKeys); const [absoluteX, absoluteY] = await toAbsoluteCoordinates.bind(this)(elementId, x, y); let clickDownInput; let clickUpInput; @@ -332,7 +330,7 @@ export async function windowsClick ( await handleInputs(modifierKeyUpInputs); } } -}; +} /** * Performs horizontal or vertical scrolling with mouse wheel. @@ -357,26 +355,17 @@ export async function windowsClick ( * ['ctrl', 'alt'] * @throws {Error} If given options are not acceptable or the gesture has failed. */ -export async function windowsScroll ( - elementId, - x, - y, - deltaX, - deltaY, - modifierKeys, -) { +export async function windowsScroll(elementId, x, y, deltaX, deltaY, modifierKeys) { await ensureDpiAwareness.bind(this)(); - const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(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(absoluteX, absoluteY); - scrollInput = toMouseWheelInput( - /** @type {number} */ (deltaX), - /** @type {number} */ (deltaY), - ); + scrollInput = toMouseWheelInput(/** @type {number} */ (deltaX), /** @type {number} */ (deltaY)); } catch (e) { throw preprocessError(e); } @@ -388,8 +377,10 @@ export async function windowsScroll ( if (scrollInput) { await handleInputs(scrollInput); } else { - this.log.info('There is no need to actually perform scroll with the given ' + - (_.isNil(deltaX) ? 'deltaY' : 'deltaX')); + this.log.info( + 'There is no need to actually perform scroll with the given ' + + (_.isNil(deltaX) ? 'deltaY' : 'deltaX'), + ); } } finally { if (!_.isEmpty(modifierKeyUpInputs)) { @@ -428,18 +419,21 @@ export async function windowsScroll ( * the left mouse button and moving the cursor to the ending drag point. * @throws {Error} If given options are not acceptable or the gesture has failed. */ -export async function windowsClickAndDrag ( +export async function windowsClickAndDrag( startElementId, - startX, startY, + startX, + startY, endElementId, - endX, endY, + endX, + endY, modifierKeys, durationMs = 5000, ) { await ensureDpiAwareness.bind(this)(); const screenSize = await getVirtualScreenSize(); - const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys); + const [modifierKeyDownInputs, modifierKeyUpInputs] = + modifierKeysToInputs.bind(this)(modifierKeys); const [[startAbsoluteX, startAbsoluteY], [endAbsoluteX, endAbsoluteY]] = await B.all([ toAbsoluteCoordinates.bind(this)(startElementId, startX, startY, 'Starting drag point'), toAbsoluteCoordinates.bind(this)(endElementId, endX, endY, 'Ending drag point'), @@ -508,18 +502,21 @@ export async function windowsClickAndDrag ( * moving the cursor from the starting to the ending hover point. * @throws {Error} If given options are not acceptable or the gesture has failed. */ -export async function windowsHover ( +export async function windowsHover( startElementId, - startX, startY, + startX, + startY, endElementId, - endX, endY, + endX, + endY, modifierKeys, durationMs = 500, ) { await ensureDpiAwareness.bind(this)(); const screenSize = await getVirtualScreenSize(); - const [modifierKeyDownInputs, modifierKeyUpInputs] = modifierKeysToInputs.bind(this)(modifierKeys); + const [modifierKeyDownInputs, modifierKeyUpInputs] = + modifierKeysToInputs.bind(this)(modifierKeys); const [[startAbsoluteX, startAbsoluteY], [endAbsoluteX, endAbsoluteY]] = await B.all([ toAbsoluteCoordinates.bind(this)(startElementId, startX, startY, 'Starting hover point'), toAbsoluteCoordinates.bind(this)(endElementId, endX, endY, 'Ending hover point'), @@ -529,11 +526,13 @@ export async function windowsHover ( const inputPromisesChunk = []; const maxChunkSize = 10; for (let step = 0; step <= stepsCount; ++step) { - const promise = B.resolve(toMouseMoveInput( - startAbsoluteX + Math.trunc((endAbsoluteX - startAbsoluteX) * step / stepsCount), - startAbsoluteY + Math.trunc((endAbsoluteY - startAbsoluteY) * step / stepsCount), - screenSize - )); + const promise = B.resolve( + toMouseMoveInput( + startAbsoluteX + Math.trunc(((endAbsoluteX - startAbsoluteX) * step) / stepsCount), + startAbsoluteY + Math.trunc(((endAbsoluteY - startAbsoluteY) * step) / stepsCount), + screenSize, + ), + ); inputPromises.push(promise); // This is needed to avoid 'Error: Too many asynchronous calls are running' inputPromisesChunk.push(promise); @@ -588,7 +587,7 @@ export async function windowsHover ( * @param {KeyAction[] | KeyAction} actions One or more key actions. * @throws {Error} If given options are not acceptable or the gesture has failed. */ -export async function windowsKeys (actions) { +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) { @@ -605,10 +604,10 @@ export async function windowsKeys (actions) { * @returns {Promise} */ async function ensureDpiAwareness() { - if (!await _ensureDpiAwareness()) { + if (!(await _ensureDpiAwareness())) { this.log.info( `The call to SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) API has failed. ` + - `Mouse cursor coordinates calculation for scaled displays might not work as expected.` + `Mouse cursor coordinates calculation for scaled displays might not work as expected.`, ); } } diff --git a/lib/commands/log.ts b/lib/commands/log.ts index b86f735..705e68c 100644 --- a/lib/commands/log.ts +++ b/lib/commands/log.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; -import type { WindowsDriver } from '../driver'; -import type { LogDefRecord, StringRecord } from '@appium/types'; +import type {WindowsDriver} from '../driver'; +import type {LogDefRecord, StringRecord} from '@appium/types'; const COLOR_CODE_PATTERN = /\u001b\[(\d+(;\d+)*)?m/g; // eslint-disable-line no-control-regex const GET_SERVER_LOGS_FEATURE = 'get_server_logs'; @@ -18,18 +18,15 @@ export const supportedLogTypes: LogDefRecord = { function nativeLogEntryToSeleniumEntry(x: StringRecord): LogEntry { const msg = _.isEmpty(x.prefix) ? x.message : `[${x.prefix}] ${x.message}`; - return toLogEntry( - _.replace(msg, COLOR_CODE_PATTERN, ''), - x.timestamp ?? Date.now() - ); + return toLogEntry(_.replace(msg, COLOR_CODE_PATTERN, ''), x.timestamp ?? Date.now()); } function toLogEntry( message: string, timestamp: number, - level: string = DEFAULT_LOG_LEVEL + level: string = DEFAULT_LOG_LEVEL, ): LogEntry { - return { timestamp, level, message }; + return {timestamp, level, message}; } interface LogEntry { diff --git a/lib/commands/powershell.js b/lib/commands/powershell.js index e604131..d9821dd 100644 --- a/lib/commands/powershell.js +++ b/lib/commands/powershell.js @@ -1,6 +1,6 @@ import _ from 'lodash'; -import { fs, tempDir } from 'appium/support'; -import { exec } from 'teen_process'; +import {fs, tempDir} from 'appium/support'; +import {exec} from 'teen_process'; import path from 'node:path'; import B from 'bluebird'; @@ -11,7 +11,6 @@ const EXECUTION_POLICY = { }; const POWER_SHELL = 'powershell.exe'; - /** * @typedef {Object} ExecPowerShellOptions * @property {string} [script] A valid Power Shell script to execute @@ -34,11 +33,8 @@ const POWER_SHELL = 'powershell.exe'; * @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. */ -export async function execPowerShell (opts) { - const { - script, - command, - } = opts ?? {}; +export async function execPowerShell(opts) { + const {script, command} = opts ?? {}; if (!script && !command) { throw this.log.errorWithException('Power Shell script/command must not be empty'); } @@ -61,13 +57,19 @@ export async function execPowerShell (opts) { if (command) { psArgs.push('-command', command); } else { - const {stdout} = await exec(POWER_SHELL, ['-command', 'Get-ExecutionPolicy -Scope CurrentUser']); + const {stdout} = await exec(POWER_SHELL, [ + '-command', + 'Get-ExecutionPolicy -Scope CurrentUser', + ]); userExecutionPolicy = _.trim(stdout); if ([EXECUTION_POLICY.RESTRICTED, EXECUTION_POLICY.UNDEFINED].includes(userExecutionPolicy)) { - this.log.debug(`Temporarily changing Power Shell execution policy to ${EXECUTION_POLICY.REMOTE_SIGNED} ` + - 'to run the given script'); + this.log.debug( + `Temporarily changing Power Shell execution policy to ${EXECUTION_POLICY.REMOTE_SIGNED} ` + + 'to run the given script', + ); await exec(POWER_SHELL, [ - '-command', `Set-ExecutionPolicy -ExecutionPolicy ${EXECUTION_POLICY.REMOTE_SIGNED} -Scope CurrentUser` + '-command', + `Set-ExecutionPolicy -ExecutionPolicy ${EXECUTION_POLICY.REMOTE_SIGNED} -Scope CurrentUser`, ]); } else { // There is no need to change the policy, scripts are allowed @@ -89,7 +91,8 @@ export async function execPowerShell (opts) { (async () => { if (userExecutionPolicy) { await exec(POWER_SHELL, [ - '-command', `Set-ExecutionPolicy -ExecutionPolicy ${userExecutionPolicy} -Scope CurrentUser` + '-command', + `Set-ExecutionPolicy -ExecutionPolicy ${userExecutionPolicy} -Scope CurrentUser`, ]); } })(), @@ -97,7 +100,7 @@ export async function execPowerShell (opts) { if (tmpRoot) { await fs.rimraf(tmpRoot); } - })() + })(), ]); } } diff --git a/lib/commands/record-screen.js b/lib/commands/record-screen.js index 72034a3..37498a4 100644 --- a/lib/commands/record-screen.js +++ b/lib/commands/record-screen.js @@ -1,7 +1,7 @@ import _ from 'lodash'; -import { waitForCondition } from 'asyncbox'; -import { util, fs, net, system, tempDir } from 'appium/support'; -import { SubProcess } from 'teen_process'; +import {waitForCondition} from 'asyncbox'; +import {util, fs, net, system, tempDir} from 'appium/support'; +import {SubProcess} from 'teen_process'; import B from 'bluebird'; const RETRY_PAUSE = 300; @@ -13,7 +13,6 @@ const FFMPEG_BINARY = `ffmpeg${system.isWindows() ? '.exe' : ''}`; const DEFAULT_FPS = 15; const DEFAULT_PRESET = 'veryfast'; - /** * * @param {string} localFile @@ -21,7 +20,7 @@ const DEFAULT_PRESET = 'veryfast'; * @param {Object} uploadOptions * @returns {Promise} */ -async function uploadRecordedMedia (localFile, remotePath = null, uploadOptions = {}) { +async function uploadRecordedMedia(localFile, remotePath = null, uploadOptions = {}) { if (_.isEmpty(remotePath) || !remotePath) { return (await util.toInMemoryBase64(localFile)).toString(); } @@ -40,12 +39,13 @@ async function uploadRecordedMedia (localFile, remotePath = null, uploadOptions return ''; } -async function requireFfmpegPath () { +async function requireFfmpegPath() { try { return await fs.which(FFMPEG_BINARY); } catch { - throw new Error(`${FFMPEG_BINARY} has not been found in PATH. ` + - `Please make sure it is installed`); + throw new Error( + `${FFMPEG_BINARY} has not been found in PATH. ` + `Please make sure it is installed`, + ); } } @@ -55,30 +55,28 @@ export class ScreenRecorder { * @param {import('@appium/types').AppiumLogger} log * @param {import('@appium/types').StringRecord} opts */ - constructor (videoPath, log, opts = {}) { + constructor(videoPath, log, opts = {}) { this.log = log; this._videoPath = videoPath; this._process = null; - this._fps = (opts.fps && opts.fps > 0) ? opts.fps : DEFAULT_FPS; + this._fps = opts.fps && opts.fps > 0 ? opts.fps : DEFAULT_FPS; this._audioInput = opts.audioInput; this._captureCursor = opts.captureCursor; this._captureClicks = opts.captureClicks; this._preset = opts.preset || DEFAULT_PRESET; this._videoFilter = opts.videoFilter; - this._timeLimit = (opts.timeLimit && opts.timeLimit > 0) - ? opts.timeLimit - : DEFAULT_TIME_LIMIT; + this._timeLimit = opts.timeLimit && opts.timeLimit > 0 ? opts.timeLimit : DEFAULT_TIME_LIMIT; } - async getVideoPath () { + async getVideoPath() { return (await fs.exists(this._videoPath)) ? this._videoPath : ''; } - isRunning () { - return !!(this._process?.isRunning); + isRunning() { + return !!this._process?.isRunning; } - async _enforceTermination () { + async _enforceTermination() { if (this._process && this.isRunning()) { this.log.debug('Force-stopping the currently running video recording'); try { @@ -93,34 +91,43 @@ export class ScreenRecorder { return ''; } - async start () { + async start() { const ffmpeg = await requireFfmpegPath(); const args = [ - '-loglevel', 'error', - '-t', `${this._timeLimit}`, - '-f', 'gdigrab', + '-loglevel', + 'error', + '-t', + `${this._timeLimit}`, + '-f', + 'gdigrab', ...(this._captureCursor ? ['-capture_cursor', '1'] : []), ...(this._captureClicks ? ['-capture_mouse_clicks', '1'] : []), - '-framerate', `${this._fps}`, - '-i', 'desktop', + '-framerate', + `${this._fps}`, + '-i', + 'desktop', ...(this._audioInput ? ['-f', 'dshow', '-i', `audio=${this._audioInput}`] : []), - '-vcodec', 'libx264', - '-preset', this._preset, - '-tune', 'zerolatency', - '-pix_fmt', 'yuv420p', - '-movflags', '+faststart', - '-fflags', 'nobuffer', - '-f', DEFAULT_EXT, - '-r', `${this._fps}`, + '-vcodec', + 'libx264', + '-preset', + this._preset, + '-tune', + 'zerolatency', + '-pix_fmt', + 'yuv420p', + '-movflags', + '+faststart', + '-fflags', + 'nobuffer', + '-f', + DEFAULT_EXT, + '-r', + `${this._fps}`, ...(this._videoFilter ? ['-filter:v', this._videoFilter] : []), ]; - const fullCmd = [ - ffmpeg, - ...args, - this._videoPath, - ]; + const fullCmd = [ffmpeg, ...args, this._videoPath]; this._process = new SubProcess(fullCmd[0], fullCmd.slice(1), { windowsHide: true, }); @@ -141,29 +148,34 @@ export class ScreenRecorder { }); await this._process.start(0); try { - await waitForCondition(async () => { - if (await this.getVideoPath()) { - return true; - } - if (!this._process) { - throw new Error(`${FFMPEG_BINARY} process died unexpectedly`); - } - return false; - }, { - waitMs: RETRY_TIMEOUT, - intervalMs: RETRY_PAUSE, - }); + await waitForCondition( + async () => { + if (await this.getVideoPath()) { + return true; + } + if (!this._process) { + throw new Error(`${FFMPEG_BINARY} process died unexpectedly`); + } + return false; + }, + { + waitMs: RETRY_TIMEOUT, + intervalMs: RETRY_PAUSE, + }, + ); } catch { await this._enforceTermination(); throw this.log.errorWithException( `The expected screen record file '${this._videoPath}' does not exist. ` + - `Check the server log for more details` + `Check the server log for more details`, ); } - this.log.info(`The video recording has started. Will timeout in ${util.pluralize('second', this._timeLimit, true)}`); + this.log.info( + `The video recording has started. Will timeout in ${util.pluralize('second', this._timeLimit, true)}`, + ); } - async stop (force = false) { + async stop(force = false) { if (force) { return await this._enforceTermination(); } @@ -176,7 +188,9 @@ export class ScreenRecorder { return new B((resolve, reject) => { const timer = setTimeout(async () => { await this._enforceTermination(); - reject(new Error(`Screen recording has failed to exit after ${PROCESS_SHUTDOWN_TIMEOUT}ms`)); + reject( + new Error(`Screen recording has failed to exit after ${PROCESS_SHUTDOWN_TIMEOUT}ms`), + ); }, PROCESS_SHUTDOWN_TIMEOUT); this._process?.once('exit', async (code, signal) => { @@ -234,7 +248,7 @@ export class ScreenRecorder { * (`false`) or to start a new recording immediately and terminate the existing one if running (`true`). * @throws {Error} If screen recording has failed to start or is not supported on the device under test. */ -export async function windowsStartRecordingScreen ( +export async function windowsStartRecordingScreen( timeLimit, videoFilter, fps, @@ -304,7 +318,7 @@ export async function windowsStartRecordingScreen ( * or the file content cannot be uploaded to the remote location * or screen recording is not supported on the device under test. */ -export async function windowsStopRecordingScreen ( +export async function windowsStopRecordingScreen( remotePath, user, pass, @@ -326,7 +340,9 @@ export async function windowsStopRecordingScreen ( } if (_.isEmpty(remotePath)) { const {size} = await fs.stat(videoPath); - this.log.debug(`The size of the resulting screen recording is ${util.toReadableSizeString(size)}`); + this.log.debug( + `The size of the resulting screen recording is ${util.toReadableSizeString(size)}`, + ); } return await uploadRecordedMedia(videoPath, remotePath, { user, @@ -368,7 +384,7 @@ export async function startRecordingScreen(options = {}) { captureCursor, captureClicks, audioInput, - forceRestart + forceRestart, ); } @@ -394,7 +410,7 @@ export async function stopRecordingScreen(options = {}) { method, headers, fileFieldName, - formFields + formFields, ); } @@ -431,4 +447,4 @@ export async function stopRecordingScreen(options = {}) { * @property {Object} [headers] * @property {string} [fileFieldName='file'] * @property {Object[]|[string, string][]} [formFields] - */ \ No newline at end of file + */ diff --git a/lib/commands/touch.js b/lib/commands/touch.js index 47ec604..fbfd3b6 100644 --- a/lib/commands/touch.js +++ b/lib/commands/touch.js @@ -1,4 +1,3 @@ - //This is needed to make clicks on -image elements work properly /** * @@ -6,10 +5,8 @@ * @param {any} actions * @returns {Promise} */ -export async function performActions (actions) { - return await this.winAppDriver.sendCommand( - '/actions', 'POST', {actions} - ); +export async function performActions(actions) { + return await this.winAppDriver.sendCommand('/actions', 'POST', {actions}); } /** diff --git a/lib/commands/winapi/user32.js b/lib/commands/winapi/user32.js index bc37e62..5c3687e 100644 --- a/lib/commands/winapi/user32.js +++ b/lib/commands/winapi/user32.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import B from 'bluebird'; -import { createInvalidArgumentError } from './errors'; -import { util } from 'appium/support'; +import {createInvalidArgumentError} from './errors'; +import {util} from 'appium/support'; import nodeUtil from 'node:util'; let /** @type {import('koffi')|undefined} */ ffi; @@ -18,14 +18,17 @@ try { UnionType = ffi?.union; } catch {} -const NATIVE_LIBS_LOAD_ERROR = `Native Windows API calls cannot be invoked. ` + +const NATIVE_LIBS_LOAD_ERROR = + `Native Windows API calls cannot be invoked. ` + `Please make sure you have the latest version of Visual Studio` + `including the "Desktop development with C++" workload. ` + `Afterwards reinstall the Windows driver.`; function requireNativeType(typ) { return _.isNil(typ) - ? () => () => { throw new Error(NATIVE_LIBS_LOAD_ERROR); } + ? () => () => { + throw new Error(NATIVE_LIBS_LOAD_ERROR); + } : typ; } @@ -36,14 +39,16 @@ const getUser32 = _.memoize(function getUser32() { const user32 = ffi.load('user32.dll'); return { SendInput: nodeUtil.promisify( - user32.func('unsigned int __stdcall SendInput(unsigned int cInputs, INPUT *pInputs, int cbSize)').async + user32.func( + 'unsigned int __stdcall SendInput(unsigned int cInputs, INPUT *pInputs, int cbSize)', + ).async, ), GetSystemMetrics: nodeUtil.promisify( - user32.func('int __stdcall GetSystemMetrics(int nIndex)').async + user32.func('int __stdcall GetSystemMetrics(int nIndex)').async, ), SetProcessDpiAwarenessContext: nodeUtil.promisify( - user32.func('int __stdcall SetProcessDpiAwarenessContext(int value)').async - ) + user32.func('int __stdcall SetProcessDpiAwarenessContext(int value)').async, + ), }; }); @@ -119,10 +124,10 @@ export const KEY_ACTION = Object.freeze({ UP: 'up', DOWN: 'down', }); -const VK_RETURN = 0x0D; +const VK_RETURN = 0x0d; const VK_SHIFT = 0x10; const VK_CONTROL = 0x11; -const VK_LWIN = 0x5B; +const VK_LWIN = 0x5b; const VK_ALT = 0x12; export const MODIFIER_KEY = Object.freeze({ shift: VK_SHIFT, @@ -165,7 +170,7 @@ const SM_XVIRTUALSCREEN = 76; const SM_YVIRTUALSCREEN = 77; const SM_CXVIRTUALSCREEN = 78; const SM_CYVIRTUALSCREEN = 79; -const MOUSE_MOVE_NORM = 0xFFFF; +const MOUSE_MOVE_NORM = 0xffff; const WHEEL_DELTA = 120; // const DPI_AWARENESS_CONTEXT_UNAWARE = 16; // const DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = 17; @@ -183,8 +188,8 @@ export function createKeyInput(params = {}) { wScan: 0, dwFlags: 0, ...params, - } - } + }, + }, }; } @@ -200,14 +205,10 @@ export function createKeyInput(params = {}) { */ export async function handleInputs(inputs) { const inputsArr = _.isArray(inputs) ? inputs : [inputs]; - const uSent = await getUser32().SendInput( - inputsArr.length, - inputsArr, - ffi?.sizeof(INPUT) - ); + const uSent = await getUser32().SendInput(inputsArr.length, inputsArr, ffi?.sizeof(INPUT)); if (uSent !== inputsArr.length) { throw new Error( - `SendInput API call failed. ${util.pluralize('input', uSent, true)} succeeded out of ${inputsArr.length}` + `SendInput API call failed. ${util.pluralize('input', uSent, true)} succeeded out of ${inputsArr.length}`, ); } return uSent; @@ -216,7 +217,7 @@ export async function handleInputs(inputs) { /** @type {() => Promise} */ export const ensureDpiAwareness = _.memoize(async function ensureDpiAwareness() { return Boolean( - await getUser32().SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) + await getUser32().SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2), ); }); @@ -229,11 +230,11 @@ async function getSystemMetrics(nIndex) { return await getUser32().GetSystemMetrics(nIndex); } -const isLeftMouseButtonSwapped = _.memoize(async function isLeftMouseButtonSwapped () { +const isLeftMouseButtonSwapped = _.memoize(async function isLeftMouseButtonSwapped() { return Boolean(await getSystemMetrics(SM_SWAPBUTTON)); }); -function createMouseInput (params = {}) { +function createMouseInput(params = {}) { return { type: INPUT_MOUSE, union: { @@ -245,8 +246,8 @@ function createMouseInput (params = {}) { dy: 0, mouseData: 0, ...params, - } - } + }, + }, }; } @@ -298,7 +299,7 @@ export async function toMouseButtonInput({button, action}) { break; default: throw createInvalidArgumentError( - `Mouse button '${button}' is unknown. Only ${_.values(MOUSE_BUTTON)} buttons are supported` + `Mouse button '${button}' is unknown. Only ${_.values(MOUSE_BUTTON)} buttons are supported`, ); } @@ -316,7 +317,7 @@ export async function toMouseButtonInput({button, action}) { default: throw createInvalidArgumentError( `Mouse button action '${action}' is unknown. ` + - `Only ${[MOUSE_BUTTON_ACTION.UP, MOUSE_BUTTON_ACTION.DOWN]} actions are supported` + `Only ${[MOUSE_BUTTON_ACTION.UP, MOUSE_BUTTON_ACTION.DOWN]} actions are supported`, ); } @@ -333,7 +334,7 @@ export async function toMouseButtonInput({button, action}) { * @param {number} max * @returns {number} */ -function clamp (num, min, max) { +function clamp(num, min, max) { return Math.min(Math.max(num, min), max); } @@ -355,7 +356,7 @@ export async function toMouseMoveInput(x, y, screenSize = null) { throw createInvalidArgumentError('Both move coordinates must be provided'); } - const {width, height} = screenSize ?? await getVirtualScreenSize(); + const {width, height} = screenSize ?? (await getVirtualScreenSize()); if (width <= 1 || height <= 1) { throw new Error('Cannot retrieve virtual screen dimensions via GetSystemMetrics WinAPI'); } @@ -388,7 +389,9 @@ export function toMouseWheelInput(dx, dy) { throw createInvalidArgumentError('Either horizontal or vertical scroll delta must be provided'); } if (hasHorizontalScroll && hasVerticalScroll) { - throw createInvalidArgumentError('Either horizontal or vertical scroll delta must be provided, but not both'); + throw createInvalidArgumentError( + 'Either horizontal or vertical scroll delta must be provided, but not both', + ); } if (hasHorizontalScroll && dx !== 0) { @@ -423,15 +426,15 @@ export function toUnicodeKeyInputs(text) { // The WM_CHAR event generated for carriage return is '\r', not '\n', and // some applications may check for VK_RETURN explicitly, so handle // newlines specially. - if (charCode === 0x0A) { + if (charCode === 0x0a) { result.push( createKeyInput({wVk: VK_RETURN, dwFlags: 0}), - createKeyInput({wVk: VK_RETURN, dwFlags: KEYEVENTF_KEYUP}) + createKeyInput({wVk: VK_RETURN, dwFlags: KEYEVENTF_KEYUP}), ); } result.push( createKeyInput({wScan: charCode, dwFlags: KEYEVENTF_UNICODE}), - createKeyInput({wScan: charCode, dwFlags: KEYEVENTF_UNICODE | KEYEVENTF_KEYUP}) + createKeyInput({wScan: charCode, dwFlags: KEYEVENTF_UNICODE | KEYEVENTF_KEYUP}), ); } return result; @@ -442,8 +445,10 @@ export function toUnicodeKeyInputs(text) { * * @returns {Promise} */ -export async function getVirtualScreenSize () { - const [width, height] = await B.all([SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN].map(getSystemMetrics)); +export async function getVirtualScreenSize() { + const [width, height] = await B.all( + [SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN].map(getSystemMetrics), + ); return {width, height}; } @@ -452,7 +457,7 @@ export async function getVirtualScreenSize () { * * @returns {Promise} */ -export async function getVirtualScreenPosition () { +export async function getVirtualScreenPosition() { const [x, y] = await B.all([SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN].map(getSystemMetrics)); return {x, y}; } diff --git a/lib/desired-caps.ts b/lib/desired-caps.ts index eefe04a..cb8604c 100644 --- a/lib/desired-caps.ts +++ b/lib/desired-caps.ts @@ -5,48 +5,48 @@ export const desiredCapConstraints = { platformName: { presence: true, isString: true, - inclusionCaseInsensitive: ['Windows'] + inclusionCaseInsensitive: ['Windows'], }, browserName: { - isString: true + isString: true, }, platformVersion: { - isString: true + isString: true, }, app: { - isString: true + isString: true, }, appArguments: { - isString: true + isString: true, }, appTopLevelWindow: { - isString: true + isString: true, }, appWorkingDir: { - isString: true + isString: true, }, createSessionTimeout: { - isNumber: true + isNumber: true, }, 'ms:waitForAppLaunch': { - isNumber: true // in seconds + isNumber: true, // in seconds }, 'ms:forcequit': { - isBoolean: true + isBoolean: true, }, 'ms:experimental-webdriver': { - isBoolean: true + isBoolean: true, }, systemPort: { - isNumber: true + isNumber: true, }, prerun: { - isObject: true + isObject: true, }, postrun: { - isObject: true + isObject: true, }, wadUrl: { - isString: true + isString: true, }, } as const satisfies Constraints; diff --git a/lib/driver.ts b/lib/driver.ts index 021dfa1..dbdefa7 100644 --- a/lib/driver.ts +++ b/lib/driver.ts @@ -11,11 +11,11 @@ import type { DriverOpts, W3CDriverCaps, } from '@appium/types'; -import { BaseDriver } from 'appium/driver'; -import { system } from 'appium/support'; -import { WinAppDriver } from './winappdriver'; -import type { WindowsDriverCaps } from './winappdriver'; -import { desiredCapConstraints } from './desired-caps'; +import {BaseDriver} from 'appium/driver'; +import {system} from 'appium/support'; +import {WinAppDriver} from './winappdriver'; +import type {WindowsDriverCaps} from './winappdriver'; +import {desiredCapConstraints} from './desired-caps'; import * as appManagementCommands from './commands/app-management'; import * as clipboardCommands from './commands/clipboard'; import * as executeCommands from './commands/execute'; @@ -28,9 +28,9 @@ import * as recordScreenCommands from './commands/record-screen'; import * as touchCommands from './commands/touch'; import * as contextCommands from './commands/context'; import * as logCommands from './commands/log'; -import { POWER_SHELL_FEATURE } from './constants'; -import { newMethodMap } from './method-map'; -import { executeMethodMap } from './execute-method-map'; +import {POWER_SHELL_FEATURE} from './constants'; +import {newMethodMap} from './method-map'; +import {executeMethodMap} from './execute-method-map'; const NO_PROXY: RouteMatcher[] = [ ['GET', new RegExp('^/session/[^/]+/appium/(?!app/)[^/]+')], @@ -76,14 +76,7 @@ export class WindowsDriver constructor(opts: InitialOpts, shouldValidateCaps = true) { super(opts, shouldValidateCaps); this.desiredCapConstraints = desiredCapConstraints; - this.locatorStrategies = [ - 'xpath', - 'id', - 'name', - 'tag name', - 'class name', - 'accessibility id', - ]; + this.locatorStrategies = ['xpath', 'id', 'name', 'tag name', 'class name', 'accessibility id']; this.resetState(); } @@ -98,7 +91,7 @@ export class WindowsDriver w3cCaps1: W3CWindowsDriverCaps, w3cCaps2?: W3CWindowsDriverCaps, w3cCaps3?: W3CWindowsDriverCaps, - driverData?: DriverData[] + driverData?: DriverData[], ): Promise> { if (!system.isWindows()) { throw new Error('WinAppDriver tests only run on Windows'); @@ -112,8 +105,10 @@ export class WindowsDriver this.log.info('Executing prerun PowerShell script'); const prerun = caps.prerun as PrerunCapability; if (!_.isString(prerun.command) && !_.isString(prerun.script)) { - throw new Error(`'prerun' capability value must either contain ` + - `'script' or 'command' entry of string type`); + throw new Error( + `'prerun' capability value must either contain ` + + `'script' or 'command' entry of string type`, + ); } this.assertFeatureEnabled(POWER_SHELL_FEATURE); const output = await this.execPowerShell(prerun); @@ -137,8 +132,10 @@ export class WindowsDriver const postrun = this.opts.postrun as PostrunCapability | undefined; if (postrun) { if (!_.isString(postrun.command) && !_.isString(postrun.script)) { - this.log.error(`'postrun' capability value must either contain ` + - `'script' or 'command' entry of string type`); + this.log.error( + `'postrun' capability value must either contain ` + + `'script' or 'command' entry of string type`, + ); } else { this.log.info('Executing postrun PowerShell script'); try { diff --git a/lib/execute-method-map.ts b/lib/execute-method-map.ts index 43adeb7..438e972 100644 --- a/lib/execute-method-map.ts +++ b/lib/execute-method-map.ts @@ -1,5 +1,5 @@ -import type { ExecuteMethodMap } from '@appium/types'; -import type { WindowsDriver } from './driver'; +import type {ExecuteMethodMap} from '@appium/types'; +import type {WindowsDriver} from './driver'; export const executeMethodMap = { 'windows: startRecordingScreen': { @@ -21,15 +21,7 @@ export const executeMethodMap = { 'windows: stopRecordingScreen': { command: 'windowsStopRecordingScreen', params: { - optional: [ - 'remotePath', - 'user', - 'pass', - 'method', - 'headers', - 'fileFieldName', - 'formFields', - ], + optional: ['remotePath', 'user', 'pass', 'method', 'headers', 'fileFieldName', 'formFields'], }, }, @@ -43,21 +35,16 @@ export const executeMethodMap = { 'windows: deleteFolder': { command: 'windowsDeleteFolder', params: { - required: [ - 'remotePath', - ], + required: ['remotePath'], }, }, 'windows: deleteFile': { command: 'windowsDeleteFile', params: { - required: [ - 'remotePath', - ], + required: ['remotePath'], }, }, - 'windows: click': { command: 'windowsClick', params: { @@ -76,14 +63,7 @@ export const executeMethodMap = { 'windows: scroll': { command: 'windowsScroll', params: { - optional: [ - 'elementId', - 'x', - 'y', - 'deltaX', - 'deltaY', - 'modifierKeys', - ], + optional: ['elementId', 'x', 'y', 'deltaX', 'deltaY', 'modifierKeys'], }, }, 'windows: clickAndDrag': { @@ -119,29 +99,21 @@ export const executeMethodMap = { 'windows: keys': { command: 'windowsKeys', params: { - required: [ - 'actions', - ], + required: ['actions'], }, }, 'windows: setClipboard': { command: 'windowsSetClipboard', params: { - required: [ - 'b64Content', - ], - optional: [ - 'contentType', - ], + required: ['b64Content'], + optional: ['contentType'], }, }, 'windows: getClipboard': { command: 'windowsGetClipboard', params: { - optional: [ - 'contentType', - ], + optional: ['contentType'], }, }, } as const satisfies ExecuteMethodMap; diff --git a/lib/index.ts b/lib/index.ts index c12a365..8dcca9a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,4 @@ -import { WindowsDriver } from './driver'; +import {WindowsDriver} from './driver'; -export { WindowsDriver }; +export {WindowsDriver}; export default WindowsDriver; diff --git a/lib/installer.ts b/lib/installer.ts index e65059c..b170bb7 100644 --- a/lib/installer.ts +++ b/lib/installer.ts @@ -1,10 +1,10 @@ import _ from 'lodash'; -import { fs, tempDir } from 'appium/support'; +import {fs, tempDir} from 'appium/support'; import path from 'node:path'; -import { exec } from 'teen_process'; -import { log } from './logger'; -import { queryRegistry, type RegEntry } from './registry'; -import { runElevated } from './utils'; +import {exec} from 'teen_process'; +import {log} from './logger'; +import {queryRegistry, type RegEntry} from './registry'; +import {runElevated} from './utils'; const POSSIBLE_WAD_INSTALL_ROOTS = [ process.env['ProgramFiles(x86)'], @@ -16,7 +16,8 @@ const UNINSTALL_REG_ROOT = 'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\ const REG_ENTRY_VALUE = 'Windows Application Driver'; const REG_ENTRY_KEY = 'DisplayName'; const REG_ENTRY_TYPE = 'REG_SZ'; -const INST_LOCATION_SCRIPT_BY_GUID = (guid: string): string => ` +const INST_LOCATION_SCRIPT_BY_GUID = (guid: string): string => + ` Set installer = CreateObject("WindowsInstaller.Installer") Set session = installer.OpenProduct("${guid}") session.DoAction("CostInitialize") @@ -66,18 +67,18 @@ export const getWADExecutablePath = _.memoize(async function getWADInstallPath() log.debug('Checking the system registry for the corresponding MSI entry'); try { const uninstallEntries = await queryRegistry(UNINSTALL_REG_ROOT); - const wadEntry = uninstallEntries.find((entry: RegEntry) => - entry.key === REG_ENTRY_KEY && entry.value === REG_ENTRY_VALUE && entry.type === REG_ENTRY_TYPE + const wadEntry = uninstallEntries.find( + (entry: RegEntry) => + entry.key === REG_ENTRY_KEY && + entry.value === REG_ENTRY_VALUE && + entry.type === REG_ENTRY_TYPE, ); if (wadEntry) { log.debug(`Found MSI entry: ${JSON.stringify(wadEntry)}`); const installerGuid = _.last(wadEntry.root.split('\\')) as string; // WAD MSI installer leaves InstallLocation registry value empty, // so we need to be hacky here - const result = path.join( - await fetchMsiInstallLocation(installerGuid), - WAD_EXE_NAME - ); + const result = path.join(await fetchMsiInstallLocation(installerGuid), WAD_EXE_NAME); log.debug(`Checking if WAD exists at '${result}'`); if (await fs.exists(result)) { return result; @@ -92,12 +93,13 @@ export const getWADExecutablePath = _.memoize(async function getWADInstallPath() } log.debug(e.stack); } - throw new WADNotFoundError(`${WAD_EXE_NAME} has not been found in any of these ` + - `locations: ${pathCandidates}. Use the following driver script to install it: ` + - `'appium driver run windows install-wad '. ` + - `Check https://github.com/microsoft/WinAppDriver/releases to list ` + - `available server versions or drop the '' argument to ` + - `install the latest stable one.` + throw new WADNotFoundError( + `${WAD_EXE_NAME} has not been found in any of these ` + + `locations: ${pathCandidates}. Use the following driver script to install it: ` + + `'appium driver run windows install-wad '. ` + + `Check https://github.com/microsoft/WinAppDriver/releases to list ` + + `available server versions or drop the '' argument to ` + + `install the latest stable one.`, ); }); diff --git a/lib/logger.ts b/lib/logger.ts index cbc5a2d..e5663bd 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -1,4 +1,4 @@ -import { logger } from 'appium/support'; +import {logger} from 'appium/support'; export const log = logger.getLogger('WinAppDriver'); diff --git a/lib/method-map.ts b/lib/method-map.ts index fbf9008..1cfc7ac 100644 --- a/lib/method-map.ts +++ b/lib/method-map.ts @@ -4,14 +4,14 @@ export const newMethodMap = { '/session/:sessionId/appium/start_recording_screen': { POST: { command: 'startRecordingScreen', - payloadParams: { optional: ['options'] } - } + payloadParams: {optional: ['options']}, + }, }, '/session/:sessionId/appium/stop_recording_screen': { POST: { command: 'stopRecordingScreen', - payloadParams: { optional: ['options'] } - } + payloadParams: {optional: ['options']}, + }, }, '/session/:sessionId/appium/device/push_file': { POST: {command: 'pushFile', payloadParams: {required: ['path', 'data']}}, @@ -55,49 +55,42 @@ export const newMethodMap = { POST: {command: 'doubleClick'}, }, '/session/:sessionId/touch/click': { - POST: { command: 'click', payloadParams: { required: ['element'] } } + POST: {command: 'click', payloadParams: {required: ['element']}}, }, '/session/:sessionId/touch/down': { - POST: { command: 'touchDown', payloadParams: { required: ['x', 'y'] } } + POST: {command: 'touchDown', payloadParams: {required: ['x', 'y']}}, }, '/session/:sessionId/touch/up': { - POST: { command: 'touchUp', payloadParams: { required: ['x', 'y'] } } + POST: {command: 'touchUp', payloadParams: {required: ['x', 'y']}}, }, '/session/:sessionId/touch/move': { - POST: { command: 'touchMove', payloadParams: { required: ['x', 'y'] } } + POST: {command: 'touchMove', payloadParams: {required: ['x', 'y']}}, }, '/session/:sessionId/touch/longclick': { POST: { command: 'touchLongClick', - payloadParams: { required: ['elements'] } - } + payloadParams: {required: ['elements']}, + }, }, '/session/:sessionId/touch/flick': { POST: { command: 'flick', payloadParams: { - optional: [ - 'element', - 'xspeed', - 'yspeed', - 'xoffset', - 'yoffset', - 'speed' - ] - } - } + optional: ['element', 'xspeed', 'yspeed', 'xoffset', 'yoffset', 'speed'], + }, + }, }, '/session/:sessionId/touch/perform': { POST: { command: 'performTouch', - payloadParams: { wrap: 'actions', required: ['actions'] } - } + payloadParams: {wrap: 'actions', required: ['actions']}, + }, }, '/session/:sessionId/touch/multi/perform': { POST: { command: 'performMultiAction', - payloadParams: { required: ['actions'], optional: ['elementId'] } - } + payloadParams: {required: ['actions'], optional: ['elementId']}, + }, }, '/session/:sessionId/keys': { POST: {command: 'keys', payloadParams: {required: ['value']}}, diff --git a/lib/registry.ts b/lib/registry.ts index 409141a..ac37a90 100644 --- a/lib/registry.ts +++ b/lib/registry.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { runElevated } from './utils'; +import {runElevated} from './utils'; const REG = 'reg.exe'; const ENTRY_PATTERN = /^\s+(\w+)\s+([A-Z_]+)\s*(.*)/; diff --git a/lib/utils.ts b/lib/utils.ts index d0b7934..b2e5d8e 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,10 +1,10 @@ import _ from 'lodash'; -import { net } from 'appium/support'; -import { promisify } from 'node:util'; -import { exec } from 'node:child_process'; -import type { ExecOptions } from 'node:child_process'; +import {net} from 'appium/support'; +import {promisify} from 'node:util'; +import {exec} from 'node:child_process'; +import type {ExecOptions} from 'node:child_process'; import B from 'bluebird'; -import { log } from './logger'; +import {log} from './logger'; const execAsync = promisify(exec); @@ -23,11 +23,9 @@ const execAsync = promisify(exec); export async function runElevated( cmd: string, args: string[] = [], - opts: RunElevatedOptions = {} + opts: RunElevatedOptions = {}, ): Promise<{stdout: string; stderr: string}> { - const { - timeoutMs = 60 * 1000 * 5 - } = opts; + const {timeoutMs = 60 * 1000 * 5} = opts; const escapePSSingleQuoted = (str: string): string => `'${String(str).replace(/'/g, "''")}'`; const psFilePath = escapePSSingleQuoted(cmd); @@ -38,8 +36,10 @@ export async function runElevated( // We avoid additional interpolation here by using only single-quoted literals inside the PS command. const fullCmd = `powershell -NoProfile -Command "${psCommand}"`; log.debug(`Executing command: ${fullCmd}`); - const { stdout, stderr } = await B.resolve(execAsync(fullCmd, opts)) - .timeout(timeoutMs, `The command '${fullCmd}' timed out after ${timeoutMs}ms`); + const {stdout, stderr} = await B.resolve(execAsync(fullCmd, opts)).timeout( + timeoutMs, + `The command '${fullCmd}' timed out after ${timeoutMs}ms`, + ); return { stdout: _.isString(stdout) ? stdout : stdout.toString(), stderr: _.isString(stderr) ? stderr : stderr.toString(), diff --git a/lib/winappdriver.ts b/lib/winappdriver.ts index 1ff034b..4300ecf 100644 --- a/lib/winappdriver.ts +++ b/lib/winappdriver.ts @@ -1,15 +1,15 @@ import _ from 'lodash'; import os from 'node:os'; import path from 'node:path'; -import type { AppiumLogger, ProxyOptions, HTTPMethod, HTTPBody } from '@appium/types'; -import { JWProxy, errors } from 'appium/driver'; -import { SubProcess } from 'teen_process'; -import { getWADExecutablePath } from './installer'; -import { waitForCondition } from 'asyncbox'; -import { execSync } from 'node:child_process'; -import { util } from 'appium/support'; -import { findAPortNotInUse, checkPortStatus } from 'portscanner'; -import { desiredCapConstraints } from './desired-caps'; +import type {AppiumLogger, ProxyOptions, HTTPMethod, HTTPBody} from '@appium/types'; +import {JWProxy, errors} from 'appium/driver'; +import {SubProcess} from 'teen_process'; +import {getWADExecutablePath} from './installer'; +import {waitForCondition} from 'asyncbox'; +import {execSync} from 'node:child_process'; +import {util} from 'appium/support'; +import {findAPortNotInUse, checkPortStatus} from 'portscanner'; +import {desiredCapConstraints} from './desired-caps'; const DEFAULT_BASE_PATH = '/wd/hub'; const DEFAULT_HOST = '127.0.0.1'; @@ -22,7 +22,8 @@ const PORT_ALLOCATION_GUARD = util.getLockFileGuard(path.resolve(os.tmpdir(), 'w timeout: 5, tryRecovery: true, }); -const TROUBLESHOOTING_LINK = 'https://github.com/appium/appium-windows-driver?tab=readme-ov-file#troubleshooting'; +const TROUBLESHOOTING_LINK = + 'https://github.com/appium/appium-windows-driver?tab=readme-ov-file#troubleshooting'; class WADProxy extends JWProxy { didProcessExit?: boolean; @@ -39,11 +40,16 @@ class WADProxy extends JWProxy { } } - override async proxyCommand(url: string, method: HTTPMethod, body: HTTPBody = null): Promise { + override async proxyCommand( + url: string, + method: HTTPMethod, + body: HTTPBody = null, + ): Promise { if (this.didProcessExit) { throw new errors.InvalidContextError( `'${method} ${url}' cannot be proxied to WinAppDriver server because ` + - 'its process is not running (probably crashed). Check the Appium log for more details'); + 'its process is not running (probably crashed). Check the Appium log for more details', + ); } return await super.proxyCommand(url, method, body); } @@ -67,7 +73,7 @@ class WADProcess { } get isRunning(): boolean { - return !!(this.proc?.isRunning); + return !!this.proc?.isRunning; } async start(): Promise { @@ -83,8 +89,9 @@ class WADProcess { } catch { throw this.log.errorWithException( `Could not find any free port in range ${startPort}..${endPort}. ` + - `Please check your system firewall settings or set 'systemPort' capability ` + - `to the desired port number`); + `Please check your system firewall settings or set 'systemPort' capability ` + + `to the desired port number`, + ); } }); } @@ -96,7 +103,7 @@ class WADProcess { } this.proc = new SubProcess(this.executablePath, args, { - encoding: 'ucs2' + encoding: 'ucs2', }); this.proc.on('output', (stdout, stderr) => { const line = _.trim(stderr || stdout); @@ -116,8 +123,10 @@ class WADProcess { try { await this.proc?.stop(); } catch (e: any) { - this.log.warn(`WinAppDriver process with PID ${this.proc?.pid} cannot be stopped. ` + - `Original error: ${e.message}`); + this.log.warn( + `WinAppDriver process with PID ${this.proc?.pid} cannot be stopped. ` + + `Original error: ${e.message}`, + ); } } } @@ -177,8 +186,10 @@ export class WinAppDriver { try { await this.proxy.command('', 'DELETE'); } catch (err: any) { - this.log.warn(`Did not get confirmation WinAppDriver deleteSession worked; ` + - `Error was: ${err.message}`); + this.log.warn( + `Did not get confirmation WinAppDriver deleteSession worked; ` + + `Error was: ${err.message}`, + ); } } @@ -222,40 +233,41 @@ export class WinAppDriver { let lastError: Error | undefined; try { - await waitForCondition(async () => { - try { - if (this.proxy) { - await this.proxy.command('/status', 'GET'); - return true; + await waitForCondition( + async () => { + try { + if (this.proxy) { + await this.proxy.command('/status', 'GET'); + return true; + } + } catch (err: any) { + if (this.proxy?.didProcessExit) { + throw new Error(err.message); + } + lastError = err; + return false; } - } catch (err: any) { - if (this.proxy?.didProcessExit) { - throw new Error(err.message); - } - lastError = err; - return false; - } - }, { - waitMs: STARTUP_TIMEOUT_MS, - intervalMs: 1000, - }); + }, + { + waitMs: STARTUP_TIMEOUT_MS, + intervalMs: 1000, + }, + ); } catch (e: any) { if (!lastError || this.proxy.didProcessExit) { throw e; } const serverUrl = this.proxy.getUrlForProxy('/status'); - let errorMessage = ( + let errorMessage = `WinAppDriver server '${executablePath}' is not listening at ${serverUrl} ` + - `after ${STARTUP_TIMEOUT_MS}ms timeout. Make sure it could be started manually.` - ); + `after ${STARTUP_TIMEOUT_MS}ms timeout. Make sure it could be started manually.`; if (await this.proxy.isListening()) { - errorMessage = ( + errorMessage = `WinAppDriver server '${executablePath}' is listening at ${serverUrl}, ` + `but fails to respond with a proper status. It is an issue with the server itself. ` + `Consider checking the troubleshooting guide at ${TROUBLESHOOTING_LINK}. ` + - `Original error: ${(lastError ?? e).message}` - ); + `Original error: ${(lastError ?? e).message}`; } throw new Error(errorMessage); } @@ -274,7 +286,7 @@ export class WinAppDriver { parsedUrl = new URL(url); } catch (e: any) { throw new Error( - `Cannot parse the provided WinAppDriver URL '${url}'. Original error: ${e.message}` + `Cannot parse the provided WinAppDriver URL '${url}'. Original error: ${e.message}`, ); } const proxyOpts: ProxyOptions = { @@ -292,26 +304,22 @@ export class WinAppDriver { try { await this.proxy.command('/status', 'GET'); } catch (e: any) { - let errorMessage = ( + let errorMessage = `WinAppDriver server is not listening at ${url}. ` + - `Make sure it is running and the provided wadUrl is correct` - ); + `Make sure it is running and the provided wadUrl is correct`; if (await this.proxy.isListening()) { - errorMessage = ( + errorMessage = `WinAppDriver server is listening at ${url}, but fails to respond with a proper status. ` + `It is an issue with the server itself. ` + `Consider checking the troubleshooting guide at ${TROUBLESHOOTING_LINK}. ` + - `Original error: ${e.message}` - ); + `Original error: ${e.message}`; } throw new Error(errorMessage); } } private async _startSession(caps: WindowsDriverCaps): Promise { - const { - createSessionTimeout = DEFAULT_CREATE_SESSION_TIMEOUT_MS - } = caps; + const {createSessionTimeout = DEFAULT_CREATE_SESSION_TIMEOUT_MS} = caps; this.log.debug(`Starting WinAppDriver session. Will timeout in '${createSessionTimeout}' ms.`); let retryIteration = 0; let lastError: Error | undefined; @@ -324,7 +332,9 @@ export class WinAppDriver { return true; } catch (error: any) { lastError = error; - this.log.warn(`Could not start WinAppDriver session error = '${error.message}', attempt = ${retryIteration}`); + this.log.warn( + `Could not start WinAppDriver session error = '${error.message}', attempt = ${retryIteration}`, + ); return false; } }; @@ -332,12 +342,12 @@ export class WinAppDriver { try { await waitForCondition(condFn, { waitMs: createSessionTimeout, - intervalMs: 500 + intervalMs: 500, }); } catch (timeoutError: any) { this.log.debug(`timeoutError was ${timeoutError.message}`); if (lastError) { - throw (lastError); + throw lastError; } throw new Error(`Could not start WinAppDriver session within ${createSessionTimeout} ms.`); } @@ -365,4 +375,3 @@ export type WindowsDriverCaps = { prerun?: {command?: string; script?: string}; postrun?: {command?: string; script?: string}; }; - diff --git a/package.json b/package.json index 1a82c5f..31e6b5d 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,8 @@ "clean": "npm run build -- --clean", "lint": "eslint .", "lint:fix": "npm run lint -- --fix", + "format": "prettier -w ./lib ./test", + "format:check": "prettier --check ./lib ./test", "prepare": "npm run build", "test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.ts\"", "e2e-test": "mocha --exit --timeout 10m \"./test/e2e/**/*-specs.ts\"" @@ -90,6 +92,7 @@ "chai-as-promised": "^8.0.0", "conventional-changelog-conventionalcommits": "^9.0.0", "mocha": "^11.0.1", + "prettier": "^3.0.3", "semantic-release": "^25.0.2", "sinon": "^21.0.0", "ts-node": "^10.9.1", diff --git a/test/e2e/commands/context-e2e-specs.ts b/test/e2e/commands/context-e2e-specs.ts index acca258..36b475b 100644 --- a/test/e2e/commands/context-e2e-specs.ts +++ b/test/e2e/commands/context-e2e-specs.ts @@ -1,7 +1,7 @@ -import { buildWdIoOptions } from '../helpers'; -import { remote as wdio } from 'webdriverio'; -import type { Browser } from 'webdriverio'; -import { expect } from 'chai'; +import {buildWdIoOptions} from '../helpers'; +import {remote as wdio} from 'webdriverio'; +import type {Browser} from 'webdriverio'; +import {expect} from 'chai'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; @@ -33,5 +33,4 @@ describe('context', function () { it('should throw an error if invalid context', async function () { await expect(driver!.switchAppiumContext('INVALID_CONTEXT')).to.be.rejected; }); - }); diff --git a/test/e2e/commands/file-movement-e2e-specs.ts b/test/e2e/commands/file-movement-e2e-specs.ts index 45d4769..747911a 100644 --- a/test/e2e/commands/file-movement-e2e-specs.ts +++ b/test/e2e/commands/file-movement-e2e-specs.ts @@ -1,10 +1,10 @@ -import { remote as wdio } from 'webdriverio'; -import type { Browser } from 'webdriverio'; +import {remote as wdio} from 'webdriverio'; +import type {Browser} from 'webdriverio'; import path from 'node:path'; -import { tempDir, fs } from 'appium/support'; -import { isAdmin } from '../../../lib/installer'; -import { buildWdIoOptions } from '../helpers'; -import { expect } from 'chai'; +import {tempDir, fs} from 'appium/support'; +import {isAdmin} from '../../../lib/installer'; +import {buildWdIoOptions} from '../helpers'; +import {expect} from 'chai'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; @@ -15,7 +15,7 @@ describe('file movement', function () { let remotePath: string | null = null; beforeEach(async function () { - if (process.env.CI || !await isAdmin()) { + if (process.env.CI || !(await isAdmin())) { return this.skip(); } @@ -41,7 +41,7 @@ describe('file movement', function () { it('should push and pull a file', async function () { const stringData = `random string data ${Math.random()}`; const base64Data = Buffer.from(stringData).toString('base64'); - remotePath = await tempDir.path({ prefix: 'appium', suffix: '.tmp' }); + remotePath = await tempDir.path({prefix: 'appium', suffix: '.tmp'}); await driver!.pushFile(remotePath, base64Data); @@ -54,7 +54,7 @@ describe('file movement', function () { it('should be able to delete a file', async function () { const stringData = `random string data ${Math.random()}`; const base64Data = Buffer.from(stringData).toString('base64'); - remotePath = await tempDir.path({ prefix: 'appium', suffix: '.tmp' }); + remotePath = await tempDir.path({prefix: 'appium', suffix: '.tmp'}); await driver!.pushFile(remotePath, base64Data); @@ -62,7 +62,7 @@ describe('file movement', function () { const remoteData = Buffer.from(remoteData64, 'base64').toString(); expect(remoteData).to.equal(stringData); - await driver!.execute('windows: deleteFile', { remotePath }); + await driver!.execute('windows: deleteFile', {remotePath}); await expect(driver!.pullFile(remotePath)).to.eventually.be.rejectedWith(/does not exist/); }); diff --git a/test/e2e/commands/log-e2e-specs.ts b/test/e2e/commands/log-e2e-specs.ts index f56073b..07625c7 100644 --- a/test/e2e/commands/log-e2e-specs.ts +++ b/test/e2e/commands/log-e2e-specs.ts @@ -1,7 +1,7 @@ -import { buildWdIoOptions } from '../helpers'; -import { remote as wdio } from 'webdriverio'; -import type { Browser } from 'webdriverio'; -import { expect } from 'chai'; +import {buildWdIoOptions} from '../helpers'; +import {remote as wdio} from 'webdriverio'; +import type {Browser} from 'webdriverio'; +import {expect} from 'chai'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; diff --git a/test/e2e/commands/winapi-e2e-specs.ts b/test/e2e/commands/winapi-e2e-specs.ts index a49a2bd..61eb3cf 100644 --- a/test/e2e/commands/winapi-e2e-specs.ts +++ b/test/e2e/commands/winapi-e2e-specs.ts @@ -1,7 +1,7 @@ -import { buildWdIoOptions } from '../helpers'; -import { remote as wdio } from 'webdriverio'; -import type { Browser } from 'webdriverio'; -import { expect } from 'chai'; +import {buildWdIoOptions} from '../helpers'; +import {remote as wdio} from 'webdriverio'; +import type {Browser} from 'webdriverio'; +import {expect} from 'chai'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; @@ -181,7 +181,7 @@ describe('winapi', function () { {text: '和製漢字'}, {pause: 100}, {virtualKeyCode: 0x10, down: false}, - ] + ], }); }); @@ -200,7 +200,7 @@ describe('winapi', function () { // down is not boolean { virtualKeyCode: 0x10, - down: 'false' + down: 'false', }, ]; @@ -209,5 +209,4 @@ describe('winapi', function () { } }); }); - }); diff --git a/test/e2e/driver-e2e-specs.ts b/test/e2e/driver-e2e-specs.ts index 0b1c736..4f1be38 100644 --- a/test/e2e/driver-e2e-specs.ts +++ b/test/e2e/driver-e2e-specs.ts @@ -1,8 +1,8 @@ -import { remote as wdio } from 'webdriverio'; -import type { Browser } from 'webdriverio'; -import { isAdmin } from '../../lib/installer'; -import { buildWdIoOptions } from './helpers'; -import { expect } from 'chai'; +import {remote as wdio} from 'webdriverio'; +import type {Browser} from 'webdriverio'; +import {isAdmin} from '../../lib/installer'; +import {buildWdIoOptions} from './helpers'; +import {expect} from 'chai'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; @@ -12,7 +12,7 @@ describe('Driver', function () { let driver: Browser | null = null; beforeEach(async function () { - if (process.env.CI || !await isAdmin()) { + if (process.env.CI || !(await isAdmin())) { return this.skip(); } diff --git a/test/e2e/helpers.ts b/test/e2e/helpers.ts index 2111260..db49328 100644 --- a/test/e2e/helpers.ts +++ b/test/e2e/helpers.ts @@ -1,4 +1,4 @@ -import type { remote } from 'webdriverio'; +import type {remote} from 'webdriverio'; 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'; @@ -17,7 +17,6 @@ export function buildWdIoOptions(app: string): Parameters[0] { platformName: 'Windows', 'appium:automationName': 'windows', 'appium:app': app, - } + }, }; } - diff --git a/test/unit/driver-specs.ts b/test/unit/driver-specs.ts index f95379a..657c6af 100644 --- a/test/unit/driver-specs.ts +++ b/test/unit/driver-specs.ts @@ -1,8 +1,8 @@ -import { WindowsDriver } from '../../lib/driver'; +import {WindowsDriver} from '../../lib/driver'; import sinon from 'sinon'; import B from 'bluebird'; -import { system } from 'appium/support'; -import { expect } from 'chai'; +import {system} from 'appium/support'; +import {expect} from 'chai'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; @@ -20,7 +20,7 @@ describe('driver', function () { describe('constructor', function () { it('calls BaseDriver constructor with opts', function () { - const driver = new WindowsDriver({ foo: 'bar' } as any); + const driver = new WindowsDriver({foo: 'bar'} as any); expect(driver).to.exist; expect((driver.opts as any).foo).to.equal('bar'); }); @@ -28,13 +28,16 @@ describe('driver', function () { describe('createSession', function () { it('should set sessionId', async function () { - const driver = new WindowsDriver({ app: 'myapp'} as any, false); - sinon.mock(driver).expects('startWinAppDriverSession') - .once() - .returns(B.resolve()); - await driver.createSession( - { alwaysMatch: { platformName: 'Windows', 'appium:automationName': 'Windows', 'appium:app': 'myapp' }, firstMatch: [{}] } - ); + const driver = new WindowsDriver({app: 'myapp'} as any, false); + sinon.mock(driver).expects('startWinAppDriverSession').once().returns(B.resolve()); + await driver.createSession({ + alwaysMatch: { + platformName: 'Windows', + 'appium:automationName': 'Windows', + 'appium:app': 'myapp', + }, + firstMatch: [{}], + }); expect(driver.sessionId).to.exist; expect((driver.caps as any).app).to.equal('myapp'); }); @@ -56,7 +59,7 @@ describe('driver', function () { describe('proxying', function () { let driver: WindowsDriver; before(function () { - driver = new WindowsDriver({ address: '127.0.0.1', port: 4723 } as any, false); + driver = new WindowsDriver({address: '127.0.0.1', port: 4723} as any, false); driver.sessionId = 'abc'; }); describe('#proxyActive', function () { @@ -67,7 +70,9 @@ describe('driver', function () { expect(driver.proxyActive('abc')).to.be.false; }); it('should throw an error if session id is wrong', function () { - expect(() => { driver.proxyActive('aaa'); }).to.throw; + expect(() => { + driver.proxyActive('aaa'); + }).to.throw; }); }); @@ -82,7 +87,9 @@ describe('driver', function () { expect(avoidList).to.eql(driver['jwpProxyAvoid']); }); it('should throw an error if session id is wrong', function () { - expect(() => { (driver.getProxyAvoidList as any)('aaa'); }).to.throw; + expect(() => { + (driver.getProxyAvoidList as any)('aaa'); + }).to.throw; }); }); @@ -94,7 +101,9 @@ describe('driver', function () { expect((driver.canProxy as any)('abc')).to.be.true; }); it('should throw an error if session id is wrong', function () { - expect(() => { (driver.canProxy as any)('aaa'); }).to.throw; + expect(() => { + (driver.canProxy as any)('aaa'); + }).to.throw; }); }); }); diff --git a/test/unit/registry-specs.ts b/test/unit/registry-specs.ts index 4b1a0d1..890d9c6 100644 --- a/test/unit/registry-specs.ts +++ b/test/unit/registry-specs.ts @@ -1,5 +1,5 @@ -import { parseRegQueryOutput } from '../../lib/registry'; -import { expect } from 'chai'; +import {parseRegQueryOutput} from '../../lib/registry'; +import {expect} from 'chai'; describe('registry', function () { it('should parse reg query output', function () { @@ -87,10 +87,18 @@ HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{29 `; const result = parseRegQueryOutput(output); - expect(Boolean(result.find( - ({root, key, type, value}) => root === 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{29DA7679-80B6-452A-B264-349BAEE7CC0E}' - && key === 'DisplayName' && type === 'REG_SZ' && value === 'Windows Application Driver' - ))).to.be.true; + expect( + Boolean( + result.find( + ({root, key, type, value}) => + root === + 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{29DA7679-80B6-452A-B264-349BAEE7CC0E}' && + key === 'DisplayName' && + type === 'REG_SZ' && + value === 'Windows Application Driver', + ), + ), + ).to.be.true; }); it('should return empty array if no matches found', function () { const output = `