Repository: wix/Detox
Pull request #2335 (2020-09-15 14:22:44)

fix: minor race condition bugs
- [x] This is a small change 

---

**Description:**

When tests burst into failures on the very beginning in Jasmine, I saw a few errors which had to do with race conditions.
Hence, I decided why don't we fix that, to be on the safe side.

Bug 1 (in the Jest-Jasmine adapter):

```
FAIL  e2e/specs/owner/ChallengesScreen/challengesScreen.spec.ts
  ● Test suite failed to run

    DetoxRuntimeError: Invalid test summary was passed to detox.beforeEach(testSummary)
    Expected to get an object of type: { title: string; fullName: string; status: "running" | "passed" | "failed"; }

    HINT: Maybe you are still using an old undocumented signature detox.beforeEach(string, string, string) in init.js ?
    See the article for the guidance: https://github.com/wix/detox/blob/master/docs/APIRef.TestLifecycle.md

    testSummary was: null

      at Detox._validateTestSummary (../node_modules/detox/src/Detox.js:220:13)
      at Detox.beforeEach (../node_modules/detox/src/Detox.js:113:10)
```

Bug 2 (in the LogArtifacts):

```
detox[10682] WARN:  [ArtifactsManager.js/SUPPRESS_PLUGIN_ERROR] Suppressed error inside function call: SimulatorLogPlugin.onIdleCallback()
  err: TypeError: Cannot read property 'save' of null
      at api.requestIdleCallback (/Users/yaroslavs/Projects/project/node_modules/detox/src/artifacts/templates/plugin/WholeTestRecorderPlugin.js:62:27)
      at process._tickCallback (internal/process/next_tick.js:68:7)
```

I'll reiterate, that happens when the tests are failing in the very beginning, and something fails asynchronously.
The goal is to make logs clearer and less prone to "surprise" crashes.


Patch:
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxMatcher.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxMatcher.java
index 63d9fdf7d..84d0b5f27 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxMatcher.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxMatcher.java
@@ -8,9 +8,11 @@ import androidx.test.espresso.matcher.ViewMatchers.Visibility;
 
 import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
 import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static androidx.test.espresso.matcher.ViewMatchers.isChecked;
 import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+import static androidx.test.espresso.matcher.ViewMatchers.isNotChecked;
 import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
 import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
 import static androidx.test.espresso.matcher.ViewMatchers.withTagValue;
@@ -47,6 +49,10 @@ public class DetoxMatcher {
         return allOf(withTagValue(is((Object) testId)), withEffectiveVisibility(Visibility.VISIBLE));
     }
 
+    public static Matcher<View> matcherForToggleable(boolean value) {
+        return (value ? isChecked() : isNotChecked());
+    }
+
     public static Matcher<View> matcherForAnd(Matcher<View> m1, Matcher<View> m2) {
         return allOf(m1, m2);
     }
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/matcher/ViewMatchers.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/matcher/ViewMatchers.kt
index 81bfd005c..26527d8ff 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/matcher/ViewMatchers.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/matcher/ViewMatchers.kt
@@ -23,7 +23,7 @@ fun isOfClassName(className: String): Matcher<View> {
         // empty
     }
 
-    return object : BaseMatcher<View>() {
+    return object: BaseMatcher<View>() {
         override fun matches(item: Any) = false
         override fun describeTo(description: Description) {
             description.appendText("Class $className not found on classpath. Are you using full class name?")
diff --git a/detox/ios/Detox.xcodeproj/project.pbxproj b/detox/ios/Detox.xcodeproj/project.pbxproj
index 59a6c1124..6bf0c4653 100644
--- a/detox/ios/Detox.xcodeproj/project.pbxproj
+++ b/detox/ios/Detox.xcodeproj/project.pbxproj
@@ -17,6 +17,8 @@
 		392324D72477D87D00A3D119 /* DetoxManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392324D62477D87D00A3D119 /* DetoxManager.swift */; };
 		392324DF24781CBD00A3D119 /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392324DE24781CBD00A3D119 /* WebSocket.swift */; };
 		392324E324782AE500A3D119 /* DetoxInit.m in Sources */ = {isa = PBXBuildFile; fileRef = 392324E124782AE500A3D119 /* DetoxInit.m */; };
+		392487AD25114B1E0024CD61 /* DetoxPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = 392487AB25114B1E0024CD61 /* DetoxPolicy.h */; };
+		392487AE25114B1E0024CD61 /* DetoxPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 392487AC25114B1E0024CD61 /* DetoxPolicy.m */; };
 		3928EFA81E47404900C19B6E /* DetoxUserNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3928EFA71E47404900C19B6E /* DetoxUserNotificationTests.swift */; };
 		3928EFAA1E47404900C19B6E /* Detox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 394767971DBF985400D72256 /* Detox.framework */; };
 		3928EFB51E47571500C19B6E /* TestableAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3928EFB41E47571500C19B6E /* TestableAppDelegate.swift */; };
