detox 20.14.4-smoke.0 → 20.14.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. package/Detox-android/com/wix/detox/{20.14.4-smoke.0/detox-20.14.4-smoke.0-javadoc.jar → 20.14.4/detox-20.14.4-javadoc.jar} +0 -0
  2. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4-javadoc.jar.md5 +1 -0
  3. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4-javadoc.jar.sha1 +1 -0
  4. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4-javadoc.jar.sha256 +1 -0
  5. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4-javadoc.jar.sha512 +1 -0
  6. package/Detox-android/com/wix/detox/{20.14.4-smoke.0/detox-20.14.4-smoke.0-sources.jar → 20.14.4/detox-20.14.4-sources.jar} +0 -0
  7. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4-sources.jar.md5 +1 -0
  8. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4-sources.jar.sha1 +1 -0
  9. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4-sources.jar.sha256 +1 -0
  10. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4-sources.jar.sha512 +1 -0
  11. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4.aar +0 -0
  12. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4.aar.md5 +1 -0
  13. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4.aar.sha1 +1 -0
  14. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4.aar.sha256 +1 -0
  15. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4.aar.sha512 +1 -0
  16. package/Detox-android/com/wix/detox/{20.14.4-smoke.0/detox-20.14.4-smoke.0.pom → 20.14.4/detox-20.14.4.pom} +1 -1
  17. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4.pom.md5 +1 -0
  18. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4.pom.sha1 +1 -0
  19. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4.pom.sha256 +1 -0
  20. package/Detox-android/com/wix/detox/20.14.4/detox-20.14.4.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/proguard-rules-app.pro +2 -1
  29. package/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt +78 -0
  30. package/android/detox/src/full/java/com/wix/detox/Detox.java +8 -70
  31. package/android/detox/src/full/java/com/wix/detox/DetoxMain.kt +69 -44
  32. package/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt +1 -1
  33. package/android/detox/src/full/java/com/wix/detox/NotificationDataParser.kt +1 -1
  34. package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt +0 -4
  35. package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt +16 -6
  36. package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt +0 -2
  37. package/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt +4 -2
  38. package/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt +44 -0
  39. package/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt +25 -32
  40. package/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt +0 -3
  41. package/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt +111 -0
  42. package/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/RegexMatcherTest.kt +0 -3
  43. package/package.json +2 -2
  44. package/src/android/espressoapi/Detox.js +0 -11
  45. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0-javadoc.jar.md5 +0 -1
  46. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0-javadoc.jar.sha1 +0 -1
  47. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0-javadoc.jar.sha256 +0 -1
  48. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0-javadoc.jar.sha512 +0 -1
  49. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0-sources.jar.md5 +0 -1
  50. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0-sources.jar.sha1 +0 -1
  51. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0-sources.jar.sha256 +0 -1
  52. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0-sources.jar.sha512 +0 -1
  53. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0.aar +0 -0
  54. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0.aar.md5 +0 -1
  55. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0.aar.sha1 +0 -1
  56. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0.aar.sha256 +0 -1
  57. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0.aar.sha512 +0 -1
  58. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0.pom.md5 +0 -1
  59. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0.pom.sha1 +0 -1
  60. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0.pom.sha256 +0 -1
  61. package/Detox-android/com/wix/detox/20.14.4-smoke.0/detox-20.14.4-smoke.0.pom.sha512 +0 -1
@@ -0,0 +1 @@
1
+ e3fe08115a10d60faff7d0666d8424a8
@@ -0,0 +1 @@
1
+ 758df184cbf0014569937f79a9159e2830f48235
@@ -0,0 +1 @@
1
+ 2790a45d2999e0dea3089ba464bca9d27745774574e55f3e352f0662ce1abd89
@@ -0,0 +1 @@
1
+ 2006fa8aca6a34168de9122808517afd219d0d70521a781c113d7192d1793b3b976ae9c982ab3f3210b5aa13be2e28c302ff68d037399549325b58e035108abd
@@ -0,0 +1 @@
1
+ f57b338070b87d202a36f02d1884880e
@@ -0,0 +1 @@
1
+ a09c7a7f11f8d6afafe3352d7bf8cbb9e038f35e
@@ -0,0 +1 @@
1
+ a3cf6c53fa57c88e34d5cf819fe5e3a55375c841efc4452c642b4ebf876d160d
@@ -0,0 +1 @@
1
+ e9d0fadd3cce27cf46744537dff210cb32fac9d34296b8ee6669ac3642db10af33ed196f70ad251afeb268acf785690d76252e539b6c5253a2a271908a3770c1
@@ -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.4-smoke.0</version>
6
+ <version>20.14.4</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
+ 30f7fec25a048bfadec9f0d4fb71c170
@@ -0,0 +1 @@
1
+ 91ad6502b9702459aa188f9f05851f5ea9e4fba3
@@ -0,0 +1 @@
1
+ 78c456b4f28e5810ea54fdf3674ad4c5782d7fdc620d6c7704dd26922368cfa4
@@ -0,0 +1 @@
1
+ 4ed59a1fe2c6b181c0530db3290cac1cd963e4760e702d7788d9dfd80561e9286bb36d3d77f591f7bcddff922109f184fab585ead9d1b0c46bf2305fefdb2308
@@ -3,11 +3,11 @@
3
3
  <groupId>com.wix</groupId>
