@midscene/ios 1.0.1-beta-20251201114631.0 → 1.0.1-beta-20251202152706.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/bin.mjs +75 -102
- package/dist/es/index.mjs +12 -16
- package/dist/lib/bin.js +45 -64
- package/dist/lib/index.js +968 -990
- package/package.json +6 -6
- package/static/index.html +1 -1
- package/static/static/css/index.987d3a64.css.map +1 -1
- package/static/static/js/index.6af41395.js +10 -0
- package/static/static/js/index.6af41395.js.map +1 -0
- package/static/static/js/index.d05ded96.js +0 -10
- package/static/static/js/index.d05ded96.js.map +0 -1
- /package/static/static/js/{index.d05ded96.js.LICENSE.txt → index.6af41395.js.LICENSE.txt} +0 -0
package/dist/lib/index.js
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
"@midscene/shared/img": function(module) {
|
|
4
|
-
module.exports = import("@midscene/shared/img").then(function(module) {
|
|
5
|
-
return module;
|
|
6
|
-
});
|
|
7
|
-
}
|
|
8
|
-
};
|
|
9
|
-
var __webpack_module_cache__ = {};
|
|
10
|
-
function __webpack_require__(moduleId) {
|
|
11
|
-
var cachedModule = __webpack_module_cache__[moduleId];
|
|
12
|
-
if (void 0 !== cachedModule) return cachedModule.exports;
|
|
13
|
-
var module = __webpack_module_cache__[moduleId] = {
|
|
14
|
-
exports: {}
|
|
15
|
-
};
|
|
16
|
-
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
17
|
-
return module.exports;
|
|
18
|
-
}
|
|
2
|
+
var __webpack_require__ = {};
|
|
19
3
|
(()=>{
|
|
20
4
|
__webpack_require__.n = (module)=>{
|
|
21
5
|
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
@@ -47,1062 +31,1056 @@ function __webpack_require__(moduleId) {
|
|
|
47
31
|
};
|
|
48
32
|
})();
|
|
49
33
|
var __webpack_exports__ = {};
|
|
50
|
-
(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/wda/apps/launch`, {
|
|
75
|
-
bundleId
|
|
76
|
-
});
|
|
77
|
-
debugIOS(`Launched app: ${bundleId}`);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
debugIOS(`Failed to launch app ${bundleId}: ${error}`);
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
async activateApp(bundleId) {
|
|
84
|
-
this.ensureSession();
|
|
85
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/wda/apps/activate`, {
|
|
34
|
+
__webpack_require__.r(__webpack_exports__);
|
|
35
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
36
|
+
overrideAIConfig: ()=>env_namespaceObject.overrideAIConfig,
|
|
37
|
+
IOSAgent: ()=>IOSAgent,
|
|
38
|
+
checkIOSEnvironment: ()=>checkIOSEnvironment,
|
|
39
|
+
IOSDevice: ()=>IOSDevice,
|
|
40
|
+
IOSWebDriverClient: ()=>IOSWebDriverClient,
|
|
41
|
+
agentFromWebDriverAgent: ()=>agentFromWebDriverAgent
|
|
42
|
+
});
|
|
43
|
+
const external_node_assert_namespaceObject = require("node:assert");
|
|
44
|
+
var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
|
|
45
|
+
const core_namespaceObject = require("@midscene/core");
|
|
46
|
+
const device_namespaceObject = require("@midscene/core/device");
|
|
47
|
+
const utils_namespaceObject = require("@midscene/core/utils");
|
|
48
|
+
const constants_namespaceObject = require("@midscene/shared/constants");
|
|
49
|
+
const img_namespaceObject = require("@midscene/shared/img");
|
|
50
|
+
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
51
|
+
const webdriver_namespaceObject = require("@midscene/webdriver");
|
|
52
|
+
const debugIOS = (0, logger_namespaceObject.getDebug)('webdriver:ios');
|
|
53
|
+
class IOSWebDriverClient extends webdriver_namespaceObject.WebDriverClient {
|
|
54
|
+
async launchApp(bundleId) {
|
|
55
|
+
this.ensureSession();
|
|
56
|
+
try {
|
|
57
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/apps/launch`, {
|
|
86
58
|
bundleId
|
|
87
59
|
});
|
|
60
|
+
debugIOS(`Launched app: ${bundleId}`);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
debugIOS(`Failed to launch app ${bundleId}: ${error}`);
|
|
63
|
+
throw error;
|
|
88
64
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
65
|
+
}
|
|
66
|
+
async activateApp(bundleId) {
|
|
67
|
+
this.ensureSession();
|
|
68
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/apps/activate`, {
|
|
69
|
+
bundleId
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async terminateApp(bundleId) {
|
|
73
|
+
this.ensureSession();
|
|
74
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/apps/terminate`, {
|
|
75
|
+
bundleId
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async openUrl(url) {
|
|
79
|
+
this.ensureSession();
|
|
80
|
+
try {
|
|
81
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/url`, {
|
|
82
|
+
url
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
debugIOS(`Direct URL opening failed, trying Safari fallback: ${error}`);
|
|
86
|
+
await this.launchApp('com.apple.mobilesafari');
|
|
87
|
+
await new Promise((resolve)=>setTimeout(resolve, 2000));
|
|
88
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/url`, {
|
|
89
|
+
url
|
|
93
90
|
});
|
|
94
91
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
url
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
async pressHomeButton() {
|
|
111
|
-
this.ensureSession();
|
|
112
|
-
try {
|
|
113
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/wda/pressButton`, {
|
|
114
|
-
name: 'home'
|
|
115
|
-
});
|
|
116
|
-
debugIOS('Home button pressed using hardware key');
|
|
117
|
-
} catch (error) {
|
|
118
|
-
debugIOS(`Failed to press home button: ${error}`);
|
|
119
|
-
throw new Error(`Failed to press home button: ${error}`);
|
|
120
|
-
}
|
|
92
|
+
}
|
|
93
|
+
async pressHomeButton() {
|
|
94
|
+
this.ensureSession();
|
|
95
|
+
try {
|
|
96
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/pressButton`, {
|
|
97
|
+
name: 'home'
|
|
98
|
+
});
|
|
99
|
+
debugIOS('Home button pressed using hardware key');
|
|
100
|
+
} catch (error) {
|
|
101
|
+
debugIOS(`Failed to press home button: ${error}`);
|
|
102
|
+
throw new Error(`Failed to press home button: ${error}`);
|
|
121
103
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
104
|
+
}
|
|
105
|
+
async appSwitcher() {
|
|
106
|
+
this.ensureSession();
|
|
107
|
+
try {
|
|
108
|
+
const windowSize = await this.getWindowSize();
|
|
109
|
+
debugIOS('Triggering app switcher with slow swipe up gesture');
|
|
110
|
+
const centerX = windowSize.width / 2;
|
|
111
|
+
const startY = windowSize.height - 5;
|
|
112
|
+
const endY = 0.5 * windowSize.height;
|
|
113
|
+
await this.swipe(centerX, startY, centerX, endY, 1500);
|
|
114
|
+
await new Promise((resolve)=>setTimeout(resolve, 800));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
debugIOS(`App switcher failed: ${error}`);
|
|
117
|
+
throw new Error(`Failed to trigger app switcher: ${error}`);
|
|
136
118
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
if ('Space' === key) try {
|
|
163
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
|
|
164
|
-
value: [
|
|
165
|
-
' '
|
|
166
|
-
]
|
|
167
|
-
});
|
|
168
|
-
debugIOS('Sent space character');
|
|
169
|
-
return;
|
|
170
|
-
} catch (error) {
|
|
171
|
-
debugIOS(`Space key failed: ${error}`);
|
|
172
|
-
}
|
|
173
|
-
const normalizedKey = this.normalizeKeyName(key);
|
|
174
|
-
const iosKeyMap = {
|
|
175
|
-
Tab: '\t',
|
|
176
|
-
ArrowUp: '\uE013',
|
|
177
|
-
ArrowDown: '\uE015',
|
|
178
|
-
ArrowLeft: '\uE012',
|
|
179
|
-
ArrowRight: '\uE014',
|
|
180
|
-
Home: '\uE011',
|
|
181
|
-
End: '\uE010'
|
|
182
|
-
};
|
|
183
|
-
if (iosKeyMap[normalizedKey]) try {
|
|
184
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
|
|
185
|
-
value: [
|
|
186
|
-
iosKeyMap[normalizedKey]
|
|
187
|
-
]
|
|
188
|
-
});
|
|
189
|
-
debugIOS(`Sent WebDriver key code for: ${key}`);
|
|
190
|
-
return;
|
|
191
|
-
} catch (error) {
|
|
192
|
-
debugIOS(`WebDriver key failed for "${key}": ${error}`);
|
|
193
|
-
}
|
|
194
|
-
if (1 === key.length) try {
|
|
195
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
|
|
196
|
-
value: [
|
|
197
|
-
key
|
|
198
|
-
]
|
|
199
|
-
});
|
|
200
|
-
debugIOS(`Sent single character: "${key}"`);
|
|
201
|
-
return;
|
|
202
|
-
} catch (error) {
|
|
203
|
-
debugIOS(`Failed to send character "${key}": ${error}`);
|
|
204
|
-
}
|
|
205
|
-
debugIOS(`Warning: Key "${key}" is not supported on iOS platform`);
|
|
206
|
-
throw new Error(`Key "${key}" is not supported on iOS platform`);
|
|
119
|
+
}
|
|
120
|
+
async pressKey(key) {
|
|
121
|
+
this.ensureSession();
|
|
122
|
+
debugIOS(`Attempting to press key: ${key}`);
|
|
123
|
+
if ('Enter' === key || 'Return' === key || 'return' === key) {
|
|
124
|
+
debugIOS('Handling Enter/Return key for iOS');
|
|
125
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
|
|
126
|
+
value: [
|
|
127
|
+
'\n'
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
debugIOS('Sent newline character for Enter key');
|
|
131
|
+
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if ('Backspace' === key || 'Delete' === key) try {
|
|
135
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
|
|
136
|
+
value: [
|
|
137
|
+
'\b'
|
|
138
|
+
]
|
|
139
|
+
});
|
|
140
|
+
debugIOS('Sent backspace character');
|
|
141
|
+
return;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
debugIOS(`Backspace failed: ${error}`);
|
|
207
144
|
}
|
|
208
|
-
|
|
209
|
-
this.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
145
|
+
if ('Space' === key) try {
|
|
146
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
|
|
147
|
+
value: [
|
|
148
|
+
' '
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
debugIOS('Sent space character');
|
|
152
|
+
return;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
debugIOS(`Space key failed: ${error}`);
|
|
155
|
+
}
|
|
156
|
+
const normalizedKey = this.normalizeKeyName(key);
|
|
157
|
+
const iosKeyMap = {
|
|
158
|
+
Tab: '\t',
|
|
159
|
+
ArrowUp: '\uE013',
|
|
160
|
+
ArrowDown: '\uE015',
|
|
161
|
+
ArrowLeft: '\uE012',
|
|
162
|
+
ArrowRight: '\uE014',
|
|
163
|
+
Home: '\uE011',
|
|
164
|
+
End: '\uE010'
|
|
165
|
+
};
|
|
166
|
+
if (iosKeyMap[normalizedKey]) try {
|
|
167
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
|
|
168
|
+
value: [
|
|
169
|
+
iosKeyMap[normalizedKey]
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
debugIOS(`Sent WebDriver key code for: ${key}`);
|
|
173
|
+
return;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
debugIOS(`WebDriver key failed for "${key}": ${error}`);
|
|
225
176
|
}
|
|
226
|
-
|
|
227
|
-
this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
177
|
+
if (1 === key.length) try {
|
|
178
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
|
|
179
|
+
value: [
|
|
180
|
+
key
|
|
181
|
+
]
|
|
182
|
+
});
|
|
183
|
+
debugIOS(`Sent single character: "${key}"`);
|
|
184
|
+
return;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
debugIOS(`Failed to send character "${key}": ${error}`);
|
|
236
187
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
debugIOS(`
|
|
248
|
-
return
|
|
188
|
+
debugIOS(`Warning: Key "${key}" is not supported on iOS platform`);
|
|
189
|
+
throw new Error(`Key "${key}" is not supported on iOS platform`);
|
|
190
|
+
}
|
|
191
|
+
async getActiveElement() {
|
|
192
|
+
this.ensureSession();
|
|
193
|
+
debugIOS('Getting active element');
|
|
194
|
+
try {
|
|
195
|
+
const response = await this.makeRequest('GET', `/session/${this.sessionId}/element/active`);
|
|
196
|
+
const elementId = response.value?.ELEMENT || response.value?.['element-6066-11e4-a52e-4f735466cecf'] || response.ELEMENT || response['element-6066-11e4-a52e-4f735466cecf'];
|
|
197
|
+
if (elementId) {
|
|
198
|
+
debugIOS(`Got active element ID: ${elementId}`);
|
|
199
|
+
return elementId;
|
|
249
200
|
}
|
|
201
|
+
debugIOS('No active element found');
|
|
202
|
+
return null;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
debugIOS(`Failed to get active element: ${error}`);
|
|
205
|
+
return null;
|
|
250
206
|
}
|
|
251
|
-
|
|
252
|
-
|
|
207
|
+
}
|
|
208
|
+
async clearElement(elementId) {
|
|
209
|
+
this.ensureSession();
|
|
210
|
+
debugIOS(`Clearing element: ${elementId}`);
|
|
211
|
+
try {
|
|
212
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/element/${elementId}/clear`);
|
|
213
|
+
debugIOS('Element cleared successfully');
|
|
214
|
+
} catch (error) {
|
|
215
|
+
debugIOS(`Failed to clear element: ${error}`);
|
|
216
|
+
throw new Error(`Failed to clear element: ${error}`);
|
|
253
217
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
]
|
|
261
|
-
});
|
|
262
|
-
debugIOS('Dismissed keyboard using WDA API');
|
|
263
|
-
return true;
|
|
264
|
-
} catch (error) {
|
|
265
|
-
debugIOS(`Failed to dismiss keyboard: ${error}`);
|
|
218
|
+
}
|
|
219
|
+
async clearActiveElement() {
|
|
220
|
+
try {
|
|
221
|
+
const elementId = await this.getActiveElement();
|
|
222
|
+
if (!elementId) {
|
|
223
|
+
debugIOS('No active element to clear');
|
|
266
224
|
return false;
|
|
267
225
|
}
|
|
226
|
+
await this.clearElement(elementId);
|
|
227
|
+
return true;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
debugIOS(`Failed to clear active element: ${error}`);
|
|
230
|
+
return false;
|
|
268
231
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
throw new Error(`Failed to type text: ${error}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
async tap(x, y) {
|
|
283
|
-
this.ensureSession();
|
|
284
|
-
try {
|
|
285
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/wda/tap`, {
|
|
286
|
-
x,
|
|
287
|
-
y
|
|
288
|
-
});
|
|
289
|
-
debugIOS(`Tapped at coordinates (${x}, ${y})`);
|
|
290
|
-
} catch (error) {
|
|
291
|
-
debugIOS(`New tap endpoint failed, trying legacy endpoint: ${error}`);
|
|
292
|
-
try {
|
|
293
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/wda/tap/0`, {
|
|
294
|
-
x,
|
|
295
|
-
y
|
|
296
|
-
});
|
|
297
|
-
debugIOS(`Tapped at coordinates (${x}, ${y}) using legacy endpoint`);
|
|
298
|
-
} catch (fallbackError) {
|
|
299
|
-
debugIOS(`Failed to tap at (${x}, ${y}): ${fallbackError}`);
|
|
300
|
-
throw new Error(`Failed to tap at coordinates: ${fallbackError}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
async swipe(fromX, fromY, toX, toY, duration = 500) {
|
|
305
|
-
this.ensureSession();
|
|
306
|
-
const actions = {
|
|
307
|
-
actions: [
|
|
308
|
-
{
|
|
309
|
-
type: 'pointer',
|
|
310
|
-
id: 'finger1',
|
|
311
|
-
parameters: {
|
|
312
|
-
pointerType: 'touch'
|
|
313
|
-
},
|
|
314
|
-
actions: [
|
|
315
|
-
{
|
|
316
|
-
type: 'pointerMove',
|
|
317
|
-
duration: 0,
|
|
318
|
-
x: fromX,
|
|
319
|
-
y: fromY
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
type: 'pointerDown',
|
|
323
|
-
button: 0
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
type: 'pause',
|
|
327
|
-
duration: 100
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
type: 'pointerMove',
|
|
331
|
-
duration,
|
|
332
|
-
x: toX,
|
|
333
|
-
y: toY
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
type: 'pointerUp',
|
|
337
|
-
button: 0
|
|
338
|
-
}
|
|
339
|
-
]
|
|
340
|
-
}
|
|
232
|
+
}
|
|
233
|
+
normalizeKeyName(key) {
|
|
234
|
+
return key.charAt(0).toUpperCase() + key.slice(1).toLowerCase();
|
|
235
|
+
}
|
|
236
|
+
async dismissKeyboard(keyNames) {
|
|
237
|
+
this.ensureSession();
|
|
238
|
+
try {
|
|
239
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keyboard/dismiss`, {
|
|
240
|
+
keyNames: keyNames || [
|
|
241
|
+
'done'
|
|
341
242
|
]
|
|
342
|
-
};
|
|
343
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/actions`, actions);
|
|
344
|
-
debugIOS(`Swiped using W3C Actions from (${fromX}, ${fromY}) to (${toX}, ${toY}) in ${duration}ms`);
|
|
345
|
-
}
|
|
346
|
-
async longPress(x, y, duration = 1000) {
|
|
347
|
-
this.ensureSession();
|
|
348
|
-
await this.makeRequest('POST', `/session/${this.sessionId}/wda/touchAndHold`, {
|
|
349
|
-
x,
|
|
350
|
-
y,
|
|
351
|
-
duration: duration / 1000
|
|
352
243
|
});
|
|
353
|
-
debugIOS(
|
|
244
|
+
debugIOS('Dismissed keyboard using WDA API');
|
|
245
|
+
return true;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
debugIOS(`Failed to dismiss keyboard: ${error}`);
|
|
248
|
+
return false;
|
|
354
249
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
250
|
+
}
|
|
251
|
+
async typeText(text) {
|
|
252
|
+
this.ensureSession();
|
|
253
|
+
try {
|
|
254
|
+
const cleanText = text.trim();
|
|
255
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
|
|
256
|
+
value: cleanText.split('')
|
|
360
257
|
});
|
|
361
|
-
debugIOS(`
|
|
258
|
+
debugIOS(`Typed text: "${text}"`);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
debugIOS(`Failed to type text "${text}": ${error}`);
|
|
261
|
+
throw new Error(`Failed to type text: ${error}`);
|
|
362
262
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
263
|
+
}
|
|
264
|
+
async tap(x, y) {
|
|
265
|
+
this.ensureSession();
|
|
266
|
+
try {
|
|
267
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/tap`, {
|
|
366
268
|
x,
|
|
367
|
-
y
|
|
368
|
-
numberOfTaps: 3,
|
|
369
|
-
numberOfTouches: 1
|
|
269
|
+
y
|
|
370
270
|
});
|
|
371
|
-
debugIOS(`
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
this.ensureSession();
|
|
375
|
-
try {
|
|
376
|
-
var _screenResponse_value;
|
|
377
|
-
const screenResponse = await this.makeRequest('GET', `/session/${this.sessionId}/wda/screen`);
|
|
378
|
-
if (null == screenResponse ? void 0 : null == (_screenResponse_value = screenResponse.value) ? void 0 : _screenResponse_value.scale) {
|
|
379
|
-
debugIOS(`Got screen scale from WDA screen endpoint: ${screenResponse.value.scale}`);
|
|
380
|
-
return screenResponse.value.scale;
|
|
381
|
-
}
|
|
382
|
-
} catch (error) {
|
|
383
|
-
debugIOS(`Failed to get screen scale from /wda/screen: ${error}`);
|
|
384
|
-
}
|
|
271
|
+
debugIOS(`Tapped at coordinates (${x}, ${y})`);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
debugIOS(`New tap endpoint failed, trying legacy endpoint: ${error}`);
|
|
385
274
|
try {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const screenshotHeight = screenshotImg.bitmap.height;
|
|
395
|
-
const scale = Math.max(screenshotWidth, screenshotHeight) / Math.max(windowSize.width, windowSize.height);
|
|
396
|
-
const roundedScale = Math.round(scale);
|
|
397
|
-
debugIOS(`Calculated screen scale: ${roundedScale} (screenshot: ${screenshotWidth}x${screenshotHeight}, window: ${windowSize.width}x${windowSize.height})`);
|
|
398
|
-
return roundedScale;
|
|
399
|
-
} catch (error) {
|
|
400
|
-
debugIOS(`Failed to calculate screen scale: ${error}`);
|
|
275
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/tap/0`, {
|
|
276
|
+
x,
|
|
277
|
+
y
|
|
278
|
+
});
|
|
279
|
+
debugIOS(`Tapped at coordinates (${x}, ${y}) using legacy endpoint`);
|
|
280
|
+
} catch (fallbackError) {
|
|
281
|
+
debugIOS(`Failed to tap at (${x}, ${y}): ${fallbackError}`);
|
|
282
|
+
throw new Error(`Failed to tap at coordinates: ${fallbackError}`);
|
|
401
283
|
}
|
|
402
|
-
debugIOS('No screen scale found');
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
async createSession(capabilities) {
|
|
406
|
-
const defaultCapabilities = {
|
|
407
|
-
platformName: 'iOS',
|
|
408
|
-
automationName: 'XCUITest',
|
|
409
|
-
shouldUseSingletonTestManager: false,
|
|
410
|
-
shouldUseTestManagerForVisibilityDetection: false,
|
|
411
|
-
...capabilities
|
|
412
|
-
};
|
|
413
|
-
const session = await super.createSession(defaultCapabilities);
|
|
414
|
-
await this.setupIOSSession();
|
|
415
|
-
return session;
|
|
416
284
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
285
|
+
}
|
|
286
|
+
async swipe(fromX, fromY, toX, toY, duration = 500) {
|
|
287
|
+
this.ensureSession();
|
|
288
|
+
const actions = {
|
|
289
|
+
actions: [
|
|
290
|
+
{
|
|
291
|
+
type: 'pointer',
|
|
292
|
+
id: 'finger1',
|
|
293
|
+
parameters: {
|
|
294
|
+
pointerType: 'touch'
|
|
295
|
+
},
|
|
296
|
+
actions: [
|
|
297
|
+
{
|
|
298
|
+
type: 'pointerMove',
|
|
299
|
+
duration: 0,
|
|
300
|
+
x: fromX,
|
|
301
|
+
y: fromY
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
type: 'pointerDown',
|
|
305
|
+
button: 0
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
type: 'pause',
|
|
309
|
+
duration: 100
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
type: 'pointerMove',
|
|
313
|
+
duration,
|
|
314
|
+
x: toX,
|
|
315
|
+
y: toY
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
type: 'pointerUp',
|
|
319
|
+
button: 0
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
};
|
|
325
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/actions`, actions);
|
|
326
|
+
debugIOS(`Swiped using W3C Actions from (${fromX}, ${fromY}) to (${toX}, ${toY}) in ${duration}ms`);
|
|
327
|
+
}
|
|
328
|
+
async longPress(x, y, duration = 1000) {
|
|
329
|
+
this.ensureSession();
|
|
330
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/touchAndHold`, {
|
|
331
|
+
x,
|
|
332
|
+
y,
|
|
333
|
+
duration: duration / 1000
|
|
334
|
+
});
|
|
335
|
+
debugIOS(`Long pressed at coordinates (${x}, ${y}) for ${duration}ms`);
|
|
336
|
+
}
|
|
337
|
+
async doubleTap(x, y) {
|
|
338
|
+
this.ensureSession();
|
|
339
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/doubleTap`, {
|
|
340
|
+
x,
|
|
341
|
+
y
|
|
342
|
+
});
|
|
343
|
+
debugIOS(`Double tapped at coordinates (${x}, ${y})`);
|
|
344
|
+
}
|
|
345
|
+
async tripleTap(x, y) {
|
|
346
|
+
this.ensureSession();
|
|
347
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/wda/tapWithNumberOfTaps`, {
|
|
348
|
+
x,
|
|
349
|
+
y,
|
|
350
|
+
numberOfTaps: 3,
|
|
351
|
+
numberOfTouches: 1
|
|
352
|
+
});
|
|
353
|
+
debugIOS(`Triple tapped at coordinates (${x}, ${y})`);
|
|
354
|
+
}
|
|
355
|
+
async getScreenScale() {
|
|
356
|
+
this.ensureSession();
|
|
357
|
+
try {
|
|
358
|
+
const screenResponse = await this.makeRequest('GET', `/session/${this.sessionId}/wda/screen`);
|
|
359
|
+
if (screenResponse?.value?.scale) {
|
|
360
|
+
debugIOS(`Got screen scale from WDA screen endpoint: ${screenResponse.value.scale}`);
|
|
361
|
+
return screenResponse.value.scale;
|
|
427
362
|
}
|
|
363
|
+
} catch (error) {
|
|
364
|
+
debugIOS(`Failed to get screen scale from /wda/screen: ${error}`);
|
|
428
365
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
366
|
+
try {
|
|
367
|
+
debugIOS('Calculating screen scale from screenshot and window size');
|
|
368
|
+
const [screenshotBase64, windowSize] = await Promise.all([
|
|
369
|
+
this.takeScreenshot(),
|
|
370
|
+
this.getWindowSize()
|
|
371
|
+
]);
|
|
372
|
+
const { jimpFromBase64 } = await import("@midscene/shared/img");
|
|
373
|
+
const screenshotImg = await jimpFromBase64(screenshotBase64);
|
|
374
|
+
const screenshotWidth = screenshotImg.bitmap.width;
|
|
375
|
+
const screenshotHeight = screenshotImg.bitmap.height;
|
|
376
|
+
const scale = Math.max(screenshotWidth, screenshotHeight) / Math.max(windowSize.width, windowSize.height);
|
|
377
|
+
const roundedScale = Math.round(scale);
|
|
378
|
+
debugIOS(`Calculated screen scale: ${roundedScale} (screenshot: ${screenshotWidth}x${screenshotHeight}, window: ${windowSize.width}x${windowSize.height})`);
|
|
379
|
+
return roundedScale;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
debugIOS(`Failed to calculate screen scale: ${error}`);
|
|
432
382
|
}
|
|
383
|
+
debugIOS('No screen scale found');
|
|
384
|
+
return null;
|
|
433
385
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
386
|
+
async createSession(capabilities) {
|
|
387
|
+
const defaultCapabilities = {
|
|
388
|
+
platformName: 'iOS',
|
|
389
|
+
automationName: 'XCUITest',
|
|
390
|
+
shouldUseSingletonTestManager: false,
|
|
391
|
+
shouldUseTestManagerForVisibilityDetection: false,
|
|
392
|
+
...capabilities
|
|
393
|
+
};
|
|
394
|
+
const session = await super.createSession(defaultCapabilities);
|
|
395
|
+
await this.setupIOSSession();
|
|
396
|
+
return session;
|
|
397
|
+
}
|
|
398
|
+
async setupIOSSession() {
|
|
399
|
+
if (!this.sessionId) return;
|
|
400
|
+
try {
|
|
401
|
+
await this.makeRequest('POST', `/session/${this.sessionId}/appium/settings`, {
|
|
402
|
+
snapshotMaxDepth: 50,
|
|
403
|
+
elementResponseAttributes: 'type,label,name,value,rect,enabled,visible'
|
|
404
|
+
});
|
|
405
|
+
debugIOS('iOS session configuration applied');
|
|
406
|
+
} catch (error) {
|
|
407
|
+
debugIOS(`Failed to apply iOS session configuration: ${error}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async executeRequest(method, endpoint, data) {
|
|
411
|
+
this.ensureSession();
|
|
412
|
+
return this.makeRequest(method, endpoint, data);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function _define_property(obj, key, value) {
|
|
416
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
417
|
+
value: value,
|
|
418
|
+
enumerable: true,
|
|
419
|
+
configurable: true,
|
|
420
|
+
writable: true
|
|
421
|
+
});
|
|
422
|
+
else obj[key] = value;
|
|
423
|
+
return obj;
|
|
424
|
+
}
|
|
425
|
+
const debugDevice = (0, logger_namespaceObject.getDebug)('ios:device');
|
|
426
|
+
const WDA_HTTP_METHODS = [
|
|
427
|
+
'GET',
|
|
428
|
+
'POST',
|
|
429
|
+
'DELETE',
|
|
430
|
+
'PUT'
|
|
431
|
+
];
|
|
432
|
+
class IOSDevice {
|
|
433
|
+
actionSpace() {
|
|
434
|
+
const defaultActions = [
|
|
435
|
+
(0, device_namespaceObject.defineActionTap)(async (param)=>{
|
|
436
|
+
const element = param.locate;
|
|
437
|
+
external_node_assert_default()(element, 'Element not found, cannot tap');
|
|
438
|
+
await this.mouseClick(element.center[0], element.center[1]);
|
|
439
|
+
}),
|
|
440
|
+
(0, device_namespaceObject.defineActionDoubleClick)(async (param)=>{
|
|
441
|
+
const element = param.locate;
|
|
442
|
+
external_node_assert_default()(element, 'Element not found, cannot double click');
|
|
443
|
+
await this.doubleTap(element.center[0], element.center[1]);
|
|
444
|
+
}),
|
|
445
|
+
(0, device_namespaceObject.defineAction)({
|
|
446
|
+
name: 'Input',
|
|
447
|
+
description: 'Input text into the input field',
|
|
448
|
+
interfaceAlias: 'aiInput',
|
|
449
|
+
paramSchema: core_namespaceObject.z.object({
|
|
450
|
+
value: core_namespaceObject.z.string().describe('The text to input. Provide the final content for replace/append modes, or an empty string when using clear mode to remove existing text.'),
|
|
451
|
+
autoDismissKeyboard: core_namespaceObject.z.boolean().optional().describe('Whether to dismiss the keyboard after input. Defaults to true if not specified. Set to false to keep the keyboard visible after input.'),
|
|
452
|
+
mode: core_namespaceObject.z["enum"]([
|
|
453
|
+
'replace',
|
|
454
|
+
'clear',
|
|
455
|
+
'append'
|
|
456
|
+
]).default('replace').optional().describe('Input mode: "replace" (default) - clear the field and input the value; "append" - append the value to existing content; "clear" - clear the field without inputting new text.'),
|
|
457
|
+
locate: (0, core_namespaceObject.getMidsceneLocationSchema)().describe('The input field to be filled').optional()
|
|
491
458
|
}),
|
|
492
|
-
|
|
459
|
+
call: async (param)=>{
|
|
493
460
|
const element = param.locate;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
top: element.center[1]
|
|
497
|
-
} : void 0;
|
|
498
|
-
const scrollToEventName = null == param ? void 0 : param.scrollType;
|
|
499
|
-
if ('scrollToTop' === scrollToEventName) await this.scrollUntilTop(startingPoint);
|
|
500
|
-
else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottom(startingPoint);
|
|
501
|
-
else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRight(startingPoint);
|
|
502
|
-
else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeft(startingPoint);
|
|
503
|
-
else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
|
|
504
|
-
else {
|
|
505
|
-
if ((null == param ? void 0 : param.direction) !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUp(param.distance || void 0, startingPoint);
|
|
506
|
-
else if ('left' === param.direction) await this.scrollLeft(param.distance || void 0, startingPoint);
|
|
507
|
-
else if ('right' === param.direction) await this.scrollRight(param.distance || void 0, startingPoint);
|
|
508
|
-
else throw new Error(`Unknown scroll direction: ${param.direction}`);
|
|
509
|
-
else await this.scrollDown((null == param ? void 0 : param.distance) || void 0, startingPoint);
|
|
510
|
-
await (0, utils_namespaceObject.sleep)(500);
|
|
511
|
-
}
|
|
512
|
-
}),
|
|
513
|
-
(0, device_namespaceObject.defineActionDragAndDrop)(async (param)=>{
|
|
514
|
-
const from = param.from;
|
|
515
|
-
const to = param.to;
|
|
516
|
-
external_node_assert_default()(from, 'missing "from" param for drag and drop');
|
|
517
|
-
external_node_assert_default()(to, 'missing "to" param for drag and drop');
|
|
518
|
-
await this.swipe(from.center[0], from.center[1], to.center[0], to.center[1]);
|
|
519
|
-
}),
|
|
520
|
-
(0, device_namespaceObject.defineActionKeyboardPress)(async (param)=>{
|
|
521
|
-
await this.pressKey(param.keyName);
|
|
522
|
-
}),
|
|
523
|
-
(0, device_namespaceObject.defineAction)({
|
|
524
|
-
name: 'IOSLongPress',
|
|
525
|
-
description: 'Trigger a long press on the screen at specified coordinates on iOS devices',
|
|
526
|
-
paramSchema: core_namespaceObject.z.object({
|
|
527
|
-
duration: core_namespaceObject.z.number().optional().describe('The duration of the long press in milliseconds'),
|
|
528
|
-
locate: (0, core_namespaceObject.getMidsceneLocationSchema)().describe('The element to be long pressed')
|
|
529
|
-
}),
|
|
530
|
-
call: async (param)=>{
|
|
531
|
-
const element = param.locate;
|
|
532
|
-
external_node_assert_default()(element, 'IOSLongPress requires an element to be located');
|
|
533
|
-
const [x, y] = element.center;
|
|
534
|
-
await this.longPress(x, y, null == param ? void 0 : param.duration);
|
|
461
|
+
if (element) {
|
|
462
|
+
if ('append' !== param.mode) await this.clearInput(element);
|
|
535
463
|
}
|
|
464
|
+
if ('clear' === param.mode) return;
|
|
465
|
+
if (!param || !param.value) return;
|
|
466
|
+
const autoDismissKeyboard = param.autoDismissKeyboard ?? this.options?.autoDismissKeyboard;
|
|
467
|
+
await this.typeText(param.value, {
|
|
468
|
+
autoDismissKeyboard
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}),
|
|
472
|
+
(0, device_namespaceObject.defineActionScroll)(async (param)=>{
|
|
473
|
+
const element = param.locate;
|
|
474
|
+
const startingPoint = element ? {
|
|
475
|
+
left: element.center[0],
|
|
476
|
+
top: element.center[1]
|
|
477
|
+
} : void 0;
|
|
478
|
+
const scrollToEventName = param?.scrollType;
|
|
479
|
+
if ('scrollToTop' === scrollToEventName) await this.scrollUntilTop(startingPoint);
|
|
480
|
+
else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottom(startingPoint);
|
|
481
|
+
else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRight(startingPoint);
|
|
482
|
+
else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeft(startingPoint);
|
|
483
|
+
else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
|
|
484
|
+
else {
|
|
485
|
+
if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUp(param.distance || void 0, startingPoint);
|
|
486
|
+
else if ('left' === param.direction) await this.scrollLeft(param.distance || void 0, startingPoint);
|
|
487
|
+
else if ('right' === param.direction) await this.scrollRight(param.distance || void 0, startingPoint);
|
|
488
|
+
else throw new Error(`Unknown scroll direction: ${param.direction}`);
|
|
489
|
+
else await this.scrollDown(param?.distance || void 0, startingPoint);
|
|
490
|
+
await (0, utils_namespaceObject.sleep)(500);
|
|
491
|
+
}
|
|
492
|
+
}),
|
|
493
|
+
(0, device_namespaceObject.defineActionDragAndDrop)(async (param)=>{
|
|
494
|
+
const from = param.from;
|
|
495
|
+
const to = param.to;
|
|
496
|
+
external_node_assert_default()(from, 'missing "from" param for drag and drop');
|
|
497
|
+
external_node_assert_default()(to, 'missing "to" param for drag and drop');
|
|
498
|
+
await this.swipe(from.center[0], from.center[1], to.center[0], to.center[1]);
|
|
499
|
+
}),
|
|
500
|
+
(0, device_namespaceObject.defineActionKeyboardPress)(async (param)=>{
|
|
501
|
+
await this.pressKey(param.keyName);
|
|
502
|
+
}),
|
|
503
|
+
(0, device_namespaceObject.defineAction)({
|
|
504
|
+
name: 'IOSLongPress',
|
|
505
|
+
description: 'Trigger a long press on the screen at specified coordinates on iOS devices',
|
|
506
|
+
paramSchema: core_namespaceObject.z.object({
|
|
507
|
+
duration: core_namespaceObject.z.number().optional().describe('The duration of the long press in milliseconds'),
|
|
508
|
+
locate: (0, core_namespaceObject.getMidsceneLocationSchema)().describe('The element to be long pressed')
|
|
536
509
|
}),
|
|
537
|
-
|
|
510
|
+
call: async (param)=>{
|
|
538
511
|
const element = param.locate;
|
|
539
|
-
external_node_assert_default()(element, '
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
];
|
|
543
|
-
const platformSpecificActions = Object.values(createPlatformActions(this));
|
|
544
|
-
const customActions = this.customActions || [];
|
|
545
|
-
return [
|
|
546
|
-
...defaultActions,
|
|
547
|
-
...platformSpecificActions,
|
|
548
|
-
...customActions
|
|
549
|
-
];
|
|
550
|
-
}
|
|
551
|
-
describe() {
|
|
552
|
-
return this.description || `Device ID: ${this.deviceId}`;
|
|
553
|
-
}
|
|
554
|
-
async getConnectedDeviceInfo() {
|
|
555
|
-
return await this.wdaBackend.getDeviceInfo();
|
|
556
|
-
}
|
|
557
|
-
async connect() {
|
|
558
|
-
external_node_assert_default()(!this.destroyed, `IOSDevice ${this.deviceId} has been destroyed and cannot execute commands`);
|
|
559
|
-
debugDevice(`Connecting to iOS device: ${this.deviceId}`);
|
|
560
|
-
try {
|
|
561
|
-
await this.wdaManager.start();
|
|
562
|
-
await this.wdaBackend.createSession();
|
|
563
|
-
const deviceInfo = await this.wdaBackend.getDeviceInfo();
|
|
564
|
-
if (null == deviceInfo ? void 0 : deviceInfo.udid) {
|
|
565
|
-
this.deviceId = deviceInfo.udid;
|
|
566
|
-
debugDevice(`Updated device ID to real UDID: ${this.deviceId}`);
|
|
512
|
+
external_node_assert_default()(element, 'IOSLongPress requires an element to be located');
|
|
513
|
+
const [x, y] = element.center;
|
|
514
|
+
await this.longPress(x, y, param?.duration);
|
|
567
515
|
}
|
|
568
|
-
|
|
569
|
-
|
|
516
|
+
}),
|
|
517
|
+
(0, device_namespaceObject.defineActionClearInput)(async (param)=>{
|
|
518
|
+
const element = param.locate;
|
|
519
|
+
external_node_assert_default()(element, 'Element not found, cannot clear input');
|
|
520
|
+
await this.clearInput(element);
|
|
521
|
+
})
|
|
522
|
+
];
|
|
523
|
+
const platformSpecificActions = Object.values(createPlatformActions(this));
|
|
524
|
+
const customActions = this.customActions || [];
|
|
525
|
+
return [
|
|
526
|
+
...defaultActions,
|
|
527
|
+
...platformSpecificActions,
|
|
528
|
+
...customActions
|
|
529
|
+
];
|
|
530
|
+
}
|
|
531
|
+
describe() {
|
|
532
|
+
return this.description || `Device ID: ${this.deviceId}`;
|
|
533
|
+
}
|
|
534
|
+
async getConnectedDeviceInfo() {
|
|
535
|
+
return await this.wdaBackend.getDeviceInfo();
|
|
536
|
+
}
|
|
537
|
+
async connect() {
|
|
538
|
+
external_node_assert_default()(!this.destroyed, `IOSDevice ${this.deviceId} has been destroyed and cannot execute commands`);
|
|
539
|
+
debugDevice(`Connecting to iOS device: ${this.deviceId}`);
|
|
540
|
+
try {
|
|
541
|
+
await this.wdaManager.start();
|
|
542
|
+
await this.wdaBackend.createSession();
|
|
543
|
+
const deviceInfo = await this.wdaBackend.getDeviceInfo();
|
|
544
|
+
if (deviceInfo?.udid) {
|
|
545
|
+
this.deviceId = deviceInfo.udid;
|
|
546
|
+
debugDevice(`Updated device ID to real UDID: ${this.deviceId}`);
|
|
547
|
+
}
|
|
548
|
+
const size = await this.getScreenSize();
|
|
549
|
+
this.description = `
|
|
570
550
|
UDID: ${this.deviceId}${deviceInfo ? `
|
|
571
551
|
Name: ${deviceInfo.name}
|
|
572
552
|
Model: ${deviceInfo.model}` : ''}
|
|
573
553
|
Type: WebDriverAgent
|
|
574
554
|
ScreenSize: ${size.width}x${size.height} (DPR: ${size.scale})
|
|
575
555
|
`;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
async launch(uri) {
|
|
583
|
-
this.uri = uri;
|
|
584
|
-
try {
|
|
585
|
-
debugDevice(`Launching app: ${uri}`);
|
|
586
|
-
if (uri.startsWith('http://') || uri.startsWith('https://') || uri.includes('://')) await this.openUrl(uri);
|
|
587
|
-
else await this.wdaBackend.launchApp(uri);
|
|
588
|
-
debugDevice(`Successfully launched: ${uri}`);
|
|
589
|
-
} catch (error) {
|
|
590
|
-
debugDevice(`Error launching ${uri}: ${error}`);
|
|
591
|
-
throw new Error(`Failed to launch ${uri}: ${error.message}`);
|
|
592
|
-
}
|
|
593
|
-
return this;
|
|
594
|
-
}
|
|
595
|
-
async getElementsInfo() {
|
|
596
|
-
return [];
|
|
597
|
-
}
|
|
598
|
-
async getElementsNodeTree() {
|
|
599
|
-
return {
|
|
600
|
-
node: null,
|
|
601
|
-
children: []
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
async initializeDevicePixelRatio() {
|
|
605
|
-
if (this.devicePixelRatioInitialized) return;
|
|
606
|
-
const apiScale = await this.wdaBackend.getScreenScale();
|
|
607
|
-
external_node_assert_default()(apiScale && apiScale > 0, 'Failed to get device pixel ratio from WebDriverAgent API');
|
|
608
|
-
debugDevice(`Got screen scale from WebDriverAgent API: ${apiScale}`);
|
|
609
|
-
this.devicePixelRatio = apiScale;
|
|
610
|
-
this.devicePixelRatioInitialized = true;
|
|
611
|
-
}
|
|
612
|
-
async getScreenSize() {
|
|
613
|
-
await this.initializeDevicePixelRatio();
|
|
614
|
-
const windowSize = await this.wdaBackend.getWindowSize();
|
|
615
|
-
return {
|
|
616
|
-
width: windowSize.width,
|
|
617
|
-
height: windowSize.height,
|
|
618
|
-
scale: this.devicePixelRatio
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
async size() {
|
|
622
|
-
const screenSize = await this.getScreenSize();
|
|
623
|
-
return {
|
|
624
|
-
width: screenSize.width,
|
|
625
|
-
height: screenSize.height,
|
|
626
|
-
dpr: screenSize.scale
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
async screenshotBase64() {
|
|
630
|
-
debugDevice('Taking screenshot via WDA');
|
|
631
|
-
try {
|
|
632
|
-
const base64Data = await this.wdaBackend.takeScreenshot();
|
|
633
|
-
const result = (0, img_namespaceObject.createImgBase64ByFormat)('png', base64Data);
|
|
634
|
-
debugDevice('Screenshot taken successfully');
|
|
635
|
-
return result;
|
|
636
|
-
} catch (error) {
|
|
637
|
-
debugDevice(`Screenshot failed: ${error}`);
|
|
638
|
-
throw new Error(`Failed to take screenshot: ${error}`);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
async clearInput(element) {
|
|
642
|
-
if (!element) return;
|
|
643
|
-
await this.tap(element.center[0], element.center[1]);
|
|
644
|
-
await (0, utils_namespaceObject.sleep)(100);
|
|
645
|
-
debugDevice('Attempting to clear input with WebDriver Clear API');
|
|
646
|
-
const cleared = await this.wdaBackend.clearActiveElement();
|
|
647
|
-
cleared ? debugDevice('Successfully cleared input with WebDriver Clear API') : debugDevice('WebDriver Clear API returned false (no active element or clear failed)');
|
|
556
|
+
debugDevice('iOS device connected successfully', this.description);
|
|
557
|
+
} catch (e) {
|
|
558
|
+
debugDevice(`Failed to connect to iOS device: ${e}`);
|
|
559
|
+
throw new Error(`Unable to connect to iOS device ${this.deviceId}: ${e}`);
|
|
648
560
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
debugDevice(`
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
await this.wdaBackend.doubleTap(Math.round(x), Math.round(y));
|
|
661
|
-
}
|
|
662
|
-
async tripleTap(x, y) {
|
|
663
|
-
await this.wdaBackend.tripleTap(Math.round(x), Math.round(y));
|
|
664
|
-
}
|
|
665
|
-
async longPress(x, y, duration = 1000) {
|
|
666
|
-
await this.wdaBackend.longPress(Math.round(x), Math.round(y), duration);
|
|
667
|
-
}
|
|
668
|
-
async swipe(fromX, fromY, toX, toY, duration = 500) {
|
|
669
|
-
await this.wdaBackend.swipe(Math.round(fromX), Math.round(fromY), Math.round(toX), Math.round(toY), duration);
|
|
670
|
-
}
|
|
671
|
-
async typeText(text, options) {
|
|
672
|
-
var _this_options;
|
|
673
|
-
if (!text) return;
|
|
674
|
-
const shouldAutoDismissKeyboard = (null == options ? void 0 : options.autoDismissKeyboard) ?? (null == (_this_options = this.options) ? void 0 : _this_options.autoDismissKeyboard) ?? true;
|
|
675
|
-
debugDevice(`Typing text: "${text}"`);
|
|
676
|
-
try {
|
|
677
|
-
await (0, utils_namespaceObject.sleep)(200);
|
|
678
|
-
await this.wdaBackend.typeText(text);
|
|
679
|
-
await (0, utils_namespaceObject.sleep)(300);
|
|
680
|
-
} catch (error) {
|
|
681
|
-
debugDevice(`Failed to type text with WDA: ${error}`);
|
|
682
|
-
throw error;
|
|
683
|
-
}
|
|
684
|
-
if (shouldAutoDismissKeyboard) await this.hideKeyboard();
|
|
685
|
-
}
|
|
686
|
-
async pressKey(key) {
|
|
687
|
-
await this.wdaBackend.pressKey(key);
|
|
688
|
-
}
|
|
689
|
-
async scrollUp(distance, startPoint) {
|
|
690
|
-
const { width, height } = await this.size();
|
|
691
|
-
const start = startPoint ? {
|
|
692
|
-
x: Math.round(startPoint.left),
|
|
693
|
-
y: Math.round(startPoint.top)
|
|
694
|
-
} : {
|
|
695
|
-
x: Math.round(width / 2),
|
|
696
|
-
y: Math.round(height / 2)
|
|
697
|
-
};
|
|
698
|
-
const scrollDistance = Math.round(distance || height / 3);
|
|
699
|
-
await this.swipe(start.x, start.y, start.x, start.y + scrollDistance);
|
|
700
|
-
}
|
|
701
|
-
async scrollDown(distance, startPoint) {
|
|
702
|
-
const { width, height } = await this.size();
|
|
703
|
-
const start = startPoint ? {
|
|
704
|
-
x: Math.round(startPoint.left),
|
|
705
|
-
y: Math.round(startPoint.top)
|
|
706
|
-
} : {
|
|
707
|
-
x: Math.round(width / 2),
|
|
708
|
-
y: Math.round(height / 2)
|
|
709
|
-
};
|
|
710
|
-
const scrollDistance = Math.round(distance || height / 3);
|
|
711
|
-
await this.swipe(start.x, start.y, start.x, start.y - scrollDistance);
|
|
712
|
-
}
|
|
713
|
-
async scrollLeft(distance, startPoint) {
|
|
714
|
-
const { width, height } = await this.size();
|
|
715
|
-
const start = startPoint ? {
|
|
716
|
-
x: Math.round(startPoint.left),
|
|
717
|
-
y: Math.round(startPoint.top)
|
|
718
|
-
} : {
|
|
719
|
-
x: Math.round(width / 2),
|
|
720
|
-
y: Math.round(height / 2)
|
|
721
|
-
};
|
|
722
|
-
const scrollDistance = Math.round(distance || 0.7 * width);
|
|
723
|
-
await this.swipe(start.x, start.y, start.x + scrollDistance, start.y);
|
|
561
|
+
}
|
|
562
|
+
async launch(uri) {
|
|
563
|
+
this.uri = uri;
|
|
564
|
+
try {
|
|
565
|
+
debugDevice(`Launching app: ${uri}`);
|
|
566
|
+
if (uri.startsWith('http://') || uri.startsWith('https://') || uri.includes('://')) await this.openUrl(uri);
|
|
567
|
+
else await this.wdaBackend.launchApp(uri);
|
|
568
|
+
debugDevice(`Successfully launched: ${uri}`);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
debugDevice(`Error launching ${uri}: ${error}`);
|
|
571
|
+
throw new Error(`Failed to launch ${uri}: ${error.message}`);
|
|
724
572
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
573
|
+
return this;
|
|
574
|
+
}
|
|
575
|
+
async getElementsInfo() {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
async getElementsNodeTree() {
|
|
579
|
+
return {
|
|
580
|
+
node: null,
|
|
581
|
+
children: []
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
async initializeDevicePixelRatio() {
|
|
585
|
+
if (this.devicePixelRatioInitialized) return;
|
|
586
|
+
const apiScale = await this.wdaBackend.getScreenScale();
|
|
587
|
+
external_node_assert_default()(apiScale && apiScale > 0, 'Failed to get device pixel ratio from WebDriverAgent API');
|
|
588
|
+
debugDevice(`Got screen scale from WebDriverAgent API: ${apiScale}`);
|
|
589
|
+
this.devicePixelRatio = apiScale;
|
|
590
|
+
this.devicePixelRatioInitialized = true;
|
|
591
|
+
}
|
|
592
|
+
async getScreenSize() {
|
|
593
|
+
await this.initializeDevicePixelRatio();
|
|
594
|
+
const windowSize = await this.wdaBackend.getWindowSize();
|
|
595
|
+
return {
|
|
596
|
+
width: windowSize.width,
|
|
597
|
+
height: windowSize.height,
|
|
598
|
+
scale: this.devicePixelRatio
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
async size() {
|
|
602
|
+
const screenSize = await this.getScreenSize();
|
|
603
|
+
return {
|
|
604
|
+
width: screenSize.width,
|
|
605
|
+
height: screenSize.height,
|
|
606
|
+
dpr: screenSize.scale
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async screenshotBase64() {
|
|
610
|
+
debugDevice('Taking screenshot via WDA');
|
|
611
|
+
try {
|
|
612
|
+
const base64Data = await this.wdaBackend.takeScreenshot();
|
|
613
|
+
const result = (0, img_namespaceObject.createImgBase64ByFormat)('png', base64Data);
|
|
614
|
+
debugDevice('Screenshot taken successfully');
|
|
615
|
+
return result;
|
|
616
|
+
} catch (error) {
|
|
617
|
+
debugDevice(`Screenshot failed: ${error}`);
|
|
618
|
+
throw new Error(`Failed to take screenshot: ${error}`);
|
|
736
619
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
620
|
+
}
|
|
621
|
+
async clearInput(element) {
|
|
622
|
+
if (!element) return;
|
|
623
|
+
await this.tap(element.center[0], element.center[1]);
|
|
624
|
+
await (0, utils_namespaceObject.sleep)(100);
|
|
625
|
+
debugDevice('Attempting to clear input with WebDriver Clear API');
|
|
626
|
+
const cleared = await this.wdaBackend.clearActiveElement();
|
|
627
|
+
cleared ? debugDevice('Successfully cleared input with WebDriver Clear API') : debugDevice('WebDriver Clear API returned false (no active element or clear failed)');
|
|
628
|
+
}
|
|
629
|
+
async url() {
|
|
630
|
+
return '';
|
|
631
|
+
}
|
|
632
|
+
async tap(x, y) {
|
|
633
|
+
await this.wdaBackend.tap(Math.round(x), Math.round(y));
|
|
634
|
+
}
|
|
635
|
+
async mouseClick(x, y) {
|
|
636
|
+
debugDevice(`mouseClick at coordinates (${x}, ${y})`);
|
|
637
|
+
await this.tap(x, y);
|
|
638
|
+
}
|
|
639
|
+
async doubleTap(x, y) {
|
|
640
|
+
await this.wdaBackend.doubleTap(Math.round(x), Math.round(y));
|
|
641
|
+
}
|
|
642
|
+
async tripleTap(x, y) {
|
|
643
|
+
await this.wdaBackend.tripleTap(Math.round(x), Math.round(y));
|
|
644
|
+
}
|
|
645
|
+
async longPress(x, y, duration = 1000) {
|
|
646
|
+
await this.wdaBackend.longPress(Math.round(x), Math.round(y), duration);
|
|
647
|
+
}
|
|
648
|
+
async swipe(fromX, fromY, toX, toY, duration = 500) {
|
|
649
|
+
await this.wdaBackend.swipe(Math.round(fromX), Math.round(fromY), Math.round(toX), Math.round(toY), duration);
|
|
650
|
+
}
|
|
651
|
+
async typeText(text, options) {
|
|
652
|
+
if (!text) return;
|
|
653
|
+
const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
|
|
654
|
+
debugDevice(`Typing text: "${text}"`);
|
|
655
|
+
try {
|
|
656
|
+
await (0, utils_namespaceObject.sleep)(200);
|
|
657
|
+
await this.wdaBackend.typeText(text);
|
|
658
|
+
await (0, utils_namespaceObject.sleep)(300);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
debugDevice(`Failed to type text with WDA: ${error}`);
|
|
661
|
+
throw error;
|
|
740
662
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
663
|
+
if (shouldAutoDismissKeyboard) await this.hideKeyboard();
|
|
664
|
+
}
|
|
665
|
+
async pressKey(key) {
|
|
666
|
+
await this.wdaBackend.pressKey(key);
|
|
667
|
+
}
|
|
668
|
+
async scrollUp(distance, startPoint) {
|
|
669
|
+
const { width, height } = await this.size();
|
|
670
|
+
const start = startPoint ? {
|
|
671
|
+
x: Math.round(startPoint.left),
|
|
672
|
+
y: Math.round(startPoint.top)
|
|
673
|
+
} : {
|
|
674
|
+
x: Math.round(width / 2),
|
|
675
|
+
y: Math.round(height / 2)
|
|
676
|
+
};
|
|
677
|
+
const scrollDistance = Math.round(distance || height / 3);
|
|
678
|
+
await this.swipe(start.x, start.y, start.x, start.y + scrollDistance);
|
|
679
|
+
}
|
|
680
|
+
async scrollDown(distance, startPoint) {
|
|
681
|
+
const { width, height } = await this.size();
|
|
682
|
+
const start = startPoint ? {
|
|
683
|
+
x: Math.round(startPoint.left),
|
|
684
|
+
y: Math.round(startPoint.top)
|
|
685
|
+
} : {
|
|
686
|
+
x: Math.round(width / 2),
|
|
687
|
+
y: Math.round(height / 2)
|
|
688
|
+
};
|
|
689
|
+
const scrollDistance = Math.round(distance || height / 3);
|
|
690
|
+
await this.swipe(start.x, start.y, start.x, start.y - scrollDistance);
|
|
691
|
+
}
|
|
692
|
+
async scrollLeft(distance, startPoint) {
|
|
693
|
+
const { width, height } = await this.size();
|
|
694
|
+
const start = startPoint ? {
|
|
695
|
+
x: Math.round(startPoint.left),
|
|
696
|
+
y: Math.round(startPoint.top)
|
|
697
|
+
} : {
|
|
698
|
+
x: Math.round(width / 2),
|
|
699
|
+
y: Math.round(height / 2)
|
|
700
|
+
};
|
|
701
|
+
const scrollDistance = Math.round(distance || 0.7 * width);
|
|
702
|
+
await this.swipe(start.x, start.y, start.x + scrollDistance, start.y);
|
|
703
|
+
}
|
|
704
|
+
async scrollRight(distance, startPoint) {
|
|
705
|
+
const { width, height } = await this.size();
|
|
706
|
+
const start = startPoint ? {
|
|
707
|
+
x: Math.round(startPoint.left),
|
|
708
|
+
y: Math.round(startPoint.top)
|
|
709
|
+
} : {
|
|
710
|
+
x: Math.round(width / 2),
|
|
711
|
+
y: Math.round(height / 2)
|
|
712
|
+
};
|
|
713
|
+
const scrollDistance = Math.round(distance || 0.7 * width);
|
|
714
|
+
await this.swipe(start.x, start.y, start.x - scrollDistance, start.y);
|
|
715
|
+
}
|
|
716
|
+
async scrollUntilTop(startPoint) {
|
|
717
|
+
debugDevice('Using screenshot-based scroll detection for better reliability');
|
|
718
|
+
await this.scrollUntilBoundary('up', startPoint, 1);
|
|
719
|
+
}
|
|
720
|
+
async scrollUntilBottom(startPoint) {
|
|
721
|
+
debugDevice('Using screenshot-based scroll detection for better reliability');
|
|
722
|
+
await this.scrollUntilBoundary('down', startPoint, 1);
|
|
723
|
+
}
|
|
724
|
+
compareScreenshots(screenshot1, screenshot2, tolerancePercent = 2) {
|
|
725
|
+
if (screenshot1 === screenshot2) {
|
|
726
|
+
debugDevice('Screenshots are identical');
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
const len1 = screenshot1.length;
|
|
730
|
+
const len2 = screenshot2.length;
|
|
731
|
+
debugDevice(`Screenshots differ: length1=${len1}, length2=${len2}`);
|
|
732
|
+
if (Math.abs(len1 - len2) > 0.1 * Math.min(len1, len2)) {
|
|
733
|
+
debugDevice('Screenshots have significant length difference');
|
|
734
|
+
return false;
|
|
744
735
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
736
|
+
if (len1 > 0 && len2 > 0) {
|
|
737
|
+
const minLength = Math.min(len1, len2);
|
|
738
|
+
const sampleSize = Math.min(2000, minLength);
|
|
739
|
+
let diffCount = 0;
|
|
740
|
+
for(let i = 0; i < sampleSize; i++)if (screenshot1[i] !== screenshot2[i]) diffCount++;
|
|
741
|
+
const diffPercent = diffCount / sampleSize * 100;
|
|
742
|
+
debugDevice(`Character differences: ${diffCount}/${sampleSize} (${diffPercent.toFixed(2)}%)`);
|
|
743
|
+
const isSimilar = diffPercent <= tolerancePercent;
|
|
744
|
+
if (isSimilar) debugDevice(`Screenshots are similar enough (${diffPercent.toFixed(2)}% <= ${tolerancePercent}%)`);
|
|
745
|
+
return isSimilar;
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
async scrollUntilBoundary(direction, startPoint, maxUnchangedCount = 1) {
|
|
750
|
+
const maxAttempts = 20;
|
|
751
|
+
const { width, height } = await this.size();
|
|
752
|
+
let start;
|
|
753
|
+
if (startPoint) start = {
|
|
754
|
+
x: Math.round(startPoint.left),
|
|
755
|
+
y: Math.round(startPoint.top)
|
|
756
|
+
};
|
|
757
|
+
else switch(direction){
|
|
758
|
+
case 'up':
|
|
759
|
+
start = {
|
|
760
|
+
x: Math.round(width / 2),
|
|
761
|
+
y: Math.round(0.2 * height)
|
|
762
|
+
};
|
|
763
|
+
break;
|
|
764
|
+
case 'down':
|
|
765
|
+
start = {
|
|
766
|
+
x: Math.round(width / 2),
|
|
767
|
+
y: Math.round(0.8 * height)
|
|
768
|
+
};
|
|
769
|
+
break;
|
|
770
|
+
case 'left':
|
|
771
|
+
start = {
|
|
772
|
+
x: Math.round(0.8 * width),
|
|
773
|
+
y: Math.round(height / 2)
|
|
774
|
+
};
|
|
775
|
+
break;
|
|
776
|
+
case 'right':
|
|
777
|
+
start = {
|
|
778
|
+
x: Math.round(0.2 * width),
|
|
779
|
+
y: Math.round(height / 2)
|
|
780
|
+
};
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
let lastScreenshot = null;
|
|
784
|
+
let unchangedCount = 0;
|
|
785
|
+
debugDevice(`Starting scroll to ${direction} with content detection`);
|
|
786
|
+
for(let i = 0; i < maxAttempts; i++)try {
|
|
787
|
+
debugDevice(`Scroll attempt ${i + 1}/${maxAttempts}`);
|
|
788
|
+
await (0, utils_namespaceObject.sleep)(500);
|
|
789
|
+
const currentScreenshot = await this.screenshotBase64();
|
|
790
|
+
if (lastScreenshot && this.compareScreenshots(lastScreenshot, currentScreenshot, 10)) {
|
|
791
|
+
unchangedCount++;
|
|
792
|
+
debugDevice(`Screen content unchanged (${unchangedCount}/${maxUnchangedCount})`);
|
|
793
|
+
if (unchangedCount >= maxUnchangedCount) {
|
|
794
|
+
debugDevice(`Reached ${direction}: screen content no longer changes`);
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
} else {
|
|
798
|
+
if (lastScreenshot) debugDevice(`Content changed, resetting counter (was ${unchangedCount})`);
|
|
799
|
+
unchangedCount = 0;
|
|
756
800
|
}
|
|
757
|
-
if (
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
let diffCount = 0;
|
|
761
|
-
for(let i = 0; i < sampleSize; i++)if (screenshot1[i] !== screenshot2[i]) diffCount++;
|
|
762
|
-
const diffPercent = diffCount / sampleSize * 100;
|
|
763
|
-
debugDevice(`Character differences: ${diffCount}/${sampleSize} (${diffPercent.toFixed(2)}%)`);
|
|
764
|
-
const isSimilar = diffPercent <= tolerancePercent;
|
|
765
|
-
if (isSimilar) debugDevice(`Screenshots are similar enough (${diffPercent.toFixed(2)}% <= ${tolerancePercent}%)`);
|
|
766
|
-
return isSimilar;
|
|
801
|
+
if (i >= 15 && 0 === unchangedCount) {
|
|
802
|
+
debugDevice(`Too many attempts with dynamic content, stopping scroll to ${direction}`);
|
|
803
|
+
break;
|
|
767
804
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const { width, height } = await this.size();
|
|
773
|
-
let start;
|
|
774
|
-
if (startPoint) start = {
|
|
775
|
-
x: Math.round(startPoint.left),
|
|
776
|
-
y: Math.round(startPoint.top)
|
|
777
|
-
};
|
|
778
|
-
else switch(direction){
|
|
805
|
+
lastScreenshot = currentScreenshot;
|
|
806
|
+
const scrollDistance = Math.round('left' === direction || 'right' === direction ? 0.6 * width : 0.6 * height);
|
|
807
|
+
debugDevice(`Performing scroll: ${direction}, distance: ${scrollDistance}`);
|
|
808
|
+
switch(direction){
|
|
779
809
|
case 'up':
|
|
780
|
-
start
|
|
781
|
-
x: Math.round(width / 2),
|
|
782
|
-
y: Math.round(0.2 * height)
|
|
783
|
-
};
|
|
810
|
+
await this.swipe(start.x, start.y, start.x, start.y + scrollDistance, 300);
|
|
784
811
|
break;
|
|
785
812
|
case 'down':
|
|
786
|
-
start
|
|
787
|
-
x: Math.round(width / 2),
|
|
788
|
-
y: Math.round(0.8 * height)
|
|
789
|
-
};
|
|
813
|
+
await this.swipe(start.x, start.y, start.x, start.y - scrollDistance, 300);
|
|
790
814
|
break;
|
|
791
815
|
case 'left':
|
|
792
|
-
start
|
|
793
|
-
x: Math.round(0.8 * width),
|
|
794
|
-
y: Math.round(height / 2)
|
|
795
|
-
};
|
|
816
|
+
await this.swipe(start.x, start.y, start.x + scrollDistance, start.y, 300);
|
|
796
817
|
break;
|
|
797
818
|
case 'right':
|
|
798
|
-
start
|
|
799
|
-
x: Math.round(0.2 * width),
|
|
800
|
-
y: Math.round(height / 2)
|
|
801
|
-
};
|
|
819
|
+
await this.swipe(start.x, start.y, start.x - scrollDistance, start.y, 300);
|
|
802
820
|
break;
|
|
803
821
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
await (0, utils_namespaceObject.sleep)(500);
|
|
810
|
-
const currentScreenshot = await this.screenshotBase64();
|
|
811
|
-
if (lastScreenshot && this.compareScreenshots(lastScreenshot, currentScreenshot, 10)) {
|
|
812
|
-
unchangedCount++;
|
|
813
|
-
debugDevice(`Screen content unchanged (${unchangedCount}/${maxUnchangedCount})`);
|
|
814
|
-
if (unchangedCount >= maxUnchangedCount) {
|
|
815
|
-
debugDevice(`Reached ${direction}: screen content no longer changes`);
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
} else {
|
|
819
|
-
if (lastScreenshot) debugDevice(`Content changed, resetting counter (was ${unchangedCount})`);
|
|
820
|
-
unchangedCount = 0;
|
|
821
|
-
}
|
|
822
|
-
if (i >= 15 && 0 === unchangedCount) {
|
|
823
|
-
debugDevice(`Too many attempts with dynamic content, stopping scroll to ${direction}`);
|
|
824
|
-
break;
|
|
825
|
-
}
|
|
826
|
-
lastScreenshot = currentScreenshot;
|
|
827
|
-
const scrollDistance = Math.round('left' === direction || 'right' === direction ? 0.6 * width : 0.6 * height);
|
|
828
|
-
debugDevice(`Performing scroll: ${direction}, distance: ${scrollDistance}`);
|
|
829
|
-
switch(direction){
|
|
830
|
-
case 'up':
|
|
831
|
-
await this.swipe(start.x, start.y, start.x, start.y + scrollDistance, 300);
|
|
832
|
-
break;
|
|
833
|
-
case 'down':
|
|
834
|
-
await this.swipe(start.x, start.y, start.x, start.y - scrollDistance, 300);
|
|
835
|
-
break;
|
|
836
|
-
case 'left':
|
|
837
|
-
await this.swipe(start.x, start.y, start.x + scrollDistance, start.y, 300);
|
|
838
|
-
break;
|
|
839
|
-
case 'right':
|
|
840
|
-
await this.swipe(start.x, start.y, start.x - scrollDistance, start.y, 300);
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
debugDevice('Waiting for scroll and inertia to complete...');
|
|
844
|
-
await (0, utils_namespaceObject.sleep)(2000);
|
|
845
|
-
} catch (error) {
|
|
846
|
-
debugDevice(`Error during scroll attempt ${i + 1}: ${error}`);
|
|
847
|
-
await (0, utils_namespaceObject.sleep)(300);
|
|
848
|
-
}
|
|
849
|
-
debugDevice(`Scroll to ${direction} completed after ${maxAttempts} attempts`);
|
|
850
|
-
}
|
|
851
|
-
async scrollUntilLeft(startPoint) {
|
|
852
|
-
await this.scrollUntilBoundary('left', startPoint, 1);
|
|
853
|
-
}
|
|
854
|
-
async scrollUntilRight(startPoint) {
|
|
855
|
-
await this.scrollUntilBoundary('right', startPoint, 3);
|
|
856
|
-
}
|
|
857
|
-
async home() {
|
|
858
|
-
await this.wdaBackend.pressHomeButton();
|
|
822
|
+
debugDevice('Waiting for scroll and inertia to complete...');
|
|
823
|
+
await (0, utils_namespaceObject.sleep)(2000);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
debugDevice(`Error during scroll attempt ${i + 1}: ${error}`);
|
|
826
|
+
await (0, utils_namespaceObject.sleep)(300);
|
|
859
827
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
828
|
+
debugDevice(`Scroll to ${direction} completed after ${maxAttempts} attempts`);
|
|
829
|
+
}
|
|
830
|
+
async scrollUntilLeft(startPoint) {
|
|
831
|
+
await this.scrollUntilBoundary('left', startPoint, 1);
|
|
832
|
+
}
|
|
833
|
+
async scrollUntilRight(startPoint) {
|
|
834
|
+
await this.scrollUntilBoundary('right', startPoint, 3);
|
|
835
|
+
}
|
|
836
|
+
async home() {
|
|
837
|
+
await this.wdaBackend.pressHomeButton();
|
|
838
|
+
}
|
|
839
|
+
async appSwitcher() {
|
|
840
|
+
try {
|
|
841
|
+
debugDevice('Triggering app switcher with slow swipe up gesture');
|
|
842
|
+
const { width, height } = await this.size();
|
|
843
|
+
const centerX = Math.round(width / 2);
|
|
844
|
+
const startY = Math.round(height - 5);
|
|
845
|
+
const endY = Math.round(0.5 * height);
|
|
846
|
+
await this.wdaBackend.swipe(centerX, startY, centerX, endY, 1500);
|
|
847
|
+
await (0, utils_namespaceObject.sleep)(800);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
debugDevice(`App switcher failed: ${error}`);
|
|
850
|
+
throw new Error(`Failed to trigger app switcher: ${error}`);
|
|
873
851
|
}
|
|
874
|
-
|
|
852
|
+
}
|
|
853
|
+
async hideKeyboard(keyNames) {
|
|
854
|
+
try {
|
|
855
|
+
const dismissKeys = keyNames && keyNames.length > 0 ? keyNames : [
|
|
856
|
+
'return',
|
|
857
|
+
'done',
|
|
858
|
+
'go',
|
|
859
|
+
'search',
|
|
860
|
+
'next',
|
|
861
|
+
'send'
|
|
862
|
+
];
|
|
863
|
+
debugDevice(`Attempting to dismiss keyboard using WDA API with keys: ${dismissKeys.join(', ')}`);
|
|
875
864
|
try {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
'done',
|
|
879
|
-
'go',
|
|
880
|
-
'search',
|
|
881
|
-
'next',
|
|
882
|
-
'send'
|
|
883
|
-
];
|
|
884
|
-
debugDevice(`Attempting to dismiss keyboard using WDA API with keys: ${dismissKeys.join(', ')}`);
|
|
885
|
-
try {
|
|
886
|
-
await this.wdaBackend.dismissKeyboard(dismissKeys);
|
|
887
|
-
debugDevice('Successfully dismissed keyboard using WDA API');
|
|
888
|
-
await (0, utils_namespaceObject.sleep)(500);
|
|
889
|
-
return true;
|
|
890
|
-
} catch (wdaError) {
|
|
891
|
-
debugDevice(`WDA dismissKeyboard failed, falling back to swipe gesture: ${wdaError}`);
|
|
892
|
-
}
|
|
893
|
-
const windowSize = await this.wdaBackend.getWindowSize();
|
|
894
|
-
const centerX = Math.round(windowSize.width / 2);
|
|
895
|
-
const startY = Math.round(0.9 * windowSize.height);
|
|
896
|
-
const endY = Math.round(0.5 * windowSize.height);
|
|
897
|
-
await this.swipe(centerX, startY, centerX, endY, 300);
|
|
898
|
-
debugDevice('Dismissed keyboard with swipe up gesture from bottom of screen');
|
|
865
|
+
await this.wdaBackend.dismissKeyboard(dismissKeys);
|
|
866
|
+
debugDevice('Successfully dismissed keyboard using WDA API');
|
|
899
867
|
await (0, utils_namespaceObject.sleep)(500);
|
|
900
868
|
return true;
|
|
901
|
-
} catch (
|
|
902
|
-
debugDevice(`
|
|
903
|
-
return false;
|
|
869
|
+
} catch (wdaError) {
|
|
870
|
+
debugDevice(`WDA dismissKeyboard failed, falling back to swipe gesture: ${wdaError}`);
|
|
904
871
|
}
|
|
872
|
+
const windowSize = await this.wdaBackend.getWindowSize();
|
|
873
|
+
const centerX = Math.round(windowSize.width / 2);
|
|
874
|
+
const startY = Math.round(0.9 * windowSize.height);
|
|
875
|
+
const endY = Math.round(0.5 * windowSize.height);
|
|
876
|
+
await this.swipe(centerX, startY, centerX, endY, 300);
|
|
877
|
+
debugDevice('Dismissed keyboard with swipe up gesture from bottom of screen');
|
|
878
|
+
await (0, utils_namespaceObject.sleep)(500);
|
|
879
|
+
return true;
|
|
880
|
+
} catch (error) {
|
|
881
|
+
debugDevice(`Failed to hide keyboard: ${error}`);
|
|
882
|
+
return false;
|
|
905
883
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
884
|
+
}
|
|
885
|
+
async openUrl(url, options) {
|
|
886
|
+
const opts = {
|
|
887
|
+
useSafariAsBackup: true,
|
|
888
|
+
waitTime: 2000,
|
|
889
|
+
...options
|
|
890
|
+
};
|
|
891
|
+
try {
|
|
892
|
+
debugDevice(`Opening URL: ${url}`);
|
|
893
|
+
await this.wdaBackend.openUrl(url);
|
|
894
|
+
await (0, utils_namespaceObject.sleep)(opts.waitTime);
|
|
895
|
+
debugDevice(`Successfully opened URL: ${url}`);
|
|
896
|
+
} catch (error) {
|
|
897
|
+
debugDevice(`Direct URL opening failed: ${error}`);
|
|
898
|
+
if (opts.useSafariAsBackup) {
|
|
899
|
+
debugDevice(`Attempting to open URL via Safari: ${url}`);
|
|
900
|
+
await this.openUrlViaSafari(url);
|
|
901
|
+
} else throw new Error(`Failed to open URL: ${error}`);
|
|
924
902
|
}
|
|
925
|
-
|
|
903
|
+
}
|
|
904
|
+
async openUrlViaSafari(url) {
|
|
905
|
+
try {
|
|
906
|
+
debugDevice(`Opening URL via Safari: ${url}`);
|
|
907
|
+
await this.wdaBackend.terminateApp('com.apple.mobilesafari');
|
|
908
|
+
await this.wdaBackend.launchApp('com.apple.mobilesafari');
|
|
909
|
+
await (0, utils_namespaceObject.sleep)(2000);
|
|
910
|
+
await this.typeText(url);
|
|
911
|
+
await (0, utils_namespaceObject.sleep)(500);
|
|
912
|
+
await this.pressKey('Return');
|
|
913
|
+
await (0, utils_namespaceObject.sleep)(1000);
|
|
926
914
|
try {
|
|
927
|
-
debugDevice(`Opening URL via Safari: ${url}`);
|
|
928
|
-
await this.wdaBackend.terminateApp('com.apple.mobilesafari');
|
|
929
|
-
await this.wdaBackend.launchApp('com.apple.mobilesafari');
|
|
930
915
|
await (0, utils_namespaceObject.sleep)(2000);
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
await (0, utils_namespaceObject.sleep)(1000);
|
|
935
|
-
try {
|
|
936
|
-
await (0, utils_namespaceObject.sleep)(2000);
|
|
937
|
-
debugDevice(`URL opened via Safari: ${url}`);
|
|
938
|
-
} catch (dialogError) {
|
|
939
|
-
debugDevice(`No confirmation dialog or dialog handling failed: ${dialogError}`);
|
|
940
|
-
}
|
|
941
|
-
} catch (error) {
|
|
942
|
-
debugDevice(`Failed to open URL via Safari: ${error}`);
|
|
943
|
-
throw new Error(`Failed to open URL via Safari: ${error}`);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
async runWdaRequest(method, endpoint, data) {
|
|
947
|
-
return await this.wdaBackend.executeRequest(method, endpoint, data);
|
|
948
|
-
}
|
|
949
|
-
async destroy() {
|
|
950
|
-
if (this.destroyed) return;
|
|
951
|
-
try {
|
|
952
|
-
await this.wdaBackend.deleteSession();
|
|
953
|
-
await this.wdaManager.stop();
|
|
954
|
-
} catch (error) {
|
|
955
|
-
debugDevice(`Error during cleanup: ${error}`);
|
|
916
|
+
debugDevice(`URL opened via Safari: ${url}`);
|
|
917
|
+
} catch (dialogError) {
|
|
918
|
+
debugDevice(`No confirmation dialog or dialog handling failed: ${dialogError}`);
|
|
956
919
|
}
|
|
957
|
-
|
|
958
|
-
debugDevice(`
|
|
920
|
+
} catch (error) {
|
|
921
|
+
debugDevice(`Failed to open URL via Safari: ${error}`);
|
|
922
|
+
throw new Error(`Failed to open URL via Safari: ${error}`);
|
|
959
923
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
_define_property(this, "options", void 0);
|
|
972
|
-
this.deviceId = 'pending-connection';
|
|
973
|
-
this.options = options;
|
|
974
|
-
this.customActions = null == options ? void 0 : options.customActions;
|
|
975
|
-
const wdaPort = (null == options ? void 0 : options.wdaPort) || constants_namespaceObject.DEFAULT_WDA_PORT;
|
|
976
|
-
const wdaHost = (null == options ? void 0 : options.wdaHost) || 'localhost';
|
|
977
|
-
this.wdaBackend = new IOSWebDriverClient({
|
|
978
|
-
port: wdaPort,
|
|
979
|
-
host: wdaHost
|
|
980
|
-
});
|
|
981
|
-
this.wdaManager = webdriver_namespaceObject.WDAManager.getInstance(wdaPort, wdaHost);
|
|
924
|
+
}
|
|
925
|
+
async runWdaRequest(method, endpoint, data) {
|
|
926
|
+
return await this.wdaBackend.executeRequest(method, endpoint, data);
|
|
927
|
+
}
|
|
928
|
+
async destroy() {
|
|
929
|
+
if (this.destroyed) return;
|
|
930
|
+
try {
|
|
931
|
+
await this.wdaBackend.deleteSession();
|
|
932
|
+
await this.wdaManager.stop();
|
|
933
|
+
} catch (error) {
|
|
934
|
+
debugDevice(`Error during cleanup: ${error}`);
|
|
982
935
|
}
|
|
936
|
+
this.destroyed = true;
|
|
937
|
+
debugDevice(`iOS device ${this.deviceId} destroyed`);
|
|
983
938
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
await device.launch(param);
|
|
1005
|
-
}
|
|
1006
|
-
}),
|
|
1007
|
-
IOSHomeButton: (0, device_namespaceObject.defineAction)({
|
|
1008
|
-
name: 'IOSHomeButton',
|
|
1009
|
-
description: 'Trigger the system "home" operation on iOS devices',
|
|
1010
|
-
call: async ()=>{
|
|
1011
|
-
await device.home();
|
|
1012
|
-
}
|
|
1013
|
-
}),
|
|
1014
|
-
IOSAppSwitcher: (0, device_namespaceObject.defineAction)({
|
|
1015
|
-
name: 'IOSAppSwitcher',
|
|
1016
|
-
description: 'Trigger the system "app switcher" operation on iOS devices',
|
|
1017
|
-
call: async ()=>{
|
|
1018
|
-
await device.appSwitcher();
|
|
1019
|
-
}
|
|
1020
|
-
})
|
|
939
|
+
constructor(options){
|
|
940
|
+
_define_property(this, "deviceId", void 0);
|
|
941
|
+
_define_property(this, "devicePixelRatio", 1);
|
|
942
|
+
_define_property(this, "devicePixelRatioInitialized", false);
|
|
943
|
+
_define_property(this, "destroyed", false);
|
|
944
|
+
_define_property(this, "description", void 0);
|
|
945
|
+
_define_property(this, "customActions", void 0);
|
|
946
|
+
_define_property(this, "wdaBackend", void 0);
|
|
947
|
+
_define_property(this, "wdaManager", void 0);
|
|
948
|
+
_define_property(this, "interfaceType", 'ios');
|
|
949
|
+
_define_property(this, "uri", void 0);
|
|
950
|
+
_define_property(this, "options", void 0);
|
|
951
|
+
this.deviceId = 'pending-connection';
|
|
952
|
+
this.options = options;
|
|
953
|
+
this.customActions = options?.customActions;
|
|
954
|
+
const wdaPort = options?.wdaPort || constants_namespaceObject.DEFAULT_WDA_PORT;
|
|
955
|
+
const wdaHost = options?.wdaHost || 'localhost';
|
|
956
|
+
this.wdaBackend = new IOSWebDriverClient({
|
|
957
|
+
port: wdaPort,
|
|
958
|
+
host: wdaHost
|
|
1021
959
|
});
|
|
1022
|
-
|
|
1023
|
-
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
1024
|
-
const external_node_os_namespaceObject = require("node:os");
|
|
1025
|
-
const external_node_util_namespaceObject = require("node:util");
|
|
1026
|
-
const execAsync = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.exec);
|
|
1027
|
-
const debugUtils = (0, logger_namespaceObject.getDebug)('ios:utils');
|
|
1028
|
-
function checkMacOSPlatform() {
|
|
1029
|
-
const currentPlatform = (0, external_node_os_namespaceObject.platform)();
|
|
1030
|
-
return {
|
|
1031
|
-
isMacOS: 'darwin' === currentPlatform,
|
|
1032
|
-
platform: currentPlatform
|
|
1033
|
-
};
|
|
960
|
+
this.wdaManager = webdriver_namespaceObject.WDAManager.getInstance(wdaPort, wdaHost);
|
|
1034
961
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
962
|
+
}
|
|
963
|
+
const runWdaRequestParamSchema = core_namespaceObject.z.object({
|
|
964
|
+
method: core_namespaceObject.z["enum"](WDA_HTTP_METHODS).describe('HTTP method (GET, POST, DELETE, PUT)'),
|
|
965
|
+
endpoint: core_namespaceObject.z.string().describe('WebDriver API endpoint'),
|
|
966
|
+
data: core_namespaceObject.z.object({}).passthrough().optional().describe('Optional request body data as JSON object')
|
|
967
|
+
});
|
|
968
|
+
const launchParamSchema = core_namespaceObject.z.string().describe('App bundle ID or URL to launch');
|
|
969
|
+
const createPlatformActions = (device)=>({
|
|
970
|
+
RunWdaRequest: (0, device_namespaceObject.defineAction)({
|
|
971
|
+
name: 'RunWdaRequest',
|
|
972
|
+
description: 'Execute WebDriverAgent API request directly on iOS device',
|
|
973
|
+
interfaceAlias: 'runWdaRequest',
|
|
974
|
+
paramSchema: runWdaRequestParamSchema,
|
|
975
|
+
call: async (param)=>await device.runWdaRequest(param.method, param.endpoint, param.data)
|
|
976
|
+
}),
|
|
977
|
+
Launch: (0, device_namespaceObject.defineAction)({
|
|
978
|
+
name: 'Launch',
|
|
979
|
+
description: 'Launch an iOS app or URL',
|
|
980
|
+
interfaceAlias: 'launch',
|
|
981
|
+
paramSchema: launchParamSchema,
|
|
982
|
+
call: async (param)=>{
|
|
983
|
+
await device.launch(param);
|
|
1054
984
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
985
|
+
}),
|
|
986
|
+
IOSHomeButton: (0, device_namespaceObject.defineAction)({
|
|
987
|
+
name: 'IOSHomeButton',
|
|
988
|
+
description: 'Trigger the system "home" operation on iOS devices',
|
|
989
|
+
call: async ()=>{
|
|
990
|
+
await device.home();
|
|
991
|
+
}
|
|
992
|
+
}),
|
|
993
|
+
IOSAppSwitcher: (0, device_namespaceObject.defineAction)({
|
|
994
|
+
name: 'IOSAppSwitcher',
|
|
995
|
+
description: 'Trigger the system "app switcher" operation on iOS devices',
|
|
996
|
+
call: async ()=>{
|
|
997
|
+
await device.appSwitcher();
|
|
998
|
+
}
|
|
999
|
+
})
|
|
1000
|
+
});
|
|
1001
|
+
const agent_namespaceObject = require("@midscene/core/agent");
|
|
1002
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
1003
|
+
const external_node_os_namespaceObject = require("node:os");
|
|
1004
|
+
const external_node_util_namespaceObject = require("node:util");
|
|
1005
|
+
const execAsync = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.exec);
|
|
1006
|
+
const debugUtils = (0, logger_namespaceObject.getDebug)('ios:utils');
|
|
1007
|
+
function checkMacOSPlatform() {
|
|
1008
|
+
const currentPlatform = (0, external_node_os_namespaceObject.platform)();
|
|
1009
|
+
return {
|
|
1010
|
+
isMacOS: 'darwin' === currentPlatform,
|
|
1011
|
+
platform: currentPlatform
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
async function checkIOSEnvironment() {
|
|
1015
|
+
try {
|
|
1016
|
+
const platformCheck = checkMacOSPlatform();
|
|
1017
|
+
if (!platformCheck.isMacOS) return {
|
|
1018
|
+
available: false,
|
|
1019
|
+
error: `iOS development is only supported on macOS. Current platform: ${platformCheck.platform}`
|
|
1020
|
+
};
|
|
1021
|
+
const { stdout: xcrunPath } = await execAsync('which xcrun');
|
|
1022
|
+
if (!xcrunPath.trim()) return {
|
|
1023
|
+
available: false,
|
|
1024
|
+
error: 'xcrun not found. Please install Xcode Command Line Tools: xcode-select --install'
|
|
1025
|
+
};
|
|
1026
|
+
try {
|
|
1027
|
+
await execAsync('xcodebuild -version');
|
|
1059
1028
|
} catch (error) {
|
|
1060
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1061
|
-
debugUtils(`iOS environment not available: ${errorMsg}`);
|
|
1062
|
-
if (errorMsg.includes('xcrun')) return {
|
|
1063
|
-
available: false,
|
|
1064
|
-
error: 'Xcode Command Line Tools not properly configured. Please run: sudo xcode-select --reset'
|
|
1065
|
-
};
|
|
1066
1029
|
return {
|
|
1067
1030
|
available: false,
|
|
1068
|
-
error:
|
|
1031
|
+
error: 'xcodebuild not found. Please install Xcode from the App Store'
|
|
1069
1032
|
};
|
|
1070
1033
|
}
|
|
1034
|
+
debugUtils('iOS environment is available for WebDriverAgent');
|
|
1035
|
+
return {
|
|
1036
|
+
available: true
|
|
1037
|
+
};
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1040
|
+
debugUtils(`iOS environment not available: ${errorMsg}`);
|
|
1041
|
+
if (errorMsg.includes('xcrun')) return {
|
|
1042
|
+
available: false,
|
|
1043
|
+
error: 'Xcode Command Line Tools not properly configured. Please run: sudo xcode-select --reset'
|
|
1044
|
+
};
|
|
1045
|
+
return {
|
|
1046
|
+
available: false,
|
|
1047
|
+
error: `iOS development environment not available: ${errorMsg}`
|
|
1048
|
+
};
|
|
1071
1049
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
constructor(device, opts){
|
|
1089
|
-
super(device, opts), agent_define_property(this, "launch", void 0), agent_define_property(this, "runWdaRequest", void 0), agent_define_property(this, "home", void 0), agent_define_property(this, "appSwitcher", void 0);
|
|
1090
|
-
this.launch = this.createActionWrapper('Launch');
|
|
1091
|
-
this.runWdaRequest = this.createActionWrapper('RunWdaRequest');
|
|
1092
|
-
this.home = this.createActionWrapper('IOSHomeButton');
|
|
1093
|
-
this.appSwitcher = this.createActionWrapper('IOSAppSwitcher');
|
|
1094
|
-
}
|
|
1050
|
+
}
|
|
1051
|
+
function agent_define_property(obj, key, value) {
|
|
1052
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
1053
|
+
value: value,
|
|
1054
|
+
enumerable: true,
|
|
1055
|
+
configurable: true,
|
|
1056
|
+
writable: true
|
|
1057
|
+
});
|
|
1058
|
+
else obj[key] = value;
|
|
1059
|
+
return obj;
|
|
1060
|
+
}
|
|
1061
|
+
const debugAgent = (0, logger_namespaceObject.getDebug)('ios:agent');
|
|
1062
|
+
class IOSAgent extends agent_namespaceObject.Agent {
|
|
1063
|
+
createActionWrapper(name) {
|
|
1064
|
+
const action = this.wrapActionInActionSpace(name);
|
|
1065
|
+
return (...args)=>action(args[0]);
|
|
1095
1066
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
return new IOSAgent(device, opts);
|
|
1067
|
+
constructor(device, opts){
|
|
1068
|
+
super(device, opts), agent_define_property(this, "launch", void 0), agent_define_property(this, "runWdaRequest", void 0), agent_define_property(this, "home", void 0), agent_define_property(this, "appSwitcher", void 0);
|
|
1069
|
+
this.launch = this.createActionWrapper('Launch');
|
|
1070
|
+
this.runWdaRequest = this.createActionWrapper('RunWdaRequest');
|
|
1071
|
+
this.home = this.createActionWrapper('IOSHomeButton');
|
|
1072
|
+
this.appSwitcher = this.createActionWrapper('IOSAppSwitcher');
|
|
1103
1073
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1074
|
+
}
|
|
1075
|
+
async function agentFromWebDriverAgent(opts) {
|
|
1076
|
+
debugAgent('Creating iOS agent with WebDriverAgent auto-detection');
|
|
1077
|
+
const envCheck = await checkIOSEnvironment();
|
|
1078
|
+
if (!envCheck.available) throw new Error(`iOS environment not available: ${envCheck.error}`);
|
|
1079
|
+
const device = new IOSDevice(opts || {});
|
|
1080
|
+
await device.connect();
|
|
1081
|
+
return new IOSAgent(device, opts);
|
|
1082
|
+
}
|
|
1083
|
+
const env_namespaceObject = require("@midscene/shared/env");
|
|
1106
1084
|
exports.IOSAgent = __webpack_exports__.IOSAgent;
|
|
1107
1085
|
exports.IOSDevice = __webpack_exports__.IOSDevice;
|
|
1108
1086
|
exports.IOSWebDriverClient = __webpack_exports__.IOSWebDriverClient;
|