Skip to content

Commit

Permalink
feat: Improve the typing system (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Apr 21, 2024
1 parent 6a00d9d commit 20bdd85
Show file tree
Hide file tree
Showing 13 changed files with 924 additions and 552 deletions.
75 changes: 23 additions & 52 deletions lib/commands/app-management.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
const commands = {};

/**
* @typedef {Object} LaunchAppOptions
* @property {string} [bundleId] Bundle identifier of the app to be launched
* or activated. Either this property or `path` must be provided
* @property {string} [path] Full path to the app bundle. Either this property
* or `bundleId` must be provided
* @property {string[]} arguments the list of command line arguments
* for the app to be be launched with. This parameter is ignored if the app
* is already running.
* @property {Object} environment environment variables mapping. Custom
* variables are added to the default process environment.
*/

/**
* Start an app with given bundle identifier or activates it
* if the app is already running. An exception is thrown if the
* app with the given identifier cannot be found.
*
* @param {LaunchAppOptions} opts
* @this {Mac2Driver}
* @param {import('../types').LaunchAppOptions} [opts={}]
*/
commands.macosLaunchApp = async function macosLaunchApp (opts) {
const { bundleId, environment, path } = opts ?? {};
export async function macosLaunchApp (opts = {}) {
const { bundleId, environment, path } = opts;
return await this.wda.proxy.command('/wda/apps/launch', 'POST', {
arguments: opts.arguments,
environment,
Expand All @@ -30,66 +16,51 @@ commands.macosLaunchApp = async function macosLaunchApp (opts) {
});
};

/**
* @typedef {Object} ActivateAppOptions
* @property {string} [bundleId] Bundle identifier of the app to be activated.
* Either this property or `path` must be provided
* @property {string} [path] Full path to the app bundle. Either this property
* or `bundleId` must be provided
*/

/**
* Activate an app with given bundle identifier. An exception is thrown if the
* app cannot be found or is not running.
*
* @param {ActivateAppOptions} opts
* @this {Mac2Driver}
* @param {import('../types').ActivateAppOptions} [opts={}]
*/
commands.macosActivateApp = async function macosActivateApp (opts) {
const { bundleId, path } = opts ?? {};
export async function macosActivateApp (opts = {}) {
const { bundleId, path } = opts;
return await this.wda.proxy.command('/wda/apps/activate', 'POST', { bundleId, path });
};

/**
* @typedef {Object} TerminateAppOptions
* @property {string} [bundleId] Bundle identifier of the app to be terminated.
* Either this property or `path` must be provided
* @property {string} [path] Full path to the app bundle. Either this property
* or `bundleId` must be provided
*/

/**
* Terminate an app with given bundle identifier. An exception is thrown if the
* app cannot be found.
*
* @param {TerminateAppOptions} opts
* @this {Mac2Driver}
* @param {import('../types').TerminateAppOptions} opts
* @returns {Promise<boolean>} `true` if the app was running and has been successfully terminated.
* `false` if the app was not running before.
*/
commands.macosTerminateApp = async function macosTerminateApp (opts) {
export async function macosTerminateApp (opts) {
const { bundleId, path } = opts ?? {};
return await this.wda.proxy.command('/wda/apps/terminate', 'POST', { bundleId, path });
return /** @type {boolean} */ (
await this.wda.proxy.command('/wda/apps/terminate', 'POST', { bundleId, path })
);
};

/**
* @typedef {Object} QueryAppStateOptions
* @property {string} [bundleId] Bundle identifier of the app whose state should be queried.
* Either this property or `path` must be provided
* @property {string} [path] Full path to the app bundle. Either this property
* or `bundleId` must be provided
*/

/**
* Query an app state with given bundle identifier. An exception is thrown if the
* app cannot be found.
*
* @param {QueryAppStateOptions} opts
* @this {Mac2Driver}
* @param {import('../types').QueryAppStateOptions} opts
* @returns {Promise<number>} The application state code. See
* https://developer.apple.com/documentation/xctest/xcuiapplicationstate?language=objc
* for more details
*/
commands.macosQueryAppState = async function macosQueryAppState (opts) {
export async function macosQueryAppState (opts) {
const { bundleId, path } = opts ?? {};
return await this.wda.proxy.command('/wda/apps/state', 'POST', { bundleId, path });
return /** @type {number} */ (
await this.wda.proxy.command('/wda/apps/state', 'POST', { bundleId, path })
);
};

export default commands;
/**
* @typedef {import('../driver').Mac2Driver} Mac2Driver
*/
37 changes: 12 additions & 25 deletions lib/commands/applescript.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
import { fs, tempDir, util } from 'appium/support';
import { exec } from 'teen_process';
import log from '../logger';
import path from 'path';

const OSASCRIPT = 'osascript';
const APPLE_SCRIPT_FEATURE = 'apple_script';

const commands = {};

/**
* @typedef {Object} ExecAppleScriptOptions
* @property {string} script A valid AppleScript to execute
* @property {string} language Overrides the scripting language. Basically, sets the value of `-l` command
* line argument of `osascript` tool. If unset the AppleScript language is assumed.
* @property {string} command A valid AppleScript as a single command (no line breaks) to execute
* @property {number} timeout [20000] The number of seconds to wait until a long-running command is
* finished. An error is thrown if the command is still running after this timeout expires.
* @property {string} cwd The path to an existing folder, which is going to be set as the
* working directory for the command/script being executed.
*/

/**
* Executes the given AppleScript command or a whole script based on the
* given options. Either of these options must be provided. If both are provided
Expand All @@ -31,12 +16,13 @@ const commands = {};
* and no permissions to do it are given to the parent (for example, Appium or Terminal)
* process in System Preferences -> Privacy list.
*
* @param {ExecAppleScriptOptions} opts
* @this {Mac2Driver}
* @param {import('../types').ExecAppleScriptOptions} opts
* @returns {Promise<string>} The actual stdout of the given command/script
* @throws {Error} If the exit code of the given command/script is not zero.
* The actual stderr output is set to the error message value.
*/
commands.macosExecAppleScript = async function macosExecAppleScript (opts) {
export async function macosExecAppleScript (opts = {}) {
this.ensureFeatureEnabled(APPLE_SCRIPT_FEATURE);

const {
Expand All @@ -45,12 +31,12 @@ commands.macosExecAppleScript = async function macosExecAppleScript (opts) {
command,
cwd,
timeout,
} = opts ?? {};
} = opts;
if (!script && !command) {
log.errorAndThrow('AppleScript script/command must not be empty');
this.log.errorAndThrow('AppleScript script/command must not be empty');
}
if (/\n/.test(command)) {
log.errorAndThrow('AppleScript commands cannot contain line breaks');
if (/\n/.test(/** @type {string} */(command))) {
this.log.errorAndThrow('AppleScript commands cannot contain line breaks');
}
// 'command' has priority over 'script'
const shouldRunScript = !command;
Expand All @@ -64,12 +50,12 @@ commands.macosExecAppleScript = async function macosExecAppleScript (opts) {
if (shouldRunScript) {
tmpRoot = await tempDir.openDir();
const tmpScriptPath = path.resolve(tmpRoot, 'appium_script.scpt');
await fs.writeFile(tmpScriptPath, script, 'utf8');
await fs.writeFile(tmpScriptPath, /** @type {string} */(script), 'utf8');
args.push(tmpScriptPath);
} else {
args.push('-e', command);
}
log.info(`Running ${OSASCRIPT} with arguments: ${util.quote(args)}`);
this.log.info(`Running ${OSASCRIPT} with arguments: ${util.quote(args)}`);
try {
const {stdout} = await exec(OSASCRIPT, args, {cwd, timeout});
return stdout;
Expand All @@ -83,5 +69,6 @@ commands.macosExecAppleScript = async function macosExecAppleScript (opts) {
}
};

export { commands };
export default commands;
/**
* @typedef {import('../driver').Mac2Driver} Mac2Driver
*/
27 changes: 20 additions & 7 deletions lib/commands/execute.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import _ from 'lodash';
import { errors } from 'appium/driver';
import log from '../logger';

const commands = {};

const EXTENSION_COMMANDS_MAPPING = {
setValue: 'macosSetValue',
Expand Down Expand Up @@ -39,21 +36,37 @@ const EXTENSION_COMMANDS_MAPPING = {
deepLink: 'macosDeepLink',
};

commands.execute = async function execute (script, args) {
/**
*
* @this {Mac2Driver}
* @param {string} script
* @param {any[]|import('@appium/types').StringRecord} [args]
* @returns {Promise<any>}
*/
export async function execute (script, args) {
if (script.match(/^macos:/)) {
log.info(`Executing extension command '${script}'`);
this.log.info(`Executing extension command '${script}'`);
script = script.replace(/^macos:/, '').trim();
return await this.executeMacosCommand(script, _.isArray(args) ? args[0] : args);
}
throw new errors.NotImplementedError();
};

commands.executeMacosCommand = async function executeMacosCommand (command, opts = {}) {
/**
*
* @this {Mac2Driver}
* @param {string} command
* @param {import('@appium/types').StringRecord} [opts={}]
* @returns {Promise<any>}
*/
export async function executeMacosCommand (command, opts = {}) {
if (!_.has(EXTENSION_COMMANDS_MAPPING, command)) {
throw new errors.UnknownCommandError(`Unknown extension command "${command}". ` +
`Only ${_.keys(EXTENSION_COMMANDS_MAPPING)} commands are supported.`);
}
return await this[EXTENSION_COMMANDS_MAPPING[command]](opts);
};

export default commands;
/**
* @typedef {import('../driver').Mac2Driver} Mac2Driver
*/
26 changes: 16 additions & 10 deletions lib/commands/find.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { util } from 'appium/support';


const commands = {};

// This is needed to make lookup by image working
commands.findElOrEls = async function findElOrEls (strategy, selector, mult, context) {
context = util.unwrapElement(context);
const endpoint = `/element${context ? `/${context}/element` : ''}${mult ? 's' : ''}`;
/**
* This is needed to make lookup by image working
*
* @this {Mac2Driver}
* @param {string} strategy
* @param {string} selector
* @param {boolean} mult
* @param {string} [context]
* @returns {Promise<any>}
*/
export async function findElOrEls (strategy, selector, mult, context) {
const contextId = context ? util.unwrapElement(context) : context;
const endpoint = `/element${contextId ? `/${contextId}/element` : ''}${mult ? 's' : ''}`;

if (strategy === '-ios predicate string') {
strategy = 'predicate string';
Expand All @@ -20,6 +26,6 @@ commands.findElOrEls = async function findElOrEls (strategy, selector, mult, con
});
};


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

0 comments on commit 20bdd85

Please sign in to comment.