detox 20.4.0 → 20.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-javadoc.jar.md5 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-javadoc.jar.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-javadoc.jar.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-javadoc.jar.sha512 +1 -0
- 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
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-sources.jar.md5 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-sources.jar.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-sources.jar.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0-sources.jar.sha512 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar +0 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar.md5 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.aar.sha512 +1 -0
- package/Detox-android/com/wix/detox/{20.4.0/detox-20.4.0.pom → 20.6.0/detox-20.6.0.pom} +1 -1
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.pom.md5 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.pom.sha1 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.pom.sha256 +1 -0
- package/Detox-android/com/wix/detox/20.6.0/detox-20.6.0.pom.sha512 +1 -0
- package/Detox-android/com/wix/detox/maven-metadata.xml +4 -4
- package/Detox-android/com/wix/detox/maven-metadata.xml.md5 +1 -1
- package/Detox-android/com/wix/detox/maven-metadata.xml.sha1 +1 -1
- package/Detox-android/com/wix/detox/maven-metadata.xml.sha256 +1 -1
- package/Detox-android/com/wix/detox/maven-metadata.xml.sha512 +1 -1
- package/Detox-ios-src.tbz +0 -0
- package/Detox-ios.tbz +0 -0
- package/android/detox/src/full/java/com/wix/detox/common/UIExtensions.kt +28 -0
- package/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java +11 -1
- package/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt +4 -3
- package/android/detox/src/full/java/com/wix/detox/espresso/matcher/ViewMatchers.kt +8 -5
- package/android/detox/src/full/java/com/wix/detox/espresso/matcher/WithAccessibilityLabelMatcher.kt +23 -0
- package/android/detox/src/full/java/com/wix/detox/reactnative/ui/UIExtensions.kt +37 -0
- package/android/detox/src/full/java/com/wix/detox/reactnative/utils/RNUtils.kt +6 -0
- package/android/detox/src/testFull/java/com/wix/detox/UTHelpers.kt +12 -0
- package/android/detox/src/testFull/java/com/wix/detox/common/UIExtensionsTest.kt +107 -0
- package/android/detox/src/testFull/java/com/wix/detox/espresso/action/GetAttributesActionTest.kt +7 -6
- package/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/ViewAtIndexMatcherSpec.kt +1 -2
- package/index.d.ts +26 -21
- package/local-cli/cli.js +1 -1
- package/local-cli/init.js +2 -2
- package/local-cli/start.js +49 -0
- package/local-cli/startCommand/AppStartCommand.js +65 -0
- package/local-cli/testCommand/TestRunnerCommand.js +29 -0
- package/local-cli/testCommand/builder.js +5 -0
- package/package.json +3 -2
- package/src/android/core/NativeMatcher.js +17 -0
- package/src/android/espressoapi/DetoxMatcher.js +24 -0
- package/src/android/matchers/index.js +2 -2
- package/src/android/matchers/native.js +9 -1
- package/src/configuration/collectCliConfig.js +1 -0
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-javadoc.jar.md5 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-javadoc.jar.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-javadoc.jar.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-javadoc.jar.sha512 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-sources.jar.md5 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-sources.jar.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-sources.jar.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0-sources.jar.sha512 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar +0 -0
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar.md5 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.aar.sha512 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.pom.md5 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.pom.sha1 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.pom.sha256 +0 -1
- package/Detox-android/com/wix/detox/20.4.0/detox-20.4.0.pom.sha512 +0 -1
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
c051f79d02bcc98757dd1ea06512a63b
|
@@ -0,0 +1 @@
|
|
1
|
+
bb48d0747cb607e73a01d64f5eb758ce00e376cb
|
@@ -0,0 +1 @@
|
|
1
|
+
b31397b81cdca47b2d5dc1aca7821f2691d236114064949ad8553fd81340fe02
|
@@ -0,0 +1 @@
|
|
1
|
+
6cec7c20f0d9643b98e2fa2f4740c9bb7a4b82ae3851069bb183e16ffcace8b0ceb07cf8d19316a960beb2330ea3e0a508df80cda1561242149ce3b16357d954
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
6202d8d0f83dda9ed0c9d6962259350e
|
@@ -0,0 +1 @@
|
|
1
|
+
505b10faaca41e1b5e7f27b8829cd14aff6b5626
|
@@ -0,0 +1 @@
|
|
1
|
+
f4b81a4c2c1665975ac5be02698e288a311e3cd3c728ba9b05e72a1932f7482e
|
@@ -0,0 +1 @@
|
|
1
|
+
e355acd2730e5ca57bb7f2c092942b6dd98997645b43350fd2c80051f3f8a6840f2746cd2e8fbeae862ab4cac0fc6339b7f298e697f3bfb4124598370c4b2a54
|
Binary file
|
@@ -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.
|
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.
|
7
|
-
<release>20.
|
6
|
+
<latest>20.6.0</latest>
|
7
|
+
<release>20.6.0</release>
|
8
8
|
<versions>
|
9
|
-
<version>20.
|
9
|
+
<version>20.6.0</version>
|
10
10
|
</versions>
|
11
|
-
<lastUpdated>
|
11
|
+
<lastUpdated>20230322171518</lastUpdated>
|
12
12
|
</versioning>
|
13
13
|
</metadata>
|
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
f72e8f5e279054eb2e8e8faab020aaab
|
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
4e3513e973cd69a3ce1b30d0756ba26660ae1ba6
|
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
319540a82e8d028d9fe417155b17c95ae4d62f0cf9f18a751a1827a27acf1802
|
@@ -1 +1 @@
|
|
1
|
-
|
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
|
-
|
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
|
70
|
-
view.
|
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
|
12
|
-
import org.hamcrest.
|
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)
|
package/android/detox/src/full/java/com/wix/detox/espresso/matcher/WithAccessibilityLabelMatcher.kt
ADDED
@@ -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
|
@@ -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
|
+
}
|
package/android/detox/src/testFull/java/com/wix/detox/espresso/action/GetAttributesActionTest.kt
CHANGED
@@ -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
|
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
|
114
|
-
val
|
115
|
-
|
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(
|
119
|
+
assertThat(resultJson.opt("label")).isEqualTo(accessibilityLabel)
|
119
120
|
}
|
120
121
|
|
121
122
|
@Test
|
122
|
-
fun `should not return label if
|
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
|
}
|
package/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/ViewAtIndexMatcherSpec.kt
CHANGED
@@ -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
|
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('
|
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('
|
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('
|
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('
|
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('
|
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('
|
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('
|
1170
|
+
* @example await expect(element(by.id('mainTitle'))).toHaveText('Welcome back!);
|
1170
1171
|
*/
|
1171
1172
|
toHaveText(text: string): R;
|
1172
1173
|
|
1173
1174
|
/**
|
1174
|
-
*
|
1175
|
-
*
|
1176
|
-
*
|
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('
|
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('
|
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('
|
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('
|
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('
|
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('
|
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('
|
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('
|
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.
|
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':
|
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
|
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
|
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
|
+
"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": "
|
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.
|
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.
|
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
|
-
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
|
Binary file
|
@@ -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
|