detox 20.45.1 → 20.46.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.
Files changed (46) hide show
  1. package/Detox-android/com/wix/detox/{20.45.1/detox-20.45.1-sources.jar → 20.46.0/detox-20.46.0-sources.jar} +0 -0
  2. package/Detox-android/com/wix/detox/20.46.0/detox-20.46.0-sources.jar.md5 +1 -0
  3. package/Detox-android/com/wix/detox/20.46.0/detox-20.46.0-sources.jar.sha1 +1 -0
  4. package/Detox-android/com/wix/detox/20.46.0/detox-20.46.0-sources.jar.sha256 +1 -0
  5. package/Detox-android/com/wix/detox/20.46.0/detox-20.46.0-sources.jar.sha512 +1 -0
  6. package/Detox-android/com/wix/detox/{20.45.1/detox-20.45.1.pom → 20.46.0/detox-20.46.0.pom} +1 -1
  7. package/Detox-android/com/wix/detox/20.46.0/detox-20.46.0.pom.md5 +1 -0
  8. package/Detox-android/com/wix/detox/20.46.0/detox-20.46.0.pom.sha1 +1 -0
  9. package/Detox-android/com/wix/detox/20.46.0/detox-20.46.0.pom.sha256 +1 -0
  10. package/Detox-android/com/wix/detox/20.46.0/detox-20.46.0.pom.sha512 +1 -0
  11. package/Detox-android/com/wix/detox/maven-metadata.xml +4 -4
  12. package/Detox-android/com/wix/detox/maven-metadata.xml.md5 +1 -1
  13. package/Detox-android/com/wix/detox/maven-metadata.xml.sha1 +1 -1
  14. package/Detox-android/com/wix/detox/maven-metadata.xml.sha256 +1 -1
  15. package/Detox-android/com/wix/detox/maven-metadata.xml.sha512 +1 -1
  16. package/Detox-ios-framework.tbz +0 -0
  17. package/Detox-ios-src.tbz +0 -0
  18. package/Detox-ios-xcuitest.tbz +0 -0
  19. package/detox.d.ts +39 -0
  20. package/package.json +2 -2
  21. package/src/configuration/composeDeviceConfig.js +99 -0
  22. package/src/devices/allocation/DeviceAllocator.js +3 -2
  23. package/src/devices/allocation/drivers/AllocationDriverBase.d.ts +1 -1
  24. package/src/devices/allocation/drivers/android/AndroidAllocDriver.js +68 -0
  25. package/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js +10 -8
  26. package/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js +9 -7
  27. package/src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.js +8 -7
  28. package/src/devices/allocation/drivers/android/utils/DeviceInitCache.js +21 -0
  29. package/src/devices/allocation/drivers/android/utils/SystemUICfgHelper.js +187 -0
  30. package/src/devices/allocation/drivers/android/utils/systemUICfgPresets.js +35 -0
  31. package/src/devices/runtime/drivers/android/AndroidDriver.js +1 -1
  32. package/src/errors/DetoxConfigErrorComposer.js +4 -0
  33. package/src/realms/DetoxPrimaryContext.js +1 -1
  34. package/Detox-android/com/wix/detox/20.45.1/detox-20.45.1-sources.jar.md5 +0 -1
  35. package/Detox-android/com/wix/detox/20.45.1/detox-20.45.1-sources.jar.sha1 +0 -1
  36. package/Detox-android/com/wix/detox/20.45.1/detox-20.45.1-sources.jar.sha256 +0 -1
  37. package/Detox-android/com/wix/detox/20.45.1/detox-20.45.1-sources.jar.sha512 +0 -1
  38. package/Detox-android/com/wix/detox/20.45.1/detox-20.45.1.pom.md5 +0 -1
  39. package/Detox-android/com/wix/detox/20.45.1/detox-20.45.1.pom.sha1 +0 -1
  40. package/Detox-android/com/wix/detox/20.45.1/detox-20.45.1.pom.sha256 +0 -1
  41. package/Detox-android/com/wix/detox/20.45.1/detox-20.45.1.pom.sha512 +0 -1
  42. /package/Detox-android/com/wix/detox/{20.45.1/detox-20.45.1.aar → 20.46.0/detox-20.46.0.aar} +0 -0
  43. /package/Detox-android/com/wix/detox/{20.45.1/detox-20.45.1.aar.md5 → 20.46.0/detox-20.46.0.aar.md5} +0 -0
  44. /package/Detox-android/com/wix/detox/{20.45.1/detox-20.45.1.aar.sha1 → 20.46.0/detox-20.46.0.aar.sha1} +0 -0
  45. /package/Detox-android/com/wix/detox/{20.45.1/detox-20.45.1.aar.sha256 → 20.46.0/detox-20.46.0.aar.sha256} +0 -0
  46. /package/Detox-android/com/wix/detox/{20.45.1/detox-20.45.1.aar.sha512 → 20.46.0/detox-20.46.0.aar.sha512} +0 -0
