playwright-automation-core 1.0.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/LICENSE +21 -0
- package/README.md +225 -0
- package/dist/api/api.assertions.d.ts +196 -0
- package/dist/api/api.assertions.d.ts.map +1 -0
- package/dist/api/api.assertions.js +462 -0
- package/dist/api/api.assertions.js.map +1 -0
- package/dist/api/api.builder.d.ts +134 -0
- package/dist/api/api.builder.d.ts.map +1 -0
- package/dist/api/api.builder.js +273 -0
- package/dist/api/api.builder.js.map +1 -0
- package/dist/api/api.client.d.ts +157 -0
- package/dist/api/api.client.d.ts.map +1 -0
- package/dist/api/api.client.js +493 -0
- package/dist/api/api.client.js.map +1 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +17 -0
- package/dist/api/index.js.map +1 -0
- package/dist/core/base.driver.d.ts +130 -0
- package/dist/core/base.driver.d.ts.map +1 -0
- package/dist/core/base.driver.js +264 -0
- package/dist/core/base.driver.js.map +1 -0
- package/dist/core/driver.factory.d.ts +120 -0
- package/dist/core/driver.factory.d.ts.map +1 -0
- package/dist/core/driver.factory.js +222 -0
- package/dist/core/driver.factory.js.map +1 -0
- package/dist/core/element.actions.d.ts +119 -0
- package/dist/core/element.actions.d.ts.map +1 -0
- package/dist/core/element.actions.js +379 -0
- package/dist/core/element.actions.js.map +1 -0
- package/dist/core/hook.manager.d.ts +186 -0
- package/dist/core/hook.manager.d.ts.map +1 -0
- package/dist/core/hook.manager.js +297 -0
- package/dist/core/hook.manager.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +22 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/dist/mobile/index.d.ts +7 -0
- package/dist/mobile/index.d.ts.map +1 -0
- package/dist/mobile/index.js +14 -0
- package/dist/mobile/index.js.map +1 -0
- package/dist/mobile/mobile.assertions.d.ts +146 -0
- package/dist/mobile/mobile.assertions.d.ts.map +1 -0
- package/dist/mobile/mobile.assertions.js +333 -0
- package/dist/mobile/mobile.assertions.js.map +1 -0
- package/dist/mobile/mobile.driver.d.ts +297 -0
- package/dist/mobile/mobile.driver.d.ts.map +1 -0
- package/dist/mobile/mobile.driver.js +808 -0
- package/dist/mobile/mobile.driver.js.map +1 -0
- package/dist/types/common.types.d.ts +200 -0
- package/dist/types/common.types.d.ts.map +1 -0
- package/dist/types/common.types.js +3 -0
- package/dist/types/common.types.js.map +1 -0
- package/dist/types/config.types.d.ts +192 -0
- package/dist/types/config.types.d.ts.map +1 -0
- package/dist/types/config.types.js +3 -0
- package/dist/types/config.types.js.map +1 -0
- package/dist/types/driver.types.d.ts +500 -0
- package/dist/types/driver.types.d.ts.map +1 -0
- package/dist/types/driver.types.js +3 -0
- package/dist/types/driver.types.js.map +1 -0
- package/dist/types/enums.d.ts +53 -0
- package/dist/types/enums.d.ts.map +1 -0
- package/dist/types/enums.js +62 -0
- package/dist/types/enums.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +15 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/config.manager.d.ts +144 -0
- package/dist/utils/config.manager.d.ts.map +1 -0
- package/dist/utils/config.manager.js +473 -0
- package/dist/utils/config.manager.js.map +1 -0
- package/dist/utils/errors.d.ts +149 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +290 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/helpers.d.ts +124 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +421 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +78 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +145 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +235 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/wait.utils.d.ts +95 -0
- package/dist/utils/wait.utils.d.ts.map +1 -0
- package/dist/utils/wait.utils.js +306 -0
- package/dist/utils/wait.utils.js.map +1 -0
- package/dist/web/index.d.ts +8 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +16 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/web.assertions.d.ts +164 -0
- package/dist/web/web.assertions.d.ts.map +1 -0
- package/dist/web/web.assertions.js +427 -0
- package/dist/web/web.assertions.js.map +1 -0
- package/dist/web/web.driver.d.ts +174 -0
- package/dist/web/web.driver.d.ts.map +1 -0
- package/dist/web/web.driver.js +571 -0
- package/dist/web/web.driver.js.map +1 -0
- package/dist/web/web.page.d.ts +286 -0
- package/dist/web/web.page.d.ts.map +1 -0
- package/dist/web/web.page.js +570 -0
- package/dist/web/web.page.js.map +1 -0
- package/package.json +115 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MobileDriver = exports.AppState = void 0;
|
|
4
|
+
const webdriverio_1 = require("webdriverio");
|
|
5
|
+
const types_1 = require("../types");
|
|
6
|
+
const base_driver_1 = require("../core/base.driver");
|
|
7
|
+
const hook_manager_1 = require("../core/hook.manager");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
/**
|
|
10
|
+
* App state enum
|
|
11
|
+
*/
|
|
12
|
+
var AppState;
|
|
13
|
+
(function (AppState) {
|
|
14
|
+
AppState[AppState["NOT_INSTALLED"] = 0] = "NOT_INSTALLED";
|
|
15
|
+
AppState[AppState["NOT_RUNNING"] = 1] = "NOT_RUNNING";
|
|
16
|
+
AppState[AppState["RUNNING_BACKGROUND_SUSPENDED"] = 2] = "RUNNING_BACKGROUND_SUSPENDED";
|
|
17
|
+
AppState[AppState["RUNNING_BACKGROUND"] = 3] = "RUNNING_BACKGROUND";
|
|
18
|
+
AppState[AppState["RUNNING_FOREGROUND"] = 4] = "RUNNING_FOREGROUND";
|
|
19
|
+
})(AppState || (exports.AppState = AppState = {}));
|
|
20
|
+
/**
|
|
21
|
+
* Mobile Driver implementation using WebdriverIO with Appium.
|
|
22
|
+
* Supports both Android and iOS platforms.
|
|
23
|
+
*/
|
|
24
|
+
class MobileDriver extends base_driver_1.BaseDriver {
|
|
25
|
+
/** WebdriverIO browser/driver instance */
|
|
26
|
+
driver = null;
|
|
27
|
+
/** Mobile-specific configuration */
|
|
28
|
+
config;
|
|
29
|
+
/** Hook manager */
|
|
30
|
+
hooks;
|
|
31
|
+
/** Current context (NATIVE_APP or WEBVIEW_xxx) */
|
|
32
|
+
currentContext = "NATIVE_APP";
|
|
33
|
+
/**
|
|
34
|
+
* Create a new MobileDriver instance
|
|
35
|
+
*/
|
|
36
|
+
constructor(config, logger) {
|
|
37
|
+
super(types_1.Platform.MOBILE, config, logger);
|
|
38
|
+
this.config = config;
|
|
39
|
+
this.hooks = (0, hook_manager_1.getHookManager)();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Initialize the mobile driver
|
|
43
|
+
*/
|
|
44
|
+
async initialize() {
|
|
45
|
+
if (this.initialized) {
|
|
46
|
+
this.logger.warn("MobileDriver already initialized");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Execute before init hooks
|
|
50
|
+
await this.hooks.execute(hook_manager_1.HookEvent.BEFORE_DRIVER_INIT, {
|
|
51
|
+
platform: this.platform,
|
|
52
|
+
data: { config: this.getSafeConfig() },
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
this.logger.info("Initializing MobileDriver", {
|
|
56
|
+
mobilePlatform: this.config.platform,
|
|
57
|
+
deviceName: this.config.deviceName,
|
|
58
|
+
});
|
|
59
|
+
// Build capabilities
|
|
60
|
+
const capabilities = this.buildCapabilities();
|
|
61
|
+
// Create WebdriverIO remote session
|
|
62
|
+
this.driver = await (0, webdriverio_1.remote)({
|
|
63
|
+
hostname: this.config.appiumHost || "localhost",
|
|
64
|
+
port: this.config.appiumPort || 4723,
|
|
65
|
+
path: this.config.appiumPath || "/wd/hub",
|
|
66
|
+
capabilities,
|
|
67
|
+
logLevel: "warn",
|
|
68
|
+
connectionRetryTimeout: this.config.timeout || 60000,
|
|
69
|
+
connectionRetryCount: 3,
|
|
70
|
+
});
|
|
71
|
+
// Generate session ID and mark initialized
|
|
72
|
+
this.generateSessionId();
|
|
73
|
+
this.markInitialized();
|
|
74
|
+
// Execute after init hooks
|
|
75
|
+
await this.hooks.execute(hook_manager_1.HookEvent.AFTER_DRIVER_INIT, {
|
|
76
|
+
platform: this.platform,
|
|
77
|
+
driverStatus: this.getStatus(),
|
|
78
|
+
data: {
|
|
79
|
+
mobilePlatform: this.config.platform,
|
|
80
|
+
deviceName: this.config.deviceName,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const err = error;
|
|
86
|
+
this.logger.error("Failed to initialize MobileDriver", {
|
|
87
|
+
error: err.message,
|
|
88
|
+
});
|
|
89
|
+
throw new utils_1.DriverInitializationError(this.platform, `Failed to initialize mobile driver: ${err.message}`, {
|
|
90
|
+
mobilePlatform: this.config.platform,
|
|
91
|
+
deviceName: this.config.deviceName,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build Appium capabilities from configuration
|
|
97
|
+
*/
|
|
98
|
+
buildCapabilities() {
|
|
99
|
+
const isIOS = this.config.platform === types_1.MobilePlatform.IOS;
|
|
100
|
+
const caps = {
|
|
101
|
+
platformName: isIOS ? "iOS" : "Android",
|
|
102
|
+
"appium:deviceName": this.config.deviceName,
|
|
103
|
+
"appium:automationName": this.config.automationName || (isIOS ? "XCUITest" : "UiAutomator2"),
|
|
104
|
+
"appium:newCommandTimeout": this.config.newCommandTimeout || 300,
|
|
105
|
+
};
|
|
106
|
+
// Add app capability
|
|
107
|
+
if (this.config.app) {
|
|
108
|
+
caps["appium:app"] = this.config.app;
|
|
109
|
+
}
|
|
110
|
+
// Platform-specific capabilities
|
|
111
|
+
if (isIOS) {
|
|
112
|
+
if (this.config.platformVersion) {
|
|
113
|
+
caps["appium:platformVersion"] = this.config.platformVersion;
|
|
114
|
+
}
|
|
115
|
+
if (this.config.udid) {
|
|
116
|
+
caps["appium:udid"] = this.config.udid;
|
|
117
|
+
}
|
|
118
|
+
if (this.config.bundleId) {
|
|
119
|
+
caps["appium:bundleId"] = this.config.bundleId;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Android
|
|
124
|
+
if (this.config.appPackage) {
|
|
125
|
+
caps["appium:appPackage"] = this.config.appPackage;
|
|
126
|
+
}
|
|
127
|
+
if (this.config.appActivity) {
|
|
128
|
+
caps["appium:appActivity"] = this.config.appActivity;
|
|
129
|
+
}
|
|
130
|
+
if (this.config.avd) {
|
|
131
|
+
caps["appium:avd"] = this.config.avd;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Additional capabilities
|
|
135
|
+
if (this.config.noReset !== undefined) {
|
|
136
|
+
caps["appium:noReset"] = this.config.noReset;
|
|
137
|
+
}
|
|
138
|
+
if (this.config.fullReset !== undefined) {
|
|
139
|
+
caps["appium:fullReset"] = this.config.fullReset;
|
|
140
|
+
}
|
|
141
|
+
// Merge additional capabilities
|
|
142
|
+
if (this.config.additionalCapabilities) {
|
|
143
|
+
Object.assign(caps, this.config.additionalCapabilities);
|
|
144
|
+
}
|
|
145
|
+
return caps;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get the underlying WebdriverIO driver
|
|
149
|
+
*/
|
|
150
|
+
getDriver() {
|
|
151
|
+
this.ensureInitialized("getDriver");
|
|
152
|
+
return this.driver;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get browser (alias for getDriver to match interface)
|
|
156
|
+
*/
|
|
157
|
+
getBrowser() {
|
|
158
|
+
return this.getDriver();
|
|
159
|
+
}
|
|
160
|
+
// ============ Element Finding ============
|
|
161
|
+
/**
|
|
162
|
+
* Find element using locator
|
|
163
|
+
*/
|
|
164
|
+
async findElement(locator) {
|
|
165
|
+
this.ensureInitialized("findElement");
|
|
166
|
+
const selector = this.buildSelector(locator);
|
|
167
|
+
return await this.driver.$(selector);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Find multiple elements using locator
|
|
171
|
+
*/
|
|
172
|
+
async findElements(locator) {
|
|
173
|
+
this.ensureInitialized("findElements");
|
|
174
|
+
const selector = this.buildSelector(locator);
|
|
175
|
+
const elements = await this.driver.$$(selector);
|
|
176
|
+
// Convert ElementArray to regular array
|
|
177
|
+
return Array.from(elements);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Build selector string from locator
|
|
181
|
+
*/
|
|
182
|
+
buildSelector(locator) {
|
|
183
|
+
if (typeof locator === "string") {
|
|
184
|
+
return locator;
|
|
185
|
+
}
|
|
186
|
+
const isIOS = this.config.platform === types_1.MobilePlatform.IOS;
|
|
187
|
+
switch (locator.strategy) {
|
|
188
|
+
case "accessibility":
|
|
189
|
+
return `~${locator.value}`;
|
|
190
|
+
case "id":
|
|
191
|
+
return isIOS
|
|
192
|
+
? `~${locator.value}`
|
|
193
|
+
: `android=new UiSelector().resourceId("${locator.value}")`;
|
|
194
|
+
case "xpath":
|
|
195
|
+
return locator.value;
|
|
196
|
+
case "class":
|
|
197
|
+
return isIOS
|
|
198
|
+
? `-ios class chain:**/${locator.value}`
|
|
199
|
+
: `android=new UiSelector().className("${locator.value}")`;
|
|
200
|
+
case "name":
|
|
201
|
+
return isIOS
|
|
202
|
+
? `-ios predicate string:name == "${locator.value}"`
|
|
203
|
+
: `android=new UiSelector().text("${locator.value}")`;
|
|
204
|
+
case "text":
|
|
205
|
+
return isIOS
|
|
206
|
+
? `-ios predicate string:label == "${locator.value}"`
|
|
207
|
+
: `android=new UiSelector().text("${locator.value}")`;
|
|
208
|
+
default:
|
|
209
|
+
return locator.value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// ============ Element Actions ============
|
|
213
|
+
/**
|
|
214
|
+
* Click on element
|
|
215
|
+
*/
|
|
216
|
+
async click(locator, _options) {
|
|
217
|
+
this.ensureInitialized("click");
|
|
218
|
+
const description = typeof locator === "string" ? locator : locator.value;
|
|
219
|
+
this.logger.elementAction("click", description);
|
|
220
|
+
const element = await this.findElement(locator);
|
|
221
|
+
await element.click();
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Tap at coordinates
|
|
225
|
+
*/
|
|
226
|
+
async tap(x, y) {
|
|
227
|
+
this.ensureInitialized("tap");
|
|
228
|
+
this.logger.debug(`Tap at coordinates: (${x}, ${y})`);
|
|
229
|
+
await this.driver.action("pointer", {
|
|
230
|
+
parameters: { pointerType: "touch" },
|
|
231
|
+
})
|
|
232
|
+
.move({ x, y })
|
|
233
|
+
.down()
|
|
234
|
+
.up()
|
|
235
|
+
.perform();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Tap on element (convenience method)
|
|
239
|
+
*/
|
|
240
|
+
async tapElement(locator) {
|
|
241
|
+
await this.click(locator);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Double tap on element
|
|
245
|
+
*/
|
|
246
|
+
async doubleTap(locator) {
|
|
247
|
+
this.ensureInitialized("doubleTap");
|
|
248
|
+
const description = typeof locator === "string" ? locator : locator.value;
|
|
249
|
+
this.logger.elementAction("doubleTap", description);
|
|
250
|
+
const element = await this.findElement(locator);
|
|
251
|
+
await element.doubleClick();
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Long press on element
|
|
255
|
+
*/
|
|
256
|
+
async longPress(locator, duration = 1000) {
|
|
257
|
+
this.ensureInitialized("longPress");
|
|
258
|
+
const description = typeof locator === "string" ? locator : locator.value;
|
|
259
|
+
this.logger.elementAction("longPress", description);
|
|
260
|
+
const element = await this.findElement(locator);
|
|
261
|
+
const location = await element.getLocation();
|
|
262
|
+
const size = await element.getSize();
|
|
263
|
+
const centerX = location.x + size.width / 2;
|
|
264
|
+
const centerY = location.y + size.height / 2;
|
|
265
|
+
await this.driver.action("pointer", {
|
|
266
|
+
parameters: { pointerType: "touch" },
|
|
267
|
+
})
|
|
268
|
+
.move({ x: Math.round(centerX), y: Math.round(centerY) })
|
|
269
|
+
.down()
|
|
270
|
+
.pause(duration)
|
|
271
|
+
.up()
|
|
272
|
+
.perform();
|
|
273
|
+
}
|
|
274
|
+
// ============ Swipe Actions ============
|
|
275
|
+
/**
|
|
276
|
+
* Swipe in direction
|
|
277
|
+
*/
|
|
278
|
+
async swipe(direction, options) {
|
|
279
|
+
this.ensureInitialized("swipe");
|
|
280
|
+
this.logger.debug(`Swipe ${direction}`);
|
|
281
|
+
const { width, height } = await this.getWindowSize();
|
|
282
|
+
const duration = options?.duration || 300;
|
|
283
|
+
let startX, startY, endX, endY;
|
|
284
|
+
switch (direction) {
|
|
285
|
+
case "up":
|
|
286
|
+
startX = Math.round(width / 2);
|
|
287
|
+
startY = Math.round(height * 0.7);
|
|
288
|
+
endX = Math.round(width / 2);
|
|
289
|
+
endY = Math.round(height * 0.3);
|
|
290
|
+
break;
|
|
291
|
+
case "down":
|
|
292
|
+
startX = Math.round(width / 2);
|
|
293
|
+
startY = Math.round(height * 0.3);
|
|
294
|
+
endX = Math.round(width / 2);
|
|
295
|
+
endY = Math.round(height * 0.7);
|
|
296
|
+
break;
|
|
297
|
+
case "left":
|
|
298
|
+
startX = Math.round(width * 0.8);
|
|
299
|
+
startY = Math.round(height / 2);
|
|
300
|
+
endX = Math.round(width * 0.2);
|
|
301
|
+
endY = Math.round(height / 2);
|
|
302
|
+
break;
|
|
303
|
+
case "right":
|
|
304
|
+
startX = Math.round(width * 0.2);
|
|
305
|
+
startY = Math.round(height / 2);
|
|
306
|
+
endX = Math.round(width * 0.8);
|
|
307
|
+
endY = Math.round(height / 2);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
await this.driver.action("pointer", {
|
|
311
|
+
parameters: { pointerType: "touch" },
|
|
312
|
+
})
|
|
313
|
+
.move({ x: startX, y: startY })
|
|
314
|
+
.down()
|
|
315
|
+
.pause(50)
|
|
316
|
+
.move({ x: endX, y: endY, duration })
|
|
317
|
+
.up()
|
|
318
|
+
.perform();
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Swipe with specific coordinates
|
|
322
|
+
*/
|
|
323
|
+
async swipeCoordinates(startX, startY, endX, endY, duration = 300) {
|
|
324
|
+
this.ensureInitialized("swipeCoordinates");
|
|
325
|
+
await this.driver.action("pointer", {
|
|
326
|
+
parameters: { pointerType: "touch" },
|
|
327
|
+
})
|
|
328
|
+
.move({ x: startX, y: startY })
|
|
329
|
+
.down()
|
|
330
|
+
.pause(50)
|
|
331
|
+
.move({ x: endX, y: endY, duration })
|
|
332
|
+
.up()
|
|
333
|
+
.perform();
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Scroll to element
|
|
337
|
+
*/
|
|
338
|
+
async scrollToElement(locator) {
|
|
339
|
+
this.ensureInitialized("scrollToElement");
|
|
340
|
+
const description = typeof locator === "string" ? locator : locator.value;
|
|
341
|
+
this.logger.debug(`Scrolling to element: ${description}`);
|
|
342
|
+
const maxScrolls = 5;
|
|
343
|
+
for (let i = 0; i < maxScrolls; i++) {
|
|
344
|
+
try {
|
|
345
|
+
const element = await this.findElement(locator);
|
|
346
|
+
if (await element.isDisplayed()) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// Element not found, continue scrolling
|
|
352
|
+
}
|
|
353
|
+
await this.swipe("up");
|
|
354
|
+
}
|
|
355
|
+
throw new utils_1.MobileOperationError("scrollToElement", types_1.Platform.MOBILE, `Element not found after ${maxScrolls} scrolls: ${description}`, { locator: description });
|
|
356
|
+
}
|
|
357
|
+
// ============ Input Actions ============
|
|
358
|
+
/**
|
|
359
|
+
* Type text into element
|
|
360
|
+
*/
|
|
361
|
+
async type(locator, text, _options) {
|
|
362
|
+
this.ensureInitialized("type");
|
|
363
|
+
const description = typeof locator === "string" ? locator : locator.value;
|
|
364
|
+
this.logger.elementAction("type", description);
|
|
365
|
+
const element = await this.findElement(locator);
|
|
366
|
+
await element.setValue(text);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Clear element text
|
|
370
|
+
*/
|
|
371
|
+
async clear(locator, _options) {
|
|
372
|
+
this.ensureInitialized("clear");
|
|
373
|
+
const description = typeof locator === "string" ? locator : locator.value;
|
|
374
|
+
this.logger.elementAction("clear", description);
|
|
375
|
+
const element = await this.findElement(locator);
|
|
376
|
+
await element.clearValue();
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get element text
|
|
380
|
+
*/
|
|
381
|
+
async getText(locator, _options) {
|
|
382
|
+
this.ensureInitialized("getText");
|
|
383
|
+
const element = await this.findElement(locator);
|
|
384
|
+
return await element.getText();
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get element attribute
|
|
388
|
+
*/
|
|
389
|
+
async getAttribute(locator, attribute) {
|
|
390
|
+
this.ensureInitialized("getAttribute");
|
|
391
|
+
const element = await this.findElement(locator);
|
|
392
|
+
return await element.getAttribute(attribute);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Check if element is displayed
|
|
396
|
+
*/
|
|
397
|
+
async isDisplayed(locator) {
|
|
398
|
+
this.ensureInitialized("isDisplayed");
|
|
399
|
+
try {
|
|
400
|
+
const element = await this.findElement(locator);
|
|
401
|
+
return await element.isDisplayed();
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Check if element is enabled
|
|
409
|
+
*/
|
|
410
|
+
async isEnabled(locator) {
|
|
411
|
+
this.ensureInitialized("isEnabled");
|
|
412
|
+
try {
|
|
413
|
+
const element = await this.findElement(locator);
|
|
414
|
+
return await element.isEnabled();
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Wait for element to be visible
|
|
422
|
+
*/
|
|
423
|
+
async waitForVisible(locator, options) {
|
|
424
|
+
this.ensureInitialized("waitForVisible");
|
|
425
|
+
const element = await this.findElement(locator);
|
|
426
|
+
await element.waitForDisplayed({
|
|
427
|
+
timeout: options?.timeout || this.getTimeout(),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Wait for element to be clickable
|
|
432
|
+
*/
|
|
433
|
+
async waitForClickable(locator, options) {
|
|
434
|
+
this.ensureInitialized("waitForClickable");
|
|
435
|
+
const element = await this.findElement(locator);
|
|
436
|
+
await element.waitForClickable({
|
|
437
|
+
timeout: options?.timeout || this.getTimeout(),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Wait for element to be displayed (same as waitForVisible)
|
|
442
|
+
*/
|
|
443
|
+
async waitForDisplayed(locator, timeout) {
|
|
444
|
+
this.ensureInitialized("waitForDisplayed");
|
|
445
|
+
const element = await this.findElement(locator);
|
|
446
|
+
await element.waitForDisplayed({
|
|
447
|
+
timeout: timeout || this.getTimeout(),
|
|
448
|
+
});
|
|
449
|
+
return element;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Wait for element to not be displayed
|
|
453
|
+
*/
|
|
454
|
+
async waitForNotDisplayed(locator, timeout) {
|
|
455
|
+
this.ensureInitialized("waitForNotDisplayed");
|
|
456
|
+
const element = await this.findElement(locator);
|
|
457
|
+
await element.waitForDisplayed({
|
|
458
|
+
timeout: timeout || this.getTimeout(),
|
|
459
|
+
reverse: true,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// ============ Context Handling ============
|
|
463
|
+
/**
|
|
464
|
+
* Get available contexts
|
|
465
|
+
*/
|
|
466
|
+
async getContexts() {
|
|
467
|
+
this.ensureInitialized("getContexts");
|
|
468
|
+
return (await this.driver.getContexts());
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Get current context
|
|
472
|
+
*/
|
|
473
|
+
async getContext() {
|
|
474
|
+
this.ensureInitialized("getContext");
|
|
475
|
+
this.currentContext = (await this.driver.getContext());
|
|
476
|
+
return this.currentContext;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Switch to context
|
|
480
|
+
*/
|
|
481
|
+
async switchContext(context) {
|
|
482
|
+
this.ensureInitialized("switchContext");
|
|
483
|
+
this.logger.debug(`Switching to context: ${context}`);
|
|
484
|
+
await this.driver.switchContext(context);
|
|
485
|
+
this.currentContext = context;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Switch to native context
|
|
489
|
+
*/
|
|
490
|
+
async switchToNative() {
|
|
491
|
+
await this.switchContext("NATIVE_APP");
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Switch to webview context
|
|
495
|
+
*/
|
|
496
|
+
async switchToWebview() {
|
|
497
|
+
this.ensureInitialized("switchToWebview");
|
|
498
|
+
const contexts = await this.getContexts();
|
|
499
|
+
const webviews = contexts.filter((c) => c.includes("WEBVIEW"));
|
|
500
|
+
if (webviews.length === 0) {
|
|
501
|
+
throw new utils_1.MobileOperationError("switchToWebview", types_1.Platform.MOBILE, "No webview contexts available");
|
|
502
|
+
}
|
|
503
|
+
await this.switchContext(webviews[0]);
|
|
504
|
+
}
|
|
505
|
+
// ============ App Management ============
|
|
506
|
+
/**
|
|
507
|
+
* Install app
|
|
508
|
+
*/
|
|
509
|
+
async installApp(appPath) {
|
|
510
|
+
this.ensureInitialized("installApp");
|
|
511
|
+
this.logger.info(`Installing app: ${appPath}`);
|
|
512
|
+
await this.driver.installApp(appPath);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Remove app
|
|
516
|
+
*/
|
|
517
|
+
async removeApp(appId) {
|
|
518
|
+
this.ensureInitialized("removeApp");
|
|
519
|
+
this.logger.info(`Removing app: ${appId}`);
|
|
520
|
+
await this.driver.removeApp(appId);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Check if app is installed
|
|
524
|
+
*/
|
|
525
|
+
async isAppInstalled(appId) {
|
|
526
|
+
this.ensureInitialized("isAppInstalled");
|
|
527
|
+
return await this.driver.isAppInstalled(appId);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Launch app
|
|
531
|
+
*/
|
|
532
|
+
async launchApp() {
|
|
533
|
+
this.ensureInitialized("launchApp");
|
|
534
|
+
this.logger.info("Launching app");
|
|
535
|
+
await this.driver.execute("mobile: launchApp", {});
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Close/terminate app
|
|
539
|
+
*/
|
|
540
|
+
async closeApp() {
|
|
541
|
+
this.ensureInitialized("closeApp");
|
|
542
|
+
const appId = this.config.appPackage || this.config.bundleId;
|
|
543
|
+
if (!appId) {
|
|
544
|
+
throw new utils_1.MobileOperationError("closeApp", types_1.Platform.MOBILE, "App ID not provided");
|
|
545
|
+
}
|
|
546
|
+
this.logger.info(`Closing app: ${appId}`);
|
|
547
|
+
await this.driver.terminateApp(appId, {});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Terminate app (alias for closeApp)
|
|
551
|
+
*/
|
|
552
|
+
async terminateApp(appId) {
|
|
553
|
+
this.ensureInitialized("terminateApp");
|
|
554
|
+
const id = appId || this.config.appPackage || this.config.bundleId;
|
|
555
|
+
if (!id) {
|
|
556
|
+
throw new utils_1.MobileOperationError("terminateApp", types_1.Platform.MOBILE, "App ID not provided");
|
|
557
|
+
}
|
|
558
|
+
this.logger.info(`Terminating app: ${id}`);
|
|
559
|
+
await this.driver.terminateApp(id, {});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Activate app (bring to foreground)
|
|
563
|
+
*/
|
|
564
|
+
async activateApp(appId) {
|
|
565
|
+
this.ensureInitialized("activateApp");
|
|
566
|
+
const id = appId || this.config.appPackage || this.config.bundleId;
|
|
567
|
+
if (!id) {
|
|
568
|
+
throw new utils_1.MobileOperationError("activateApp", types_1.Platform.MOBILE, "App ID not provided");
|
|
569
|
+
}
|
|
570
|
+
this.logger.info(`Activating app: ${id}`);
|
|
571
|
+
await this.driver.activateApp(id);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Get app state
|
|
575
|
+
*/
|
|
576
|
+
async getAppState(appId) {
|
|
577
|
+
this.ensureInitialized("getAppState");
|
|
578
|
+
const id = appId || this.config.appPackage || this.config.bundleId;
|
|
579
|
+
if (!id) {
|
|
580
|
+
throw new utils_1.MobileOperationError("getAppState", types_1.Platform.MOBILE, "App ID not provided");
|
|
581
|
+
}
|
|
582
|
+
return (await this.driver.queryAppState(id));
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Background app for duration
|
|
586
|
+
*/
|
|
587
|
+
async backgroundApp(seconds = -1) {
|
|
588
|
+
this.ensureInitialized("backgroundApp");
|
|
589
|
+
this.logger.info(`Backgrounding app for ${seconds} seconds`);
|
|
590
|
+
await this.driver.background(seconds);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Reset app (clear data)
|
|
594
|
+
*/
|
|
595
|
+
async resetApp() {
|
|
596
|
+
this.ensureInitialized("resetApp");
|
|
597
|
+
this.logger.info("Resetting app");
|
|
598
|
+
const appId = this.config.appPackage || this.config.bundleId;
|
|
599
|
+
if (appId) {
|
|
600
|
+
await this.terminateApp(appId);
|
|
601
|
+
await this.activateApp(appId);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// ============ Device Actions ============
|
|
605
|
+
/**
|
|
606
|
+
* Get window/screen size
|
|
607
|
+
*/
|
|
608
|
+
async getWindowSize() {
|
|
609
|
+
this.ensureInitialized("getWindowSize");
|
|
610
|
+
return await this.driver.getWindowSize();
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Get device orientation
|
|
614
|
+
*/
|
|
615
|
+
async getOrientation() {
|
|
616
|
+
this.ensureInitialized("getOrientation");
|
|
617
|
+
const orientation = await this.driver.getOrientation();
|
|
618
|
+
return orientation.toUpperCase();
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Set device orientation
|
|
622
|
+
*/
|
|
623
|
+
async setOrientation(orientation) {
|
|
624
|
+
this.ensureInitialized("setOrientation");
|
|
625
|
+
this.logger.debug(`Setting orientation: ${orientation}`);
|
|
626
|
+
await this.driver.setOrientation(orientation);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Rotate device
|
|
630
|
+
*/
|
|
631
|
+
async rotate() {
|
|
632
|
+
this.ensureInitialized("rotate");
|
|
633
|
+
const current = await this.getOrientation();
|
|
634
|
+
const next = current === "PORTRAIT" ? "LANDSCAPE" : "PORTRAIT";
|
|
635
|
+
await this.setOrientation(next);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Lock device
|
|
639
|
+
*/
|
|
640
|
+
async lock(seconds) {
|
|
641
|
+
this.ensureInitialized("lock");
|
|
642
|
+
this.logger.debug("Locking device");
|
|
643
|
+
await this.driver.lock(seconds);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Unlock device
|
|
647
|
+
*/
|
|
648
|
+
async unlock() {
|
|
649
|
+
this.ensureInitialized("unlock");
|
|
650
|
+
this.logger.debug("Unlocking device");
|
|
651
|
+
await this.driver.unlock();
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Check if device is locked
|
|
655
|
+
*/
|
|
656
|
+
async isLocked() {
|
|
657
|
+
this.ensureInitialized("isLocked");
|
|
658
|
+
return await this.driver.isLocked();
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Press hardware back button (Android only)
|
|
662
|
+
*/
|
|
663
|
+
async back() {
|
|
664
|
+
this.ensureInitialized("back");
|
|
665
|
+
if (this.config.platform !== types_1.MobilePlatform.ANDROID) {
|
|
666
|
+
this.logger.warn("Back button is only available on Android");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
await this.driver.back();
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Press home button
|
|
673
|
+
*/
|
|
674
|
+
async home() {
|
|
675
|
+
this.ensureInitialized("home");
|
|
676
|
+
if (this.config.platform === types_1.MobilePlatform.ANDROID) {
|
|
677
|
+
await this.driver.execute("mobile: pressKey", { keycode: 3 });
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
await this.driver.execute("mobile: pressButton", { name: "home" });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Hide keyboard
|
|
685
|
+
*/
|
|
686
|
+
async hideKeyboard() {
|
|
687
|
+
this.ensureInitialized("hideKeyboard");
|
|
688
|
+
try {
|
|
689
|
+
await this.driver.hideKeyboard();
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// Keyboard might not be visible
|
|
693
|
+
this.logger.debug("Keyboard was not visible");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Check if keyboard is shown
|
|
698
|
+
*/
|
|
699
|
+
async isKeyboardShown() {
|
|
700
|
+
this.ensureInitialized("isKeyboardShown");
|
|
701
|
+
return await this.driver.isKeyboardShown();
|
|
702
|
+
}
|
|
703
|
+
// ============ Additional Device Methods ============
|
|
704
|
+
/**
|
|
705
|
+
* Get device time
|
|
706
|
+
*/
|
|
707
|
+
async getDeviceTime() {
|
|
708
|
+
this.ensureInitialized("getDeviceTime");
|
|
709
|
+
return (await this.driver.execute("mobile: getDeviceTime", {}));
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Press back button (Android) - alias for back()
|
|
713
|
+
*/
|
|
714
|
+
async pressBack() {
|
|
715
|
+
await this.back();
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Press home button - alias for home()
|
|
719
|
+
*/
|
|
720
|
+
async pressHome() {
|
|
721
|
+
await this.home();
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Shake device
|
|
725
|
+
*/
|
|
726
|
+
async shake() {
|
|
727
|
+
this.ensureInitialized("shake");
|
|
728
|
+
this.logger.debug("Shaking device");
|
|
729
|
+
await this.driver.execute("mobile: shake", {});
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Get battery info
|
|
733
|
+
*/
|
|
734
|
+
async getBatteryInfo() {
|
|
735
|
+
this.ensureInitialized("getBatteryInfo");
|
|
736
|
+
const info = (await this.driver.execute("mobile: batteryInfo", {}));
|
|
737
|
+
return info;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Push file to device
|
|
741
|
+
*/
|
|
742
|
+
async pushFile(remotePath, data) {
|
|
743
|
+
this.ensureInitialized("pushFile");
|
|
744
|
+
this.logger.debug(`Pushing file to: ${remotePath}`);
|
|
745
|
+
await this.driver.pushFile(remotePath, data);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Pull file from device
|
|
749
|
+
*/
|
|
750
|
+
async pullFile(remotePath) {
|
|
751
|
+
this.ensureInitialized("pullFile");
|
|
752
|
+
this.logger.debug(`Pulling file from: ${remotePath}`);
|
|
753
|
+
return await this.driver.pullFile(remotePath);
|
|
754
|
+
}
|
|
755
|
+
// ============ Screenshot ============
|
|
756
|
+
/**
|
|
757
|
+
* Take a screenshot
|
|
758
|
+
*/
|
|
759
|
+
async takeScreenshot(options = {}) {
|
|
760
|
+
this.ensureInitialized("takeScreenshot");
|
|
761
|
+
const base64 = await this.driver.takeScreenshot();
|
|
762
|
+
const buffer = Buffer.from(base64, "base64");
|
|
763
|
+
// Save to file if path specified
|
|
764
|
+
if (options.path) {
|
|
765
|
+
await this.saveScreenshot(buffer, options);
|
|
766
|
+
}
|
|
767
|
+
return buffer;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Execute script
|
|
771
|
+
*/
|
|
772
|
+
async executeScript(script, ...args) {
|
|
773
|
+
this.ensureInitialized("executeScript");
|
|
774
|
+
return (await this.driver.execute(script, ...args));
|
|
775
|
+
}
|
|
776
|
+
// ============ Cleanup ============
|
|
777
|
+
/**
|
|
778
|
+
* Quit the driver and cleanup
|
|
779
|
+
*/
|
|
780
|
+
async quit() {
|
|
781
|
+
if (!this.initialized) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
// Execute before quit hooks
|
|
785
|
+
await this.hooks.execute(hook_manager_1.HookEvent.BEFORE_DRIVER_QUIT, {
|
|
786
|
+
platform: this.platform,
|
|
787
|
+
driverStatus: this.getStatus(),
|
|
788
|
+
});
|
|
789
|
+
try {
|
|
790
|
+
if (this.driver) {
|
|
791
|
+
await this.driver.deleteSession();
|
|
792
|
+
this.driver = null;
|
|
793
|
+
}
|
|
794
|
+
await super.quit();
|
|
795
|
+
// Execute after quit hooks
|
|
796
|
+
await this.hooks.execute(hook_manager_1.HookEvent.AFTER_DRIVER_QUIT, {
|
|
797
|
+
platform: this.platform,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
this.logger.error("Error during MobileDriver quit", {
|
|
802
|
+
error: error.message,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
exports.MobileDriver = MobileDriver;
|
|
808
|
+
//# sourceMappingURL=mobile.driver.js.map
|