appium-android-driver 5.0.6 → 5.0.9
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/README.md +0 -1
- package/build/lib/commands/actions.js +16 -20
- package/build/lib/commands/app-management.js +15 -17
- package/build/lib/commands/context.js +46 -66
- package/build/lib/commands/coverage.js +2 -6
- package/build/lib/commands/element.js +3 -9
- package/build/lib/commands/emu-console.js +3 -5
- package/build/lib/commands/execute.js +3 -6
- package/build/lib/commands/file-actions.js +22 -29
- package/build/lib/commands/general.js +18 -30
- package/build/lib/commands/ime.js +8 -21
- package/build/lib/commands/network.js +14 -27
- package/build/lib/commands/performance.js +3 -6
- package/build/lib/commands/recordscreen.js +27 -38
- package/build/lib/commands/shell.js +6 -7
- package/build/lib/commands/streamscreen.js +29 -43
- package/build/lib/commands/system-bars.js +10 -16
- package/build/lib/commands/touch.js +4 -10
- package/build/lib/driver.js +61 -67
- package/build/lib/webview-helpers.js +2 -7
- package/lib/commands/actions.js +18 -19
- package/lib/commands/app-management.js +13 -12
- package/lib/commands/context.js +36 -40
- package/lib/commands/coverage.js +1 -4
- package/lib/commands/element.js +2 -3
- package/lib/commands/emu-console.js +2 -2
- package/lib/commands/execute.js +2 -3
- package/lib/commands/file-actions.js +31 -25
- package/lib/commands/general.js +16 -18
- package/lib/commands/ime.js +7 -8
- package/lib/commands/network.js +15 -16
- package/lib/commands/performance.js +2 -3
- package/lib/commands/recordscreen.js +23 -22
- package/lib/commands/shell.js +6 -6
- package/lib/commands/streamscreen.js +22 -23
- package/lib/commands/system-bars.js +11 -10
- package/lib/commands/touch.js +3 -4
- package/lib/driver.js +45 -46
- package/lib/webview-helpers.js +1 -7
- package/package.json +2 -2
package/lib/commands/context.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
|
-
import log from '../logger';
|
|
3
2
|
import Chromedriver from 'appium-chromedriver';
|
|
4
3
|
import PortFinder from 'portfinder';
|
|
5
4
|
import B from 'bluebird';
|
|
@@ -26,8 +25,7 @@ commands.getCurrentContext = async function getCurrentContext () { // eslint-dis
|
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
commands.getContexts = async function getContexts () {
|
|
29
|
-
const
|
|
30
|
-
const webviewsMapping = await webviewHelpers.getWebViewsMapping(this.adb, opts);
|
|
28
|
+
const webviewsMapping = await webviewHelpers.getWebViewsMapping(this.adb, this.opts);
|
|
31
29
|
return this.assignContexts(webviewsMapping);
|
|
32
30
|
};
|
|
33
31
|
|
|
@@ -43,8 +41,7 @@ commands.setContext = async function setContext (name) {
|
|
|
43
41
|
return;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
const
|
|
47
|
-
const webviewsMapping = await webviewHelpers.getWebViewsMapping(this.adb, opts);
|
|
44
|
+
const webviewsMapping = await webviewHelpers.getWebViewsMapping(this.adb, this.opts);
|
|
48
45
|
const contexts = this.assignContexts(webviewsMapping);
|
|
49
46
|
// if the context we want doesn't exist, fail
|
|
50
47
|
if (!_.includes(contexts, name)) {
|
|
@@ -106,8 +103,7 @@ commands.mobileGetContexts = async function mobileGetContexts () {
|
|
|
106
103
|
androidDeviceSocket: this.opts.androidDeviceSocket,
|
|
107
104
|
ensureWebviewsHavePages: true,
|
|
108
105
|
webviewDevtoolsPort: this.opts.webviewDevtoolsPort,
|
|
109
|
-
enableWebviewDetailsCollection: true
|
|
110
|
-
isChromeSession: this.isChromeSession
|
|
106
|
+
enableWebviewDetailsCollection: true
|
|
111
107
|
};
|
|
112
108
|
return await webviewHelpers.getWebViewsMapping(this.adb, opts);
|
|
113
109
|
};
|
|
@@ -116,7 +112,7 @@ helpers.assignContexts = function assignContexts (webviewsMapping) {
|
|
|
116
112
|
const opts = Object.assign({isChromeSession: this.isChromeSession}, this.opts);
|
|
117
113
|
const webviews = webviewHelpers.parseWebviewNames(webviewsMapping, opts);
|
|
118
114
|
this.contexts = [NATIVE_WIN, ...webviews];
|
|
119
|
-
log.debug(`Available contexts: ${JSON.stringify(this.contexts)}`);
|
|
115
|
+
this.log.debug(`Available contexts: ${JSON.stringify(this.contexts)}`);
|
|
120
116
|
return this.contexts;
|
|
121
117
|
};
|
|
122
118
|
|
|
@@ -132,7 +128,7 @@ helpers.switchContext = async function switchContext (name, webviewsMapping) {
|
|
|
132
128
|
// to true then kill chromedriver session using stopChromedriverProxies or
|
|
133
129
|
// else simply suspend proxying to the latter
|
|
134
130
|
if (this.opts.recreateChromeDriverSessions) {
|
|
135
|
-
log.debug('recreateChromeDriverSessions set to true; killing existing chromedrivers');
|
|
131
|
+
this.log.debug('recreateChromeDriverSessions set to true; killing existing chromedrivers');
|
|
136
132
|
await this.stopChromedriverProxies();
|
|
137
133
|
} else {
|
|
138
134
|
await this.suspendChromedriverProxy();
|
|
@@ -164,15 +160,15 @@ helpers.isWebContext = function isWebContext () {
|
|
|
164
160
|
|
|
165
161
|
// Turn on proxying to an existing Chromedriver session or a new one
|
|
166
162
|
helpers.startChromedriverProxy = async function startChromedriverProxy (context, webviewsMapping) {
|
|
167
|
-
log.debug(`Connecting to chrome-backed webview context '${context}'`);
|
|
163
|
+
this.log.debug(`Connecting to chrome-backed webview context '${context}'`);
|
|
168
164
|
|
|
169
165
|
let cd;
|
|
170
166
|
if (this.sessionChromedrivers[context]) {
|
|
171
167
|
// in the case where we've already set up a chromedriver for a context,
|
|
172
168
|
// we want to reconnect to it, not create a whole new one
|
|
173
|
-
log.debug(`Found existing Chromedriver for context '${context}'. Using it.`);
|
|
169
|
+
this.log.debug(`Found existing Chromedriver for context '${context}'. Using it.`);
|
|
174
170
|
cd = this.sessionChromedrivers[context];
|
|
175
|
-
await setupExistingChromedriver(cd);
|
|
171
|
+
await setupExistingChromedriver(this.log, cd);
|
|
176
172
|
} else {
|
|
177
173
|
let opts = _.cloneDeep(this.opts);
|
|
178
174
|
opts.chromeUseRunningApp = true;
|
|
@@ -200,7 +196,7 @@ helpers.startChromedriverProxy = async function startChromedriverProxy (context,
|
|
|
200
196
|
const appState = await this.queryAppState(knownPackage);
|
|
201
197
|
if (_.includes([APP_STATE.RUNNING_IN_BACKGROUND, APP_STATE.RUNNING_IN_FOREGROUND], appState)) {
|
|
202
198
|
opts.chromeAndroidPackage = knownPackage;
|
|
203
|
-
log.debug(`Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` +
|
|
199
|
+
this.log.debug(`Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` +
|
|
204
200
|
`for context '${context}' by querying states of Chrome app packages`);
|
|
205
201
|
break;
|
|
206
202
|
}
|
|
@@ -209,7 +205,7 @@ helpers.startChromedriverProxy = async function startChromedriverProxy (context,
|
|
|
209
205
|
for (const wm of webviewsMapping) {
|
|
210
206
|
if (wm.webviewName === context && _.has(wm?.info, 'Android-Package')) {
|
|
211
207
|
opts.chromeAndroidPackage = wm.info['Android-Package'];
|
|
212
|
-
log.debug(`Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` +
|
|
208
|
+
this.log.debug(`Identified chromeAndroidPackage as '${opts.chromeAndroidPackage}' ` +
|
|
213
209
|
`for context '${context}' by CDP`);
|
|
214
210
|
break;
|
|
215
211
|
}
|
|
@@ -246,7 +242,7 @@ helpers.suspendChromedriverProxy = function suspendChromedriverProxy () {
|
|
|
246
242
|
|
|
247
243
|
// Handle an out-of-band Chromedriver stop event
|
|
248
244
|
helpers.onChromedriverStop = async function onChromedriverStop (context) {
|
|
249
|
-
log.warn(`Chromedriver for context ${context} stopped unexpectedly`);
|
|
245
|
+
this.log.warn(`Chromedriver for context ${context} stopped unexpectedly`);
|
|
250
246
|
if (context === this.curContext) {
|
|
251
247
|
// we exited unexpectedly while automating the current context and so want
|
|
252
248
|
// to shut down the session and respond with an error
|
|
@@ -255,8 +251,8 @@ helpers.onChromedriverStop = async function onChromedriverStop (context) {
|
|
|
255
251
|
} else {
|
|
256
252
|
// if a Chromedriver in the non-active context barfs, we don't really
|
|
257
253
|
// care, we'll just make a new one next time we need the context.
|
|
258
|
-
log.warn("Chromedriver quit unexpectedly, but it wasn't the active " +
|
|
259
|
-
|
|
254
|
+
this.log.warn("Chromedriver quit unexpectedly, but it wasn't the active " +
|
|
255
|
+
'context, ignoring');
|
|
260
256
|
delete this.sessionChromedrivers[context];
|
|
261
257
|
}
|
|
262
258
|
};
|
|
@@ -267,13 +263,13 @@ helpers.stopChromedriverProxies = async function stopChromedriverProxies () {
|
|
|
267
263
|
this.suspendChromedriverProxy(); // make sure we turn off the proxy flag
|
|
268
264
|
for (let context of _.keys(this.sessionChromedrivers)) {
|
|
269
265
|
let cd = this.sessionChromedrivers[context];
|
|
270
|
-
log.debug(`Stopping chromedriver for context ${context}`);
|
|
266
|
+
this.log.debug(`Stopping chromedriver for context ${context}`);
|
|
271
267
|
// stop listening for the stopped state event
|
|
272
268
|
cd.removeAllListeners(Chromedriver.EVENT_CHANGED);
|
|
273
269
|
try {
|
|
274
270
|
await cd.stop();
|
|
275
271
|
} catch (err) {
|
|
276
|
-
log.warn(`Error stopping Chromedriver: ${err.message}`);
|
|
272
|
+
this.log.warn(`Error stopping Chromedriver: ${err.message}`);
|
|
277
273
|
}
|
|
278
274
|
delete this.sessionChromedrivers[context];
|
|
279
275
|
}
|
|
@@ -290,10 +286,10 @@ helpers.shouldDismissChromeWelcome = function shouldDismissChromeWelcome () {
|
|
|
290
286
|
};
|
|
291
287
|
|
|
292
288
|
helpers.dismissChromeWelcome = async function dismissChromeWelcome () {
|
|
293
|
-
log.info('Trying to dismiss Chrome welcome');
|
|
289
|
+
this.log.info('Trying to dismiss Chrome welcome');
|
|
294
290
|
let activity = await this.getCurrentActivity();
|
|
295
291
|
if (activity !== 'org.chromium.chrome.browser.firstrun.FirstRunActivity') {
|
|
296
|
-
log.info('Chrome welcome dialog never showed up! Continuing');
|
|
292
|
+
this.log.info('Chrome welcome dialog never showed up! Continuing');
|
|
297
293
|
return;
|
|
298
294
|
}
|
|
299
295
|
let el = await this.findElOrEls('id', 'com.android.chrome:id/terms_accept', false);
|
|
@@ -304,12 +300,12 @@ helpers.dismissChromeWelcome = async function dismissChromeWelcome () {
|
|
|
304
300
|
} catch (e) {
|
|
305
301
|
// DO NOTHING, THIS DEVICE DIDNT LAUNCH THE SIGNIN DIALOG
|
|
306
302
|
// IT MUST BE A NON GMS DEVICE
|
|
307
|
-
log.warn(`This device did not show Chrome SignIn dialog, ${e.message}`);
|
|
303
|
+
this.log.warn(`This device did not show Chrome SignIn dialog, ${e.message}`);
|
|
308
304
|
}
|
|
309
305
|
};
|
|
310
306
|
|
|
311
307
|
helpers.startChromeSession = async function startChromeSession () {
|
|
312
|
-
log.info('Starting a chrome-based browser session');
|
|
308
|
+
this.log.info('Starting a chrome-based browser session');
|
|
313
309
|
let opts = _.cloneDeep(this.opts);
|
|
314
310
|
|
|
315
311
|
const knownPackages = [
|
|
@@ -352,7 +348,7 @@ helpers.startChromeSession = async function startChromeSession () {
|
|
|
352
348
|
* Internal library functions
|
|
353
349
|
* -------------------------- */
|
|
354
350
|
|
|
355
|
-
async function setupExistingChromedriver (chromedriver) {
|
|
351
|
+
async function setupExistingChromedriver (log, chromedriver) {
|
|
356
352
|
// check the status by sending a simple window-based command to ChromeDriver
|
|
357
353
|
// if there is an error, we want to recreate the ChromeDriver session
|
|
358
354
|
if (!await chromedriver.hasWorkingWebview()) {
|
|
@@ -366,28 +362,29 @@ async function setupExistingChromedriver (chromedriver) {
|
|
|
366
362
|
/**
|
|
367
363
|
* Find a free port to have Chromedriver listen on.
|
|
368
364
|
*
|
|
369
|
-
* @param {array}
|
|
365
|
+
* @param {array} portSpec - Array which is a list of ports. A list item may
|
|
370
366
|
* also itself be an array of length 2 specifying a start and end port of
|
|
371
367
|
* a range. Some valid port specs:
|
|
372
368
|
* - [8000, 8001, 8002]
|
|
373
369
|
* - [[8000, 8005]]
|
|
374
370
|
* - [8000, [9000, 9100]]
|
|
371
|
+
* @param {Object?} log Logger instance
|
|
375
372
|
*
|
|
376
373
|
* @return {number} A free port
|
|
377
374
|
*/
|
|
378
|
-
async function getChromedriverPort (portSpec) {
|
|
375
|
+
async function getChromedriverPort (portSpec, log = null) {
|
|
379
376
|
const getPort = B.promisify(PortFinder.getPort, {context: PortFinder});
|
|
380
377
|
|
|
381
378
|
// if the user didn't give us any specific information about chromedriver
|
|
382
379
|
// port ranges, just find any free port
|
|
383
380
|
if (!portSpec) {
|
|
384
381
|
const port = await getPort();
|
|
385
|
-
log
|
|
382
|
+
log?.debug(`A port was not given, using random free port: ${port}`);
|
|
386
383
|
return port;
|
|
387
384
|
}
|
|
388
385
|
|
|
389
386
|
// otherwise find the free port based on a list or range provided by the user
|
|
390
|
-
log
|
|
387
|
+
log?.debug(`Finding a free port for chromedriver using spec ${JSON.stringify(portSpec)}`);
|
|
391
388
|
let foundPort = null;
|
|
392
389
|
for (const potentialPort of portSpec) {
|
|
393
390
|
let port, stopPort;
|
|
@@ -398,11 +395,11 @@ async function getChromedriverPort (portSpec) {
|
|
|
398
395
|
stopPort = port;
|
|
399
396
|
}
|
|
400
397
|
try {
|
|
401
|
-
log
|
|
398
|
+
log?.debug(`Checking port range ${port}:${stopPort}`);
|
|
402
399
|
foundPort = await getPort({port, stopPort});
|
|
403
400
|
break;
|
|
404
401
|
} catch (e) {
|
|
405
|
-
log
|
|
402
|
+
log?.debug(`Nothing in port range ${port}:${stopPort} was available`);
|
|
406
403
|
}
|
|
407
404
|
}
|
|
408
405
|
|
|
@@ -411,7 +408,7 @@ async function getChromedriverPort (portSpec) {
|
|
|
411
408
|
`chromedriverPorts spec ${JSON.stringify(portSpec)}`);
|
|
412
409
|
}
|
|
413
410
|
|
|
414
|
-
log
|
|
411
|
+
log?.debug(`Using free port ${foundPort} for chromedriver`);
|
|
415
412
|
return foundPort;
|
|
416
413
|
}
|
|
417
414
|
|
|
@@ -419,27 +416,27 @@ helpers.isChromedriverAutodownloadEnabled = function isChromedriverAutodownloadE
|
|
|
419
416
|
if (this.isFeatureEnabled(CHROMEDRIVER_AUTODOWNLOAD_FEATURE)) {
|
|
420
417
|
return true;
|
|
421
418
|
}
|
|
422
|
-
log
|
|
419
|
+
this?.log?.debug(`Automated Chromedriver download is disabled. ` +
|
|
423
420
|
`Use '${CHROMEDRIVER_AUTODOWNLOAD_FEATURE}' server feature to enable it`);
|
|
424
421
|
return false;
|
|
425
422
|
};
|
|
426
423
|
|
|
427
424
|
helpers.setupNewChromedriver = async function setupNewChromedriver (opts, curDeviceId, adb, context = null) {
|
|
428
425
|
if (opts.chromeDriverPort) {
|
|
429
|
-
log
|
|
426
|
+
this?.log?.warn(`The 'chromeDriverPort' capability is deprecated. Please use 'chromedriverPort' instead`);
|
|
430
427
|
opts.chromedriverPort = opts.chromeDriverPort;
|
|
431
428
|
}
|
|
432
429
|
|
|
433
430
|
if (opts.chromedriverPort) {
|
|
434
|
-
log
|
|
431
|
+
this?.log?.debug(`Using user-specified port ${opts.chromedriverPort} for chromedriver`);
|
|
435
432
|
} else {
|
|
436
433
|
// if a single port wasn't given, we'll look for a free one
|
|
437
|
-
opts.chromedriverPort = await getChromedriverPort(opts.chromedriverPorts);
|
|
434
|
+
opts.chromedriverPort = await getChromedriverPort(opts.chromedriverPorts, this?.log);
|
|
438
435
|
}
|
|
439
436
|
|
|
440
437
|
const details = context ? webviewHelpers.getWebviewDetails(adb, context) : undefined;
|
|
441
438
|
if (!_.isEmpty(details)) {
|
|
442
|
-
log
|
|
439
|
+
this?.log?.debug('Passing web view details to the Chromedriver constructor: ' +
|
|
443
440
|
JSON.stringify(details, null, 2));
|
|
444
441
|
}
|
|
445
442
|
|
|
@@ -455,8 +452,7 @@ helpers.setupNewChromedriver = async function setupNewChromedriver (opts, curDev
|
|
|
455
452
|
useSystemExecutable: opts.chromedriverUseSystemExecutable,
|
|
456
453
|
disableBuildCheck: opts.chromedriverDisableBuildCheck,
|
|
457
454
|
details,
|
|
458
|
-
isAutodownloadEnabled: (
|
|
459
|
-
? this.isChromedriverAutodownloadEnabled() : undefined,
|
|
455
|
+
isAutodownloadEnabled: this?.isChromedriverAutodownloadEnabled?.()
|
|
460
456
|
});
|
|
461
457
|
|
|
462
458
|
// make sure there are chromeOptions
|
|
@@ -465,13 +461,13 @@ helpers.setupNewChromedriver = async function setupNewChromedriver (opts, curDev
|
|
|
465
461
|
// and strip the prefix
|
|
466
462
|
for (const opt of _.keys(opts)) {
|
|
467
463
|
if (opt.endsWith(':chromeOptions')) {
|
|
468
|
-
log
|
|
464
|
+
this?.log?.warn(`Merging '${opt}' into 'chromeOptions'. This may cause unexpected behavior`);
|
|
469
465
|
_.merge(opts.chromeOptions, opts[opt]);
|
|
470
466
|
}
|
|
471
467
|
}
|
|
472
468
|
|
|
473
469
|
const caps = webviewHelpers.createChromedriverCaps(opts, curDeviceId, details);
|
|
474
|
-
log
|
|
470
|
+
this?.log?.debug(`Before starting chromedriver, androidPackage is '${caps.chromeOptions.androidPackage}'`);
|
|
475
471
|
await chromedriver.start(caps);
|
|
476
472
|
return chromedriver;
|
|
477
473
|
};
|
package/lib/commands/coverage.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import log from '../logger';
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
const commands = {};
|
|
5
2
|
|
|
6
3
|
|
|
@@ -11,7 +8,7 @@ commands.endCoverage = async function endCoverage (intentToBroadcast, ecOnDevice
|
|
|
11
8
|
await this.adb.broadcastProcessEnd(intentToBroadcast, this.appProcess);
|
|
12
9
|
return await this.pullFile(ecOnDevicePath);
|
|
13
10
|
} catch (err) {
|
|
14
|
-
log.warn(`Error ending test coverage: ${err.message}`);
|
|
11
|
+
this.log.warn(`Error ending test coverage: ${err.message}`);
|
|
15
12
|
}
|
|
16
13
|
return '';
|
|
17
14
|
};
|
package/lib/commands/element.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import androidHelpers from '../android-helpers';
|
|
2
2
|
import { retryInterval } from 'asyncbox';
|
|
3
|
-
import logger from '../logger';
|
|
4
3
|
import { util } from '@appium/support';
|
|
5
4
|
|
|
6
5
|
|
|
@@ -90,12 +89,12 @@ commands.clear = async function clear (elementId) {
|
|
|
90
89
|
length = 100;
|
|
91
90
|
}
|
|
92
91
|
await this.click(elementId);
|
|
93
|
-
|
|
92
|
+
this.log.debug(`Sending up to ${length} clear characters to device`);
|
|
94
93
|
return await retryInterval(5, 500, async () => {
|
|
95
94
|
let remainingLength = length;
|
|
96
95
|
while (remainingLength > 0) {
|
|
97
96
|
let lengthToSend = remainingLength < 50 ? remainingLength : 50;
|
|
98
|
-
|
|
97
|
+
this.log.debug(`Sending ${lengthToSend} clear characters to device`);
|
|
99
98
|
await this.adb.clearTextField(lengthToSend);
|
|
100
99
|
remainingLength -= lengthToSend;
|
|
101
100
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { errors } from '@appium/base-driver';
|
|
2
2
|
|
|
3
3
|
const EMU_CONSOLE_FEATURE = 'emulator_console';
|
|
4
4
|
|
|
@@ -37,7 +37,7 @@ commands.mobileExecEmuConsoleCommand = async function mobileExecEmuConsoleComman
|
|
|
37
37
|
} = opts;
|
|
38
38
|
|
|
39
39
|
if (!command) {
|
|
40
|
-
|
|
40
|
+
throw new errors.InvalidArgumentError(`The 'command' argument is mandatory`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
return await this.adb.execEmuConsoleCommand(command, {
|
package/lib/commands/execute.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import { errors, PROTOCOLS } from '@appium/base-driver';
|
|
3
|
-
import logger from '../logger';
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
const extensions = {};
|
|
6
5
|
|
|
7
6
|
extensions.execute = async function execute (script, args) {
|
|
8
7
|
if (script.match(/^mobile:/)) {
|
|
9
|
-
|
|
8
|
+
this.log.info(`Executing native command '${script}'`);
|
|
10
9
|
script = script.replace(/^mobile:/, '').trim();
|
|
11
10
|
return await this.executeMobile(script, _.isArray(args) ? args[0] : args);
|
|
12
11
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
|
-
import { fs, util, zip, tempDir} from '@appium/support';
|
|
3
|
-
import log from '../logger';
|
|
2
|
+
import { fs, util, zip, tempDir } from '@appium/support';
|
|
4
3
|
import path from 'path';
|
|
4
|
+
import { errors } from '@appium/base-driver';
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
const CONTAINER_PATH_MARKER = '@';
|
|
@@ -24,8 +24,8 @@ const commands = {};
|
|
|
24
24
|
function parseContainerPath (remotePath) {
|
|
25
25
|
const match = CONTAINER_PATH_PATTERN.exec(remotePath);
|
|
26
26
|
if (!match) {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
throw new Error(`It is expected that package identifier is separated from the relative path with a single slash. ` +
|
|
28
|
+
`'${remotePath}' is given instead`);
|
|
29
29
|
}
|
|
30
30
|
return [match[1], path.posix.resolve(`/data/data/${match[1]}`, match[2])];
|
|
31
31
|
}
|
|
@@ -36,10 +36,11 @@ function parseContainerPath (remotePath) {
|
|
|
36
36
|
* Exceptions are ignored and written into the log.
|
|
37
37
|
*
|
|
38
38
|
* @param {ADB} adb ADB instance
|
|
39
|
+
* @param {Object?} log Logger instance
|
|
39
40
|
* @param {string} remotePath The file/folder path on the remote device
|
|
40
41
|
*/
|
|
41
|
-
async function scanMedia (adb, remotePath) {
|
|
42
|
-
log
|
|
42
|
+
async function scanMedia (adb, remotePath, log = null) {
|
|
43
|
+
log?.debug(`Performing media scan of '${remotePath}'`);
|
|
43
44
|
try {
|
|
44
45
|
// https://github.com/appium/appium/issues/16184
|
|
45
46
|
if (await adb.getApiLevel() >= 29) {
|
|
@@ -52,7 +53,7 @@ async function scanMedia (adb, remotePath) {
|
|
|
52
53
|
]);
|
|
53
54
|
}
|
|
54
55
|
} catch (e) {
|
|
55
|
-
log
|
|
56
|
+
log?.warn(`Ignoring an unexpected error upon media scanning of '${remotePath}': ${e.stderr || e.message}`);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -79,13 +80,13 @@ function escapePath (p) {
|
|
|
79
80
|
*/
|
|
80
81
|
commands.pullFile = async function pullFile (remotePath) {
|
|
81
82
|
if (remotePath.endsWith('/')) {
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
throw new errors.InvalidArgumentError(`It is expected that remote path points to a file and not to a folder. ` +
|
|
84
|
+
`'${remotePath}' is given instead`);
|
|
84
85
|
}
|
|
85
86
|
let tmpDestination = null;
|
|
86
87
|
if (remotePath.startsWith(CONTAINER_PATH_MARKER)) {
|
|
87
88
|
const [packageId, pathInContainer] = parseContainerPath(remotePath);
|
|
88
|
-
log.debug(`Parsed package identifier '${packageId}' from '${remotePath}'. Will get the data from '${pathInContainer}'`);
|
|
89
|
+
this.log.debug(`Parsed package identifier '${packageId}' from '${remotePath}'. Will get the data from '${pathInContainer}'`);
|
|
89
90
|
tmpDestination = `/data/local/tmp/${path.posix.basename(pathInContainer)}`;
|
|
90
91
|
try {
|
|
91
92
|
await this.adb.shell(['run-as', packageId, `chmod 777 '${escapePath(pathInContainer)}'`]);
|
|
@@ -94,7 +95,7 @@ commands.pullFile = async function pullFile (remotePath) {
|
|
|
94
95
|
`cp -f '${escapePath(pathInContainer)}' '${escapePath(tmpDestination)}'`
|
|
95
96
|
]);
|
|
96
97
|
} catch (e) {
|
|
97
|
-
log.errorAndThrow(`Cannot access the container of '${packageId}' application. ` +
|
|
98
|
+
this.log.errorAndThrow(`Cannot access the container of '${packageId}' application. ` +
|
|
98
99
|
`Is the application installed and has 'debuggable' build option set to true? ` +
|
|
99
100
|
`Original error: ${e.message}`);
|
|
100
101
|
}
|
|
@@ -126,8 +127,10 @@ commands.pullFile = async function pullFile (remotePath) {
|
|
|
126
127
|
*/
|
|
127
128
|
commands.pushFile = async function pushFile (remotePath, base64Data) {
|
|
128
129
|
if (remotePath.endsWith('/')) {
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
throw new errors.InvalidArgumentError(
|
|
131
|
+
`It is expected that remote path points to a file and not to a folder. ` +
|
|
132
|
+
`'${remotePath}' is given instead`
|
|
133
|
+
);
|
|
131
134
|
}
|
|
132
135
|
const localFile = await tempDir.path({prefix: 'appium', suffix: '.tmp'});
|
|
133
136
|
if (_.isArray(base64Data)) {
|
|
@@ -141,7 +144,8 @@ commands.pushFile = async function pushFile (remotePath, base64Data) {
|
|
|
141
144
|
await fs.writeFile(localFile, content.toString('binary'), 'binary');
|
|
142
145
|
if (remotePath.startsWith(CONTAINER_PATH_MARKER)) {
|
|
143
146
|
const [packageId, pathInContainer] = parseContainerPath(remotePath);
|
|
144
|
-
log.debug(`Parsed package identifier '${packageId}' from '${remotePath}'.
|
|
147
|
+
this.log.debug(`Parsed package identifier '${packageId}' from '${remotePath}'. ` +
|
|
148
|
+
`Will put the data into '${pathInContainer}'`);
|
|
145
149
|
tmpDestination = `/data/local/tmp/${path.posix.basename(pathInContainer)}`;
|
|
146
150
|
try {
|
|
147
151
|
await this.adb.shell(
|
|
@@ -155,7 +159,7 @@ commands.pushFile = async function pushFile (remotePath, base64Data) {
|
|
|
155
159
|
`cp -f '${escapePath(tmpDestination)}' '${escapePath(pathInContainer)}'`
|
|
156
160
|
]);
|
|
157
161
|
} catch (e) {
|
|
158
|
-
log.errorAndThrow(`Cannot access the container of '${packageId}' application. ` +
|
|
162
|
+
this.log.errorAndThrow(`Cannot access the container of '${packageId}' application. ` +
|
|
159
163
|
`Is the application installed and has 'debuggable' build option set to true? ` +
|
|
160
164
|
`Original error: ${e.message}`);
|
|
161
165
|
}
|
|
@@ -165,7 +169,7 @@ commands.pushFile = async function pushFile (remotePath, base64Data) {
|
|
|
165
169
|
|
|
166
170
|
// if we have pushed a file, it might be a media file, so ensure that
|
|
167
171
|
// apps know about it
|
|
168
|
-
await scanMedia(this.adb, remotePath);
|
|
172
|
+
await scanMedia(this.adb, remotePath, this.log);
|
|
169
173
|
}
|
|
170
174
|
} finally {
|
|
171
175
|
if (await fs.exists(localFile)) {
|
|
@@ -224,7 +228,7 @@ async function deleteFileOrFolder (adb, remotePath) {
|
|
|
224
228
|
let pkgId = null;
|
|
225
229
|
if (remotePath.startsWith(CONTAINER_PATH_MARKER)) {
|
|
226
230
|
const [packageId, pathInContainer] = parseContainerPath(remotePath);
|
|
227
|
-
log.debug(`Parsed package identifier '${packageId}' from '${remotePath}'`);
|
|
231
|
+
this.log.debug(`Parsed package identifier '${packageId}' from '${remotePath}'`);
|
|
228
232
|
dstPath = pathInContainer;
|
|
229
233
|
pkgId = packageId;
|
|
230
234
|
}
|
|
@@ -233,22 +237,22 @@ async function deleteFileOrFolder (adb, remotePath) {
|
|
|
233
237
|
try {
|
|
234
238
|
await adb.shell(['run-as', pkgId, 'ls']);
|
|
235
239
|
} catch (e) {
|
|
236
|
-
log.errorAndThrow(`Cannot access the container of '${pkgId}' application. ` +
|
|
240
|
+
this.log.errorAndThrow(`Cannot access the container of '${pkgId}' application. ` +
|
|
237
241
|
`Is the application installed and has 'debuggable' build option set to true? ` +
|
|
238
242
|
`Original error: ${e.message}`);
|
|
239
243
|
}
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
if (!await isPresent(dstPath, pkgId)) {
|
|
243
|
-
log.info(`The item at '${dstPath}' does not exist. Perhaps, already deleted?`);
|
|
247
|
+
this.log.info(`The item at '${dstPath}' does not exist. Perhaps, already deleted?`);
|
|
244
248
|
return false;
|
|
245
249
|
}
|
|
246
250
|
|
|
247
251
|
const expectsFile = !remotePath.endsWith('/');
|
|
248
252
|
if (expectsFile && !await isFile(dstPath, pkgId)) {
|
|
249
|
-
log.errorAndThrow(`The item at '${dstPath}' is not a file`);
|
|
253
|
+
this.log.errorAndThrow(`The item at '${dstPath}' is not a file`);
|
|
250
254
|
} else if (!expectsFile && !await isDir(dstPath, pkgId)) {
|
|
251
|
-
log.errorAndThrow(`The item at '${dstPath}' is not a folder`);
|
|
255
|
+
this.log.errorAndThrow(`The item at '${dstPath}' is not a folder`);
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
if (pkgId) {
|
|
@@ -258,7 +262,7 @@ async function deleteFileOrFolder (adb, remotePath) {
|
|
|
258
262
|
await adb.shell(['rm', `-f${expectsFile ? '' : 'r'}`, dstPath]);
|
|
259
263
|
}
|
|
260
264
|
if (await isPresent(dstPath, pkgId)) {
|
|
261
|
-
log.errorAndThrow(`The item at '${dstPath}' still exists after being deleted. ` +
|
|
265
|
+
this.log.errorAndThrow(`The item at '${dstPath}' still exists after being deleted. ` +
|
|
262
266
|
`Is it writable?`);
|
|
263
267
|
}
|
|
264
268
|
return true;
|
|
@@ -283,11 +287,13 @@ async function deleteFileOrFolder (adb, remotePath) {
|
|
|
283
287
|
commands.mobileDeleteFile = async function mobileDeleteFile (opts = {}) {
|
|
284
288
|
const {remotePath} = opts;
|
|
285
289
|
if (!remotePath) {
|
|
286
|
-
|
|
290
|
+
throw new errors.InvalidArgumentError(`The 'remotePath' argument is mandatory`);
|
|
287
291
|
}
|
|
288
292
|
if (remotePath.endsWith('/')) {
|
|
289
|
-
|
|
290
|
-
|
|
293
|
+
throw new errors.InvalidArgumentError(
|
|
294
|
+
`It is expected that remote path points to a folder and not to a file. ` +
|
|
295
|
+
`'${remotePath}' is given instead`
|
|
296
|
+
);
|
|
291
297
|
}
|
|
292
298
|
return await deleteFileOrFolder(this.adb, remotePath);
|
|
293
299
|
};
|
package/lib/commands/general.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import androidHelpers from '../android-helpers';
|
|
3
3
|
import { util } from '@appium/support';
|
|
4
|
-
import log from '../logger';
|
|
5
4
|
import moment from 'moment';
|
|
6
5
|
import { longSleep } from 'asyncbox';
|
|
6
|
+
import { errors } from '@appium/base-driver';
|
|
7
7
|
|
|
8
8
|
const MOMENT_FORMAT_ISO8601 = 'YYYY-MM-DDTHH:mm:ssZ';
|
|
9
9
|
|
|
@@ -36,13 +36,13 @@ commands.doSendKeys = async function doSendKeys (params) {
|
|
|
36
36
|
* @return {string} Formatted datetime string or the raw command output if formatting fails
|
|
37
37
|
*/
|
|
38
38
|
commands.getDeviceTime = async function getDeviceTime (format = MOMENT_FORMAT_ISO8601) {
|
|
39
|
-
log.debug('Attempting to capture android device date and time. ' +
|
|
39
|
+
this.log.debug('Attempting to capture android device date and time. ' +
|
|
40
40
|
`The format specifier is '${format}'`);
|
|
41
41
|
const deviceTimestamp = (await this.adb.shell(['date', '+%Y-%m-%dT%T%z'])).trim();
|
|
42
|
-
log.debug(`Got device timestamp: ${deviceTimestamp}`);
|
|
42
|
+
this.log.debug(`Got device timestamp: ${deviceTimestamp}`);
|
|
43
43
|
const parsedTimestamp = moment.utc(deviceTimestamp, 'YYYY-MM-DDTHH:mm:ssZZ');
|
|
44
44
|
if (!parsedTimestamp.isValid()) {
|
|
45
|
-
log.warn('Cannot parse the returned timestamp. Returning as is');
|
|
45
|
+
this.log.warn('Cannot parse the returned timestamp. Returning as is');
|
|
46
46
|
return deviceTimestamp;
|
|
47
47
|
}
|
|
48
48
|
return parsedTimestamp.utcOffset(parsedTimestamp._tzm || 0).format(format);
|
|
@@ -128,7 +128,7 @@ commands.background = async function background (seconds) {
|
|
|
128
128
|
const progressCb = ({elapsedMs, progress}) => {
|
|
129
129
|
const waitSecs = (elapsedMs / 1000).toFixed(0);
|
|
130
130
|
const progressPct = (progress * 100).toFixed(2);
|
|
131
|
-
log.debug(`Waited ${waitSecs}s so far (${progressPct}%)`);
|
|
131
|
+
this.log.debug(`Waited ${waitSecs}s so far (${progressPct}%)`);
|
|
132
132
|
};
|
|
133
133
|
await longSleep(sleepMs, {thresholdMs, intervalMs, progressCb});
|
|
134
134
|
|
|
@@ -138,7 +138,7 @@ commands.background = async function background (seconds) {
|
|
|
138
138
|
args = this._cachedActivityArgs[`${appPackage}/${appActivity}`];
|
|
139
139
|
} else {
|
|
140
140
|
try {
|
|
141
|
-
log.debug(`Activating app '${appPackage}' in order to restore it`);
|
|
141
|
+
this.log.debug(`Activating app '${appPackage}' in order to restore it`);
|
|
142
142
|
await this.activateApp(appPackage);
|
|
143
143
|
return true;
|
|
144
144
|
} catch (ign) {}
|
|
@@ -165,14 +165,14 @@ commands.background = async function background (seconds) {
|
|
|
165
165
|
stopApp: false};
|
|
166
166
|
}
|
|
167
167
|
args = await util.filterObject(args);
|
|
168
|
-
log.debug(`Bringing application back to foreground with arguments: ${JSON.stringify(args)}`);
|
|
168
|
+
this.log.debug(`Bringing application back to foreground with arguments: ${JSON.stringify(args)}`);
|
|
169
169
|
return await this.adb.startApp(args);
|
|
170
170
|
};
|
|
171
171
|
|
|
172
172
|
commands.getStrings = async function getStrings (language) {
|
|
173
173
|
if (!language) {
|
|
174
174
|
language = await this.adb.getDeviceLanguage();
|
|
175
|
-
log.info(`No language specified, returning strings for: ${language}`);
|
|
175
|
+
this.log.info(`No language specified, returning strings for: ${language}`);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// Clients require the resulting mapping to have both keys
|
|
@@ -207,7 +207,7 @@ commands.launchApp = async function launchApp () {
|
|
|
207
207
|
commands.startActivity = async function startActivity (appPackage, appActivity,
|
|
208
208
|
appWaitPackage, appWaitActivity, intentAction, intentCategory, intentFlags,
|
|
209
209
|
optionalIntentArguments, dontStopAppOnReset) {
|
|
210
|
-
log.debug(`Starting package '${appPackage}' and activity '${appActivity}'`);
|
|
210
|
+
this.log.debug(`Starting package '${appPackage}' and activity '${appActivity}'`);
|
|
211
211
|
|
|
212
212
|
// dontStopAppOnReset is both an argument here, and a desired capability
|
|
213
213
|
// if the argument is set, use it, otherwise use the cap
|
|
@@ -277,7 +277,7 @@ commands.getDisplayDensity = async function getDisplayDensity () {
|
|
|
277
277
|
if (!isNaN(val)) {
|
|
278
278
|
return val;
|
|
279
279
|
}
|
|
280
|
-
log.debug(`Parsed density value was NaN: "${out}"`);
|
|
280
|
+
this.log.debug(`Parsed density value was NaN: "${out}"`);
|
|
281
281
|
}
|
|
282
282
|
// fallback to trying property for emulators
|
|
283
283
|
out = await this.adb.shell(['getprop', 'qemu.sf.lcd_density']);
|
|
@@ -286,16 +286,16 @@ commands.getDisplayDensity = async function getDisplayDensity () {
|
|
|
286
286
|
if (!isNaN(val)) {
|
|
287
287
|
return val;
|
|
288
288
|
}
|
|
289
|
-
log.debug(`Parsed density value was NaN: "${out}"`);
|
|
289
|
+
this.log.debug(`Parsed density value was NaN: "${out}"`);
|
|
290
290
|
}
|
|
291
291
|
// couldn't get anything, so error out
|
|
292
|
-
log.errorAndThrow('Failed to get display density property.');
|
|
292
|
+
this.log.errorAndThrow('Failed to get display density property.');
|
|
293
293
|
};
|
|
294
294
|
|
|
295
295
|
commands.mobilePerformEditorAction = async function mobilePerformEditorAction (opts = {}) {
|
|
296
296
|
const {action} = opts;
|
|
297
297
|
if (!util.hasValue(action)) {
|
|
298
|
-
|
|
298
|
+
throw new errors.InvalidArgumentError(`'action' argument is required`);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
await this.adb.performEditorAction(action);
|
|
@@ -328,7 +328,7 @@ commands.mobileChangePermissions = async function mobileChangePermissions (opts
|
|
|
328
328
|
action = PERMISSION_ACTION.GRANT,
|
|
329
329
|
} = opts;
|
|
330
330
|
if (!util.hasValue(permissions)) {
|
|
331
|
-
|
|
331
|
+
throw new errors.InvalidArgumentError(`'permissions' argument is required`);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
let actionFunc;
|
|
@@ -340,9 +340,8 @@ commands.mobileChangePermissions = async function mobileChangePermissions (opts
|
|
|
340
340
|
actionFunc = (appPackage, permission) => this.adb.revokePermission(appPackage, permission);
|
|
341
341
|
break;
|
|
342
342
|
default:
|
|
343
|
-
|
|
343
|
+
throw new errors.InvalidArgumentError(`Unknown action '${action}'. ` +
|
|
344
344
|
`Only ${JSON.stringify(_.values(PERMISSION_ACTION))} actions are supported`);
|
|
345
|
-
break;
|
|
346
345
|
}
|
|
347
346
|
for (const permission of (_.isArray(permissions) ? permissions : [permissions])) {
|
|
348
347
|
await actionFunc(appPackage, permission);
|
|
@@ -389,9 +388,8 @@ commands.mobileGetPermissions = async function mobileGetPermissions (opts = {})
|
|
|
389
388
|
actionFunc = (appPackage) => this.adb.getDeniedPermissions(appPackage);
|
|
390
389
|
break;
|
|
391
390
|
default:
|
|
392
|
-
|
|
391
|
+
throw new errors.InvalidArgumentError(`Unknown permissions type '${type}'. ` +
|
|
393
392
|
`Only ${JSON.stringify(_.values(PERMISSIONS_TYPE))} types are supported`);
|
|
394
|
-
break;
|
|
395
393
|
}
|
|
396
394
|
return await actionFunc(appPackage);
|
|
397
395
|
};
|