detox 20.41.3 → 20.42.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 (51) hide show
  1. package/Detox-android/com/wix/detox/{20.41.3/detox-20.41.3-sources.jar → 20.42.0/detox-20.42.0-sources.jar} +0 -0
  2. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0-sources.jar.md5 +1 -0
  3. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0-sources.jar.sha1 +1 -0
  4. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0-sources.jar.sha256 +1 -0
  5. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0-sources.jar.sha512 +1 -0
  6. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0.aar +0 -0
  7. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0.aar.md5 +1 -0
  8. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0.aar.sha1 +1 -0
  9. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0.aar.sha256 +1 -0
  10. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0.aar.sha512 +1 -0
  11. package/Detox-android/com/wix/detox/{20.41.3/detox-20.41.3.pom → 20.42.0/detox-20.42.0.pom} +1 -1
  12. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0.pom.md5 +1 -0
  13. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0.pom.sha1 +1 -0
  14. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0.pom.sha256 +1 -0
  15. package/Detox-android/com/wix/detox/20.42.0/detox-20.42.0.pom.sha512 +1 -0
  16. package/Detox-android/com/wix/detox/maven-metadata.xml +4 -4
  17. package/Detox-android/com/wix/detox/maven-metadata.xml.md5 +1 -1
  18. package/Detox-android/com/wix/detox/maven-metadata.xml.sha1 +1 -1
  19. package/Detox-android/com/wix/detox/maven-metadata.xml.sha256 +1 -1
  20. package/Detox-android/com/wix/detox/maven-metadata.xml.sha512 +1 -1
  21. package/Detox-ios-framework.tbz +0 -0
  22. package/Detox-ios-src.tbz +0 -0
  23. package/Detox-ios-xcuitest.tbz +0 -0
  24. package/android/detox/src/full/java/com/wix/detox/espresso/hierarchy/ViewHierarchyGenerator.kt +3 -0
  25. package/detox.d.ts +29 -0
  26. package/package.json +3 -3
  27. package/runners/jest/reporter.js +1 -1
  28. package/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js +10 -3
  29. package/src/devices/common/drivers/android/exec/ADB.js +12 -0
  30. package/src/devices/common/drivers/ios/tools/SimulatorAppCache.js +148 -0
  31. package/src/devices/runtime/RuntimeDevice.js +10 -1
  32. package/src/devices/runtime/drivers/DeviceDriverBase.js +4 -0
  33. package/src/devices/runtime/drivers/android/AndroidDriver.js +6 -0
  34. package/src/devices/runtime/drivers/ios/AppStateResetFallback.js +69 -0
  35. package/src/devices/runtime/drivers/ios/SimulatorDriver.js +17 -1
  36. package/src/utils/constructSafeFilename.js +2 -1
  37. package/src/utils/environment.js +12 -0
  38. package/src/utils/fsext.js +13 -0
  39. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3-sources.jar.md5 +0 -1
  40. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3-sources.jar.sha1 +0 -1
  41. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3-sources.jar.sha256 +0 -1
  42. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3-sources.jar.sha512 +0 -1
  43. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3.aar +0 -0
  44. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3.aar.md5 +0 -1
  45. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3.aar.sha1 +0 -1
  46. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3.aar.sha256 +0 -1
  47. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3.aar.sha512 +0 -1
  48. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3.pom.md5 +0 -1
  49. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3.pom.sha1 +0 -1
  50. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3.pom.sha256 +0 -1
  51. package/Detox-android/com/wix/detox/20.41.3/detox-20.41.3.pom.sha512 +0 -1
