detox 20.14.3 → 20.15.0-prerelease.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. package/Detox-android/com/wix/detox/{20.14.3/detox-20.14.3-javadoc.jar → 20.15.0-prerelease.0/detox-20.15.0-prerelease.0-javadoc.jar} +0 -0
  2. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0-javadoc.jar.md5 +1 -0
  3. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0-javadoc.jar.sha1 +1 -0
  4. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0-javadoc.jar.sha256 +1 -0
  5. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0-javadoc.jar.sha512 +1 -0
  6. package/Detox-android/com/wix/detox/{20.14.3/detox-20.14.3-sources.jar → 20.15.0-prerelease.0/detox-20.15.0-prerelease.0-sources.jar} +0 -0
  7. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0-sources.jar.md5 +1 -0
  8. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0-sources.jar.sha1 +1 -0
  9. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0-sources.jar.sha256 +1 -0
  10. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0-sources.jar.sha512 +1 -0
  11. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0.aar +0 -0
  12. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0.aar.md5 +1 -0
  13. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0.aar.sha1 +1 -0
  14. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0.aar.sha256 +1 -0
  15. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0.aar.sha512 +1 -0
  16. package/Detox-android/com/wix/detox/{20.14.3/detox-20.14.3.pom → 20.15.0-prerelease.0/detox-20.15.0-prerelease.0.pom} +1 -1
  17. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0.pom.md5 +1 -0
  18. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0.pom.sha1 +1 -0
  19. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0.pom.sha256 +1 -0
  20. package/Detox-android/com/wix/detox/20.15.0-prerelease.0/detox-20.15.0-prerelease.0.pom.sha512 +1 -0
  21. package/Detox-android/com/wix/detox/maven-metadata.xml +4 -4
  22. package/Detox-android/com/wix/detox/maven-metadata.xml.md5 +1 -1
  23. package/Detox-android/com/wix/detox/maven-metadata.xml.sha1 +1 -1
  24. package/Detox-android/com/wix/detox/maven-metadata.xml.sha256 +1 -1
  25. package/Detox-android/com/wix/detox/maven-metadata.xml.sha512 +1 -1
  26. package/Detox-ios-src.tbz +0 -0
  27. package/Detox-ios.tbz +0 -0
  28. package/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt +78 -0
  29. package/android/detox/src/full/java/com/wix/detox/Detox.java +8 -70
  30. package/android/detox/src/full/java/com/wix/detox/DetoxMain.kt +69 -44
  31. package/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt +1 -1
  32. package/android/detox/src/full/java/com/wix/detox/NotificationDataParser.kt +1 -1
  33. package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt +0 -4
  34. package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt +16 -6
  35. package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt +0 -2
  36. package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt +4 -2
  37. package/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt +44 -0
  38. package/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt +25 -32
  39. package/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt +0 -3
  40. package/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt +111 -0
  41. package/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/RegexMatcherTest.kt +0 -3
  42. package/package.json +2 -2
  43. package/src/android/espressoapi/Detox.js +0 -11
  44. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3-javadoc.jar.md5 +0 -1
  45. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3-javadoc.jar.sha1 +0 -1
  46. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3-javadoc.jar.sha256 +0 -1
  47. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3-javadoc.jar.sha512 +0 -1
  48. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3-sources.jar.md5 +0 -1
  49. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3-sources.jar.sha1 +0 -1
  50. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3-sources.jar.sha256 +0 -1
  51. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3-sources.jar.sha512 +0 -1
  52. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3.aar +0 -0
  53. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3.aar.md5 +0 -1
  54. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3.aar.sha1 +0 -1
  55. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3.aar.sha256 +0 -1
  56. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3.aar.sha512 +0 -1
  57. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3.pom.md5 +0 -1
  58. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3.pom.sha1 +0 -1
  59. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3.pom.sha256 +0 -1
  60. package/Detox-android/com/wix/detox/20.14.3/detox-20.14.3.pom.sha512 +0 -1
@@ -0,0 +1 @@
1
+ 5d4de8f6419f956ca8ababe7421ddbcb44205391
@@ -0,0 +1 @@
1
+ 7b3135220fb3f688adc1b6028f462e8b15b10ba6f847e273c43b5ec8ce6c7787
@@ -0,0 +1 @@
1
+ 5644850b63ced1e2a2ac4f2d009e576d4d2164140d12493afe4dea9bbf9ae15cd2e67c875de90acaf2664f9095646d885e1a33f89b9f9e1018dd039c317462ff
@@ -0,0 +1 @@
1
+ 978a04494b3655d7bad767746e4b5f87517d425c
@@ -0,0 +1 @@
1
+ 397159775586aa3ee57476b45c587e64154feba1fff0ff452192f7fbec1df667
@@ -0,0 +1 @@
1
+ ae033768f1b9dd43e19f5606c20a8271e22e1f3f5fc88ea1d892fdcb09770f6907427bf81320564c62e5dbd78beab6a798167b0f1d776047e6f0fa5c74dc2c2d
@@ -0,0 +1 @@
1
+ c27920150917efb5b25878186e904fe0
@@ -0,0 +1 @@
1
+ ad9e12738ca3c82606a58bacea6d50b51687aee9
@@ -0,0 +1 @@
1
+ ab07c0011f9cee612e5b56ea42618978b75a5cfff9841766fc3f796e730c3571
@@ -0,0 +1 @@
1
+ a26f964a1ef775262740a23fbbeca9e5754a5da7896bcab22a476f684d40168b67ad4a97087a1f6fe484343e70eb32063adfeac3a2879f887e8d7fc616e6eacc
@@ -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.14.3</version>
6
+ <version>20.15.0-prerelease.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
+ 5252ce03ecdbc8c6e367a4a6c9f308ca
@@ -0,0 +1 @@
1
+ 7d885823adf8e1d2217ab89909d53fbc9acb23de
@@ -0,0 +1 @@
1
+ a0c47342c3898b2cfe040146683d58d022bca2606d676003dfac59be11cbca05
@@ -0,0 +1 @@
1
+ 6ab3dd11adfba07baeb98509a3c0e86718a00eeee5b2470429dc10e79e22b74b579a3d5ff06fcc1375f999e5e7a676a3cf49e8a570dd04b6666c63675a1196e1
@@ -3,11 +3,11 @@
3
3
  <groupId>com.wix</groupId>
