Skip to content

Commit

Permalink
feat: Add support of executeMethodMap
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach committed Feb 1, 2025
1 parent e4849fb commit 8d92568
Show file tree
Hide file tree
Showing 15 changed files with 585 additions and 400 deletions.
27 changes: 17 additions & 10 deletions lib/commands/app-management.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const commands = {};

// https://github.com/microsoft/WinAppDriver/blob/master/Docs/SupportedAPIs.md

/**
Expand All @@ -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 /** @type {WinAppDriver} */ (this.winAppDriver).sendCommand(
'/appium/app/launch', 'POST', {}
);
}

/**
* Close the active window of the app under test. Check
Expand All @@ -25,11 +27,16 @@ 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 /** @type {WinAppDriver} */ (this.winAppDriver).sendCommand(
'/appium/app/close', 'POST', {}
);
}

export { commands };
export default commands;
/**
* @typedef {import('../driver').WindowsDriver} WindowsDriver
* @typedef {import('../winappdriver').WinAppDriver} WinAppDriver
*/
48 changes: 18 additions & 30 deletions lib/commands/clipboard.js
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -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<SetClipboardOptions>} 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`);
}
Expand All @@ -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<GetClipboardOptions>} 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<string>} 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',
Expand All @@ -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
*/
76 changes: 35 additions & 41 deletions lib/commands/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,42 @@ 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<any>}
*/
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} `);
return await this.executeMethod(formattedScript, [preprocessExecuteMethodArgs(args)]);
} 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<StringRecord>} ExecuteMethodArgs
*/
94 changes: 60 additions & 34 deletions lib/commands/file-movement.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>}
*/
export async function pushFile (remotePath, base64Data) {
this.assertFeatureEnabled(MODIFY_FS_FEATURE);
if (remotePath.endsWith(path.sep)) {
throw new errors.InvalidArgumentError(
Expand All @@ -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<string>}
*/
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<string>}
*/
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. ' +
Expand All @@ -109,6 +124,11 @@ function resolveToAbsolutePath (remotePath) {
return resolvedPath;
}

/**
*
* @param {string} remotePath
* @returns {Promise<void>}
*/
async function checkFileExists (remotePath) {
if (!await fs.exists(remotePath)) {
throw new errors.InvalidArgumentError(`The remote file '${remotePath}' does not exist.`);
Expand All @@ -121,6 +141,11 @@ async function checkFileExists (remotePath) {
}
}

/**
*
* @param {string} remotePath
* @returns {Promise<void>}
*/
async function checkFolderExists (remotePath) {
if (!await fs.exists(remotePath)) {
throw new errors.InvalidArgumentError(`The remote folder '${remotePath}' does not exist.`);
Expand All @@ -133,5 +158,6 @@ async function checkFolderExists (remotePath) {
}
}

export { commands };
export default commands;
/**
* @typedef {import('../driver').WindowsDriver} WindowsDriver
*/
Loading

0 comments on commit 8d92568

Please sign in to comment.