detox 20.4.0 → 20.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. package/Detox-android/com/wix/detox/{20.4.0/detox-20.4.0-javadoc.jar → 20.6.0/detox-20.6.0-javadoc.jar} +0 -0
  2. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-javadoc.jar.md5 +1 -0
  3. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-javadoc.jar.sha1 +1 -0
  4. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-javadoc.jar.sha256 +1 -0
  5. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-javadoc.jar.sha512 +1 -0
  6. package/Detox-android/com/wix/detox/{20.4.0/detox-20.4.0-sources.jar → 20.6.0/detox-20.6.0-sources.jar} +0 -0
  7. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-sources.jar.md5 +1 -0
  8. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-sources.jar.sha1 +1 -0
  9. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-sources.jar.sha256 +1 -0
  10. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-sources.jar.sha512 +1 -0
  11. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar +0 -0
  12. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar.md5 +1 -0
  13. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar.sha1 +1 -0
  14. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar.sha256 +1 -0
  15. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar.sha512 +1 -0
  16. package/Detox-android/com/wix/detox/{20.4.0/detox-20.4.0.pom → 20.6.0/detox-20.6.0.pom} +1 -1
  17. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.pom.md5 +1 -0
  18. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.pom.sha1 +1 -0
  19. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.pom.sha256 +1 -0
  20. package/Detox-android/com/wix/detox/20.6.0/detox-20.6.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/common/UIExtensions.kt +28 -0
  29. package/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java +11 -1
  30. package/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt +4 -3
  31. package/android/detox/src/full/java/com/wix/detox/espresso/matcher/ViewMatchers.kt +8 -5
  32. package/android/detox/src/full/java/com/wix/detox/espresso/matcher/WithAccessibilityLabelMatcher.kt +23 -0
  33. package/android/detox/src/full/java/com/wix/detox/reactnative/ui/UIExtensions.kt +37 -0
  34. package/android/detox/src/full/java/com/wix/detox/reactnative/utils/RNUtils.kt +6 -0
  35. package/android/detox/src/testFull/java/com/wix/detox/UTHelpers.kt +12 -0
  36. package/android/detox/src/testFull/java/com/wix/detox/common/UIExtensionsTest.kt +107 -0
  37. package/android/detox/src/testFull/java/com/wix/detox/espresso/action/GetAttributesActionTest.kt +7 -6
  38. package/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/ViewAtIndexMatcherSpec.kt +1 -2
  39. package/index.d.ts +26 -21
  40. package/local-cli/cli.js +1 -1
  41. package/local-cli/init.js +2 -2
  42. package/local-cli/start.js +49 -0
  43. package/local-cli/startCommand/AppStartCommand.js +65 -0
  44. package/local-cli/testCommand/TestRunnerCommand.js +29 -0
  45. package/local-cli/testCommand/builder.js +5 -0
  46. package/package.json +3 -2
  47. package/src/android/core/NativeMatcher.js +17 -0
  48. package/src/android/espressoapi/DetoxMatcher.js +24 -0
  49. package/src/android/matchers/index.js +2 -2
  50. package/src/android/matchers/native.js +9 -1
  51. package/src/configuration/collectCliConfig.js +1 -0
  52. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-javadoc.jar.md5 +0 -1
  53. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-javadoc.jar.sha1 +0 -1
  54. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-javadoc.jar.sha256 +0 -1
  55. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-javadoc.jar.sha512 +0 -1
  56. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-sources.jar.md5 +0 -1
  57. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-sources.jar.sha1 +0 -1
  58. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-sources.jar.sha256 +0 -1
  59. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-sources.jar.sha512 +0 -1
  60. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar +0 -0
  61. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar.md5 +0 -1
  62. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar.sha1 +0 -1
  63. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar.sha256 +0 -1
  64. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar.sha512 +0 -1
  65. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.pom.md5 +0 -1
  66. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.pom.sha1 +0 -1
  67. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.pom.sha256 +0 -1
  68. package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.pom.sha512 +0 -1
@@ -0,0 +1 @@
1
+ c051f79d02bcc98757dd1ea06512a63b
@@ -0,0 +1 @@
1
+ bb48d0747cb607e73a01d64f5eb758ce00e376cb
@@ -0,0 +1 @@
1
+ b31397b81cdca47b2d5dc1aca7821f2691d236114064949ad8553fd81340fe02
@@ -0,0 +1 @@
1
+ 6cec7c20f0d9643b98e2fa2f4740c9bb7a4b82ae3851069bb183e16ffcace8b0ceb07cf8d19316a960beb2330ea3e0a508df80cda1561242149ce3b16357d954
@@ -0,0 +1 @@
1
+ 6202d8d0f83dda9ed0c9d6962259350e
@@ -0,0 +1 @@
1
+ 505b10faaca41e1b5e7f27b8829cd14aff6b5626
@@ -0,0 +1 @@
1
+ f4b81a4c2c1665975ac5be02698e288a311e3cd3c728ba9b05e72a1932f7482e
@@ -0,0 +1 @@
1
+ e355acd2730e5ca57bb7f2c092942b6dd98997645b43350fd2c80051f3f8a6840f2746cd2e8fbeae862ab4cac0fc6339b7f298e697f3bfb4124598370c4b2a54
@@ -0,0 +1 @@
1
+ f62ab63d03ba71b4776965696f3d4a88
@@ -0,0 +1 @@
1
+ 1a38f931edf9925af1d131a0e0a0822bd880dcc8
@@ -0,0 +1 @@
1
+ 4145b5a8078b613e10205937ffb3da254d006fe78fbb169d8b2cedb3d02c021c
@@ -0,0 +1 @@
1
+ ab25979faccc1aa8200a637ea866a5fc59e35bf340170673a01253ae8ce479fb5b7af87167dbde2d43d5b536f3fb350b34b478bbf75f5e87d5276940283a8ab6
@@ -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.4.0</version>
6
+ <version>20.6.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
+ 120956f03efbba5c9cec898b7da52efe
@@ -0,0 +1 @@
1
+ b6c2c0740f1de17d7dc5671d096debbe7eb46dd3
@@ -0,0 +1 @@
1
+ 8fdf9933acc2bcc22588bc3cffb6348ab73f8100c5212f7e607e315bce59a265
@@ -0,0 +1 @@
1
+ f3ae74b6397013d7a5af6ed08cbbd739f5e207df9463a6b6cb496bd1b9a87022f193b87207d6424f2b8b85e022836211215cd01eaa88f9adcea6515594f59e76
@@ -3,11 +3,11 @@
3
3
  <groupId>com.wix</groupId>
