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