appium-xcuitest-driver 10.3.0 → 10.4.1
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 +12 -0
- package/build/lib/commands/active-app-info.d.ts +9 -0
- package/build/lib/commands/active-app-info.d.ts.map +1 -0
- package/build/lib/commands/active-app-info.js +14 -0
- package/build/lib/commands/active-app-info.js.map +1 -0
- package/build/lib/commands/alert.d.ts +42 -45
- package/build/lib/commands/alert.d.ts.map +1 -1
- package/build/lib/commands/alert.js +66 -62
- package/build/lib/commands/alert.js.map +1 -1
- package/build/lib/commands/app-management.d.ts +150 -153
- package/build/lib/commands/app-management.d.ts.map +1 -1
- package/build/lib/commands/app-management.js +300 -286
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/app-strings.d.ts +14 -17
- package/build/lib/commands/app-strings.d.ts.map +1 -1
- package/build/lib/commands/app-strings.js +23 -24
- package/build/lib/commands/app-strings.js.map +1 -1
- package/build/lib/commands/appearance.d.ts +19 -22
- package/build/lib/commands/appearance.d.ts.map +1 -1
- package/build/lib/commands/appearance.js +56 -56
- package/build/lib/commands/appearance.js.map +1 -1
- package/build/lib/commands/audit.d.ts +22 -17
- package/build/lib/commands/audit.d.ts.map +1 -1
- package/build/lib/commands/audit.js +17 -18
- package/build/lib/commands/audit.js.map +1 -1
- package/build/lib/commands/battery.d.ts +11 -14
- package/build/lib/commands/battery.d.ts.map +1 -1
- package/build/lib/commands/battery.js +36 -37
- package/build/lib/commands/battery.js.map +1 -1
- package/build/lib/commands/biometric.d.ts +30 -33
- package/build/lib/commands/biometric.d.ts.map +1 -1
- package/build/lib/commands/biometric.js +42 -41
- package/build/lib/commands/biometric.js.map +1 -1
- package/build/lib/commands/certificate.d.ts +48 -45
- package/build/lib/commands/certificate.d.ts.map +1 -1
- package/build/lib/commands/certificate.js +218 -205
- package/build/lib/commands/certificate.js.map +1 -1
- package/build/lib/commands/clipboard.d.ts +19 -22
- package/build/lib/commands/clipboard.d.ts.map +1 -1
- package/build/lib/commands/clipboard.js +30 -30
- package/build/lib/commands/clipboard.js.map +1 -1
- package/build/lib/commands/condition.d.ts +49 -26
- package/build/lib/commands/condition.d.ts.map +1 -1
- package/build/lib/commands/condition.js +87 -86
- package/build/lib/commands/condition.js.map +1 -1
- package/build/lib/commands/content-size.d.ts +26 -29
- package/build/lib/commands/content-size.d.ts.map +1 -1
- package/build/lib/commands/content-size.js +36 -36
- package/build/lib/commands/content-size.js.map +1 -1
- package/build/lib/commands/context.d.ts +161 -108
- package/build/lib/commands/context.d.ts.map +1 -1
- package/build/lib/commands/context.js +530 -517
- package/build/lib/commands/context.js.map +1 -1
- package/build/lib/commands/deviceInfo.d.ts +9 -12
- package/build/lib/commands/deviceInfo.d.ts.map +1 -1
- package/build/lib/commands/deviceInfo.js +17 -18
- package/build/lib/commands/deviceInfo.js.map +1 -1
- package/build/lib/commands/element.d.ts +102 -105
- package/build/lib/commands/element.d.ts.map +1 -1
- package/build/lib/commands/element.js +337 -323
- package/build/lib/commands/element.js.map +1 -1
- package/build/lib/commands/execute.d.ts +24 -19
- package/build/lib/commands/execute.d.ts.map +1 -1
- package/build/lib/commands/execute.js +63 -62
- package/build/lib/commands/execute.js.map +1 -1
- package/build/lib/commands/file-movement.d.ts +77 -80
- package/build/lib/commands/file-movement.d.ts.map +1 -1
- package/build/lib/commands/file-movement.js +130 -124
- package/build/lib/commands/file-movement.js.map +1 -1
- package/build/lib/commands/find.d.ts +18 -21
- package/build/lib/commands/find.d.ts.map +1 -1
- package/build/lib/commands/find.js +158 -156
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/commands/general.d.ts +124 -116
- package/build/lib/commands/general.d.ts.map +1 -1
- package/build/lib/commands/general.js +248 -232
- package/build/lib/commands/general.js.map +1 -1
- package/build/lib/commands/geolocation.d.ts +43 -46
- package/build/lib/commands/geolocation.d.ts.map +1 -1
- package/build/lib/commands/geolocation.js +10 -11
- package/build/lib/commands/geolocation.js.map +1 -1
- package/build/lib/commands/gesture.d.ts +273 -276
- package/build/lib/commands/gesture.d.ts.map +1 -1
- package/build/lib/commands/gesture.js +506 -492
- package/build/lib/commands/gesture.js.map +1 -1
- package/build/lib/commands/increase-contrast.d.ts +20 -23
- package/build/lib/commands/increase-contrast.d.ts.map +1 -1
- package/build/lib/commands/increase-contrast.js +30 -30
- package/build/lib/commands/increase-contrast.js.map +1 -1
- package/build/lib/commands/iohid.d.ts +1370 -1373
- package/build/lib/commands/iohid.d.ts.map +1 -1
- package/build/lib/commands/iohid.js +30 -31
- package/build/lib/commands/iohid.js.map +1 -1
- package/build/lib/commands/keyboard.d.ts +29 -32
- package/build/lib/commands/keyboard.d.ts.map +1 -1
- package/build/lib/commands/keyboard.js +53 -51
- package/build/lib/commands/keyboard.js.map +1 -1
- package/build/lib/commands/keychains.d.ts +9 -12
- package/build/lib/commands/keychains.d.ts.map +1 -1
- package/build/lib/commands/keychains.js +13 -14
- package/build/lib/commands/keychains.js.map +1 -1
- package/build/lib/commands/localization.d.ts +16 -19
- package/build/lib/commands/localization.d.ts.map +1 -1
- package/build/lib/commands/localization.js +25 -26
- package/build/lib/commands/localization.js.map +1 -1
- package/build/lib/commands/location.d.ts +36 -39
- package/build/lib/commands/location.d.ts.map +1 -1
- package/build/lib/commands/location.js +99 -98
- package/build/lib/commands/location.js.map +1 -1
- package/build/lib/commands/lock.d.ts +21 -24
- package/build/lib/commands/lock.d.ts.map +1 -1
- package/build/lib/commands/lock.js +39 -38
- package/build/lib/commands/lock.js.map +1 -1
- package/build/lib/commands/log.d.ts +43 -37
- package/build/lib/commands/log.d.ts.map +1 -1
- package/build/lib/commands/log.js +174 -171
- package/build/lib/commands/log.js.map +1 -1
- package/build/lib/commands/memory.d.ts +9 -12
- package/build/lib/commands/memory.d.ts.map +1 -1
- package/build/lib/commands/memory.js +37 -38
- package/build/lib/commands/memory.js.map +1 -1
- package/build/lib/commands/navigation.d.ts +30 -33
- package/build/lib/commands/navigation.d.ts.map +1 -1
- package/build/lib/commands/navigation.js +92 -92
- package/build/lib/commands/navigation.js.map +1 -1
- package/build/lib/commands/notifications.d.ts +26 -29
- package/build/lib/commands/notifications.d.ts.map +1 -1
- package/build/lib/commands/notifications.js +53 -53
- package/build/lib/commands/notifications.js.map +1 -1
- package/build/lib/commands/pasteboard.d.ts +21 -24
- package/build/lib/commands/pasteboard.d.ts.map +1 -1
- package/build/lib/commands/pasteboard.js +37 -37
- package/build/lib/commands/pasteboard.js.map +1 -1
- package/build/lib/commands/pcap.d.ts +39 -26
- package/build/lib/commands/pcap.d.ts.map +1 -1
- package/build/lib/commands/pcap.js +81 -81
- package/build/lib/commands/pcap.js.map +1 -1
- package/build/lib/commands/performance.d.ts +63 -44
- package/build/lib/commands/performance.d.ts.map +1 -1
- package/build/lib/commands/performance.js +105 -105
- package/build/lib/commands/performance.js.map +1 -1
- package/build/lib/commands/permissions.d.ts +33 -36
- package/build/lib/commands/permissions.d.ts.map +1 -1
- package/build/lib/commands/permissions.js +66 -65
- package/build/lib/commands/permissions.js.map +1 -1
- package/build/lib/commands/proxy-helper.d.ts +12 -15
- package/build/lib/commands/proxy-helper.d.ts.map +1 -1
- package/build/lib/commands/proxy-helper.js +53 -54
- package/build/lib/commands/proxy-helper.js.map +1 -1
- package/build/lib/commands/record-audio.d.ts +49 -29
- package/build/lib/commands/record-audio.d.ts.map +1 -1
- package/build/lib/commands/record-audio.js +100 -104
- package/build/lib/commands/record-audio.js.map +1 -1
- package/build/lib/commands/recordscreen.d.ts +54 -18
- package/build/lib/commands/recordscreen.d.ts.map +1 -1
- package/build/lib/commands/recordscreen.js +127 -129
- package/build/lib/commands/recordscreen.js.map +1 -1
- package/build/lib/commands/screenshots.d.ts +14 -17
- package/build/lib/commands/screenshots.d.ts.map +1 -1
- package/build/lib/commands/screenshots.js +108 -107
- package/build/lib/commands/screenshots.js.map +1 -1
- package/build/lib/commands/simctl.d.ts +11 -14
- package/build/lib/commands/simctl.d.ts.map +1 -1
- package/build/lib/commands/simctl.js +23 -26
- package/build/lib/commands/simctl.js.map +1 -1
- package/build/lib/commands/source.d.ts +14 -17
- package/build/lib/commands/source.d.ts.map +1 -1
- package/build/lib/commands/source.js +40 -43
- package/build/lib/commands/source.js.map +1 -1
- package/build/lib/commands/timeouts.d.ts +44 -33
- package/build/lib/commands/timeouts.d.ts.map +1 -1
- package/build/lib/commands/timeouts.js +65 -63
- package/build/lib/commands/timeouts.js.map +1 -1
- package/build/lib/commands/web.d.ts +275 -197
- package/build/lib/commands/web.d.ts.map +1 -1
- package/build/lib/commands/web.js +866 -785
- package/build/lib/commands/web.js.map +1 -1
- package/build/lib/commands/xctest-record-screen.d.ts +63 -66
- package/build/lib/commands/xctest-record-screen.d.ts.map +1 -1
- package/build/lib/commands/xctest-record-screen.js +103 -102
- package/build/lib/commands/xctest-record-screen.js.map +1 -1
- package/build/lib/commands/xctest.d.ts +55 -51
- package/build/lib/commands/xctest.d.ts.map +1 -1
- package/build/lib/commands/xctest.js +116 -117
- package/build/lib/commands/xctest.js.map +1 -1
- package/build/lib/driver.d.ts +277 -1597
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +318 -235
- 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 +9 -0
- package/build/lib/execute-method-map.js.map +1 -1
- package/lib/commands/active-app-info.js +12 -0
- package/lib/commands/alert.js +68 -65
- package/lib/commands/app-management.js +308 -301
- package/lib/commands/app-strings.js +24 -26
- package/lib/commands/appearance.js +54 -56
- package/lib/commands/audit.js +18 -20
- package/lib/commands/battery.js +35 -37
- package/lib/commands/biometric.js +44 -46
- package/lib/commands/certificate.js +226 -215
- package/lib/commands/clipboard.js +30 -32
- package/lib/commands/condition.js +98 -100
- package/lib/commands/content-size.js +36 -38
- package/lib/commands/context.js +495 -490
- package/lib/commands/deviceInfo.js +19 -20
- package/lib/commands/element.js +367 -357
- package/lib/commands/execute.js +72 -72
- package/lib/commands/file-movement.js +132 -134
- package/lib/commands/find.js +160 -159
- package/lib/commands/general.js +238 -231
- package/lib/commands/geolocation.js +6 -14
- package/lib/commands/gesture.js +525 -515
- package/lib/commands/increase-contrast.js +30 -32
- package/lib/commands/iohid.js +32 -34
- package/lib/commands/keyboard.js +49 -51
- package/lib/commands/keychains.js +12 -14
- package/lib/commands/localization.js +24 -26
- package/lib/commands/location.js +102 -104
- package/lib/commands/lock.js +38 -38
- package/lib/commands/log.js +197 -198
- package/lib/commands/memory.js +40 -42
- package/lib/commands/navigation.js +96 -100
- package/lib/commands/notifications.js +57 -59
- package/lib/commands/pasteboard.js +37 -39
- package/lib/commands/pcap.js +84 -86
- package/lib/commands/performance.js +132 -133
- package/lib/commands/permissions.js +67 -69
- package/lib/commands/proxy-helper.js +60 -61
- package/lib/commands/record-audio.js +115 -120
- package/lib/commands/recordscreen.js +145 -149
- package/lib/commands/screenshots.js +116 -116
- package/lib/commands/simctl.js +25 -29
- package/lib/commands/source.js +42 -46
- package/lib/commands/timeouts.js +59 -63
- package/lib/commands/web.js +932 -859
- package/lib/commands/xctest-record-screen.js +103 -105
- package/lib/commands/xctest.js +134 -139
- package/lib/driver.js +286 -235
- package/lib/execute-method-map.ts +9 -0
- package/npm-shrinkwrap.json +8 -8
- package/package.json +1 -1
- package/build/lib/commands/activeAppInfo.d.ts +0 -12
- package/build/lib/commands/activeAppInfo.d.ts.map +0 -1
- package/build/lib/commands/activeAppInfo.js +0 -15
- package/build/lib/commands/activeAppInfo.js.map +0 -1
- package/build/lib/commands/index.d.ts +0 -96
- package/build/lib/commands/index.d.ts.map +0 -1
- package/build/lib/commands/index.js +0 -100
- package/build/lib/commands/index.js.map +0 -1
- package/build/lib/cookies.d.ts +0 -15
- package/build/lib/cookies.d.ts.map +0 -1
- package/build/lib/cookies.js +0 -84
- package/build/lib/cookies.js.map +0 -1
- package/lib/commands/activeAppInfo.js +0 -14
- package/lib/commands/index.js +0 -95
- package/lib/cookies.js +0 -92
package/lib/commands/web.js
CHANGED
|
@@ -3,7 +3,6 @@ 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 * as cookieUtils from '../cookies';
|
|
7
6
|
|
|
8
7
|
const IPHONE_TOP_BAR_HEIGHT = 71;
|
|
9
8
|
const IPHONE_SCROLLED_TOP_BAR_HEIGHT = 41;
|
|
@@ -54,949 +53,915 @@ const TAB_BAR_POSSITIONS = [TAB_BAR_POSITION_TOP, TAB_BAR_POSITION_BOTTOM];
|
|
|
54
53
|
|
|
55
54
|
/**
|
|
56
55
|
* @this {XCUITestDriver}
|
|
57
|
-
* @
|
|
58
|
-
* @
|
|
56
|
+
* @group Mobile Web Only
|
|
57
|
+
* @param {number|string|null} frame
|
|
58
|
+
* @returns {Promise<void>}
|
|
59
59
|
*/
|
|
60
|
-
async function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const [text1, text2] = await B.all([
|
|
65
|
-
this.executeAtom('get_text', [atomsElement]),
|
|
66
|
-
this.executeAtom('get_attribute_value', [atomsElement, 'value'])
|
|
67
|
-
]);
|
|
68
|
-
const text = text1 || text2;
|
|
69
|
-
if (!text) {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
60
|
+
export async function setFrame(frame) {
|
|
61
|
+
if (!this.isWebContext()) {
|
|
62
|
+
throw new errors.NotImplementedError();
|
|
63
|
+
}
|
|
72
64
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
if (_.isNull(frame)) {
|
|
66
|
+
this.curWebFrames = [];
|
|
67
|
+
this.log.debug('Leaving web frame and going back to default content');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (hasElementId(frame)) {
|
|
72
|
+
const atomsElement = this.getAtomsElement(frame);
|
|
73
|
+
const value = await this.executeAtom('get_frame_window', [atomsElement]);
|
|
74
|
+
this.log.debug(`Entering new web frame: '${value.WINDOW}'`);
|
|
75
|
+
this.curWebFrames.unshift(value.WINDOW);
|
|
76
|
+
} else {
|
|
77
|
+
const atom = _.isNumber(frame) ? 'frame_by_index' : 'frame_by_id_or_name';
|
|
78
|
+
const value = await this.executeAtom(atom, [frame]);
|
|
79
|
+
if (_.isNull(value) || _.isUndefined(value.WINDOW)) {
|
|
80
|
+
throw new errors.NoSuchFrameError();
|
|
76
81
|
}
|
|
82
|
+
this.log.debug(`Entering new web frame: '${value.WINDOW}'`);
|
|
83
|
+
this.curWebFrames.unshift(value.WINDOW);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* @this {XCUITestDriver}
|
|
89
|
+
* @group Mobile Web Only
|
|
90
|
+
* @param {string} propertyName
|
|
91
|
+
* @param {Element | string} el
|
|
92
|
+
* @returns {Promise<string>}
|
|
93
|
+
*/
|
|
94
|
+
export async function getCssProperty(propertyName, el) {
|
|
95
|
+
if (!this.isWebContext()) {
|
|
96
|
+
throw new errors.NotImplementedError();
|
|
97
|
+
}
|
|
88
98
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
const atomsElement = this.getAtomsElement(el);
|
|
100
|
+
return await this.executeAtom('get_value_of_css_property', [atomsElement, propertyName]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Submit the form an element is in
|
|
105
|
+
*
|
|
106
|
+
* @param {string|Element} el - the element ID
|
|
107
|
+
* @group Mobile Web Only
|
|
108
|
+
* @this {XCUITestDriver}
|
|
109
|
+
*/
|
|
110
|
+
export async function submit(el) {
|
|
111
|
+
if (!this.isWebContext()) {
|
|
112
|
+
throw new errors.NotImplementedError();
|
|
103
113
|
}
|
|
104
|
-
|
|
114
|
+
|
|
115
|
+
const atomsElement = this.getAtomsElement(el);
|
|
116
|
+
await this.executeAtom('submit', [atomsElement]);
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
/**
|
|
108
|
-
* @
|
|
109
|
-
* @
|
|
120
|
+
* @this {XCUITestDriver}
|
|
121
|
+
* @group Mobile Web Only
|
|
110
122
|
*/
|
|
111
|
-
function
|
|
112
|
-
if (!
|
|
113
|
-
|
|
123
|
+
export async function refresh() {
|
|
124
|
+
if (!this.isWebContext()) {
|
|
125
|
+
throw new errors.NotImplementedError();
|
|
114
126
|
}
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
|
|
128
|
+
await (/** @type {RemoteDebugger} */ (this.remote)).execute('window.location.reload()');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @this {XCUITestDriver}
|
|
133
|
+
* @group Mobile Web Only
|
|
134
|
+
* @returns {Promise<string>}
|
|
135
|
+
*/
|
|
136
|
+
export async function getUrl() {
|
|
137
|
+
if (!this.isWebContext()) {
|
|
138
|
+
throw new errors.NotImplementedError();
|
|
117
139
|
}
|
|
118
|
-
|
|
119
|
-
|
|
140
|
+
|
|
141
|
+
return await (/** @type {RemoteDebugger} */ (this.remote)).execute('window.location.href');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @this {XCUITestDriver}
|
|
146
|
+
* @group Mobile Web Only
|
|
147
|
+
* @returns {Promise<string>}
|
|
148
|
+
*/
|
|
149
|
+
export async function title() {
|
|
150
|
+
if (!this.isWebContext()) {
|
|
151
|
+
throw new errors.NotImplementedError();
|
|
120
152
|
}
|
|
121
|
-
|
|
153
|
+
|
|
154
|
+
return await (/** @type {RemoteDebugger} */ (this.remote)).execute('window.document.title');
|
|
122
155
|
}
|
|
123
156
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
157
|
+
/**
|
|
158
|
+
* @this {XCUITestDriver}
|
|
159
|
+
* @group Mobile Web Only
|
|
160
|
+
* @returns {Promise<import('@appium/types').Cookie[]>}
|
|
161
|
+
*/
|
|
162
|
+
export async function getCookies() {
|
|
163
|
+
if (!this.isWebContext()) {
|
|
164
|
+
throw new errors.NotImplementedError();
|
|
165
|
+
}
|
|
133
166
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.log.debug('Leaving web frame and going back to default content');
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
167
|
+
// get the cookies from the remote debugger, or an empty object
|
|
168
|
+
const {cookies} = await (/** @type {RemoteDebugger} */ (this.remote)).getCookies();
|
|
139
169
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
170
|
+
// the value is URI encoded, so decode it safely
|
|
171
|
+
return cookies.map((cookie) => {
|
|
172
|
+
if (!_.isEmpty(cookie.value)) {
|
|
173
|
+
try {
|
|
174
|
+
cookie.value = decodeURI(cookie.value);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.log.debug(
|
|
177
|
+
`Cookie ${cookie.name} was not decoded successfully. Cookie value: ${cookie.value}`,
|
|
178
|
+
);
|
|
179
|
+
this.log.warn(error);
|
|
180
|
+
// Keep the original value
|
|
150
181
|
}
|
|
151
|
-
this.log.debug(`Entering new web frame: '${value.WINDOW}'`);
|
|
152
|
-
this.curWebFrames.unshift(value.WINDOW);
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
/**
|
|
156
|
-
* @this {XCUITestDriver}
|
|
157
|
-
* @group Mobile Web Only
|
|
158
|
-
*/
|
|
159
|
-
async getCssProperty(propertyName, el) {
|
|
160
|
-
if (!this.isWebContext()) {
|
|
161
|
-
throw new errors.NotImplementedError();
|
|
162
182
|
}
|
|
183
|
+
return cookie;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
163
186
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
async submit(el) {
|
|
175
|
-
if (!this.isWebContext()) {
|
|
176
|
-
throw new errors.NotImplementedError();
|
|
177
|
-
}
|
|
187
|
+
/**
|
|
188
|
+
* @this {XCUITestDriver}
|
|
189
|
+
* @group Mobile Web Only
|
|
190
|
+
* @param {import('@appium/types').Cookie} cookie
|
|
191
|
+
* @returns {Promise<void>}
|
|
192
|
+
*/
|
|
193
|
+
export async function setCookie(cookie) {
|
|
194
|
+
if (!this.isWebContext()) {
|
|
195
|
+
throw new errors.NotImplementedError();
|
|
196
|
+
}
|
|
178
197
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
const clonedCookie = _.clone(cookie);
|
|
199
|
+
// if `path` field is not specified, Safari will not update cookies as expected; eg issue #1708
|
|
200
|
+
if (!clonedCookie.path) {
|
|
201
|
+
clonedCookie.path = '/';
|
|
202
|
+
}
|
|
203
|
+
const jsCookie = createJSCookie(clonedCookie.name, clonedCookie.value, {
|
|
204
|
+
expires: _.isNumber(clonedCookie.expiry)
|
|
205
|
+
? new Date(clonedCookie.expiry * 1000).toUTCString()
|
|
206
|
+
: clonedCookie.expiry,
|
|
207
|
+
path: clonedCookie.path,
|
|
208
|
+
domain: clonedCookie.domain,
|
|
209
|
+
httpOnly: clonedCookie.httpOnly,
|
|
210
|
+
secure: clonedCookie.secure,
|
|
211
|
+
});
|
|
212
|
+
const script = `document.cookie = ${JSON.stringify(jsCookie)}`;
|
|
213
|
+
await this.executeAtom('execute_script', [script, []]);
|
|
214
|
+
}
|
|
190
215
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
/**
|
|
217
|
+
* @this {XCUITestDriver}
|
|
218
|
+
* @param {string} cookieName
|
|
219
|
+
* @returns {Promise<void>}
|
|
220
|
+
* @group Mobile Web Only
|
|
221
|
+
*/
|
|
222
|
+
export async function deleteCookie(cookieName) {
|
|
223
|
+
if (!this.isWebContext()) {
|
|
224
|
+
throw new errors.NotImplementedError();
|
|
225
|
+
}
|
|
201
226
|
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
async title() {
|
|
209
|
-
if (!this.isWebContext()) {
|
|
210
|
-
throw new errors.NotImplementedError();
|
|
211
|
-
}
|
|
227
|
+
const cookies = await this.getCookies();
|
|
228
|
+
const cookie = cookies.find(({name}) => name === cookieName);
|
|
229
|
+
if (!cookie) {
|
|
230
|
+
this.log.debug(`Cookie '${cookieName}' not found. Ignoring.`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
212
233
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* @this {XCUITestDriver}
|
|
217
|
-
* @group Mobile Web Only
|
|
218
|
-
*/
|
|
219
|
-
async getCookies() {
|
|
220
|
-
if (!this.isWebContext()) {
|
|
221
|
-
throw new errors.NotImplementedError();
|
|
222
|
-
}
|
|
234
|
+
await _deleteCookie.bind(this)(cookie);
|
|
235
|
+
}
|
|
223
236
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
this.log.debug(
|
|
234
|
-
`Cookie ${cookie.name} was not decoded successfully. Cookie value: ${cookie.value}`,
|
|
235
|
-
);
|
|
236
|
-
this.log.warn(error);
|
|
237
|
-
// Keep the original value
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return cookie;
|
|
241
|
-
});
|
|
242
|
-
},
|
|
243
|
-
/**
|
|
244
|
-
* @this {XCUITestDriver}
|
|
245
|
-
* @group Mobile Web Only
|
|
246
|
-
*/
|
|
247
|
-
async setCookie(cookie) {
|
|
248
|
-
if (!this.isWebContext()) {
|
|
249
|
-
throw new errors.NotImplementedError();
|
|
250
|
-
}
|
|
237
|
+
/**
|
|
238
|
+
* @this {XCUITestDriver}
|
|
239
|
+
* @group Mobile Web Only
|
|
240
|
+
* @returns {Promise<void>}
|
|
241
|
+
*/
|
|
242
|
+
export async function deleteCookies() {
|
|
243
|
+
if (!this.isWebContext()) {
|
|
244
|
+
throw new errors.NotImplementedError();
|
|
245
|
+
}
|
|
251
246
|
|
|
252
|
-
|
|
247
|
+
const cookies = await this.getCookies();
|
|
248
|
+
await B.all(cookies.map((cookie) => _deleteCookie.bind(this)(cookie)));
|
|
249
|
+
}
|
|
253
250
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
/**
|
|
252
|
+
* @this {XCUITestDriver}
|
|
253
|
+
* @param {Element | string} el
|
|
254
|
+
* @returns {Element | string}
|
|
255
|
+
*/
|
|
256
|
+
export function cacheWebElement(el) {
|
|
257
|
+
if (!_.isPlainObject(el)) {
|
|
258
|
+
return el;
|
|
259
|
+
}
|
|
260
|
+
const elId = util.unwrapElement(el);
|
|
261
|
+
if (!isValidElementIdentifier(elId)) {
|
|
262
|
+
return el;
|
|
263
|
+
}
|
|
264
|
+
// In newer debugger releases element identifiers look like `:wdc:1628151649325`
|
|
265
|
+
// We assume it is safe to use these to identify cached elements
|
|
266
|
+
const cacheId = _.includes(elId, ':') ? elId : util.uuidV4();
|
|
267
|
+
this.webElementsCache.set(cacheId, elId);
|
|
268
|
+
return util.wrapElement(cacheId);
|
|
269
|
+
}
|
|
258
270
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
271
|
+
/**
|
|
272
|
+
* @this {XCUITestDriver}
|
|
273
|
+
* @param {any} response
|
|
274
|
+
* @returns {any}
|
|
275
|
+
*/
|
|
276
|
+
export function cacheWebElements(response) {
|
|
277
|
+
const toCached = (/** @type {any} */ v) => (_.isArray(v) || _.isPlainObject(v)) ? this.cacheWebElements(v) : v;
|
|
278
|
+
|
|
279
|
+
if (_.isArray(response)) {
|
|
280
|
+
return response.map(toCached);
|
|
281
|
+
} else if (_.isPlainObject(response)) {
|
|
282
|
+
const result = {...response, ...(/** @type {Element} */ (this.cacheWebElement(response)))};
|
|
283
|
+
return _.toPairs(result).reduce((acc, [key, value]) => {
|
|
284
|
+
acc[key] = toCached(value);
|
|
285
|
+
return acc;
|
|
286
|
+
}, {});
|
|
287
|
+
}
|
|
288
|
+
return response;
|
|
289
|
+
}
|
|
279
290
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
/**
|
|
292
|
+
* @param {string} atom
|
|
293
|
+
* @param {unknown[]} args
|
|
294
|
+
* @returns {Promise<any>}
|
|
295
|
+
* @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
|
+
*/
|
|
298
|
+
export async function executeAtom(atom, args, alwaysDefaultFrame = false) {
|
|
299
|
+
let frames = alwaysDefaultFrame === true ? [] : this.curWebFrames;
|
|
300
|
+
let promise = (/** @type {RemoteDebugger} */ (this.remote)).executeAtom(atom, args, frames);
|
|
301
|
+
return await this.waitForAtom(promise);
|
|
302
|
+
}
|
|
286
303
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
304
|
+
/**
|
|
305
|
+
* @this {XCUITestDriver}
|
|
306
|
+
* @param {string} atom
|
|
307
|
+
* @param {any[]} args
|
|
308
|
+
*/
|
|
309
|
+
export async function executeAtomAsync(atom, args) {
|
|
310
|
+
// save the resolve and reject methods of the promise to be waited for
|
|
311
|
+
let promise = new B((resolve, reject) => {
|
|
312
|
+
this.asyncPromise = {resolve, reject};
|
|
313
|
+
});
|
|
314
|
+
await (/** @type {RemoteDebugger} */ (this.remote)).executeAtomAsync(atom, args, this.curWebFrames);
|
|
315
|
+
return await this.waitForAtom(promise);
|
|
316
|
+
}
|
|
297
317
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const cacheId = _.includes(elId, ':') ? elId : util.uuidV4();
|
|
325
|
-
this.webElementsCache.set(cacheId, elId);
|
|
326
|
-
return util.wrapElement(cacheId);
|
|
327
|
-
},
|
|
328
|
-
/**
|
|
329
|
-
* @this {XCUITestDriver}
|
|
330
|
-
*/
|
|
331
|
-
cacheWebElements(response) {
|
|
332
|
-
const toCached = (v) => (_.isArray(v) || _.isPlainObject(v) ? this.cacheWebElements(v) : v);
|
|
333
|
-
|
|
334
|
-
if (_.isArray(response)) {
|
|
335
|
-
return response.map(toCached);
|
|
336
|
-
} else if (_.isPlainObject(response)) {
|
|
337
|
-
const result = {...response, ...this.cacheWebElement(response)};
|
|
338
|
-
return _.toPairs(result).reduce((acc, [key, value]) => {
|
|
339
|
-
acc[key] = toCached(value);
|
|
340
|
-
return acc;
|
|
341
|
-
}, {});
|
|
342
|
-
}
|
|
343
|
-
return response;
|
|
344
|
-
},
|
|
345
|
-
/**
|
|
346
|
-
* @param {string} atom
|
|
347
|
-
* @param {unknown[]} args
|
|
348
|
-
* @returns {Promise<any>}
|
|
349
|
-
* @privateRemarks This should return `Promise<T>` where `T` extends `unknown`, but that's going to cause a lot of things to break.
|
|
350
|
-
* @this {XCUITestDriver}
|
|
351
|
-
*/
|
|
352
|
-
async executeAtom(atom, args, alwaysDefaultFrame = false) {
|
|
353
|
-
let frames = alwaysDefaultFrame === true ? [] : this.curWebFrames;
|
|
354
|
-
let promise = (/** @type {RemoteDebugger} */ (this.remote)).executeAtom(atom, args, frames);
|
|
355
|
-
return await this.waitForAtom(promise);
|
|
356
|
-
},
|
|
357
|
-
/**
|
|
358
|
-
* @this {XCUITestDriver}
|
|
359
|
-
* @param {string} atom
|
|
360
|
-
* @param {any[]} args
|
|
361
|
-
*/
|
|
362
|
-
async executeAtomAsync(atom, args) {
|
|
363
|
-
// save the resolve and reject methods of the promise to be waited for
|
|
364
|
-
let promise = new B((resolve, reject) => {
|
|
365
|
-
this.asyncPromise = {resolve, reject};
|
|
366
|
-
});
|
|
367
|
-
await (/** @type {RemoteDebugger} */ (this.remote)).executeAtomAsync(atom, args, this.curWebFrames);
|
|
368
|
-
return await this.waitForAtom(promise);
|
|
369
|
-
},
|
|
370
|
-
/**
|
|
371
|
-
* @template {string} S
|
|
372
|
-
* @param {S|Element<S>} elOrId
|
|
373
|
-
* @returns {import('./types').AtomsElement<S>}
|
|
374
|
-
* @this {XCUITestDriver}
|
|
375
|
-
*/
|
|
376
|
-
getAtomsElement(elOrId) {
|
|
377
|
-
const elId = util.unwrapElement(elOrId);
|
|
378
|
-
if (!this.webElementsCache?.has(elId)) {
|
|
379
|
-
throw new errors.StaleElementReferenceError();
|
|
380
|
-
}
|
|
381
|
-
return {ELEMENT: this.webElementsCache.get(elId)};
|
|
382
|
-
},
|
|
383
|
-
/**
|
|
384
|
-
* @param {readonly any[]} [args]
|
|
385
|
-
* @this {XCUITestDriver}
|
|
386
|
-
*/
|
|
387
|
-
convertElementsForAtoms(args = []) {
|
|
388
|
-
return args.map((arg) => {
|
|
389
|
-
if (helpers.hasElementId(arg)) {
|
|
390
|
-
try {
|
|
391
|
-
return this.getAtomsElement(arg);
|
|
392
|
-
} catch (err) {
|
|
393
|
-
if (!isErrorType(err, errors.StaleElementReferenceError)) {
|
|
394
|
-
throw err;
|
|
395
|
-
}
|
|
318
|
+
/**
|
|
319
|
+
* @template {string} S
|
|
320
|
+
* @param {S|Element<S>} elOrId
|
|
321
|
+
* @returns {import('./types').AtomsElement<S>}
|
|
322
|
+
* @this {XCUITestDriver}
|
|
323
|
+
*/
|
|
324
|
+
export function getAtomsElement(elOrId) {
|
|
325
|
+
const elId = util.unwrapElement(elOrId);
|
|
326
|
+
if (!this.webElementsCache?.has(elId)) {
|
|
327
|
+
throw new errors.StaleElementReferenceError();
|
|
328
|
+
}
|
|
329
|
+
return {ELEMENT: this.webElementsCache.get(elId)};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @param {readonly any[]} [args]
|
|
334
|
+
* @this {XCUITestDriver}
|
|
335
|
+
*/
|
|
336
|
+
export function convertElementsForAtoms(args = []) {
|
|
337
|
+
return args.map((arg) => {
|
|
338
|
+
if (hasElementId(arg)) {
|
|
339
|
+
try {
|
|
340
|
+
return this.getAtomsElement(arg);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
if (!isErrorType(err, errors.StaleElementReferenceError)) {
|
|
343
|
+
throw err;
|
|
396
344
|
}
|
|
397
|
-
return arg;
|
|
398
|
-
}
|
|
399
|
-
return _.isArray(arg) ? this.convertElementsForAtoms(arg) : arg;
|
|
400
|
-
});
|
|
401
|
-
},
|
|
402
|
-
getElementId(element) {
|
|
403
|
-
return element.ELEMENT || element[W3C_WEB_ELEMENT_IDENTIFIER];
|
|
404
|
-
},
|
|
405
|
-
/**
|
|
406
|
-
* @param {any} element
|
|
407
|
-
* @returns {element is Element}
|
|
408
|
-
*/
|
|
409
|
-
hasElementId(element) {
|
|
410
|
-
return (
|
|
411
|
-
util.hasValue(element) &&
|
|
412
|
-
(util.hasValue(element.ELEMENT) || util.hasValue(element[W3C_WEB_ELEMENT_IDENTIFIER]))
|
|
413
|
-
);
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const extensions = {
|
|
418
|
-
/**
|
|
419
|
-
* @this {XCUITestDriver}
|
|
420
|
-
*/
|
|
421
|
-
async findWebElementOrElements(strategy, selector, many, ctx) {
|
|
422
|
-
const contextElement = _.isNil(ctx) ? null : this.getAtomsElement(ctx);
|
|
423
|
-
const atomName = many ? 'find_elements' : 'find_element_fragment';
|
|
424
|
-
let element;
|
|
425
|
-
let doFind = async () => {
|
|
426
|
-
element = await this.executeAtom(atomName, [strategy, selector, contextElement]);
|
|
427
|
-
return !_.isNull(element);
|
|
428
|
-
};
|
|
429
|
-
try {
|
|
430
|
-
await this.implicitWaitForCondition(doFind);
|
|
431
|
-
} catch (err) {
|
|
432
|
-
if (err.message && _.isFunction(err.message.match) && err.message.match(/Condition unmet/)) {
|
|
433
|
-
// condition was not met setting res to empty array
|
|
434
|
-
element = [];
|
|
435
|
-
} else {
|
|
436
|
-
throw err;
|
|
437
345
|
}
|
|
346
|
+
return arg;
|
|
438
347
|
}
|
|
348
|
+
return _.isArray(arg) ? this.convertElementsForAtoms(arg) : arg;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
439
351
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
352
|
+
/**
|
|
353
|
+
*
|
|
354
|
+
* @param {any} element
|
|
355
|
+
* @returns {string | undefined}
|
|
356
|
+
*/
|
|
357
|
+
export function getElementId(element) {
|
|
358
|
+
return element?.ELEMENT || element?.[W3C_WEB_ELEMENT_IDENTIFIER];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @param {any} element
|
|
363
|
+
* @returns {element is Element}
|
|
364
|
+
*/
|
|
365
|
+
export function hasElementId(element) {
|
|
366
|
+
return (
|
|
367
|
+
util.hasValue(element) &&
|
|
368
|
+
(util.hasValue(element.ELEMENT) || util.hasValue(element[W3C_WEB_ELEMENT_IDENTIFIER]))
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @this {XCUITestDriver}
|
|
374
|
+
* @param {string} strategy
|
|
375
|
+
* @param {string} selector
|
|
376
|
+
* @param {boolean} [many]
|
|
377
|
+
* @param {Element | string | null} [ctx]
|
|
378
|
+
* @returns {Promise<Element | Element[]>}
|
|
379
|
+
*/
|
|
380
|
+
export async function findWebElementOrElements(strategy, selector, many, ctx) {
|
|
381
|
+
const contextElement = _.isNil(ctx) ? null : this.getAtomsElement(ctx);
|
|
382
|
+
const atomName = many ? 'find_elements' : 'find_element_fragment';
|
|
383
|
+
let element;
|
|
384
|
+
const doFind = async () => {
|
|
385
|
+
element = await this.executeAtom(atomName, [strategy, selector, contextElement]);
|
|
386
|
+
return !_.isNull(element);
|
|
387
|
+
};
|
|
388
|
+
try {
|
|
389
|
+
await this.implicitWaitForCondition(doFind);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (err.message && _.isFunction(err.message.match) && err.message.match(/Condition unmet/)) {
|
|
392
|
+
// condition was not met setting res to empty array
|
|
393
|
+
element = [];
|
|
394
|
+
} else {
|
|
395
|
+
throw err;
|
|
445
396
|
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (many) {
|
|
446
400
|
return this.cacheWebElements(element);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
async clickWebCoords(x, y) {
|
|
454
|
-
const {x: translatedX, y: translatedY} = await this.translateWebCoords(x, y);
|
|
455
|
-
await this.mobileTap(translatedX, translatedY);
|
|
456
|
-
},
|
|
457
|
-
/**
|
|
458
|
-
* @this {XCUITestDriver}
|
|
459
|
-
* @returns {Promise<boolean>}
|
|
460
|
-
*/
|
|
461
|
-
async getSafariIsIphone() {
|
|
462
|
-
if (_.isBoolean(this._isSafariIphone)) {
|
|
463
|
-
return this._isSafariIphone;
|
|
464
|
-
}
|
|
465
|
-
try {
|
|
466
|
-
const userAgent = /** @type {string} */ (await this.execute('return navigator.userAgent'));
|
|
467
|
-
this._isSafariIphone = userAgent.toLowerCase().includes('iphone');
|
|
468
|
-
} catch (err) {
|
|
469
|
-
this.log.warn(`Unable to find device type from useragent. Assuming iPhone`);
|
|
470
|
-
this.log.debug(`Error: ${err.message}`);
|
|
471
|
-
}
|
|
472
|
-
return this._isSafariIphone ?? true;
|
|
473
|
-
},
|
|
474
|
-
/**
|
|
475
|
-
* @this {XCUITestDriver}
|
|
476
|
-
* @returns {Promise<import('@appium/types').Size>}
|
|
477
|
-
*/
|
|
478
|
-
async getSafariDeviceSize() {
|
|
479
|
-
const script =
|
|
480
|
-
'return {height: window.screen.availHeight * window.devicePixelRatio, width: window.screen.availWidth * window.devicePixelRatio};';
|
|
481
|
-
const {width, height} = /** @type {import('@appium/types').Size} */ (
|
|
482
|
-
await this.execute(script)
|
|
483
|
-
);
|
|
484
|
-
const [normHeight, normWidth] = height > width ? [height, width] : [width, height];
|
|
485
|
-
return {
|
|
486
|
-
width: normWidth,
|
|
487
|
-
height: normHeight,
|
|
488
|
-
};
|
|
489
|
-
},
|
|
490
|
-
/**
|
|
491
|
-
* @this {XCUITestDriver}
|
|
492
|
-
* @returns {Promise<boolean>}
|
|
493
|
-
*/
|
|
494
|
-
async getSafariIsNotched() {
|
|
495
|
-
if (_.isBoolean(this._isSafariNotched)) {
|
|
496
|
-
return this._isSafariNotched;
|
|
497
|
-
}
|
|
401
|
+
}
|
|
402
|
+
if (_.isEmpty(element)) {
|
|
403
|
+
throw new errors.NoSuchElementError();
|
|
404
|
+
}
|
|
405
|
+
return this.cacheWebElements(element);
|
|
406
|
+
}
|
|
498
407
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
408
|
+
/**
|
|
409
|
+
* @this {XCUITestDriver}
|
|
410
|
+
* @param {number} x
|
|
411
|
+
* @param {number} y
|
|
412
|
+
*/
|
|
413
|
+
export async function clickWebCoords(x, y) {
|
|
414
|
+
const {x: translatedX, y: translatedY} = await this.translateWebCoords(x, y);
|
|
415
|
+
await this.mobileTap(translatedX, translatedY);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* @this {XCUITestDriver}
|
|
420
|
+
* @returns {Promise<boolean>}
|
|
421
|
+
*/
|
|
422
|
+
export async function getSafariIsIphone() {
|
|
423
|
+
if (_.isBoolean(this._isSafariIphone)) {
|
|
424
|
+
return this._isSafariIphone;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const userAgent = /** @type {string} */ (await this.execute('return navigator.userAgent'));
|
|
428
|
+
this._isSafariIphone = userAgent.toLowerCase().includes('iphone');
|
|
429
|
+
} catch (err) {
|
|
430
|
+
this.log.warn(`Unable to find device type from useragent. Assuming iPhone`);
|
|
431
|
+
this.log.debug(`Error: ${err.message}`);
|
|
432
|
+
}
|
|
433
|
+
return this._isSafariIphone ?? true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* @this {XCUITestDriver}
|
|
438
|
+
* @returns {Promise<import('@appium/types').Size>}
|
|
439
|
+
*/
|
|
440
|
+
export async function getSafariDeviceSize() {
|
|
441
|
+
const script =
|
|
442
|
+
'return {height: window.screen.availHeight * window.devicePixelRatio, width: window.screen.availWidth * window.devicePixelRatio};';
|
|
443
|
+
const {width, height} = /** @type {import('@appium/types').Size} */ (
|
|
444
|
+
await this.execute(script)
|
|
445
|
+
);
|
|
446
|
+
const [normHeight, normWidth] = height > width ? [height, width] : [width, height];
|
|
447
|
+
return {
|
|
448
|
+
width: normWidth,
|
|
449
|
+
height: normHeight,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* @this {XCUITestDriver}
|
|
455
|
+
* @returns {Promise<boolean>}
|
|
456
|
+
*/
|
|
457
|
+
export async function getSafariIsNotched() {
|
|
458
|
+
if (_.isBoolean(this._isSafariNotched)) {
|
|
459
|
+
return this._isSafariNotched;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const {width, height} = await this.getSafariDeviceSize();
|
|
464
|
+
for (const device of NOTCHED_DEVICE_SIZES) {
|
|
465
|
+
if (device.w === width && device.h === height) {
|
|
466
|
+
this._isSafariNotched = true;
|
|
505
467
|
}
|
|
506
|
-
} catch (err) {
|
|
507
|
-
this.log.warn(
|
|
508
|
-
`Unable to find device type from dimensions. Assuming the device is not notched`,
|
|
509
|
-
);
|
|
510
|
-
this.log.debug(`Error: ${err.message}`);
|
|
511
|
-
}
|
|
512
|
-
return this._isSafariNotched ?? false;
|
|
513
|
-
},
|
|
514
|
-
/**
|
|
515
|
-
* @this {XCUITestDriver}
|
|
516
|
-
*/
|
|
517
|
-
async getExtraTranslateWebCoordsOffset(wvPos, realDims) {
|
|
518
|
-
let topOffset = 0;
|
|
519
|
-
let bottomOffset = 0;
|
|
520
|
-
|
|
521
|
-
const isIphone = await this.getSafariIsIphone();
|
|
522
|
-
|
|
523
|
-
// No need to check whether the Smart App Banner or Tab Bar is visible or not
|
|
524
|
-
// if already defined by nativeWebTapTabBarVisibility or nativeWebTapSmartAppBannerVisibility in settings.
|
|
525
|
-
const {
|
|
526
|
-
nativeWebTapTabBarVisibility,
|
|
527
|
-
nativeWebTapSmartAppBannerVisibility,
|
|
528
|
-
safariTabBarPosition = util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '>=', '15.0') &&
|
|
529
|
-
isIphone
|
|
530
|
-
? TAB_BAR_POSITION_BOTTOM
|
|
531
|
-
: TAB_BAR_POSITION_TOP,
|
|
532
|
-
} = this.settings.getSettings();
|
|
533
|
-
let tabBarVisibility = _.lowerCase(String(nativeWebTapTabBarVisibility));
|
|
534
|
-
let bannerVisibility = _.lowerCase(String(nativeWebTapSmartAppBannerVisibility));
|
|
535
|
-
const tabBarPosition = _.lowerCase(String(safariTabBarPosition));
|
|
536
|
-
|
|
537
|
-
if (!VISIBILITIES.includes(tabBarVisibility)) {
|
|
538
|
-
tabBarVisibility = DETECT;
|
|
539
|
-
}
|
|
540
|
-
if (!VISIBILITIES.includes(bannerVisibility)) {
|
|
541
|
-
bannerVisibility = DETECT;
|
|
542
468
|
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
this.log.warn(
|
|
471
|
+
`Unable to find device type from dimensions. Assuming the device is not notched`,
|
|
472
|
+
);
|
|
473
|
+
this.log.debug(`Error: ${err.message}`);
|
|
474
|
+
}
|
|
475
|
+
return this._isSafariNotched ?? false;
|
|
476
|
+
}
|
|
543
477
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
478
|
+
/**
|
|
479
|
+
* @this {XCUITestDriver}
|
|
480
|
+
*/
|
|
481
|
+
export async function getExtraTranslateWebCoordsOffset(wvPos, realDims) {
|
|
482
|
+
let topOffset = 0;
|
|
483
|
+
let bottomOffset = 0;
|
|
484
|
+
|
|
485
|
+
const isIphone = await this.getSafariIsIphone();
|
|
486
|
+
|
|
487
|
+
// No need to check whether the Smart App Banner or Tab Bar is visible or not
|
|
488
|
+
// if already defined by nativeWebTapTabBarVisibility or nativeWebTapSmartAppBannerVisibility in settings.
|
|
489
|
+
const {
|
|
490
|
+
nativeWebTapTabBarVisibility,
|
|
491
|
+
nativeWebTapSmartAppBannerVisibility,
|
|
492
|
+
safariTabBarPosition = util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '>=', '15.0') &&
|
|
493
|
+
isIphone
|
|
494
|
+
? TAB_BAR_POSITION_BOTTOM
|
|
495
|
+
: TAB_BAR_POSITION_TOP,
|
|
496
|
+
} = this.settings.getSettings();
|
|
497
|
+
let tabBarVisibility = _.lowerCase(String(nativeWebTapTabBarVisibility));
|
|
498
|
+
let bannerVisibility = _.lowerCase(String(nativeWebTapSmartAppBannerVisibility));
|
|
499
|
+
const tabBarPosition = _.lowerCase(String(safariTabBarPosition));
|
|
500
|
+
|
|
501
|
+
if (!VISIBILITIES.includes(tabBarVisibility)) {
|
|
502
|
+
tabBarVisibility = DETECT;
|
|
503
|
+
}
|
|
504
|
+
if (!VISIBILITIES.includes(bannerVisibility)) {
|
|
505
|
+
bannerVisibility = DETECT;
|
|
506
|
+
}
|
|
549
507
|
|
|
550
|
-
|
|
508
|
+
if (!TAB_BAR_POSSITIONS.includes(tabBarPosition)) {
|
|
509
|
+
throw new errors.InvalidArgumentError(
|
|
510
|
+
`${safariTabBarPosition} is invalid as Safari tab bar position. Available positions are ${TAB_BAR_POSSITIONS}.`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
551
513
|
|
|
552
|
-
|
|
514
|
+
const isNotched = isIphone && (await this.getSafariIsNotched());
|
|
553
515
|
|
|
554
|
-
|
|
555
|
-
? util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '=', '13.0')
|
|
556
|
-
? IPHONE_X_NOTCH_OFFSET_IOS_13
|
|
557
|
-
: IPHONE_X_NOTCH_OFFSET_IOS
|
|
558
|
-
: 0;
|
|
516
|
+
const orientation = realDims.h > realDims.w ? 'PORTRAIT' : 'LANDSCAPE';
|
|
559
517
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
518
|
+
const notchOffset = isNotched
|
|
519
|
+
? util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '=', '13.0')
|
|
520
|
+
? IPHONE_X_NOTCH_OFFSET_IOS_13
|
|
521
|
+
: IPHONE_X_NOTCH_OFFSET_IOS
|
|
522
|
+
: 0;
|
|
563
523
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
524
|
+
const isScrolled = await this.execute('return document.documentElement.scrollTop > 0');
|
|
525
|
+
if (isScrolled) {
|
|
526
|
+
topOffset = IPHONE_SCROLLED_TOP_BAR_HEIGHT + notchOffset;
|
|
567
527
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
topOffset
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
528
|
+
if (isNotched) {
|
|
529
|
+
topOffset -= IPHONE_X_SCROLLED_OFFSET;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// If the iPhone is landscape then there is no top bar
|
|
533
|
+
if (orientation === 'LANDSCAPE' && isIphone) {
|
|
534
|
+
topOffset = 0;
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
topOffset = tabBarPosition === TAB_BAR_POSITION_BOTTOM ? 0 : IPHONE_TOP_BAR_HEIGHT;
|
|
538
|
+
topOffset += notchOffset;
|
|
539
|
+
this.log.debug(`tabBarPosition and topOffset: ${tabBarPosition}, ${topOffset}`);
|
|
540
|
+
|
|
541
|
+
if (isIphone) {
|
|
542
|
+
if (orientation === 'PORTRAIT') {
|
|
543
|
+
// The bottom bar is only visible when portrait
|
|
544
|
+
bottomOffset = IPHONE_BOTTOM_BAR_OFFSET;
|
|
545
|
+
} else {
|
|
546
|
+
topOffset = IPHONE_LANDSCAPE_TOP_BAR_HEIGHT;
|
|
584
547
|
}
|
|
548
|
+
}
|
|
585
549
|
|
|
586
|
-
|
|
587
|
-
|
|
550
|
+
if (orientation === 'LANDSCAPE' || !isIphone) {
|
|
551
|
+
if (tabBarVisibility === VISIBLE) {
|
|
552
|
+
topOffset += TAB_BAR_OFFSET;
|
|
553
|
+
} else if (tabBarVisibility === DETECT) {
|
|
554
|
+
// Tabs only appear if the device is landscape or if it's an iPad so we only check visibility in this case
|
|
555
|
+
// Assume that each tab bar is a WebView
|
|
556
|
+
const contextsAndViews = await this.getContextsAndViews();
|
|
557
|
+
const tabs = contextsAndViews.filter((ctx) => ctx.id.startsWith('WEBVIEW_'));
|
|
558
|
+
|
|
559
|
+
if (tabs.length > 1) {
|
|
560
|
+
this.log.debug(`Found ${tabs.length} tabs. Assuming the tab bar is visible`);
|
|
588
561
|
topOffset += TAB_BAR_OFFSET;
|
|
589
|
-
} else if (tabBarVisibility === DETECT) {
|
|
590
|
-
// Tabs only appear if the device is landscape or if it's an iPad so we only check visibility in this case
|
|
591
|
-
// Assume that each tab bar is a WebView
|
|
592
|
-
const contextsAndViews = await this.getContextsAndViews();
|
|
593
|
-
const tabs = contextsAndViews.filter((ctx) => ctx.id.startsWith('WEBVIEW_'));
|
|
594
|
-
|
|
595
|
-
if (tabs.length > 1) {
|
|
596
|
-
this.log.debug(`Found ${tabs.length} tabs. Assuming the tab bar is visible`);
|
|
597
|
-
topOffset += TAB_BAR_OFFSET;
|
|
598
|
-
}
|
|
599
562
|
}
|
|
600
563
|
}
|
|
601
564
|
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
topOffset += await this.getExtraNativeWebTapOffset(isIphone, bannerVisibility);
|
|
568
|
+
|
|
569
|
+
wvPos.y += topOffset;
|
|
570
|
+
realDims.h -= topOffset + bottomOffset;
|
|
571
|
+
}
|
|
602
572
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if
|
|
573
|
+
/**
|
|
574
|
+
* @this {XCUITestDriver}
|
|
575
|
+
* @param {boolean} isIphone
|
|
576
|
+
* @param {string} bannerVisibility
|
|
577
|
+
* @returns {Promise<number>}
|
|
578
|
+
*/
|
|
579
|
+
export async function getExtraNativeWebTapOffset(isIphone, bannerVisibility) {
|
|
580
|
+
let offset = 0;
|
|
581
|
+
|
|
582
|
+
if (bannerVisibility === VISIBLE) {
|
|
583
|
+
offset += isIphone
|
|
584
|
+
? IPHONE_WEB_COORD_SMART_APP_BANNER_OFFSET
|
|
585
|
+
: IPAD_WEB_COORD_SMART_APP_BANNER_OFFSET;
|
|
586
|
+
} else if (bannerVisibility === DETECT) {
|
|
587
|
+
// try to see if there is an Smart App Banner
|
|
588
|
+
const banners = /** @type {import('@appium/types').Element[]} */ (
|
|
589
|
+
await this.findNativeElementOrElements('accessibility id', 'Close app download offer', true)
|
|
590
|
+
);
|
|
591
|
+
if (banners?.length) {
|
|
618
592
|
offset += isIphone
|
|
619
593
|
? IPHONE_WEB_COORD_SMART_APP_BANNER_OFFSET
|
|
620
594
|
: IPAD_WEB_COORD_SMART_APP_BANNER_OFFSET;
|
|
621
|
-
} else if (bannerVisibility === DETECT) {
|
|
622
|
-
// try to see if there is an Smart App Banner
|
|
623
|
-
const banners = /** @type {import('@appium/types').Element[]} */ (
|
|
624
|
-
await this.findNativeElementOrElements('accessibility id', 'Close app download offer', true)
|
|
625
|
-
);
|
|
626
|
-
if (banners?.length) {
|
|
627
|
-
offset += isIphone
|
|
628
|
-
? IPHONE_WEB_COORD_SMART_APP_BANNER_OFFSET
|
|
629
|
-
: IPAD_WEB_COORD_SMART_APP_BANNER_OFFSET;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
this.log.debug(`Additional native web tap offset computed: ${offset}`);
|
|
634
|
-
return offset;
|
|
635
|
-
},
|
|
636
|
-
/**
|
|
637
|
-
* @this {XCUITestDriver}
|
|
638
|
-
* @param {any} el
|
|
639
|
-
* @returns {Promise<void>}
|
|
640
|
-
*/
|
|
641
|
-
async nativeWebTap(el) {
|
|
642
|
-
const atomsElement = this.getAtomsElement(el);
|
|
643
|
-
|
|
644
|
-
// if strict native tap, do not try to do it with WDA directly
|
|
645
|
-
if (
|
|
646
|
-
!(this.settings.getSettings()).nativeWebTapStrict &&
|
|
647
|
-
(await tapWebElementNatively.bind(this)(atomsElement))
|
|
648
|
-
) {
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
this.log.warn('Unable to do simple native web tap. Attempting to convert coordinates');
|
|
652
|
-
|
|
653
|
-
const [size, coordinates] =
|
|
654
|
-
/** @type {[import('@appium/types').Size, import('@appium/types').Position]} */ (
|
|
655
|
-
await B.Promise.all([
|
|
656
|
-
this.executeAtom('get_size', [atomsElement]),
|
|
657
|
-
this.executeAtom('get_top_left_coordinates', [atomsElement]),
|
|
658
|
-
])
|
|
659
|
-
);
|
|
660
|
-
const {width, height} = size;
|
|
661
|
-
const {x, y} = coordinates;
|
|
662
|
-
await this.clickWebCoords(x + width / 2, y + height / 2);
|
|
663
|
-
},
|
|
664
|
-
/**
|
|
665
|
-
* @this {XCUITestDriver}
|
|
666
|
-
* @param {number} x
|
|
667
|
-
* @param {number} y
|
|
668
|
-
* @returns {Promise<import('@appium/types').Position>}
|
|
669
|
-
*/
|
|
670
|
-
async translateWebCoords(x, y) {
|
|
671
|
-
this.log.debug(`Translating web coordinates (${JSON.stringify({x, y})}) to native coordinates`);
|
|
672
|
-
|
|
673
|
-
if (this.webviewCalibrationResult) {
|
|
674
|
-
this.log.debug(`Will use the recent calibration result: ${JSON.stringify(this.webviewCalibrationResult)}`);
|
|
675
|
-
const { offsetX, offsetY, pixelRatioX, pixelRatioY } = this.webviewCalibrationResult;
|
|
676
|
-
const cmd = '(function () {return {innerWidth: window.innerWidth, innerHeight: window.innerHeight, ' +
|
|
677
|
-
'outerWidth: window.outerWidth, outerHeight: window.outerHeight}; })()';
|
|
678
|
-
const wvDims = await (/** @type {RemoteDebugger} */ (this.remote)).execute(cmd);
|
|
679
|
-
// https://tripleodeon.com/2011/12/first-understand-your-screen/
|
|
680
|
-
const shouldApplyPixelRatio = wvDims.innerWidth > wvDims.outerWidth
|
|
681
|
-
|| wvDims.innerHeight > wvDims.outerHeight;
|
|
682
|
-
return {
|
|
683
|
-
x: offsetX + x * (shouldApplyPixelRatio ? pixelRatioX : 1),
|
|
684
|
-
y: offsetY + y * (shouldApplyPixelRatio ? pixelRatioY : 1),
|
|
685
|
-
};
|
|
686
|
-
} else {
|
|
687
|
-
this.log.debug(
|
|
688
|
-
`Using the legacy algorithm for coordinates translation. ` +
|
|
689
|
-
`Invoke 'mobile: calibrateWebToRealCoordinatesTranslation' to change that.`
|
|
690
|
-
);
|
|
691
595
|
}
|
|
596
|
+
}
|
|
692
597
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
try {
|
|
697
|
-
webview = /** @type {import('@appium/types').Element|undefined} */ (
|
|
698
|
-
await retryInterval(
|
|
699
|
-
5,
|
|
700
|
-
100,
|
|
701
|
-
async () =>
|
|
702
|
-
await this.findNativeElementOrElements('class name', 'XCUIElementTypeWebView', false),
|
|
703
|
-
)
|
|
704
|
-
);
|
|
705
|
-
} catch {}
|
|
598
|
+
this.log.debug(`Additional native web tap offset computed: ${offset}`);
|
|
599
|
+
return offset;
|
|
600
|
+
}
|
|
706
601
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
602
|
+
/**
|
|
603
|
+
* @this {XCUITestDriver}
|
|
604
|
+
* @param {any} el
|
|
605
|
+
* @returns {Promise<void>}
|
|
606
|
+
*/
|
|
607
|
+
export async function nativeWebTap(el) {
|
|
608
|
+
const atomsElement = this.getAtomsElement(el);
|
|
609
|
+
|
|
610
|
+
// if strict native tap, do not try to do it with WDA directly
|
|
611
|
+
if (
|
|
612
|
+
!(this.settings.getSettings()).nativeWebTapStrict &&
|
|
613
|
+
(await tapWebElementNatively.bind(this)(atomsElement))
|
|
614
|
+
) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
this.log.warn('Unable to do simple native web tap. Attempting to convert coordinates');
|
|
618
|
+
|
|
619
|
+
const [size, coordinates] =
|
|
620
|
+
/** @type {[import('@appium/types').Size, import('@appium/types').Position]} */ (
|
|
621
|
+
await B.Promise.all([
|
|
622
|
+
this.executeAtom('get_size', [atomsElement]),
|
|
623
|
+
this.executeAtom('get_top_left_coordinates', [atomsElement]),
|
|
624
|
+
])
|
|
625
|
+
);
|
|
626
|
+
const {width, height} = size;
|
|
627
|
+
const {x, y} = coordinates;
|
|
628
|
+
await this.clickWebCoords(x + width / 2, y + height / 2);
|
|
629
|
+
}
|
|
710
630
|
|
|
711
|
-
|
|
631
|
+
/**
|
|
632
|
+
* @this {XCUITestDriver}
|
|
633
|
+
* @param {number} x
|
|
634
|
+
* @param {number} y
|
|
635
|
+
* @returns {Promise<import('@appium/types').Position>}
|
|
636
|
+
*/
|
|
637
|
+
export async function translateWebCoords(x, y) {
|
|
638
|
+
this.log.debug(`Translating web coordinates (${JSON.stringify({x, y})}) to native coordinates`);
|
|
639
|
+
|
|
640
|
+
if (this.webviewCalibrationResult) {
|
|
641
|
+
this.log.debug(`Will use the recent calibration result: ${JSON.stringify(this.webviewCalibrationResult)}`);
|
|
642
|
+
const { offsetX, offsetY, pixelRatioX, pixelRatioY } = this.webviewCalibrationResult;
|
|
643
|
+
const cmd = '(function () {return {innerWidth: window.innerWidth, innerHeight: window.innerHeight, ' +
|
|
644
|
+
'outerWidth: window.outerWidth, outerHeight: window.outerHeight}; })()';
|
|
645
|
+
const wvDims = await (/** @type {RemoteDebugger} */ (this.remote)).execute(cmd);
|
|
646
|
+
// https://tripleodeon.com/2011/12/first-understand-your-screen/
|
|
647
|
+
const shouldApplyPixelRatio = wvDims.innerWidth > wvDims.outerWidth
|
|
648
|
+
|| wvDims.innerHeight > wvDims.outerHeight;
|
|
649
|
+
return {
|
|
650
|
+
x: offsetX + x * (shouldApplyPixelRatio ? pixelRatioX : 1),
|
|
651
|
+
y: offsetY + y * (shouldApplyPixelRatio ? pixelRatioY : 1),
|
|
652
|
+
};
|
|
653
|
+
} else {
|
|
654
|
+
this.log.debug(
|
|
655
|
+
`Using the legacy algorithm for coordinates translation. ` +
|
|
656
|
+
`Invoke 'mobile: calibrateWebToRealCoordinatesTranslation' to change that.`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
712
659
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
660
|
+
// absolutize web coords
|
|
661
|
+
/** @type {import('@appium/types').Element|undefined|string} */
|
|
662
|
+
let webview;
|
|
663
|
+
try {
|
|
664
|
+
webview = /** @type {import('@appium/types').Element|undefined} */ (
|
|
665
|
+
await retryInterval(
|
|
666
|
+
5,
|
|
667
|
+
100,
|
|
668
|
+
async () =>
|
|
669
|
+
await this.findNativeElementOrElements('class name', 'XCUIElementTypeWebView', false),
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
} catch {}
|
|
716
673
|
|
|
717
|
-
|
|
718
|
-
|
|
674
|
+
if (!webview) {
|
|
675
|
+
throw new Error(`No WebView found. Unable to translate web coordinates for native web tap.`);
|
|
676
|
+
}
|
|
719
677
|
|
|
720
|
-
|
|
721
|
-
// https://github.com/appium/appium/issues/14988
|
|
722
|
-
const implicitWaitMs = this.implicitWaitMs;
|
|
723
|
-
this.setImplicitWait(0);
|
|
724
|
-
try {
|
|
725
|
-
await this.getExtraTranslateWebCoordsOffset(wvPos, realDims);
|
|
726
|
-
} finally {
|
|
727
|
-
this.setImplicitWait(implicitWaitMs);
|
|
728
|
-
}
|
|
729
|
-
if (!wvDims || !realDims || !wvPos) {
|
|
730
|
-
throw new Error(
|
|
731
|
-
`Web coordinates ${JSON.stringify({x, y})} cannot be translated into real coordinates. ` +
|
|
732
|
-
`Try to invoke 'mobile: calibrateWebToRealCoordinatesTranslation' or consider translating the ` +
|
|
733
|
-
`coordinates from the client code.`
|
|
734
|
-
);
|
|
735
|
-
}
|
|
678
|
+
webview = util.unwrapElement(webview);
|
|
736
679
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
x: wvPos.x + Math.round(xRatio * x),
|
|
741
|
-
y: wvPos.y + Math.round(yRatio * y),
|
|
742
|
-
};
|
|
680
|
+
const rect = /** @type {Rect} */ (await this.proxyCommand(`/element/${webview}/rect`, 'GET'));
|
|
681
|
+
const wvPos = {x: rect.x, y: rect.y};
|
|
682
|
+
const realDims = {w: rect.width, h: rect.height};
|
|
743
683
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
this.log.debug(`Converted coordinates: ${JSON.stringify(newCoords)}`);
|
|
747
|
-
this.log.debug(` rect: ${JSON.stringify(rect)}`);
|
|
748
|
-
this.log.debug(` wvPos: ${JSON.stringify(wvPos)}`);
|
|
749
|
-
this.log.debug(` realDims: ${JSON.stringify(realDims)}`);
|
|
750
|
-
this.log.debug(` wvDims: ${JSON.stringify(wvDims)}`);
|
|
751
|
-
this.log.debug(` xRatio: ${JSON.stringify(xRatio)}`);
|
|
752
|
-
this.log.debug(` yRatio: ${JSON.stringify(yRatio)}`);
|
|
684
|
+
const cmd = '(function () { return {w: window.innerWidth, h: window.innerHeight}; })()';
|
|
685
|
+
const wvDims = await (/** @type {RemoteDebugger} */ (this.remote)).execute(cmd);
|
|
753
686
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
687
|
+
// keep track of implicit wait, and set locally to 0
|
|
688
|
+
// https://github.com/appium/appium/issues/14988
|
|
689
|
+
const implicitWaitMs = this.implicitWaitMs;
|
|
690
|
+
this.setImplicitWait(0);
|
|
691
|
+
try {
|
|
692
|
+
await this.getExtraTranslateWebCoordsOffset(wvPos, realDims);
|
|
693
|
+
} finally {
|
|
694
|
+
this.setImplicitWait(implicitWaitMs);
|
|
695
|
+
}
|
|
696
|
+
if (!wvDims || !realDims || !wvPos) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`Web coordinates ${JSON.stringify({x, y})} cannot be translated into real coordinates. ` +
|
|
699
|
+
`Try to invoke 'mobile: calibrateWebToRealCoordinatesTranslation' or consider translating the ` +
|
|
700
|
+
`coordinates from the client code.`
|
|
758
701
|
);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
} catch (err) {
|
|
786
|
-
const originalError = err instanceof AggregateError ? err[0] : err;
|
|
787
|
-
this.log.debug(`Error received while executing atom: ${originalError.message}`);
|
|
788
|
-
throw (
|
|
789
|
-
originalError instanceof TimeoutError
|
|
790
|
-
? (await generateAtomTimeoutError.bind(this)(timer))
|
|
791
|
-
: originalError
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
};
|
|
795
|
-
// if the atom promise is fulfilled within ATOM_INITIAL_WAIT_MS
|
|
796
|
-
// then we don't need to check for an alert presence
|
|
797
|
-
await handlePromiseError(B.any([B.delay(ATOM_INITIAL_WAIT_MS), timedAtomPromise]));
|
|
798
|
-
if (timedAtomPromise.isFulfilled()) {
|
|
799
|
-
return await timedAtomPromise;
|
|
800
|
-
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const xRatio = realDims.w / wvDims.w;
|
|
705
|
+
const yRatio = realDims.h / wvDims.h;
|
|
706
|
+
const newCoords = {
|
|
707
|
+
x: wvPos.x + Math.round(xRatio * x),
|
|
708
|
+
y: wvPos.y + Math.round(yRatio * y),
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// additional logging for coordinates, since it is sometimes broken
|
|
712
|
+
// see https://github.com/appium/appium/issues/9159
|
|
713
|
+
this.log.debug(`Converted coordinates: ${JSON.stringify(newCoords)}`);
|
|
714
|
+
this.log.debug(` rect: ${JSON.stringify(rect)}`);
|
|
715
|
+
this.log.debug(` wvPos: ${JSON.stringify(wvPos)}`);
|
|
716
|
+
this.log.debug(` realDims: ${JSON.stringify(realDims)}`);
|
|
717
|
+
this.log.debug(` wvDims: ${JSON.stringify(wvDims)}`);
|
|
718
|
+
this.log.debug(` xRatio: ${JSON.stringify(xRatio)}`);
|
|
719
|
+
this.log.debug(` yRatio: ${JSON.stringify(yRatio)}`);
|
|
720
|
+
|
|
721
|
+
this.log.debug(
|
|
722
|
+
`Converted web coords ${JSON.stringify({x, y})} into real coords ${JSON.stringify(
|
|
723
|
+
newCoords,
|
|
724
|
+
)}`,
|
|
725
|
+
);
|
|
726
|
+
return newCoords;
|
|
727
|
+
}
|
|
801
728
|
|
|
802
|
-
|
|
803
|
-
|
|
729
|
+
/**
|
|
730
|
+
* @this {XCUITestDriver}
|
|
731
|
+
* @returns {Promise<boolean>}
|
|
732
|
+
*/
|
|
733
|
+
export async function checkForAlert() {
|
|
734
|
+
return _.isString(await this.getAlertText());
|
|
735
|
+
}
|
|
804
736
|
|
|
805
|
-
|
|
806
|
-
|
|
737
|
+
/**
|
|
738
|
+
* @param {Promise<any>} promise
|
|
739
|
+
* @this {XCUITestDriver}
|
|
740
|
+
*/
|
|
741
|
+
export async function waitForAtom(promise) {
|
|
742
|
+
const timer = new timing.Timer().start();
|
|
743
|
+
|
|
744
|
+
const atomWaitTimeoutMs = _.isNumber(this.opts.webviewAtomWaitTimeout) && this.opts.webviewAtomWaitTimeout > 0
|
|
745
|
+
? this.opts.webviewAtomWaitTimeout
|
|
746
|
+
: ATOM_WAIT_TIMEOUT_MS;
|
|
747
|
+
// need to check for alert while the atom is being executed.
|
|
748
|
+
// so notify ourselves when it happens
|
|
749
|
+
const timedAtomPromise = B.resolve(promise).timeout(atomWaitTimeoutMs);
|
|
750
|
+
const handlePromiseError = async (p) => {
|
|
807
751
|
try {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
752
|
+
return await p;
|
|
753
|
+
} catch (err) {
|
|
754
|
+
const originalError = err instanceof AggregateError ? err[0] : err;
|
|
755
|
+
this.log.debug(`Error received while executing atom: ${originalError.message}`);
|
|
756
|
+
throw (
|
|
757
|
+
originalError instanceof TimeoutError
|
|
758
|
+
? (await generateAtomTimeoutError.bind(this)(timer))
|
|
759
|
+
: originalError
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
// if the atom promise is fulfilled within ATOM_INITIAL_WAIT_MS
|
|
764
|
+
// then we don't need to check for an alert presence
|
|
765
|
+
await handlePromiseError(B.any([B.delay(ATOM_INITIAL_WAIT_MS), timedAtomPromise]));
|
|
766
|
+
if (timedAtomPromise.isFulfilled()) {
|
|
767
|
+
return await timedAtomPromise;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ...otherwise make sure there is no unexpected alert covering the element
|
|
771
|
+
this._waitingAtoms.count++;
|
|
772
|
+
|
|
773
|
+
let onAlertCallback;
|
|
774
|
+
let onAppCrashCallback;
|
|
775
|
+
try {
|
|
776
|
+
// only restart the monitor if it is not running already
|
|
777
|
+
if (this._waitingAtoms.alertMonitor.isResolved()) {
|
|
778
|
+
this._waitingAtoms.alertMonitor = B.resolve(
|
|
779
|
+
(async () => {
|
|
780
|
+
while (this._waitingAtoms.count > 0) {
|
|
781
|
+
try {
|
|
782
|
+
if (await this.checkForAlert()) {
|
|
783
|
+
this._waitingAtoms.alertNotifier.emit(ON_OBSTRUCTING_ALERT_EVENT);
|
|
784
|
+
}
|
|
785
|
+
} catch (err) {
|
|
786
|
+
if (isErrorType(err, errors.InvalidElementStateError)) {
|
|
787
|
+
this._waitingAtoms.alertNotifier.emit(ON_APP_CRASH_EVENT, err);
|
|
821
788
|
}
|
|
822
|
-
await B.delay(OBSTRUCTING_ALERT_PRESENCE_CHECK_INTERVAL_MS);
|
|
823
789
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return await new B((resolve, reject) => {
|
|
829
|
-
onAlertCallback = () => reject(new errors.UnexpectedAlertOpenError());
|
|
830
|
-
onAppCrashCallback = reject;
|
|
831
|
-
this._waitingAtoms.alertNotifier.once(ON_OBSTRUCTING_ALERT_EVENT, onAlertCallback);
|
|
832
|
-
this._waitingAtoms.alertNotifier.once(ON_APP_CRASH_EVENT, onAppCrashCallback);
|
|
833
|
-
handlePromiseError(timedAtomPromise)
|
|
834
|
-
.then(resolve)
|
|
835
|
-
.catch(reject);
|
|
836
|
-
});
|
|
837
|
-
} finally {
|
|
838
|
-
if (onAlertCallback) {
|
|
839
|
-
this._waitingAtoms.alertNotifier.removeListener(
|
|
840
|
-
ON_OBSTRUCTING_ALERT_EVENT,
|
|
841
|
-
onAlertCallback,
|
|
842
|
-
);
|
|
843
|
-
}
|
|
844
|
-
if (onAppCrashCallback) {
|
|
845
|
-
this._waitingAtoms.alertNotifier.removeListener(ON_APP_CRASH_EVENT, onAppCrashCallback);
|
|
846
|
-
}
|
|
847
|
-
this._waitingAtoms.count--;
|
|
790
|
+
await B.delay(OBSTRUCTING_ALERT_PRESENCE_CHECK_INTERVAL_MS);
|
|
791
|
+
}
|
|
792
|
+
})(),
|
|
793
|
+
);
|
|
848
794
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
795
|
+
|
|
796
|
+
return await new B((resolve, reject) => {
|
|
797
|
+
onAlertCallback = () => reject(new errors.UnexpectedAlertOpenError());
|
|
798
|
+
onAppCrashCallback = reject;
|
|
799
|
+
this._waitingAtoms.alertNotifier.once(ON_OBSTRUCTING_ALERT_EVENT, onAlertCallback);
|
|
800
|
+
this._waitingAtoms.alertNotifier.once(ON_APP_CRASH_EVENT, onAppCrashCallback);
|
|
801
|
+
handlePromiseError(timedAtomPromise)
|
|
802
|
+
.then(resolve)
|
|
803
|
+
.catch(reject);
|
|
804
|
+
});
|
|
805
|
+
} finally {
|
|
806
|
+
if (onAlertCallback) {
|
|
807
|
+
this._waitingAtoms.alertNotifier.removeListener(
|
|
808
|
+
ON_OBSTRUCTING_ALERT_EVENT,
|
|
809
|
+
onAlertCallback,
|
|
810
|
+
);
|
|
861
811
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* @this {XCUITestDriver}
|
|
866
|
-
* @returns {string} The base url which could be used to access WDA HTTP endpoints
|
|
867
|
-
* FROM THE SAME DEVICE where WDA is running
|
|
868
|
-
*/
|
|
869
|
-
getWdaLocalhostRoot() {
|
|
870
|
-
const remotePort =
|
|
871
|
-
((this.isRealDevice() ? this.opts.wdaRemotePort : null)
|
|
872
|
-
?? this.wda?.url?.port
|
|
873
|
-
?? this.opts.wdaLocalPort)
|
|
874
|
-
|| 8100;
|
|
875
|
-
return `http://127.0.0.1:${remotePort}`;
|
|
876
|
-
},
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* Calibrates web to real coordinates translation.
|
|
880
|
-
* This API can only be called from Safari web context.
|
|
881
|
-
* It must load a custom page to the browser, and then restore
|
|
882
|
-
* the original one, so don't call it if you can potentially
|
|
883
|
-
* lose the current web app state.
|
|
884
|
-
* The outcome of this API is then used in nativeWebTap mode.
|
|
885
|
-
* The returned value could also be used to manually transform web coordinates
|
|
886
|
-
* to real devices ones in client scripts.
|
|
887
|
-
*
|
|
888
|
-
* @this {XCUITestDriver}
|
|
889
|
-
* @returns {Promise<import('../types').CalibrationData>}
|
|
890
|
-
*/
|
|
891
|
-
async mobileCalibrateWebToRealCoordinatesTranslation() {
|
|
892
|
-
if (!this.isWebContext()) {
|
|
893
|
-
throw new errors.NotImplementedError('This API can only be called from a web context');
|
|
812
|
+
if (onAppCrashCallback) {
|
|
813
|
+
this._waitingAtoms.alertNotifier.removeListener(ON_APP_CRASH_EVENT, onAppCrashCallback);
|
|
894
814
|
}
|
|
815
|
+
this._waitingAtoms.count--;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
895
818
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
819
|
+
/**
|
|
820
|
+
* @param {string} navType
|
|
821
|
+
* @this {XCUITestDriver}
|
|
822
|
+
*/
|
|
823
|
+
export async function mobileWebNav(navType) {
|
|
824
|
+
(/** @type {RemoteDebugger} */ (this.remote)).allowNavigationWithoutReload = true;
|
|
825
|
+
try {
|
|
826
|
+
await this.executeAtom('execute_script', [`history.${navType}();`, null]);
|
|
827
|
+
} finally {
|
|
828
|
+
(/** @type {RemoteDebugger} */ (this.remote)).allowNavigationWithoutReload = false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
903
831
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
throw new Error(errorPrefix);
|
|
918
|
-
}
|
|
919
|
-
return result;
|
|
920
|
-
};
|
|
832
|
+
/**
|
|
833
|
+
* @this {XCUITestDriver}
|
|
834
|
+
* @returns {string} The base url which could be used to access WDA HTTP endpoints
|
|
835
|
+
* FROM THE SAME DEVICE where WDA is running
|
|
836
|
+
*/
|
|
837
|
+
export function getWdaLocalhostRoot() {
|
|
838
|
+
const remotePort =
|
|
839
|
+
((this.isRealDevice() ? this.opts.wdaRemotePort : null)
|
|
840
|
+
?? this.wda?.url?.port
|
|
841
|
+
?? this.opts.wdaLocalPort)
|
|
842
|
+
|| 8100;
|
|
843
|
+
return `http://127.0.0.1:${remotePort}`;
|
|
844
|
+
}
|
|
921
845
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
);
|
|
846
|
+
/**
|
|
847
|
+
* Calibrates web to real coordinates translation.
|
|
848
|
+
* This API can only be called from Safari web context.
|
|
849
|
+
* It must load a custom page to the browser, and then restore
|
|
850
|
+
* the original one, so don't call it if you can potentially
|
|
851
|
+
* lose the current web app state.
|
|
852
|
+
* The outcome of this API is then used in nativeWebTap mode.
|
|
853
|
+
* The returned value could also be used to manually transform web coordinates
|
|
854
|
+
* to real devices ones in client scripts.
|
|
855
|
+
*
|
|
856
|
+
* @this {XCUITestDriver}
|
|
857
|
+
* @returns {Promise<import('../types').CalibrationData>}
|
|
858
|
+
*/
|
|
859
|
+
export async function mobileCalibrateWebToRealCoordinatesTranslation() {
|
|
860
|
+
if (!this.isWebContext()) {
|
|
861
|
+
throw new errors.NotImplementedError('This API can only be called from a web context');
|
|
862
|
+
}
|
|
942
863
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
864
|
+
const currentUrl = await this.getUrl();
|
|
865
|
+
await this.setUrl(`${this.getWdaLocalhostRoot()}/calibrate`);
|
|
866
|
+
const {width, height} = /** @type {import('@appium/types').Rect} */(
|
|
867
|
+
await this.proxyCommand('/window/rect', 'GET')
|
|
868
|
+
);
|
|
869
|
+
const [centerX, centerY] = [width / 2, height / 2];
|
|
870
|
+
const errorPrefix = 'Cannot determine web view coordinates offset. Are you in Safari context?';
|
|
871
|
+
|
|
872
|
+
const performCalibrationTap = async (/** @type {number} */ tapX, /** @type {number} */ tapY) => {
|
|
873
|
+
await this.mobileTap(tapX, tapY);
|
|
874
|
+
/** @type {import('@appium/types').Position} */
|
|
875
|
+
let result;
|
|
876
|
+
try {
|
|
877
|
+
const title = await this.title();
|
|
878
|
+
this.log.debug(JSON.stringify(title));
|
|
879
|
+
result = _.isPlainObject(title) ? title : JSON.parse(title);
|
|
880
|
+
} catch (e) {
|
|
881
|
+
throw new Error(`${errorPrefix} Original error: ${e.message}`);
|
|
946
882
|
}
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
offsetX: Math.round(result.offsetX),
|
|
951
|
-
offsetY: Math.round(result.offsetY),
|
|
952
|
-
};
|
|
953
|
-
},
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
* @typedef {Object} SafariOpts
|
|
957
|
-
* @property {object} preferences An object containing Safari settings to be updated.
|
|
958
|
-
* The list of available setting names and their values could be retrieved by
|
|
959
|
-
* changing the corresponding Safari settings in the UI and then inspecting
|
|
960
|
-
* 'Library/Preferences/com.apple.mobilesafari.plist' file inside of
|
|
961
|
-
* com.apple.mobilesafari app container.
|
|
962
|
-
* The full path to the Mobile Safari's container could be retrieved from
|
|
963
|
-
* `xcrun simctl get_app_container <sim_udid> com.apple.mobilesafari data`
|
|
964
|
-
* command output.
|
|
965
|
-
* Use the `xcrun simctl spawn <sim_udid> defaults read <path_to_plist>` command
|
|
966
|
-
* to print the plist content to the Terminal.
|
|
967
|
-
*/
|
|
968
|
-
|
|
969
|
-
/**
|
|
970
|
-
* Updates Mobile Safari preferences on an iOS Simulator
|
|
971
|
-
*
|
|
972
|
-
* @param {import('@appium/types').StringRecord} preferences - An object containing Safari settings to be updated.
|
|
973
|
-
* The list of available setting names and their values can be retrieved by changing the
|
|
974
|
-
* corresponding Safari settings in the UI and then inspecting
|
|
975
|
-
* `Library/Preferences/com.apple.mobilesafari.plist` file inside of the `com.apple.mobilesafari`
|
|
976
|
-
* app container within the simulator filesystem. The full path to Mobile Safari's container can
|
|
977
|
-
* be retrieved by running `xcrun simctl get_app_container <sim_udid> com.apple.mobilesafari
|
|
978
|
-
* data`. Use the `xcrun simctl spawn <sim_udid> defaults read <path_to_plist>` command to print
|
|
979
|
-
* the plist content to the Terminal.
|
|
980
|
-
*
|
|
981
|
-
* @group Simulator Only
|
|
982
|
-
* @returns {Promise<void>}
|
|
983
|
-
* @throws {Error} if run on a real device or if the preferences argument is invalid
|
|
984
|
-
* @this {XCUITestDriver}
|
|
985
|
-
*/
|
|
986
|
-
async mobileUpdateSafariPreferences(preferences) {
|
|
987
|
-
if (!this.isSimulator()) {
|
|
988
|
-
throw new Error('This extension is only available for Simulator');
|
|
883
|
+
const {x, y} = result;
|
|
884
|
+
if (!_.isInteger(x) || !_.isInteger(y)) {
|
|
885
|
+
throw new Error(errorPrefix);
|
|
989
886
|
}
|
|
990
|
-
|
|
991
|
-
|
|
887
|
+
return result;
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
await retryInterval(
|
|
891
|
+
6,
|
|
892
|
+
500,
|
|
893
|
+
async () => {
|
|
894
|
+
const {x: x0, y: y0} = await performCalibrationTap(
|
|
895
|
+
centerX - CALIBRATION_TAP_DELTA_PX, centerY - CALIBRATION_TAP_DELTA_PX
|
|
896
|
+
);
|
|
897
|
+
const {x: x1, y: y1} = await performCalibrationTap(
|
|
898
|
+
centerX + CALIBRATION_TAP_DELTA_PX, centerY + CALIBRATION_TAP_DELTA_PX
|
|
899
|
+
);
|
|
900
|
+
const pixelRatioX = CALIBRATION_TAP_DELTA_PX * 2 / (x1 - x0);
|
|
901
|
+
const pixelRatioY = CALIBRATION_TAP_DELTA_PX * 2 / (y1 - y0);
|
|
902
|
+
this.webviewCalibrationResult = {
|
|
903
|
+
offsetX: centerX - CALIBRATION_TAP_DELTA_PX - x0 * pixelRatioX,
|
|
904
|
+
offsetY: centerY - CALIBRATION_TAP_DELTA_PX - y0 * pixelRatioY,
|
|
905
|
+
pixelRatioX,
|
|
906
|
+
pixelRatioY,
|
|
907
|
+
};
|
|
992
908
|
}
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
if (currentUrl) {
|
|
912
|
+
// restore the previous url
|
|
913
|
+
await this.setUrl(currentUrl);
|
|
914
|
+
}
|
|
915
|
+
const result = /** @type {import('../types').CalibrationData} */ (this.webviewCalibrationResult);
|
|
916
|
+
return {
|
|
917
|
+
...result,
|
|
918
|
+
offsetX: Math.round(result.offsetX),
|
|
919
|
+
offsetY: Math.round(result.offsetY),
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
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
|
+
*/
|
|
993
936
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
}
|
|
937
|
+
/**
|
|
938
|
+
* Updates Mobile Safari preferences on an iOS Simulator
|
|
939
|
+
*
|
|
940
|
+
* @param {import('@appium/types').StringRecord} preferences - An object containing Safari settings to be updated.
|
|
941
|
+
* The list of available setting names and their values can be retrieved by changing the
|
|
942
|
+
* corresponding Safari settings in the UI and then inspecting
|
|
943
|
+
* `Library/Preferences/com.apple.mobilesafari.plist` file inside of the `com.apple.mobilesafari`
|
|
944
|
+
* app container within the simulator filesystem. The full path to Mobile Safari's container can
|
|
945
|
+
* be retrieved by running `xcrun simctl get_app_container <sim_udid> com.apple.mobilesafari
|
|
946
|
+
* data`. Use the `xcrun simctl spawn <sim_udid> defaults read <path_to_plist>` command to print
|
|
947
|
+
* the plist content to the Terminal.
|
|
948
|
+
*
|
|
949
|
+
* @group Simulator Only
|
|
950
|
+
* @returns {Promise<void>}
|
|
951
|
+
* @throws {Error} if run on a real device or if the preferences argument is invalid
|
|
952
|
+
* @this {XCUITestDriver}
|
|
953
|
+
*/
|
|
954
|
+
export async function mobileUpdateSafariPreferences(preferences) {
|
|
955
|
+
if (!this.isSimulator()) {
|
|
956
|
+
throw new Error('This extension is only available for Simulator');
|
|
957
|
+
}
|
|
958
|
+
if (!_.isPlainObject(preferences)) {
|
|
959
|
+
throw new errors.InvalidArgumentError('"preferences" argument must be a valid object');
|
|
960
|
+
}
|
|
998
961
|
|
|
999
|
-
|
|
962
|
+
this.log.debug(`About to update Safari preferences: ${JSON.stringify(preferences)}`);
|
|
963
|
+
await /** @type {import('../driver').Simulator} */ (this.device).updateSafariSettings(preferences);
|
|
964
|
+
}
|
|
1000
965
|
|
|
1001
966
|
/**
|
|
1002
967
|
* @this {XCUITestDriver}
|
|
@@ -1025,6 +990,114 @@ async function generateAtomTimeoutError(timer) {
|
|
|
1025
990
|
return new errors.TimeoutError(message);
|
|
1026
991
|
}
|
|
1027
992
|
|
|
993
|
+
/**
|
|
994
|
+
* @this {XCUITestDriver}
|
|
995
|
+
* @param {any} atomsElement
|
|
996
|
+
* @returns {Promise<boolean>}
|
|
997
|
+
*/
|
|
998
|
+
async function tapWebElementNatively(atomsElement) {
|
|
999
|
+
// try to get the text of the element, which will be accessible in the
|
|
1000
|
+
// native context
|
|
1001
|
+
try {
|
|
1002
|
+
const [text1, text2] = await B.all([
|
|
1003
|
+
this.executeAtom('get_text', [atomsElement]),
|
|
1004
|
+
this.executeAtom('get_attribute_value', [atomsElement, 'value'])
|
|
1005
|
+
]);
|
|
1006
|
+
const text = text1 || text2;
|
|
1007
|
+
if (!text) {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const els = await this.findNativeElementOrElements('accessibility id', text, true);
|
|
1012
|
+
if (![1, 2].includes(els.length)) {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const el = els[0];
|
|
1017
|
+
// use tap because on iOS 11.2 and below `nativeClick` crashes WDA
|
|
1018
|
+
const rect = /** @type {import('@appium/types').Rect} */ (await this.proxyCommand(
|
|
1019
|
+
`/element/${util.unwrapElement(el)}/rect`, 'GET'
|
|
1020
|
+
));
|
|
1021
|
+
if (els.length > 1) {
|
|
1022
|
+
const el2 = els[1];
|
|
1023
|
+
const rect2 = /** @type {import('@appium/types').Rect} */ (await this.proxyCommand(
|
|
1024
|
+
`/element/${util.unwrapElement(el2)}/rect`, 'GET',
|
|
1025
|
+
));
|
|
1026
|
+
|
|
1027
|
+
if (
|
|
1028
|
+
rect.x !== rect2.x || rect.y !== rect2.y
|
|
1029
|
+
|| rect.width !== rect2.width || rect.height !== rect2.height
|
|
1030
|
+
) {
|
|
1031
|
+
// These 2 native elements are not referring to the same web element
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
await this.mobileTap(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
1036
|
+
return true;
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
// any failure should fall through and trigger the more elaborate
|
|
1039
|
+
// method of clicking
|
|
1040
|
+
this.log.warn(`Error attempting to click: ${err.message}`);
|
|
1041
|
+
}
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* @param {any} id
|
|
1047
|
+
* @returns {boolean}
|
|
1048
|
+
*/
|
|
1049
|
+
function isValidElementIdentifier(id) {
|
|
1050
|
+
if (!_.isString(id) && !_.isNumber(id)) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
if (_.isString(id) && _.isEmpty(id)) {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
if (_.isNumber(id) && isNaN(id)) {
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Creates a JavaScript Cookie
|
|
1064
|
+
*
|
|
1065
|
+
* @param {string} key
|
|
1066
|
+
* @param {string} value
|
|
1067
|
+
* @param {CookieOptions} [options={}]
|
|
1068
|
+
* @returns {string}
|
|
1069
|
+
*/
|
|
1070
|
+
function createJSCookie(key, value, options = {}) {
|
|
1071
|
+
return [
|
|
1072
|
+
encodeURIComponent(key),
|
|
1073
|
+
'=',
|
|
1074
|
+
value,
|
|
1075
|
+
options.expires ? `; expires=${options.expires}` : '',
|
|
1076
|
+
options.path ? `; path=${options.path}` : '',
|
|
1077
|
+
options.domain ? `; domain=${options.domain}` : '',
|
|
1078
|
+
options.secure ? '; secure' : '',
|
|
1079
|
+
].join('');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* @this {XCUITestDriver}
|
|
1084
|
+
* @param {import('@appium/types').Cookie} cookie
|
|
1085
|
+
* @returns {Promise<any>}
|
|
1086
|
+
*/
|
|
1087
|
+
async function _deleteCookie(cookie) {
|
|
1088
|
+
const url = `http${cookie.secure ? 's' : ''}://${cookie.domain}${cookie.path}`;
|
|
1089
|
+
return await (/** @type {RemoteDebugger} */ (this.remote)).deleteCookie(cookie.name, url);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
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
|
+
|
|
1028
1101
|
/**
|
|
1029
1102
|
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
|
|
1030
1103
|
* @typedef {import('@appium/types').Rect} Rect
|