@@ -260,6 +262,8 @@
 		392324D62477D87D00A3D119 /* DetoxManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetoxManager.swift; sourceTree = "<group>"; };
 		392324DE24781CBD00A3D119 /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
 		392324E124782AE500A3D119 /* DetoxInit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetoxInit.m; sourceTree = "<group>"; };
+		392487AB25114B1E0024CD61 /* DetoxPolicy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DetoxPolicy.h; sourceTree = "<group>"; };
+		392487AC25114B1E0024CD61 /* DetoxPolicy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetoxPolicy.m; sourceTree = "<group>"; };
 		3928EFA51E47404900C19B6E /* DetoxUserNotificationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DetoxUserNotificationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		3928EFA71E47404900C19B6E /* DetoxUserNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetoxUserNotificationTests.swift; sourceTree = "<group>"; };
 		3928EFA91E47404900C19B6E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -418,6 +422,15 @@
 			path = Assertions;
 			sourceTree = "<group>";
 		};
+		392487A425114AB50024CD61 /* Policy */ = {
+			isa = PBXGroup;
+			children = (
+				392487AB25114B1E0024CD61 /* DetoxPolicy.h */,
+				392487AC25114B1E0024CD61 /* DetoxPolicy.m */,
+			);
+			path = Policy;
+			sourceTree = "<group>";
+		};
 		3928EFA61E47404900C19B6E /* DetoxUserNotificationTests */ = {
 			isa = PBXGroup;
 			children = (
@@ -491,6 +504,7 @@
 				3980D12C244C40E2004812DD /* AppleTouchEvents */,
 				39232594247AAAAC00A3D119 /* Assertions */,
 				3980D13C244C44C3004812DD /* Invocation */,
+				392487A425114AB50024CD61 /* Policy */,
 				3980D132244C435B004812DD /* Matchers */,
 				3980D130244C42E7004812DD /* Utilities */,
 				392324E124782AE500A3D119 /* DetoxInit.m */,
@@ -719,6 +733,7 @@
 				39E7AB3E24F40DA10011CE29 /* DTXLogging.h in Headers */,
 				3980D169244DD837004812DD /* UIDatePicker+DetoxActions.h in Headers */,
 				391FA5E91E7FD96D0056F82F /* GREYIdlingResourcePrettyPrint.h in Headers */,
+				392487AD25114B1E0024CD61 /* DetoxPolicy.h in Headers */,
 				39999EBA2416339E004E6366 /* __DTXDeallocSafeProxy.h in Headers */,
 				396EA67A22E0AE7D0087E4D4 /* ___DTXAddressInfo.h in Headers */,
 				39A34C711E30F10D00BEBB59 /* DetoxAppDelegateProxy.h in Headers */,
@@ -1017,6 +1032,7 @@
 				39E7AB3924F40CDE0011CE29 /* fishhook.c in Sources */,
 				39CA978D245B13CB00A7FC43 /* UIDevice+DetoxActions.m in Sources */,
 				46A6A63D1EF697BB00E3AA79 /* GREYConfiguration+Detox.m in Sources */,
+				392487AE25114B1E0024CD61 /* DetoxPolicy.m in Sources */,
 				39CE25AC22197F0900D78AA1 /* DetoxInstrumentsManager.m in Sources */,
 				3980D1162448B52C004812DD /* DTXTouchInfo.m in Sources */,
 				3990BA29245882EF00B608C8 /* DTXAssertionHandler.m in Sources */,
diff --git a/detox/ios/Detox/Policy/DetoxPolicy.h b/detox/ios/Detox/Policy/DetoxPolicy.h
new file mode 100644
index 000000000..88d405eb9
--- /dev/null
+++ b/detox/ios/Detox/Policy/DetoxPolicy.h
@@ -0,0 +1,23 @@
+//
+//  DetoxPolicy.h
+//  Detox
+//
+//  Created by Leo Natan on 9/15/20.
+//  Copyright © 2020 Wix. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface DetoxPolicy : NSObject
+
+@property (class, nonatomic, strong, readonly) DetoxPolicy* activePolicy NS_SWIFT_NAME(active);
+
+@property (nonatomic, readonly) CGFloat visibilityPixelAlphaThreshold;
+@property (nonatomic, readonly) CGFloat visibilityVisiblePixelRatioThreshold;
+@property (nonatomic, copy, readonly) NSString* visibilityVisiblePixelRatioThresholdDescription;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/detox/ios/Detox/Policy/DetoxPolicy.m b/detox/ios/Detox/Policy/DetoxPolicy.m
new file mode 100644
index 000000000..9eb29c52b
--- /dev/null
+++ b/detox/ios/Detox/Policy/DetoxPolicy.m
@@ -0,0 +1,47 @@
+//
+//  DetoxPolicy.m
+//  Detox
+//
+//  Created by Leo Natan on 9/15/20.
+//  Copyright © 2020 Wix. All rights reserved.
+//
+
+#import "DetoxPolicy.h"
+
+static DetoxPolicy* _activePolicy;
+
+@implementation DetoxPolicy
+
++ (DetoxPolicy *)activePolicy
+{
+	static dispatch_once_t onceToken;
+	dispatch_once(&onceToken, ^{
+		_activePolicy = [DetoxPolicy new];
+	});
+	
+	return _activePolicy;
+}
+
+- (CGFloat)visibilityPixelAlphaThreshold
+{
+	return 0.5;
+}
+
+- (CGFloat)visibilityVisiblePixelRatioThreshold
+{
+	return 0.75;
+}
+
+- (NSString *)visibilityVisiblePixelRatioThresholdDescription
+{
+	static NSNumberFormatter* formatter;
+	static dispatch_once_t onceToken;
+	dispatch_once(&onceToken, ^{
+		formatter = [NSNumberFormatter new];
+		formatter.numberStyle = NSNumberFormatterPercentStyle;
+	});
+	
+	return [formatter stringFromNumber:@(self.visibilityVisiblePixelRatioThreshold)];
+}
+
+@end
diff --git a/detox/ios/Detox/Utilities/UIImage+DetoxUtils.h b/detox/ios/Detox/Utilities/UIImage+DetoxUtils.h
index af2e8ec07..bb3d08d49 100644
--- a/detox/ios/Detox/Utilities/UIImage+DetoxUtils.h
+++ b/detox/ios/Detox/Utilities/UIImage+DetoxUtils.h
@@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 @interface UIImage (DetoxUtils)
 
-- (BOOL)dtx_isTransparentEnoughWithThreshold:(CGFloat)threshold;
+- (NSUInteger)dtx_numberOfVisiblePixelsWithThreshold:(CGFloat)threshold totalPixels:(NSUInteger*)totalPixels;
 
 #if DEBUG
 - (void)dtx_saveToDesktop;
diff --git a/detox/ios/Detox/Utilities/UIImage+DetoxUtils.m b/detox/ios/Detox/Utilities/UIImage+DetoxUtils.m
index 8372c718b..4ab9bdf40 100644
--- a/detox/ios/Detox/Utilities/UIImage+DetoxUtils.m
+++ b/detox/ios/Detox/Utilities/UIImage+DetoxUtils.m
@@ -10,7 +10,7 @@
 
 @implementation UIImage (DetoxUtils)
 
-- (BOOL)dtx_isTransparentEnoughWithThreshold:(CGFloat)threshold
+- (NSUInteger)dtx_numberOfVisiblePixelsWithThreshold:(CGFloat)threshold totalPixels:(NSUInteger*)totalPixels
 {
 	CGImageRef cgImage = self.CGImage;
 	
@@ -20,21 +20,27 @@
 	};
 	const UInt8* data = CFDataGetBytePtr(pixelData);
 	
+	NSUInteger visible = 0;
+	NSUInteger total = 0;
+	
 	for (NSUInteger y = 0; y < self.size.height; y++) {
-		double alphaSum = 0.0;
 		for (NSUInteger x = 0; x < self.size.width; x++) {
-			uint8_t alpha = data[((NSUInteger)self.size.width * y + x) * 4 + 3];
-			alphaSum += (alpha / 255.0);
-		}
-		CGFloat avg = alphaSum / self.size.width;
-		
-		if(avg > threshold)
-		{
-			return NO;
+			total++;
+			uint8_t alpha255 = data[((NSUInteger)self.size.width * y + x) * 4 + 3];
+			CGFloat alpha = alpha255 / 255.0;
+			if(alpha < threshold)
+			{
+				visible++;
+			}
 		}
 	}
 	
-	return YES;
+	if(totalPixels != NULL)
+	{
+		*totalPixels = total;
+	}
+	
+	return visible;
 }
 
 #if DEBUG
@@ -46,10 +52,6 @@
 		NSString* somePath = NSHomeDirectory();
 		NSString* userPath = [somePath substringToIndex:[somePath rangeOfString:@"/Library"].location];
 		desktopURL = [[NSURL fileURLWithPath:userPath] URLByAppendingPathComponent:@"Desktop"];
-		
-//		let somePath = NSHomeDirectory()
-//		let userPath = somePath[somePath.startIndex..<somePath.range(of: "/Library")!.lowerBound]
-//		try! LNViewHierarchyDumper.shared.dumpViewHierarchy(to: URL(fileURLWithPath: String(userPath)).appendingPathComponent("Desktop"))
 	});
 	
 	[UIImagePNGRepresentation(self) writeToURL:[desktopURL URLByAppendingPathComponent:@"view.png"] atomically:YES];
diff --git a/detox/ios/Detox/Utilities/UIView+DetoxUtils.h b/detox/ios/Detox/Utilities/UIView+DetoxUtils.h
index 74a2568a3..c6f8f0c8d 100644
--- a/detox/ios/Detox/Utilities/UIView+DetoxUtils.h
+++ b/detox/ios/Detox/Utilities/UIView+DetoxUtils.h
@@ -40,18 +40,11 @@ static double LNLinearInterpolate(CGFloat from, CGFloat to, CGFloat p)
 @property (nonatomic, readonly) CGPoint dtx_accessibilityActivationPoint;
 @property (nonatomic, readonly) CGPoint dtx_accessibilityActivationPointInViewCoordinateSpace;
 
