detox 20.27.6 → 20.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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