4
4
  <artifactId>detox</artifactId>
5
5
  <versioning>
6
- <latest>20.4.0</latest>
7
- <release>20.4.0</release>
6
+ <latest>20.6.0</latest>
7
+ <release>20.6.0</release>
8
8
  <versions>
9
- <version>20.4.0</version>
9
+ <version>20.6.0</version>
10
10
  </versions>
11
- <lastUpdated>20230228144344</lastUpdated>
11
+ <lastUpdated>20230322171518</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- cdc80e0982319a663e6f0e4ac720d785
1
+ f72e8f5e279054eb2e8e8faab020aaab
@@ -1 +1 @@
1
- 5cd147e3e28a8e1ae4b256edfabaac149e2c1387
1
+ 4e3513e973cd69a3ce1b30d0756ba26660ae1ba6
@@ -1 +1 @@
1
- 0fd84cc6744732697aa0a9769b6febb6b432d765b4223b48c34a6b3bfd240483
1
+ 319540a82e8d028d9fe417155b17c95ae4d62f0cf9f18a751a1827a27acf1802
@@ -1 +1 @@
1
- 452aea17d2dbacd7d844c8f9d370c9931ae1196ac85020666f60ed7d8ddfc561d783abcec6f3c3b6b3a677be57cdf0a8966fc8254876e079e4c2d5dd0a229288
1
+ 92f9a95efb7117c43c5df7765e56d81d8ef34b2413ef04e6133d7a3d51ccb88bec0aa80bba83d302ebe4dee638270ec57e03b0a3fc972e8552fd94365065fd3a
package/Detox-ios-src.tbz CHANGED
Binary file
package/Detox-ios.tbz CHANGED
Binary file
@@ -0,0 +1,28 @@
1
+ package com.wix.detox.common
2
+
3
+ import android.view.View
4
+ import android.view.ViewGroup
5
+
6
+ fun View.forEachChild(callback: (child: View) -> Unit) {
7
+ if (this is ViewGroup) {
8
+ for (index in 0 until childCount) {
9
+ val child = getChildAt(index)
10
+ callback(child)
11
+ }
12
+ }
13
+ }
14
+
15
+ /**
16
+ * In-order traverse the view-hierarchy specified by a view, considered to be the hierarchy's root.
17
+ *
18
+ * @param view The hierarchy's root-view.
19
+ * @param callback A function to call per each view. Returning `false` from the callback indicates
20
+ * a request to refrain from traversing the sub-hierarchy associated with the current view.
21
+ */
22
+ fun traverseViewHierarchy(view: View, callback: (view: View) -> Boolean) {
23
+ if (callback(view)) {
24
+ view.forEachChild { child ->
25
+ traverseViewHierarchy(child, callback)
26
+ }
27
+ }
28
+ }
@@ -12,15 +12,17 @@ import static androidx.test.espresso.matcher.ViewMatchers.isChecked;
12
12
  import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
13
13
  import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
14
14
  import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
15
+ import static androidx.test.espresso.matcher.ViewMatchers.isFocused;
15
16
  import static androidx.test.espresso.matcher.ViewMatchers.isNotChecked;
16
17
  import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
17
18
  import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
18
19
  import static androidx.test.espresso.matcher.ViewMatchers.withTagValue;
19
20
  import static androidx.test.espresso.matcher.ViewMatchers.withText;
20
- import static androidx.test.espresso.matcher.ViewMatchers.isFocused;
21
21
  import static com.wix.detox.espresso.matcher.ViewMatchers.isMatchingAtIndex;
22
22
  import static com.wix.detox.espresso.matcher.ViewMatchers.isOfClassName;
23
23
  import static com.wix.detox.espresso.matcher.ViewMatchers.toHaveSliderPosition;
24
+ import static com.wix.detox.espresso.matcher.ViewMatchers.withAccessibilityLabel;
25
+ import static com.wix.detox.espresso.matcher.ViewMatchers.withShallowAccessibilityLabel;
24
26
  import static org.hamcrest.Matchers.allOf;
25
27
  import static org.hamcrest.Matchers.anyOf;
26
28
  import static org.hamcrest.Matchers.is;
@@ -43,6 +45,14 @@ public class DetoxMatcher {
43
45
  return allOf(withText(text), withEffectiveVisibility(Visibility.VISIBLE));
44
46
  }
45
47
 
48
+ public static Matcher<View> matcherForAccessibilityLabel(String label) {
49
+ return allOf(withAccessibilityLabel(label), withEffectiveVisibility(Visibility.VISIBLE));
50
+ }
51
+
52
+ public static Matcher<View> matcherForShallowAccessibilityLabel(String label) {
53
+ return allOf(withShallowAccessibilityLabel(label), withEffectiveVisibility(Visibility.VISIBLE));
54
+ }
55
+
46
56
  public static Matcher<View> matcherForContentDescription(String contentDescription) {
47
57
  return allOf(withContentDescription(contentDescription), withEffectiveVisibility(Visibility.VISIBLE));
48
58
  }
@@ -10,6 +10,7 @@ import androidx.test.espresso.UiController
10
10
  import com.google.android.material.slider.Slider
11
11
  import com.wix.detox.espresso.ViewActionWithResult
12
12
  import com.wix.detox.espresso.common.SliderHelper
13
+ import com.wix.detox.reactnative.ui.getAccessibilityLabel
13
14
  import org.hamcrest.Matcher
14
15
  import org.hamcrest.Matchers
15
16
  import org.hamcrest.Matchers.allOf