@@ -0,0 +1 @@
1
+ f92fee8e55802955d0d0c8c0e8d17a9d
@@ -0,0 +1 @@
1
+ 0e247005d3b10b618a1e60fc4c5980f39c0f4161
@@ -0,0 +1 @@
1
+ d62189a293ef9a32eba47253df150f55af4e6c3f1389fab5d7e655abd6d2f450
@@ -0,0 +1 @@
1
+ 6638d6c8dd8491bd39bd8ce705ea287df708fe295956da5ca575fbf3c93eaba22ef37a26f8ac035413146416dfc08d0d48b436726d472e8bfa2b4ab8ffeee5f0
@@ -3,7 +3,7 @@
3
3
  <modelVersion>4.0.0</modelVersion>
4
4
  <groupId>com.wix</groupId>
5
5
  <artifactId>detox</artifactId>
6
- <version>20.45.1</version>
6
+ <version>20.46.0</version>
7
7
  <packaging>aar</packaging>
8
8
  <name>Detox</name>
9
9
  <description>Gray box end-to-end testing and automation library for mobile apps</description>
@@ -0,0 +1 @@
1
+ 8743ffbe906bda40e18f4f56f0b17731
@@ -0,0 +1 @@
1
+ f238ef617997264a605dd604a42b1580b4c10d69
@@ -0,0 +1 @@
1
+ adf08d12927e279de3e410db7bb58191baaf62b351090f082e476cec1deaf7e0
@@ -0,0 +1 @@
1
+ 2af20a02179995ca60fe481cd9a80e0981520c04f7369350db8935bb59248737bf06792f6f7208f4ee9e6b3a7165484e4a2471b0c657c12f603e0fae865de1f4
@@ -3,11 +3,11 @@
3
3
  <groupId>com.wix</groupId>
4
4
  <artifactId>detox</artifactId>
5
5
  <versioning>
6
- <latest>20.45.1</latest>
7
- <release>20.45.1</release>
6
+ <latest>20.46.0</latest>
7
+ <release>20.46.0</release>
8
8
  <versions>
9
- <version>20.45.1</version>
9
+ <version>20.46.0</version>
10
10
  </versions>
11
- <lastUpdated>20251030165738</lastUpdated>
11
+ <lastUpdated>20251116141022</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 91b7d57a8c6d220ea8c0b302350ef444
1
+ 2188c9401bfa538bb0576bc5167edf47
@@ -1 +1 @@
1
- 0431fedec1af54a5b47e09664c672f8003b7f995
1
+ f6ca6f49b2cdfd86faae22d064c6a8f584ab5479
@@ -1 +1 @@
1
- cf40046dbf6b26afe2d8f472072098db3f5b841c1f030a8e11dad03318aca4b7
1
+ 15ef80746452d06400cb3c56a76949bbc5cb34b31142fa6b5e9a7c7a672d67dd
@@ -1 +1 @@
1
- c0081527b1fa7a3cea51e8c56321165cf7295dc351def86ca6c102b3a73e26efc3e8ab765e1ccf625fb4df2d17252d6d1aff03c471c4dfd173db32456880f652
1
+ 8461cb4d98b01f06622281fe12897cd437d33b2b40914f761fd4d5b38045095117d37817686411d6a5744e6e4ef1d35294386a61680dac5549177ae9ac9ff5e8
Binary file
package/Detox-ios-src.tbz CHANGED
Binary file
Binary file
package/detox.d.ts CHANGED
@@ -363,6 +363,42 @@ declare global {
363
363
  [prop: string]: unknown;
364
364
  }
365
365
 
366
+ /**
367
+ * `minimal`: Configuration for a minimal system UI.
368
+ * `genymotion`: Configuration for a Genymotion-equivalent system UI.
369
+ *
370
+ * Visit https://github.com/wix/Detox/blob/master/detox/src/devices/allocation/drivers/android/utils/systemUICfgPresets.js to learn about
371
+ * the specifics of each preset.
372
+ */
373
+ type DetoxSystemUIPresetName = 'minimal' | 'genymotion';
374
+ type DetoxSystemUI = DetoxSystemUIPresetName | DetoxSystemUIConfig;
375
+
376
+ interface DetoxSystemUIConfig {
377
+ extends?: DetoxSystemUIPresetName;
378
+
379
+ /**
380
+ * Note: For 'hide' to work in Google emulators, need to set `hw.keyboard=yes` in AVD configuration (i.e.
381
+ * in manually `config.ini` file or via AVD Manager on Android Studio).
382
+ */
383
+ keyboard?: 'hide' | 'show' | null;
384
+ touches?: 'hide' | 'show' | null;
385
+ pointerLocationBar?: 'hide' | 'show' | null;
386
+ /** Note: 2-button mode is not supported in recent Android versions; Detox ignores it to avoid confusion. */
387
+ navigationMode?: '3-button' | 'gesture' | null;
388
+ statusBar?: DetoxSystemUIStatusBarConfig;
389
+ }
390
+
391
+ interface DetoxSystemUIStatusBarConfig {
392
+ notifications?: 'show' | 'hide' | null;
393
+ wifiSignal?: 'strong' | 'weak' | 'none' | null;
394
+ /** Disclaimer: Some Android versions fail to set the network type (3g, lte, etc.) */
395
+ cellSignal?: 'strong' | 'weak' | 'none' | null;
396
+ batteryLevel?: 'full' | 'half' | 'low' | null;
397
+ charging?: boolean | null;
398
+ /** In "hhmm" format (e.g. "1234" for 12:34) */
399
+ clock?: string | null;
400
+ }
401
+
366
402
  type DetoxBuiltInDeviceConfig =
