-
-
Notifications
You must be signed in to change notification settings - Fork 409
/
execute.js
154 lines (143 loc) · 5.34 KB
/
execute.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import _ from 'lodash';
import {XCUITestDriver} from '../driver';
import {errors, errorFromCode, errorFromW3CJsonCode} from 'appium/driver';
import {util} from 'appium/support';
/**
* Checks if script expects a particular parameter (either optional or required).
* @template {keyof XCUITestDriver.executeMethodMap} Script
* @param {Script} script - Script name
* @param {string} param - Parameter name
* @returns {boolean}
*/
function executeMethodExpectsParam(script, param) {
/** @type {ReadonlyArray<string>|undefined} */
let required;
/** @type {ReadonlyArray<string>|undefined} */
let optional;
const execMethodDef = XCUITestDriver.executeMethodMap[script];
if ('params' in execMethodDef) {
if ('required' in execMethodDef.params) {
required = execMethodDef.params.required;
}
if ('optional' in execMethodDef.params) {
optional = execMethodDef.params.optional;
}
}
const allParams = new Set(_.flatten([...(required ?? []), ...(optional ?? [])]));
return allParams.has(param);
}
/**
* @param {any} script
* @returns {script is keyof XCUITestDriver.executeMethodMap}
*/
function isExecuteMethod(script) {
return script in XCUITestDriver.executeMethodMap;
}
/**
* Massages the arguments going into an execute method.
* @param {keyof XCUITestDriver.executeMethodMap} script
* @param {ExecuteMethodArgs} [args]
* @returns {StringRecord<unknown>}
*/
function preprocessExecuteMethodArgs(script, args) {
if (_.isArray(args)) {
args = _.first(args);
}
const executeMethodArgs = /** @type {StringRecord<unknown>} */ (args ?? {});
/**
* Renames the deprecated `element` key to `elementId`. Historically,
* all of the pre-Execute-Method-Map execute methods accepted an `element` _or_ and `elementId` param.
* This assigns the `element` value to `elementId` if `elementId` is not already present.
*/
if (!('elementId' in executeMethodArgs) && 'element' in executeMethodArgs) {
executeMethodArgs.elementId = executeMethodArgs.element;
delete executeMethodArgs.element;
}
/**
* Automatically unwraps the `elementId` prop _if and only if_ the execute method expects it.
*
* Most of these Execute Methods (typically beginning with `mobile*`) will accept an `Element|string` for `elementId`, in practice they will only ever get a `string`. `Element|string` in the method's docstring is simply for documentation purposes.
*/
if ('elementId' in executeMethodArgs && executeMethodExpectsParam(script, 'elementId')) {
executeMethodArgs.elementId = util.unwrapElement(
/** @type {import('@appium/types').Element|string} */ (executeMethodArgs.elementId),
);
}
return executeMethodArgs;
}
export default {
/**
* Collect the response of an async script execution
* @this {XCUITestDriver}
* @deprecated
* @privateRemarks It's unclear what this is for. Don't use it.
*/
// eslint-disable-next-line require-await
async receiveAsyncResponse(status, value) {
this.log.debug(`Received async response: ${JSON.stringify(value)}`);
if (!util.hasValue(this.asyncPromise)) {
this.log.warn(
`Received async response when we were not expecting one! ` +
`Response was: ${JSON.stringify(value)}`,
);
return;
}
if (util.hasValue(status) && status !== 0) {
// MJSONWP
return this.asyncPromise.reject(errorFromCode(status, value.message));
}
if (!util.hasValue(status) && value && _.isString(value.error)) {
// W3C
return this.asyncPromise.reject(
errorFromW3CJsonCode(value.error, value.message, value.stacktrace),
);
}
return this.asyncPromise.resolve(value);
},
/**
* @template {ExecuteMethodArgs} [TArgs = unknown[]]
* @template [TReturn = unknown]
* @param {string} script - Either a script to run, or in the case of an Execute Method, the name of the script to execute.
* @param {TArgs} [args]
* @this {XCUITestDriver}
* @returns {Promise<TReturn>}
*/
async execute(script, args) {
// TODO: create a type that converts args to the parameters of the associated method using the `command` prop of `executeMethodMap`
script = script.trim().replace(/^mobile:\s*/, 'mobile: ');
if (isExecuteMethod(script)) {
const executeMethodArgs = preprocessExecuteMethodArgs(script, args);
return await this.executeMethod(script, [executeMethodArgs]);
} else if (this.isWebContext()) {
const atomsArgs = this.convertElementsForAtoms(/** @type {readonly any[]} */ (args));
const result = await this.executeAtom('execute_script', [script, atomsArgs]);
return this.cacheWebElements(result);
} else {
throw new errors.NotImplementedError();
}
},
/**
* @this {XCUITestDriver}
* @group Mobile Web Only
*/
async executeAsync(script, args) {
if (!this.isWebContext()) {
throw new errors.NotImplementedError();
}
args = this.convertElementsForAtoms(args);
this.asyncWaitMs = this.asyncWaitMs || 0;
const promise = this.remote.executeAtomAsync(
'execute_async_script',
[script, args, this.asyncWaitMs],
this.curWebFrames,
);
return this.cacheWebElements(await this.waitForAtom(promise));
},
};
/**
* @template [T=any]
* @typedef {import('@appium/types').StringRecord<T>} StringRecord
*/
/**
* @typedef {readonly any[] | readonly [StringRecord] | Readonly<StringRecord>} ExecuteMethodArgs
*/