@@ -0,0 +1 @@
1
+ 1887d4991e0ed4ee1f60fe19496a9cfa
@@ -0,0 +1 @@
1
+ 697906ce37fa13e262c3cfdaba6a6e23af05123d
@@ -0,0 +1 @@
1
+ 81b7cec264869ffb4a51c3f49d0458ea42f02a8921afeeef9fba6d13f81f76c6
@@ -0,0 +1 @@
1
+ 2345745b36a549ab7230b6cbc55cad2a2a469720a8622a2592669c3a25be55c8a9d0bb7b36123c68e91bc37c6df04f477849018c034e0dd956d1d5e9cb2a1cfb
@@ -0,0 +1 @@
1
+ 69c3525aa2e433c05bee1f04c2724502
@@ -0,0 +1 @@
1
+ 5dac9bbd10339143f33f66269d560c940dabde5a
@@ -0,0 +1 @@
1
+ 04cb33cabcf007a0767a85ea4b9322efe008d9ffb2e4a5636f2eb6513fcc81d3
@@ -0,0 +1 @@
1
+ aec098beccb4827b287051cfd924ba72a4b8ad6a5d3cf3c5f645dea50ba704f683a2dbe5e724a94801189aded1fe494a69cef0b31f9128dde89ee1ec2fc92e2d
@@ -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.41.3</version>
6
+ <version>20.42.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
+ c194caa27e6cabf5bc5bbede5c1e22ad
@@ -0,0 +1 @@
1
+ d844bb306d465880c159bb56238879e2395cee9a
@@ -0,0 +1 @@
1
+ 6989731d5223c5a9f236e3911eaef2e85fbca229eb68bfbec44e31b3529ae70d
@@ -0,0 +1 @@
1
+ 13500f06af3b22637f0469dbce8b92b7da859ec3b08a28b8874be4ee1116599b97533cc7307a23beebe6c0508e57038ba2f6f195d69a09a994a599071bdce886
@@ -3,11 +3,11 @@
3
3
  <groupId>com.wix</groupId>
4
4
  <artifactId>detox</artifactId>
5
5
  <versioning>
6
- <latest>20.41.3</latest>
7
- <release>20.41.3</release>
6
+ <latest>20.42.0</latest>
7
+ <release>20.42.0</release>
8
8
  <versions>
9
- <version>20.41.3</version>
9
+ <version>20.42.0</version>
10
10
  </versions>
11
- <lastUpdated>20250918141124</lastUpdated>
11
+ <lastUpdated>20250925083025</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 6f57826a3230198297bce5e007333fac
1
+ cffd00ec1fa7d24dfa670f1f73cb2157
@@ -1 +1 @@
1
- c8416ca659f3d8c733dfa7b9f187558b8fd9c716
1
+ 2f1f1e00242921f53643aeea67e863765c97bdef
@@ -1 +1 @@
1
- def56429b5323f8b16fe2d813001f68694bffe308821c3d3ecc28f4d5c04a679
1
+ be3543d79c53d124eb70e536793549e36b7f2a5c196b524f55d39cdb322a3216
@@ -1 +1 @@
1
- ae9fae3a019fdc3e7d87689f447e7817a942f68dbae4b3e6ce9fa395bab9cc017d1df68ae3af204275c10d553bb00c62aac54be18f2bcf06a58ae2c886ebbeaa
1
+ 003eb81934b9bf5c98fc53dfa0d8871d436a7ef88fd0d430206672a38c898e47c4535ca1a2b621a65163b4c672fbbf65daba9ed9dbc0ee17461a64c674d63cd6
Binary file
package/Detox-ios-src.tbz CHANGED
Binary file
Binary file
@@ -1,10 +1,12 @@
1
1
  package com.wix.detox.espresso.hierarchy
2
2
 
3
+ import android.util.DisplayMetrics
3
4
  import android.util.Xml
4
5
  import android.view.View
5
6
  import android.view.ViewGroup
6
7
  import android.webkit.WebView
7
8
  import android.widget.TextView
9
+ import com.wix.detox.espresso.DeviceDisplay
8
10
  import com.wix.detox.reactnative.ui.getAccessibilityLabel
9
11
  import kotlinx.coroutines.Dispatchers
10
12
  import kotlinx.coroutines.runBlocking
@@ -63,6 +65,7 @@ object ViewHierarchyGenerator {
63
65
  startDocument(Xml.Encoding.UTF_8.name, true)
64
66
  setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
65
67
  startTag("", "ViewHierarchy")
68
+ attribute("", "density", DeviceDisplay.getDensity().toString())
66
69
  }
67
70
 
