-
-
Notifications
You must be signed in to change notification settings - Fork 210
/
Copy pathdriver.js
498 lines (441 loc) · 17.5 KB
/
driver.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
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
491
492
493
494
495
496
497
498
import { BaseDriver, DeviceSettings } from 'appium-base-driver';
import desiredConstraints from './desired-caps';
import commands from './commands/index';
import helpers from './android-helpers';
import log from './logger';
import _ from 'lodash';
import { DEFAULT_ADB_PORT } from 'appium-adb';
import { fs, tempDir, util } from 'appium-support';
import { retryInterval } from 'asyncbox';
import { SharedPrefsBuilder } from 'shared-preferences-builder';
import B from 'bluebird';
const APP_EXTENSION = '.apk';
const DEVICE_PORT = 4724;
// This is a set of methods and paths that we never want to proxy to
// Chromedriver
const NO_PROXY = [
['POST', new RegExp('^/session/[^/]+/context')],
['GET', new RegExp('^/session/[^/]+/context')],
['POST', new RegExp('^/session/[^/]+/appium')],
['GET', new RegExp('^/session/[^/]+/appium')],
['POST', new RegExp('^/session/[^/]+/touch/perform')],
['POST', new RegExp('^/session/[^/]+/touch/multi/perform')],
['POST', new RegExp('^/session/[^/]+/orientation')],
['GET', new RegExp('^/session/[^/]+/orientation')],
];
class AndroidDriver extends BaseDriver {
constructor (opts = {}, shouldValidateCaps = true) {
super(opts, shouldValidateCaps);
this.locatorStrategies = [
'xpath',
'id',
'class name',
'accessibility id',
'-android uiautomator'
];
this.desiredCapConstraints = desiredConstraints;
this.sessionChromedrivers = {};
this.jwpProxyActive = false;
this.jwpProxyAvoid = _.clone(NO_PROXY);
this.settings = new DeviceSettings({ignoreUnimportantViews: false},
this.onSettingsUpdate.bind(this));
this.chromedriver = null;
this.apkStrings = {};
this.bootstrapPort = opts.bootstrapPort || DEVICE_PORT;
this.unlocker = helpers.unlocker;
for (let [cmd, fn] of _.toPairs(commands)) {
AndroidDriver.prototype[cmd] = fn;
}
}
async createSession (...args) {
// the whole createSession flow is surrounded in a try-catch statement
// if creating a session fails at any point, we teardown everything we
// set up before throwing the error.
try {
let [sessionId, caps] = await super.createSession(...args);
let serverDetails = {
platform: 'LINUX',
webStorageEnabled: false,
takesScreenshot: true,
javascriptEnabled: true,
databaseEnabled: false,
networkConnectionEnabled: true,
locationContextEnabled: false,
warnings: {},
desired: this.caps
};
this.caps = Object.assign(serverDetails, this.caps);
// assigning defaults
let defaultOpts = {
action: "android.intent.action.MAIN",
category: "android.intent.category.LAUNCHER",
flags: "0x10200000",
disableAndroidWatchers: false,
tmpDir: await tempDir.staticDir(),
fullReset: false,
autoLaunch: true,
adbPort: DEFAULT_ADB_PORT,
androidInstallTimeout: 90000,
};
_.defaults(this.opts, defaultOpts);
if (!this.opts.javaVersion) {
this.opts.javaVersion = await helpers.getJavaVersion();
}
this.useUnlockHelperApp = _.isUndefined(this.caps.unlockType);
// not user visible via caps
if (this.opts.noReset === true) {
this.opts.fullReset = false;
}
if (this.opts.fullReset === true) {
this.opts.noReset = false;
}
this.opts.fastReset = !this.opts.fullReset && !this.opts.noReset;
this.opts.skipUninstall = this.opts.fastReset || this.opts.noReset;
this.curContext = this.defaultContextName();
if (this.isChromeSession) {
log.info("We're going to run a Chrome-based session");
let {pkg, activity} = helpers.getChromePkg(this.opts.browserName);
this.opts.appPackage = pkg;
this.opts.appActivity = activity;
log.info(`Chrome-type package and activity are ${pkg} and ${activity}`);
}
if (this.opts.nativeWebScreenshot) {
this.jwpProxyAvoid.push(['GET', new RegExp('^/session/[^/]+/screenshot')]);
}
if (this.opts.reboot) {
this.setAvdFromCapabilities(caps);
}
// get device udid for this session
let {udid, emPort} = await helpers.getDeviceInfoFromCaps(this.opts);
this.opts.udid = udid;
this.opts.emPort = emPort;
// set up an instance of ADB
this.adb = await helpers.createADB({
javaVersion: this.opts.javaVersion,
udid: this.opts.udid,
emPort: this.opts.emPort,
adbPort: this.opts.adbPort,
suppressKillServer: this.opts.suppressKillServer,
remoteAdbHost: this.opts.remoteAdbHost,
clearDeviceLogsOnStart: this.opts.clearDeviceLogsOnStart,
adbExecTimeout: this.opts.adbExecTimeout,
});
if (await this.adb.getApiLevel() >= 23) {
log.warn("Consider setting 'automationName' capability to " +
"'uiautomator2' on Android >= 6, since UIAutomator framework " +
"is not maintained anymore by the OS vendor.");
}
if (this.helpers.isPackageOrBundle(this.opts.app)) {
// user provided package instead of app for 'app' capability, massage options
this.opts.appPackage = this.opts.app;
this.opts.app = null;
}
if (this.opts.app) {
// find and copy, or download and unzip an app url or path
this.opts.app = await this.helpers.configureApp(this.opts.app, APP_EXTENSION);
this.opts.appIsTemp = caps.app !== this.opts.app; // did we make a temporary copy?
await this.checkAppPresent();
} else if (this.appOnDevice) {
// the app isn't an actual app file but rather something we want to
// assume is on the device and just launch via the appPackage
log.info(`App file was not listed, instead we're going to run ` +
`${this.opts.appPackage} directly on the device`);
await this.checkPackagePresent();
}
// Some cloud services using appium launch the avd themselves, so we ensure netspeed
// is set for emulators by calling adb.networkSpeed before running the app
if (util.hasValue(this.opts.networkSpeed)) {
if (!this.isEmulator()) {
log.warn("Sorry, networkSpeed capability is only available for emulators");
} else {
let networkSpeed = helpers.ensureNetworkSpeed(this.adb, this.opts.networkSpeed);
await this.adb.networkSpeed(networkSpeed);
}
}
// check if we have to enable/disable gps before running the application
if (util.hasValue(this.opts.gpsEnabled)) {
if (this.isEmulator()) {
log.info(`Trying to ${this.opts.gpsEnabled ? "enable" : "disable"} gps location provider`);
await this.adb.toggleGPSLocationProvider(this.opts.gpsEnabled);
} else {
log.warn('Sorry! gpsEnabled capability is only available for emulators');
}
}
await this.startAndroidSession(this.opts);
return [sessionId, this.caps];
} catch (e) {
// ignoring delete session exception if any and throw the real error
// that happened while creating the session.
try {
await this.deleteSession();
} catch (ign) {}
throw e;
}
}
isEmulator () {
return !!(this.opts.avd || /emulator/.test(this.opts.udid));
}
setAvdFromCapabilities (caps) {
if (this.opts.avd) {
log.info('avd name defined, ignoring device name and platform version');
} else {
if (!caps.deviceName) {
log.errorAndThrow('avd or deviceName should be specified when reboot option is enables');
}
if (!caps.platformVersion) {
log.errorAndThrow('avd or platformVersion should be specified when reboot option is enabled');
}
let avdDevice = caps.deviceName.replace(/[^a-zA-Z0-9_.]/g, "-");
this.opts.avd = `${avdDevice}__${caps.platformVersion}`;
}
}
get appOnDevice () {
return this.helpers.isPackageOrBundle(this.opts.app) || (!this.opts.app &&
this.helpers.isPackageOrBundle(this.opts.appPackage));
}
get isChromeSession () {
return helpers.isChromeBrowser(this.opts.browserName);
}
async onSettingsUpdate (key, value) {
if (key === "ignoreUnimportantViews") {
await this.setCompressedLayoutHierarchy(value);
}
}
async startAndroidSession () {
log.info(`Starting Android session`);
// set up the device to run on (real or emulator, etc)
this.defaultIME = await helpers.initDevice(this.adb, this.opts);
// set actual device name, udid, platform version, screen size, model and manufacturer details
this.caps.deviceName = this.adb.curDeviceId;
this.caps.deviceUDID = this.opts.udid;
this.caps.platformVersion = await this.adb.getPlatformVersion();
this.caps.deviceScreenSize = await this.adb.getScreenSize();
this.caps.deviceModel = await this.adb.getModel();
this.caps.deviceManufacturer = await this.adb.getManufacturer();
// If the user sets autoLaunch to false, they are responsible for initAUT() and startAUT()
if (this.opts.autoLaunch) {
// set up app under test
await this.initAUT();
}
if (this.opts.disableWindowAnimation) {
if (await this.adb.isAnimationOn()) {
log.info('Disabling window animation as it is requested by "disableWindowAnimation" capability');
await this.adb.setAnimationState(false);
this._wasWindowAnimationDisabled = true;
} else {
log.info('Window animation is already disabled');
}
}
// start UiAutomator
this.bootstrap = new helpers.bootstrap(this.adb, this.bootstrapPort, this.opts.websocket);
await this.bootstrap.start(this.opts.appPackage, this.opts.disableAndroidWatchers, this.opts.acceptSslCerts);
// handling unexpected shutdown
this.bootstrap.onUnexpectedShutdown.catch(async (err) => { // eslint-disable-line promise/prefer-await-to-callbacks
if (!this.bootstrap.ignoreUnexpectedShutdown) {
await this.startUnexpectedShutdown(err);
}
});
if (!this.opts.skipUnlock) {
// Let's try to unlock the device
await helpers.unlock(this, this.adb, this.caps);
}
// Set CompressedLayoutHierarchy on the device based on current settings object
// this has to happen _after_ bootstrap is initialized
if (this.opts.ignoreUnimportantViews) {
await this.settings.update({ignoreUnimportantViews: this.opts.ignoreUnimportantViews});
}
if (this.isChromeSession) {
// start a chromedriver session and proxy to it
await this.startChromeSession();
} else {
if (this.opts.autoLaunch) {
// start app
await this.startAUT();
}
}
if (util.hasValue(this.opts.orientation)) {
log.debug(`Setting initial orientation to '${this.opts.orientation}'`);
await this.setOrientation(this.opts.orientation);
}
await this.initAutoWebview();
}
async initAutoWebview () {
if (this.opts.autoWebview) {
let viewName = this.defaultWebviewName();
let timeout = (this.opts.autoWebviewTimeout) || 2000;
log.info(`Setting auto webview to context '${viewName}' with timeout ${timeout}ms`);
// try every 500ms until timeout is over
await retryInterval(timeout / 500, 500, async () => {
await this.setContext(viewName);
});
}
}
async initAUT () {
// populate appPackage, appActivity, appWaitPackage, appWaitActivity,
// and the device being used
// in the opts and caps (so it gets back to the user on session creation)
let launchInfo = await helpers.getLaunchInfo(this.adb, this.opts);
Object.assign(this.opts, launchInfo);
Object.assign(this.caps, launchInfo);
// Install any "otherApps" that were specified in caps
if (this.opts.otherApps) {
let otherApps;
try {
otherApps = helpers.parseArray(this.opts.otherApps);
} catch (e) {
log.errorAndThrow(`Could not parse "otherApps" capability: ${e.message}`);
}
otherApps = await B.all(otherApps.map((app) => this.helpers.configureApp(app, APP_EXTENSION)));
await helpers.installOtherApks(otherApps, this.adb, this.opts);
}
// install app
if (!this.opts.app) {
if (this.opts.fullReset) {
log.errorAndThrow('Full reset requires an app capability, use fastReset if app is not provided');
}
log.debug('No app capability. Assuming it is already on the device');
if (this.opts.fastReset) {
await helpers.resetApp(this.adb, this.opts);
}
return;
}
if (!this.opts.skipUninstall) {
await this.adb.uninstallApk(this.opts.appPackage);
}
await helpers.installApk(this.adb, this.opts);
const apkStringsForLanguage = await helpers.pushStrings(this.opts.language, this.adb, this.opts);
if (this.opts.language) {
this.apkStrings[this.opts.language] = apkStringsForLanguage;
}
// This must run after installing the apk, otherwise it would cause the
// install to fail. And before running the app.
if (!_.isUndefined(this.opts.sharedPreferences)) {
await this.setSharedPreferences(this.opts);
}
}
async checkAppPresent () {
log.debug("Checking whether app is actually present");
if (!(await fs.exists(this.opts.app))) {
log.errorAndThrow(`Could not find app apk at ${this.opts.app}`);
}
}
async checkPackagePresent () {
log.debug("Checking whether package is present on the device");
if (!(await this.adb.shell(['pm', 'list', 'packages', this.opts.appPackage]))) {
log.errorAndThrow(`Could not find package ${this.opts.appPackage} on the device`);
}
}
// Set CompressedLayoutHierarchy on the device
async setCompressedLayoutHierarchy (compress) {
await this.bootstrap.sendAction("compressedLayoutHierarchy", {compressLayout: compress});
}
async deleteSession () {
log.debug("Shutting down Android driver");
await helpers.removeAllSessionWebSocketHandlers(this.server, this.sessionId);
await super.deleteSession();
if (this.bootstrap) {
// certain cleanup we only care to do if the bootstrap was ever run
await this.stopChromedriverProxies();
if (this.opts.unicodeKeyboard && this.opts.resetKeyboard && this.defaultIME) {
log.debug(`Resetting IME to ${this.defaultIME}`);
await this.adb.setIME(this.defaultIME);
}
if (!this.isChromeSession) {
await this.adb.forceStop(this.opts.appPackage);
}
await this.adb.goToHome();
if (this.opts.fullReset && !this.opts.skipUninstall && !this.appOnDevice) {
await this.adb.uninstallApk(this.opts.appPackage);
}
await this.bootstrap.shutdown();
this.bootstrap = null;
} else {
log.debug("Called deleteSession but bootstrap wasn't active");
}
// some cleanup we want to do regardless, in case we are shutting down
// mid-startup
await this.adb.stopLogcat();
if (this.useUnlockHelperApp) {
await this.adb.forceStop('io.appium.unlock');
}
if (this._wasWindowAnimationDisabled) {
log.info('Restoring window animation state');
await this.adb.setAnimationState(true);
}
if (this.opts.reboot) {
let avdName = this.opts.avd.replace('@', '');
log.debug(`closing emulator '${avdName}'`);
await this.adb.killEmulator(avdName);
}
if (this.opts.clearSystemFiles) {
if (this.opts.appIsTemp) {
log.debug(`Temporary copy of app was made: deleting '${this.opts.app}'`);
try {
await fs.rimraf(this.opts.app);
} catch (err) {
log.warn(`Unable to delete temporary app: ${err.message}`);
}
} else {
log.debug('App was not copied, so not deleting');
}
} else {
log.debug('Not cleaning generated files. Add `clearSystemFiles` capability if wanted.');
}
}
async setSharedPreferences () {
let sharedPrefs = this.opts.sharedPreferences;
log.info("Trying to set shared preferences");
let name = sharedPrefs.name;
if (_.isUndefined(name)) {
log.warn(`Skipping setting Shared preferences, name is undefined: ${JSON.stringify(sharedPrefs)}`);
return false;
}
let remotePath = `/data/data/${this.opts.appPackage}/shared_prefs`;
let remoteFile = `${remotePath}/${name}.xml`;
let localPath = `/tmp/${name}.xml`;
let builder = this.getPrefsBuilder();
builder.build(sharedPrefs.prefs);
log.info(`Creating temporary shared preferences: ${localPath}`);
builder.toFile(localPath);
log.info(`Creating shared_prefs remote folder: ${remotePath}`);
await this.adb.shell(['mkdir', '-p', remotePath]);
log.info(`Pushing shared_prefs to ${remoteFile}`);
await this.adb.push(localPath, remoteFile);
try {
log.info(`Trying to remove shared preferences temporary file`);
if (await fs.exists(localPath)) {
await fs.unlink(localPath);
}
} catch (e) {
log.warn(`Error trying to remove temporary file ${localPath}`);
}
return true;
}
getPrefsBuilder () {
/* Add this method to create a new SharedPrefsBuilder instead of
* directly creating the object on setSharedPreferences for testing purposes
*/
return new SharedPrefsBuilder();
}
validateDesiredCaps (caps) {
if (!super.validateDesiredCaps(caps)) {
return false;
}
return helpers.validateDesiredCaps(caps);
}
proxyActive (sessionId) {
super.proxyActive(sessionId);
return this.jwpProxyActive;
}
getProxyAvoidList (sessionId) {
super.getProxyAvoidList(sessionId);
return this.jwpProxyAvoid;
}
canProxy (sessionId) {
super.canProxy(sessionId);
// this will change depending on ChromeDriver status
return _.isFunction(this.proxyReqRes);
}
}
export { AndroidDriver };
export default AndroidDriver;