appium-xcuitest-driver 10.12.2 → 10.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/build/lib/commands/context.d.ts +130 -161
- package/build/lib/commands/context.d.ts.map +1 -1
- package/build/lib/commands/context.js +122 -107
- package/build/lib/commands/context.js.map +1 -1
- package/build/lib/commands/execute.js +1 -1
- package/build/lib/commands/execute.js.map +1 -1
- package/build/lib/commands/general.js +1 -1
- package/build/lib/commands/general.js.map +1 -1
- package/build/lib/commands/gesture.d.ts +103 -119
- package/build/lib/commands/gesture.d.ts.map +1 -1
- package/build/lib/commands/gesture.js +98 -138
- package/build/lib/commands/gesture.js.map +1 -1
- package/build/lib/commands/screenshots.d.ts.map +1 -1
- package/build/lib/commands/screenshots.js +3 -5
- package/build/lib/commands/screenshots.js.map +1 -1
- package/build/lib/commands/timeouts.js +1 -1
- package/build/lib/commands/timeouts.js.map +1 -1
- package/build/lib/commands/web.d.ts +199 -202
- package/build/lib/commands/web.d.ts.map +1 -1
- package/build/lib/commands/web.js +206 -174
- package/build/lib/commands/web.js.map +1 -1
- package/build/lib/driver.d.ts +2 -1
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +10 -4
- package/build/lib/driver.js.map +1 -1
- package/build/lib/execute-method-map.d.ts.map +1 -1
- package/build/lib/execute-method-map.js +0 -1
- package/build/lib/execute-method-map.js.map +1 -1
- package/lib/commands/{context.js → context.ts} +172 -145
- package/lib/commands/execute.js +1 -1
- package/lib/commands/general.js +1 -1
- package/lib/commands/{gesture.js → gesture.ts} +225 -183
- package/lib/commands/screenshots.js +3 -5
- package/lib/commands/timeouts.js +1 -1
- package/lib/commands/{web.js → web.ts} +305 -263
- package/lib/driver.ts +11 -4
- package/lib/execute-method-map.ts +0 -1
- package/npm-shrinkwrap.json +13 -43
- package/package.json +1 -1
|
@@ -3,6 +3,11 @@ import {timing, util} from 'appium/support';
|
|
|
3
3
|
import {retryInterval} from 'asyncbox';
|
|
4
4
|
import B, {TimeoutError, AggregateError} from 'bluebird';
|
|
5
5
|
import _ from 'lodash';
|
|
6
|
+
import {assertSimulator} from '../utils';
|
|
7
|
+
import type {XCUITestDriver} from '../driver';
|
|
8
|
+
import type {Element, Cookie, Size, Position, Rect} from '@appium/types';
|
|
9
|
+
import type {AtomsElement} from './types';
|
|
10
|
+
import type {CalibrationData} from '../types';
|
|
6
11
|
|
|
7
12
|
const IPHONE_TOP_BAR_HEIGHT = 71;
|
|
8
13
|
const IPHONE_SCROLLED_TOP_BAR_HEIGHT = 41;
|
|
@@ -43,21 +48,23 @@ const ON_APP_CRASH_EVENT = 'app_crash';
|
|
|
43
48
|
const VISIBLE = 'visible';
|
|
44
49
|
const INVISIBLE = 'invisible';
|
|
45
50
|
const DETECT = 'detect';
|
|
46
|
-
const VISIBILITIES = [VISIBLE, INVISIBLE, DETECT];
|
|
51
|
+
const VISIBILITIES = [VISIBLE, INVISIBLE, DETECT] as const;
|
|
47
52
|
|
|
48
53
|
// The position of Safari's tab (search bar).
|
|
49
54
|
// Since iOS 15, the bar is the bottom by default.
|
|
50
55
|
const TAB_BAR_POSITION_TOP = 'top';
|
|
51
56
|
const TAB_BAR_POSITION_BOTTOM = 'bottom';
|
|
52
|
-
const TAB_BAR_POSSITIONS = [TAB_BAR_POSITION_TOP, TAB_BAR_POSITION_BOTTOM];
|
|
57
|
+
const TAB_BAR_POSSITIONS = [TAB_BAR_POSITION_TOP, TAB_BAR_POSITION_BOTTOM] as const;
|
|
53
58
|
|
|
54
59
|
/**
|
|
55
|
-
*
|
|
60
|
+
* Sets the current web frame context.
|
|
61
|
+
*
|
|
62
|
+
* @param frame - Frame identifier (number, string, or null to return to default content)
|
|
56
63
|
* @group Mobile Web Only
|
|
57
|
-
* @
|
|
58
|
-
* @
|
|
64
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
65
|
+
* @throws {errors.NoSuchFrameError} If the specified frame is not found
|
|
59
66
|
*/
|
|
60
|
-
export async function setFrame(frame) {
|
|
67
|
+
export async function setFrame(this: XCUITestDriver, frame: number | string | null): Promise<void> {
|
|
61
68
|
if (!this.isWebContext()) {
|
|
62
69
|
throw new errors.NotImplementedError();
|
|
63
70
|
}
|
|
@@ -70,12 +77,12 @@ export async function setFrame(frame) {
|
|
|
70
77
|
|
|
71
78
|
if (hasElementId(frame)) {
|
|
72
79
|
const atomsElement = this.getAtomsElement(frame);
|
|
73
|
-
const value = await this.executeAtom('get_frame_window', [atomsElement]);
|
|
80
|
+
const value = await this.executeAtom('get_frame_window', [atomsElement]) as {WINDOW: string};
|
|
74
81
|
this.log.debug(`Entering new web frame: '${value.WINDOW}'`);
|
|
75
82
|
this.curWebFrames.unshift(value.WINDOW);
|
|
76
83
|
} else {
|
|
77
84
|
const atom = _.isNumber(frame) ? 'frame_by_index' : 'frame_by_id_or_name';
|
|
78
|
-
const value = await this.executeAtom(atom, [frame]);
|
|
85
|
+
const value = await this.executeAtom(atom, [frame]) as {WINDOW?: string} | null;
|
|
79
86
|
if (_.isNull(value) || _.isUndefined(value.WINDOW)) {
|
|
80
87
|
throw new errors.NoSuchFrameError();
|
|
81
88
|
}
|
|
@@ -85,29 +92,30 @@ export async function setFrame(frame) {
|
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
/**
|
|
88
|
-
*
|
|
95
|
+
* Gets the value of a CSS property for an element.
|
|
96
|
+
*
|
|
97
|
+
* @param propertyName - Name of the CSS property
|
|
98
|
+
* @param el - Element to get the property from
|
|
89
99
|
* @group Mobile Web Only
|
|
90
|
-
* @
|
|
91
|
-
* @param {Element | string} el
|
|
92
|
-
* @returns {Promise<string>}
|
|
100
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
93
101
|
*/
|
|
94
|
-
export async function getCssProperty(propertyName, el) {
|
|
102
|
+
export async function getCssProperty(this: XCUITestDriver, propertyName: string, el: Element | string): Promise<string> {
|
|
95
103
|
if (!this.isWebContext()) {
|
|
96
104
|
throw new errors.NotImplementedError();
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
const atomsElement = this.getAtomsElement(el);
|
|
100
|
-
return await this.executeAtom('get_value_of_css_property', [atomsElement, propertyName]);
|
|
108
|
+
return await this.executeAtom('get_value_of_css_property', [atomsElement, propertyName]) as string;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
/**
|
|
104
|
-
*
|
|
112
|
+
* Submits the form that contains the specified element.
|
|
105
113
|
*
|
|
106
|
-
* @param
|
|
114
|
+
* @param el - The element ID or element object
|
|
107
115
|
* @group Mobile Web Only
|
|
108
|
-
* @
|
|
116
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
109
117
|
*/
|
|
110
|
-
export async function submit(el) {
|
|
118
|
+
export async function submit(this: XCUITestDriver, el: string | Element): Promise<void> {
|
|
111
119
|
if (!this.isWebContext()) {
|
|
112
120
|
throw new errors.NotImplementedError();
|
|
113
121
|
}
|
|
@@ -117,62 +125,69 @@ export async function submit(el) {
|
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
/**
|
|
120
|
-
*
|
|
128
|
+
* Refreshes the current page.
|
|
129
|
+
*
|
|
121
130
|
* @group Mobile Web Only
|
|
131
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
122
132
|
*/
|
|
123
|
-
export async function refresh() {
|
|
133
|
+
export async function refresh(this: XCUITestDriver): Promise<void> {
|
|
124
134
|
if (!this.isWebContext()) {
|
|
125
135
|
throw new errors.NotImplementedError();
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
await
|
|
138
|
+
await this.remote.execute('window.location.reload()');
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
/**
|
|
132
|
-
*
|
|
142
|
+
* Gets the current page URL.
|
|
143
|
+
*
|
|
133
144
|
* @group Mobile Web Only
|
|
134
|
-
* @
|
|
145
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
135
146
|
*/
|
|
136
|
-
export async function getUrl() {
|
|
147
|
+
export async function getUrl(this: XCUITestDriver): Promise<string> {
|
|
137
148
|
if (!this.isWebContext()) {
|
|
138
149
|
throw new errors.NotImplementedError();
|
|
139
150
|
}
|
|
140
151
|
|
|
141
|
-
return await
|
|
152
|
+
return await this.remote.execute('window.location.href') as string;
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
/**
|
|
145
|
-
*
|
|
156
|
+
* Gets the current page title.
|
|
157
|
+
*
|
|
146
158
|
* @group Mobile Web Only
|
|
147
|
-
* @
|
|
159
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
148
160
|
*/
|
|
149
|
-
export async function title() {
|
|
161
|
+
export async function title(this: XCUITestDriver): Promise<string> {
|
|
150
162
|
if (!this.isWebContext()) {
|
|
151
163
|
throw new errors.NotImplementedError();
|
|
152
164
|
}
|
|
153
165
|
|
|
154
|
-
return await
|
|
166
|
+
return await this.remote.execute('window.document.title') as string;
|
|
155
167
|
}
|
|
156
168
|
|
|
157
169
|
/**
|
|
158
|
-
*
|
|
170
|
+
* Gets all cookies for the current page.
|
|
171
|
+
*
|
|
172
|
+
* Cookie values are automatically URI-decoded.
|
|
173
|
+
*
|
|
159
174
|
* @group Mobile Web Only
|
|
160
|
-
* @
|
|
175
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
161
176
|
*/
|
|
162
|
-
export async function getCookies() {
|
|
177
|
+
export async function getCookies(this: XCUITestDriver): Promise<Cookie[]> {
|
|
163
178
|
if (!this.isWebContext()) {
|
|
164
179
|
throw new errors.NotImplementedError();
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
// get the cookies from the remote debugger, or an empty object
|
|
168
|
-
const {cookies} = await
|
|
183
|
+
const {cookies} = await this.remote.getCookies();
|
|
169
184
|
|
|
170
185
|
// the value is URI encoded, so decode it safely
|
|
171
186
|
return cookies.map((cookie) => {
|
|
172
187
|
if (!_.isEmpty(cookie.value)) {
|
|
173
188
|
try {
|
|
174
189
|
cookie.value = decodeURI(cookie.value);
|
|
175
|
-
} catch (error) {
|
|
190
|
+
} catch (error: any) {
|
|
176
191
|
this.log.debug(
|
|
177
192
|
`Cookie ${cookie.name} was not decoded successfully. Cookie value: ${cookie.value}`,
|
|
178
193
|
);
|
|
@@ -185,12 +200,15 @@ export async function getCookies() {
|
|
|
185
200
|
}
|
|
186
201
|
|
|
187
202
|
/**
|
|
188
|
-
*
|
|
203
|
+
* Sets a cookie for the current page.
|
|
204
|
+
*
|
|
205
|
+
* If the cookie's path is not specified, it defaults to '/'.
|
|
206
|
+
*
|
|
207
|
+
* @param cookie - Cookie object to set
|
|
189
208
|
* @group Mobile Web Only
|
|
190
|
-
* @
|
|
191
|
-
* @returns {Promise<void>}
|
|
209
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
192
210
|
*/
|
|
193
|
-
export async function setCookie(cookie) {
|
|
211
|
+
export async function setCookie(this: XCUITestDriver, cookie: Cookie): Promise<void> {
|
|
194
212
|
if (!this.isWebContext()) {
|
|
195
213
|
throw new errors.NotImplementedError();
|
|
196
214
|
}
|
|
@@ -214,12 +232,15 @@ export async function setCookie(cookie) {
|
|
|
214
232
|
}
|
|
215
233
|
|
|
216
234
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
235
|
+
* Deletes a cookie by name.
|
|
236
|
+
*
|
|
237
|
+
* If the cookie is not found, the operation is silently ignored.
|
|
238
|
+
*
|
|
239
|
+
* @param cookieName - Name of the cookie to delete
|
|
220
240
|
* @group Mobile Web Only
|
|
241
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
221
242
|
*/
|
|
222
|
-
export async function deleteCookie(cookieName) {
|
|
243
|
+
export async function deleteCookie(this: XCUITestDriver, cookieName: string): Promise<void> {
|
|
223
244
|
if (!this.isWebContext()) {
|
|
224
245
|
throw new errors.NotImplementedError();
|
|
225
246
|
}
|
|
@@ -235,11 +256,12 @@ export async function deleteCookie(cookieName) {
|
|
|
235
256
|
}
|
|
236
257
|
|
|
237
258
|
/**
|
|
238
|
-
*
|
|
259
|
+
* Deletes all cookies for the current page.
|
|
260
|
+
*
|
|
239
261
|
* @group Mobile Web Only
|
|
240
|
-
* @
|
|
262
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
241
263
|
*/
|
|
242
|
-
export async function deleteCookies() {
|
|
264
|
+
export async function deleteCookies(this: XCUITestDriver): Promise<void> {
|
|
243
265
|
if (!this.isWebContext()) {
|
|
244
266
|
throw new errors.NotImplementedError();
|
|
245
267
|
}
|
|
@@ -249,11 +271,12 @@ export async function deleteCookies() {
|
|
|
249
271
|
}
|
|
250
272
|
|
|
251
273
|
/**
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* @
|
|
274
|
+
* Caches a web element for later use.
|
|
275
|
+
*
|
|
276
|
+
* @param el - Element to cache
|
|
277
|
+
* @returns The cached element wrapper
|
|
255
278
|
*/
|
|
256
|
-
export function cacheWebElement(el) {
|
|
279
|
+
export function cacheWebElement(this: XCUITestDriver, el: Element | string): Element | string {
|
|
257
280
|
if (!_.isPlainObject(el)) {
|
|
258
281
|
return el;
|
|
259
282
|
}
|
|
@@ -269,71 +292,78 @@ export function cacheWebElement(el) {
|
|
|
269
292
|
}
|
|
270
293
|
|
|
271
294
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* @
|
|
295
|
+
* Recursively caches all web elements in a response object.
|
|
296
|
+
*
|
|
297
|
+
* @param response - Response object that may contain web elements
|
|
298
|
+
* @returns Response with cached element wrappers
|
|
275
299
|
*/
|
|
276
|
-
export function cacheWebElements(response) {
|
|
277
|
-
const toCached = (
|
|
300
|
+
export function cacheWebElements(this: XCUITestDriver, response: any): any {
|
|
301
|
+
const toCached = (v: any) => (_.isArray(v) || _.isPlainObject(v)) ? this.cacheWebElements(v) : v;
|
|
278
302
|
|
|
279
303
|
if (_.isArray(response)) {
|
|
280
304
|
return response.map(toCached);
|
|
281
305
|
} else if (_.isPlainObject(response)) {
|
|
282
|
-
const result = {...response, ...(
|
|
306
|
+
const result = {...response, ...(this.cacheWebElement(response) as Element)};
|
|
283
307
|
return _.toPairs(result).reduce((acc, [key, value]) => {
|
|
284
308
|
acc[key] = toCached(value);
|
|
285
309
|
return acc;
|
|
286
|
-
}, {});
|
|
310
|
+
}, {} as any);
|
|
287
311
|
}
|
|
288
312
|
return response;
|
|
289
313
|
}
|
|
290
314
|
|
|
291
315
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* @
|
|
316
|
+
* Executes a Selenium atom script in the current web context.
|
|
317
|
+
*
|
|
318
|
+
* @param atom - Name of the atom to execute
|
|
319
|
+
* @param args - Arguments to pass to the atom
|
|
320
|
+
* @param alwaysDefaultFrame - If true, always use the default frame instead of current frames
|
|
295
321
|
* @privateRemarks This should return `Promise<T>` where `T` extends `unknown`, but that's going to cause a lot of things to break.
|
|
296
|
-
* @this {XCUITestDriver}
|
|
297
322
|
*/
|
|
298
|
-
export async function executeAtom(atom, args, alwaysDefaultFrame = false) {
|
|
299
|
-
|
|
300
|
-
|
|
323
|
+
export async function executeAtom(this: XCUITestDriver, atom: string, args: unknown[], alwaysDefaultFrame: boolean = false): Promise<any> {
|
|
324
|
+
const frames = alwaysDefaultFrame === true ? [] : this.curWebFrames;
|
|
325
|
+
const promise = this.remote.executeAtom(atom, args, frames);
|
|
301
326
|
return await this.waitForAtom(promise);
|
|
302
327
|
}
|
|
303
328
|
|
|
304
329
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* @param
|
|
330
|
+
* Executes a Selenium atom script asynchronously.
|
|
331
|
+
*
|
|
332
|
+
* @param atom - Name of the atom to execute
|
|
333
|
+
* @param args - Arguments to pass to the atom
|
|
308
334
|
*/
|
|
309
|
-
export async function executeAtomAsync(atom, args) {
|
|
335
|
+
export async function executeAtomAsync(this: XCUITestDriver, atom: string, args: any[]): Promise<any> {
|
|
310
336
|
// save the resolve and reject methods of the promise to be waited for
|
|
311
|
-
|
|
337
|
+
const promise = new B((resolve, reject) => {
|
|
312
338
|
this.asyncPromise = {resolve, reject};
|
|
313
339
|
});
|
|
314
|
-
await
|
|
340
|
+
await this.remote.executeAtomAsync(atom, args, this.curWebFrames);
|
|
315
341
|
return await this.waitForAtom(promise);
|
|
316
342
|
}
|
|
317
343
|
|
|
318
344
|
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* @
|
|
322
|
-
* @
|
|
345
|
+
* Gets the atoms-compatible element representation.
|
|
346
|
+
*
|
|
347
|
+
* @template S - Element identifier type
|
|
348
|
+
* @param elOrId - Element or element ID
|
|
349
|
+
* @returns Atoms-compatible element object
|
|
350
|
+
* @throws {errors.StaleElementReferenceError} If the element is not in the cache
|
|
323
351
|
*/
|
|
324
|
-
export function getAtomsElement(elOrId) {
|
|
352
|
+
export function getAtomsElement<S extends string = string>(this: XCUITestDriver, elOrId: S | Element<S>): AtomsElement<S> {
|
|
325
353
|
const elId = util.unwrapElement(elOrId);
|
|
326
354
|
if (!this.webElementsCache?.has(elId)) {
|
|
327
355
|
throw new errors.StaleElementReferenceError();
|
|
328
356
|
}
|
|
329
|
-
return {ELEMENT: this.webElementsCache.get(elId)}
|
|
357
|
+
return {ELEMENT: this.webElementsCache.get(elId)} as AtomsElement<S>;
|
|
330
358
|
}
|
|
331
359
|
|
|
332
360
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
361
|
+
* Converts elements in an argument array to atoms-compatible format.
|
|
362
|
+
*
|
|
363
|
+
* @param args - Array of arguments that may contain elements
|
|
364
|
+
* @returns Array with elements converted to atoms format
|
|
335
365
|
*/
|
|
336
|
-
export function convertElementsForAtoms(args = []) {
|
|
366
|
+
export function convertElementsForAtoms(this: XCUITestDriver, args: readonly any[] = []): any[] {
|
|
337
367
|
return args.map((arg) => {
|
|
338
368
|
if (hasElementId(arg)) {
|
|
339
369
|
try {
|
|
@@ -350,19 +380,22 @@ export function convertElementsForAtoms(args = []) {
|
|
|
350
380
|
}
|
|
351
381
|
|
|
352
382
|
/**
|
|
383
|
+
* Extracts the element ID from an element object.
|
|
353
384
|
*
|
|
354
|
-
* @param
|
|
355
|
-
* @returns
|
|
385
|
+
* @param element - Element object
|
|
386
|
+
* @returns Element ID if found, undefined otherwise
|
|
356
387
|
*/
|
|
357
|
-
export function getElementId(element) {
|
|
388
|
+
export function getElementId(element: any): string | undefined {
|
|
358
389
|
return element?.ELEMENT || element?.[W3C_WEB_ELEMENT_IDENTIFIER];
|
|
359
390
|
}
|
|
360
391
|
|
|
361
392
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
393
|
+
* Checks if an object has an element ID (type guard).
|
|
394
|
+
*
|
|
395
|
+
* @param element - Object to check
|
|
396
|
+
* @returns True if the object has an element ID
|
|
364
397
|
*/
|
|
365
|
-
export function hasElementId(element) {
|
|
398
|
+
export function hasElementId(element: any): element is Element {
|
|
366
399
|
return (
|
|
367
400
|
util.hasValue(element) &&
|
|
368
401
|
(util.hasValue(element.ELEMENT) || util.hasValue(element[W3C_WEB_ELEMENT_IDENTIFIER]))
|
|
@@ -370,24 +403,32 @@ export function hasElementId(element) {
|
|
|
370
403
|
}
|
|
371
404
|
|
|
372
405
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* @param
|
|
376
|
-
* @param
|
|
377
|
-
* @param
|
|
378
|
-
* @
|
|
406
|
+
* Finds one or more web elements using the specified strategy.
|
|
407
|
+
*
|
|
408
|
+
* @param strategy - Locator strategy (e.g., 'id', 'css selector')
|
|
409
|
+
* @param selector - Selector value
|
|
410
|
+
* @param many - If true, returns array of elements; if false, returns single element
|
|
411
|
+
* @param ctx - Optional context element to search within
|
|
412
|
+
* @returns Element or array of elements
|
|
413
|
+
* @throws {errors.NoSuchElementError} If element not found and many is false
|
|
379
414
|
*/
|
|
380
|
-
export async function findWebElementOrElements(
|
|
415
|
+
export async function findWebElementOrElements(
|
|
416
|
+
this: XCUITestDriver,
|
|
417
|
+
strategy: string,
|
|
418
|
+
selector: string,
|
|
419
|
+
many?: boolean,
|
|
420
|
+
ctx?: Element | string | null,
|
|
421
|
+
): Promise<Element | Element[]> {
|
|
381
422
|
const contextElement = _.isNil(ctx) ? null : this.getAtomsElement(ctx);
|
|
382
423
|
const atomName = many ? 'find_elements' : 'find_element_fragment';
|
|
383
|
-
let element;
|
|
424
|
+
let element: any;
|
|
384
425
|
const doFind = async () => {
|
|
385
426
|
element = await this.executeAtom(atomName, [strategy, selector, contextElement]);
|
|
386
427
|
return !_.isNull(element);
|
|
387
428
|
};
|
|
388
429
|
try {
|
|
389
430
|
await this.implicitWaitForCondition(doFind);
|
|
390
|
-
} catch (err) {
|
|
431
|
+
} catch (err: any) {
|
|
391
432
|
if (err.message && _.isFunction(err.message.match) && err.message.match(/Condition unmet/)) {
|
|
392
433
|
// condition was not met setting res to empty array
|
|
393
434
|
element = [];
|
|
@@ -406,27 +447,33 @@ export async function findWebElementOrElements(strategy, selector, many, ctx) {
|
|
|
406
447
|
}
|
|
407
448
|
|
|
408
449
|
/**
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
450
|
+
* Clicks at the specified web coordinates.
|
|
451
|
+
*
|
|
452
|
+
* Coordinates are automatically translated from web to native coordinates.
|
|
453
|
+
*
|
|
454
|
+
* @param x - X coordinate in web space
|
|
455
|
+
* @param y - Y coordinate in web space
|
|
412
456
|
*/
|
|
413
|
-
export async function clickWebCoords(x, y) {
|
|
457
|
+
export async function clickWebCoords(this: XCUITestDriver, x: number, y: number): Promise<void> {
|
|
414
458
|
const {x: translatedX, y: translatedY} = await this.translateWebCoords(x, y);
|
|
415
459
|
await this.mobileTap(translatedX, translatedY);
|
|
416
460
|
}
|
|
417
461
|
|
|
418
462
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
463
|
+
* Determines if the current Safari session is running on an iPhone.
|
|
464
|
+
*
|
|
465
|
+
* The result is cached after the first call.
|
|
466
|
+
*
|
|
467
|
+
* @returns True if running on iPhone, false otherwise
|
|
421
468
|
*/
|
|
422
|
-
export async function getSafariIsIphone() {
|
|
469
|
+
export async function getSafariIsIphone(this: XCUITestDriver): Promise<boolean> {
|
|
423
470
|
if (_.isBoolean(this._isSafariIphone)) {
|
|
424
471
|
return this._isSafariIphone;
|
|
425
472
|
}
|
|
426
473
|
try {
|
|
427
|
-
const userAgent =
|
|
474
|
+
const userAgent = await this.execute('return navigator.userAgent') as string;
|
|
428
475
|
this._isSafariIphone = userAgent.toLowerCase().includes('iphone');
|
|
429
|
-
} catch (err) {
|
|
476
|
+
} catch (err: any) {
|
|
430
477
|
this.log.warn(`Unable to find device type from useragent. Assuming iPhone`);
|
|
431
478
|
this.log.debug(`Error: ${err.message}`);
|
|
432
479
|
}
|
|
@@ -434,15 +481,16 @@ export async function getSafariIsIphone() {
|
|
|
434
481
|
}
|
|
435
482
|
|
|
436
483
|
/**
|
|
437
|
-
*
|
|
438
|
-
*
|
|
484
|
+
* Gets the device size from Safari's perspective.
|
|
485
|
+
*
|
|
486
|
+
* Returns normalized dimensions (width <= height).
|
|
487
|
+
*
|
|
488
|
+
* @returns Device size with width and height
|
|
439
489
|
*/
|
|
440
|
-
export async function getSafariDeviceSize() {
|
|
490
|
+
export async function getSafariDeviceSize(this: XCUITestDriver): Promise<Size> {
|
|
441
491
|
const script =
|
|
442
492
|
'return {height: window.screen.availHeight * window.devicePixelRatio, width: window.screen.availWidth * window.devicePixelRatio};';
|
|
443
|
-
const {width, height} =
|
|
444
|
-
await this.execute(script)
|
|
445
|
-
);
|
|
493
|
+
const {width, height} = await this.execute(script) as Size;
|
|
446
494
|
const [normHeight, normWidth] = height > width ? [height, width] : [width, height];
|
|
447
495
|
return {
|
|
448
496
|
width: normWidth,
|
|
@@ -451,10 +499,13 @@ export async function getSafariDeviceSize() {
|
|
|
451
499
|
}
|
|
452
500
|
|
|
453
501
|
/**
|
|
454
|
-
*
|
|
455
|
-
*
|
|
502
|
+
* Determines if the current device has a notch (iPhone X and later).
|
|
503
|
+
*
|
|
504
|
+
* The result is cached after the first call.
|
|
505
|
+
*
|
|
506
|
+
* @returns True if device has a notch, false otherwise
|
|
456
507
|
*/
|
|
457
|
-
export async function getSafariIsNotched() {
|
|
508
|
+
export async function getSafariIsNotched(this: XCUITestDriver): Promise<boolean> {
|
|
458
509
|
if (_.isBoolean(this._isSafariNotched)) {
|
|
459
510
|
return this._isSafariNotched;
|
|
460
511
|
}
|
|
@@ -466,7 +517,7 @@ export async function getSafariIsNotched() {
|
|
|
466
517
|
this._isSafariNotched = true;
|
|
467
518
|
}
|
|
468
519
|
}
|
|
469
|
-
} catch (err) {
|
|
520
|
+
} catch (err: any) {
|
|
470
521
|
this.log.warn(
|
|
471
522
|
`Unable to find device type from dimensions. Assuming the device is not notched`,
|
|
472
523
|
);
|
|
@@ -476,9 +527,20 @@ export async function getSafariIsNotched() {
|
|
|
476
527
|
}
|
|
477
528
|
|
|
478
529
|
/**
|
|
479
|
-
*
|
|
530
|
+
* Calculates and applies extra offset for web coordinate translation.
|
|
531
|
+
*
|
|
532
|
+
* Takes into account Safari UI elements like tab bars, smart app banners, and device notches.
|
|
533
|
+
* Modifies wvPos and realDims in place.
|
|
534
|
+
*
|
|
535
|
+
* @param wvPos - WebView position object (modified in place)
|
|
536
|
+
* @param realDims - Real dimensions object (modified in place)
|
|
537
|
+
* @throws {errors.InvalidArgumentError} If Safari tab bar position is invalid
|
|
480
538
|
*/
|
|
481
|
-
export async function getExtraTranslateWebCoordsOffset(
|
|
539
|
+
export async function getExtraTranslateWebCoordsOffset(
|
|
540
|
+
this: XCUITestDriver,
|
|
541
|
+
wvPos: {x: number; y: number},
|
|
542
|
+
realDims: {w: number; h: number},
|
|
543
|
+
): Promise<void> {
|
|
482
544
|
let topOffset = 0;
|
|
483
545
|
let bottomOffset = 0;
|
|
484
546
|
|
|
@@ -489,7 +551,7 @@ export async function getExtraTranslateWebCoordsOffset(wvPos, realDims) {
|
|
|
489
551
|
const {
|
|
490
552
|
nativeWebTapTabBarVisibility,
|
|
491
553
|
nativeWebTapSmartAppBannerVisibility,
|
|
492
|
-
safariTabBarPosition = util.compareVersions(
|
|
554
|
+
safariTabBarPosition = util.compareVersions(this.opts.platformVersion as string, '>=', '15.0') &&
|
|
493
555
|
isIphone
|
|
494
556
|
? TAB_BAR_POSITION_BOTTOM
|
|
495
557
|
: TAB_BAR_POSITION_TOP,
|
|
@@ -498,14 +560,14 @@ export async function getExtraTranslateWebCoordsOffset(wvPos, realDims) {
|
|
|
498
560
|
let bannerVisibility = _.lowerCase(String(nativeWebTapSmartAppBannerVisibility));
|
|
499
561
|
const tabBarPosition = _.lowerCase(String(safariTabBarPosition));
|
|
500
562
|
|
|
501
|
-
if (!VISIBILITIES.includes(tabBarVisibility)) {
|
|
563
|
+
if (!VISIBILITIES.includes(tabBarVisibility as any)) {
|
|
502
564
|
tabBarVisibility = DETECT;
|
|
503
565
|
}
|
|
504
|
-
if (!VISIBILITIES.includes(bannerVisibility)) {
|
|
566
|
+
if (!VISIBILITIES.includes(bannerVisibility as any)) {
|
|
505
567
|
bannerVisibility = DETECT;
|
|
506
568
|
}
|
|
507
569
|
|
|
508
|
-
if (!TAB_BAR_POSSITIONS.includes(tabBarPosition)) {
|
|
570
|
+
if (!TAB_BAR_POSSITIONS.includes(tabBarPosition as any)) {
|
|
509
571
|
throw new errors.InvalidArgumentError(
|
|
510
572
|
`${safariTabBarPosition} is invalid as Safari tab bar position. Available positions are ${TAB_BAR_POSSITIONS}.`,
|
|
511
573
|
);
|
|
@@ -516,12 +578,12 @@ export async function getExtraTranslateWebCoordsOffset(wvPos, realDims) {
|
|
|
516
578
|
const orientation = realDims.h > realDims.w ? 'PORTRAIT' : 'LANDSCAPE';
|
|
517
579
|
|
|
518
580
|
const notchOffset = isNotched
|
|
519
|
-
? util.compareVersions(
|
|
581
|
+
? util.compareVersions(this.opts.platformVersion as string, '=', '13.0')
|
|
520
582
|
? IPHONE_X_NOTCH_OFFSET_IOS_13
|
|
521
583
|
: IPHONE_X_NOTCH_OFFSET_IOS
|
|
522
584
|
: 0;
|
|
523
585
|
|
|
524
|
-
const isScrolled = await this.execute('return document.documentElement.scrollTop > 0');
|
|
586
|
+
const isScrolled = await this.execute('return document.documentElement.scrollTop > 0') as boolean;
|
|
525
587
|
if (isScrolled) {
|
|
526
588
|
topOffset = IPHONE_SCROLLED_TOP_BAR_HEIGHT + notchOffset;
|
|
527
589
|
|
|
@@ -571,12 +633,17 @@ export async function getExtraTranslateWebCoordsOffset(wvPos, realDims) {
|
|
|
571
633
|
}
|
|
572
634
|
|
|
573
635
|
/**
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
* @param
|
|
577
|
-
* @
|
|
636
|
+
* Calculates additional offset for native web tap based on smart app banner visibility.
|
|
637
|
+
*
|
|
638
|
+
* @param isIphone - Whether the device is an iPhone
|
|
639
|
+
* @param bannerVisibility - Banner visibility setting ('visible', 'invisible', or 'detect')
|
|
640
|
+
* @returns Additional offset in pixels
|
|
578
641
|
*/
|
|
579
|
-
export async function getExtraNativeWebTapOffset(
|
|
642
|
+
export async function getExtraNativeWebTapOffset(
|
|
643
|
+
this: XCUITestDriver,
|
|
644
|
+
isIphone: boolean,
|
|
645
|
+
bannerVisibility: string,
|
|
646
|
+
): Promise<number> {
|
|
580
647
|
let offset = 0;
|
|
581
648
|
|
|
582
649
|
if (bannerVisibility === VISIBLE) {
|
|
@@ -585,9 +652,7 @@ export async function getExtraNativeWebTapOffset(isIphone, bannerVisibility) {
|
|
|
585
652
|
: IPAD_WEB_COORD_SMART_APP_BANNER_OFFSET;
|
|
586
653
|
} else if (bannerVisibility === DETECT) {
|
|
587
654
|
// try to see if there is an Smart App Banner
|
|
588
|
-
const banners =
|
|
589
|
-
await this.findNativeElementOrElements('accessibility id', 'Close app download offer', true)
|
|
590
|
-
);
|
|
655
|
+
const banners = await this.findNativeElementOrElements('accessibility id', 'Close app download offer', true) as Element[];
|
|
591
656
|
if (banners?.length) {
|
|
592
657
|
offset += isIphone
|
|
593
658
|
? IPHONE_WEB_COORD_SMART_APP_BANNER_OFFSET
|
|
@@ -600,11 +665,13 @@ export async function getExtraNativeWebTapOffset(isIphone, bannerVisibility) {
|
|
|
600
665
|
}
|
|
601
666
|
|
|
602
667
|
/**
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
668
|
+
* Performs a native tap on a web element.
|
|
669
|
+
*
|
|
670
|
+
* Attempts to use a simple native tap first, falling back to coordinate-based tapping if needed.
|
|
671
|
+
*
|
|
672
|
+
* @param el - Element to tap
|
|
606
673
|
*/
|
|
607
|
-
export async function nativeWebTap(el) {
|
|
674
|
+
export async function nativeWebTap(this: XCUITestDriver, el: any): Promise<void> {
|
|
608
675
|
const atomsElement = this.getAtomsElement(el);
|
|
609
676
|
|
|
610
677
|
// if strict native tap, do not try to do it with WDA directly
|
|
@@ -616,25 +683,26 @@ export async function nativeWebTap(el) {
|
|
|
616
683
|
}
|
|
617
684
|
this.log.warn('Unable to do simple native web tap. Attempting to convert coordinates');
|
|
618
685
|
|
|
619
|
-
const [size, coordinates] =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
this.executeAtom('get_top_left_coordinates', [atomsElement]),
|
|
624
|
-
])
|
|
625
|
-
);
|
|
686
|
+
const [size, coordinates] = await B.Promise.all([
|
|
687
|
+
this.executeAtom('get_size', [atomsElement]),
|
|
688
|
+
this.executeAtom('get_top_left_coordinates', [atomsElement]),
|
|
689
|
+
]) as [Size, Position];
|
|
626
690
|
const {width, height} = size;
|
|
627
691
|
const {x, y} = coordinates;
|
|
628
692
|
await this.clickWebCoords(x + width / 2, y + height / 2);
|
|
629
693
|
}
|
|
630
694
|
|
|
631
695
|
/**
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
696
|
+
* Translates web coordinates to native screen coordinates.
|
|
697
|
+
*
|
|
698
|
+
* Uses calibration data if available, otherwise falls back to legacy algorithm.
|
|
699
|
+
*
|
|
700
|
+
* @param x - X coordinate in web space
|
|
701
|
+
* @param y - Y coordinate in web space
|
|
702
|
+
* @returns Translated position in native coordinates
|
|
703
|
+
* @throws {Error} If no WebView is found or if translation fails
|
|
636
704
|
*/
|
|
637
|
-
export async function translateWebCoords(x, y) {
|
|
705
|
+
export async function translateWebCoords(this: XCUITestDriver, x: number, y: number): Promise<Position> {
|
|
638
706
|
this.log.debug(`Translating web coordinates (${JSON.stringify({x, y})}) to native coordinates`);
|
|
639
707
|
|
|
640
708
|
if (this.webviewCalibrationResult) {
|
|
@@ -642,7 +710,7 @@ export async function translateWebCoords(x, y) {
|
|
|
642
710
|
const { offsetX, offsetY, pixelRatioX, pixelRatioY } = this.webviewCalibrationResult;
|
|
643
711
|
const cmd = '(function () {return {innerWidth: window.innerWidth, innerHeight: window.innerHeight, ' +
|
|
644
712
|
'outerWidth: window.outerWidth, outerHeight: window.outerHeight}; })()';
|
|
645
|
-
const wvDims = await
|
|
713
|
+
const wvDims = await this.remote.execute(cmd) as {innerWidth: number; innerHeight: number; outerWidth: number; outerHeight: number};
|
|
646
714
|
// https://tripleodeon.com/2011/12/first-understand-your-screen/
|
|
647
715
|
const shouldApplyPixelRatio = wvDims.innerWidth > wvDims.outerWidth
|
|
648
716
|
|| wvDims.innerHeight > wvDims.outerHeight;
|
|
@@ -658,17 +726,14 @@ export async function translateWebCoords(x, y) {
|
|
|
658
726
|
}
|
|
659
727
|
|
|
660
728
|
// absolutize web coords
|
|
661
|
-
|
|
662
|
-
let webview;
|
|
729
|
+
let webview: Element | undefined | string;
|
|
663
730
|
try {
|
|
664
|
-
webview =
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
)
|
|
671
|
-
);
|
|
731
|
+
webview = await retryInterval(
|
|
732
|
+
5,
|
|
733
|
+
100,
|
|
734
|
+
async () =>
|
|
735
|
+
await this.findNativeElementOrElements('class name', 'XCUIElementTypeWebView', false),
|
|
736
|
+
) as Element | undefined;
|
|
672
737
|
} catch {}
|
|
673
738
|
|
|
674
739
|
if (!webview) {
|
|
@@ -677,12 +742,12 @@ export async function translateWebCoords(x, y) {
|
|
|
677
742
|
|
|
678
743
|
webview = util.unwrapElement(webview);
|
|
679
744
|
|
|
680
|
-
const rect =
|
|
745
|
+
const rect = await this.proxyCommand(`/element/${webview}/rect`, 'GET') as Rect;
|
|
681
746
|
const wvPos = {x: rect.x, y: rect.y};
|
|
682
747
|
const realDims = {w: rect.width, h: rect.height};
|
|
683
748
|
|
|
684
749
|
const cmd = '(function () { return {w: window.innerWidth, h: window.innerHeight}; })()';
|
|
685
|
-
const wvDims = await
|
|
750
|
+
const wvDims = await this.remote.execute(cmd) as {w: number; h: number};
|
|
686
751
|
|
|
687
752
|
// keep track of implicit wait, and set locally to 0
|
|
688
753
|
// https://github.com/appium/appium/issues/14988
|
|
@@ -727,18 +792,23 @@ export async function translateWebCoords(x, y) {
|
|
|
727
792
|
}
|
|
728
793
|
|
|
729
794
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
795
|
+
* Checks if an alert is currently present.
|
|
796
|
+
*
|
|
797
|
+
* @returns True if an alert is present, false otherwise
|
|
732
798
|
*/
|
|
733
|
-
export async function checkForAlert() {
|
|
799
|
+
export async function checkForAlert(this: XCUITestDriver): Promise<boolean> {
|
|
734
800
|
return _.isString(await this.getAlertText());
|
|
735
801
|
}
|
|
736
802
|
|
|
737
803
|
/**
|
|
738
|
-
*
|
|
739
|
-
*
|
|
804
|
+
* Waits for an atom promise to resolve, monitoring for alerts during execution.
|
|
805
|
+
*
|
|
806
|
+
* @param promise - Promise returned by atom execution
|
|
807
|
+
* @returns The result of the atom execution
|
|
808
|
+
* @throws {errors.UnexpectedAlertOpenError} If an alert appears during execution
|
|
809
|
+
* @throws {errors.TimeoutError} If the atom execution times out
|
|
740
810
|
*/
|
|
741
|
-
export async function waitForAtom(promise) {
|
|
811
|
+
export async function waitForAtom(this: XCUITestDriver, promise: Promise<any>): Promise<any> {
|
|
742
812
|
const timer = new timing.Timer().start();
|
|
743
813
|
|
|
744
814
|
const atomWaitTimeoutMs = _.isNumber(this.opts.webviewAtomWaitTimeout) && this.opts.webviewAtomWaitTimeout > 0
|
|
@@ -747,10 +817,10 @@ export async function waitForAtom(promise) {
|
|
|
747
817
|
// need to check for alert while the atom is being executed.
|
|
748
818
|
// so notify ourselves when it happens
|
|
749
819
|
const timedAtomPromise = B.resolve(promise).timeout(atomWaitTimeoutMs);
|
|
750
|
-
const handlePromiseError = async (p) => {
|
|
820
|
+
const handlePromiseError = async (p: Promise<any>) => {
|
|
751
821
|
try {
|
|
752
822
|
return await p;
|
|
753
|
-
} catch (err) {
|
|
823
|
+
} catch (err: any) {
|
|
754
824
|
const originalError = err instanceof AggregateError ? err[0] : err;
|
|
755
825
|
this.log.debug(`Error received while executing atom: ${originalError.message}`);
|
|
756
826
|
throw (
|
|
@@ -770,8 +840,8 @@ export async function waitForAtom(promise) {
|
|
|
770
840
|
// ...otherwise make sure there is no unexpected alert covering the element
|
|
771
841
|
this._waitingAtoms.count++;
|
|
772
842
|
|
|
773
|
-
let onAlertCallback;
|
|
774
|
-
let onAppCrashCallback;
|
|
843
|
+
let onAlertCallback: (() => void) | undefined;
|
|
844
|
+
let onAppCrashCallback: ((err: any) => void) | undefined;
|
|
775
845
|
try {
|
|
776
846
|
// only restart the monitor if it is not running already
|
|
777
847
|
if (this._waitingAtoms.alertMonitor.isResolved()) {
|
|
@@ -782,7 +852,7 @@ export async function waitForAtom(promise) {
|
|
|
782
852
|
if (await this.checkForAlert()) {
|
|
783
853
|
this._waitingAtoms.alertNotifier.emit(ON_OBSTRUCTING_ALERT_EVENT);
|
|
784
854
|
}
|
|
785
|
-
} catch (err) {
|
|
855
|
+
} catch (err: any) {
|
|
786
856
|
if (isErrorType(err, errors.InvalidElementStateError)) {
|
|
787
857
|
this._waitingAtoms.alertNotifier.emit(ON_APP_CRASH_EVENT, err);
|
|
788
858
|
}
|
|
@@ -817,23 +887,25 @@ export async function waitForAtom(promise) {
|
|
|
817
887
|
}
|
|
818
888
|
|
|
819
889
|
/**
|
|
820
|
-
*
|
|
821
|
-
*
|
|
890
|
+
* Performs browser navigation (back, forward, etc.) using history API.
|
|
891
|
+
*
|
|
892
|
+
* @param navType - Navigation type (e.g., 'back', 'forward')
|
|
822
893
|
*/
|
|
823
|
-
export async function mobileWebNav(navType) {
|
|
824
|
-
|
|
894
|
+
export async function mobileWebNav(this: XCUITestDriver, navType: string): Promise<void> {
|
|
895
|
+
this.remote.allowNavigationWithoutReload = true;
|
|
825
896
|
try {
|
|
826
897
|
await this.executeAtom('execute_script', [`history.${navType}();`, null]);
|
|
827
898
|
} finally {
|
|
828
|
-
|
|
899
|
+
this.remote.allowNavigationWithoutReload = false;
|
|
829
900
|
}
|
|
830
901
|
}
|
|
831
902
|
|
|
832
903
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
904
|
+
* Gets the base URL for accessing WDA HTTP endpoints.
|
|
905
|
+
*
|
|
906
|
+
* @returns The base URL (e.g., 'http://127.0.0.1:8100')
|
|
835
907
|
*/
|
|
836
|
-
export function getWdaLocalhostRoot() {
|
|
908
|
+
export function getWdaLocalhostRoot(this: XCUITestDriver): string {
|
|
837
909
|
const remotePort =
|
|
838
910
|
((this.isRealDevice() ? this.opts.wdaRemotePort : null)
|
|
839
911
|
?? this.wda?.url?.port
|
|
@@ -853,31 +925,28 @@ export function getWdaLocalhostRoot() {
|
|
|
853
925
|
* The returned value could also be used to manually transform web coordinates
|
|
854
926
|
* to real devices ones in client scripts.
|
|
855
927
|
*
|
|
856
|
-
* @
|
|
857
|
-
* @
|
|
928
|
+
* @returns Calibration data with offset and pixel ratio information
|
|
929
|
+
* @throws {errors.NotImplementedError} If not in a web context
|
|
858
930
|
*/
|
|
859
|
-
export async function mobileCalibrateWebToRealCoordinatesTranslation() {
|
|
931
|
+
export async function mobileCalibrateWebToRealCoordinatesTranslation(this: XCUITestDriver): Promise<CalibrationData> {
|
|
860
932
|
if (!this.isWebContext()) {
|
|
861
933
|
throw new errors.NotImplementedError('This API can only be called from a web context');
|
|
862
934
|
}
|
|
863
935
|
|
|
864
936
|
const currentUrl = await this.getUrl();
|
|
865
937
|
await this.setUrl(`${this.getWdaLocalhostRoot()}/calibrate`);
|
|
866
|
-
const {width, height} =
|
|
867
|
-
await this.proxyCommand('/window/rect', 'GET')
|
|
868
|
-
);
|
|
938
|
+
const {width, height} = await this.proxyCommand('/window/rect', 'GET') as Rect;
|
|
869
939
|
const [centerX, centerY] = [width / 2, height / 2];
|
|
870
940
|
const errorPrefix = 'Cannot determine web view coordinates offset. Are you in Safari context?';
|
|
871
941
|
|
|
872
|
-
const performCalibrationTap = async (
|
|
942
|
+
const performCalibrationTap = async (tapX: number, tapY: number): Promise<Position> => {
|
|
873
943
|
await this.mobileTap(tapX, tapY);
|
|
874
|
-
|
|
875
|
-
let result;
|
|
944
|
+
let result: Position;
|
|
876
945
|
try {
|
|
877
946
|
const title = await this.title();
|
|
878
947
|
this.log.debug(JSON.stringify(title));
|
|
879
|
-
result = _.isPlainObject(title) ? title : JSON.parse(title);
|
|
880
|
-
} catch (e) {
|
|
948
|
+
result = _.isPlainObject(title) ? title as unknown as Position : JSON.parse(title) as Position;
|
|
949
|
+
} catch (e: any) {
|
|
881
950
|
throw new Error(`${errorPrefix} Original error: ${e.message}`);
|
|
882
951
|
}
|
|
883
952
|
const {x, y} = result;
|
|
@@ -912,7 +981,7 @@ export async function mobileCalibrateWebToRealCoordinatesTranslation() {
|
|
|
912
981
|
// restore the previous url
|
|
913
982
|
await this.setUrl(currentUrl);
|
|
914
983
|
}
|
|
915
|
-
const result =
|
|
984
|
+
const result = this.webviewCalibrationResult as CalibrationData;
|
|
916
985
|
return {
|
|
917
986
|
...result,
|
|
918
987
|
offsetX: Math.round(result.offsetX),
|
|
@@ -920,24 +989,10 @@ export async function mobileCalibrateWebToRealCoordinatesTranslation() {
|
|
|
920
989
|
};
|
|
921
990
|
}
|
|
922
991
|
|
|
923
|
-
/**
|
|
924
|
-
* @typedef {Object} SafariOpts
|
|
925
|
-
* @property {object} preferences An object containing Safari settings to be updated.
|
|
926
|
-
* The list of available setting names and their values could be retrieved by
|
|
927
|
-
* changing the corresponding Safari settings in the UI and then inspecting
|
|
928
|
-
* 'Library/Preferences/com.apple.mobilesafari.plist' file inside of
|
|
929
|
-
* com.apple.mobilesafari app container.
|
|
930
|
-
* The full path to the Mobile Safari's container could be retrieved from
|
|
931
|
-
* `xcrun simctl get_app_container <sim_udid> com.apple.mobilesafari data`
|
|
932
|
-
* command output.
|
|
933
|
-
* Use the `xcrun simctl spawn <sim_udid> defaults read <path_to_plist>` command
|
|
934
|
-
* to print the plist content to the Terminal.
|
|
935
|
-
*/
|
|
936
|
-
|
|
937
992
|
/**
|
|
938
993
|
* Updates Mobile Safari preferences on an iOS Simulator
|
|
939
994
|
*
|
|
940
|
-
* @param
|
|
995
|
+
* @param preferences - An object containing Safari settings to be updated.
|
|
941
996
|
* The list of available setting names and their values can be retrieved by changing the
|
|
942
997
|
* corresponding Safari settings in the UI and then inspecting
|
|
943
998
|
* `Library/Preferences/com.apple.mobilesafari.plist` file inside of the `com.apple.mobilesafari`
|
|
@@ -947,33 +1002,31 @@ export async function mobileCalibrateWebToRealCoordinatesTranslation() {
|
|
|
947
1002
|
* the plist content to the Terminal.
|
|
948
1003
|
*
|
|
949
1004
|
* @group Simulator Only
|
|
950
|
-
* @
|
|
951
|
-
* @throws {
|
|
952
|
-
* @this {XCUITestDriver}
|
|
1005
|
+
* @throws {Error} If run on a real device
|
|
1006
|
+
* @throws {errors.InvalidArgumentError} If the preferences argument is invalid
|
|
953
1007
|
*/
|
|
954
|
-
export async function mobileUpdateSafariPreferences(preferences) {
|
|
955
|
-
|
|
956
|
-
throw new Error('This extension is only available for Simulator');
|
|
957
|
-
}
|
|
1008
|
+
export async function mobileUpdateSafariPreferences(this: XCUITestDriver, preferences: Record<string, any>): Promise<void> {
|
|
1009
|
+
const simulator = assertSimulator.call(this, 'Updating Safari preferences');
|
|
958
1010
|
if (!_.isPlainObject(preferences)) {
|
|
959
1011
|
throw new errors.InvalidArgumentError('"preferences" argument must be a valid object');
|
|
960
1012
|
}
|
|
961
1013
|
|
|
962
1014
|
this.log.debug(`About to update Safari preferences: ${JSON.stringify(preferences)}`);
|
|
963
|
-
await
|
|
1015
|
+
await simulator.updateSafariSettings(preferences);
|
|
964
1016
|
}
|
|
965
1017
|
|
|
966
1018
|
/**
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
* @
|
|
1019
|
+
* Generates a timeout error with detailed information about atom execution failure.
|
|
1020
|
+
*
|
|
1021
|
+
* @param timer - Timer instance to get duration from
|
|
1022
|
+
* @returns Timeout error with descriptive message
|
|
970
1023
|
*/
|
|
971
|
-
async function generateAtomTimeoutError(timer) {
|
|
1024
|
+
async function generateAtomTimeoutError(this: XCUITestDriver, timer: timing.Timer): Promise<InstanceType<typeof errors.TimeoutError>> {
|
|
972
1025
|
let message = (
|
|
973
1026
|
`The remote Safari debugger did not respond to the requested ` +
|
|
974
1027
|
`command after ${timer.getDuration().asMilliSeconds}ms. `
|
|
975
1028
|
);
|
|
976
|
-
message += (await this.
|
|
1029
|
+
message += (await this._remote?.isJavascriptExecutionBlocked()) ? (
|
|
977
1030
|
`It appears that JavaScript execution is blocked, ` +
|
|
978
1031
|
`which could be caused by either a modal dialog obstructing the current page, ` +
|
|
979
1032
|
`or a JavaScript routine monopolizing the event loop.`
|
|
@@ -991,38 +1044,41 @@ async function generateAtomTimeoutError(timer) {
|
|
|
991
1044
|
}
|
|
992
1045
|
|
|
993
1046
|
/**
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
1047
|
+
* Attempts to tap a web element using native element matching.
|
|
1048
|
+
*
|
|
1049
|
+
* Tries to find a native element by matching text content, then taps it directly.
|
|
1050
|
+
*
|
|
1051
|
+
* @param atomsElement - Atoms-compatible element to tap
|
|
1052
|
+
* @returns True if the native tap was successful, false otherwise
|
|
997
1053
|
*/
|
|
998
|
-
async function tapWebElementNatively(atomsElement) {
|
|
1054
|
+
async function tapWebElementNatively(this: XCUITestDriver, atomsElement: AtomsElement): Promise<boolean> {
|
|
999
1055
|
// try to get the text of the element, which will be accessible in the
|
|
1000
1056
|
// native context
|
|
1001
1057
|
try {
|
|
1002
1058
|
const [text1, text2] = await B.all([
|
|
1003
1059
|
this.executeAtom('get_text', [atomsElement]),
|
|
1004
1060
|
this.executeAtom('get_attribute_value', [atomsElement, 'value'])
|
|
1005
|
-
]);
|
|
1061
|
+
]) as [string | null, string | null];
|
|
1006
1062
|
const text = text1 || text2;
|
|
1007
1063
|
if (!text) {
|
|
1008
1064
|
return false;
|
|
1009
1065
|
}
|
|
1010
1066
|
|
|
1011
|
-
const els = await this.findNativeElementOrElements('accessibility id', text, true);
|
|
1067
|
+
const els = await this.findNativeElementOrElements('accessibility id', text, true) as Element[];
|
|
1012
1068
|
if (![1, 2].includes(els.length)) {
|
|
1013
1069
|
return false;
|
|
1014
1070
|
}
|
|
1015
1071
|
|
|
1016
1072
|
const el = els[0];
|
|
1017
1073
|
// use tap because on iOS 11.2 and below `nativeClick` crashes WDA
|
|
1018
|
-
const rect =
|
|
1074
|
+
const rect = await this.proxyCommand(
|
|
1019
1075
|
`/element/${util.unwrapElement(el)}/rect`, 'GET'
|
|
1020
|
-
)
|
|
1076
|
+
) as Rect;
|
|
1021
1077
|
if (els.length > 1) {
|
|
1022
1078
|
const el2 = els[1];
|
|
1023
|
-
const rect2 =
|
|
1079
|
+
const rect2 = await this.proxyCommand(
|
|
1024
1080
|
`/element/${util.unwrapElement(el2)}/rect`, 'GET',
|
|
1025
|
-
)
|
|
1081
|
+
) as Rect;
|
|
1026
1082
|
|
|
1027
1083
|
if (
|
|
1028
1084
|
rect.x !== rect2.x || rect.y !== rect2.y
|
|
@@ -1034,7 +1090,7 @@ async function tapWebElementNatively(atomsElement) {
|
|
|
1034
1090
|
}
|
|
1035
1091
|
await this.mobileTap(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
1036
1092
|
return true;
|
|
1037
|
-
} catch (err) {
|
|
1093
|
+
} catch (err: any) {
|
|
1038
1094
|
// any failure should fall through and trigger the more elaborate
|
|
1039
1095
|
// method of clicking
|
|
1040
1096
|
this.log.warn(`Error attempting to click: ${err.message}`);
|
|
@@ -1043,10 +1099,12 @@ async function tapWebElementNatively(atomsElement) {
|
|
|
1043
1099
|
}
|
|
1044
1100
|
|
|
1045
1101
|
/**
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1102
|
+
* Validates if a value is a valid element identifier.
|
|
1103
|
+
*
|
|
1104
|
+
* @param id - Value to validate
|
|
1105
|
+
* @returns True if the value is a valid element identifier
|
|
1048
1106
|
*/
|
|
1049
|
-
function isValidElementIdentifier(id) {
|
|
1107
|
+
function isValidElementIdentifier(id: any): boolean {
|
|
1050
1108
|
if (!_.isString(id) && !_.isNumber(id)) {
|
|
1051
1109
|
return false;
|
|
1052
1110
|
}
|
|
@@ -1060,14 +1118,20 @@ function isValidElementIdentifier(id) {
|
|
|
1060
1118
|
}
|
|
1061
1119
|
|
|
1062
1120
|
/**
|
|
1063
|
-
* Creates a JavaScript
|
|
1121
|
+
* Creates a JavaScript cookie string.
|
|
1064
1122
|
*
|
|
1065
|
-
* @param
|
|
1066
|
-
* @param
|
|
1067
|
-
* @param
|
|
1068
|
-
* @returns
|
|
1123
|
+
* @param key - Cookie name
|
|
1124
|
+
* @param value - Cookie value
|
|
1125
|
+
* @param options - Cookie options (expires, path, domain, secure, httpOnly)
|
|
1126
|
+
* @returns Cookie string suitable for document.cookie
|
|
1069
1127
|
*/
|
|
1070
|
-
function createJSCookie(key, value, options
|
|
1128
|
+
function createJSCookie(key: string, value: string, options: {
|
|
1129
|
+
expires?: string;
|
|
1130
|
+
path?: string;
|
|
1131
|
+
domain?: string;
|
|
1132
|
+
secure?: boolean;
|
|
1133
|
+
httpOnly?: boolean;
|
|
1134
|
+
} = {}): string {
|
|
1071
1135
|
return [
|
|
1072
1136
|
encodeURIComponent(key),
|
|
1073
1137
|
'=',
|
|
@@ -1080,34 +1144,12 @@ function createJSCookie(key, value, options = {}) {
|
|
|
1080
1144
|
}
|
|
1081
1145
|
|
|
1082
1146
|
/**
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
* @
|
|
1147
|
+
* Deletes a cookie via the remote debugger.
|
|
1148
|
+
*
|
|
1149
|
+
* @param cookie - Cookie object to delete
|
|
1086
1150
|
*/
|
|
1087
|
-
async function _deleteCookie(cookie) {
|
|
1151
|
+
async function _deleteCookie(this: XCUITestDriver, cookie: Cookie): Promise<any> {
|
|
1088
1152
|
const url = `http${cookie.secure ? 's' : ''}://${cookie.domain}${cookie.path}`;
|
|
1089
|
-
return await
|
|
1153
|
+
return await this.remote.deleteCookie(cookie.name, url);
|
|
1090
1154
|
}
|
|
1091
1155
|
|
|
1092
|
-
/**
|
|
1093
|
-
* @typedef {Object} CookieOptions
|
|
1094
|
-
* @property {string} [expires]
|
|
1095
|
-
* @property {string} [path]
|
|
1096
|
-
* @property {string} [domain]
|
|
1097
|
-
* @property {boolean} [secure]
|
|
1098
|
-
* @property {boolean} [httpOnly]
|
|
1099
|
-
*/
|
|
1100
|
-
|
|
1101
|
-
/**
|
|
1102
|
-
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
|
|
1103
|
-
* @typedef {import('@appium/types').Rect} Rect
|
|
1104
|
-
*/
|
|
1105
|
-
|
|
1106
|
-
/**
|
|
1107
|
-
* @template {string} [S=string]
|
|
1108
|
-
* @typedef {import('@appium/types').Element<S>} Element
|
|
1109
|
-
*/
|
|
1110
|
-
|
|
1111
|
-
/**
|
|
1112
|
-
* @typedef {import('appium-remote-debugger').RemoteDebugger} RemoteDebugger
|
|
1113
|
-
*/
|