367
403
  | DetoxIosSimulatorDriverConfig
368
404
  | DetoxAttachedAndroidDriverConfig
@@ -378,6 +414,9 @@ declare global {
378
414
  interface DetoxSharedAndroidDriverConfig {
379
415
  forceAdbInstall?: boolean;
380
416
  utilBinaryPaths?: string[];
417
+
418
+ /** Disclaimer: Some features are not seamlessly supported by all Android versions and vendors. */
419
+ systemUI?: DetoxSystemUI;
381
420
  }
382
421
 
383
422
  interface DetoxAttachedAndroidDriverConfig extends DetoxSharedAndroidDriverConfig {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "detox",
3
3
  "description": "E2E tests and automation for mobile",
4
- "version": "20.45.1",
4
+ "version": "20.46.0",
5
5
  "bin": {
6
6
  "detox": "local-cli/cli.js"
7
7
  },
@@ -120,5 +120,5 @@
120
120
  "browserslist": [
121
121
  "node 14"
122
122
  ],
123
- "gitHead": "43b45aefe4249aebe34622f63389b363591362ae"
123
+ "gitHead": "992fb3900be3b5f95c2a38d2bd12440f048b3363"
124
124
  }
@@ -60,6 +60,97 @@ function composeDeviceConfigFromAliased(opts) {
60
60
  return { ...deviceConfig };
61
61
  }
62
62
 
63
+ /**
64
+ * Validates systemUI configuration
65
+ * @param {string | object} systemUI - The systemUI configuration
66
+ * @param {string} deviceAlias - The device alias for error reporting
67
+ * @param {DetoxConfigErrorComposer} errorComposer - Error composer instance
68
+ */
69
+ function validateSystemUIConfig(systemUI, deviceAlias, errorComposer) {
70
+ if (_.isString(systemUI)) {
71
+ if (!['minimal', 'genymotion'].includes(systemUI)) {
72
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
73
+ }
74
+ return;
75
+ }
76
+
77
+ if (!_.isObject(systemUI)) {
78
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
79
+ }
80
+
81
+ if (systemUI.extends !== undefined) {
82
+ if (!['minimal', 'genymotion'].includes(systemUI.extends)) {
83
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
84
+ }
85
+ }
86
+
87
+ if (systemUI.keyboard !== undefined && systemUI.keyboard !== null) {
88
+ if (!['hide', 'show'].includes(systemUI.keyboard)) {
89
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
90
+ }
91
+ }
92
+
93
+ if (systemUI.touches !== undefined && systemUI.touches !== null) {
94
+ if (!['hide', 'show'].includes(systemUI.touches)) {
95
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
96
+ }
97
+ }
98
+
99
+ if (systemUI.pointerLocationBar !== undefined && systemUI.pointerLocationBar !== null) {
100
+ if (!['hide', 'show'].includes(systemUI.pointerLocationBar)) {
101
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
102
+ }
103
+ }
104
+
105
+ if (systemUI.navigationMode !== undefined && systemUI.navigationMode !== null) {
106
+ if (!['3-button', 'gesture'].includes(systemUI.navigationMode)) {
107
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
108
+ }
109
+ }
110
+
111
+ if (systemUI.statusBar !== undefined && systemUI.statusBar !== null) {
112
+ if (!_.isObject(systemUI.statusBar)) {
113
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
114
+ }
115
+
116
+ if (systemUI.statusBar.notifications !== undefined && systemUI.statusBar.notifications !== null) {
117
+ if (!['hide', 'show'].includes(systemUI.statusBar.notifications)) {
118
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
119
+ }
120
+ }
121
+
122
+ if (systemUI.statusBar.wifiSignal !== undefined && systemUI.statusBar.wifiSignal !== null) {
123
+ if (!['weak', 'strong', 'none'].includes(systemUI.statusBar.wifiSignal)) {
124
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
125
+ }
126
+ }
127
+
128
+ if (systemUI.statusBar.cellSignal !== undefined && systemUI.statusBar.cellSignal !== null) {
129
+ if (!['strong', 'weak', 'none'].includes(systemUI.statusBar.cellSignal)) {
130
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
131
+ }
132
+ }
133
+
134
+ if (systemUI.statusBar.batteryLevel !== undefined && systemUI.statusBar.batteryLevel !== null) {
135
+ if (!['full', 'half', 'low'].includes(systemUI.statusBar.batteryLevel)) {
136
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
137
+ }
138
+ }
139
+
140
+ if (systemUI.statusBar.charging !== undefined && systemUI.statusBar.charging !== null) {
141
+ if (!_.isBoolean(systemUI.statusBar.charging)) {
142
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
143
+ }
144
+ }
145
+
146
+ if (systemUI.statusBar.clock !== undefined && systemUI.statusBar.clock !== null) {
147
+ if (!_.isString(systemUI.statusBar.clock) || !/^\d{2}\d{2}$/.test(systemUI.statusBar.clock)) {
148
+ throw errorComposer.malformedDeviceProperty(deviceAlias, 'systemUI');
149
+ }
150
+ }
151
+ }
152
+ }
153
+
63
154
  /**
64
155
  * @param {DetoxConfigErrorComposer} errorComposer
65
156
  * @param {Detox.DetoxDeviceConfig} deviceConfig
@@ -147,6 +238,14 @@ function validateDeviceConfig({ deviceConfig, errorComposer, deviceAlias }) {
147
238
  }
148
239
  }
149
240
 
241
+ if (deviceConfig.systemUI !== undefined) {
242
+ validateSystemUIConfig(deviceConfig.systemUI, deviceAlias, errorComposer);
243
+
244
+ if (!deviceConfig.type.match(/^android\.(emulator|genycloud|attached)$/)) {
245
+ throw errorComposer.unsupportedDeviceProperty(deviceAlias, 'systemUI');
246
+ }
247
+ }
248
+
150
249
  if (_.isObject(deviceConfig.device)) {
151
250
  const expectedProperties = EXPECTED_DEVICE_MATCHER_PROPS[deviceConfig.type];
152
251
  /* istanbul ignore else */
@@ -50,13 +50,14 @@ class DeviceAllocator {
50
50
 
51
51
  /**
52
52
  * @param {DeviceCookie} cookie
53
+ * @param {{ deviceConfig: Detox.DetoxDeviceConfig }} [configs]
53
54
  * @returns {Promise<DeviceCookie>}
54
55
  */
55
- async postAllocate(cookie) {
56
+ async postAllocate(cookie, configs) {
56
57
  const tid = this._ids.get(cookie.id);
57
58
  return await log.trace.complete({ data: cookie, id: tid }, `post-allocate: ${cookie.id}`, async () => {
58
59
  const updatedCookie = typeof this._driver.postAllocate === 'function'
59
- ? await this._driver.postAllocate(cookie)
60
+ ? await this._driver.postAllocate(cookie, configs)
60
61
  : undefined;
61
62
 
62
63
  return updatedCookie || cookie;
@@ -8,7 +8,7 @@ export interface DeallocOptions {
8
8
  export interface AllocationDriverBase {
9
9
  init?(): Promise<void>;
10
10
  allocate(deviceConfig: any): Promise<DeviceCookie>;
11
- postAllocate?(deviceCookie: DeviceCookie): Promise<DeviceCookie | void>;
11
+ postAllocate?(deviceCookie: DeviceCookie, configs?: { deviceConfig: Detox.DetoxDeviceConfig }): Promise<DeviceCookie | void>;
12
12
  free(cookie: DeviceCookie, options: DeallocOptions): Promise<void>;
13
13
  cleanup?(): Promise<void>;
14
14
  emergencyCleanup?(): void;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @typedef {import('../AllocationDriverBase').AllocationDriverBase} AllocationDriverBase
3
+ * @typedef {import('../../../common/drivers/DeviceCookie').DeviceCookie} DeviceCookie
4
+ */
5
+
6
+ const log = require('../../../../utils/logger').child({ __filename });
7
+ const sleep = require('../../../../utils/sleep');
8
+
9
+ const DeviceInitCache = require('./utils/DeviceInitCache');
10
+ const SystemUIDemoMode = require('./utils/SystemUICfgHelper');
11
+
12
+ /**
13
+ * @abstract {AllocationDriverBase}
14
+ */
15
+ class AndroidAllocDriver {
16
+ /**
17
+ * @param {object} options
18
+ * @param {import('../../../common/drivers/android/exec/ADB')} options.adb
19
+ */
20
+ constructor({
21
+ adb
22
+ }) {
23
+ this._adb = adb;
24
+ this._systemUIInitCache = new DeviceInitCache();
25
+ }
26
+
27
+ /**
28
+ * @param {DeviceCookie & { adbName: string }} deviceCookie
29
+ * @param {{ deviceConfig: Detox.DetoxSharedAndroidDriverConfig }} configs
30
+ * @returns {Promise<DeviceCookie>}
31
+ */
32
+ async postAllocate(deviceCookie, { deviceConfig }) {
33
+ const { adbName } = deviceCookie;
34
+
35
+ if (deviceConfig.systemUI) {
36
+ if (this._systemUIInitCache.hasInitialized(adbName)) {
37
+ log.debug(`Skipping System UI setup for ${adbName}, already initialized`);
38
+ } else {
39
+ await this._setupSystemUI(deviceCookie, deviceConfig);
40
+ this._systemUIInitCache.setInitialized(adbName);
41
+ }
42
+ }
43
+ return deviceCookie;
44
+ }
45
+
46
+ /**
47
+ * @param {DeviceCookie & { adbName: string }} deviceCookie
48
+ * @param {Detox.DetoxSharedAndroidDriverConfig} deviceConfig
49
+ * @private
50
+ */
51
+ async _setupSystemUI(deviceCookie, deviceConfig) {
52
+ const { adbName } = deviceCookie;
53
+
54
+ const systemUIDemoMode = new SystemUIDemoMode({ adb: this._adb, adbName });
55
+ const systemUIConfig = systemUIDemoMode.resolveConfig(deviceConfig.systemUI);
56
+
57
+ log.debug(`Running keyboard behavior setup for ${adbName}`);
58
+ await systemUIDemoMode.setupKeyboardBehavior(systemUIConfig);
59
+
60
+ log.debug(`Running system UI setup for ${adbName}`);
61
+ await systemUIDemoMode.setupPointerIndicators(systemUIConfig);
62
+ await systemUIDemoMode.setupNavigationMode(systemUIConfig);
63
+ await systemUIDemoMode.setupStatusBar(systemUIConfig);
64
+ log.debug(`Finished system UI setup for ${adbName}`);
65
+ }
66
+ }
67
+
68
+ module.exports = AndroidAllocDriver;
@@ -1,12 +1,10 @@
1
1
  /**
2
- * @typedef {import('../../AllocationDriverBase').AllocationDriverBase} AllocationDriverBase
3
2
  * @typedef {import('../../../../common/drivers/android/cookies').AndroidDeviceCookie} AndroidDeviceCookie
4
3
  */
5
4
 
6
- /**
7
- * @implements {AllocationDriverBase}
8
- */
9
- class AttachedAndroidAllocDriver {
5
+ const AndroidAllocDriver = require('../AndroidAllocDriver');
6
+
7
+ class AttachedAndroidAllocDriver extends AndroidAllocDriver {
10
8
  /**
11
9
  * @param {object} options
12
10
  * @param {import('../../../../common/drivers/android/exec/ADB')} options.adb
@@ -14,7 +12,7 @@ class AttachedAndroidAllocDriver {
14
12
  * @param {import('../FreeDeviceFinder')} options.freeDeviceFinder
15
13
  */
16
14
  constructor({ adb, deviceRegistry, freeDeviceFinder }) {
17
- this._adb = adb;
15
+ super({ adb });
18
16
  this._deviceRegistry = deviceRegistry;
19
17
  this._freeDeviceFinder = freeDeviceFinder;
20
18
  }
@@ -36,13 +34,17 @@ class AttachedAndroidAllocDriver {
36
34
 
37
35
  /**
38
36
  * @param {AndroidDeviceCookie} deviceCookie
39
- * @returns {Promise<void>}
37
+ * @param {{ deviceConfig: Detox.DetoxSharedAndroidDriverConfig }} configs
38
+ * @returns {Promise<AndroidDeviceCookie>}
40
39
  */
41
- async postAllocate(deviceCookie) {
40
+ async postAllocate(deviceCookie, configs) {
42
41
  const { adbName } = deviceCookie;
43
42
 
44
43
  await this._adb.apiLevel(adbName);
45
44
  await this._adb.unlockScreen(adbName);
45
+ await super.postAllocate(deviceCookie, configs);
46
+
47
+ return deviceCookie;
46
48
  }
47
49
 
48
50
  /**
@@ -1,18 +1,15 @@
1
1
  /**
2
- * @typedef {import('../../AllocationDriverBase').AllocationDriverBase} AllocationDriverBase
3
2
  * @typedef {import('../../../../common/drivers/android/cookies').AndroidDeviceCookie} AndroidDeviceCookie
4
3
  */
5
4
 
6
5
  const _ = require('lodash');
7
6
 
8
7
  const log = require('../../../../../utils/logger').child({ cat: 'device,device-allocation' });
8
+ const AndroidAllocDriver = require('../AndroidAllocDriver');
9
9
 
10
10
  const { patchAvdSkinConfig } = require('./patchAvdSkinConfig');
11
11
 
12
- /**
13
- * @implements {AllocationDriverBase}
14
- */
15
- class EmulatorAllocDriver {
12
+ class EmulatorAllocDriver extends AndroidAllocDriver {
16
13
  /**
17
14
  * @param {object} options
18
15
  * @param {import('../../../../common/drivers/android/exec/ADB')} options.adb
@@ -34,7 +31,7 @@ class EmulatorAllocDriver {
34
31
  emulatorVersionResolver,
35
32
  emulatorLauncher
36
33
  }) {
37
- this._adb = adb;
34
+ super({ adb });
38
35
  this._avdValidator = avdValidator;
39
36
  this._deviceRegistry = deviceRegistry;
40
37
  this._emulatorVersionResolver = emulatorVersionResolver;
@@ -88,14 +85,19 @@ class EmulatorAllocDriver {
88
85
 
89
86
  /**
90
87
  * @param {AndroidDeviceCookie} deviceCookie
88
+ * @param {{ deviceConfig: Detox.DetoxSharedAndroidDriverConfig }} configs
89
+ * @returns {Promise<AndroidDeviceCookie>}
91
90
  */
92
- async postAllocate(deviceCookie) {
91
+ async postAllocate(deviceCookie, configs) {
93
92
  const { adbName } = deviceCookie;
94
93
 
95
94
  await this._emulatorLauncher.awaitEmulatorBoot(adbName);
96
95
  await this._adb.apiLevel(adbName);
97
96
  await this._adb.disableAndroidAnimations(adbName);
98
97
  await this._adb.unlockScreen(adbName);
98
+ await super.postAllocate(deviceCookie, configs);
99
+
100
+ return deviceCookie;
99
101
  }
100
102
 
101
103
  /**
@@ -1,10 +1,10 @@
1
1
  /**
2
- * @typedef {import('../../AllocationDriverBase').AllocationDriverBase} AllocationDriverBase
3
2
  * @typedef {import('../../../../common/drivers/android/cookies').GenycloudEmulatorCookie} GenycloudEmulatorCookie
4
3
  */
5
4
 
6
5
  const Timer = require('../../../../../utils/Timer');
7
6
  const log = require('../../../../../utils/logger').child({ cat: 'device' });
7
+ const AndroidAllocDriver = require('../AndroidAllocDriver');
8
8
 
9
9
  const GenyRegistry = require('./GenyRegistry');
10
10
 
@@ -13,10 +13,7 @@ const events = {
13
13
  GENYCLOUD_TEARDOWN: { event: 'GENYCLOUD_TEARDOWN' },
14
14
  };
15
15
 
16
- /**
17
- * @implements {AllocationDriverBase}
18
- */
19
- class GenyAllocDriver {
16
+ class GenyAllocDriver extends AndroidAllocDriver {
20
17
  /**
21
18
  * @param {object} options
22
19
  * @param {import('../../../../common/drivers/android/exec/ADB')} options.adb
@@ -32,7 +29,7 @@ class GenyAllocDriver {
32
29
  instanceLauncher,
33
30
  recipeQuerying,
34
31
  }) {
35
- this._adb = adb;
32
+ super({ adb });
36
33
  this._detoxSessionId = detoxSession.id;
37
34
  this._genyRegistry = genyRegistry;
38
35
  this._instanceLauncher = instanceLauncher;
@@ -73,8 +70,10 @@ class GenyAllocDriver {
73
70
 
74
71
  /**
75
72
  * @param {GenycloudEmulatorCookie} cookie
73
+ * @param {{ deviceConfig: Detox.DetoxSharedAndroidDriverConfig }} configs
74
+ * @returns {Promise<GenycloudEmulatorCookie>}
76
75
  */
77
- async postAllocate(cookie) {
76
+ async postAllocate(cookie, configs) {
78
77
  const instance = await this._instanceLauncher.connect(cookie.instance);
79
78
  this._genyRegistry.updateInstance(instance);
80
79
 
@@ -88,6 +87,8 @@ class GenyAllocDriver {
88
87
  });
89
88
  }
90
89
 
90
+ await super.postAllocate(cookie, configs);
91
+
91
92
  return {
92
93
  ...cookie,
93
94
  adbName: instance.adbName,
@@ -0,0 +1,21 @@
1
+ class DeviceInitCache {
2
+ constructor() {
3
+ this._cache = new Map();
4
+ }
5
+
6
+ /**
7
+ * @param {string} adbName
8
+ * @returns {boolean}
9
+ */
10
+ hasInitialized(adbName) {
11
+ return this._cache.get(adbName) === true;
12
+ }
13
+
14
+ /** @param {string} adbName */
15
+ setInitialized(adbName) {
16
+ this._cache.set(adbName, true);
17
+ }
18
+ }
19
+
20
+ module.exports = DeviceInitCache;
21
+
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @typedef {import('../../../../common/drivers/DeviceCookie').DeviceCookie} DeviceCookie
3
+ */
4
+
5
+ const _ = require('lodash');
6
+
7
+ const { DetoxConfigError } = require('../../../../../errors');
8
+ const sleep = require('../../../../../utils/sleep');
9
+
10
+ const presets = require('./systemUICfgPresets');
11
+
12
+ const batteryPrecents = {
13
+ full: 100,
14
+ half: 50,
15
+ low: 20,
16
+ };
17
+
18
+ const navigationModes = {
19
+ // For clarity: 2-button mode is not supported in recent Android versions; Detox ignores it to avoid confusion
20
+ // '2-button': 'com.android.internal.systemui.navbar.twobutton',
21
+ '3-button': 'com.android.internal.systemui.navbar.threebutton',
22
+ 'gesture': 'com.android.internal.systemui.navbar.gestural',
23
+ };
24
+
25
+ /**
26
+ * Maps a nullish value (null or undefined) to undefined, otherwise applies the function.
27
+ *
28
+ * @param {any | undefined | null} value
29
+ * @param {(value: any) => any | undefined} fn
30
+ * @returns {any | undefined}
31
+ */
32
+ const nullishOrMap = (value, fn) => (value === undefined || value === null) ? undefined : fn(value);
33
+
34
+ /**
35
+ * Helper class for initializing Android System UI demo mode.
36
+ */
37
+ class SystemUICfgHelper {
38
+ /**
39
+ * @param {object} options
40
+ * @param {object} options.adb An ADB shell wrapper with a shell method
41
+ * @param {string} options.adbName The ADB device name
42
+ */
43
+ constructor({ adb, adbName }) {
44
+ this._adb = {
45
+ shell: async (cmd) => {
46
+ await adb.shell(adbName, cmd);
47
+ await sleep(200);
48
+ },
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Resolves the system UI configuration, handling presets and extends.
54
+ *
55
+ * @param {Detox.DetoxSystemUI} systemUICfg
56
+ * @returns {Detox.DetoxSystemUIConfig}
57
+ */
58
+ resolveConfig(systemUICfg) {
59
+ const preset = _.isString(systemUICfg) && this._resolvePreset(systemUICfg);
60
+ if (preset) {
61
+ return preset;
62
+ }
63
+
64
+ /** @type {Detox.DetoxSystemUIConfig} */
65
+ // @ts-ignore
66
+ const _systemUICfg = systemUICfg;
67
+
68
+ if (_systemUICfg.extends) {
69
+ const preset = this._resolvePreset(_systemUICfg.extends);
70
+ return _.chain({})
71
+ .merge(preset)
72
+ .merge(_systemUICfg)
73
+ .omit('extends')
74
+ .value();
75
+ }
76
+ return _systemUICfg;
77
+ }
78
+
79
+ /**
80
+ * @param {Detox.DetoxSystemUIConfig} systemUIConfig
81
+ */
82
+ async setupKeyboardBehavior(systemUIConfig) {
83
+ const showKbd = nullishOrMap(systemUIConfig.keyboard, (keyboard) => Number(keyboard === 'show'));
84
+
85
+ if (showKbd !== undefined) {
86
+ await this._adb.shell(`settings put Secure show_ime_with_hard_keyboard ${showKbd}`);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * @param {Detox.DetoxSystemUIConfig} systemUIConfig
92
+ */
93
+ async setupPointerIndicators(systemUIConfig) {
94
+ const showTouches = nullishOrMap(systemUIConfig.touches, (touches) => Number(touches === 'show'));
95
+ if (showTouches !== undefined) {
96
+ await this._adb.shell(`settings put system show_touches ${showTouches}`);
97
+ }
98
+
99
+ const showPointerLocationBar = nullishOrMap(systemUIConfig.pointerLocationBar, (pointerLocationBar) => Number(pointerLocationBar === 'show'));
100
+ if (showPointerLocationBar !== undefined) {
101
+ await this._adb.shell(`settings put system pointer_location ${showPointerLocationBar}`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * @param {Detox.DetoxSystemUIConfig} systemUIConfig
107
+ */
108
+ async setupNavigationMode(systemUIConfig) {
109
+ const navigationMode = nullishOrMap(systemUIConfig.navigationMode, (navigationMode) => navigationModes[navigationMode]);
110
+ if (navigationMode !== undefined) {
111
+ await this._adb.shell(`cmd overlay enable ${navigationMode}`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Ref: https://android.googlesource.com/platform/frameworks/base/+/master/packages/SystemUI/docs/demo_mode.md
117
+ *
118
+ * @param {Detox.DetoxSystemUIConfig} systemUIConfig
119
+ */
120
+ async setupStatusBar(systemUIConfig) {
121
+ const { statusBar: statusBarConfig } = systemUIConfig;
122
+ if (_.isUndefined(statusBarConfig)) {
123
+ return;
124
+ }
125
+
126
+ // Enable, then get out (= reset status-bar) and back into demo mode
127
+ await this._adb.shell('settings put global sysui_demo_allowed 1');
128
+ await this._adb.shell('am broadcast -a com.android.systemui.demo -e command exit');
129
+ await this._adb.shell('am broadcast -a com.android.systemui.demo -e command enter');
130
+
131
+ // Force status bar content
132
+ const notificationsVisible = nullishOrMap(statusBarConfig.notifications, (notifications) => Number(notifications === 'show'));
133
+ if (notificationsVisible !== undefined) {
134
+ await this._adb.shell(`am broadcast -a com.android.systemui.demo -e command notifications -e visible ${notificationsVisible}`);
135
+ }
136
+
137
+ const wifiSignal = nullishOrMap(statusBarConfig.wifiSignal, (wifiSignal) => wifiSignal);
138
+ if (wifiSignal !== undefined) {
139
+ await this._adb.shell('am broadcast -a com.android.systemui.demo -e command network -e wifi hide');
140
+ if (wifiSignal !== 'none') {
141
+ const wifiLevel = wifiSignal === 'strong' ? 4 : 2;
142
+ await this._adb.shell(`am broadcast -a com.android.systemui.demo -e command network -e wifi show -e level ${wifiLevel} -e fully true`);
143
+ }
144
+ }
145
+
146
+ const cellSignal = nullishOrMap(statusBarConfig.cellSignal, (cellSignal) => cellSignal);
147
+ if (cellSignal !== undefined) {
148
+ await this._adb.shell('am broadcast -a com.android.systemui.demo -e command network -e mobile hide');
149
+ if (cellSignal !== 'none') {
150
+ const cellLevel = cellSignal === 'strong' ? 4 : 2;
151
+ await this._adb.shell(`am broadcast -a com.android.systemui.demo -e command network -e mobile show -e level ${cellLevel} -e fully true -e datatype none`);
152
+ }
153
+ }
154
+
155
+ const clock = nullishOrMap(statusBarConfig.clock, (clock) => clock);
156
+ if (clock !== undefined) {
157
+ await this._adb.shell(`am broadcast -a com.android.systemui.demo -e command clock -e hhmm ${clock}`);
158
+ }
159
+
160
+ // Best to keep this last due to a "charging" indicator animation which can
161
+ // break UI changes made by consequent commands
162
+ const batteryLevel = nullishOrMap(statusBarConfig.batteryLevel, (level) => batteryPrecents[level]);
163
+ const charging = nullishOrMap(statusBarConfig.charging, (isCharging) => String(isCharging));
164
+ if (batteryLevel !== undefined || charging !== undefined) {
165
+ const command = 'am broadcast -a com.android.systemui.demo -e command battery' +
166
+ (batteryLevel !== undefined ? ` -e level ${batteryLevel}` : '') +
167
+ (charging !== undefined ? ` -e plugged ${charging}` : '');
168
+ await this._adb.shell(command);
169
+ await sleep(1500); // Wait for the charging animation to finish
170
+ }
171
+ }
172
+
173
+ /**
174
+ * @param {Detox.DetoxSystemUI} presetName
175
+ * @returns {Detox.DetoxSystemUIConfig | null}
176
+ * @private
177
+ */
178
+ _resolvePreset(presetName) {
179
+ const preset = presets[presetName];
180
+ if (!preset) {
181
+ throw new DetoxConfigError(`Invalid system UI preset name '${presetName}'`);
182
+ }
183
+ return preset;
184
+ }
185
+ }
186
+
187
+ module.exports = SystemUICfgHelper;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @type Detox.DetoxSystemUIConfig
3
+ */
4
+ const minimalConfigPreset = {
5
+ keyboard: 'hide',
6
+ touches: 'show',
7
+ navigationMode: '3-button',
8
+ statusBar: {
9
+ notifications: 'hide',
10
+ wifiSignal: 'strong',
11
+ cellSignal: 'none',
12
+ batteryLevel: 'full',
13
+ charging: false,
14
+ clock: '1337',
15
+ },
16
+ };
17
+
18
+ /**
19
+ * @type Detox.DetoxSystemUIConfig
20
+ */
21
+ const genyConfigPreset = {
22
+ keyboard: 'hide',
23
+ statusBar: {
24
+ notifications: 'hide',
25
+ wifiSignal: 'strong',
26
+ cellSignal: 'none',
27
+ batteryLevel: 'full',
28
+ charging: true,
29
+ },
30
+ };
31
+
32
+ module.exports = {
33
+ minimal: minimalConfigPreset,
34
+ genymotion: genyConfigPreset,
35
+ };
@@ -266,7 +266,7 @@ class AndroidDriver extends DeviceDriverBase {
266
266
  const call = duration ? EspressoDetoxApi.longPress(x, y, duration, _shouldIgnoreStatusBar): EspressoDetoxApi.longPress(x, y, _shouldIgnoreStatusBar);
267
267
  await this.invocationManager.execute(call);
268
268
  }
269
-
269
+
270
270
  async generateViewHierarchyXml(shouldInjectTestIds) {
271
271
  const hierarchy = await this.invocationManager.execute(DetoxApi.generateViewHierarchyXml(shouldInjectTestIds));
272
272
  return hierarchy.result;
@@ -406,6 +406,8 @@ Please check your Detox config${this._atPath()}`,
406
406
  return this._invalidPropertyType('headless', 'a boolean value', deviceAlias);
407
407
  case 'readonly':
408
408
  return this._invalidPropertyType('readonly', 'a boolean value', deviceAlias);
409
+ case 'systemUI':
410
+ return this._invalidPropertyType('systemUI', "'minimal', 'genymotion' or an object", deviceAlias);
409
411
  default:
410
412
  throw new DetoxInternalError(`Composing .malformedDeviceProperty(${propertyName}) is not implemented`);
411
413
  }
@@ -425,6 +427,8 @@ Please check your Detox config${this._atPath()}`,
425
427
  return this._unsupportedPropertyByDeviceType('readonly', ['android.emulator'], deviceAlias);
426
428
  case 'utilBinaryPaths':
427
429
  return this._unsupportedPropertyByDeviceType('utilBinaryPaths', ['android.attached', 'android.emulator', 'android.genycloud'], deviceAlias);
430
+ case 'systemUI':
431
+ return this._unsupportedPropertyByDeviceType('systemUI', ['android.emulator', 'android.genycloud', 'android.attached'], deviceAlias);
428
432
  default:
429
433
  throw new DetoxInternalError(`Composing .unsupportedDeviceProperty(${propertyName}) is not implemented`);
430
434
  }
@@ -174,7 +174,7 @@ class DetoxPrimaryContext extends DetoxContext {
174
174
  this[_cookieAllocators][deviceCookie.id] = deviceAllocator;
175
175
 
176
176
  try {
177
- return await deviceAllocator.postAllocate(deviceCookie);
177
+ return await deviceAllocator.postAllocate(deviceCookie, { deviceConfig });
178
178
  } catch (e) {
179
179
  try {
180
180
  await deviceAllocator.free(deviceCookie, { shutdown: true });
@@ -1 +0,0 @@
1
- 437da6c0a5ae2c1d4977a5cb3c594381
@@ -1 +0,0 @@
1
- 46a778d2606c567f78710bdd758ecfe44e3e97a8
@@ -1 +0,0 @@
1
- 686ba729c1fd277f3d65743f0d9986cb87aa0e09893ea7b1a774b9b4b3c50bfb
@@ -1 +0,0 @@
1
- 2339b87ef6be1b8c719fc4f4b85c645ce09ae62f1dceac7e1045e08f35a9e376e3b1421b1c619690e8c3e1682d4491730e1f8b4d304f2d7f2a707810495d6913
@@ -1 +0,0 @@
1
- 43ac5b85bfa0841e9d6d174bad45c8cc
@@ -1 +0,0 @@
1
- b67f418f459b003c18fbcd07e4126b9c83f9dda8
@@ -1 +0,0 @@
1
- 05ae1f6e116faadd83a030406cbe6776ee6e36762505e59e563c861a98cbc31e
@@ -1 +0,0 @@
1
- 195b29f8eac21dbce9cea79a5f0097e14307568400f96dfc5a058117deceb9ccf99c4a0867a9a72d2d019b3c07fb41453c661b16b5235f398df816c5a815b51c