detox 20.27.6 → 20.28.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. package/Detox-android/com/wix/detox/{20.27.6/detox-20.27.6-sources.jar → 20.28.0/detox-20.28.0-sources.jar} +0 -0
  2. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0-sources.jar.md5 +1 -0
  3. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0-sources.jar.sha1 +1 -0
  4. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0-sources.jar.sha256 +1 -0
  5. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0-sources.jar.sha512 +1 -0
  6. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0.aar +0 -0
  7. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0.aar.md5 +1 -0
  8. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0.aar.sha1 +1 -0
  9. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0.aar.sha256 +1 -0
  10. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0.aar.sha512 +1 -0
  11. package/Detox-android/com/wix/detox/{20.27.6/detox-20.27.6.pom → 20.28.0/detox-20.28.0.pom} +1 -1
  12. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0.pom.md5 +1 -0
  13. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0.pom.sha1 +1 -0
  14. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0.pom.sha256 +1 -0
  15. package/Detox-android/com/wix/detox/20.28.0/detox-20.28.0.pom.sha512 +1 -0
  16. package/Detox-android/com/wix/detox/maven-metadata.xml +4 -4
  17. package/Detox-android/com/wix/detox/maven-metadata.xml.md5 +1 -1
  18. package/Detox-android/com/wix/detox/maven-metadata.xml.sha1 +1 -1
  19. package/Detox-android/com/wix/detox/maven-metadata.xml.sha256 +1 -1
  20. package/Detox-android/com/wix/detox/maven-metadata.xml.sha512 +1 -1
  21. package/Detox-android/com/wix/detox-legacy/{20.27.6/detox-legacy-20.27.6-sources.jar → 20.28.0/detox-legacy-20.28.0-sources.jar} +0 -0
  22. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0-sources.jar.md5 +1 -0
  23. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0-sources.jar.sha1 +1 -0
  24. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0-sources.jar.sha256 +1 -0
  25. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0-sources.jar.sha512 +1 -0
  26. package/Detox-android/com/wix/detox-legacy/{20.27.6/detox-legacy-20.27.6.aar → 20.28.0/detox-legacy-20.28.0.aar} +0 -0
  27. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0.aar.md5 +1 -0
  28. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0.aar.sha1 +1 -0
  29. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0.aar.sha256 +1 -0
  30. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0.aar.sha512 +1 -0
  31. package/Detox-android/com/wix/detox-legacy/{20.27.6/detox-legacy-20.27.6.pom → 20.28.0/detox-legacy-20.28.0.pom} +1 -1
  32. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0.pom.md5 +1 -0
  33. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0.pom.sha1 +1 -0
  34. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0.pom.sha256 +1 -0
  35. package/Detox-android/com/wix/detox-legacy/20.28.0/detox-legacy-20.28.0.pom.sha512 +1 -0
  36. package/Detox-android/com/wix/detox-legacy/maven-metadata.xml +4 -4
  37. package/Detox-android/com/wix/detox-legacy/maven-metadata.xml.md5 +1 -1
  38. package/Detox-android/com/wix/detox-legacy/maven-metadata.xml.sha1 +1 -1
  39. package/Detox-android/com/wix/detox-legacy/maven-metadata.xml.sha256 +1 -1
  40. package/Detox-android/com/wix/detox-legacy/maven-metadata.xml.sha512 +1 -1
  41. package/Detox-ios-framework.tbz +0 -0
  42. package/Detox-ios-src.tbz +0 -0
  43. package/Detox-ios-xcuitest.tbz +0 -0
  44. package/android/detox/src/full/java/com/wix/detox/espresso/EspressoDetox.java +53 -2
  45. package/android/detox/src/full/java/com/wix/detox/espresso/UiAutomatorHelper.java +11 -0
  46. package/android/detox/src/full/java/com/wix/detox/espresso/matcher/IsDisplayingAtLeastDetoxMatcher.kt +2 -2
  47. package/detox.d.ts +39 -0
  48. package/package.json +3 -3
  49. package/src/android/espressoapi/EspressoDetox.js +83 -0
  50. package/src/copilot/detoxCopilotFrameworkDriver.js +27 -15
  51. package/src/devices/runtime/RuntimeDevice.js +11 -0
  52. package/src/devices/runtime/drivers/DeviceDriverBase.js +8 -0
  53. package/src/devices/runtime/drivers/android/AndroidDriver.js +16 -0
  54. package/src/devices/runtime/drivers/ios/SimulatorDriver.js +35 -0
  55. package/src/utils/assertArgument.js +9 -0
  56. package/src/utils/invocationTraceDescriptions.js +1 -0
  57. package/src/utils/mapDeviceLongPressArguments.js +56 -0
  58. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6-sources.jar.md5 +0 -1
  59. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6-sources.jar.sha1 +0 -1
  60. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6-sources.jar.sha256 +0 -1
  61. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6-sources.jar.sha512 +0 -1
  62. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6.aar +0 -0
  63. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6.aar.md5 +0 -1
  64. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6.aar.sha1 +0 -1
  65. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6.aar.sha256 +0 -1
  66. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6.aar.sha512 +0 -1
  67. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6.pom.md5 +0 -1
  68. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6.pom.sha1 +0 -1
  69. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6.pom.sha256 +0 -1
  70. package/Detox-android/com/wix/detox/20.27.6/detox-20.27.6.pom.sha512 +0 -1
  71. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6-sources.jar.md5 +0 -1
  72. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6-sources.jar.sha1 +0 -1
  73. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6-sources.jar.sha256 +0 -1
  74. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6-sources.jar.sha512 +0 -1
  75. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6.aar.md5 +0 -1
  76. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6.aar.sha1 +0 -1
  77. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6.aar.sha256 +0 -1
  78. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6.aar.sha512 +0 -1
  79. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6.pom.md5 +0 -1
  80. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6.pom.sha1 +0 -1
  81. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6.pom.sha256 +0 -1
  82. package/Detox-android/com/wix/detox-legacy/20.27.6/detox-legacy-20.27.6.pom.sha512 +0 -1