@@ -47,7 +48,7 @@ private class CommonAttributes {
47
48
  fun get(json: JSONObject, view: View) {
48
49
  getId(json, view)
49
50
  getVisibility(json, view)
50
- getContentDescription(json, view)
51
+ getAccessibilityLabel(json, view)
51
52
  getAlpha(json, view)
52
53
  getElevation(json, view)
53
54
  getHeight(json, view)
@@ -66,8 +67,8 @@ private class CommonAttributes {
66
67
  json.put("visible", view.getLocalVisibleRect(Rect()))
67
68
  }
68
69
 
69
- private fun getContentDescription(json: JSONObject, view: View) =
70
- view.contentDescription?.let {
70
+ private fun getAccessibilityLabel(json: JSONObject, view: View) =
71
+ view.getAccessibilityLabel()?.let {
71
72
  json.put("label", it)
72
73
  }
73
74
 
@@ -8,17 +8,20 @@ import androidx.test.espresso.matcher.BoundedMatcher
8
8
  import androidx.test.espresso.matcher.ViewMatchers
9
9
  import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
10
10
  import com.wix.detox.espresso.common.SliderHelper
11
- import org.hamcrest.BaseMatcher
12
- import org.hamcrest.Description
13
- import org.hamcrest.Matcher
14
- import org.hamcrest.Matchers.allOf
15
- import org.hamcrest.TypeSafeMatcher
11
+ import org.hamcrest.*
12
+ import org.hamcrest.Matchers.*
16
13
  import kotlin.math.abs
17
14
 
18
15
  /*
19
16
  * An extension of [androidx.test.espresso.matcher.ViewMatchers].
20
17
  */
21
18
 
19
+ fun withAccessibilityLabel(text: String) =
20
+ WithAccessibilityLabelMatcher(`is`(text))
21
+
22
+ fun withShallowAccessibilityLabel(label: String): Matcher<View>
23
+ = anyOf(ViewMatchers.withContentDescription(label), ViewMatchers.withText(label))
24
+
22
25
  fun isOfClassName(className: String): Matcher<View> {
23
26
  try {
24
27
  val cls = Class.forName(className)
@@ -0,0 +1,23 @@
1
+ package com.wix.detox.espresso.matcher
2
+
3
+ import android.view.View
4
+ import com.wix.detox.reactnative.ui.getAccessibilityLabel
5
+ import org.hamcrest.Description
6
+ import org.hamcrest.Matcher
7
+ import org.hamcrest.TypeSafeDiagnosingMatcher
8
+
9
+ class WithAccessibilityLabelMatcher(private val textMatcher: Matcher<String>): TypeSafeDiagnosingMatcher<View>() {
10
+ override fun matchesSafely(view: View, mismatchDescription: Description): Boolean =
11
+ view.getAccessibilityLabel().let { contentDescription ->
12
+ return textMatcher.matches(contentDescription).also { matched ->
13
+ if (!matched) {
14
+ mismatchDescription.appendText("view.getAccessibilityLabel() ")
15
+ textMatcher.describeMismatch(contentDescription, mismatchDescription)
16
+ }
17
+ }
18
+ }
19
+
20
+ override fun describeTo(description: Description) {
21
+ description.appendText("view.getAccessibilityLabel() ").appendDescriptionOf(textMatcher)
22
+ }
23
+ }
@@ -0,0 +1,37 @@
1
+ package com.wix.detox.reactnative.ui
2
+
3
+ import android.view.View
4
+ import android.widget.TextView
5
+ import com.wix.detox.common.traverseViewHierarchy
6
+ import com.wix.detox.reactnative.utils.isReactNativeObject
7
+
8
+ fun View.getAccessibilityLabel(
9
+ isReactNativeObjectFn: (Any) -> Boolean = { isReactNativeObject(it) }
10
+ ): CharSequence? =
11
+ if (isReactNativeObjectFn(this)) {
12
+ val subLabels = collectAccessibilityLabelsFromHierarchy(this)
13
+ if (subLabels.isEmpty()) null else subLabels.joinToString(" ")
14
+ } else {
15
+ getRawAccessibilityLabel(this)
16
+ }
17
+
18
+ private fun collectAccessibilityLabelsFromHierarchy(
19
+ rootView: View,
20
+ subLabels: MutableList<CharSequence> = mutableListOf(),
21
+ ): List<CharSequence> {
22
+ traverseViewHierarchy(rootView) { view ->
23
+ getRawAccessibilityLabel(view)?.let { rawLabel ->
24
+ subLabels.add(rawLabel)
25
+ false
26
+ } ?: true
27
+
28
+ }
29
+ return subLabels
30
+ }
31
+
32
+ private fun getRawAccessibilityLabel(view: View): CharSequence? =
33
+ if (view.contentDescription != null) {
34
+ view.contentDescription
35
+ } else if (view is TextView) {
36
+ view.text
37
+ } else null
@@ -0,0 +1,6 @@
1
+ package com.wix.detox.reactnative.utils
2
+
3
+ private const val REACT_NATIVE_PACKAGE = "com.facebook.react"
4
+
5
+ fun isReactNativeObject(obj: Any): Boolean =
6
+ obj.javaClass.canonicalName?.startsWith(REACT_NATIVE_PACKAGE) == true
@@ -1,8 +1,20 @@
1
1
  package com.wix.detox
2
2
 
3
+ import android.view.View
4
+ import android.view.ViewGroup
5
+ import org.mockito.ArgumentMatchers.eq
6
+ import org.mockito.kotlin.whenever
3
7
  import java.util.concurrent.ExecutorService
4
8
  import java.util.concurrent.TimeUnit
5
9
 
6
10
  object UTHelpers {
7
11
  fun yieldToOtherThreads(executor: ExecutorService) = executor.awaitTermination(100L, TimeUnit.MILLISECONDS)
12
+
13
+ fun mockViewHierarchy(parent: ViewGroup, vararg children: View) {
14
+ whenever(parent.childCount).thenReturn(children.size)
15
+
16
+ children.forEachIndexed { index, view ->
17
+ whenever(parent.getChildAt(eq(index))).thenReturn(view)
18
+ }
19
+ }
8
20
  }
@@ -0,0 +1,107 @@
1
+ package com.wix.detox.common
2
+
3
+ import android.view.View
4
+ import android.view.ViewGroup
5
+ import android.widget.TextView
6
+ import com.wix.detox.UTHelpers.mockViewHierarchy
7
+ import com.wix.detox.reactnative.ui.getAccessibilityLabel
8
+ import org.assertj.core.api.Assertions
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import org.mockito.kotlin.doReturn
12
+ import org.mockito.kotlin.mock
13
+ import org.mockito.kotlin.whenever
14
+ import org.robolectric.RobolectricTestRunner
15
+
16
+ @RunWith(RobolectricTestRunner::class)
17
+ class UIExtensionsTest {
18
+ private val alwaysReactNativeObjFn: (Any) -> Boolean = { true }
19
+ private val neverReactNativeObjFn: (Any) -> Boolean = { false }
20
+
21
+ private fun withContentDescription(value: String, v: View) { whenever(v.contentDescription).doReturn(value) }
22
+ private fun withText(value: String, v: TextView) { whenever(v.text).doReturn(value) }
23
+
24
+ @Test
25
+ fun `should return accessibility label according to content-description`() {
26
+ val view: View = mock()
27
+
28
+ val contentDescription = "content-description-mock"
29
+ withContentDescription(contentDescription, view)
30
+
31
+ val label = view.getAccessibilityLabel()
32
+ Assertions.assertThat(label).isEqualTo(contentDescription)
33
+ }
34
+
35
+ @Test
36
+ fun `should return accessibility label according to children's content-description, recursively`() {
37
+ val contentDescription1st = "cd.1"
38
+ val contentDescription2nd = "cd.2"
39
+ val expectedLabel = "$contentDescription1st $contentDescription2nd"
40
+
41
+
42
+ val parent: ViewGroup = mock()
43
+ val sibling1: ViewGroup = mock()
44
+ val sibling2: ViewGroup = mock<ViewGroup>().also {
45
+ withContentDescription(contentDescription2nd, it)
46
+ }
47
+ val grandchild: View = mock<View>().also {
48
+ withContentDescription(contentDescription1st, it)
49
+ }
50
+
51
+ mockViewHierarchy(parent, sibling1, sibling2)
52
+ mockViewHierarchy(sibling1, grandchild)
53
+
54
+ val label = parent.getAccessibilityLabel(alwaysReactNativeObjFn)
55
+ Assertions.assertThat(label).isEqualTo(expectedLabel)
56
+ }
57
+
58
+ @Test
59
+ fun `should return accessibility label according to children's text, on top of label`() {
60
+ val text = "some mocked text"
61
+
62
+ val parent: ViewGroup = mock()
63
+ val grandchild: TextView = mock<TextView>().also {
64
+ withText(text, it)
65
+ }
66
+ mockViewHierarchy(parent, grandchild)
67
+
68
+ val label = parent.getAccessibilityLabel(alwaysReactNativeObjFn)
69
+ Assertions.assertThat(label).isEqualTo(text)
70
+ }
71
+
72
+ @Test
73
+ fun `should not return accessibility label if content description not set in view nor its descendants`() {
74
+ val parent: ViewGroup = mock()
75
+ val child: View = mock()
76
+
77
+ mockViewHierarchy(parent, child)
78
+
79
+ val label = parent.getAccessibilityLabel(alwaysReactNativeObjFn)
80
+ Assertions.assertThat(label).isNull()
81
+ }
82
+
83
+ @Test
84
+ fun `should not return accessibility label based on children for non-RN views`() {
85
+ val childContentDescription = "content-description-mock"
86
+
87
+ val parent: ViewGroup = mock()
88
+ val child: View = mock<View>().also {
89
+ withContentDescription(childContentDescription, it)
90
+ }
91
+ mockViewHierarchy(parent, child)
92
+
93
+ val label = parent.getAccessibilityLabel(neverReactNativeObjFn)
94
+ Assertions.assertThat(label).isNull()
95
+ }
96
+
97
+ @Test
98
+ fun `should return accessibility label for non-RN views`() {
99
+ val view: View = mock()
100
+
101
+ val contentDescription = "content-description-mock"
102
+ withContentDescription(contentDescription, view)
103
+
104
+ val label = view.getAccessibilityLabel(neverReactNativeObjFn)
105
+ Assertions.assertThat(label).isEqualTo(contentDescription)
106
+ }
107
+ }
@@ -6,6 +6,7 @@ import android.widget.ProgressBar
6
6
  import android.widget.TextView
7
7
  import com.facebook.react.views.slider.ReactSlider
8
8
  import com.google.android.material.slider.Slider
9
+ import com.wix.detox.reactnative.ui.getAccessibilityLabel
9
10
  import org.assertj.core.api.Assertions.assertThat
10
11
  import org.json.JSONObject
11
12
  import org.junit.Before
@@ -32,7 +33,7 @@ class GetAttributesActionTest {
32
33
  private fun givenNoViewTag() = givenViewTag(null)
33
34
  private fun givenVisibility(value: Int) { whenever(view.visibility).doReturn(value) }
34
35
  private fun givenVisibilityRectAvailability(value: Boolean) { whenever(view.getLocalVisibleRect(any())).doReturn(value) }
35
- private fun givenContentDescription(value: String) { whenever(view.contentDescription).doReturn(value) }
36
+ private fun givenAccessibilityLabel(value: String) { whenever(view.getAccessibilityLabel()).doReturn(value) }
36
37
 
37
38
  private fun perform(v: View = view): JSONObject {
38
39
  uut.perform(null, v)
@@ -110,16 +111,16 @@ class GetAttributesActionTest {
110
111
  }
111
112
 
112
113
  @Test
113
- fun `should return label according to content-description`() {
114
- val contentDescription = "content-description-mock"
115
- givenContentDescription(contentDescription)
114
+ fun `should return label according to accessibilityLabel extension`() {
115
+ val accessibilityLabel = "label-mock"
116
+ givenAccessibilityLabel(accessibilityLabel)
116
117
 
117
118
  val resultJson = perform()
118
- assertThat(resultJson.opt("label")).isEqualTo(contentDescription)
119
+ assertThat(resultJson.opt("label")).isEqualTo(accessibilityLabel)
119
120
  }
120
121
 
121
122
  @Test
122
- fun `should not return label if content description no set`() {
123
+ fun `should not return label if accessibility label is not available`() {
123
124
  val resultJson = perform()
124
125
  assertThat(resultJson.opt("label")).isNull()
125
126
  }
@@ -3,7 +3,6 @@ package com.wix.detox.espresso.matcher
3
3
  import android.view.View
4
4
  import org.hamcrest.Description
5
5
  import org.hamcrest.Matcher
6
- import org.hamcrest.Matchers
7
6
  import org.mockito.kotlin.mock
8
7
  import org.mockito.kotlin.verify
9
8
  import org.mockito.kotlin.whenever
@@ -28,7 +27,7 @@ object ViewAtIndexMatcherSpec: Spek({
28
27
  uut.describeTo(description)
29
28
  verify(description).appendText("View at index #0, of those matching MATCHER(innerMatcher description)")
30
29
  }
31
-
30
+
32
31
  it("should append a valid description for index≥0") {
33
32
  val uut = ViewAtIndexMatcher(7, innerMatcher)
34
33
  uut.describeTo(description)
package/index.d.ts CHANGED
@@ -331,6 +331,7 @@ declare global {
331
331
  binaryPath: string;
332
332
  bundleId?: string;
333
333
  build?: string;
334
+ start?: string;
334
335
  launchArgs?: Record<string, any>;
335
336
  }
336
337
 
@@ -339,6 +340,7 @@ declare global {
339
340
  binaryPath: string;
340
341
  bundleId?: string;
341
342
  build?: string;
343
+ start?: string;
342
344
  testBinaryPath?: string;
343
345
  launchArgs?: Record<string, any>;
344
346
  /**
@@ -624,9 +626,8 @@ declare global {
624
626
 
625
627
  interface Device {
626
628
  /**
627
- * Holds the environment-unique ID of the device - namely, the adb ID on Android (e.g. emulator-5554) and the Mac-global simulator UDID on iOS,
629
+ * Holds the environment-unique ID of the device, namely, the adb ID on Android (e.g. emulator-5554) and the Mac-global simulator UDID on iOS -
628
630
  * as used by simctl (e.g. AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE).
629
- *
630
631
  */
631
632
  id: string;
632
633
  /**
@@ -1120,39 +1121,39 @@ declare global {
1120
1121
  * expectation with a `not` expects the view's visible area to be smaller than N%.
1121
1122
  * @param pct optional integer ranging from 1 to 100, indicating how much percent of the view should be
1122
1123
  * visible to the user to be accepted.
1123
- * @example await expect(element(by.id('UniqueId204'))).toBeVisible(35);
1124
+ * @example await expect(element(by.id('mainTitle'))).toBeVisible(35);
1124
1125
  */
1125
1126
  toBeVisible(pct?: number): R;
1126
1127
 
1127
1128
  /**
1128
1129
  * Negate the expectation.
1129
- * @example await expect(element(by.id('UniqueId205'))).not.toBeVisible();
1130
+ * @example await expect(element(by.id('cancelButton'))).not.toBeVisible();
1130
1131
  */
1131
1132
  not: this;
1132
1133
 
1133
1134
  /**
1134
1135
  * Expect the view to not be visible.
1135
- * @example await expect(element(by.id('UniqueId205'))).toBeNotVisible();
1136
+ * @example await expect(element(by.id('cancelButton'))).toBeNotVisible();
1136
1137
  * @deprecated Use `.not.toBeVisible()` instead.
1137
1138
  */
1138
1139
  toBeNotVisible(): R;
1139
1140
 
1140
1141
  /**
1141
1142
  * Expect the view to exist in the UI hierarchy.
1142
- * @example await expect(element(by.id('UniqueId205'))).toExist();
1143
+ * @example await expect(element(by.id('okButton'))).toExist();
1143
1144
  */
1144
1145
  toExist(): R;
1145
1146
 
1146
1147
  /**
1147
1148
  * Expect the view to not exist in the UI hierarchy.
1148
- * @example await expect(element(by.id('RandomJunk959'))).toNotExist();
1149
+ * @example await expect(element(by.id('cancelButton'))).toNotExist();
1149
1150
  * @deprecated Use `.not.toExist()` instead.
1150
1151
  */
1151
1152
  toNotExist(): R;
1152
1153
 
1153
1154
  /**
1154
1155
  * Expect the view to be focused.
1155
- * @example await expect(element(by.id('loginInput'))).toBeFocused();
1156
+ * @example await expect(element(by.id('emailInput'))).toBeFocused();
1156
1157
  */
1157
1158
  toBeFocused(): R;
1158
1159
 
@@ -1166,21 +1167,23 @@ declare global {
1166
1167
  /**
1167
1168
  * In React Native apps, expect UI component of type <Text> to have text.
1168
1169
  * In native iOS apps, expect UI elements of type UIButton, UILabel, UITextField or UITextViewIn to have inputText with text.
1169
- * @example await expect(element(by.id('UniqueId204'))).toHaveText('I contain some text');
1170
+ * @example await expect(element(by.id('mainTitle'))).toHaveText('Welcome back!);
1170
1171
  */
1171
1172
  toHaveText(text: string): R;
1172
1173
 
1173
1174
  /**
1174
- * It searches by accessibilityLabel on iOS, or by contentDescription on Android.
1175
- * In React Native it can be set for both platforms by defining an accessibilityLabel on the view.
1176
- * @example await expect(element(by.id('UniqueId204'))).toHaveLabel('Done');
1175
+ * Expects a specific accessibilityLabel, as specified via the `accessibilityLabel` prop in React Native.
1176
+ * On the native side (in both React Native and pure-native apps), that is equivalent to `accessibilityLabel`
1177
+ * on iOS and contentDescription on Android. Refer to Detox's documentation in order to learn about caveats
1178
+ * with accessibility-labels in React Native apps.
1179
+ * @example await expect(element(by.id('submitButton'))).toHaveLabel('Submit');
1177
1180
  */
1178
1181
  toHaveLabel(label: string): R;
1179
1182
 
1180
1183
  /**
1181
1184
  * In React Native apps, expect UI component to have testID with that id.
1182
1185
  * In native iOS apps, expect UI element to have accessibilityIdentifier with that id.
1183
- * @example await expect(element(by.text('I contain some text'))).toHaveId('UniqueId204');
1186
+ * @example await expect(element(by.text('Submit'))).toHaveId('submitButton');
1184
1187
  */
1185
1188
  toHaveId(id: string): R;
1186
1189
 
@@ -1193,7 +1196,7 @@ declare global {
1193
1196
 
1194
1197
  /**
1195
1198
  * Expect components like a Switch to have a value ('0' for off, '1' for on).
1196
- * @example await expect(element(by.id('UniqueId533'))).toHaveValue('0');
1199
+ * @example await expect(element(by.id('temperatureDial'))).toHaveValue('25');
1197
1200
  */
1198
1201
  toHaveValue(value: any): R;
1199
1202
 
@@ -1210,7 +1213,7 @@ declare global {
1210
1213
  /**
1211
1214
  * This API polls using the given expectation continuously until the expectation is met. Use manual synchronization with waitFor only as a last resort.
1212
1215
  * NOTE: Every waitFor call must set a timeout using withTimeout(). Calling waitFor without setting a timeout will do nothing.
1213
- * @example await waitFor(element(by.id('UniqueId336'))).toExist().withTimeout(2000);
1216
+ * @example await waitFor(element(by.id('bigButton'))).toExist().withTimeout(2000);
1214
1217
  */
1215
1218
  (element: NativeElement): Expect<WaitFor>;
1216
1219
  }
@@ -1218,13 +1221,13 @@ declare global {
1218
1221
  interface WaitFor {
1219
1222
  /**
1220
1223
  * Waits for the condition to be met until the specified time (millis) have elapsed.
1221
- * @example await waitFor(element(by.id('UniqueId336'))).toExist().withTimeout(2000);
1224
+ * @example await waitFor(element(by.id('bigButton'))).toExist().withTimeout(2000);
1222
1225
  */
1223
1226
  withTimeout(millis: number): Promise<void>;
1224
1227
 
1225
1228
  /**
1226
1229
  * Performs the action repeatedly on the element until an expectation is met
1227
- * @example await waitFor(element(by.text('Text5'))).toBeVisible().whileElement(by.id('ScrollView630')).scroll(50, 'down');
1230
+ * @example await waitFor(element(by.text('Item #5'))).toBeVisible().whileElement(by.id('itemsList')).scroll(50, 'down');
1228
1231
  */
1229
1232
  whileElement(by: NativeMatcher): NativeElement & WaitFor;
1230
1233
 
@@ -1436,7 +1439,7 @@ declare global {
1436
1439
  interface WebExpect<R = Promise<void>> {
1437
1440
  /**
1438
1441
  * Negate the expectation.
1439
- * @example await expect(web.element(by.web.id('UniqueId205'))).not.toExist();
1442
+ * @example await expect(web.element(by.web.id('sessionTimeout'))).not.toExist();
1440
1443
  */
1441
1444
  not: this;
1442
1445
 
@@ -1444,13 +1447,13 @@ declare global {
1444
1447
  * Expect the element content to have the `text` supplied
1445
1448
  * @param text expected to be on the element content
1446
1449
  * @example
1447
- * await expect(web.element(by.web.id('UniqueId205'))).toHaveText('ExactText');
1450
+ * await expect(web.element(by.web.id('checkoutButton'))).toHaveText('Proceed to check out');
1448
1451
  */
1449
1452
  toHaveText(text: string): R;
1450
1453
 
1451
1454
  /**
1452
1455
  * Expect the view to exist in the webview DOM tree.
1453
- * @example await expect(web.element(by.web.id('UniqueId205'))).toExist();
1456
+ * @example await expect(web.element(by.web.id('submitButton'))).toExist();
1454
1457
  */
1455
1458
  toExist(): R;
1456
1459
  }
@@ -1649,7 +1652,9 @@ declare global {
1649
1652
  */
1650
1653
  text?: string;
1651
1654
  /**
1652
- * The label of the element. Matches accessibilityLabel for ios, and contentDescription for android.
1655
+ * The label of the element. Largely matches accessibilityLabel for ios, and contentDescription for android.
1656
+ * Refer to Detox's documentation (`toHaveLabel()` subsection) in order to learn about caveats associated with
1657
+ * this property in React Native apps.
1653
1658
  */
1654
1659
  label?: string;
1655
1660
  /**
package/local-cli/cli.js CHANGED
@@ -12,7 +12,7 @@ const { isErrorAlreadyLogged } = require('./utils/cliErrorHandling');
12
12
  yargs
13
13
  .scriptName('detox')
14
14
  .parserConfiguration({
15
- 'boolean-negation': false,
15
+ 'boolean-negation': true,
16
16
  'camel-case-expansion': false,
17
17
  'dot-notation': false,
18
18
  'duplicate-arguments-array': false,
package/local-cli/init.js CHANGED
@@ -93,13 +93,13 @@ function createDefaultConfigurations() {
93
93
  'android.debug': {
94
94
  type: 'android.apk',
95
95
  binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
96
- build: 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -',
96
+ build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
97
97
  reversePorts: [8081],
98
98
  },
99
99
  'android.release': {
100
100
  type: 'android.apk',
101
101
  binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
102
- build: 'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -',
102
+ build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
103
103
  },
104
104
  },
105
105
  devices: {
@@ -0,0 +1,49 @@
1
+ const _ = require('lodash');
2
+
3
+ const detox = require('../internals');
4
+
5
+ const AppStartCommand = require('./startCommand/AppStartCommand');
6
+
7
+ module.exports.command = 'start';
8
+ module.exports.desc = 'Run app "start" scripts inside the selected configuration';
9
+ module.exports.builder = {
10
+ C: {
11
+ alias: 'config-path',
12
+ describe: 'Specify Detox config file path. If not supplied, Detox searches for .detoxrc[.js] or "detox" section in package.json',
13
+ },
14
+ c: {
15
+ alias: ['configuration'],
16
+ describe:
17
+ 'Select a local configuration from your defined configurations to extract the app "start" scripts from. If not supplied, and there\'s only one configuration, Detox will default to it',
18
+ },
19
+ f: {
20
+ alias: 'force',
21
+ describe: 'Ignore errors from the "start" scripts and proceed',
22
+ boolean: true,
23
+ }
24
+ };
25
+
26
+ module.exports.handler = async function start(argv) {
27
+ const { apps: appsConfig } = await detox.resolveConfig({ argv });
28
+ const startCommands = _(appsConfig)
29
+ .values()
30
+ .map(app => app.start)
31
+ .filter(Boolean)
32
+ .map(cmd => new AppStartCommand({
33
+ cmd,
34
+ passthrough: argv['--'],
35
+ forceSpawn: argv.force,
36
+ }))
37
+ .value();
38
+
39
+ if (startCommands.length) {
40
+ try {
41
+ await Promise.all(startCommands.map(c => c.execute()));
42
+ } catch (e) {
43
+ await Promise.allSettled(startCommands.map(c => c.stop()));
44
+ throw e;
45
+ }
46
+ } else {
47
+ detox.log.warn('No "start" commands were found in the app configs.');
48
+ }
49
+ };
@@ -0,0 +1,65 @@
1
+ const execa = require('execa');
2
+
3
+ const detox = require('../../internals');
4
+ const { DetoxRuntimeError } = require('../../src/errors');
5
+ const Deferred = require('../../src/utils/Deferred');
6
+ const log = detox.log.child({ cat: ['lifecycle', 'cli'] });
7
+
8
+ class AppStartCommand {
9
+ constructor({ cmd, passthrough = [], forceSpawn = false }) {
10
+ this._id = Math.random();
11
+ this._cmd = cmd;
12
+ this._passthrough = passthrough;
13
+ this._forceSpawn = forceSpawn;
14
+
15
+ this._cpHandle = null;
16
+ this._cpDeferred = new Deferred();
17
+ }
18
+
19
+ execute() {
20
+ const cmd = [this._cmd, ...this._passthrough].join(' ');
21
+
22
+ log.info.begin({ id: this._id }, cmd);
23
+
24
+ const onEnd = (msg, code, signal) => {
25
+ log.trace.end({ id: this._id, code, signal }, msg);
26
+ this._cpDeferred.resolve();
27
+ };
28
+
29
+ const onError = (msg, code, signal) => {
30
+ const logLevel = this._forceSpawn ? 'warn' : 'error';
31
+ log[logLevel].end({ id: this._id, code, signal }, msg);
32
+ if (this._forceSpawn) {
33
+ this._cpDeferred.resolve();
34
+ } else {
35
+ this._cpDeferred.reject(new DetoxRuntimeError(msg));
36
+ }
37
+ };
38
+
39
+ this._cpHandle = execa.command(cmd, { stdio: 'inherit', shell: true });
40
+ this._cpHandle.on('error', onError);
41
+ this._cpHandle.on('exit', (code, signal) => {
42
+ const reason = code == null ? `signal ${signal}` : `code ${code}`;
43
+ const msg = `Command exited with ${reason}: ${cmd}`;
44
+ if (signal || code === 0) {
45
+ onEnd(msg, code, signal);
46
+ } else {
47
+ onError(msg, code, signal);
48
+ }
49
+
50
+ this._cpHandle = null;
51
+ });
52
+
53
+ return this._cpDeferred.promise;
54
+ }
55
+
56
+ async stop() {
57
+ if (this._cpHandle) {
58
+ this._cpHandle.kill();
59
+ }
60
+
61
+ return this._cpDeferred.promise;
62
+ }
63
+ }
64
+
65
+ module.exports = AppStartCommand;
@@ -9,6 +9,8 @@ const log = detox.log.child({ cat: ['lifecycle', 'cli'] });
9
9
  const { printEnvironmentVariables, prependNodeModulesBinToPATH } = require('../../src/utils/envUtils');
10
10
  const { toSimplePath } = require('../../src/utils/pathUtils');
11
11
  const { escapeSpaces, useForwardSlashes } = require('../../src/utils/shellUtils');
12
+ const sleep = require('../../src/utils/sleep');
13
+ const AppStartCommand = require('../startCommand/AppStartCommand');
12
14
  const { markErrorAsLogged } = require('../utils/cliErrorHandling');
13
15
 
14
16
  const TestRunnerError = require('./TestRunnerError');
@@ -23,11 +25,14 @@ class TestRunnerCommand {
23
25
  const cliConfig = opts.config.cli;
24
26
  const deviceConfig = opts.config.device;
25
27
  const runnerConfig = opts.config.testRunner;
28
+ const appsConfig = opts.config.apps;
26
29
 
27
30
  this._argv = runnerConfig.args;
28
31
  this._retries = runnerConfig.retries;
29
32
  this._envHint = this._buildEnvHint(opts.env);
33
+ this._startCommands = this._prepareStartCommands(appsConfig, cliConfig);
30
34
  this._envFwd = {};
35
+
31
36
  if (runnerConfig.forwardEnv) {
32
37
  this._envFwd = this._buildEnvOverride(cliConfig, deviceConfig);
33
38
  Object.assign(this._envHint, this._envFwd);
@@ -38,6 +43,15 @@ class TestRunnerCommand {
38
43
  let runsLeft = 1 + this._retries;
39
44
  let launchError = null;
40
45
 
46
+ if (this._startCommands.length > 0) {
47
+ try {
48
+ await Promise.race([sleep(1000), ...this._startCommands.map(cmd => cmd.execute())]);
49
+ } catch (e) {
50
+ await Promise.allSettled(this._startCommands.map(cmd => cmd.stop()));
51
+ throw e;
52
+ }
53
+ }
54
+
41
55
  do {
42
56
  try {
43
57
  await this._spawnTestRunner();
@@ -67,6 +81,8 @@ class TestRunnerCommand {
67
81
  }
68
82
  } while (launchError && runsLeft > 0);
69
83
 
84
+ await Promise.allSettled(this._startCommands.map(cmd => cmd.stop()));
85
+
70
86
  if (launchError) {
71
87
  throw launchError;
72
88
  }
@@ -80,6 +96,19 @@ class TestRunnerCommand {
80
96
  .value();
81
97
  }
82
98
 
99
+ _prepareStartCommands(appsConfig, cliConfig) {
100
+ if (`${cliConfig.start}` === 'false') {
101
+ return [];
102
+ }
103
+
104
+ return _.values(appsConfig)
105
+ .filter(app => app.start)
106
+ .map(app => new AppStartCommand({
107
+ cmd: app.start,
108
+ forceSpawn: cliConfig.start === 'force',
109
+ }));
110
+ }
111
+
83
112
  /**
84
113
  * @param {DetoxInternals.CLIConfig} cliConfig
85
114
  * @param {Detox.DetoxDeviceConfig} deviceConfig
@@ -28,6 +28,11 @@ module.exports = {
28
28
  describe: 'Reuse existing installed app (do not delete + reinstall) for a faster run.',
29
29
  boolean: true,
30
30
  },
31
+ start: {
32
+ group: 'Execution:',
33
+ describe: 'Run app "start" scripts before running the tests. Use --no-start to disable that, and --start=force to ignore errors.',
34
+ default: true,
35
+ },
31
36
  u: {
32
37
  alias: 'cleanup',
33
38
  group: 'Execution:',
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.4.0",
4
+ "version": "20.6.0",
5
5
  "bin": {
6
6
  "detox": "local-cli/cli.js"
7
7
  },
@@ -63,6 +63,7 @@
63
63
  "caf": "^15.0.1",
64
64
  "chalk": "^2.4.2",
65
65
  "child-process-promise": "^2.2.0",
66
+ "execa": "^5.1.1",
66
67
  "find-up": "^4.1.0",
67
68
  "fs-extra": "^4.0.2",
68
69
  "funpermaproxy": "^1.1.0",
@@ -199,5 +200,5 @@
199
200
  }
200
201
  }
201
202
  },
202
- "gitHead": "9329070b1dca0702015f44b3e75dd11f70651a5b"
203
+ "gitHead": "03e23e76b54f1e7de70495e0ec8a8db45872b0e6"
203
204
  }
@@ -1,27 +1,44 @@
1
+ const { inspect } = require('util');
2
+
3
+ const { DetoxRuntimeError } = require('../../errors');
1
4
  const invoke = require('../../invoke');
2
5
  const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
3
6
 
4
7
  class NativeMatcher {
8
+ static _assertMatcher(matcher) {
9
+ if (!(matcher instanceof NativeMatcher)) {
10
+ throw new DetoxRuntimeError({ message: `Expected a matcher, got: ${inspect(matcher)}` });
11
+ }
12
+ }
13
+
5
14
  constructor(call) {
6
15
  this._call = call || null;
7
16
  }
8
17
 
9
18
  withAncestor(matcher) {
19
+ NativeMatcher._assertMatcher(matcher);
20
+
10
21
  const call = invoke.callDirectly(DetoxMatcherApi.matcherWithAncestor(this, matcher));
11
22
  return new NativeMatcher(call);
12
23
  }
13
24
 
14
25
  withDescendant(matcher) {
26
+ NativeMatcher._assertMatcher(matcher);
27
+
15
28
  const call = invoke.callDirectly(DetoxMatcherApi.matcherWithDescendant(this, matcher));
16
29
  return new NativeMatcher(call);
17
30
  }
18
31
 
19
32
  and(matcher) {
33
+ NativeMatcher._assertMatcher(matcher);
34
+
20
35
  const call = invoke.callDirectly(DetoxMatcherApi.matcherForAnd(this, matcher));
21
36
  return new NativeMatcher(call);
22
37
  }
23
38
 
24
39
  or(matcher) {
40
+ NativeMatcher._assertMatcher(matcher);
41
+
25
42
  const call = invoke.callDirectly(DetoxMatcherApi.matcherForOr(this, matcher));
26
43
  return new NativeMatcher(call);
27
44
  }
@@ -26,6 +26,30 @@ class DetoxMatcher {
26
26
  };
27
27
  }
28
28
 
29
+ static matcherForAccessibilityLabel(label) {
30
+ if (typeof label !== "string") throw new Error("label should be a string, but got " + (label + (" (" + (typeof label + ")"))));
31
+ return {
32
+ target: {
33
+ type: "Class",
34
+ value: "com.wix.detox.espresso.DetoxMatcher"
35
+ },
36
+ method: "matcherForAccessibilityLabel",
37
+ args: [label]
38
+ };
39
+ }
40
+
41
+ static matcherForShallowAccessibilityLabel(label) {
42
+ if (typeof label !== "string") throw new Error("label should be a string, but got " + (label + (" (" + (typeof label + ")"))));
43
+ return {
44
+ target: {
45
+ type: "Class",
46
+ value: "com.wix.detox.espresso.DetoxMatcher"
47
+ },
48
+ method: "matcherForShallowAccessibilityLabel",
49
+ args: [label]
50
+ };
51
+ }
52
+
29
53
  static matcherForContentDescription(contentDescription) {
30
54
  if (typeof contentDescription !== "string") throw new Error("contentDescription should be a string, but got " + (contentDescription + (" (" + (typeof contentDescription + ")"))));
31
55
  return {
@@ -2,9 +2,9 @@ const native = require('./native');
2
2
  const web = require('./web');
3
3
 
4
4
  module.exports = {
5
- accessibilityLabel: (value) => new native.LabelMatcher(value),
6
5
  id: (value) => new native.IdMatcher(value),
7
- label: (value) => new native.LabelMatcher(value),
6
+ label: (value) => new native.ShallowLabelMatcher(value),
7
+ accessibilityLabel: (value) => new native.ShallowLabelMatcher(value),
8
8
  text: (value) => new native.TextMatcher(value),
9
9
  traits: (value) => new native.TraitsMatcher(value),
10
10
  type: (value) => new native.TypeMatcher(value),
@@ -6,7 +6,14 @@ const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
6
6
  class LabelMatcher extends NativeMatcher {
7
7
  constructor(value) {
8
8
  super();
9
- this._call = invoke.callDirectly(DetoxMatcherApi.matcherForContentDescription(value));
9
+ this._call = invoke.callDirectly(DetoxMatcherApi.matcherForAccessibilityLabel(value));
10
+ }
11
+ }
12
+
13
+ class ShallowLabelMatcher extends NativeMatcher {
14
+ constructor(value) {
15
+ super();
16
+ this._call = invoke.callDirectly(DetoxMatcherApi.matcherForShallowAccessibilityLabel(value));
10
17
  }
11
18
  }
12
19
 
@@ -90,6 +97,7 @@ class SliderPositionMatcher extends NativeMatcher {
90
97
 
91
98
  module.exports = {
92
99
  LabelMatcher,
100
+ ShallowLabelMatcher,
93
101
  IdMatcher,
94
102
  TypeMatcher,
95
103
  TraitsMatcher,
@@ -55,6 +55,7 @@ function collectCliConfig({ argv }) {
55
55
  useCustomLogger: asBoolean(get('use-custom-logger')),
56
56
  retries: asNumber(get('retries')),
57
57
  inspectBrk: asBoolean(get('inspect-brk')),
58
+ start: get('start'),
58
59
  }, _.isUndefined);
59
60
  }
60
61
 
@@ -1 +0,0 @@
1
- fe1f11c2f7311ae0f6b13926808a59fc
@@ -1 +0,0 @@
1
- bd54a4027638a599254b2df73919dd4fc2633ec2
@@ -1 +0,0 @@
1
- 7f7ab53c44e53bb959746e8ccc107e3f04d2aca31ec75fb544891db0eb91a476
@@ -1 +0,0 @@
1
- 83af81079557e46d8b995f28b6e8ed8a221d02e9d6319146d978b84df530ff6289806c6b9c13de94cb5023948a986ee8dc8a4dfc15ab8c683af1cb6013e4e1a8
@@ -1 +0,0 @@
1
- 1493e043853c3f2a42d0d63062b796f1
@@ -1 +0,0 @@
1
- a2da124a011cf479474605ddefd629fc192498b4
@@ -1 +0,0 @@
1
- 8d64a4b32323c64dcec482f322d19233577ffc5fe3895e3ca30d9cb220504409
@@ -1 +0,0 @@
1
- 2bb0aaf9f7232f18adcebd3d1fc1424d1d8f1d47892663f962030ef7db1a02eb7943844aa69f14b4ebeb01d0dd562dc0420fb0ff7152a38ca44e1a525a58d745
@@ -1 +0,0 @@
1
- e8ad50ff5afce32b3cb728f837917c3a
@@ -1 +0,0 @@
1
- 8f1738000005765fd1d4746e5f2d85c766dd591f
@@ -1 +0,0 @@
1
- da1efa26b94b52cdbb831470e5753e0982bbe5ec1c861f8965a7347199e67b8b
@@ -1 +0,0 @@
1
- 9940e5dd655923d057b095817edc22c952105b1518ff02f1316cfd50be45999127e8410eb9b22e95d155517f455a484b3a52f9533f6f40e3f1e9efbf49a5eaa9
@@ -1 +0,0 @@
1
- a2316bed8a49f4fd9eb9658c90571244
@@ -1 +0,0 @@
1
- 101c01798b6766fca29a04a0cfc4a236db6416fe
@@ -1 +0,0 @@
1
- cc58b9b38e7a2ea38be0e50f76895e2d01806142ea99293eab307482321586c6
@@ -1 +0,0 @@
1
- df4124c805c1232369779953fb57f1b8d8bb9ee0de3f3e66e72f33254422e2a39a8b76fdbbffef7dc8c796eefce26fbf85723f4e7a28c8b6591d2fe085b89e14