detox 20.5.0 → 20.6.0

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.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