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