68
71
  rootViews?.forEach { rootView ->
package/detox.d.ts CHANGED
@@ -773,6 +773,28 @@ declare global {
773
773
  */
774
774
  reloadReactNative(): Promise<void>;
775
775
 
776
+ /**
777
+ * Resets the app state by clearing app data and restoring it to a clean state.
778
+ *
779
+ * On Android, this command clears the app's data using the `pm clear` command,
780
+ * effectively resetting the app to its initial installed state without uninstalling it.
781
+ *
782
+ * On iOS, Detox uses a fallback mechanism - it backs up, deletes and installs the app from cache.
783
+ * This process ensures the app is returned to a clean state.
784
+ *
785
+ * @param bundleIds Optional bundle IDs to reset. If none provided, resets the currently selected app.
786
+ * @example
787
+ * // Reset current app state
788
+ * await device.resetAppState();
789
+ * @example
790
+ * // Reset specific app state
791
+ * await device.resetAppState('com.example.app');
792
+ * @example
793
+ * // Reset multiple apps
794
+ * await device.resetAppState('com.app1', 'com.app2');
795
+ */
796
+ resetAppState(...bundleIds: string[]): Promise<void>;
797
+
776
798
  /**
777
799
  * By default, installApp() with no params will install the app file defined in the current configuration.
778
800
  * To install another app, specify its path
@@ -2014,6 +2036,13 @@ declare global {
2014
2036
  * Launch with user activity
2015
2037
  */
2016
2038
  userActivity?: any;
2039
+ /**
2040
+ * Similar to {@link Detox.DeviceLaunchAppConfig.delete | { delete: true }}, but instead of uninstalling and installing the app,
2041
+ * it runs {@link Detox.Device.resetAppState | device.resetAppState()} instead.
2042
+ * @example
2043
+ * await device.launchApp({resetAppState: true});
2044
+ */
2045
+ resetAppState?: boolean;
2017
2046
  /**
2018
2047
  * Launch into a fresh installation
2019
2048
  * A flag that enables relaunching into a fresh installation of the app (it will uninstall and install the binary again), default is false.
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.41.3",
4
+ "version": "20.42.0",
5
5
  "bin": {
6
6
  "detox": "local-cli/cli.js"
7
7
  },
@@ -68,7 +68,7 @@
68
68
  "wtfnode": "^0.9.1"
69
69
  },
70
70
  "dependencies": {
71
- "@wix-pilot/core": "^3.4.1",
71
+ "@wix-pilot/core": "^3.4.2",
72
72
  "@wix-pilot/detox": "^1.0.13",
73
73
  "ajv": "^8.6.3",
74
74
  "bunyan": "^1.8.12",
@@ -120,5 +120,5 @@
120
120
  "browserslist": [
121
121
  "node 14"
122
122
  ],
123
- "gitHead": "fc33401cc27b42f8a7c48f2d6d6e8cee65117476"
123
+ "gitHead": "e86f949767d1fb178f97fa31ec9cb6e2226e86fa"
124
124
  }
@@ -11,8 +11,8 @@ const {
11
11
  class DetoxReporter extends DetoxReporterDispatcher {
12
12
  constructor(globalConfig) {
13
13
  super(globalConfig, {
14
- DetoxSummaryReporter,
15
14
  DetoxVerboseReporter,
15
+ DetoxSummaryReporter,
16
16
  DetoxIPCReporter,
17
17
  });
18
18
  }
@@ -8,6 +8,7 @@ const _ = require('lodash');
8
8
 
9
9
  const { DetoxRuntimeError } = require('../../../../errors');
10
10
  const log = require('../../../../utils/logger').child({ cat: 'device,device-allocation' });
11
+ const SimulatorAppCache = require('../../../common/drivers/ios/tools/SimulatorAppCache');
11
12
 
12
13
  const SimulatorQuery = require('./SimulatorQuery');
13
14
 
@@ -24,6 +25,7 @@ class SimulatorAllocDriver {
24
25
  constructor({ detoxConfig, deviceRegistry, applesimutils }) {
25
26
  this._deviceRegistry = deviceRegistry;
26
27
  this._applesimutils = applesimutils;
28
+ this._appCache = new SimulatorAppCache({ applesimutils });
27
29
  this._launchInfo = {};
28
30
  this._shouldShutdown = detoxConfig.behavior.cleanup.shutdownDevice;
29
31
  }
@@ -58,7 +60,9 @@ class SimulatorAllocDriver {
58
60
  async postAllocate(deviceCookie) {
59
61
  const { udid } = deviceCookie;
60
62
  const { deviceConfig } = this._launchInfo[udid];
63
+
61
64
  await this._applesimutils.boot(udid, deviceConfig.bootArgs, deviceConfig.headless);
65
+ await this._appCache.cleanupOnce(udid);
62
66
 
63
67
  return {
64
68
  id: udid,
@@ -86,12 +90,15 @@ class SimulatorAllocDriver {
86
90
  }
87
91
 
88
92
  async cleanup() {
93
+ const sessionDevices = await this._deviceRegistry.readSessionDevices();
94
+ const deviceIds = sessionDevices.getIds();
95
+
89
96
  if (this._shouldShutdown) {
90
- const sessionDevices = await this._deviceRegistry.readSessionDevices();
91
- const shutdownPromises = sessionDevices.getIds().map((udid) => this._doShutdown(udid));
97
+ const shutdownPromises = deviceIds.map((udid) => this._doShutdown(udid));
92
98
  await Promise.all(shutdownPromises);
93
99
  }
94
100
 
101
+ await Promise.all(deviceIds.map((udid) => this._appCache.cleanup(udid)));
95
102
  await this._deviceRegistry.unregisterSessionDevices();
96
103
  }
97
104
 
@@ -172,7 +179,7 @@ class SimulatorAllocDriver {
172
179
  deviceQuery,
173
180
  {
174
181
  trying: `Searching for device ${deviceQuery} ...`,
175
- fields: ['udid', 'os', 'identifier'],
182
+ fields: ['udid', 'name', 'deviceType', 'os', 'identifier'],
176
183
  }
177
184
  );
178
185
 
@@ -148,6 +148,18 @@ class ADB {
148
148
  await this.shell(deviceId, `am force-stop ${appId}`);
149
149
  }
150
150
 
151
+ async clearAppData(deviceId, packageId) {
152
+ try {
153
+ return await this.shell(deviceId, `pm clear ${packageId}`);
154
+ } catch (reason) {
155
+ throw new DetoxRuntimeError({
156
+ message: `Failed to clear ${packageId} app data on ${deviceId}`,
157
+ hint: `Please verify that the package is installed on the device:\nadb -s ${deviceId} shell pm list packages ${packageId}`,
158
+ debugInfo: reason,
159
+ });
160
+ }
161
+ }
162
+
151
163
  async setLocation(deviceId, lat, lon) {
152
164
  // NOTE: QEMU for Android for the telnet part relies on C stdlib
153
165
  // function `strtod` which is locale-sensitive, meaning that depending
@@ -0,0 +1,148 @@
1
+ const path = require('path');
2
+
3
+ const { DetoxInternalError, DetoxRuntimeError } = require('../../../../../errors');
4
+ const { getDetoxAppsCachePath } = require('../../../../../utils/environment');
5
+ const fse = require('../../../../../utils/fsext');
6
+ const log = require('../../../../../utils/logger').child({ cat: 'device,app-cache' });
7
+
8
+ class SimulatorAppCache {
9
+ constructor({ applesimutils, rootDir = getDetoxAppsCachePath() }) {
10
+ this.applesimutils = applesimutils;
11
+ this.rootDir = rootDir;
12
+ this.cleanDeviceIds = new Set();
13
+ }
14
+
15
+ /**
16
+ * Add an app to cache from a binary path
17
+ * @param {string} deviceId - The device identifier
18
+ * @param {string} bundleId - The app's bundle identifier
19
+ * @param {string} binaryPath - Path to the app binary
20
+ */
21
+ async add(deviceId, bundleId, binaryPath) {
22
+ log.trace({ deviceId }, `Caching app (${bundleId}) from binary path: ${binaryPath}`);
23
+
24
+ const cacheAppPath = this._getAppCachePath(deviceId, bundleId);
25
+ await fse.ensureDir(path.dirname(cacheAppPath));
26
+ await fse.remove(cacheAppPath);
27
+ await fse.copy(binaryPath, cacheAppPath);
28
+ }
29
+
30
+ /**
31
+ * Back up an app to cache, if it is not already cached
32
+ * @param {string} deviceId - The device identifier
33
+ * @param {string} bundleId - The app's bundle identifier
34
+ */
35
+ async backup(deviceId, bundleId) {
36
+ const appContainerPath = await this.applesimutils.getAppContainer(deviceId, bundleId).catch((reason) => {
37
+ throw new DetoxRuntimeError({
38
+ message: `App with bundle ID '${bundleId}' is not installed on this device (${deviceId}). Please install the app first before attempting to reset its state.`,
39
+ hint: `To check apps installed on the device, use: xcrun simctl listapps ${deviceId}`,
40
+ debugInfo: reason,
41
+ });
42
+ });
43
+
44
+ if (!await this.exists(deviceId, bundleId)) {
45
+ await this.add(deviceId, bundleId, appContainerPath);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Restore an app from cache
51
+ * @param {string} deviceId - The device identifier
52
+ * @param {string} bundleId - The app's bundle identifier
53
+ * @returns {Promise<void>}
54
+ */
55
+ async restore(deviceId, bundleId) {
56
+ log.trace(`Restoring app (${bundleId}) from cache to device (${deviceId})`);
57
+
58
+ const cacheAppPath = this._getAppCachePath(deviceId, bundleId);
59
+
60
+ if (!await fse.exists(cacheAppPath)) {
61
+ throw new DetoxInternalError(`No backup found for bundle ID '${bundleId}' on device ${deviceId}. This should not happen unless you're calling SimulatorAppCache#restore() directly.`);
62
+ }
63
+
64
+ await this.applesimutils.install(deviceId, cacheAppPath);
65
+ }
66
+
67
+ /**
68
+ * Check if an app is cached
69
+ * @param {string} deviceId - The device identifier
70
+ * @param {string} bundleId - The app's bundle identifier
71
+ * @returns {Promise<boolean>} True if the app is cached
72
+ */
73
+ async exists(deviceId, bundleId) {
74
+ const cacheAppPath = this._getAppCachePath(deviceId, bundleId);
75
+ return await fse.exists(cacheAppPath);
76
+ }
77
+
78
+ /**
79
+ * Remove cache for a specific app
80
+ * @param {string} deviceId - The device identifier
81
+ * @param {string} bundleId - The app's bundle identifier
82
+ * @returns {Promise<void>}
83
+ */
84
+ async remove(deviceId, bundleId) {
85
+ try {
86
+ const cacheAppPath = this._getAppCachePath(deviceId, bundleId);
87
+ if (await fse.remove(cacheAppPath)) {
88
+ log.trace({ path: cacheAppPath }, `Removed cached app (${bundleId}) for device (${deviceId})`);
89
+ }
90
+ } catch (err) {
91
+ log.warn({ err }, `Failed to remove app cache for ${bundleId} on device ${deviceId}`);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Clean up cache for a device
97
+ * @param {string} deviceId - The device identifier
98
+ * @returns {Promise<void>}
99
+ */
100
+ async cleanup(deviceId) {
101
+ try {
102
+ const deviceCachePath = this._getDeviceCachePath(deviceId);
103
+ if (await fse.remove(deviceCachePath)) {
104
+ log.trace({ path: deviceCachePath }, `Cleaned up the app cache for device (${deviceId})`);
105
+ }
106
+ } catch (err) {
107
+ log.warn({ err }, `Failed to cleanup app cache for device ${deviceId}`);
108
+ }
109
+
110
+ this.cleanDeviceIds.add(deviceId);
111
+ }
112
+
113
+ /**
114
+ * Clean up cache for a device only once per instance.
115
+ * If the device has already been cleaned, this is a no-op.
116
+ *
117
+ * @param {string} deviceId - The device identifier
118
+ * @returns {Promise<void>}
119
+ */
120
+ async cleanupOnce(deviceId) {
121
+ if (!this.cleanDeviceIds.has(deviceId)) {
122
+ await this.cleanup(deviceId);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Get the cache path for a specific app on a device
128
+ * @param {string} deviceId - The device identifier
129
+ * @param {string} bundleId - The app's bundle identifier
130
+ * @returns {string} The full path to the cached app
131
+ * @private
132
+ */
133
+ _getAppCachePath(deviceId, bundleId) {
134
+ return path.join(this._getDeviceCachePath(deviceId), `${bundleId}.app`);
135
+ }
136
+
137
+ /**
138
+ * Get the device cache directory path
139
+ * @param {string} deviceId - The device identifier
140
+ * @returns {string} The device cache directory path
141
+ * @private
142
+ */
143
+ _getDeviceCachePath(deviceId) {
144
+ return path.join(this.rootDir, deviceId);
145
+ }
146
+ }
147
+
148
+ module.exports = SimulatorAppCache;
@@ -31,6 +31,7 @@ class RuntimeDevice {
31
31
  'pressBack',
32
32
  'relaunchApp',
33
33
  'reloadReactNative',
34
+ 'resetAppState',
34
35
  'resetContentAndSettings',
35
36
  'resetStatusBar',
36
37
  'reverseTcpPort',
@@ -119,7 +120,10 @@ class RuntimeDevice {
119
120
  ? params.newInstance
120
121
  : this._processes[bundleId] == null;
121
122
 
122
- if (params.delete) {
123
+ if (params.resetAppState) {
124
+ await this.terminateApp(bundleId);
125
+ await this.resetAppState(bundleId);
126
+ } else if (params.delete) {
123
127
  await this.terminateApp(bundleId);
124
128
  await this.uninstallApp();
125
129
  await this.installApp();
@@ -261,6 +265,11 @@ class RuntimeDevice {
261
265
  await this.deviceDriver.uninstallApp(_bundleId);
262
266
  }
263
267
 
268
+ async resetAppState(...bundleIds) {
269
+ const _bundleIds = bundleIds.length > 0 ? bundleIds : [this._bundleId];
270
+ await this.deviceDriver.resetAppState(..._bundleIds);
271
+ }
272
+
264
273
  async installUtilBinaries() {
265
274
  const paths = this._deviceConfig.utilBinaryPaths;
266
275
  if (paths) {
@@ -91,6 +91,10 @@ class DeviceDriverBase {
91
91
  return '';
92
92
  }
93
93
 
94
+ async resetAppState(..._bundleIds) {
95
+ return undefined;
96
+ }
97
+
94
98
  async uninstallApp() {
95
99
  return '';
96
100
  }
@@ -84,6 +84,12 @@ class AndroidDriver extends DeviceDriverBase {
84
84
  await this.appUninstallHelper.uninstall(this.adbName, bundleId);
85
85
  }
86
86
 
87
+ async resetAppState(...bundleIds) {
88
+ for (const bundleId of bundleIds) {
89
+ await this.adb.clearAppData(this.adbName, bundleId);
90
+ }
91
+ }
92
+
87
93
  async installUtilBinaries(paths) {
88
94
  for (const path of paths) {
89
95
  const packageId = await this.getBundleIdFromBinary(path);
@@ -0,0 +1,69 @@
1
+ const SimulatorAppCache = require('../../../common/drivers/ios/tools/SimulatorAppCache');
2
+
3
+ /**
4
+ * AppStateResetFallback provides app state reset functionality for iOS simulators.
5
+ *
6
+ * This class serves as a fallback implementation because iOS does not provide a native
7
+ * way to reset an app's state (unlike Android's `pm clear` command). To achieve app
8
+ * state reset on iOS, this class implements a backup-uninstall-reinstall strategy:
9
+ *
10
+ * 1. Backs up the app installation to cache
11
+ * 2. Uninstalls the current app instance
12
+ * 3. Restores the app installation from cache
13
+ *
14
+ * This approach ensures the app is returned to a clean state without requiring
15
+ * a full app reinstallation from the original bundle, making it more friendly
16
+ * to local development flows.
17
+ *
18
+ * @class AppStateResetFallback
19
+ */
20
+ class AppStateResetFallback {
21
+ /**
22
+ * Creates an instance of AppStateResetFallback.
23
+ * @param {Object} config
24
+ * @param {import('../../../common/drivers/ios/tools/AppleSimUtils')} config.applesimutils - AppleSimUtils instance
25
+ * @param {SimulatorAppCache} [config.appCache] - Optional SimulatorAppCache instance (for testing/mocking)
26
+ */
27
+ constructor({ applesimutils, appCache }) {
28
+ this.applesimutils = applesimutils;
29
+ this.appCache = appCache ?? new SimulatorAppCache({ applesimutils });
30
+ }
31
+
32
+ /**
33
+ * Resets the app state for multiple apps by backing them up and restoring them.
34
+ * This effectively clears the app state while preserving the app installation.
35
+ *
36
+ * @param {string} udid - The device identifier
37
+ * @param {string[]} bundleIds - Array of app bundle identifiers to reset
38
+ * @returns {Promise<void>}
39
+ */
40
+ async resetAppState(udid, bundleIds) {
41
+ for (const bundleId of bundleIds) {
42
+ await this.appCache.backup(udid, bundleId);
43
+ await this.applesimutils.uninstall(udid, bundleId);
44
+ }
45
+
46
+ for (const bundleId of bundleIds) {
47
+ await this.appCache.restore(udid, bundleId);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Ensures removal of cached app(s) after a potentially
53
+ * destructive action (such as install or uninstall),
54
+ * so {@link resetAppState} can't use a stale backup from cache.
55
+ *
56
+ * @param {string} udid - The device identifier
57
+ * @param {string} [bundleId] - Optional bundle identifier for specific app invalidation
58
+ * @returns {Promise<void>}
59
+ */
60
+ async invalidate(udid, bundleId) {
61
+ if (bundleId) {
62
+ await this.appCache.remove(udid, bundleId);
63
+ } else {
64
+ await this.appCache.cleanup(udid);
65
+ }
66
+ }
67
+ }
68
+
69
+ module.exports = AppStateResetFallback;
@@ -15,6 +15,7 @@ const log = require('../../../../utils/logger').child({ cat: 'device' });
15
15
  const pressAnyKey = require('../../../../utils/pressAnyKey');
16
16
  const traceInvocationCall = require('../../../../utils/traceInvocationCall').bind(null, log);
17
17
 
18
+ const AppStateResetFallback = require('./AppStateResetFallback');
18
19
  const IosDriver = require('./IosDriver');
19
20
 
20
21
  /**
@@ -37,12 +38,15 @@ class SimulatorDriver extends IosDriver {
37
38
  constructor(deps, { udid, type, bootArgs, headless }) {
38
39
  super(deps);
39
40
 
41
+ this.getBundleIdFromBinary = _.memoize(this.getBundleIdFromBinary.bind(this));
42
+
40
43
  this.udid = udid;
41
44
  this._type = type;
42
45
  this._bootArgs = bootArgs;
43
46
  this._headless = headless;
44
47
  this._deviceName = `${udid} (${this._type})`;
45
48
  this._applesimutils = deps.applesimutils;
49
+ this._appStateResetFallback = new AppStateResetFallback({ applesimutils: this._applesimutils });
46
50
  }
47
51
 
48
52
  withAction(xcuitestRunner, action, traceDescription, ...params) {
@@ -80,13 +84,24 @@ class SimulatorDriver extends IosDriver {
80
84
  }
81
85
 
82
86
  async installApp(binaryPath) {
83
- await this._applesimutils.install(this.udid, getAbsoluteBinaryPath(binaryPath));
87
+ const absoluteBinaryPath = getAbsoluteBinaryPath(binaryPath);
88
+ await this._applesimutils.install(this.udid, absoluteBinaryPath);
89
+
90
+ const bundleId = await this.getBundleIdFromBinary(binaryPath);
91
+ await this._appStateResetFallback.invalidate(this.udid, bundleId);
84
92
  }
85
93
 
86
94
  async uninstallApp(bundleId) {
87
95
  const { udid } = this;
88
96
  await this.emitter.emit('beforeUninstallApp', { deviceId: udid, bundleId });
89
97
  await this._applesimutils.uninstall(udid, bundleId);
98
+ await this._appStateResetFallback.invalidate(udid, bundleId);
99
+ }
100
+
101
+ async resetAppState(...bundleIds) {
102
+ const { udid } = this;
103
+ const _bundleIds = bundleIds.length > 0 ? bundleIds : [this._bundleId];
104
+ await this._appStateResetFallback.resetAppState(udid, _bundleIds);
90
105
  }
91
106
 
92
107
  async launchApp(bundleId, launchArgs, languageAndLocale) {
@@ -186,6 +201,7 @@ class SimulatorDriver extends IosDriver {
186
201
  await this._applesimutils.shutdown(this.udid);
187
202
  await this.emitter.emit('shutdownDevice', { deviceId: this.udid });
188
203
  await this._applesimutils.resetContentAndSettings(this.udid);
204
+ await this._appStateResetFallback.invalidate(this.udid);
189
205
  await this._applesimutils.boot(this.udid, this._bootArgs, this._headless);
190
206
  await this.emitter.emit('bootDevice', { deviceId: this.udid });
191
207
  }
@@ -41,7 +41,8 @@ function constructSafeFilename(prefix = '', trimmable = '', suffix = '') {
41
41
 
42
42
  const trimmed = trimmable.slice(-MAX_FILE_LENGTH + nonTrimmableLength);
43
43
  const unsafe = prefix + trimmed + suffix;
44
- const sanitized = sanitize(unsafe, sanitizeOptions);
44
+ const sanitized = sanitize(unsafe, sanitizeOptions)
45
+ .replace(/\$/g, sanitizeOptions.replacement);
45
46
 
46
47
  return sanitized;
47
48
  }
@@ -20,6 +20,7 @@ function which(executable, path) {
20
20
  const DETOX_LIBRARY_ROOT_PATH = path.join(appdatapath.appDataPath(), 'Detox');
21
21
  const MISSING_SDK_ERROR = `$ANDROID_SDK_ROOT is not defined, set the path to the SDK installation directory into $ANDROID_SDK_ROOT,
22
22
  Go to https://developer.android.com/studio/command-line/variables.html for more details`;
23
+ const DETOX_APPS_CACHE_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'apps-cache');
23
24
  const DETOX_LOCK_FILE_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'global-context.json');
24
25
  const DEVICE_REGISTRY_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'device.registry.json');
25
26
  const LAST_FAILED_TESTS_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'last-failed.txt');
@@ -217,6 +218,16 @@ function getDetoxLibraryRootPath() {
217
218
  return DETOX_LIBRARY_ROOT_PATH;
218
219
  }
219
220
 
221
+ function getDetoxAppsCachePath(udid, bundleId) {
222
+ if (udid && bundleId) {
223
+ return path.join(DETOX_APPS_CACHE_PATH, udid, bundleId);
224
+ } else if (udid) {
225
+ return path.join(DETOX_APPS_CACHE_PATH, udid);
226
+ } else {
227
+ return DETOX_APPS_CACHE_PATH;
228
+ }
229
+ }
230
+
220
231
  function getDetoxLockFilePath() {
221
232
  return DETOX_LOCK_FILE_PATH;
222
233
  }
@@ -249,6 +260,7 @@ module.exports = {
249
260
  getXCUITestRunnerPath,
250
261
  getAndroidSDKPath,
251
262
  getAndroidEmulatorPath,
263
+ getDetoxAppsCachePath,
252
264
  getDetoxLibraryRootPath,
253
265
  getDetoxLockFilePath,
254
266
  getDeviceRegistryPath,
@@ -19,8 +19,21 @@ async function getDirectories (rootPath) {
19
19
  return dirs.sort();
20
20
  }
21
21
 
22
+ async function remove(filePath) {
23
+ if (await fs.exists(filePath)) {
24
+ await fs.remove(filePath);
25
+ return true;
26
+ }
27
+
28
+ return false;
29
+ }
30
+
22
31
  module.exports = {
32
+ copy: fs.copy,
33
+ ensureDir: fs.ensureDir,
34
+ exists: fs.exists,
23
35
  getDirectories,
24
36
  isDirEmptySync,
25
37
  readdirSync: fs.readdirSync,
38
+ remove,
26
39
  };
@@ -1 +0,0 @@
1
- d62daa3c6360e40eee316c7e7d391989
@@ -1 +0,0 @@
1
- 7188b3152f33cad1dbd467ee2c9daa649f1482a7
@@ -1 +0,0 @@
1
- b7a9075a28971504483bdca303e45acb179c2aa16583d03397496372f40aca8a
@@ -1 +0,0 @@
1
- 83d09a205dd16e6d517249b663f56634604066ac3a974f018cdeed02c0868d4ce67db7b23015592d9a9c3692ec0339124df33644882824245b84413fb7b9ed81
@@ -1 +0,0 @@
1
- 13c04ef5c334f8dd971b29fd29746cef
@@ -1 +0,0 @@
1
- 5f3e7cb44fef6005ce377795a87ea2974031960e
@@ -1 +0,0 @@
1
- d49345f1dde088f8e36d1acb664552465b8f86c763a29551e339f4a94267cc36
@@ -1 +0,0 @@
1
- 7c053d70ecb0c178011dc1f0d769a3bd74342f32e857124016c71876dd3debd6f251bdd2c14c2eb39d60fde8e6fed6454ae22bd07795b78da3a6b5ed87fe8de6
@@ -1 +0,0 @@
1
- 6c53141aae1e1550c586efc36bb0dd0c
@@ -1 +0,0 @@
1
- 740de0f44e34398d3f248ab00f9fd3c9b0886c9b
@@ -1 +0,0 @@
1
- 9bbc99998c52463f7523b399ca94a59bf01ee8eff482ebc9b492b97e26e9295c
@@ -1 +0,0 @@
1
- eb9826a24c573fab3363c644b8557642fd8875fc64466d04c4600287d22d9c909d7b000aaafc159678b3ca444984d6525f3f047e4c700be3e28b9df2d4d28a16