-- (UIImage*)dtx_imageAroundPoint:(CGPoint)point maxSize:(CGSize)maxSize;
-
 @property (nonatomic, readonly, copy) NSDictionary<NSString*, id>* dtx_attributes;
 @property (nonatomic, readonly, copy) NSDictionary<NSString*, id>* dtx_viewDebugAttributes;
 
 @property (nonatomic, readonly, weak) UIViewController* dtx_containingViewController;
 
-#if DEBUG
-- (void)dtx_saveSnapshotToDesktop;
-- (void)dtx_saveSnapshotToDesktopWithPoint:(CGPoint)point;
-#endif
-
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/detox/ios/Detox/Utilities/UIView+DetoxUtils.m b/detox/ios/Detox/Utilities/UIView+DetoxUtils.m
index 3e13b7862..866c4355f 100644
--- a/detox/ios/Detox/Utilities/UIView+DetoxUtils.m
+++ b/detox/ios/Detox/Utilities/UIView+DetoxUtils.m
@@ -11,6 +11,7 @@
 #import "UIWindow+DetoxUtils.h"
 #import "UISlider+DetoxUtils.h"
 #import "UIImage+DetoxUtils.h"
+#import "DetoxPolicy.h"
 
 @interface DTXTouchVisualizerWindow : UIWindow @end
 
@@ -141,17 +142,7 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	return [self hitTest:point withEvent:event];
 }
 