4
4
  <artifactId>detox</artifactId>
5
5
  <versioning>
6
- <latest>20.14.3</latest>
7
- <release>20.14.3</release>
6
+ <latest>20.15.0-prerelease.0</latest>
7
+ <release>20.15.0-prerelease.0</release>
8
8
  <versions>
9
- <version>20.14.3</version>
9
+ <version>20.15.0-prerelease.0</version>
10
10
  </versions>
11
- <lastUpdated>20231219153636</lastUpdated>
11
+ <lastUpdated>20231220150359</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- fc0fd4a19cec1d107b1bd9fd90013cf4
1
+ 273bcf78a3612642a9b19326f7a7fb3a
@@ -1 +1 @@
1
- 6d3386088773329bf556fbe64b844403f2b8f037
1
+ 08771ff6772b4c990504fbe9ab123ef0e2ddacd7
@@ -1 +1 @@
1
- 7e9f03941de1513d09d0e46256c9545fb517e0a1736ef1dc38590f347ff4c515
1
+ 3bf9e3855939fc03f5374c0af9878a5110428da69ef8810a2c9bcc13f66a7637
@@ -1 +1 @@
1
- 05bb3d2da873b14e798fdf91cf1e41976b36bdbb1dbc1713ff0e9d28010b2856146dff2146f145c7e928eb678939be02b7d9e5c413ad52296951468284daaf85
1
+ 7e2da3c400021c8d949fa5d580ea7b6449531ce0329666cd1c8c1b95ee0fb0c5248130b37db7a64bf227f4c3d0e5b0eb3e500c4a969c99fa6e52e6641cefd274
package/Detox-ios-src.tbz CHANGED
Binary file
package/Detox-ios.tbz CHANGED
Binary file
@@ -0,0 +1,78 @@
1
+ package com.wix.detox
2
+
3
+ import android.app.Instrumentation.ActivityMonitor
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import androidx.test.platform.app.InstrumentationRegistry
7
+ import androidx.test.rule.ActivityTestRule
8
+
9
+ class ActivityLaunchHelper
10
+ @JvmOverloads constructor(
11
+ private val activityTestRule: ActivityTestRule<*>,
12
+ private val launchArgs: LaunchArgs = LaunchArgs(),
13
+ private val intentsFactory: LaunchIntentsFactory = LaunchIntentsFactory(),
14
+ private val notificationDataParserGen: (String) -> NotificationDataParser = { path -> NotificationDataParser(path) }
15
+ ) {
16
+ fun launchActivityUnderTest() {
17
+ val intent = extractInitialIntent()
18
+ activityTestRule.launchActivity(intent)
19
+ }
20
+
21
+ fun launchMainActivity() {
22
+ val activity = activityTestRule.activity
23
+ launchActivitySync(intentsFactory.activityLaunchIntent(activity))
24
+ }
25
+
26
+ fun startActivityFromUrl(url: String) {
27
+ launchActivitySync(intentsFactory.intentWithUrl(url, false))
28
+ }
29
+
30
+ fun startActivityFromNotification(dataFilePath: String) {
31
+ val notificationData = notificationDataParserGen(dataFilePath).toBundle()
32
+ val intent = intentsFactory.intentWithNotificationData(appContext, notificationData, false)
33
+ launchActivitySync(intent)
34
+ }
35
+
36
+ private fun extractInitialIntent(): Intent =
37
+ (if (launchArgs.hasUrlOverride()) {
38
+ intentsFactory.intentWithUrl(launchArgs.urlOverride, true)
39
+ } else if (launchArgs.hasNotificationPath()) {
40
+ val notificationData = notificationDataParserGen(launchArgs.notificationPath).toBundle()
41
+ intentsFactory.intentWithNotificationData(appContext, notificationData, true)
42
+ } else {
43
+ intentsFactory.cleanIntent()
44
+ }).also {
45
+ it.putExtra(INTENT_LAUNCH_ARGS_KEY, launchArgs.asIntentBundle())
46
+ }
47
+
48
+ private fun launchActivitySync(intent: Intent) {
49
+ // Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with.
50
+ // BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background
51
+ // would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without
52
+ // ever being released*. It will finally fail after a 45 seconds timeout.
53
+ // Without going into full details, it seems that activity test rules were not meant to be used this way. However,
54
+ // the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using
55
+ // dedicated methods such as moveToState(), which give better control over the lifecycle).
56
+ // In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125
57
+ // What it forces us to do, then, is this -
58
+ // 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()).
59
+ // 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready.
60
+ // ^ Hence the code below.
61
+ val activity = activityTestRule.activity
62
+ val activityMonitor = ActivityMonitor(activity.javaClass.name, null, true)
63
+ activity.startActivity(intent)
64
+
65
+ InstrumentationRegistry.getInstrumentation().run {
66
+ addMonitor(activityMonitor)
67
+ waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT)
68
+ }
69
+ }
70
+
71
+ private val appContext: Context
72
+ get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
73
+
74
+ companion object {
75
+ private const val INTENT_LAUNCH_ARGS_KEY = "launchArgs"
76
+ private const val ACTIVITY_LAUNCH_TIMEOUT = 10000L
77
+ }
78
+ }
@@ -1,18 +1,13 @@
1
1
  package com.wix.detox;