@@ -0,0 +1 @@
1
+ 5f9c61888ac541880ca966b9cf54f996
@@ -0,0 +1 @@
1
+ 905d3c70363af1ef378373c1337b2a02fb085847
@@ -0,0 +1 @@
1
+ 9da6d1b24a67e2c3f54e4f85b76d2f308df4f4783a2ead715cacae4078d54198
@@ -0,0 +1 @@
1
+ 488ca79bd9330a788fe0659818e9256d06908d3c9ff765ddf9ee41e253b78dbbb0db1409f794532019fa39ca3395c8a27396398cbe56d203989afb258c31f1b8
@@ -0,0 +1 @@
1
+ 2d6e006f6dc0428fd9be3efb971433cf
@@ -0,0 +1 @@
1
+ 7e71f1e0bd4e0beea5f2c8db164c560255332a30
@@ -0,0 +1 @@
1
+ d0d12c5ecb2c4d760332b168a9237aa1948ac10ea1736a4b75067de6ebaaf6cb
@@ -0,0 +1 @@
1
+ 1ebe46cd76b0bcfdcab9fc6b1a863b257da71ee05bac63f538fab1351f55ba858d97b81aba1d040d5db74c9e27508b12dbeb6ae2e6c09ef469a184516fc82c00
@@ -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.27.6</version>
6
+ <version>20.28.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
+ 72dd9fb9e071a00dfb6b7731da5a8268
@@ -0,0 +1 @@
1
+ 5ea89a7c06c7d3e3ebee78b290422c25471ffccf
@@ -0,0 +1 @@
1
+ 48839af705f5ce69be3f8075367e012c822f2cb9b4d5d66d1466c718f7adecd4
@@ -0,0 +1 @@
1
+ 01b1f663413f39c8f25e81f70669ddce75f2f332f154c531cb5289a646d25e3cc80120580202da5bc3cf1e4fd865a61b0551126be9da37b61eeec6f75fc2b9cf
@@ -3,11 +3,11 @@
3
3
  <groupId>com.wix</groupId>
4
4
  <artifactId>detox</artifactId>
5
5
  <versioning>
6
- <latest>20.27.6</latest>
7
- <release>20.27.6</release>
6
+ <latest>20.28.0</latest>
7
+ <release>20.28.0</release>
8
8
  <versions>
9
- <version>20.27.6</version>
9
+ <version>20.28.0</version>
10
10
  </versions>
11
- <lastUpdated>20241028085405</lastUpdated>
11
+ <lastUpdated>20241114182711</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- c6048a4804a00c640dd27f84e2452c46
1
+ 4409ef64d1213179048bfd9fed283361
@@ -1 +1 @@
1
- 6d5a74fb04b17b1bcf036c6bef0cc9e89c149b96
1
+ 112cd653d1f7777391df1fb10e9a33619f5d0c1b
@@ -1 +1 @@
1
- 27ca3908ac60eb0dbb4345d0cf06eb099cda5bf717bea60add2a0fc7b9a66d3f
1
+ 2177b496977ac272c3b7333fa2a4505f8756dca0bc265970801d60985a6a8dfd
@@ -1 +1 @@
1
- f3c8e726366163a7fac72e38412cd55257d21b3927305a2210851e4bc70d83298aa30a830a2d1ece614e2aeee3bb68065f8964442a1e5125d82bb5b6ca8a88bd
1
+ da830892bc25886891e341fe7e167f4aa1138c8fc2c28246d31409c39e82aa64f13c0a0a39bb6394a260ee2f449bf16e5378be6026cf1862759d57cec40fd053
@@ -0,0 +1 @@
1
+ d01d2e826e3b99f843a7e444d309d6d2
@@ -0,0 +1 @@
1
+ 3cf94f158d84d98241e421c64cc5c06142fbf4c0
@@ -0,0 +1 @@
1
+ cb302fd2cd81b8ba3a03115ab529d073bb8f223af0088e89e22af90120117412
@@ -0,0 +1 @@
1
+ 37109f12c35e795d76f25a7c462c143dcedbbd0cb66bdde25364f237754da179307e87b73fae67e5432b7724903abe485c5105ac5667a7f6f26806b6c5229d88
@@ -0,0 +1 @@
1
+ f3e0e6447e243a7a93b9425d8ca2c34d
@@ -0,0 +1 @@
1
+ 135215a6c872a80b0020ecface42fd07fde6e8a2
@@ -0,0 +1 @@
1
+ 67f6e9df85d01f17887b135aa64b3db8b10afce941b70314fa1cc15580699102
@@ -0,0 +1 @@
1
+ a71be556721d9c7b600914bcbeaff171d0d959a3dbb816d4467c8f52caa5703d9b241c0a76c13e4010c57036fff63b0c157eb126c58a96fe593ef1a71dcd3778
@@ -3,7 +3,7 @@
3
3
  <modelVersion>4.0.0</modelVersion>
4
4
  <groupId>com.wix</groupId>
5
5
  <artifactId>detox-legacy</artifactId>
6
- <version>20.27.6</version>
6
+ <version>20.28.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
+ 2c9a5efdc4cc9949403de29543d189c3
@@ -0,0 +1 @@
1
+ dd1b295394376809aa40565060e7f0068758476d
@@ -0,0 +1 @@
1
+ 28a059a24a50d3e94b75af039c1aee8dd403bb862525cce01876a4a188313eb1
@@ -0,0 +1 @@
1
+ 252cf3d47cd05805ab38367c8b33793b5b7e210a5f7f88717b1439127f168181bd5b3a05fa020c4c444aad0dfb6ad81547a7bc05ce6dc5f8043aff2eea3096f1
@@ -3,11 +3,11 @@
3
3
  <groupId>com.wix</groupId>
4
4
  <artifactId>detox-legacy</artifactId>
5
5
  <versioning>
6
- <latest>20.27.6</latest>
7
- <release>20.27.6</release>
6
+ <latest>20.28.0</latest>
7
+ <release>20.28.0</release>
8
8
  <versions>
9
- <version>20.27.6</version>
9
+ <version>20.28.0</version>
10
10
  </versions>