-- (UIView *)dtx_hitVisTest:(CGPoint)point withEvent:(UIEvent *)event lookingFor:(UIView*)lookingFor
-{
-	return [self dtx_visTest:point withEvent:event lookingFor:lookingFor maxSize:CGSizeMake(1, 1)];
-}
-
-- (UIView*)dtx_visTest:(CGPoint)point withEvent:(UIEvent *)event lookingFor:(UIView*)lookingFor
-{
-	return [self dtx_visTest:point withEvent:event lookingFor:lookingFor maxSize:CGSizeMake(44, 44)];
-}
-
-- (UIView*)dtx_visTest:(CGPoint)point withEvent:(UIEvent *)event lookingFor:(UIView*)lookingFor maxSize:(CGSize)maxSize
+- (UIView*)dtx_visTest:(CGPoint)point withEvent:(UIEvent *)event lookingFor:(UIView*)lookingFor limitTestedRegion:(BOOL)limitTestedRegion regionLimit:(CGSize)regionLimit
 {
 	if(self.isHiddenOrHasHiddenAncestor == YES)
 	{
@@ -180,7 +171,7 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	for (__kindof UIView * _Nonnull obj in self.subviews.reverseObjectEnumerator) {
 		CGPoint localPoint = [self convertPoint:point toView:obj];
 
-		UIView* candidate = [obj dtx_visTest:localPoint withEvent:event lookingFor:lookingFor maxSize:maxSize];
+		UIView* candidate = [obj dtx_visTest:localPoint withEvent:event lookingFor:lookingFor limitTestedRegion:limitTestedRegion regionLimit:regionLimit];
 
 		if(candidate == nil)
 		{
@@ -193,12 +184,27 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	
 	if(rv == nil && CGRectGetWidth(self.bounds) > 0 && CGRectGetHeight(self.bounds) > 0)
 	{
-		//Check the candidate view for transparency
-		UIImage* img = [self dtx_imageAroundPoint:point maxSize:CGSizeMake(MIN(lookingFor.bounds.size.width, maxSize.width), MIN(lookingFor.bounds.size.height, maxSize.height))];
-		if([img dtx_isTransparentEnoughWithThreshold:0.5] == NO)
+		CGRect testedRegion;
+		if(limitTestedRegion)
+		{
+			testedRegion = [self _dtx_testRegionAroundPoint:point viewSize:lookingFor.bounds.size hardLimitSize:regionLimit];
+		}
+		else
+		{
+			testedRegion = [self convertRect:lookingFor.bounds fromView:lookingFor];
+		}
+		
+		UIImage* img = [self _dtx_imageInTestedRegion:testedRegion];
+		
+		
+		
+		NSUInteger total;
+		NSUInteger visible = [img dtx_numberOfVisiblePixelsWithThreshold:DetoxPolicy.activePolicy.visibilityPixelAlphaThreshold totalPixels:&total];
+		
+		if([self _dtx_isTestedRegionObscured:testedRegion byView:self withVisiblePixels:visible totalPixels:total ofView:lookingFor])
 		{
 #if DEBUG
-//			[self.window dtx_saveSnapshotToDesktopWithPoint:[self.window convertPoint:point fromView:self]];
+//			[self.window dtx_saveSnapshotToDesktopWithPoint:[self.window convertPoint:point fromView:self] rects:@[@([self.window convertRect:testedRegion fromView:self]), @([self.window convertRect:self.bounds fromView:self])]];
 //			[img dtx_saveToDesktop];
 #endif
 			//If a view is not transparent around the hit point, take it as the visible view.
@@ -214,11 +220,6 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	return [self dtx_isVisibleAtPoint:self.dtx_accessibilityActivationPointInViewCoordinateSpace];
 }
 
-- (BOOL)dtx_isHittable
-{
-	return [self dtx_isHittableAtPoint:self.dtx_accessibilityActivationPointInViewCoordinateSpace];
-}
-
 - (BOOL)dtx_isVisibleAtPoint:(CGPoint)point
 {
 	return [self dtx_isVisibleAtPoint:point error:NULL];
@@ -226,7 +227,12 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 
 - (BOOL)dtx_isVisibleAtPoint:(CGPoint)point error:(NSError* __strong *)error
 {
-	return [self _dtx_someTestAtPoint:point testSelector:@selector(dtx_visTest:withEvent:lookingFor:) error:error];
+	return [self _dtx_someTestAtPoint:point testSelector:@selector(dtx_visTest:withEvent:lookingFor:limitTestedRegion:regionLimit:) limitTestedRegion:NO regionLimit:CGSizeZero error:error];
+}
+
+- (BOOL)dtx_isHittable
+{
+	return [self dtx_isHittableAtPoint:self.dtx_accessibilityActivationPointInViewCoordinateSpace];
 }
 
 - (BOOL)dtx_isHittableAtPoint:(CGPoint)point
@@ -236,49 +242,49 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 
 - (BOOL)dtx_isHittableAtPoint:(CGPoint)point error:(NSError* __strong *)error
 {
-	return [self _dtx_someTestAtPoint:point testSelector:@selector(dtx_hitVisTest:withEvent:lookingFor:) error:error];
+	return [self _dtx_someTestAtPoint:point testSelector:@selector(dtx_visTest:withEvent:lookingFor:limitTestedRegion:regionLimit:) limitTestedRegion:YES regionLimit:CGSizeMake(1, 1) error:error];
 }
 
-- (BOOL)dtx_isActuallyHittableAtPoint:(CGPoint)point error:(NSError* __strong *)error
-{
-	if([self isKindOfClass:NSClassFromString(@"UISegmentLabel")] || [self isKindOfClass:NSClassFromString(@"UISegment")])
-	{
-		UISegmentedControl* segmentControl = (id)self;
-		while(segmentControl != nil && [segmentControl isKindOfClass:UISegmentedControl.class] == NO)
-		{
-			segmentControl = (id)segmentControl.superview;
-		}
-
-		return [segmentControl dtx_isHittableAtPoint:[segmentControl convertPoint:point fromView:self] error:error];
-	}
-	
-	if([self isKindOfClass:NSClassFromString(@"UIButtonLabel")])
-	{
-		UIView* button = (id)self;
-		while(button != nil && [button isKindOfClass:UIButton.class] == NO)
-		{
-			button = (id)button.superview;
-		}
-		
-		if([button isKindOfClass:NSClassFromString(@"_UIModernBarButton")] && button.userInteractionEnabled == NO && [button.superview isKindOfClass:NSClassFromString(@"_UIButtonBarButton")])
-		{
-			button = (id)button.superview;
-		}
-		
-		return [button dtx_isHittableAtPoint:[button convertPoint:point fromView:self] error:error];
-	}
-	
-	if([self isKindOfClass:UILabel.class] && [self.dtx_containingViewController isKindOfClass:UIAlertController.class])
-	{		
-		return YES;
-	}
-	
-	return [self _dtx_someTestAtPoint:point testSelector:@selector(dtx_hitTest:withEvent:lookingFor:) error:error];
-}
+//- (BOOL)_dtx_isActuallyHittableAtPoint:(CGPoint)point error:(NSError* __strong *)error
+//{
+//	if([self isKindOfClass:NSClassFromString(@"UISegmentLabel")] || [self isKindOfClass:NSClassFromString(@"UISegment")])
+//	{
+//		UISegmentedControl* segmentControl = (id)self;
+//		while(segmentControl != nil && [segmentControl isKindOfClass:UISegmentedControl.class] == NO)
+//		{
+//			segmentControl = (id)segmentControl.superview;
+//		}
+//
+//		return [segmentControl dtx_isHittableAtPoint:[segmentControl convertPoint:point fromView:self] error:error];
+//	}
+//
+//	if([self isKindOfClass:NSClassFromString(@"UIButtonLabel")])
+//	{
+//		UIView* button = (id)self;
+//		while(button != nil && [button isKindOfClass:UIButton.class] == NO)
+//		{
+//			button = (id)button.superview;
+//		}
+//
+//		if([button isKindOfClass:NSClassFromString(@"_UIModernBarButton")] && button.userInteractionEnabled == NO && [button.superview isKindOfClass:NSClassFromString(@"_UIButtonBarButton")])
+//		{
+//			button = (id)button.superview;
+//		}
+//
+//		return [button dtx_isHittableAtPoint:[button convertPoint:point fromView:self] error:error];
+//	}
+//
+//	if([self isKindOfClass:UILabel.class] && [self.dtx_containingViewController isKindOfClass:UIAlertController.class])
+//	{
+//		return YES;
+//	}
+//
+//	return [self _dtx_someTestAtPoint:point testSelector:@selector(dtx_hitTest:withEvent:lookingFor:) error:error];
+//}
 
 #define APPLY_PREFIX(...) [NSString stringWithFormat:@"%@ %@", prefix, __VA_ARGS__]
 
-- (BOOL)_dtx_someTestAtPoint:(CGPoint)point testSelector:(SEL)selector error:(NSError* __strong *)error
+- (BOOL)_dtx_someTestAtPoint:(CGPoint)point testSelector:(SEL)selector limitTestedRegion:(BOOL)limitTestedRegion regionLimit:(CGSize)regionLimit error:(NSError* __strong *)error
 {
 	BOOL isHit = (selector == @selector(dtx_hitTest:withEvent:lookingFor:));
 	NSString* prefix = [NSString stringWithFormat:@"View “%@” is not %@ at point “%@”;", self.dtx_shortDescription, isHit ? @"hittable" : @"visible", DTXPointToString(point)];
@@ -290,7 +296,9 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 		return NO;
 	}
 	
-	if(self.window == nil || self.window.screen == nil)
+	UIWindow* windowToUse = [self isKindOfClass:UIWindow.class] ? (id)self : self.window;
+	
+	if(windowToUse == nil || windowToUse.screen == nil)
 	{
 		_DTXPopulateError([NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX(@"Either window or screen are nil")}]);
 		
@@ -299,7 +307,7 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	
 	if(@available(iOS 13.0, *))
 	{
-		if(self.window.windowScene == nil)
+		if(windowToUse.windowScene == nil)
 		{
 			_DTXPopulateError([NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX(@"Window scene is nil")}]);
 			return NO;
@@ -307,19 +315,20 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	}
 	
 	//Point in window coordinate space
-	UIScreen* screen = self.window.screen;
+	UIScreen* screen = windowToUse.screen;
 	CGPoint screenActivationPoint = [self convertPoint:point toCoordinateSpace:screen.coordinateSpace];
-	CGPoint windowActivationPoint = [self.window convertPoint:point fromView:self];
+	CGRect windowTestedRegion;
 	
-	if(CGRectContainsPoint(self.window.bounds, windowActivationPoint) == NO)
+	if(limitTestedRegion == YES)
 	{
-		NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX([NSString stringWithFormat:@"Point “%@” is outside of window bounds", DTXPointToString(windowActivationPoint)])}];
-		_DTXPopulateError(err);
-		
-		return NO;
+		windowTestedRegion = [windowToUse convertRect:[self _dtx_testRegionAroundPoint:point viewSize:self.bounds.size hardLimitSize:regionLimit] fromView:self];
+	}
+	else
+	{
+		windowTestedRegion = [windowToUse convertRect:self.bounds fromView:self];
 	}
 	
-	if(CGRectGetWidth(self.dtx_safeAreaBounds) == 0 || CGRectGetHeight(self.dtx_safeAreaBounds) == 0)
+	if(isHit && (CGRectGetWidth(self.dtx_safeAreaBounds) == 0 || CGRectGetHeight(self.dtx_safeAreaBounds) == 0))
 	{
 		_DTXPopulateError([NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX(@"View safe area bounds are empty")}]);
 		
@@ -334,20 +343,28 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	}
 	
 	__block BOOL rv = NO;
-
+	
 	if(isHit && self.userInteractionEnabled == NO)
 	{
 		_DTXPopulateError([NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX(@"View has user interaction disabled (userInteractionEnabled == NO)")}]);
-
+		
+		return NO;
+	}
+	
+	if([self _dtx_isTestedRegionObscured:windowTestedRegion inWindowBounds:windowToUse.bounds])
+	{
+		NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX([NSString stringWithFormat:@"View “%@” does not pass visibility threshold (%@) within window bounds", self.dtx_shortDescription, DetoxPolicy.activePolicy.visibilityVisiblePixelRatioThresholdDescription])}];
+		_DTXPopulateError(err);
+		
 		return NO;
 	}
 	