2
2
 
3
- import android.app.Activity;
4
- import android.app.Instrumentation;
5
3
  import android.content.Context;
6
- import android.content.Intent;
7
- import android.os.Bundle;
8
-
9
- import com.wix.detox.config.DetoxConfig;
10
- import com.wix.detox.espresso.UiControllerSpy;
11
4
 
12
5
  import androidx.annotation.NonNull;
13
6
  import androidx.test.platform.app.InstrumentationRegistry;
14
7
  import androidx.test.rule.ActivityTestRule;
15
8
 
9
+ import com.wix.detox.config.DetoxConfig;
10
+
16
11
  /**
17
12
  * <p>Static class.</p>
18
13
  *
@@ -67,12 +62,7 @@ import androidx.test.rule.ActivityTestRule;
67
62
  * <p>If not set, then Detox tests are no ops. So it's safe to mix it with other tests.</p>
68
63
  */
69
64
  public final class Detox {
70
- private static final String INTENT_LAUNCH_ARGS_KEY = "launchArgs";
71
- private static final long ACTIVITY_LAUNCH_TIMEOUT = 10000L;
72
-
73
- private static final LaunchArgs sLaunchArgs = new LaunchArgs();
74
- private static final LaunchIntentsFactory sIntentsFactory = new LaunchIntentsFactory();
75
- private static ActivityTestRule sActivityTestRule;
65
+ private static ActivityLaunchHelper sActivityLaunchHelper;
76
66
 
77
67
  private Detox() {
78
68
  }
@@ -132,72 +122,20 @@ public final class Detox {
132
122
  DetoxConfig.CONFIG = detoxConfig;
133
123
  DetoxConfig.CONFIG.apply();
134
124
 
135
- sActivityTestRule = activityTestRule;
136
-
137
- UiControllerSpy.attachThroughProxy();
138
-
139
- Intent intent = extractInitialIntent();
140
- sActivityTestRule.launchActivity(intent);
141
-
142
- try {
143
- DetoxMain.run(context);
144
- } catch (Exception e) {
145
- Thread.currentThread().interrupt();
146
- throw new RuntimeException("Detox got interrupted prematurely", e);
147
- }
125
+ sActivityLaunchHelper = new ActivityLaunchHelper(activityTestRule);
126
+ DetoxMain.run(context, sActivityLaunchHelper);
148
127
  }
149
128
 
150
129
  public static void launchMainActivity() {
151
- final Activity activity = sActivityTestRule.getActivity();
152
- launchActivitySync(sIntentsFactory.activityLaunchIntent(activity));
130
+ sActivityLaunchHelper.launchMainActivity();
153
131
  }
154
132
 
155
133
  public static void startActivityFromUrl(String url) {
156
- launchActivitySync(sIntentsFactory.intentWithUrl(url, false));
134
+ sActivityLaunchHelper.startActivityFromUrl(url);
157
135
  }
158
136
 
159
137
  public static void startActivityFromNotification(String dataFilePath) {
160
- Bundle notificationData = new NotificationDataParser(dataFilePath).toBundle();
161
- Intent intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, false);
162
- launchActivitySync(intent);
163
- }
164
-
165
- private static Intent extractInitialIntent() {
166
- Intent intent;
167
-
168
- if (sLaunchArgs.hasUrlOverride()) {
169
- intent = sIntentsFactory.intentWithUrl(sLaunchArgs.getUrlOverride(), true);
170
- } else if (sLaunchArgs.hasNotificationPath()) {
171
- Bundle notificationData = new NotificationDataParser(sLaunchArgs.getNotificationPath()).toBundle();
172
- intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, true);
173
- } else {
174
- intent = sIntentsFactory.cleanIntent();
175
- }
176
- intent.putExtra(INTENT_LAUNCH_ARGS_KEY, sLaunchArgs.asIntentBundle());
177
- return intent;
178
- }
179
-
180
- private static void launchActivitySync(Intent intent) {
181
- // Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with.
182
- // BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background
183
- // would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without
184
- // ever being released*. It will finally fail after a 45 seconds timeout.
185
- // Without going into full details, it seems that activity test rules were not meant to be used this way. However,
186
- // the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using
187
- // dedicated methods such as moveToState(), which give better control over the lifecycle).
188
- // In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125
189
- // What it forces us to do, then, is this -
190
- // 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()).
191
- // 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready.
192
- // ^ Hence the code below.
193
-
194
- final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
195
- final Activity activity = sActivityTestRule.getActivity();
196
- final Instrumentation.ActivityMonitor activityMonitor = new Instrumentation.ActivityMonitor(activity.getClass().getName(), null, true);
197
-
198
- activity.startActivity(intent);
199
- instrumentation.addMonitor(activityMonitor);
200
- instrumentation.waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT);
138
+ sActivityLaunchHelper.startActivityFromNotification(dataFilePath);
201
139
  }
202
140
 
