@sqaitech/webdriver 0.30.10
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/LICENSE +21 -0
- package/dist/es/index.mjs +293 -0
- package/dist/lib/index.js +339 -0
- package/dist/types/index.d.ts +122 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Bytedance, Inc. and its affiliates.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { DEFAULT_WDA_PORT } from "@sqaitech/shared/constants";
|
|
4
|
+
import { getDebug } from "@sqaitech/shared/logger";
|
|
5
|
+
function _define_property(obj, key, value) {
|
|
6
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
7
|
+
value: value,
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true
|
|
11
|
+
});
|
|
12
|
+
else obj[key] = value;
|
|
13
|
+
return obj;
|
|
14
|
+
}
|
|
15
|
+
class BaseServiceManager {
|
|
16
|
+
async restart() {
|
|
17
|
+
if (this.isRunning()) await this.stop();
|
|
18
|
+
await this.start();
|
|
19
|
+
}
|
|
20
|
+
getEndpoint() {
|
|
21
|
+
return `http://${this.host}:${this.port}`;
|
|
22
|
+
}
|
|
23
|
+
getPort() {
|
|
24
|
+
return this.port;
|
|
25
|
+
}
|
|
26
|
+
getHost() {
|
|
27
|
+
return this.host;
|
|
28
|
+
}
|
|
29
|
+
constructor(port, host = 'localhost'){
|
|
30
|
+
_define_property(this, "port", void 0);
|
|
31
|
+
_define_property(this, "host", void 0);
|
|
32
|
+
this.port = port;
|
|
33
|
+
this.host = host;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function WDAManager_define_property(obj, key, value) {
|
|
37
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
38
|
+
value: value,
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true
|
|
42
|
+
});
|
|
43
|
+
else obj[key] = value;
|
|
44
|
+
return obj;
|
|
45
|
+
}
|
|
46
|
+
const execAsync = promisify(exec);
|
|
47
|
+
const debugWDA = getDebug('webdriver:wda-manager');
|
|
48
|
+
class WDAManager extends BaseServiceManager {
|
|
49
|
+
static getInstance(port = DEFAULT_WDA_PORT, host) {
|
|
50
|
+
const key = `${host || 'localhost'}:${port}`;
|
|
51
|
+
if (!WDAManager.instances.has(key)) WDAManager.instances.set(key, new WDAManager({
|
|
52
|
+
port,
|
|
53
|
+
host
|
|
54
|
+
}));
|
|
55
|
+
return WDAManager.instances.get(key);
|
|
56
|
+
}
|
|
57
|
+
async start() {
|
|
58
|
+
if (this.isStarted) return void debugWDA(`WDA already started on ${this.config.host}:${this.config.port}`);
|
|
59
|
+
try {
|
|
60
|
+
if (await this.isWDARunning()) {
|
|
61
|
+
debugWDA(`WDA already running on port ${this.config.port}`);
|
|
62
|
+
this.isStarted = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await this.startWDA();
|
|
66
|
+
await this.waitForWDA();
|
|
67
|
+
this.isStarted = true;
|
|
68
|
+
debugWDA(`WDA started successfully on ${this.config.host}:${this.config.port}`);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
debugWDA(`Failed to start WDA: ${error}`);
|
|
71
|
+
throw new Error(`Failed to start WebDriverAgent: ${error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async stop() {
|
|
75
|
+
if (!this.isStarted) return;
|
|
76
|
+
try {
|
|
77
|
+
this.isStarted = false;
|
|
78
|
+
debugWDA(`WDA stopped on ${this.config.host}:${this.config.port}`);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
debugWDA(`Error stopping WDA: ${error}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
isRunning() {
|
|
84
|
+
return this.isStarted;
|
|
85
|
+
}
|
|
86
|
+
async startWDA() {
|
|
87
|
+
await this.checkWDAPreparation();
|
|
88
|
+
debugWDA('WebDriverAgent verification completed');
|
|
89
|
+
}
|
|
90
|
+
async checkWDAPreparation() {
|
|
91
|
+
if (await this.isWDARunning()) return void debugWDA(`WebDriverAgent is already running on port ${this.config.port}`);
|
|
92
|
+
throw new Error(`WebDriverAgent is not running on ${this.config.host}:${this.config.port}. Please start WebDriverAgent manually:
|
|
93
|
+
|
|
94
|
+
\u{1F527} Setup Instructions:
|
|
95
|
+
1. Install WebDriverAgent: npm install appium-webdriveragent
|
|
96
|
+
2. Build and run WebDriverAgent:
|
|
97
|
+
- For simulators: Use Xcode to run WebDriverAgentRunner on your target simulator
|
|
98
|
+
- For real devices: Build WebDriverAgentRunner and install on your device
|
|
99
|
+
3. Ensure WebDriverAgent is listening on ${this.config.host}:${this.config.port}
|
|
100
|
+
|
|
101
|
+
\u{1F4A1} Alternative: You can also specify a different host/port where WebDriverAgent is running.`);
|
|
102
|
+
}
|
|
103
|
+
async isWDARunning() {
|
|
104
|
+
try {
|
|
105
|
+
const url = `http://${this.config.host}:${this.config.port}/status`;
|
|
106
|
+
const response = await fetch(url);
|
|
107
|
+
if (!response.ok) return false;
|
|
108
|
+
const responseText = await response.text();
|
|
109
|
+
return responseText.includes('sessionId');
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async waitForWDA(timeout = 30000) {
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
while(Date.now() - startTime < timeout){
|
|
117
|
+
if (await this.isWDARunning()) return;
|
|
118
|
+
await new Promise((resolve)=>setTimeout(resolve, 1000));
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`WebDriverAgent did not start within ${timeout}ms`);
|
|
121
|
+
}
|
|
122
|
+
async killWDAProcesses() {
|
|
123
|
+
try {
|
|
124
|
+
await execAsync('pkill -f "xcodebuild.*WebDriverAgent"').catch(()=>{});
|
|
125
|
+
await execAsync('pkill -f "WebDriverAgentRunner"').catch(()=>{});
|
|
126
|
+
debugWDA('Killed WDA processes');
|
|
127
|
+
} catch (error) {}
|
|
128
|
+
}
|
|
129
|
+
constructor(config){
|
|
130
|
+
super(config.port, config.host), WDAManager_define_property(this, "config", void 0), WDAManager_define_property(this, "isStarted", false);
|
|
131
|
+
this.config = {
|
|
132
|
+
bundleId: 'com.apple.WebDriverAgentRunner.xctrunner',
|
|
133
|
+
usePrebuiltWDA: true,
|
|
134
|
+
host: 'localhost',
|
|
135
|
+
...config,
|
|
136
|
+
port: config.port || DEFAULT_WDA_PORT
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
WDAManager_define_property(WDAManager, "instances", new Map());
|
|
141
|
+
function request_define_property(obj, key, value) {
|
|
142
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
143
|
+
value: value,
|
|
144
|
+
enumerable: true,
|
|
145
|
+
configurable: true,
|
|
146
|
+
writable: true
|
|
147
|
+
});
|
|
148
|
+
else obj[key] = value;
|
|
149
|
+
return obj;
|
|
150
|
+
}
|
|
151
|
+
const debugRequest = getDebug('webdriver:request');
|
|
152
|
+
class WebDriverRequestError extends Error {
|
|
153
|
+
constructor(message, status, response){
|
|
154
|
+
super(message), request_define_property(this, "status", void 0), request_define_property(this, "response", void 0), this.status = status, this.response = response;
|
|
155
|
+
this.name = 'WebDriverRequestError';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function makeWebDriverRequest(baseUrl, method, endpoint, data, timeout = 30000) {
|
|
159
|
+
const url = `${baseUrl}${endpoint}`;
|
|
160
|
+
debugRequest(`${method} ${url}${data ? ` with data: ${JSON.stringify(data)}` : ''}`);
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
const timeoutId = setTimeout(()=>controller.abort(), timeout);
|
|
163
|
+
try {
|
|
164
|
+
const response = await fetch(url, {
|
|
165
|
+
method,
|
|
166
|
+
headers: {
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
Accept: 'application/json'
|
|
169
|
+
},
|
|
170
|
+
body: data ? JSON.stringify(data) : void 0,
|
|
171
|
+
signal: controller.signal
|
|
172
|
+
});
|
|
173
|
+
clearTimeout(timeoutId);
|
|
174
|
+
let responseData;
|
|
175
|
+
const contentType = response.headers.get('content-type');
|
|
176
|
+
if (null == contentType ? void 0 : contentType.includes('application/json')) responseData = await response.json();
|
|
177
|
+
else {
|
|
178
|
+
const textData = await response.text();
|
|
179
|
+
responseData = textData;
|
|
180
|
+
}
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const errorMessage = (null == responseData ? void 0 : responseData.error) || (null == responseData ? void 0 : responseData.message) || `HTTP ${response.status}`;
|
|
183
|
+
throw new WebDriverRequestError(`WebDriver request failed: ${errorMessage}`, response.status, responseData);
|
|
184
|
+
}
|
|
185
|
+
return responseData;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
clearTimeout(timeoutId);
|
|
188
|
+
if (error instanceof WebDriverRequestError) throw error;
|
|
189
|
+
if (error instanceof Error && 'AbortError' === error.name) throw new WebDriverRequestError(`Request timeout after ${timeout}ms`);
|
|
190
|
+
debugRequest(`Request failed: ${error}`);
|
|
191
|
+
throw new WebDriverRequestError(`Request failed: ${error}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function WebDriverClient_define_property(obj, key, value) {
|
|
195
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
196
|
+
value: value,
|
|
197
|
+
enumerable: true,
|
|
198
|
+
configurable: true,
|
|
199
|
+
writable: true
|
|
200
|
+
});
|
|
201
|
+
else obj[key] = value;
|
|
202
|
+
return obj;
|
|
203
|
+
}
|
|
204
|
+
const debugClient = getDebug('webdriver:client');
|
|
205
|
+
class WebDriverClient {
|
|
206
|
+
get sessionInfo() {
|
|
207
|
+
if (!this.sessionId) return null;
|
|
208
|
+
return {
|
|
209
|
+
sessionId: this.sessionId,
|
|
210
|
+
capabilities: {}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async createSession(capabilities) {
|
|
214
|
+
try {
|
|
215
|
+
var _response_value, _response_value1;
|
|
216
|
+
const response = await this.makeRequest('POST', '/session', {
|
|
217
|
+
capabilities: {
|
|
218
|
+
alwaysMatch: {
|
|
219
|
+
...capabilities
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
this.sessionId = response.sessionId || (null == (_response_value = response.value) ? void 0 : _response_value.sessionId);
|
|
224
|
+
if (!this.sessionId) throw new Error('Failed to get session ID from response');
|
|
225
|
+
debugClient(`Created session: ${this.sessionId}`);
|
|
226
|
+
return {
|
|
227
|
+
sessionId: this.sessionId,
|
|
228
|
+
capabilities: response.capabilities || (null == (_response_value1 = response.value) ? void 0 : _response_value1.capabilities) || {}
|
|
229
|
+
};
|
|
230
|
+
} catch (error) {
|
|
231
|
+
debugClient(`Failed to create session: ${error}`);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async deleteSession() {
|
|
236
|
+
if (!this.sessionId) return void debugClient('No active session to delete');
|
|
237
|
+
try {
|
|
238
|
+
await this.makeRequest('DELETE', `/session/${this.sessionId}`);
|
|
239
|
+
debugClient(`Deleted session: ${this.sessionId}`);
|
|
240
|
+
this.sessionId = null;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
debugClient(`Failed to delete session: ${error}`);
|
|
243
|
+
this.sessionId = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async takeScreenshot() {
|
|
247
|
+
this.ensureSession();
|
|
248
|
+
const response = await this.makeRequest('GET', `/session/${this.sessionId}/screenshot`);
|
|
249
|
+
return response.value || response;
|
|
250
|
+
}
|
|
251
|
+
async getWindowSize() {
|
|
252
|
+
this.ensureSession();
|
|
253
|
+
const response = await this.makeRequest('GET', `/session/${this.sessionId}/window/rect`);
|
|
254
|
+
const rect = response.value || response;
|
|
255
|
+
return {
|
|
256
|
+
width: rect.width,
|
|
257
|
+
height: rect.height
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
async getDeviceInfo() {
|
|
261
|
+
try {
|
|
262
|
+
const statusResponse = await this.makeRequest('GET', '/status');
|
|
263
|
+
if (null == statusResponse ? void 0 : statusResponse.device) return {
|
|
264
|
+
udid: statusResponse.device.udid || statusResponse.device.identifier || '',
|
|
265
|
+
name: statusResponse.device.name || '',
|
|
266
|
+
model: statusResponse.device.model || statusResponse.device.productName || ''
|
|
267
|
+
};
|
|
268
|
+
return null;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
debugClient(`Failed to get device info: ${error}`);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async makeRequest(method, endpoint, data) {
|
|
275
|
+
return makeWebDriverRequest(this.baseUrl, method, endpoint, data, this.timeout);
|
|
276
|
+
}
|
|
277
|
+
ensureSession() {
|
|
278
|
+
if (!this.sessionId) throw new Error('No active WebDriver session. Call createSession() first.');
|
|
279
|
+
}
|
|
280
|
+
constructor(options = {}){
|
|
281
|
+
WebDriverClient_define_property(this, "baseUrl", void 0);
|
|
282
|
+
WebDriverClient_define_property(this, "sessionId", null);
|
|
283
|
+
WebDriverClient_define_property(this, "port", void 0);
|
|
284
|
+
WebDriverClient_define_property(this, "host", void 0);
|
|
285
|
+
WebDriverClient_define_property(this, "timeout", void 0);
|
|
286
|
+
this.port = options.port || DEFAULT_WDA_PORT;
|
|
287
|
+
this.host = options.host || 'localhost';
|
|
288
|
+
this.timeout = options.timeout || 30000;
|
|
289
|
+
this.baseUrl = `http://${this.host}:${this.port}`;
|
|
290
|
+
debugClient(`Initialized WebDriver client on ${this.host}:${this.port}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
export { BaseServiceManager, WDAManager, WebDriverClient, WebDriverRequestError, makeWebDriverRequest };
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, definition)=>{
|
|
5
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: definition[key]
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
})();
|
|
11
|
+
(()=>{
|
|
12
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
13
|
+
})();
|
|
14
|
+
(()=>{
|
|
15
|
+
__webpack_require__.r = (exports1)=>{
|
|
16
|
+
if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
17
|
+
value: 'Module'
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
20
|
+
value: true
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
})();
|
|
24
|
+
var __webpack_exports__ = {};
|
|
25
|
+
__webpack_require__.r(__webpack_exports__);
|
|
26
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
27
|
+
WebDriverRequestError: ()=>WebDriverRequestError,
|
|
28
|
+
WDAManager: ()=>WDAManager,
|
|
29
|
+
WebDriverClient: ()=>WebDriverClient,
|
|
30
|
+
BaseServiceManager: ()=>BaseServiceManager,
|
|
31
|
+
makeWebDriverRequest: ()=>makeWebDriverRequest
|
|
32
|
+
});
|
|
33
|
+
function _define_property(obj, key, value) {
|
|
34
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
35
|
+
value: value,
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true
|
|
39
|
+
});
|
|
40
|
+
else obj[key] = value;
|
|
41
|
+
return obj;
|
|
42
|
+
}
|
|
43
|
+
class BaseServiceManager {
|
|
44
|
+
async restart() {
|
|
45
|
+
if (this.isRunning()) await this.stop();
|
|
46
|
+
await this.start();
|
|
47
|
+
}
|
|
48
|
+
getEndpoint() {
|
|
49
|
+
return `http://${this.host}:${this.port}`;
|
|
50
|
+
}
|
|
51
|
+
getPort() {
|
|
52
|
+
return this.port;
|
|
53
|
+
}
|
|
54
|
+
getHost() {
|
|
55
|
+
return this.host;
|
|
56
|
+
}
|
|
57
|
+
constructor(port, host = 'localhost'){
|
|
58
|
+
_define_property(this, "port", void 0);
|
|
59
|
+
_define_property(this, "host", void 0);
|
|
60
|
+
this.port = port;
|
|
61
|
+
this.host = host;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
65
|
+
const external_node_util_namespaceObject = require("node:util");
|
|
66
|
+
const constants_namespaceObject = require("@sqaitech/shared/constants");
|
|
67
|
+
const logger_namespaceObject = require("@sqaitech/shared/logger");
|
|
68
|
+
function WDAManager_define_property(obj, key, value) {
|
|
69
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
70
|
+
value: value,
|
|
71
|
+
enumerable: true,
|
|
72
|
+
configurable: true,
|
|
73
|
+
writable: true
|
|
74
|
+
});
|
|
75
|
+
else obj[key] = value;
|
|
76
|
+
return obj;
|
|
77
|
+
}
|
|
78
|
+
const execAsync = (0, external_node_util_namespaceObject.promisify)(external_node_child_process_namespaceObject.exec);
|
|
79
|
+
const debugWDA = (0, logger_namespaceObject.getDebug)('webdriver:wda-manager');
|
|
80
|
+
class WDAManager extends BaseServiceManager {
|
|
81
|
+
static getInstance(port = constants_namespaceObject.DEFAULT_WDA_PORT, host) {
|
|
82
|
+
const key = `${host || 'localhost'}:${port}`;
|
|
83
|
+
if (!WDAManager.instances.has(key)) WDAManager.instances.set(key, new WDAManager({
|
|
84
|
+
port,
|
|
85
|
+
host
|
|
86
|
+
}));
|
|
87
|
+
return WDAManager.instances.get(key);
|
|
88
|
+
}
|
|
89
|
+
async start() {
|
|
90
|
+
if (this.isStarted) return void debugWDA(`WDA already started on ${this.config.host}:${this.config.port}`);
|
|
91
|
+
try {
|
|
92
|
+
if (await this.isWDARunning()) {
|
|
93
|
+
debugWDA(`WDA already running on port ${this.config.port}`);
|
|
94
|
+
this.isStarted = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await this.startWDA();
|
|
98
|
+
await this.waitForWDA();
|
|
99
|
+
this.isStarted = true;
|
|
100
|
+
debugWDA(`WDA started successfully on ${this.config.host}:${this.config.port}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
debugWDA(`Failed to start WDA: ${error}`);
|
|
103
|
+
throw new Error(`Failed to start WebDriverAgent: ${error}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async stop() {
|
|
107
|
+
if (!this.isStarted) return;
|
|
108
|
+
try {
|
|
109
|
+
this.isStarted = false;
|
|
110
|
+
debugWDA(`WDA stopped on ${this.config.host}:${this.config.port}`);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
debugWDA(`Error stopping WDA: ${error}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
isRunning() {
|
|
116
|
+
return this.isStarted;
|
|
117
|
+
}
|
|
118
|
+
async startWDA() {
|
|
119
|
+
await this.checkWDAPreparation();
|
|
120
|
+
debugWDA('WebDriverAgent verification completed');
|
|
121
|
+
}
|
|
122
|
+
async checkWDAPreparation() {
|
|
123
|
+
if (await this.isWDARunning()) return void debugWDA(`WebDriverAgent is already running on port ${this.config.port}`);
|
|
124
|
+
throw new Error(`WebDriverAgent is not running on ${this.config.host}:${this.config.port}. Please start WebDriverAgent manually:
|
|
125
|
+
|
|
126
|
+
\u{1F527} Setup Instructions:
|
|
127
|
+
1. Install WebDriverAgent: npm install appium-webdriveragent
|
|
128
|
+
2. Build and run WebDriverAgent:
|
|
129
|
+
- For simulators: Use Xcode to run WebDriverAgentRunner on your target simulator
|
|
130
|
+
- For real devices: Build WebDriverAgentRunner and install on your device
|
|
131
|
+
3. Ensure WebDriverAgent is listening on ${this.config.host}:${this.config.port}
|
|
132
|
+
|
|
133
|
+
\u{1F4A1} Alternative: You can also specify a different host/port where WebDriverAgent is running.`);
|
|
134
|
+
}
|
|
135
|
+
async isWDARunning() {
|
|
136
|
+
try {
|
|
137
|
+
const url = `http://${this.config.host}:${this.config.port}/status`;
|
|
138
|
+
const response = await fetch(url);
|
|
139
|
+
if (!response.ok) return false;
|
|
140
|
+
const responseText = await response.text();
|
|
141
|
+
return responseText.includes('sessionId');
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async waitForWDA(timeout = 30000) {
|
|
147
|
+
const startTime = Date.now();
|
|
148
|
+
while(Date.now() - startTime < timeout){
|
|
149
|
+
if (await this.isWDARunning()) return;
|
|
150
|
+
await new Promise((resolve)=>setTimeout(resolve, 1000));
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`WebDriverAgent did not start within ${timeout}ms`);
|
|
153
|
+
}
|
|
154
|
+
async killWDAProcesses() {
|
|
155
|
+
try {
|
|
156
|
+
await execAsync('pkill -f "xcodebuild.*WebDriverAgent"').catch(()=>{});
|
|
157
|
+
await execAsync('pkill -f "WebDriverAgentRunner"').catch(()=>{});
|
|
158
|
+
debugWDA('Killed WDA processes');
|
|
159
|
+
} catch (error) {}
|
|
160
|
+
}
|
|
161
|
+
constructor(config){
|
|
162
|
+
super(config.port, config.host), WDAManager_define_property(this, "config", void 0), WDAManager_define_property(this, "isStarted", false);
|
|
163
|
+
this.config = {
|
|
164
|
+
bundleId: 'com.apple.WebDriverAgentRunner.xctrunner',
|
|
165
|
+
usePrebuiltWDA: true,
|
|
166
|
+
host: 'localhost',
|
|
167
|
+
...config,
|
|
168
|
+
port: config.port || constants_namespaceObject.DEFAULT_WDA_PORT
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
WDAManager_define_property(WDAManager, "instances", new Map());
|
|
173
|
+
function request_define_property(obj, key, value) {
|
|
174
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
175
|
+
value: value,
|
|
176
|
+
enumerable: true,
|
|
177
|
+
configurable: true,
|
|
178
|
+
writable: true
|
|
179
|
+
});
|
|
180
|
+
else obj[key] = value;
|
|
181
|
+
return obj;
|
|
182
|
+
}
|
|
183
|
+
const debugRequest = (0, logger_namespaceObject.getDebug)('webdriver:request');
|
|
184
|
+
class WebDriverRequestError extends Error {
|
|
185
|
+
constructor(message, status, response){
|
|
186
|
+
super(message), request_define_property(this, "status", void 0), request_define_property(this, "response", void 0), this.status = status, this.response = response;
|
|
187
|
+
this.name = 'WebDriverRequestError';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function makeWebDriverRequest(baseUrl, method, endpoint, data, timeout = 30000) {
|
|
191
|
+
const url = `${baseUrl}${endpoint}`;
|
|
192
|
+
debugRequest(`${method} ${url}${data ? ` with data: ${JSON.stringify(data)}` : ''}`);
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
const timeoutId = setTimeout(()=>controller.abort(), timeout);
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(url, {
|
|
197
|
+
method,
|
|
198
|
+
headers: {
|
|
199
|
+
'Content-Type': 'application/json',
|
|
200
|
+
Accept: 'application/json'
|
|
201
|
+
},
|
|
202
|
+
body: data ? JSON.stringify(data) : void 0,
|
|
203
|
+
signal: controller.signal
|
|
204
|
+
});
|
|
205
|
+
clearTimeout(timeoutId);
|
|
206
|
+
let responseData;
|
|
207
|
+
const contentType = response.headers.get('content-type');
|
|
208
|
+
if (null == contentType ? void 0 : contentType.includes('application/json')) responseData = await response.json();
|
|
209
|
+
else {
|
|
210
|
+
const textData = await response.text();
|
|
211
|
+
responseData = textData;
|
|
212
|
+
}
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
const errorMessage = (null == responseData ? void 0 : responseData.error) || (null == responseData ? void 0 : responseData.message) || `HTTP ${response.status}`;
|
|
215
|
+
throw new WebDriverRequestError(`WebDriver request failed: ${errorMessage}`, response.status, responseData);
|
|
216
|
+
}
|
|
217
|
+
return responseData;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
clearTimeout(timeoutId);
|
|
220
|
+
if (error instanceof WebDriverRequestError) throw error;
|
|
221
|
+
if (error instanceof Error && 'AbortError' === error.name) throw new WebDriverRequestError(`Request timeout after ${timeout}ms`);
|
|
222
|
+
debugRequest(`Request failed: ${error}`);
|
|
223
|
+
throw new WebDriverRequestError(`Request failed: ${error}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function WebDriverClient_define_property(obj, key, value) {
|
|
227
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
228
|
+
value: value,
|
|
229
|
+
enumerable: true,
|
|
230
|
+
configurable: true,
|
|
231
|
+
writable: true
|
|
232
|
+
});
|
|
233
|
+
else obj[key] = value;
|
|
234
|
+
return obj;
|
|
235
|
+
}
|
|
236
|
+
const debugClient = (0, logger_namespaceObject.getDebug)('webdriver:client');
|
|
237
|
+
class WebDriverClient {
|
|
238
|
+
get sessionInfo() {
|
|
239
|
+
if (!this.sessionId) return null;
|
|
240
|
+
return {
|
|
241
|
+
sessionId: this.sessionId,
|
|
242
|
+
capabilities: {}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async createSession(capabilities) {
|
|
246
|
+
try {
|
|
247
|
+
var _response_value, _response_value1;
|
|
248
|
+
const response = await this.makeRequest('POST', '/session', {
|
|
249
|
+
capabilities: {
|
|
250
|
+
alwaysMatch: {
|
|
251
|
+
...capabilities
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
this.sessionId = response.sessionId || (null == (_response_value = response.value) ? void 0 : _response_value.sessionId);
|
|
256
|
+
if (!this.sessionId) throw new Error('Failed to get session ID from response');
|
|
257
|
+
debugClient(`Created session: ${this.sessionId}`);
|
|
258
|
+
return {
|
|
259
|
+
sessionId: this.sessionId,
|
|
260
|
+
capabilities: response.capabilities || (null == (_response_value1 = response.value) ? void 0 : _response_value1.capabilities) || {}
|
|
261
|
+
};
|
|
262
|
+
} catch (error) {
|
|
263
|
+
debugClient(`Failed to create session: ${error}`);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async deleteSession() {
|
|
268
|
+
if (!this.sessionId) return void debugClient('No active session to delete');
|
|
269
|
+
try {
|
|
270
|
+
await this.makeRequest('DELETE', `/session/${this.sessionId}`);
|
|
271
|
+
debugClient(`Deleted session: ${this.sessionId}`);
|
|
272
|
+
this.sessionId = null;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
debugClient(`Failed to delete session: ${error}`);
|
|
275
|
+
this.sessionId = null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async takeScreenshot() {
|
|
279
|
+
this.ensureSession();
|
|
280
|
+
const response = await this.makeRequest('GET', `/session/${this.sessionId}/screenshot`);
|
|
281
|
+
return response.value || response;
|
|
282
|
+
}
|
|
283
|
+
async getWindowSize() {
|
|
284
|
+
this.ensureSession();
|
|
285
|
+
const response = await this.makeRequest('GET', `/session/${this.sessionId}/window/rect`);
|
|
286
|
+
const rect = response.value || response;
|
|
287
|
+
return {
|
|
288
|
+
width: rect.width,
|
|
289
|
+
height: rect.height
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async getDeviceInfo() {
|
|
293
|
+
try {
|
|
294
|
+
const statusResponse = await this.makeRequest('GET', '/status');
|
|
295
|
+
if (null == statusResponse ? void 0 : statusResponse.device) return {
|
|
296
|
+
udid: statusResponse.device.udid || statusResponse.device.identifier || '',
|
|
297
|
+
name: statusResponse.device.name || '',
|
|
298
|
+
model: statusResponse.device.model || statusResponse.device.productName || ''
|
|
299
|
+
};
|
|
300
|
+
return null;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
debugClient(`Failed to get device info: ${error}`);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async makeRequest(method, endpoint, data) {
|
|
307
|
+
return makeWebDriverRequest(this.baseUrl, method, endpoint, data, this.timeout);
|
|
308
|
+
}
|
|
309
|
+
ensureSession() {
|
|
310
|
+
if (!this.sessionId) throw new Error('No active WebDriver session. Call createSession() first.');
|
|
311
|
+
}
|
|
312
|
+
constructor(options = {}){
|
|
313
|
+
WebDriverClient_define_property(this, "baseUrl", void 0);
|
|
314
|
+
WebDriverClient_define_property(this, "sessionId", null);
|
|
315
|
+
WebDriverClient_define_property(this, "port", void 0);
|
|
316
|
+
WebDriverClient_define_property(this, "host", void 0);
|
|
317
|
+
WebDriverClient_define_property(this, "timeout", void 0);
|
|
318
|
+
this.port = options.port || constants_namespaceObject.DEFAULT_WDA_PORT;
|
|
319
|
+
this.host = options.host || 'localhost';
|
|
320
|
+
this.timeout = options.timeout || 30000;
|
|
321
|
+
this.baseUrl = `http://${this.host}:${this.port}`;
|
|
322
|
+
debugClient(`Initialized WebDriver client on ${this.host}:${this.port}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
exports.BaseServiceManager = __webpack_exports__.BaseServiceManager;
|
|
326
|
+
exports.WDAManager = __webpack_exports__.WDAManager;
|
|
327
|
+
exports.WebDriverClient = __webpack_exports__.WebDriverClient;
|
|
328
|
+
exports.WebDriverRequestError = __webpack_exports__.WebDriverRequestError;
|
|
329
|
+
exports.makeWebDriverRequest = __webpack_exports__.makeWebDriverRequest;
|
|
330
|
+
for(var __webpack_i__ in __webpack_exports__)if (-1 === [
|
|
331
|
+
"BaseServiceManager",
|
|
332
|
+
"WDAManager",
|
|
333
|
+
"WebDriverClient",
|
|
334
|
+
"WebDriverRequestError",
|
|
335
|
+
"makeWebDriverRequest"
|
|
336
|
+
].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
|
|
337
|
+
Object.defineProperty(exports, '__esModule', {
|
|
338
|
+
value: true
|
|
339
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { WEBDRIVER_ELEMENT_ID_KEY } from '@sqaitech/shared/constants';
|
|
2
|
+
|
|
3
|
+
export declare abstract class BaseServiceManager implements WebDriverServiceManager {
|
|
4
|
+
protected port: number;
|
|
5
|
+
protected host: string;
|
|
6
|
+
constructor(port: number, host?: string);
|
|
7
|
+
abstract start(): Promise<void>;
|
|
8
|
+
abstract stop(): Promise<void>;
|
|
9
|
+
abstract isRunning(): boolean;
|
|
10
|
+
restart(): Promise<void>;
|
|
11
|
+
getEndpoint(): string;
|
|
12
|
+
getPort(): number;
|
|
13
|
+
getHost(): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export declare interface DeviceInfo {
|
|
17
|
+
udid: string;
|
|
18
|
+
name: string;
|
|
19
|
+
model: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare function makeWebDriverRequest(baseUrl: string, method: string, endpoint: string, data?: any, timeout?: number): Promise<any>;
|
|
23
|
+
|
|
24
|
+
export declare interface Point {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare interface Size {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export declare interface WDAConfig {
|
|
35
|
+
port: number;
|
|
36
|
+
host?: string;
|
|
37
|
+
wdaPath?: string;
|
|
38
|
+
bundleId?: string;
|
|
39
|
+
usePrebuiltWDA?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export declare interface WDAElement {
|
|
43
|
+
ELEMENT: string;
|
|
44
|
+
[WEBDRIVER_ELEMENT_ID_KEY]: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export declare interface WDAElementInfo {
|
|
48
|
+
type: string;
|
|
49
|
+
name: string;
|
|
50
|
+
label: string;
|
|
51
|
+
value: string;
|
|
52
|
+
rect: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
};
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
visible: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export declare class WDAManager extends BaseServiceManager {
|
|
63
|
+
private static instances;
|
|
64
|
+
private config;
|
|
65
|
+
private isStarted;
|
|
66
|
+
private constructor();
|
|
67
|
+
static getInstance(port?: number, host?: string): WDAManager;
|
|
68
|
+
start(): Promise<void>;
|
|
69
|
+
stop(): Promise<void>;
|
|
70
|
+
isRunning(): boolean;
|
|
71
|
+
private startWDA;
|
|
72
|
+
private checkWDAPreparation;
|
|
73
|
+
private isWDARunning;
|
|
74
|
+
private waitForWDA;
|
|
75
|
+
private killWDAProcesses;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export declare interface WDASession {
|
|
79
|
+
sessionId: string;
|
|
80
|
+
capabilities: Record<string, any>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export declare class WebDriverClient {
|
|
84
|
+
protected baseUrl: string;
|
|
85
|
+
protected sessionId: string | null;
|
|
86
|
+
protected port: number;
|
|
87
|
+
protected host: string;
|
|
88
|
+
protected timeout: number;
|
|
89
|
+
constructor(options?: WebDriverOptions);
|
|
90
|
+
get sessionInfo(): WDASession | null;
|
|
91
|
+
createSession(capabilities?: any): Promise<WDASession>;
|
|
92
|
+
deleteSession(): Promise<void>;
|
|
93
|
+
takeScreenshot(): Promise<string>;
|
|
94
|
+
getWindowSize(): Promise<Size>;
|
|
95
|
+
getDeviceInfo(): Promise<DeviceInfo | null>;
|
|
96
|
+
protected makeRequest(method: string, endpoint: string, data?: any): Promise<any>;
|
|
97
|
+
protected ensureSession(): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export declare interface WebDriverOptions {
|
|
101
|
+
port?: number;
|
|
102
|
+
host?: string;
|
|
103
|
+
timeout?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export declare class WebDriverRequestError extends Error {
|
|
107
|
+
status?: number | undefined;
|
|
108
|
+
response?: any | undefined;
|
|
109
|
+
constructor(message: string, status?: number | undefined, response?: any | undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export declare interface WebDriverServiceManager {
|
|
113
|
+
start(): Promise<void>;
|
|
114
|
+
stop(): Promise<void>;
|
|
115
|
+
restart(): Promise<void>;
|
|
116
|
+
isRunning(): boolean;
|
|
117
|
+
getEndpoint(): string;
|
|
118
|
+
getPort(): number;
|
|
119
|
+
getHost(): string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { }
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sqaitech/webdriver",
|
|
3
|
+
"version": "0.30.10",
|
|
4
|
+
"description": "WebDriver protocol implementation for SQAI automation",
|
|
5
|
+
"main": "dist/lib/index.js",
|
|
6
|
+
"module": "dist/es/index.mjs",
|
|
7
|
+
"types": "dist/types/index.d.ts",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"node-fetch": "^3.3.2",
|
|
10
|
+
"@sqaitech/shared": "0.30.10"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@rslib/core": "^0.11.2",
|
|
14
|
+
"@types/node": "^18.0.0",
|
|
15
|
+
"typescript": "^5.8.3",
|
|
16
|
+
"vitest": "3.0.5"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/types/index.d.ts",
|
|
21
|
+
"import": "./dist/es/index.mjs",
|
|
22
|
+
"require": "./dist/lib/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"webdriver",
|
|
30
|
+
"automation",
|
|
31
|
+
"ios",
|
|
32
|
+
"mobile"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "rslib build",
|
|
36
|
+
"dev": "rslib build --watch",
|
|
37
|
+
"test": "vitest",
|
|
38
|
+
"lint": "biome check .",
|
|
39
|
+
"lint:fix": "biome check . --write"
|
|
40
|
+
}
|
|
41
|
+
}
|