-	id (*testFunc)(id, SEL, CGPoint, id, id) = (void*)objc_msgSend;
+	id (*testFunc)(id, SEL, CGPoint, id, id, BOOL, CGSize) = (void*)objc_msgSend;
 	
 	id scene = nil;
 	if(@available(iOS 13.0, *))
 	{
-		scene = self.window.windowScene;
+		scene = windowToUse.windowScene;
 	}
 	
 	[UIWindow dtx_enumerateWindowsInScene:scene usingBlock:^(UIWindow * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
@@ -364,17 +381,30 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 		
 		CGPoint currentWindowActivationPoint = [screen.coordinateSpace convertPoint:screenActivationPoint toCoordinateSpace:obj.coordinateSpace];
 		
-		if(self.window != obj && isHit == NO)
+		if(windowToUse != obj && isHit == NO)
 		{
-			UIImage* windowImage = [obj dtx_imageAroundPoint:currentWindowActivationPoint maxSize:self.window.bounds.size];
-//			[UIImagePNGRepresentation(windowImage) writeToFile:[NSString stringWithFormat:@"/Users/lnatan/Desktop/%@.png", NSStringFromClass(obj.class)] atomically:YES];
-			if([windowImage dtx_isTransparentEnoughWithThreshold:0.5] == NO)
+			CGRect testedRegion;
+			if(limitTestedRegion == NO)
+			{
+				testedRegion = [windowToUse convertRect:self.bounds fromView:self];
+			}
+			else
+			{
+				testedRegion = [windowToUse _dtx_testRegionAroundPoint:point viewSize:windowToUse.bounds.size hardLimitSize:regionLimit];
+			}
+			testedRegion = [obj convertRect:testedRegion fromWindow:windowToUse];
+			UIImage* windowImage = [obj _dtx_imageInTestedRegion:testedRegion];
+			
+			NSUInteger total;
+			NSUInteger visible = [windowImage dtx_numberOfVisiblePixelsWithThreshold:DetoxPolicy.activePolicy.visibilityPixelAlphaThreshold totalPixels:&total];
+			
+			if([self _dtx_isTestedRegionObscured:testedRegion byView:self withVisiblePixels:visible totalPixels:total ofView:windowToUse])
 			{
 #if DEBUG
 //				[windowImage dtx_saveToDesktop];
 #endif
 				
-				NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX([NSString stringWithFormat:@"Window “%@” is above the tested view's window and its transparency around point “%@” is below the tested threshold (0.5)", obj.dtx_shortDescription, DTXPointToString(currentWindowActivationPoint)])}];
+				NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX([NSString stringWithFormat:@"Window “%@” obscures view “%@” above visibility threshold (%@)", obj.dtx_shortDescription, self.dtx_shortDescription, DetoxPolicy.activePolicy.visibilityVisiblePixelRatioThresholdDescription])}];
 				_DTXPopulateError(err);
 				
 				//The window is not transparent at the hit point, stop
@@ -389,9 +419,9 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 			}
 		}
 		
-		UIView* visibleView = testFunc(obj, selector, currentWindowActivationPoint, nil, self);
+		UIView* visibleView = testFunc(obj, selector, currentWindowActivationPoint, nil, self, limitTestedRegion, regionLimit);
 		
-		if(self.window != obj && isHit == YES)
+		if(windowToUse != obj && isHit == YES)
 		{
 			if(visibleView != nil)
 			{
@@ -405,7 +435,7 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 			return;
 		}
 		
-		NSAssert(self.window == obj, @"Detox logic failure!");
+		NSAssert(windowToUse == obj, @"Detox logic failure!");
 		*stop = YES;
 		
 		if(visibleView == self || [visibleView isDescendantOfView:self])
@@ -415,11 +445,11 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 		else
 		{
 			rv = NO;
-			NSString* str = isHit ? @"hittable" : @"visible";
+//			NSString* str = isHit ? @"hittable" : @"visible";
 			
 			if(visibleView == nil)
 			{
-				NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX([NSString stringWithFormat:@"No view is %@ at window point “%@”", str, DTXPointToString(windowActivationPoint)])}];
+				NSError* err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX([NSString stringWithFormat:@"View “%@” not visible", self.dtx_shortDescription])}];
 				_DTXPopulateError(err);
 			}
 			else
@@ -431,7 +461,7 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 				}
 				else
 				{
-					err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX([NSString stringWithFormat:@"View “%@” is above the tested view “%@”'s screen position and its transparency around window point “%@” is below the tested threshold (0.5)", visibleView.dtx_shortDescription, self.dtx_shortDescription, DTXPointToString(currentWindowActivationPoint)])}];
+					err = [NSError errorWithDomain:@"DetoxErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey: APPLY_PREFIX([NSString stringWithFormat:@"View “%@” does not pass visibility threshold (%@)", visibleView.dtx_shortDescription, DetoxPolicy.activePolicy.visibilityVisiblePixelRatioThresholdDescription])}];
 				}
 				_DTXPopulateError(err);
 			}
@@ -443,14 +473,33 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	return rv;
 }
 