203
141
  private static Context getAppContext() {
@@ -3,35 +3,62 @@ package com.wix.detox
3
3
  import android.content.Context
4
4
  import android.util.Log
5
5
  import com.wix.detox.adapters.server.*
6
- import com.wix.detox.common.DetoxLog.Companion.LOG_TAG
6
+ import com.wix.detox.common.DetoxLog
7
+ import com.wix.detox.espresso.UiControllerSpy
7
8
  import com.wix.detox.instruments.DetoxInstrumentsManager
8
9
  import com.wix.detox.reactnative.ReactNativeExtension
9
10
  import com.wix.invoke.MethodInvocation
11
+ import java.util.concurrent.CountDownLatch
10
12
 
11
- private const val INIT_ACTION = "_init"
12
- private const val IS_READY_ACTION = "isReady"
13
13
  private const val TERMINATION_ACTION = "_terminate"
14
14
 
15
15
  object DetoxMain {
16
+ private val handshakeLock = CountDownLatch(1)
17
+
16
18
  @JvmStatic
17
- fun run(rnHostHolder: Context) {
19
+ fun run(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) {
18
20
  val detoxServerInfo = DetoxServerInfo()
19
- Log.i(LOG_TAG, "Detox server connection details: $detoxServerInfo")
20
-
21
21
  val testEngineFacade = TestEngineFacade()
22
22
  val actionsDispatcher = DetoxActionsDispatcher()
23
- val externalAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, IS_READY_ACTION, TERMINATION_ACTION)
24
- initActionHandlers(actionsDispatcher, externalAdapter, testEngineFacade, rnHostHolder)
25
- actionsDispatcher.dispatchAction(INIT_ACTION, "", 0)
23
+ val serverAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, TERMINATION_ACTION)
24
+
25
+ initCrashHandler(serverAdapter)
26
+ initANRListener(serverAdapter)
27
+ initEspresso()
28
+ initReactNative()
29
+
30
+ setupActionHandlers(actionsDispatcher, serverAdapter, testEngineFacade, rnHostHolder)
31
+ serverAdapter.connect()
32
+
33
+ launchActivityOnCue(rnHostHolder, activityLaunchHelper)
26
34
  actionsDispatcher.join()
27
35
  }
28
36
 
29
- private fun doInit(externalAdapter: DetoxServerAdapter, rnHostHolder: Context) {
30
- externalAdapter.connect()
37
+ /**
38
+ * Launch the tested activity "on cue", namely, right after a connection is established and the handshake
39
+ * completes successfully.
40
+ *
41
+ * This has to be synchronized so that an `isReady` isn't handled *before* the activity is launched (albeit not fully
42
+ * initialized - all native modules and everything) and a react context is available.
43
+ *
44
+ * As a better alternative, it would make sense to execute this as a simple action from within the actions
45
+ * dispatcher (i.e. handler of `loginSuccess`), in which case, no inter-thread locking would be required
46
+ * thanks to the usage of Handlers. However, in this type of a solution, errors / crashes would be reported
47
+ * not by instrumentation itself, but based on the `AppWillTerminateWithError` message; In it's own, it is a good
48
+ * thing, but for a reason we're not sure of yet, it is ignored by the test runner at this point in the flow.
49
+ */
50
+ @Synchronized
51
+ private fun launchActivityOnCue(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) {
52
+ awaitHandshake()
53
+ launchActivity(rnHostHolder, activityLaunchHelper)
54
+ }
55
+
56
+ private fun awaitHandshake() {
57
+ handshakeLock.await()
58
+ }
31
59
 
32
- initCrashHandler(externalAdapter)
33
- initANRListener(externalAdapter)
34
- initReactNativeIfNeeded(rnHostHolder)
60
+ private fun onLoginSuccess() {
61
+ handshakeLock.countDown()
35
62
  }
36
63
 
37
64
  private fun doTeardown(serverAdapter: DetoxServerAdapter, actionsDispatcher: DetoxActionsDispatcher, testEngineFacade: TestEngineFacade) {
@@ -41,35 +68,28 @@ object DetoxMain {
41
68
  actionsDispatcher.teardown()
42
69
  }
43
70
 
44
- private fun initActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) {
71
+ private fun setupActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) {
72
+ class SynchronizedActionHandler(private val actionHandler: DetoxActionHandler): DetoxActionHandler {
73
+ override fun handle(params: String, messageId: Long) {
74
+ synchronized(this@DetoxMain) {
75
+ actionHandler.handle(params, messageId)
76
+ }
77
+ }
78
+ }
79
+
45
80
  // Primary actions
46
81
  with(actionsDispatcher) {
47
- val rnReloadHandler = ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade)
82
+ val readyHandler = SynchronizedActionHandler( ReadyActionHandler(serverAdapter, testEngineFacade) )
83
+ val rnReloadHandler = SynchronizedActionHandler( ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade) )
48
84
 
49
- associateActionHandler(INIT_ACTION, object : DetoxActionHandler {
50
- override fun handle(params: String, messageId: Long) =
51
- synchronized(this@DetoxMain) {
52
- this@DetoxMain.doInit(serverAdapter, rnHostHolder)
53
- }
54
- })
55
- associateActionHandler(IS_READY_ACTION, ReadyActionHandler(serverAdapter, testEngineFacade))
56
-
57
- associateActionHandler("loginSuccess", ScarceActionHandler())
58
- associateActionHandler("reactNativeReload", object: DetoxActionHandler {
59
- override fun handle(params: String, messageId: Long) =
60
- synchronized(this@DetoxMain) {
61
- rnReloadHandler.handle(params, messageId)
62
- }
63
- })
85
+ associateActionHandler("loginSuccess", ::onLoginSuccess)
86
+ associateActionHandler("isReady", readyHandler)
87
+ associateActionHandler("reactNativeReload", rnReloadHandler)
64
88
  associateActionHandler("invoke", InvokeActionHandler(MethodInvocation(), serverAdapter))
65
89
  associateActionHandler("cleanup", CleanupActionHandler(serverAdapter, testEngineFacade) {
66
90
  dispatchAction(TERMINATION_ACTION, "", 0)
67
91
  })
68
- associateActionHandler(TERMINATION_ACTION, object: DetoxActionHandler {
69
- override fun handle(params: String, messageId: Long) {
70
- this@DetoxMain.doTeardown(serverAdapter, actionsDispatcher, testEngineFacade)
71
- }
72
- })
92
+ associateActionHandler(TERMINATION_ACTION) { -> doTeardown(serverAdapter, actionsDispatcher, testEngineFacade) }
73
93
 
74
94
  if (DetoxInstrumentsManager.supports()) {
75
95
  val instrumentsManager = DetoxInstrumentsManager(rnHostHolder)
@@ -80,13 +100,8 @@ object DetoxMain {
80
100
 
81
101
  // Secondary actions
82
102
  with(actionsDispatcher) {
83
- val queryStatusHandler = QueryStatusActionHandler(serverAdapter, testEngineFacade)
84
- associateActionHandler("currentStatus", object: DetoxActionHandler {
85
- override fun handle(params: String, messageId: Long) =
86
- synchronized(this@DetoxMain) {
87
- queryStatusHandler.handle(params, messageId)
88
- }
89
- }, false)
103
+ val queryStatusHandler = SynchronizedActionHandler( QueryStatusActionHandler(serverAdapter, testEngineFacade) )
104
+ associateSecondaryActionHandler("currentStatus", queryStatusHandler)
90
105
  }
91
106
  }
92
107
 
@@ -98,7 +113,17 @@ object DetoxMain {
98
113
  DetoxANRHandler(outboundServerAdapter).attach()
99
114
  }
100
115
 
101
- private fun initReactNativeIfNeeded(rnHostHolder: Context) {
116
+ private fun initEspresso() {
117
+ UiControllerSpy.attachThroughProxy()
118
+ }
119
+
120
+ private fun initReactNative() {
121
+ ReactNativeExtension.initIfNeeded()
122
+ }
123
+
124
+ private fun launchActivity(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) {
125
+ Log.i(DetoxLog.LOG_TAG, "Launching the tested activity!")
126
+ activityLaunchHelper.launchActivityUnderTest()
102
127
  ReactNativeExtension.waitForRNBootstrap(rnHostHolder)
103
128
  }
104
129
  }
@@ -6,7 +6,7 @@ import android.content.Intent
6
6
  import android.net.Uri
7
7
  import android.os.Bundle
8
8
 
9
- internal class LaunchIntentsFactory {
9
+ class LaunchIntentsFactory {
10
10
 
11
11
  /**
12
12
  * Constructs an intent tightly associated with a specific activity.
@@ -5,7 +5,7 @@ import com.wix.detox.common.JsonConverter
5
5
  import com.wix.detox.common.TextFileReader
6
6
  import org.json.JSONObject
7
7
 
8
- internal class NotificationDataParser(private val notificationPath: String) {
8
+ class NotificationDataParser(private val notificationPath: String) {
9
9
  fun toBundle(): Bundle {
10
10
  val rawData = readNotificationFromFile()
11
11
  val json = JSONObject(rawData)
@@ -152,7 +152,3 @@ class InstrumentsEventsActionsHandler(
152
152
  outboundServerAdapter.sendMessage("eventDone", emptyMap<String, Any>(), messageId)
153
153
  }
154
154
  }
155
-
156
- class ScarceActionHandler: DetoxActionHandler {
157
- override fun handle(params: String, messageId: Long) {}
158
- }
@@ -11,11 +11,18 @@ class DetoxActionsDispatcher {
11
11
  private val primaryExec = ActionsExecutor("detox.primary")
12
12
  private val secondaryExec = ActionsExecutor("detox.secondary")
13
13
 
14
- fun associateActionHandler(type: String, actionHandler: DetoxActionHandler, isPrimary: Boolean = true) {
15
- val actionsExecutor = (if (isPrimary) primaryExec else secondaryExec)
16
- actionsExecutor.associateHandler(type, actionHandler)
14
+ fun associateActionHandler(type: String, actionHandler: DetoxActionHandler) =
15
+ associateActionHandler(type, actionHandler, true)
16
+
17
+ fun associateActionHandler(type: String, handlerFunc: () -> Unit) {
18
+ associateActionHandler(type, object: DetoxActionHandler {
19
+ override fun handle(params: String, messageId: Long) = handlerFunc()
20
+ })
17
21
  }
18
22
 
23
+ fun associateSecondaryActionHandler(type: String, actionHandler: DetoxActionHandler) =
24
+ associateActionHandler(type, actionHandler, false)
25
+
19
26
  fun dispatchAction(type: String, params: String, messageId: Long) {
20
27
  (primaryExec.executeAction(type, params, messageId) ||
21
28
  secondaryExec.executeAction(type, params, messageId))
@@ -33,6 +40,11 @@ class DetoxActionsDispatcher {
33
40
  primaryExec.join()
34
41
  secondaryExec.join()
35
42
  }
43
+
44
+ private fun associateActionHandler(type: String, actionHandler: DetoxActionHandler, isPrimary: Boolean = true) {
45
+ val actionsExecutor = (if (isPrimary) primaryExec else secondaryExec)
46
+ actionsExecutor.associateHandler(type, actionHandler)
47
+ }
36
48
  }
37
49
 
38
50
  private class ActionsExecutor(name: String) {
@@ -74,7 +86,5 @@ private class ActionsExecutor(name: String) {
74
86
  handler.looper.quit()
75
87
  }
76
88
 
77
- fun join() {
78
- thread.join()
79
- }
89
+ fun join() = thread.join()
80
90
  }
@@ -10,7 +10,6 @@ interface OutboundServerAdapter {
10
10
  class DetoxServerAdapter(
11
11
  private val actionsDispatcher: DetoxActionsDispatcher,
12
12
  private val detoxServerInfo: DetoxServerInfo,
13
- private val readyActionType: String,
14
13
  private val terminationActionType: String)
15
14
  : WebSocketClient.WSEventsHandler, OutboundServerAdapter {
16
15
 
@@ -27,7 +26,6 @@ class DetoxServerAdapter(
27
26
 
28
27
  override fun onConnect() {
29
28
  Log.i(DetoxLog.LOG_TAG, "Connected to server!")
30
- actionsDispatcher.dispatchAction(readyActionType, "", -1000L)
31
29
  }
32
30
 
33
31
  override fun onClosed() {
@@ -1,7 +1,9 @@
1
1
  package com.wix.detox.adapters.server
2
2
 
3
+ import android.util.Log
3
4
  import androidx.test.platform.app.InstrumentationRegistry
4
5
  import com.wix.detox.LaunchArgs
6
+ import com.wix.detox.common.DetoxLog
5
7
 
6
8
  private const val DEFAULT_URL = "ws://localhost:8099"
7
9
 
@@ -9,7 +11,7 @@ class DetoxServerInfo internal constructor(launchArgs: LaunchArgs = LaunchArgs()
9
11
  val serverUrl: String = launchArgs.detoxServerUrl ?: DEFAULT_URL
10
12
  val sessionId: String = launchArgs.detoxSessionId ?: InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName
11
13
 
12
- override fun toString(): String {
13
- return "url=$serverUrl, sessionId=$sessionId"
14
+ init {
15
+ Log.i(DetoxLog.LOG_TAG, "Detox server connection details: url=$serverUrl, sessionId=$sessionId")
14
16
  }
15
17
  }
@@ -0,0 +1,44 @@
1
+ package com.wix.detox.reactnative
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.ReactMarker
5
+ import com.facebook.react.bridge.ReactMarkerConstants
6
+ import com.facebook.react.bridge.ReactMarkerConstants.*
7
+
8
+ object ReactMarkersLogger : ReactMarker.MarkerListener {
9
+
10
+ fun attach() {
11
+ ReactMarker.addListener(this)
12
+ }
13
+
14
+ override fun logMarker(marker: ReactMarkerConstants, p1: String?, p2: Int) {
15
+ when {
16
+ marker == DOWNLOAD_START ||
17
+ marker == DOWNLOAD_END ||
18
+ marker == BUILD_REACT_INSTANCE_MANAGER_START ||
19
+ marker == BUILD_REACT_INSTANCE_MANAGER_END ||
20
+ marker == REACT_BRIDGE_LOADING_START ||
21
+ marker == REACT_BRIDGE_LOADING_END ||
22
+ marker == REACT_BRIDGELESS_LOADING_START ||
23
+ marker == REACT_BRIDGELESS_LOADING_END ||
24
+ marker == CREATE_MODULE_START ||
25
+ marker == CREATE_MODULE_END ||
26
+ marker == NATIVE_MODULE_SETUP_START ||
27
+ marker == NATIVE_MODULE_SETUP_END ||
28
+ marker == PRE_RUN_JS_BUNDLE_START ||
29
+ marker == RUN_JS_BUNDLE_START ||
30
+ marker == RUN_JS_BUNDLE_END ||
31
+ marker == CONTENT_APPEARED ||
32
+ marker == CREATE_CATALYST_INSTANCE_START ||
33
+ marker == CREATE_CATALYST_INSTANCE_END ||
34
+ marker == DESTROY_CATALYST_INSTANCE_START ||
35
+ marker == DESTROY_CATALYST_INSTANCE_END ||
36
+ marker == CREATE_REACT_CONTEXT_START ||
37
+ marker == CREATE_REACT_CONTEXT_END ||
38
+ marker == PROCESS_PACKAGES_START ||
39
+ marker == PROCESS_PACKAGES_END ||
40
+ false ->
41
+ Log.d("Detox.RNMarker", "$marker ($p1)")
42
+ }
43
+ }
44
+ }
@@ -14,6 +14,31 @@ private const val LOG_TAG = "DetoxRNExt"
14
14
  object ReactNativeExtension {
15
15
  private var rnIdlingResources: ReactNativeIdlingResources? = null
16
16
 
17
+ fun initIfNeeded() {
18
+ if (!ReactNativeInfo.isReactNativeApp()) {
19
+ return
20
+ }
21
+
22
+ ReactMarkersLogger.attach()
23
+ }
24
+
25
+ /**
26
+ * Wait for React-Native to finish loading (i.e. make RN context available).
27
+ *
28
+ * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance.
29
+ */
30
+ fun waitForRNBootstrap(applicationContext: Context) {
31
+ if (!ReactNativeInfo.isReactNativeApp()) {
32
+ return
33
+ }
34
+
35
+ (applicationContext as ReactApplication).let {
36
+ val reactContext = awaitNewReactNativeContext(it, null)
37
+
38
+ enableOrDisableSynchronization(reactContext)
39
+ }
40
+ }
41
+
17
42
  /**
18
43
  * Reloads the React Native context and thus all javascript code.
19
44
  *
@@ -40,26 +65,6 @@ object ReactNativeExtension {
40
65
  val reactContext = awaitNewReactNativeContext(it, previousReactContext)
41
66
 
42
67
  enableOrDisableSynchronization(reactContext, networkSyncEnabled)
43
- hackRN50OrHigherWaitForReady()
44
- }
45
- }
46
-
47
- /**
48
- * Wait for React-Native to finish loading (i.e. make RN context available).
49
- *
50
- * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance.
51
- */
52
- @JvmStatic
53
- fun waitForRNBootstrap(applicationContext: Context) {
54
- if (!ReactNativeInfo.isReactNativeApp()) {
55
- return
56
- }
57
-
58
- (applicationContext as ReactApplication).let {
59
- val reactContext = awaitNewReactNativeContext(it, null)
60
-
61
- enableOrDisableSynchronization(reactContext)
62
- hackRN50OrHigherWaitForReady()
63
68
  }
64
69
  }
65
70
 
@@ -145,18 +150,6 @@ object ReactNativeExtension {
145
150
  }
146
151
  }
147
152
 
148
- private fun hackRN50OrHigherWaitForReady() {
149
- if (ReactNativeInfo.rnVersion().minor in 50..62) {
150
- try {
151
- //TODO- Temp hack to make Detox usable for RN>=50 till we find a better sync solution.
152
- Thread.sleep(1000)
153
- } catch (e: InterruptedException) {
154
- e.printStackTrace()
155
- }
156
-
157
- }
158
- }
159
-
160
153
  private fun clearIdlingResources() {
161
154
  rnIdlingResources?.unregisterAll()
162
155
  rnIdlingResources = null
@@ -11,11 +11,8 @@ class UiControllerSpy: MethodsSpy("uiController") {
11
11
  fun eventInjectionsIterator(): Iterator<CallInfo?> = historyOf("injectMotionEvent").iterator()
12
12
 
13
13
  companion object {
14
- @JvmStatic
15
14
  val instance = UiControllerSpy()
16
15
 
17
- @JvmStatic
18
- @JvmOverloads
19
16
  fun attachThroughProxy(spy: UiControllerSpy = instance) {
20
17
  val eventsInjectorReflected = EventsInjectorReflected(getUiController())
21
18
 
@@ -0,0 +1,111 @@
1
+ package com.wix.detox
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.os.Bundle
6
+ import org.mockito.kotlin.*
7
+ import androidx.test.rule.ActivityTestRule
8
+ import org.junit.runner.RunWith
9
+ import org.assertj.core.api.Assertions.assertThat
10
+ import org.junit.Before
11
+ import org.junit.Test
12
+ import org.mockito.ArgumentMatchers.anyBoolean
13
+ import org.mockito.ArgumentMatchers.anyString
14
+ import org.robolectric.RobolectricTestRunner
15
+
16
+ @RunWith(RobolectricTestRunner::class)
17
+ class ActivityLaunchHelperTest {
18
+
19
+ private val initialURL = "detox://unit-test"
20
+ private val bundleExtraLaunchArgs = "launchArgs"
21
+ private val notificationPath = "path/to/notification.data"
22
+
23
+ private lateinit var intent: Intent
24
+ private lateinit var launchArgsAsBundle: Bundle
25
+ private lateinit var notificationDataAsBundle: Bundle
26
+ private lateinit var testRule: ActivityTestRule<Activity>
27
+ private lateinit var launchArgs: LaunchArgs
28
+ private lateinit var intentsFactory: LaunchIntentsFactory
29
+ private lateinit var notificationDataParser: NotificationDataParser
30
+
31
+ private fun uut() = ActivityLaunchHelper(testRule, launchArgs, intentsFactory, { notificationDataParser })
32
+
33
+ @Before
34
+ fun setup() {
35
+ intent = Intent()
36
+ launchArgsAsBundle = mock()
37
+ notificationDataAsBundle = mock()
38
+
39
+ testRule = mock()
40
+ launchArgs = mock() {
41
+ on { asIntentBundle() }.thenReturn(launchArgsAsBundle)
42
+ }
43
+ intentsFactory = mock()
44
+ notificationDataParser = mock() {
45
+ on { toBundle() }.thenReturn(notificationDataAsBundle)
46
+ }
47
+ }
48
+
49
+ @Test
50
+ fun `default-activity -- should launch using test rule, with a clean intent`() {
51
+ givenCleanLaunch()
52
+ uut().launchActivityUnderTest()
53
+ verify(testRule).launchActivity(eq(intent))
54
+ }
55
+
56
+ @Test
57
+ fun `default-activity -- should apply launch args to intent`() {
58
+ givenCleanLaunch()
59
+ uut().launchActivityUnderTest()
60
+ assertIntentHasLaunchArgs()
61
+ }
62
+
63
+ @Test
64
+ fun `default activity, with a url -- should launch based on the url`() {
65
+ givenLaunchWithInitialURL()
66
+ uut().launchActivityUnderTest()
67
+ verify(testRule).launchActivity(eq(intent))
68
+ verify(intentsFactory).intentWithUrl(initialURL, true)
69
+ }
70
+
71
+ @Test
72
+ fun `default activity, with a url -- should apply launch args to intent`() {
73
+ givenLaunchWithInitialURL()
74
+ uut().launchActivityUnderTest()
75
+ assertIntentHasLaunchArgs()
76
+ }
77
+
78
+ @Test
79
+ fun `default activity, with notification data -- should launch with the data as bundle`() {
80
+ givenLaunchWithNotificationData()
81
+ uut().launchActivityUnderTest()
82
+ verify(testRule).launchActivity(eq(intent))
83
+ verify(intentsFactory).intentWithNotificationData(any(), eq(notificationDataAsBundle), eq(true))
84
+ }
85
+
86
+ @Test
87
+ fun `default activity, with notification data -- should apply launch args to intent`() {
88
+ givenLaunchWithNotificationData()
89
+ uut().launchActivityUnderTest()
90
+ assertIntentHasLaunchArgs()
91
+ }
92
+
93
+ private fun givenCleanLaunch() {
94
+ whenever(intentsFactory.cleanIntent()).thenReturn(intent)
95
+ }
96
+ private fun givenLaunchWithInitialURL() {
97
+ whenever(launchArgs.hasUrlOverride()).thenReturn(true)
98
+ whenever(launchArgs.urlOverride).thenReturn(initialURL)
99
+ whenever(intentsFactory.intentWithUrl(anyString(), anyBoolean())).thenReturn(intent)
100
+ }
101
+ private fun givenLaunchWithNotificationData() {
102
+ whenever(launchArgs.hasNotificationPath()).thenReturn(true)
103
+ whenever(launchArgs.notificationPath).thenReturn(notificationPath)
104
+ whenever(intentsFactory.intentWithNotificationData(any(), any(), anyBoolean()))
105
+ .thenReturn(intent)
106
+ }
107
+ private fun assertIntentHasLaunchArgs() {
108
+ assertThat(intent.hasExtra(bundleExtraLaunchArgs)).isTrue
109
+ assertThat(intent.getBundleExtra(bundleExtraLaunchArgs)).isEqualTo(launchArgsAsBundle)
110
+ }
111
+ }
@@ -3,10 +3,7 @@ package com.wix.detox.espresso.matcher
3
3
  import org.junit.Test
4
4
  import kotlin.test.assertFalse
5
5
  import kotlin.test.assertTrue
6
- import org.junit.runner.RunWith
7
- import org.robolectric.RobolectricTestRunner
8
6
 
9
- @RunWith(RobolectricTestRunner::class)
10
7
  class RegexMatcherTest {
11
8
  @Test
12
9
  fun `should work with string matching regex`() {
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.14.3",
4
+ "version": "20.15.0-prerelease.0",
5
5
  "bin": {
6
6
  "detox": "local-cli/cli.js"
7
7
  },
@@ -109,5 +109,5 @@
109
109
  "browserslist": [
110
110
  "node 14"
111
111
  ],
112
- "gitHead": "a7bbc444bab6fbce3d561cd110c40ba1b2b84a6b"
112
+ "gitHead": "d8d24e996503ce2618991d4ec6b9ae546335ad49"
113
113
  }
@@ -58,17 +58,6 @@ class Detox {
58
58
  };
59
59
  }
60
60
 
61
- static extractInitialIntent() {
62
- return {
63
- target: {
64
- type: "Class",
65
- value: "com.wix.detox.Detox"
66
- },
67
- method: "extractInitialIntent",
68
- args: []
69
- };
70
- }
71
-
72
61
  static getAppContext() {
73
62
  return {
74
63
  target: {
@@ -1 +0,0 @@
1
- 3b1d7b9d4af2ad509d1e2986540f5208
@@ -1 +0,0 @@
1
- 4e8792170dfbac074d9e5fc88548c9e85b05dd67
@@ -1 +0,0 @@
1
- 57e41d5affbdcb771d08c680235f5ed17534420bfee9c6489c0baf2c4550d857
@@ -1 +0,0 @@
1
- fb2650ae7ae4e56953fcfc7579bdf99bebedeed3cc575413bf56b6251362853fe5c16f17a490d031742b16e4735f009513d93a89636f8c069f06bc34f5a2b205
@@ -1 +0,0 @@
1
- 16eebdb32005b5bc0474d0426e92a7ef
@@ -1 +0,0 @@
1
- 858e7440d43d7dd6155435c778dc20cddde0eac9
@@ -1 +0,0 @@
1
- be627415e355baea1337b7efd541614ba258290227b23f11b943c774fffeafa8
@@ -1 +0,0 @@
1
- e489a68066a5e07dbaed4b780e81453755e5ef9f31cb0fe1125cbf156e3ccef0a34cc00abdb84f4788a864ac9c1a4b8c1d8bef1d30ce2eaec8903758c211873c
@@ -1 +0,0 @@
1
- 070f7c4048400fe1d6b1599414f5c7f1
@@ -1 +0,0 @@
1
- 52ebf8f7cd97ab4d28216ce8504635be1f74b57c
@@ -1 +0,0 @@
1
- 42299d1835b5db9fe3b7523d2d656a8834fff583ae82521ff69b9e050814ce3a
@@ -1 +0,0 @@
1
- aeecbc2448822395dc7d9674c0a30f5cffd9cacd8361bd55a1a4f0bb8cafe82cb812fbe1da890395cfcc65439dcb4666e119a356a054c582cb7dfe225fb9d858
@@ -1 +0,0 @@
1
- a69ee7399f34d05477dad353196d10ce
@@ -1 +0,0 @@
1
- fdd3056e3977c32501d0aba7e0b71fa9cc706384
@@ -1 +0,0 @@
1
- 6ff96bda411d85d30d6075a8485e8a463c807e4673d4b038ef331bb67b31a577
@@ -1 +0,0 @@
1
- ea6781750ec935a59476e178814f6d5b0eb3f5a3559e6f19a69b86c321a21641dd49f103adcd6843b68c27f530bdbb24eb838e35e933e931c30a2e567a8c9996