11
- <lastUpdated>20241028085440</lastUpdated>
11
+ <lastUpdated>20241114182808</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 13ddd43ad56b9aa278e0c66d15b01b24
1
+ dbb9351f570b2e9f79cd97583cda0e41
@@ -1 +1 @@
1
- 80895bf7ff050aecd0f6952aa5515e30efc99ad2
1
+ e043135169acc282528cb1990f80ae8be7fd89b2
@@ -1 +1 @@
1
- 9bbe583005a3408878ff88161454adbde91024cabbccc59fbc76a88afcafca75
1
+ 723c473ca26e2ced79ca5d707aa4e96f068d3bb86577a66d54ae5c41231127f2
@@ -1 +1 @@
1
- a753957dcc440153f8fed723cd0520cb38f04d8ae87e491d92d09fe93b4f95602d3d07dea0ee145e18ae4621b33e42fa9f248e8c7586b77154d224b157879782
1
+ 5061e8696bf7b07237dd2b8fc02f65054eaaa87884723ca0f706b3b1de63e153e636db8077c6fdfd29ad8d0ca67433ff6a327bf6ffaddf040e671a2cafb4106e
Binary file
package/Detox-ios-src.tbz CHANGED
Binary file
Binary file
@@ -21,12 +21,11 @@ import java.util.ArrayList;
21
21
 
22
22
  import androidx.test.espresso.UiController;
23
23
  import androidx.test.espresso.ViewAction;
24
- import androidx.test.espresso.ViewInteraction;
25
- import androidx.test.espresso.NoMatchingViewException;
26
24
  import androidx.test.platform.app.InstrumentationRegistry;
27
25
 
28
26
  import static androidx.test.espresso.Espresso.onView;
29
27
  import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
28
+ import static com.wix.detox.espresso.UiAutomatorHelper.getStatusBarHeightDps;
30
29
 
31
30
  /**
32
31
  * Created by rotemm on 26/12/2016.
@@ -34,6 +33,10 @@ import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
34
33
  public class EspressoDetox {
35
34
  private static final String LOG_TAG = "detox";
36
35
 
36
+ private static int calculateAdjustedY(View view, Integer y, boolean shouldIgnoreStatusBar) {
37
+ return shouldIgnoreStatusBar ? y + getStatusBarHeightDps(view) : y;
38
+ }
39
+
37
40
  public static Object perform(Matcher<View> matcher, ViewAction action) {
38
41
  ViewActionPerformer performer = ViewActionPerformer.forAction(action);
39
42
  return performer.performOn(matcher);
@@ -121,5 +124,53 @@ public class EspressoDetox {
121
124
  }
122
125
  });
123
126
  }
127
+
128
+ public static void tap(Integer x, Integer y, boolean shouldIgnoreStatusBar) {
129
+ onView(isRoot()).perform(new ViewAction() {
130
+ @Override
131
+ public Matcher<View> getConstraints() {
132
+ return isRoot();
133
+ }
134
+
135
+ @Override
136
+ public String getDescription() {
137
+ return "tap on screen";
138
+ }
139
+
140
+ @Override
141
+ public void perform(UiController uiController, View view) {
142
+ int adjustedY = calculateAdjustedY(view, y, shouldIgnoreStatusBar);
143
+ ViewAction action = DetoxAction.tapAtLocation(x, adjustedY);
144
+ action.perform(uiController, view);
145
+ uiController.loopMainThreadUntilIdle();
146
+ }
147
+ });
148
+ }
149
+
150
+ public static void longPress(Integer x, Integer y, boolean shouldIgnoreStatusBar) {
151
+ longPress(x, y, null, shouldIgnoreStatusBar);
152
+ }
153
+
154
+ public static void longPress(Integer x, Integer y, Integer duration, boolean shouldIgnoreStatusBar) {
155
+ onView(isRoot()).perform(new ViewAction() {
156
+ @Override
157
+ public Matcher<View> getConstraints() {
158
+ return isRoot();
159
+ }
160
+
161
+ @Override
162
+ public String getDescription() {
163
+ return "long press on screen";
164
+ }
165
+
166
+ @Override
167
+ public void perform(UiController uiController, View view) {
168
+ int adjustedY = calculateAdjustedY(view, y, shouldIgnoreStatusBar);
169
+ ViewAction action = DetoxAction.longPress(x, adjustedY, duration);
170
+ action.perform(uiController, view);
171
+ uiController.loopMainThreadUntilIdle();
172
+ }
173
+ });
174
+ }
124
175
  }
125
176
 
@@ -1,8 +1,13 @@
1
1
  package com.wix.detox.espresso;
2
2
 
3
+ import android.annotation.SuppressLint;
4
+ import android.content.Context;
3
5
  import android.os.Handler;
6
+ import android.util.DisplayMetrics;
4
7
  import android.util.Log;
8
+ import android.util.TypedValue;
5
9
  import android.view.Choreographer;
10
+ import android.view.View;
6
11
 
7
12
  import com.wix.detox.common.UIThread;
8
13
  import com.wix.detox.espresso.action.common.utils.UiControllerUtils;
@@ -111,4 +116,10 @@ public class UiAutomatorHelper {
111
116
  }
112
117
  }
113
118
 
119
+ @SuppressLint({"DiscouragedApi", "InternalInsetResource"})
120
+ public static int getStatusBarHeightDps(View view) {
121
+ Context context = view.getContext();
122
+ int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
123
+ return (int) (context.getResources().getDimensionPixelSize(resourceId) / ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT));
124
+ }
114
125
  }
@@ -120,7 +120,7 @@ class IsDisplayingAtLeastDetoxMatcher(private val areaPercentage: Int) : TypeSaf
120
120
  .defaultDisplay
121
121
  .getMetrics(m)
122
122
 
123
- val statusBarHeight = getStatusBarHeight(view)
123
+ val statusBarHeight = getStatusBarHeightPixels(view)
124
124
  val actionBarHeight = getActionBarHeight(view)
125
125
  return Rect(0, 0, m.widthPixels, m.heightPixels - (statusBarHeight + actionBarHeight))
126
126
  }
@@ -138,7 +138,7 @@ class IsDisplayingAtLeastDetoxMatcher(private val areaPercentage: Int) : TypeSaf
138
138
  }
139
139
 
140
140
  @SuppressLint("InternalInsetResource", "DiscouragedApi")
141
- private fun getStatusBarHeight(view: View): Int {
141
+ private fun getStatusBarHeightPixels(view: View): Int {
142
142
  val resourceId = view.context.resources.getIdentifier("status_bar_height", "dimen", "android")
143
143
  return if (resourceId > 0) view.context.resources.getDimensionPixelSize(resourceId) else 0
144
144
  }
package/detox.d.ts CHANGED
@@ -784,6 +784,45 @@ declare global {
784
784
  */