-- (UIImage*)dtx_imageAroundPoint:(CGPoint)point maxSize:(CGSize)maxSize
+- (CGRect)_dtx_testRegionAroundPoint:(CGPoint)point viewSize:(CGSize)viewSize hardLimitSize:(CGSize)hardLimitSize
 {
 	static const CGFloat maxDimension = 44;
-	CGFloat width = ceil(MIN(maxDimension, maxSize.width));
-	CGFloat height = ceil(MIN(maxDimension, maxSize.height));
+	
+	return [self _dtx_testRegionAroundPoint:point maxDimension:maxDimension viewSize:viewSize hardLimitSize:hardLimitSize];
+}
+
+- (CGRect)_dtx_testRegionAroundPoint:(CGPoint)point maxDimension:(CGFloat)maxDimension viewSize:(CGSize)viewSize hardLimitSize:(CGSize)hardLimitSize
+{
+	CGSize maxSize = CGSizeMake(MIN(viewSize.width, hardLimitSize.width), MIN(viewSize.height, hardLimitSize.height));
+	
+	CGFloat width = floor(MIN(maxDimension, maxSize.width));
+	CGFloat height = floor(MIN(maxDimension, maxSize.height));
 	CGFloat x = point.x - width / 2.0;
 	CGFloat y = point.y - height / 2.0;
 	
+	return CGRectMake(x, y, width, height);
+}
+
+- (UIImage*)_dtx_imageInTestedRegion:(CGRect)testedRegion
+{
+//	UIGraphicsBeginImageContext(testedRegion.size);
+//	CGContextTranslateCTM(UIGraphicsGetCurrentContext(), -testedRegion.origin.x, -testedRegion.origin.y);
+//	[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:NO];
+//	UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
+//	UIGraphicsEndImageContext();
+	
 	CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
 	if (colorSpace == NULL)
 	{
@@ -459,8 +508,8 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	dtx_defer {
 		CGColorSpaceRelease(colorSpace);
 	};
-	
-	CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
+
+	CGContextRef context = CGBitmapContextCreate(NULL, ceil(testedRegion.size.width), ceil(testedRegion.size.height), 8, ceil(testedRegion.size.width) * 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
 	if(context == NULL)
 	{
 		return nil;
@@ -468,25 +517,31 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	dtx_defer {
 		CGContextRelease(context);
 	};
-	
-	UIGraphicsPushContext(context);
-	
-	CGContextTranslateCTM(context, -x, -y);
-	
+
+	CGContextTranslateCTM(context, -testedRegion.origin.x, -testedRegion.origin.y);
 	[self.layer.presentationLayer renderInContext:context];
-	
-	UIGraphicsPopContext();
-	
+
 	CGImageRef imageRef = CGBitmapContextCreateImage(context);
 	dtx_defer {
 		CGImageRelease(imageRef);
 	};
-	
+
 	UIImage* image = [UIImage imageWithCGImage:imageRef];
 	
 	return image;
 }
 
+- (BOOL)_dtx_isTestedRegionObscured:(CGRect)testedRegion byView:(UIView*)view withVisiblePixels:(NSUInteger)visible totalPixels:(NSUInteger)total ofView:(UIView*)lookingFor
+{
+	return (visible / (double)total) < DetoxPolicy.activePolicy.visibilityVisiblePixelRatioThreshold;
+}
+
+- (BOOL)_dtx_isTestedRegionObscured:(CGRect)testedRegion inWindowBounds:(CGRect)windowBounds
+{
+	CGRect intersection = CGRectIntersection(windowBounds, testedRegion);
+	return (intersection.size.width * intersection.size.height) / (testedRegion.size.width * testedRegion.size.height) < DetoxPolicy.activePolicy.visibilityVisiblePixelRatioThreshold;
+}
+
 - (NSDictionary<NSString *,id> *)dtx_attributes
 {
 	NSMutableDictionary* rv = [NSMutableDictionary new];
@@ -597,15 +652,15 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 #if DEBUG
 - (void)dtx_saveSnapshotToDesktop
 {
-	[self _dtx_saveSnapshotToDesktopWithPointPtr:NULL];
+	[self _dtx_saveSnapshotToDesktopWithPointPtr:NULL rects:nil];
 }
 
-- (void)dtx_saveSnapshotToDesktopWithPoint:(CGPoint)point
+- (void)dtx_saveSnapshotToDesktopWithPoint:(CGPoint)point rects:(NSArray*)rects
 {
-	[self _dtx_saveSnapshotToDesktopWithPointPtr:&point];
+	[self _dtx_saveSnapshotToDesktopWithPointPtr:&point rects:rects];
 }
 
-- (void)_dtx_saveSnapshotToDesktopWithPointPtr:(CGPoint*)pointPtrOrNULL
+- (void)_dtx_saveSnapshotToDesktopWithPointPtr:(CGPoint*)pointPtrOrNULL rects:(NSArray*)rects
 {
 	UIWindow* windowToUse = [self isKindOfClass:UIWindow.class] ? (id)self : self.window;
 	UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, windowToUse.screen.scale);
@@ -614,10 +669,19 @@ BOOL __DTXPointEqualToPoint(CGPoint a, CGPoint b)
 	if(pointPtrOrNULL != NULL)
 	{
 		CGContextRef ctx = UIGraphicsGetCurrentContext();
-		[UIColor.systemRedColor setFill];
-		CGContextFillRect(ctx, CGRectMake(pointPtrOrNULL->x, pointPtrOrNULL->y, 1, 1));
+		[UIColor.blackColor setFill];
+		CGContextFillRect(ctx, CGRectMake(pointPtrOrNULL->x - 0.5, pointPtrOrNULL->y - 0.5, 1, 1));
 	}
 	
+	[rects enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+		CGRect rect = [obj CGRectValue];
+		
+		CGContextRef ctx = UIGraphicsGetCurrentContext();
+		[UIColor.blackColor setStroke];
+		CGContextSetLineWidth(ctx, 1.0);
+		CGContextStrokeRect(ctx, rect);
+	}];
+	
 	UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
 	UIGraphicsEndImageContext();
 	
diff --git a/detox/local-cli/utils/splitArgv.js b/detox/local-cli/utils/splitArgv.js
index fe2fc34d7..a83ccb9f8 100644
--- a/detox/local-cli/utils/splitArgv.js
+++ b/detox/local-cli/utils/splitArgv.js
@@ -30,7 +30,7 @@ function disengageBooleanArgs(argv, booleanKeys) {
 
   for (const entry of Object.entries(argv)) {
     const [key, value] = entry;
-    if (key === '_') {
+    if (key === '_' || key === '--') {
       continue;
     }
 
@@ -46,7 +46,7 @@ function disengageBooleanArgs(argv, booleanKeys) {
   return {
     specs: passthrough.concat(argv._),
     passthrough: {
-      _: [],
+      _: argv['--'] || [],
       ...result,
     },
   };
diff --git a/detox/package.json b/detox/package.json
index 725d6f5de..978fa9bbd 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "17.5.3",
+  "version": "17.5.6",
   "bin": {
     "detox": "local-cli/cli.js"
   },
diff --git a/detox/runners/jest/DetoxAdapterImpl.js b/detox/runners/jest/DetoxAdapterImpl.js
index 963a63d17..ae30f3e28 100644
--- a/detox/runners/jest/DetoxAdapterImpl.js
+++ b/detox/runners/jest/DetoxAdapterImpl.js
@@ -22,8 +22,10 @@ class DetoxAdapterImpl {
       throw new DetoxRuntimeError(this._describeInitError());
     }
 
+    const currentTest = this._currentTest;
+
     await this._flush();
-    await this.detox.beforeEach(this._currentTest);
+    await this.detox.beforeEach(currentTest);
   }
 
   async afterAll() {
diff --git a/detox/src/android/espressoapi/DetoxMatcher.js b/detox/src/android/espressoapi/DetoxMatcher.js
index 6dc617741..a5b35af3e 100644
--- a/detox/src/android/espressoapi/DetoxMatcher.js
+++ b/detox/src/android/espressoapi/DetoxMatcher.js
@@ -50,6 +50,21 @@ class DetoxMatcher {
     };
   }
 
+  static matcherForToggleable(value) {
+    if (typeof value !== "boolean") throw new Error("value should be a boolean, but got " + (value + (" (" + (typeof value + ")"))));
+    return {
+      target: {
+        type: "Class",
+        value: "com.wix.detox.espresso.DetoxMatcher"
+      },
+      method: "matcherForToggleable",
+      args: [{
+        type: "boolean",
+        value: value
+      }]
+    };
+  }
+
   static matcherForAnd(m1, m2) {
     return {
       target: {
diff --git a/detox/src/android/expect.js b/detox/src/android/expect.js
index dbfdad1b9..82f02eb9a 100644
--- a/detox/src/android/expect.js
+++ b/detox/src/android/expect.js
@@ -2,22 +2,24 @@ const fs = require('fs-extra');
 const path = require('path');
 const tempfile = require('tempfile');
 const invoke = require('../invoke');
-const matchers = require('./matcher');
 const DetoxActionApi = require('./espressoapi/DetoxAction');
 const ViewActionsApi = require('./espressoapi/ViewActions');
 const DetoxViewActionsApi = require('./espressoapi/DetoxViewActions');
 const DetoxAssertionApi = require('./espressoapi/DetoxAssertion');
 const EspressoDetoxApi = require('./espressoapi/EspressoDetox');
 const DetoxMatcherApi = require('./espressoapi/DetoxMatcher');
-const Matcher = matchers.Matcher;
-const LabelMatcher = matchers.LabelMatcher;
-const IdMatcher = matchers.IdMatcher;
-const TypeMatcher = matchers.TypeMatcher;
-const TraitsMatcher = matchers.TraitsMatcher;
-const VisibleMatcher = matchers.VisibleMatcher;
-const ExistsMatcher = matchers.ExistsMatcher;
-const TextMatcher = matchers.TextMatcher;
-const ValueMatcher = matchers.ValueMatcher;
+const {
+  Matcher,
+  LabelMatcher,
+  IdMatcher,
+  TypeMatcher,
+  TraitsMatcher,
+  VisibleMatcher,
+  ExistsMatcher,
+  TextMatcher,
+  ValueMatcher,
+  ToggleMatcher,
+} = require('./matcher');
 
 function call(maybeAFunction) {
   return maybeAFunction instanceof Function ? maybeAFunction() : maybeAFunction;
@@ -360,6 +362,10 @@ class ExpectElement extends Expect {
   async toNotHaveValue(value) {
     return await this.not.toHaveValue(value);
   }
+
+  async toHaveToggleValue(value) {
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new ToggleMatcher(value).not : new ToggleMatcher(value)).execute()
+  }
 }
 
 class WaitFor {
diff --git a/detox/src/android/matcher.js b/detox/src/android/matcher.js
index 49e544666..b12ef44e5 100644
--- a/detox/src/android/matcher.js
+++ b/detox/src/android/matcher.js
@@ -92,6 +92,13 @@ class ValueMatcher extends Matcher {
   }
 }
 
+class ToggleMatcher extends Matcher {
+  constructor(toggleState) {
+    super();
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForToggleable(toggleState));
+  }
+}
+
 // TODO
 // Please be aware, that this is just a dummy matcher
 class TraitsMatcher extends Matcher {
@@ -112,5 +119,6 @@ module.exports = {
   VisibleMatcher,
   ExistsMatcher,
   TextMatcher,
-  ValueMatcher
+  ValueMatcher,
+  ToggleMatcher,
 };
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index 70c668192..d253915c3 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -60,6 +60,10 @@ class Expect {
     return this.expect('toHaveSliderPosition', position, tolerance);
   }
 
+  toHaveToggleValue(value) {
+    return this.toHaveValue(`${Number(value)}`);
+  }
+
   get not() {
     this.modifiers.push('not');
     return this;
diff --git a/docs/APIRef.Expect.md b/docs/APIRef.Expect.md
index 78f1d2f37..75cbb06b3 100644
--- a/docs/APIRef.Expect.md
+++ b/docs/APIRef.Expect.md
@@ -13,6 +13,7 @@ Use [actions](APIRef.ActionsOnElement.md) to simulate use interaction with eleme
 - [`.toHaveId()`](#tohaveidid)
 - [`.toHaveValue()`](#tohavevaluevalue)
 - [`.toHaveSliderPosition()`](#tohavesliderpositionnormalizedposition-tolerance--ios-only) **iOS only**
+- [`.toHaveToggleValue()`](#tohavetogglevaluevalue)
 - [`.not`](#not)
 - [`.withTimeout()`](#withtimeouttimeout)
 
@@ -73,6 +74,15 @@ await expect(element(by.id('slider'))).toHaveSliderPosition(0.75);
 await expect(element(by.id('slider'))).toHaveSliderPosition(0.3113, 0.00001);
 ```
 
+### `toHaveToggleValue(value)`
+
+Expects a toggle-able element (e.g. a Switch or a Check-Box) to be on/checked or off/unchecked. As a reference, in react-native, this is the [equivalent switch component](https://reactnative.dev/docs/switch).
+
+```js
+await expect(element(by.id('switch'))).toHaveToggleValue(true);
+await expect(element(by.id('checkbox'))).toHaveToggleValue(false);
+```
+
 ### `not`
 
 Negates the expectation.
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index b798576b3..5b9c8affa 100644
--- a/examples/demo-native-android/package.json
+++ b/examples/demo-native-android/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-demo-native-android",
-  "version": "17.5.3",
+  "version": "17.5.6",
   "private": true,
   "scripts": {
     "packager": "react-native start",
@@ -8,7 +8,7 @@
     "e2e": "mocha e2e --opts ./e2e/mocha.opts"
   },
   "devDependencies": {
-    "detox": "^17.5.3",
+    "detox": "^17.5.6",
     "mocha": "^6.1.3"
   },
   "detox": {}
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index 875844f44..68178bf53 100644
--- a/examples/demo-native-ios/package.json
+++ b/examples/demo-native-ios/package.json
@@ -1,9 +1,9 @@
 {
   "name": "detox-demo-native-ios",
-  "version": "17.5.3",
+  "version": "17.5.6",
   "private": true,
   "devDependencies": {
-    "detox": "^17.5.3",
+    "detox": "^17.5.6",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index 76a10eb93..522a87991 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -1,12 +1,12 @@
 {
   "name": "demo-plugin",
-  "version": "17.5.3",
+  "version": "17.5.6",
   "private": true,
   "scripts": {
     "test:plugin": "detox test --configuration plugin -l verbose"
   },
   "devDependencies": {
-    "detox": "^17.5.3",
+    "detox": "^17.5.6",
     "jest": "26.x.x"
   },
   "detox": {
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index 40bff95de..7028eedde 100644
--- a/examples/demo-react-native-detox-instruments/package.json
+++ b/examples/demo-react-native-detox-instruments/package.json
@@ -1,10 +1,10 @@
 {
   "name": "demo-react-native-detox-instruments",
-  "version": "17.5.3",
+  "version": "17.5.6",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^17.5.3",
+    "detox": "^17.5.6",
     "mocha": "6.x.x"
   },
   "detox": {
diff --git a/examples/demo-react-native-jest/package.json b/examples/demo-react-native-jest/package.json
index 6aa12f4f7..485b66616 100644
--- a/examples/demo-react-native-jest/package.json
+++ b/examples/demo-react-native-jest/package.json
@@ -1,6 +1,6 @@
 {
   "name": "demo-react-native-jest",
-  "version": "17.5.3",
+  "version": "17.5.6",
   "private": true,
   "scripts": {
     "test:ios-release": "detox test --configuration ios.sim.release -l verbose",
@@ -9,7 +9,7 @@
     "test:android-release-ci": "detox test --configuration android.emu.release -l verbose --workers 2 --headless --record-logs all --take-screenshots all"
   },
   "devDependencies": {
-    "detox": "^17.5.3",
+    "detox": "^17.5.6",
     "jest": "26.x.x",
     "jest-circus": "26.x.x",
     "sanitize-filename": "^1.6.1"
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index a007388e7..d9cecbbe0 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "17.5.3",
+  "version": "17.5.6",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -22,7 +22,7 @@
     "react-native": "0.62.x"
   },
   "devDependencies": {
-    "detox": "^17.5.3",
+    "detox": "^17.5.6",
     "mocha": "^6.1.3"
   }
 }
diff --git a/lerna.json b/lerna.json
index 9dea7e9c2..b106a2e73 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,7 +13,7 @@
     "generation",
     "."
   ],
-  "version": "17.5.3",
+  "version": "17.5.6",
   "npmClient": "npm",
   "command": {
     "publish": {
diff --git a/package.json b/package.json
index ba26a8b85..ceb9e3142 100644
--- a/package.json
+++ b/package.json
@@ -18,5 +18,5 @@
     "semver": "5.x.x",
     "shell-utils": "1.x.x"
   },
-  "version": "17.5.3"
+  "version": "17.5.6"
 }


Test patch:
diff --git a/detox/local-cli/test.test.js b/detox/local-cli/test.test.js
index 67dd3c756..40959d754 100644
--- a/detox/local-cli/test.test.js
+++ b/detox/local-cli/test.test.js
@@ -344,8 +344,8 @@ describe('CLI', () => {
       cp.execSync.mockImplementation(() => { throw new Error; });
 
       await run(`-R 1 tests -- --debug`).catch(_.noop);
-      expect(cliCall(0).command).toMatch(/ --debug tests$/);
-      expect(cliCall(1).command).toMatch(/ --debug tests\/failing.test.js$/);
+      expect(cliCall(0).command).toMatch(/ --debug .* tests$/);
+      expect(cliCall(1).command).toMatch(/ --debug .* tests\/failing.test.js$/);
     });
 
     test.each([['-r'], ['--reuse']])('%s <value> should be passed as environment variable', async (__reuse) => {
@@ -574,10 +574,17 @@ describe('CLI', () => {
 
     test('-- <...explicitPassthroughArgs> should be forwarded to the test runner CLI as-is', async () => {
       await run('--device-launch-args detoxArgs e2eFolder -- a -a --a --device-launch-args runnerArgs');
-      expect(cliCall().command).toMatch(/a -a --a --device-launch-args runnerArgs e2eFolder$/);
+      expect(cliCall().command).toMatch(/a -a --a --device-launch-args runnerArgs .* e2eFolder$/);
       expect(cliCall().env).toEqual(expect.objectContaining({ deviceLaunchArgs: 'detoxArgs' }));
     });
 
+    test('-- <...explicitPassthroughArgs> should omit double-dash "--" itself, when forwarding args', async () => {
+      await run('./detox -- --forceExit');
+
+      expect(cliCall().command).toMatch(/ --forceExit .* \.\/detox$/);
+      expect(cliCall().command).not.toMatch(/ -- --forceExit .* \.\/detox$/);
+    });
+
     test('--inspect-brk should prepend "node --inspect-brk" to the command', async () => {
       await run('--inspect-brk');
       expect(cliCall().command).toMatch(RegExp(`^node --inspect-brk ${testRunner}`));
diff --git a/detox/src/android/expect.test.js b/detox/src/android/expect.test.js
index 7cfbecfa7..a3f5369a6 100644
--- a/detox/src/android/expect.test.js
+++ b/detox/src/android/expect.test.js
@@ -38,6 +38,8 @@ describe('expect', () => {
     await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveValue('value');
     await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotHaveValue('value');
     await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveValue('value');
+    await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveToggleValue(true);
+    await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveToggleValue(true);
   });
 
   it(`element by label (for backwards compat)`, async () => {
@@ -49,6 +51,8 @@ describe('expect', () => {
     await e.expect(e.element(e.by.label('test'))).toHaveLabel('label');
     await e.expect(e.element(e.by.label('test'))).toHaveId('id');
     await e.expect(e.element(e.by.label('test'))).toHaveValue('value');
+    await e.expect(e.element(e.by.label('test'))).toHaveToggleValue(false);
+    await e.expect(e.element(e.by.label('test'))).not.toHaveToggleValue(false);
   });
 
   it(`element by id`, async () => {
@@ -119,10 +123,9 @@ describe('expect', () => {
     await e.waitFor(e.element(e.by.id('id'))).toNotHaveValue('value');
     await e.waitFor(e.element(e.by.id('id'))).not.toHaveValue('value');
 
-
-
     await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50, 'down');
     await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50);
+
   });
 
   it(`waitFor (element) with wrong parameters should throw`, async () => {
diff --git a/detox/src/artifacts/templates/plugin/WholeTestRecorderPlugin.js b/detox/src/artifacts/templates/plugin/WholeTestRecorderPlugin.js
index 4541ec1c9..de6d2e332 100644
--- a/detox/src/artifacts/templates/plugin/WholeTestRecorderPlugin.js
+++ b/detox/src/artifacts/templates/plugin/WholeTestRecorderPlugin.js
@@ -23,12 +23,13 @@ class WholeTestRecorderPlugin extends ArtifactPlugin {
     await super.onTestDone(testSummary);
 
     if (this.testRecording) {
-      await this.testRecording.stop();
+      const testRecording = this.testRecording;
+      await testRecording.stop();
 
       if (this.shouldKeepArtifactOfTest(testSummary)) {
-        this._startSavingTestRecording(this.testRecording, testSummary)
+        this._startSavingTestRecording(testRecording, testSummary)
       } else {
-        this._startDiscardingTestRecording(this.testRecording);
+        this._startDiscardingTestRecording(testRecording);
       }
 
       this.testRecording = null;
diff --git a/detox/src/ios/expectTwo.test.js b/detox/src/ios/expectTwo.test.js
index a7fd6e0e7..c1e882f5b 100644
--- a/detox/src/ios/expectTwo.test.js
+++ b/detox/src/ios/expectTwo.test.js
@@ -300,6 +300,23 @@ describe('expectTwo', () => {
     expect(testCall).deepEquals(jsonOutput);
   });
 
+  it(`expect(element(by.id('switch'))).toHaveToggleValue(value)`, () => {
+    const testCall = e.expect(e.element(e.by.id('switch'))).toHaveToggleValue(true);
+    const jsonOutput = {
+      'invocation': {
+        'type': 'expectation',
+        'predicate': {
+          'type': 'id',
+          'value': 'switch'
+        },
+        'expectation': 'toHaveValue',
+        'params': ['1']
+      }
+    };
+
+    expect(testCall).deepEquals(jsonOutput);
+  });
+
   it(`waitFor(element(by.text('Text5'))).toBeNotVisible().whileElement(by.id('ScrollView630')).scroll(50, 'down')`, () => {
     const testCall = e.waitFor(e.element(e.by.text('Text5'))).toBeNotVisible().whileElement(e.by.id('ScrollView630')).scroll(50, 'down');
     const jsonOutput = {
diff --git a/detox/test/e2e/04.assertions.test.js b/detox/test/e2e/04.assertions.test.js
index f1c63c92f..7264bb737 100644
--- a/detox/test/e2e/04.assertions.test.js
+++ b/detox/test/e2e/04.assertions.test.js
@@ -41,4 +41,11 @@ describe('Assertions', () => {
     await element(by.id('UniqueId146')).tap();
     await expect(element(by.id('UniqueId146'))).toHaveValue('1');
   });
+
+  it('assert toggle-switch widget', async () => {
+    await expect(element(by.id('UniqueId146'))).toHaveToggleValue(false);
+    await element(by.id('UniqueId146')).tap();
+    await expect(element(by.id('UniqueId146'))).toHaveToggleValue(true);
+    await expect(element(by.id('UniqueId146'))).not.toHaveToggleValue(false);
+  });
 });
diff --git a/detox/test/package.json b/detox/test/package.json
index bbe954944..664370248 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "17.5.3",
+  "version": "17.5.6",
   "private": true,
   "engines": {
     "node": ">=8.3.0"
@@ -30,7 +30,7 @@
   },
   "devDependencies": {
     "@babel/core": "^7.8.7",
-    "detox": "^17.5.3",
+    "detox": "^17.5.6",
     "express": "^4.15.3",
     "jest": "26.x.x",
     "jest-circus": "26.x.x",
@@ -68,7 +68,7 @@
       "plugins": {
         "log": "all",
         "screenshot": {
-          "shouldTakeAutomaticSnapshots": true,
+          "shouldTakeAutomaticSnapshots": false,
           "takeWhen": {
             "testDone": true
           }
diff --git a/detox/test/src/Screens/AssertionsScreen.js b/detox/test/src/Screens/AssertionsScreen.js
index 3ba907a51..baeb83f23 100644
--- a/detox/test/src/Screens/AssertionsScreen.js
+++ b/detox/test/src/Screens/AssertionsScreen.js
@@ -2,9 +2,23 @@ import React, { Component } from 'react';
 import {
   Text,
   View,
-  Switch
+  Switch,
 } from 'react-native';
 
+class TestSwitchWidget extends Switch {
+  constructor(props) {
+    super(props);
+    this.state = {
+      value: false
+    }
+    this.onChange = () => this.setState({ value: !this.state.value });
+  }
+
+  render() {
+    return <Switch testID={this.props.testID} onChange={this.onChange} value={this.state.value}/>
+  }
+}
+
 export default class AssertionsScreen extends Component {
 
   constructor(props) {
@@ -17,17 +31,10 @@ export default class AssertionsScreen extends Component {
   render() {
     return (
       <View style={{flex: 1, paddingTop: 20, justifyContent: 'center', alignItems: 'center'}}>
-
         <Text testID='UniqueId204' style={{marginBottom: 20}} accessibilityLabel={'I contain some text'}>I contain some text</Text>
-
         <Text testID='UniqueId205' style={{marginBottom: 20, position: 'absolute', left: -200}}>I am not visible</Text>
-
-        <Switch testID='UniqueId146'
-          onValueChange={(value) => this.setState({switchValue: value})}
-          value={this.state.switchValue} />
-
+        <TestSwitchWidget testID='UniqueId146'/>
       </View>
     );
   }
-
 }