4
4
  <artifactId>detox</artifactId>
5
5
  <versioning>
6
- <latest>20.14.4-smoke.0</latest>
7
- <release>20.14.4-smoke.0</release>
6
+ <latest>20.14.4</latest>
7
+ <release>20.14.4</release>
8
8
  <versions>
9
- <version>20.14.4-smoke.0</version>
9
+ <version>20.14.4</version>
10
10
  </versions>
11
- <lastUpdated>20231221025706</lastUpdated>
11
+ <lastUpdated>20231224123046</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 891c5308c9e49fc080cb14e82d7e6919
1
+ 89a763b99524e7f751453c53ef3812eb
@@ -1 +1 @@
1
- 30bbc998cdcd5633acd13644f592f971a142dc3f
1
+ adb3657478aff09342d22101b7ee1a8b8f5d2721
@@ -1 +1 @@
1
- b12dc7fa9e99b2896286d020a9d7386a8321f19c8a1e6872dbf51d40ae4d45f7
1
+ 3d1078d09bb0d52a126b9892a605a408444deffdebd97f89d4d79f1e2f271583
@@ -1 +1 @@
1
- 39aeb05537af1b3f2b2124fe5c0775c21f6a682770ee478874a0c845cea3ccf4a68bbab568d0249a1942d5be71edc0d05355484018cdb93da4fadc733a82de23
1
+ 64e8a28c598f81b0e2cdf19da8f9cd674ff7a67995535cf38a9b78dff24d6217f810472b10699e3294b12034caa6e1906ca8b70d86d539d11ff7a752012aad2b
package/Detox-ios-src.tbz CHANGED
Binary file
package/Detox-ios.tbz CHANGED
Binary file
@@ -20,4 +20,5 @@
20
20
  -keep class kotlin.io.** { *; }
21
21
  -keep class okhttp3.** { *; }
22
22
 
23
- -keep class androidx.concurrent.futures.** { *; }
23
+ -keep class androidx.concurrent.futures.CallbackToFutureAdapter$* { *; }
24
+ -keep class androidx.concurrent.futures.CallbackToFutureAdapter { *; }
@@ -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.4-smoke.0",
4
+ "version": "20.14.4",
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": "cef00d5518934c10b512869ee716be753e20cad6"
112
+ "gitHead": "7e6ab808abdf8754a120bca27b4a1d998883cdc2"
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
- 3d0b64bdd6e7a436ee911d1418503a6d
@@ -1 +0,0 @@
1
- 6635f90d741216b5aa76b5032514ba9784e4c671
@@ -1 +0,0 @@
1
- b4b6831643b10e07a874aa90546f704478697908361c10679b4fb03f070ea287
@@ -1 +0,0 @@
1
- 335d6a60ba8ac361a6404f631e0a2b1ba07a1e823dfe606fc35d28100516bd2a0eb727097d9c648b801192d6f5a0fcc61c6435a8e459ba0c7d83de435af4b255
@@ -1 +0,0 @@
1
- f5c0348c6cf33679d63e5f79a4cb7be9
@@ -1 +0,0 @@
1
- 4289a96b2a89d2d1ac3299142c65d7cbe7f7126d
@@ -1 +0,0 @@
1
- e8f3230b45eb8950baa3400408ef9e21a5d0d61275fb63bffdfa11f97298a68f
@@ -1 +0,0 @@
1
- f3420efb7574cee763513971f8a6753d72ad2aa47a479a825f321ae8c3bf98680b42bf5d9c1c37edcf6aa8528581a65ac8ee2ea0ef0f04682a5c693d2397eb62
@@ -1 +0,0 @@
1
- 070f7c4048400fe1d6b1599414f5c7f1
@@ -1 +0,0 @@
1
- 52ebf8f7cd97ab4d28216ce8504635be1f74b57c
@@ -1 +0,0 @@
1
- 42299d1835b5db9fe3b7523d2d656a8834fff583ae82521ff69b9e050814ce3a
@@ -1 +0,0 @@
1
- aeecbc2448822395dc7d9674c0a30f5cffd9cacd8361bd55a1a4f0bb8cafe82cb812fbe1da890395cfcc65439dcb4666e119a356a054c582cb7dfe225fb9d858
@@ -1 +0,0 @@
1
- 0fc89c4edda6315cb57a7cf8e97f90dc
@@ -1 +0,0 @@
1
- f3fe5b9e1063318f207d7c185d19ecbe21a84f6d
@@ -1 +0,0 @@
1
- ba3399fb186167a3d399cb00c12f39db8597fc00c5ca269e6ae98fd8af8dd4eb
@@ -1 +0,0 @@
1
- 34e9d1dc0267749af9e4cba6899b3c75e0275f16edca353e9d52fbc07a8f1582aeebead081595c7794fd8bbb37680870e7281a9f4d249be3de73dec508a41ce8