785
785
  setOrientation(orientation: Orientation): Promise<void>;
786
786
 
787
+ /**
788
+ * Perform a tap at arbitrary coordinates on the device's screen.
789
+ * @param point Coordinates in the element's coordinate space. Optional. defaults: x: 100, y: 100
790
+ * @param shouldIgnoreStatusBar Coordinates will be measured starting from under the status bar. this param will affect only in Android tests. Optional. default: true
791
+ * @example await device.tap();
792
+ * @example await device.tap({ x: 100, y: 150 }, false);
793
+ * @example await device.tap({ x: 100, y: 150 });
794
+ * @example await device.tap(false);
795
+ */
796
+ tap(): Promise<void>;
797
+ tap(point: Point2D): Promise<void>;
798
+ tap(point: Point2D, shouldIgnoreStatusBar: boolean): Promise<void>;
799
+ tap(shouldIgnoreStatusBar: boolean): Promise<void>;
800
+
801
+ /**
802
+ * Perform a long press at arbitrary coordinates on the device's screen. Custom press duration if needed.
803
+ * @param point Coordinates in the device's coordinate space. Optional. defaults: x: 100, y: 100
804
+ * @param duration Custom press duration time, in milliseconds. Optional (defaults to the standard long-press duration for Android and 1000 milliseconds for ios).
805
+ * Custom durations should be used cautiously, as they can affect test consistency and user experience expectations.
806
+ * They are typically necessary when testing components that behave differently from the platform's defaults or when simulating unique user interactions.
807
+ * @param shouldIgnoreStatusBar Coordinates will be measured starting from under the status bar. this param will affect only in Android tests. Optional. default: true
808
+ * @example await device.longPress();
809
+ * @example await device.longPress({ x: 100, y: 150 }, 2000, false);
810
+ * @example await device.longPress({ x: 100, y: 150 }, 2000);
811
+ * @example await device.longPress(2000, false);
812
+ * @example await device.longPress({ x: 100, y: 150 }, false);
813
+ * @example await device.longPress({ x: 100, y: 150 });
814
+ * @example await device.longPress(2000);
815
+ * @example await device.longPress(false);
816
+ */
817
+ longPress(): Promise<void>;
818
+ longPress(point: Point2D, duration: number, shouldIgnoreStatusBar: boolean): Promise<void>;
819
+ longPress(point: Point2D, duration: number): Promise<void>;
820
+ longPress(duration: number, shouldIgnoreStatusBar: boolean): Promise<void>;
821
+ longPress(point: Point2D, shouldIgnoreStatusBar: boolean): Promise<void>;
822
+ longPress(point: Point2D): Promise<void>;
823
+ longPress(duration: number): Promise<void>;
824
+ longPress(shouldIgnoreStatusBar: boolean): Promise<void>;
825
+
787
826
  /**
788
827
  * Sets the simulator/emulator location to the given latitude and longitude.
789
828
  *
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.27.6",
4
+ "version": "20.28.0",
5
5
  "bin": {
6
6
  "detox": "local-cli/cli.js"
7
7
  },
@@ -71,7 +71,7 @@
71
71
  "caf": "^15.0.1",
72
72
  "chalk": "^4.0.0",
73
73
  "child-process-promise": "^2.2.0",
74
- "detox-copilot": "^0.0.23",
74
+ "detox-copilot": "^0.0.24",
75
75
  "execa": "^5.1.1",
76
76
  "find-up": "^5.0.0",
77
77
  "fs-extra": "^11.0.0",
@@ -116,5 +116,5 @@
116
116
  "browserslist": [
117
117
  "node 14"
118
118
  ],
119
- "gitHead": "620d20c813b0d5507d1a415a0efd8be59ea5b691"
119
+ "gitHead": "c97e583d7e33ad9472d5b3e68dda724f1300c83c"
120
120
  }
@@ -73,6 +73,89 @@ class EspressoDetox {
73
73
  };
74
74
  }
75
75
 
76
+ static tap(x, y, shouldIgnoreStatusBar) {
77
+ if (typeof x !== "number") throw new Error("x should be a number, but got " + (x + (" (" + (typeof x + ")"))));
78
+ if (typeof y !== "number") throw new Error("y should be a number, but got " + (y + (" (" + (typeof y + ")"))));
79
+ if (typeof shouldIgnoreStatusBar !== "boolean") throw new Error("shouldIgnoreStatusBar should be a boolean, but got " + (shouldIgnoreStatusBar + (" (" + (typeof shouldIgnoreStatusBar + ")"))));
80
+ return {
81
+ target: {
82
+ type: "Class",
83
+ value: "com.wix.detox.espresso.EspressoDetox"
84
+ },
85
+ method: "tap",
86
+ args: [{
87
+ type: "Integer",
88
+ value: x
89
+ }, {
90
+ type: "Integer",
91
+ value: y
92
+ }, {
93
+ type: "boolean",
94
+ value: shouldIgnoreStatusBar
95
+ }]
96
+ };
97
+ }
98
+
99
+ static longPress(x, y, shouldIgnoreStatusBar) {
100
+ function longPress3(x, y, shouldIgnoreStatusBar) {
101
+ if (typeof x !== "number") throw new Error("x should be a number, but got " + (x + (" (" + (typeof x + ")"))));
102
+ if (typeof y !== "number") throw new Error("y should be a number, but got " + (y + (" (" + (typeof y + ")"))));
103
+ if (typeof shouldIgnoreStatusBar !== "boolean") throw new Error("shouldIgnoreStatusBar should be a boolean, but got " + (shouldIgnoreStatusBar + (" (" + (typeof shouldIgnoreStatusBar + ")"))));
104
+ return {
105
+ target: {
106
+ type: "Class",
107
+ value: "com.wix.detox.espresso.EspressoDetox"
108
+ },
109
+ method: "longPress",
110
+ args: [{
111
+ type: "Integer",
112
+ value: x
113
+ }, {
114
+ type: "Integer",
115
+ value: y
116
+ }, {
117
+ type: "boolean",
118
+ value: shouldIgnoreStatusBar
119
+ }]
120
+ };
121
+ }
122
+
123
+ function longPress4(x, y, duration, shouldIgnoreStatusBar) {
124
+ if (typeof x !== "number") throw new Error("x should be a number, but got " + (x + (" (" + (typeof x + ")"))));
125
+ if (typeof y !== "number") throw new Error("y should be a number, but got " + (y + (" (" + (typeof y + ")"))));
126
+ if (typeof duration !== "number") throw new Error("duration should be a number, but got " + (duration + (" (" + (typeof duration + ")"))));
127
+ if (typeof shouldIgnoreStatusBar !== "boolean") throw new Error("shouldIgnoreStatusBar should be a boolean, but got " + (shouldIgnoreStatusBar + (" (" + (typeof shouldIgnoreStatusBar + ")"))));
128
+ return {
129
+ target: {
130
+ type: "Class",
131
+ value: "com.wix.detox.espresso.EspressoDetox"
132
+ },
133
+ method: "longPress",
134
+ args: [{
135
+ type: "Integer",
136
+ value: x
137
+ }, {
138
+ type: "Integer",
139
+ value: y
140
+ }, {
141
+ type: "Integer",
142
+ value: duration
143
+ }, {
144
+ type: "boolean",
145
+ value: shouldIgnoreStatusBar
146
+ }]
147
+ };
148
+ }
149
+
150
+ if (arguments.length === 3) {
151
+ return longPress3.apply(null, arguments);
152
+ }
153
+
154
+ if (arguments.length === 4) {
155
+ return longPress4.apply(null, arguments);
156
+ }
157
+ }
158
+
76
159
  }
77
160
 
78
161
  module.exports = EspressoDetox;
@@ -18,7 +18,7 @@ const detoxCopilotFrameworkDriver = {
18
18
  },
19
19
  {
20
20
  signature: 'by.text(text: string)',
21
- description: 'Matches elements by their text.',
21
+ description: 'Matches elements by their text (value).',
22
22
  example: "element(by.text('Login'))",
23
23
  guidelines: ['Prefer test IDs over text matchers when possible.'],
24
24
  },
@@ -34,6 +34,12 @@ const detoxCopilotFrameworkDriver = {
34
34
  example: "element(by.id('listItem')).atIndex(2)",
35
35
  guidelines: ['Use when multiple elements match the same matcher.'],
36
36
  },
37
+ {
38
+ signature: 'by.label(label: string)',
39
+ description: 'Match elements with the specified label.',
40
+ example: "element(by.label('Tuesday, 1 October'));",
41
+ guidelines: ['Use when there are no other identifiers, such as for date pickers to select specific days.'],
42
+ },
37
43
  ],
38
44
  },
39
45
  {
@@ -106,7 +112,7 @@ const detoxCopilotFrameworkDriver = {
106
112
  ],
107
113
  },
108
114
  {
109
- signature: 'waitFor(element: Matcher)..toBeVisible().whileElement(element: Matcher).scroll(offset: number, direction: string)',
115
+ signature: 'waitFor(element: Matcher).toBeVisible(percent?: number).whileElement(element: Matcher).scroll(offset: number, direction: string)',
110
116
  description: 'Continuously performs an action while waiting for an expectation to be fulfilled.',
111
117
  example: `
112
118
  await waitFor(element(by.text('Load More')))
@@ -185,9 +191,13 @@ jestExpect(attributes.text).toBe('Tap Me');`,
185
191
  title: 'Assertions',
186
192
  items: [
187
193
  {
188
- signature: 'toBeVisible()',
189
- description: 'Asserts that the element is visible.',
190
- example: "await expect(element(by.id('loginButton'))).toBeVisible();",
194
+ signature: 'toBeVisible(percent?: number)',
195
+ description: 'Asserts that the element is visible with at-least the specified percentage. Default percent is 75%.',
196
+ example: "await expect(element(by.id('loginButton'))).toBeVisible(38);",
197
+ guidelines: [
198
+ 'Use the default visibility percent unless a different percentage is required.',
199
+ 'If a percentage value is provided, use the exact percentage required for the test.',
200
+ ],
191
201
  },
192
202
  {
193
203
  signature: 'toExist()',
@@ -377,8 +387,8 @@ await device.launchApp({ launchArgs: { someLaunchArg: 1234 } });`,
377
387
  items: [
378
388
  {
379
389
  signature: 'web.element(matcher: Matcher)',
380
- description: 'Selects an element within a web view. Use when there is only one web view on the screen.',
381
- example: `
390
+ description: 'Selects an element within a web view (`WKWebView` or `RNCWebView`). Use when there is only one web view on the screen.',
391
+ example: `
382
392
  await web.element(by.web.id('email')).typeText('test@example.com');
383
393
  await web.element(by.web.id('password')).typeText('password123');
384
394
  await web.element(by.web.id('login-button')).tap();
@@ -387,19 +397,20 @@ await web.element(by.web.id('login-button')).tap();
387
397
  'The web view may take time to load; add a delay using `await new Promise(resolve => setTimeout(resolve, milliseconds));` before the first interaction. This wait should happen only once.',
388
398
  'After the initial wait, you can interact with web elements without additional delays.',
389
399
  'Use `by.web.id` matcher when possible for matching web elements, as it is the most reliable.',
390
- 'Web APIs can only be used with web elements (within web views). Avoid using web APIs for native elements or native APIs for web elements.',
400
+ 'Web APIs can only be used with web elements (within web views). Do not use web APIs for native elements or native APIs for web elements!',
401
+ 'Confirm that you are targeting a web view before using this method.'
391
402
  ],
392
403
  },
393
404
  {
394
405
  signature: 'web(nativeMatcher: NativeMatcher).element(matcher: Matcher)',
395
- description: 'Selects an element within a specific web view matched by a native matcher. Use when there are multiple web views on the screen.',
406
+ description: 'Selects an element within a specific web view (`WKWebView` or `RNCWebView`) matched by a native matcher. Use when there are multiple web views on the screen.',
396
407
  example: `
397
408
  // Wait for the specific web view to appear and load (only once before interacting)
398
409
  await expect(element(by.id('checkout-webview'))).toBeVisible();
399
410
 
400
411
  // Interact with elements within a specific web view
401
412
  const specificWebView = web(by.id('checkout-webview'));
402
-
413
+
403
414
  await specificWebView.element(by.web.id('credit-card-number')).typeText('4111111111111111');
404
415
  await specificWebView.element(by.web.id('expiration-date')).typeText('12/25');
405
416
  await specificWebView.element(by.web.id('cvv')).typeText('123');
@@ -411,6 +422,7 @@ await specificWebView.element(by.web.id('pay-button')).tap();
411
422
  'After the initial wait, you can interact with elements within the web view without additional delays.',
412
423
  'Webview must be matched with `web(nativeMatcher)` (e.g., `web(by.id(..))` instead of `element(by.id(..))`).',
413
424
  'Prefer the basic `web.element()` if only one web view is present on the screen.',
425
+ 'Confirm that you are targeting a web view before using this method.'
414
426
  ],
415
427
  },
416
428
  {
@@ -485,8 +497,8 @@ await secondWebView.element(by.web.id('search-button')).tap();
485
497
  example: `await web.element(by.web.label('Next')).tap();`,
486
498
  guidelines: [
487
499
  'Available on iOS only.',
488
- 'Use when the element has a unique label or aria-label.',
489
- 'Can be used to match buttons and input elements by their inner text content.',
500
+ 'Use when the inner web element has a unique label or aria-label.',
501
+ 'Can be used to match buttons and input elements inside a web view, by their inner text content.',
490
502
  ],
491
503
  },
492
504
  {
@@ -567,13 +579,13 @@ await web.element(by.web.id('email-input')).focus();
567
579
  await web.element(by.web.id('email-input')).typeText('user@example.com');
568
580
  `,
569
581
  guidelines: [
570
- 'Useful for input fields that require focus before typing.',
582
+ 'Useful for input fields in a web view that require focus before typing.',
571
583
  'No need for secured interactions on iOS.',
572
584
  ]
573
585
  },
574
586
  {
575
587
  signature: 'moveCursorToEnd()',
576
- description: 'Moves the input cursor to the end of the element\'s content.',
588
+ description: 'Moves the input cursor in a web view to the end of the element\'s content.',
577
589
  example: `
578
590
  await web.element(by.web.id('message-box')).moveCursorToEnd();
579
591
  await web.element(by.web.id('message-box')).typeText(' Adding more text.');
@@ -581,7 +593,7 @@ await web.element(by.web.id('message-box')).typeText(' Adding more text.');
581
593
  },
582
594
  {
583
595
  signature: 'runScript(script: string, args?: any[])',
584
- description: 'Runs a JavaScript function on the element.',
596
+ description: 'Runs a JavaScript function on the web view element.',
585
597
  example: `
586
598
  // Click an element using a custom script
587
599
  await web.element(by.web.id('hidden-button')).runScript('el => el.click()');
@@ -1,6 +1,7 @@
1
1
  const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
2
2
  const debug = require('../../utils/debug'); // debug utils, leave here even if unused
3
3
  const log = require('../../utils/logger').child({ cat: 'device' });
4
+ const mapDeviceLongPressArguments = require('../../utils/mapDeviceLongPressArguments');
4
5
  const traceMethods = require('../../utils/traceMethods');
5
6
  const wrapWithStackTraceCutter = require('../../utils/wrapWithStackTraceCutter');
6
7
 
@@ -283,6 +284,16 @@ class RuntimeDevice {
283
284
  await this.deviceDriver.setOrientation(orientation);
284
285
  }
285
286
 
287
+ async tap(point, shouldIgnoreStatusBar) {
288
+ await this.deviceDriver.tap(point, shouldIgnoreStatusBar, this._bundleId);
289
+ }
290
+
291
+ async longPress(arg1, arg2, arg3) {
292
+ let { point, duration, shouldIgnoreStatusBar } = mapDeviceLongPressArguments(arg1, arg2, arg3);
293
+
294
+ await this.deviceDriver.longPress(point, duration, shouldIgnoreStatusBar, this._bundleId);
295
+ }
296
+
286
297
  async setLocation(lat, lon) {
287
298
  lat = String(lat);
288
299
  lon = String(lon);
@@ -51,6 +51,14 @@ class DeviceDriverBase {
51
51
  return '';
52
52
  }
53
53
 
54
+ async tap(_bundleId) {
55
+ return '';
56
+ }
57
+
58
+ async longPress(_bundleId) {
59
+ return '';
60
+ }
61
+
54
62
  async sendToHome() {
55
63
  return '';
56
64
  }
@@ -245,6 +245,22 @@ class AndroidDriver extends DeviceDriverBase {
245
245
  await this.invocationManager.execute(call);
246
246
  }
247
247
 
248
+ async tap(point, shouldIgnoreStatusBar) {
249
+ let x = point?.x ?? 100;
250
+ let y = point?.y ?? 100;
251
+ let _shouldIgnoreStatusBar = shouldIgnoreStatusBar ?? true;
252
+ const call = EspressoDetoxApi.tap(x, y, _shouldIgnoreStatusBar);
253
+ await this.invocationManager.execute(call);
254
+ }
255
+
256
+ async longPress(point, duration, shouldIgnoreStatusBar) {
257
+ let x = point?.x ?? 100;
258
+ let y = point?.y ?? 100;
259
+ let _shouldIgnoreStatusBar = shouldIgnoreStatusBar ?? true;
260
+ const call = duration ? EspressoDetoxApi.longPress(x, y, duration, _shouldIgnoreStatusBar): EspressoDetoxApi.longPress(x, y, _shouldIgnoreStatusBar);
261
+ await this.invocationManager.execute(call);
262
+ }
263
+
248
264
  async generateViewHierarchyXml(shouldInjectTestIds) {
249
265
  const hierarchy = await this.invocationManager.execute(DetoxApi.generateViewHierarchyXml(shouldInjectTestIds));
250
266
  return hierarchy.result;
@@ -6,12 +6,17 @@ const _ = require('lodash');
6
6
 
7
7
  const temporaryPath = require('../../../../artifacts/utils/temporaryPath');
8
8
  const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError');
9
+ const XCUITestRunner = require('../../../../ios/XCUITestRunner');
10
+ const { assertTraceDescription } = require('../../../../utils/assertArgument');
9
11
  const getAbsoluteBinaryPath = require('../../../../utils/getAbsoluteBinaryPath');
12
+ const { actionDescription } = require('../../../../utils/invocationTraceDescriptions');
10
13
  const log = require('../../../../utils/logger').child({ cat: 'device' });
11
14
  const pressAnyKey = require('../../../../utils/pressAnyKey');
15
+ const traceInvocationCall = require('../../../../utils/traceInvocationCall').bind(null, log);
12
16
 
13
17
  const IosDriver = require('./IosDriver');
14
18
 
19
+
15
20
  /**
16
21
  * @typedef SimulatorDriverDeps { DeviceDriverDeps }
17
22
  * @property applesimutils { AppleSimUtils }
@@ -40,6 +45,19 @@ class SimulatorDriver extends IosDriver {
40
45
  this._applesimutils = deps.applesimutils;
41
46
  }
42
47
 
48
+ withAction(xcuitestRunner, action, traceDescription, ...params) {
49
+ assertTraceDescription(traceDescription);
50
+
51
+ const invocation = {
52
+ ...(params.length !== 0 && { params }),
53
+ type: 'systemAction',
54
+ ...(this.index !== undefined && { systemAtIndex: this.index }),
55
+ systemAction: action
56
+ };
57
+
58
+ return traceInvocationCall(traceDescription, invocation, xcuitestRunner.execute(invocation));
59
+ }
60
+
43
61
  getExternalId() {
44
62
  return this.udid;
45
63
  }
@@ -111,6 +129,23 @@ class SimulatorDriver extends IosDriver {
111
129
  await this.emitter.emit('terminateApp', { deviceId: udid, bundleId });
112
130
  }
113
131
 
132
+ async tap(point, shouldIgnoreStatusBar, _bundleId) {
133
+ const xcuitestRunner = new XCUITestRunner({ runtimeDevice: { id: this.getExternalId(), _bundleId } });
134
+ let x = point?.x ?? 100;
135
+ let y = point?.y ?? 100;
136
+ const traceDescription = actionDescription.tap({ x, y });
137
+ return this.withAction(xcuitestRunner, 'coordinateTap', traceDescription, x.toString(), y.toString());
138
+ }
139
+
140
+ async longPress(point, pressDuration, shouldIgnoreStatusBar, _bundleId) {
141
+ const xcuitestRunner = new XCUITestRunner({ runtimeDevice: { id: this.getExternalId(), _bundleId } });
142
+ let x = point?.x ?? 100;
143
+ let y = point?.y ?? 100;
144
+ let _pressDuration = pressDuration ? (pressDuration / 1000) : 1;
145
+ const traceDescription = actionDescription.longPress({ x, y }, _pressDuration);
146
+ return this.withAction(xcuitestRunner, 'coordinateLongPress', traceDescription, x.toString(), y.toString(), _pressDuration.toString());
147
+ }
148
+
114
149
  async setBiometricEnrollment(yesOrNo) {
115
150
  await this._applesimutils.setBiometricEnrollment(this.udid, yesOrNo);
116
151
  }
@@ -52,6 +52,14 @@ function assertPoint(point) {
52
52
  throw new DetoxRuntimeError(`point should be an object with x and y properties, but got ${JSON.stringify(point)}`);
53
53
  }
54
54
 
55
+ function assertShouldIgnoreStatusBar(shouldIgnoreStatusBar) {
56
+ if (typeof shouldIgnoreStatusBar === 'boolean') {
57
+ return true;
58
+ }
59
+
60
+ throw new DetoxRuntimeError('shouldIgnoreStatusBar should be a boolean, but got ' + (shouldIgnoreStatusBar + (' (' + (typeof shouldIgnoreStatusBar + ')'))));
61
+ }
62
+
55
63
  function assertUndefined(arg) {
56
64
  if (arg === undefined) {
57
65
  return true;
@@ -76,6 +84,7 @@ module.exports = {
76
84
  assertString,
77
85
  assertDuration,
78
86
  assertPoint,
87
+ assertShouldIgnoreStatusBar,
79
88
  assertUndefined,
80
89
  assertTraceDescription
81
90
  };
@@ -3,6 +3,7 @@ module.exports = {
3
3
  adjustSliderToPosition: (newPosition) => `adjust slider to position ${newPosition}`,
4
4
  clearText: () => 'clear input text',
5
5
  getAttributes: () => 'get element attributes',
6
+ tap: (point) => `tap at ${JSON.stringify(point)}`,
6
7
  longPress: (point, duration) => `long press${duration !== null ? ` for ${duration}ms` : ''}${point !== null ? ` at ${JSON.stringify(point)}` : ''}`,
7
8
  longPressAndDrag: (duration, startX, startY, targetElement, endX, endY, speed, holdDuration) =>
8
9
  `long press and drag from ${startX}, ${startY} to ${endX}, ${endY} with speed ${speed} and hold duration ${holdDuration}`,
@@ -0,0 +1,56 @@
1
+ const { DetoxRuntimeError } = require('../errors');
2
+
3
+ const { assertPoint, assertDuration, assertUndefined, assertShouldIgnoreStatusBar } = require('./assertArgument');
4
+
5
+ function mapDeviceLongPressArguments(optionalAllParams, optionalDurationOrIgnoreStatusBar, optionalIgnoreStatusBar) {
6
+ let point = null;
7
+ let duration = null;
8
+ let shouldIgnoreStatusBar = null;
9
+
10
+ try {
11
+ if (optionalAllParams === undefined) {
12
+ // Do nothing.
13
+ } else if (typeof optionalAllParams === 'number') {
14
+ duration = optionalAllParams;
15
+ if (typeof optionalDurationOrIgnoreStatusBar === 'boolean') {
16
+ shouldIgnoreStatusBar = optionalDurationOrIgnoreStatusBar;
17
+ } else {
18
+ assertUndefined(optionalDurationOrIgnoreStatusBar);
19
+ }
20
+ assertUndefined(optionalIgnoreStatusBar);
21
+ } else if (typeof optionalAllParams === 'boolean') {
22
+ shouldIgnoreStatusBar = optionalAllParams;
23
+ assertUndefined(optionalDurationOrIgnoreStatusBar);
24
+ assertUndefined(optionalIgnoreStatusBar);
25
+ } else {
26
+ assertPoint(optionalAllParams);
27
+ point = optionalAllParams;
28
+
29
+ if (typeof optionalDurationOrIgnoreStatusBar === 'number') {
30
+ assertDuration(optionalDurationOrIgnoreStatusBar);
31
+ duration = optionalDurationOrIgnoreStatusBar;
32
+ } else if (typeof optionalDurationOrIgnoreStatusBar === 'boolean') {
33
+ assertShouldIgnoreStatusBar(optionalDurationOrIgnoreStatusBar);
34
+ shouldIgnoreStatusBar = optionalDurationOrIgnoreStatusBar;
35
+ assertUndefined(optionalIgnoreStatusBar);
36
+ } else if (optionalDurationOrIgnoreStatusBar !== undefined) {
37
+ assertDuration(optionalDurationOrIgnoreStatusBar);
38
+ } else {
39
+ assertUndefined(optionalDurationOrIgnoreStatusBar);
40
+ assertUndefined(optionalIgnoreStatusBar);
41
+ }
42
+
43
+ if (optionalIgnoreStatusBar !== undefined) {
44
+ assertShouldIgnoreStatusBar(optionalIgnoreStatusBar);
45
+ shouldIgnoreStatusBar = optionalIgnoreStatusBar;
46
+ }
47
+ }
48
+ } catch (e) {
49
+ throw new DetoxRuntimeError(`longPress accepts either a duration (number) or a point ({x: number, y: number}) as ` +
50
+ `its first argument, optionally a duration (number) as its second argument, and optionally a ignoreStatusBar (boolean) as its third argument. Error: ${e.message}`);
51
+ }
52
+
53
+ return { point, duration, shouldIgnoreStatusBar };
54
+ }
55
+
56
+ module.exports = mapDeviceLongPressArguments;
@@ -1 +0,0 @@
1
- c517cf62a949c264ce5aa29dbe7eb0b3
@@ -1 +0,0 @@
1
- 0fd30160288562361e27f4dae15e88dd8c063321
@@ -1 +0,0 @@
1
- 43af12e399f647d3c726f03f03c6d5e9953ab473bc565018672c1dd6f9c23fa6
@@ -1 +0,0 @@
1
- 7eb38a3cfc446ee433478167eefe863d2faacfa5b2102685702d83d0b944236d3e4be9d89ad653134402872945bb122db884470d327bf99f7ecd769dacf437bc
@@ -1 +0,0 @@
1
- cf8dd3a0b09c20f6536c73bb9b643c19
@@ -1 +0,0 @@
1
- 087bb87ec12653d348901f95966ed16f46f870b2
@@ -1 +0,0 @@
1
- 77ee8830fdf301ef8a7895575a4f0f3b76db9614b61942979b73d3b5001fac57
@@ -1 +0,0 @@
1
- 3eb1ffc33ecae7c14b6ff36a525abacb4aa05f6d05083d2dc79bad26b63badecc5d22d45bf7dc97cc90804e4245db826e14c86d86fa49540be209708a6f854d1
@@ -1 +0,0 @@
1
- b1fa5d53cea5a7597921246ec09f437f
@@ -1 +0,0 @@
1
- d28ba67e5c3648d25e9c592b78803d33a1805ed8
@@ -1 +0,0 @@
1
- 9563aa36da9534de48b62cdfc7ff3b14782e260a29fbbff7189a220e1d74eaf7
@@ -1 +0,0 @@
1
- ff322f7d952260e532f7678a490b6672136925f9d77420203d6038cde5a38a2fa2af2db6fbe5ba8005fe3668074228efdca4ea8f2f3e8077fc9386ded37e1642
@@ -1 +0,0 @@
1
- 367dd3d7936f459a3d2b84a90a4d4277
@@ -1 +0,0 @@
1
- bc18b82a4beeee1569c3cfae9ccbbd11ab133d21
@@ -1 +0,0 @@
1
- 31152c35e9eac0a236cb5d016c3200ecd5181f7639c376f02d04289a06c76930
@@ -1 +0,0 @@
1
- 7a911e94257c299c2d560fe6d84ddc2d5264bec7953e39ae64bcafbe5041e43ffbb5bffcfd6c04bcbf62e62b4b764e609e35d37d5e4903266ace32fa214a0726
@@ -1 +0,0 @@
1
- 768c2187e364924f92a9b6ccf3c7d0a2
@@ -1 +0,0 @@
1
- 7e17fc60050feb2ea5e7dc9a93bf8b2fba13486d
@@ -1 +0,0 @@
1
- a64ce40bbbf3b827d598206f3829f34072b1d6bcd0115d930e7aabadf7477534
@@ -1 +0,0 @@
1
- 60d8f60ededcfdd91034b452971b511a40a99a5a4afb42ae6fa88261cf5d29b8493b824eb2e8f453653bf723668b417943177f02fb0dcc9d58501563e7c10b54
@@ -1 +0,0 @@
1
- acec75d4c28b08969fcdf94aff6253d3
@@ -1 +0,0 @@
1
- c9739dec1a97e33efb975a87083b618963b98ed7
@@ -1 +0,0 @@
1
- 652c04a47a8ffb644d3e6085070a0b7401f2cb5ab5bc09d5a7a73f21d4a9fb9d
@@ -1 +0,0 @@
1
- 913e879197e5d3622aee6c3db2444ce453912f3ab383bebae3b0fd09e518b8c301e377666a13a3359f841307e895094b5a82e6eafda83e69abd43c3f10eec484