detox 20.5.0 → 20.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/Detox-android/com/wix/detox/{20.5.0/detox-20.5.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.5.0/detox-20.5.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.5.0/detox-20.5.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 +24 -21
  40. package/package.json +2 -2
  41. package/src/android/core/NativeMatcher.js +17 -0
  42. package/src/android/espressoapi/DetoxMatcher.js +24 -0
  43. package/src/android/matchers/index.js +2 -2
  44. package/src/android/matchers/native.js +9 -1
  45. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0-javadoc.jar.md5 +0 -1
  46. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0-javadoc.jar.sha1 +0 -1
  47. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0-javadoc.jar.sha256 +0 -1
  48. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0-javadoc.jar.sha512 +0 -1
  49. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0-sources.jar.md5 +0 -1
  50. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0-sources.jar.sha1 +0 -1
  51. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0-sources.jar.sha256 +0 -1
  52. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0-sources.jar.sha512 +0 -1
  53. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0.aar +0 -0
  54. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0.aar.md5 +0 -1
  55. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0.aar.sha1 +0 -1
  56. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0.aar.sha256 +0 -1
  57. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0.aar.sha512 +0 -1
  58. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0.pom.md5 +0 -1
  59. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0.pom.sha1 +0 -1
  60. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.0.pom.sha256 +0 -1
  61. package/Detox-android/com/wix/detox/20.5.0/detox-20.5.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.5.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.5.0</latest>
7
- <release>20.5.0</release>
6
+ <latest>20.6.0</latest>
7
+ <release>20.6.0</release>
8
8
  <versions>
9
- <version>20.5.0</version>
9
+ <version>20.6.0</version>
10
10
  </versions>
11
- <lastUpdated>20230304081841</lastUpdated>
11
+ <lastUpdated>20230322171518</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 7ef4d0c49e15b7641923a73bf049cb58
1
+ f72e8f5e279054eb2e8e8faab020aaab
@@ -1 +1 @@
1
- 7612e7860c59949c39e71cd063713aa3a3fc9022
1
+ 4e3513e973cd69a3ce1b30d0756ba26660ae1ba6
@@ -1 +1 @@
1
- 1c0015aa8cdb2feaedfea04b080a429eac94a39c39220f67eb46dcd205ff136b
1
+ 319540a82e8d028d9fe417155b17c95ae4d62f0cf9f18a751a1827a27acf1802
@@ -1 +1 @@
1
- 585e914dd526582d4dc520c65d85c69d5ec7ad6abcac74db26832293e300ecf64241d0d1a4814b2b9627125cc02ffbee8964b23783a016233c6ba5676989952c
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
@@ -626,9 +626,8 @@ declare global {
626
626
 
627
627
  interface Device {
628
628
  /**
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,
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 -
630
630
  * as used by simctl (e.g. AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE).
631
- *
632
631
  */
633
632
  id: string;
634
633
  /**
@@ -1122,39 +1121,39 @@ declare global {
1122
1121
  * expectation with a `not` expects the view's visible area to be smaller than N%.
1123
1122
  * @param pct optional integer ranging from 1 to 100, indicating how much percent of the view should be
1124
1123
  * visible to the user to be accepted.
1125
- * @example await expect(element(by.id('UniqueId204'))).toBeVisible(35);
1124
+ * @example await expect(element(by.id('mainTitle'))).toBeVisible(35);
1126
1125
  */
1127
1126
  toBeVisible(pct?: number): R;
1128
1127
 
1129
1128
  /**
1130
1129
  * Negate the expectation.
1131
- * @example await expect(element(by.id('UniqueId205'))).not.toBeVisible();
1130
+ * @example await expect(element(by.id('cancelButton'))).not.toBeVisible();
1132
1131
  */
1133
1132
  not: this;
1134
1133
 
1135
1134
  /**
1136
1135
  * Expect the view to not be visible.
1137
- * @example await expect(element(by.id('UniqueId205'))).toBeNotVisible();
1136
+ * @example await expect(element(by.id('cancelButton'))).toBeNotVisible();
1138
1137
  * @deprecated Use `.not.toBeVisible()` instead.
1139
1138
  */
1140
1139
  toBeNotVisible(): R;
1141
1140
 
1142
1141
  /**
1143
1142
  * Expect the view to exist in the UI hierarchy.
1144
- * @example await expect(element(by.id('UniqueId205'))).toExist();
1143
+ * @example await expect(element(by.id('okButton'))).toExist();
1145
1144
  */
1146
1145
  toExist(): R;
1147
1146
 
1148
1147
  /**
1149
1148
  * Expect the view to not exist in the UI hierarchy.
1150
- * @example await expect(element(by.id('RandomJunk959'))).toNotExist();
1149
+ * @example await expect(element(by.id('cancelButton'))).toNotExist();
1151
1150
  * @deprecated Use `.not.toExist()` instead.
1152
1151
  */
1153
1152
  toNotExist(): R;
1154
1153
 
1155
1154
  /**
1156
1155
  * Expect the view to be focused.
1157
- * @example await expect(element(by.id('loginInput'))).toBeFocused();
1156
+ * @example await expect(element(by.id('emailInput'))).toBeFocused();
1158
1157
  */
1159
1158
  toBeFocused(): R;
1160
1159
 
@@ -1168,21 +1167,23 @@ declare global {
1168
1167
  /**
1169
1168
  * In React Native apps, expect UI component of type <Text> to have text.
1170
1169
  * In native iOS apps, expect UI elements of type UIButton, UILabel, UITextField or UITextViewIn to have inputText with text.
1171
- * @example await expect(element(by.id('UniqueId204'))).toHaveText('I contain some text');
1170
+ * @example await expect(element(by.id('mainTitle'))).toHaveText('Welcome back!);
1172
1171
  */
1173
1172
  toHaveText(text: string): R;
1174
1173
 
1175
1174
  /**
1176
- * It searches by accessibilityLabel on iOS, or by contentDescription on Android.
1177
- * In React Native it can be set for both platforms by defining an accessibilityLabel on the view.
1178
- * @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');
1179
1180
  */
1180
1181
  toHaveLabel(label: string): R;
1181
1182
 
1182
1183
  /**
1183
1184
  * In React Native apps, expect UI component to have testID with that id.
1184
1185
  * In native iOS apps, expect UI element to have accessibilityIdentifier with that id.
1185
- * @example await expect(element(by.text('I contain some text'))).toHaveId('UniqueId204');
1186
+ * @example await expect(element(by.text('Submit'))).toHaveId('submitButton');
1186
1187
  */
1187
1188
  toHaveId(id: string): R;
1188
1189
 
@@ -1195,7 +1196,7 @@ declare global {
1195
1196
 
1196
1197
  /**
1197
1198
  * Expect components like a Switch to have a value ('0' for off, '1' for on).
1198
- * @example await expect(element(by.id('UniqueId533'))).toHaveValue('0');
1199
+ * @example await expect(element(by.id('temperatureDial'))).toHaveValue('25');
1199
1200
  */
1200
1201
  toHaveValue(value: any): R;
1201
1202
 
@@ -1212,7 +1213,7 @@ declare global {
1212
1213
  /**
1213
1214
  * This API polls using the given expectation continuously until the expectation is met. Use manual synchronization with waitFor only as a last resort.
1214
1215
  * NOTE: Every waitFor call must set a timeout using withTimeout(). Calling waitFor without setting a timeout will do nothing.
1215
- * @example await waitFor(element(by.id('UniqueId336'))).toExist().withTimeout(2000);
1216
+ * @example await waitFor(element(by.id('bigButton'))).toExist().withTimeout(2000);
1216
1217
  */
1217
1218
  (element: NativeElement): Expect<WaitFor>;
1218
1219
  }
@@ -1220,13 +1221,13 @@ declare global {
1220
1221
  interface WaitFor {
1221
1222
  /**
1222
1223
  * Waits for the condition to be met until the specified time (millis) have elapsed.
1223
- * @example await waitFor(element(by.id('UniqueId336'))).toExist().withTimeout(2000);
1224
+ * @example await waitFor(element(by.id('bigButton'))).toExist().withTimeout(2000);
1224
1225
  */
1225
1226
  withTimeout(millis: number): Promise<void>;
1226
1227
 
1227
1228
  /**
1228
1229
  * Performs the action repeatedly on the element until an expectation is met
1229
- * @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');
1230
1231
  */
1231
1232
  whileElement(by: NativeMatcher): NativeElement & WaitFor;
1232
1233
 
@@ -1438,7 +1439,7 @@ declare global {
1438
1439
  interface WebExpect<R = Promise<void>> {
1439
1440
  /**
1440
1441
  * Negate the expectation.
1441
- * @example await expect(web.element(by.web.id('UniqueId205'))).not.toExist();
1442
+ * @example await expect(web.element(by.web.id('sessionTimeout'))).not.toExist();
1442
1443
  */
1443
1444
  not: this;
1444
1445
 
@@ -1446,13 +1447,13 @@ declare global {
1446
1447
  * Expect the element content to have the `text` supplied
1447
1448
  * @param text expected to be on the element content
1448
1449
  * @example
1449
- * await expect(web.element(by.web.id('UniqueId205'))).toHaveText('ExactText');
1450
+ * await expect(web.element(by.web.id('checkoutButton'))).toHaveText('Proceed to check out');
1450
1451
  */
1451
1452
  toHaveText(text: string): R;
1452
1453
 
1453
1454
  /**
1454
1455
  * Expect the view to exist in the webview DOM tree.
1455
- * @example await expect(web.element(by.web.id('UniqueId205'))).toExist();
1456
+ * @example await expect(web.element(by.web.id('submitButton'))).toExist();
1456
1457
  */
1457
1458
  toExist(): R;
1458
1459
  }
@@ -1651,7 +1652,9 @@ declare global {
1651
1652
  */
1652
1653
  text?: string;
1653
1654
  /**
1654
- * 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.
1655
1658
  */
1656
1659
  label?: string;
1657
1660
  /**
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.5.0",
4
+ "version": "20.6.0",
5
5
  "bin": {
6
6
  "detox": "local-cli/cli.js"
7
7
  },
@@ -200,5 +200,5 @@
200
200
  }
201
201
  }
202
202
  },
203
- "gitHead": "e7866370faef479abd6ff98aa1607e420d75b9ef"
203
+ "gitHead": "03e23e76b54f1e7de70495e0ec8a8db45872b0e6"
204
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,
@@ -1 +0,0 @@
1
- 312ca2e6d26a005375c7d31f7a489b60
@@ -1 +0,0 @@
1
- 80debe0a3a1ed317173f56f92cbfd1f108f3fb6c
@@ -1 +0,0 @@
1
- ceeb582236c99040fb116be37c6eb97c67ecdf91f8cdd1d5dade614a3180375c
@@ -1 +0,0 @@
1
- 08dcd64b4e65c486a7fe0d4a59b23e4b9ffc037b4afad03db9cb820b97867e22693d929b7e896f41c11c8a4e3e0d0dbbc11799a69f438d46aeeb8ebb8b157056
@@ -1 +0,0 @@
1
- 643850c6dad298f22d9ce657af17dd5c
@@ -1 +0,0 @@
1
- e483f7ab88c2b2d5882d1c825155856c61ba059f
@@ -1 +0,0 @@
1
- 2ee1a851c254668619e44df167d0b1d86bd63edb9414a239f1fcea380c39a077
@@ -1 +0,0 @@
1
- 3446a90564f2af11a6b6221f114ccd8786044080e222ed698fef6a92d706faed154fdaa976c0dca162db58976170a60b7bc05d5e931a1b1bbf02ff8292d0c47c
@@ -1 +0,0 @@
1
- e8ad50ff5afce32b3cb728f837917c3a
@@ -1 +0,0 @@
1
- 8f1738000005765fd1d4746e5f2d85c766dd591f
@@ -1 +0,0 @@
1
- da1efa26b94b52cdbb831470e5753e0982bbe5ec1c861f8965a7347199e67b8b
@@ -1 +0,0 @@
1
- 9940e5dd655923d057b095817edc22c952105b1518ff02f1316cfd50be45999127e8410eb9b22e95d155517f455a484b3a52f9533f6f40e3f1e9efbf49a5eaa9
@@ -1 +0,0 @@
1
- e6f8b937b6573484515f78adcc830096
@@ -1 +0,0 @@
1
- 59e4b461ba168a526c1c629fb29ded508472e732
@@ -1 +0,0 @@
1
- b455e110295c40221b1620cfe6ffc905fc9623b36a5f38fabf52316def1f9749
@@ -1 +0,0 @@
1
- 766004a843b60ee7b385f26324762d8f18d35064324fb7ab5c24e59816869745401dafbfc3517ff55948527fd0411a2b9382b3f29de8aa6756e61df58a3c416c