Repository: wix/Detox
Pull request #1473 (2019-06-24 15:40:30)

Fail test when waitFor().withTimeout() reaches timeout and assertion fails
Also removed no longer needed comments in docs regarding the behavior of waitFor().

LeoNatan: It wasn't "very hard". 😉
LeoNatan: Related to #1463 , #1471

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 000000000..9aef1d569
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,6 @@
+version: 2
+jobs:
+  build:
+    branches:
+      only:
+        - TravisCITest
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 599eb3a35..0dbd37a96 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
 # Change Log
 
+## [12.11.3](https://github.com/wix/Detox/tree/12.11.3) (2019-06-25)
+[Full Changelog](https://github.com/wix/Detox/compare/12.11.2...12.11.3)
+
+**Enhancements**
+
+- Table of contents in documentation [\#1449](https://github.com/wix/Detox/issues/1449)
+- Upgrade to RN 0.59.9 to support Xcode 11 [\#1472](https://github.com/wix/Detox/pull/1472) ([LeoNatan](https://github.com/LeoNatan))
+
+**Fixed Bugs**
+
+- \(Android\) optimize waitFor\(\) + toBeNotVisible\(\) + withTimeout\(\) combo [\#1463](https://github.com/wix/Detox/issues/1463)
+
+**Closed Issues**
+
+- Can't find a simulator to match with... [\#1469](https://github.com/wix/Detox/issues/1469)
+- Allow disabling of post install with env var [\#1338](https://github.com/wix/Detox/issues/1338)
+- Detox tests hang on detox.init\(\) [\#1288](https://github.com/wix/Detox/issues/1288)
+
+**Merged Pull Requests**
+
+- \(Android\) Improve reporting of 'adb devices' for \#1427 [\#1474](https://github.com/wix/Detox/pull/1474) ([d4vidi](https://github.com/d4vidi))
+- \(Android\) Fix a unidentified non-visibility matching when using waitFor\(\) [\#1471](https://github.com/wix/Detox/pull/1471) ([d4vidi](https://github.com/d4vidi))
+- Update Guide.Expo.md [\#1470](https://github.com/wix/Detox/pull/1470) ([yaron1m](https://github.com/yaron1m))
+- Updating Guide.Mocking with Metro breaking changes [\#1440](https://github.com/wix/Detox/pull/1440) ([antonabyzov](https://github.com/antonabyzov))
+- feat: allow disabling of post install with env var [\#1347](https://github.com/wix/Detox/pull/1347) ([noomorph](https://github.com/noomorph))
+
 ## [12.11.2](https://github.com/wix/Detox/tree/12.11.2) (2019-06-21)
 [Full Changelog](https://github.com/wix/Detox/compare/12.11.1...12.11.2)
 
diff --git a/detox/ios/Detox/DTXMethodInvocation.m b/detox/ios/Detox/DTXMethodInvocation.m
index fc617beeb..5754bfca2 100644
--- a/detox/ios/Detox/DTXMethodInvocation.m
+++ b/detox/ios/Detox/DTXMethodInvocation.m
@@ -15,58 +15,58 @@
 
 + (id)getTarget:(id)param onError:(void (^)(NSString*))onError
 {
-    if (param == nil) return nil;
-    if ([param isKindOfClass:[NSDictionary class]])
-    {
-        NSDictionary *p = (NSDictionary*)param;
-        NSString *type = [DTXMethodInvocation getString:[p objectForKey:@"type"]];
-        id value = [p objectForKey:@"value"];
-        return [DTXMethodInvocation getValue:value withType:type onError:onError];
-    }
-    return nil;
+	if (param == nil) return nil;
+	if ([param isKindOfClass:[NSDictionary class]])
+	{
+		NSDictionary *p = (NSDictionary*)param;
+		NSString *type = [DTXMethodInvocation getString:[p objectForKey:@"type"]];
+		id value = [p objectForKey:@"value"];
+		return [DTXMethodInvocation getValue:value withType:type onError:onError];
+	}
+	return nil;
 }
 
 + (NSString*)getString:(id)param
 {
-    if (param == nil) return nil;
-    if ([param isKindOfClass:[NSString class]]) return param;
-    return nil;
+	if (param == nil) return nil;
+	if ([param isKindOfClass:[NSString class]]) return param;
+	return nil;
 }
 
 + (id)getValue:(id)value withType:(id)type onError:(void (^)(NSString*))onError
 {
-    if (type == nil || value == nil) return nil;
-    if ([type isEqualToString:@"EarlGrey"])
-    {
-        return EarlGrey;
-    }
-    if ([type isEqualToString:@"Class"])
-    {
-        if (![value isKindOfClass:[NSString class]]) return nil;
-        return NSClassFromString(value);
-    }
-    if ([type isEqualToString:@"NSString"])
-    {
-        if (![value isKindOfClass:[NSString class]]) return nil;
-        return value;
-    }
-    if ([type isEqualToString:@"NSNumber"])
-    {
-        if (![value isKindOfClass:[NSNumber class]]) return nil;
-        return value;
-    }
-    if ([type isEqualToString:@"NSInteger"])
-    {
-        if (![value isKindOfClass:[NSNumber class]]) return nil;
-        NSInteger v = [value integerValue];
-        return [NSNumber numberWithInteger:v];
-    }
-    if ([type isEqualToString:@"CGFloat"])
-    {
-        if (![value isKindOfClass:[NSNumber class]]) return @(NAN);
-        CGFloat v = [value doubleValue];
-        return [NSNumber numberWithDouble:v];
-    }
+	if (type == nil || value == nil) return nil;
+	if ([type isEqualToString:@"EarlGrey"])
+	{
+		return EarlGrey;
+	}
+	if ([type isEqualToString:@"Class"])
+	{
+		if (![value isKindOfClass:[NSString class]]) return nil;
+		return NSClassFromString(value);
+	}
+	if ([type isEqualToString:@"NSString"])
+	{
+		if (![value isKindOfClass:[NSString class]]) return nil;
+		return value;
+	}
+	if ([type isEqualToString:@"NSNumber"])
+	{
+		if (![value isKindOfClass:[NSNumber class]]) return nil;
+		return value;
+	}
+	if ([type isEqualToString:@"NSInteger"])
+	{
+		if (![value isKindOfClass:[NSNumber class]]) return nil;
+		NSInteger v = [value integerValue];
+		return [NSNumber numberWithInteger:v];
+	}
+	if ([type isEqualToString:@"CGFloat"])
+	{
+		if (![value isKindOfClass:[NSNumber class]]) return @(NAN);
+		CGFloat v = [value doubleValue];
+		return [NSNumber numberWithDouble:v];
+	}
 	if ([type isEqualToString:@"CGPoint"])
 	{
 		if (![value isKindOfClass:[NSDictionary class]]) return nil;
@@ -75,169 +75,176 @@
 		CGFloat y = [[v objectForKey:@"y"] floatValue];
 		return [NSValue valueWithCGPoint:CGPointMake(x, y)];
 	}
-    if ([type isEqualToString:@"CGRect"])
-    {
-        if (![value isKindOfClass:[NSDictionary class]]) return nil;
-        NSDictionary *v = (NSDictionary*)value;
-        CGFloat x = [[v objectForKey:@"x"] floatValue];
-        CGFloat y = [[v objectForKey:@"y"] floatValue];
-        CGFloat width = [[v objectForKey:@"width"] floatValue];
-        CGFloat height = [[v objectForKey:@"height"] floatValue];
-        return [NSValue valueWithCGRect:CGRectMake(x, y, width, height)];
-    }
-    if ([type isEqualToString:@"Invocation"])
-    {
-        if (![value isKindOfClass:[NSDictionary class]]) return nil;
-        return [DTXMethodInvocation invoke:value onError:onError];
-    }
-    return nil;
+	if ([type isEqualToString:@"CGRect"])
+	{
+		if (![value isKindOfClass:[NSDictionary class]]) return nil;
+		NSDictionary *v = (NSDictionary*)value;
+		CGFloat x = [[v objectForKey:@"x"] floatValue];
+		CGFloat y = [[v objectForKey:@"y"] floatValue];
+		CGFloat width = [[v objectForKey:@"width"] floatValue];
+		CGFloat height = [[v objectForKey:@"height"] floatValue];
+		return [NSValue valueWithCGRect:CGRectMake(x, y, width, height)];
+	}
+	if ([type isEqualToString:@"Invocation"])
+	{
+		if (![value isKindOfClass:[NSDictionary class]]) return nil;
+		return [DTXMethodInvocation invoke:value onError:onError];
+	}
+	return nil;
 }
 
 + (id)getArgValue:(id)param onError:(void (^)(NSString*))onError
 {
-    if (param == nil) return nil;
-    if ([param isKindOfClass:[NSDictionary class]])
-    {
-        NSDictionary *p = (NSDictionary*)param;
-        NSString *type = [DTXMethodInvocation getString:[p objectForKey:@"type"]];
-        id value = [p objectForKey:@"value"];
-        return [DTXMethodInvocation getValue:value withType:type onError:onError];
-    }
+	if (param == nil) return nil;
+	if ([param isKindOfClass:[NSDictionary class]])
+	{
+		NSDictionary *p = (NSDictionary*)param;
+		NSString *type = [DTXMethodInvocation getString:[p objectForKey:@"type"]];
+		id value = [p objectForKey:@"value"];
+		return [DTXMethodInvocation getValue:value withType:type onError:onError];
+	}
 	
-    return param;
+	return param;
 }
 
 + (NSArray*)getArray:(id)param
 {
-    if (param == nil) return nil;
-    if ([param isKindOfClass:[NSArray class]]) return param;
-    return nil;
+	if (param == nil) return nil;
+	if ([param isKindOfClass:[NSArray class]]) return param;
+	return nil;
 }
 
 + (id)getReturnValue:(NSInvocation*)invocation
 {
-    id res = nil;
-    NSString *type = [NSString stringWithUTF8String:invocation.methodSignature.methodReturnType];
-    if ([type isEqualToString:@"v"]) return nil;
-    if ([type isEqualToString:@"@"])
-    {
-        id __unsafe_unretained tempResultSet;
-        [invocation getReturnValue:&tempResultSet];
-        res = tempResultSet;
-        return res;
-    }
-    NSUInteger length = [[invocation methodSignature] methodReturnLength];
-    void *buffer = (void *)malloc(length);
-    [invocation getReturnValue:buffer];
-    if ([type isEqualToString:@"{CGPoint=dd}"])
-    {
-        res = [NSValue valueWithCGPoint:*(CGPoint*)buffer];
-    }
-    if ([type isEqualToString:@"{CGRect={CGPoint=dd}{CGSize=dd}}"])
-    {
-        res = [NSValue valueWithCGRect:*(CGRect*)buffer];
-    }
-    free(buffer);
-    return res;
+	id res = nil;
+	NSString *type = [NSString stringWithUTF8String:invocation.methodSignature.methodReturnType];
+	if ([type isEqualToString:@"v"]) return nil;
+	if ([type isEqualToString:@"@"])
+	{
+		id __unsafe_unretained tempResultSet;
+		[invocation getReturnValue:&tempResultSet];
+		res = tempResultSet;
+		return res;
+	}
+	if([type isEqualToString:@"B"])
+	{
+		BOOL rv;
+		[invocation getReturnValue:&rv];
+		res = [NSNumber numberWithBool:rv];
+		return res;
+	}
+	NSUInteger length = [[invocation methodSignature] methodReturnLength];
+	void *buffer = (void *)malloc(length);
+	[invocation getReturnValue:buffer];
+	if([type isEqualToString:@"{CGPoint=dd}"])
+	{
+		res = [NSValue valueWithCGPoint:*(CGPoint*)buffer];
+	}
+	else if([type isEqualToString:@"{CGRect={CGPoint=dd}{CGSize=dd}}"])
+	{
+		res = [NSValue valueWithCGRect:*(CGRect*)buffer];
+	}
+	free(buffer);
+	return res;
 }
 
 + (id)serializeValue:(id)value onError:(void (^)(NSString*))onError
 {
-    if (value == nil) return nil;
-    if ([value isKindOfClass:[NSValue class]])
-    {
-        NSValue *v = (NSValue*)value;
-        NSString *type = [NSString stringWithUTF8String:v.objCType];
-        if ([type isEqualToString:@"{CGPoint=dd}"])
-        {
-            CGPoint p = [value CGPointValue];
-            return @{@"x": @(p.x), @"y": @(p.y)};
-        }
-        if ([type isEqualToString:@"{CGRect={CGPoint=dd}{CGSize=dd}}"])
-        {
-            CGRect r = [value CGRectValue];
-            return @{@"x": @(r.origin.x), @"y": @(r.origin.y), @"width": @(r.size.width), @"height": @(r.size.height)};
-        }
-    }
-    return value;
+	if (value == nil) return nil;
+	if ([value isKindOfClass:[NSValue class]])
+	{
+		NSValue *v = (NSValue*)value;
+		NSString *type = [NSString stringWithUTF8String:v.objCType];
+		if ([type isEqualToString:@"{CGPoint=dd}"])
+		{
+			CGPoint p = [value CGPointValue];
+			return @{@"x": @(p.x), @"y": @(p.y)};
+		}
+		if ([type isEqualToString:@"{CGRect={CGPoint=dd}{CGSize=dd}}"])
+		{
+			CGRect r = [value CGRectValue];
+			return @{@"x": @(r.origin.x), @"y": @(r.origin.y), @"width": @(r.size.width), @"height": @(r.size.height)};
+		}
+	}
+	return value;
 }
 
 + (void)invocation:(NSInvocation*)invocation setNonPointerArg:(NSValue*)value atIndex:(NSInteger)idx
 {
-    NSString *type = [NSString stringWithUTF8String:value.objCType];
-    if ([type isEqualToString:@"q"])
-    {
-        NSInteger v = [(NSNumber*)value integerValue];
-        [invocation setArgument:&v atIndex:idx];
-    }
-    if ([type isEqualToString:@"d"])
-    {
-        CGFloat v = [(NSNumber*)value doubleValue];
-        [invocation setArgument:&v atIndex:idx];
-    }
-    if ([type isEqualToString:@"{CGPoint=dd}"])
-    {
-        CGPoint v = [value CGPointValue];
-        [invocation setArgument:&v atIndex:idx];
-    }
-    if ([type isEqualToString:@"{CGRect={CGPoint=dd}{CGSize=dd}}"])
-    {
-        CGRect v = [value CGRectValue];
-        [invocation setArgument:&v atIndex:idx];
-    }
+	NSString *type = [NSString stringWithUTF8String:value.objCType];
+	if ([type isEqualToString:@"q"])
+	{
+		NSInteger v = [(NSNumber*)value integerValue];
+		[invocation setArgument:&v atIndex:idx];
+	}
+	if ([type isEqualToString:@"d"])
+	{
+		CGFloat v = [(NSNumber*)value doubleValue];
+		[invocation setArgument:&v atIndex:idx];
+	}
+	if ([type isEqualToString:@"{CGPoint=dd}"])
+	{
+		CGPoint v = [value CGPointValue];
+		[invocation setArgument:&v atIndex:idx];
+	}
+	if ([type isEqualToString:@"{CGRect={CGPoint=dd}{CGSize=dd}}"])
+	{
+		CGRect v = [value CGRectValue];
+		[invocation setArgument:&v atIndex:idx];
+	}
 }
 
 + (id)invoke:(NSDictionary*)params onError:(void (^)(NSString*))onError
 {
-    id target = [DTXMethodInvocation getTarget:[params objectForKey:@"target"] onError:onError];
-    if (target == nil)
-    {
-        onError(@"target is invalid");
-        return nil;
-    }
-    NSString *method = [DTXMethodInvocation getString:[params objectForKey:@"method"]];
-    if (method == nil)
-    {
-        onError(@"method is not a string");
-        return nil;
-    }
-    NSArray *args = [DTXMethodInvocation getArray:[params objectForKey:@"args"]];
-    if (args == nil)
-    {
-        onError(@"args is not an array");
-        return nil;
-    }
-    NSString *selector = method;
-    SEL s = NSSelectorFromString(selector);
-    NSMethodSignature *signature = [target methodSignatureForSelector:s];
-    if (signature == nil)
-    {
-        onError([NSString stringWithFormat:@"selector %@ not found on class %@", selector, [target class]]);
-        return nil;
-    }
-    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
-    invocation.target = target;
-    [invocation setSelector:s];
-    for (int i = 0; i<[args count]; i++)
-    {
-        id arg = args[i];
-        id argValue = [DTXMethodInvocation getArgValue:arg onError:onError];
-        if (argValue == nil)
-        {
-            onError([NSString stringWithFormat:@"invalid arg value %d", i]);
-            return nil;
-        }
-        if (![argValue isKindOfClass:[NSValue class]])
-        {
-            [invocation setArgument:&argValue atIndex:i + 2];
-        }
-        else
-        {
-            [DTXMethodInvocation invocation:invocation setNonPointerArg:argValue atIndex:i + 2];
-        }
-    }
-    [invocation invoke];
-    return [DTXMethodInvocation getReturnValue:invocation];
+	id target = [DTXMethodInvocation getTarget:[params objectForKey:@"target"] onError:onError];
+	if (target == nil)
+	{
+		onError(@"target is invalid");
+		return nil;
+	}
+	NSString *method = [DTXMethodInvocation getString:[params objectForKey:@"method"]];
+	if (method == nil)
+	{
+		onError(@"method is not a string");
+		return nil;
+	}
+	NSArray *args = [DTXMethodInvocation getArray:[params objectForKey:@"args"]];
+	if (args == nil)
+	{
+		onError(@"args is not an array");
+		return nil;
+	}
+	NSString *selector = method;
+	SEL s = NSSelectorFromString(selector);
+	NSMethodSignature *signature = [target methodSignatureForSelector:s];
+	if (signature == nil)
+	{
+		onError([NSString stringWithFormat:@"selector %@ not found on class %@", selector, [target class]]);
+		return nil;
+	}
+	NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
+	invocation.target = target;
+	[invocation setSelector:s];
+	for (int i = 0; i<[args count]; i++)
+	{
+		id arg = args[i];
+		id argValue = [DTXMethodInvocation getArgValue:arg onError:onError];
+		if (argValue == nil)
+		{
+			onError([NSString stringWithFormat:@"invalid arg value %d", i]);
+			return nil;
+		}
+		if (![argValue isKindOfClass:[NSValue class]])
+		{
+			[invocation setArgument:&argValue atIndex:i + 2];
+		}
+		else
+		{
+			[DTXMethodInvocation invocation:invocation setNonPointerArg:argValue atIndex:i + 2];
+		}
+	}
+	[invocation invoke];
+	return [DTXMethodInvocation getReturnValue:invocation];
 }
 
 @end
diff --git a/detox/ios/Detox/GREYCondition+Detox.m b/detox/ios/Detox/GREYCondition+Detox.m
index 4c3f5f314..68e6d23b2 100644
--- a/detox/ios/Detox/GREYCondition+Detox.m
+++ b/detox/ios/Detox/GREYCondition+Detox.m
@@ -7,24 +7,27 @@
 //
 
 #import "GREYCondition+Detox.h"
+#import <EarlGrey/GREYError.h>
+
+GREYError* _dtx_elementMatcherError;
 
 @implementation GREYCondition (Detox)
 
 + (instancetype)detoxConditionForElementMatched:(GREYElementInteraction*)interaction
 {
     return [self conditionWithName:@"Wait for element Detox Condition" block:^BOOL{
-        NSError *error;
-        [interaction assertWithMatcher:grey_notNil() error:&error];
-        return (error == nil);
+		_dtx_elementMatcherError = nil;
+        [interaction assertWithMatcher:grey_notNil() error:&_dtx_elementMatcherError];
+        return (_dtx_elementMatcherError == nil);
     }];
 }
 
 + (instancetype)detoxConditionForNotElementMatched:(GREYElementInteraction*)interaction
 {
     return [self conditionWithName:@"Wait for not element Detox Condition" block:^BOOL{
-        NSError *error;
-        [interaction assertWithMatcher:grey_nil() error:&error];
-        return (error == nil);
+		_dtx_elementMatcherError = nil;
+        [interaction assertWithMatcher:grey_nil() error:&_dtx_elementMatcherError];
+        return (_dtx_elementMatcherError == nil);
     }];
 }
 
diff --git a/detox/package.json b/detox/package.json
index 21e132937..05ab29d4e 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "12.11.2",
+  "version": "12.11.3",
   "bin": {
     "detox": "local-cli/cli.js"
   },
@@ -104,7 +104,8 @@
       "src/android/espressoapi",
       "appdatapath.js",
       ".test.js",
-      ".mock.js"
+      ".mock.js",
+	  "src/ios/expect.js"
     ],
     "resetMocks": true,
     "resetModules": true,
diff --git a/detox/src/devices/android/ADB.js b/detox/src/devices/android/ADB.js
index 1f0b6cd17..6ccb7e701 100644
--- a/detox/src/devices/android/ADB.js
+++ b/detox/src/devices/android/ADB.js
@@ -66,6 +66,10 @@ class ADB {
       let device;
       if (this.isEmulator(deviceAdbName)) {
         const port = _.split(deviceAdbName, '-')[1];
+        if (!port) {
+          _reportTelnetPortResolutionError(input, devicesList, deviceAdbName, port);
+        }
+
         const telnet = new EmulatorTelnet();
         await telnet.connect(port);
         const name = await telnet.avdName();
@@ -103,7 +107,7 @@ class ADB {
 
   async install(deviceId, apkPath) {
     apkPath = `"${escape.inQuotedString(apkPath)}"`;
-    
+
     const apiLvl = await this.apiLevel(deviceId);
 
     let childProcess;
@@ -283,4 +287,20 @@ class ADB {
   }
 }
 
+function _reportTelnetPortResolutionError(input, devicesList, deviceAdbName, port) {
+  const {encodeBase64} = require('../../utils/encoding');
+  const errorMessage = [
+    `Failed to determine telnet port for emulator device '${deviceAdbName}'!`,
+    `Please help us out by reporting dump below in: https://github.com/wix/Detox/issues/1427`,
+    `------ BEGIN DUMP ------`,
+    `adb devices base64: ${encodeBase64(input)}`,
+    `adb devices: ${input}`,
+    `devicesList: ${devicesList}`,
+    `port: ${port}`,
+    `------  END DUMP  ------`,
+  ].join('\n');
+
+  throw new Error(errorMessage);
+}
+
 module.exports = ADB;
diff --git a/detox/src/ios/expect.js b/detox/src/ios/expect.js
index 39487afe1..8375d8b82 100644
--- a/detox/src/ios/expect.js
+++ b/detox/src/ios/expect.js
@@ -282,10 +282,14 @@ class Element {
   _selectElementWithMatcher(matcher) {
     if (!(matcher instanceof Matcher)) throw new Error(`Element _selectElementWithMatcher argument must be a valid Matcher, got ${typeof matcher}`);
     this._call = invoke.call(invoke.EarlGrey.instance, 'detox_selectElementWithMatcher:', matcher._call);
+    if(this._atIndex !== undefined) {
+      this.atIndex(this._atIndex);
+    }
   }
   atIndex(index) {
     if (typeof index !== 'number') throw new Error(`Element atIndex argument must be a number, got ${typeof index}`);
     const _originalCall = this._call;
+    this._atIndex = index;
     this._call = invoke.call(_originalCall, 'atIndex:', invoke.IOS.NSInteger(index));
     return this;
   }
diff --git a/docs/APIRef.ActionsOnElement.md b/docs/APIRef.ActionsOnElement.md
index 0dbdc8f9f..044578c8d 100644
--- a/docs/APIRef.ActionsOnElement.md
+++ b/docs/APIRef.ActionsOnElement.md
@@ -20,9 +20,9 @@ Actions are functions that emulate user behavior. They are being performed on ma
 - [`.scroll()`](#scrollpixels-direction)
 - [`.scrollTo()`](#scrolltoedge)
 - [`.swipe()`](#swipedirection-speed-percentage)
-- [`.setColumnToValue()`](#setcolumntovaluecolumnvalue--ios-only) **iOS only**
-- [`.pinchWithAngle()`](#pinchWithAngle--ios-only) **iOS only**
-- [`.setDatePickerDate()`](#setdatepickerdate--ios-only) **iOS only**
+- [`.setColumnToValue()`](#setcolumntovaluecolumn-value--ios-only) **iOS only**
+- [`.pinchWithAngle()`](#pinchwithangledirection-speed-angle--ios-only) **iOS only**
+- [`.setDatePickerDate()`](#setdatepickerdatedatestring-dateformat--ios-only) **iOS only**
 
 
 ### `tap()`
@@ -144,6 +144,8 @@ await element(by.type('UIPickerView')).setColumnToValue(1,"6");
 await element(by.type('UIPickerView')).setColumnToValue(2,"34");
 ```
 
+>  **Note:** When working with date pickers, you should always set an explicit locale when launching your app in order to prevent flakiness from different date and time styles. See [here](https://github.com/wix/Detox/blob/master/docs/APIRef.DeviceObjectAPI.md#9-launch-with-a-specific-language-ios-only) for more information.
+
 ### `pinchWithAngle(direction, speed, angle)`  iOS only
 
 direction - inward/outward
diff --git a/docs/APIRef.waitFor.md b/docs/APIRef.waitFor.md
index c3be43ad3..decbf9505 100644
--- a/docs/APIRef.waitFor.md
+++ b/docs/APIRef.waitFor.md
@@ -21,60 +21,54 @@ Test async code with waitFor.
 >NOTE: `waitFor` will not throw when reaching timeout, instead it will just continue to the next line. To make sure your tests work as you expect them to add `expect()` at the following line.  
 
 ### `toBeVisible()`
-Test will hang until expectation is met or a timeout has occurred. Should be accompanied with [`expect.toBeVisible()`](APIRef.Expect.md#tobevisible)
+Test will hang until expectation is met or a timeout has occurred.
 Wait for the view to be at least 75% visible.
 
 ```js
 await waitFor(element(by.id('UniqueId204'))).toBeVisible().withTimeout(2000);
-await expect(element(by.id('UniqueId204'))).toBeVisible();
 ```
 
 ### `toBeNotVisible()`
-Test will hang until expectation is met or a timeout has occurred. Should be accompanied with [`expect.toBeNotVisible()`](APIRef.Expect.md#tobenotvisible)
+Test will hang until expectation is met or a timeout has occurred.
 Wait for the view to not be visible.
 
 ```js
 await waitFor(element(by.id('UniqueId205'))).toBeNotVisible().withTimeout(2000);
-await expect(element(by.id('UniqueId205'))).toBeNotVisible();
 ```
 
 ### `toExist()`
-Test will hang until expectation is met or a timeout has occurred. Should be accompanied with [`expect.toExist()`](APIRef.Expect.md#toexist)
+Test will hang until expectation is met or a timeout has occurred.
 Wait for the view to exist in the UI hierarchy.
 
 ```js
 await waitFor(element(by.id('UniqueId205'))).toExist().withTimeout(2000);
-await expect(element(by.id('UniqueId205'))).toExist();
 ```
 
 ### `toNotExist()`
-Test will hang until expectation is met or a timeout has occurred. Should be accompanied with [`expect.toNotExist()`](APIRef.Expect.md#tonotexist)
+Test will hang until expectation is met or a timeout has occurred.
 Wait for the view to not exist in the UI hierarchy.
 
 ```js
 await waitFor(element(by.id('RandomJunk959'))).toNotExist().withTimeout(2000);
-await expect(element(by.id('RandomJunk959'))).toNotExist();
 ```
 
 ### `toHaveText(text)`
-Test will hang until expectation is met or a timeout has occurred. Should be accompanied with [`expect.toHaveText(text)`](APIRef.Expect.md#tohavetexttext)
+Test will hang until expectation is met or a timeout has occurred.
 - In React Native apps, expect UI component of type `<Text>` to have text.
 - In native iOS apps, expect UI elements of type UIButton, UILabel, UITextField or UITextViewIn to have inputText with text.
 
 ```js
 await waitFor(element(by.id('UniqueId204'))).toHaveText('I contain some text').withTimeout(2000);
-await expect(element(by.id('UniqueId204'))).toHaveText('I contain some text');
 ```
 
 ### `toHaveValue(value)`
-Test will hang until expectation is met or a timeout has occurred. Should be accompanied with [`expect.toHaveValue(value)`](APIRef.Expect.md#tohavevaluevalue)
+Test will hang until expectation is met or a timeout has occurred.
 
 - In React Native apps, expect UI component to have [`testID`](https://facebook.github.io/react-native/docs/view.html#testid) with that id.
 - In native iOS apps, expect UI element to have accesibilityIdentifier with that id.
 
 ```js
 await waitFor(element(by.id('uniqueId'))).toHaveValue('Some value').withTimeout(2000);
-await expect(element(by.id('uniqueId'))).toHaveValue('Some value');
 ```
 
 
@@ -83,14 +77,12 @@ Waits for the condition to be met until the specified time (millis) have elapsed
 
 ```js
 await waitFor(element(by.id('UniqueId336'))).toExist().withTimeout(2000);
-await expect(element(by.id('UniqueId336'))).toExist();
 ```
 
 
 ### `whileElement(element)`
 Performs the action repeatedly on the element until an expectation is met.
- 
+
 ```js
 await waitFor(element(by.text('Text5'))).toBeVisible().whileElement(by.id('ScrollView630')).scroll(50, 'down');
-await expect(element(by.text('Text5'))).toBeVisible();
 ```
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index 3139be9bf..f2882314e 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": "12.11.2",
+  "version": "12.11.3",
   "private": true,
   "scripts": {
     "packager": "react-native start",
@@ -8,7 +8,7 @@
     "e2e": "mocha e2e --opts ./e2e/mocha.opts"
   },
   "devDependencies": {
-    "detox": "^12.11.2",
+    "detox": "^12.11.3",
     "mocha": "^6.1.3"
   },
   "detox": {}
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index dc67b3317..fb1a9bac5 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": "12.11.2",
+  "version": "12.11.3",
   "private": true,
   "devDependencies": {
-    "detox": "^12.11.2",
+    "detox": "^12.11.3",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index 814cd9b05..ab3170e12 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": "12.11.2",
+  "version": "12.11.3",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^12.11.2",
+    "detox": "^12.11.3",
     "mocha": "6.x.x"
   },
   "detox": {
diff --git a/examples/demo-react-native-jest/package.json b/examples/demo-react-native-jest/package.json
index 66b68f1fa..fede40327 100644
--- a/examples/demo-react-native-jest/package.json
+++ b/examples/demo-react-native-jest/package.json
@@ -1,12 +1,12 @@
 {
   "name": "demo-react-native-jest",
-  "version": "12.11.2",
+  "version": "12.11.3",
   "private": true,
   "scripts": {
     "test:android-release": "detox test --configuration android.emu.release -l verbose"
   },
   "devDependencies": {
-    "detox": "^12.11.2",
+    "detox": "^12.11.3",
     "jest": "24.x.x"
   },
   "detox": {
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index 7f2e71569..550b01853 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "12.11.2",
+  "version": "12.11.3",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -20,7 +20,7 @@
     "react-native": "0.59.8"
   },
   "devDependencies": {
-    "detox": "^12.11.2",
+    "detox": "^12.11.3",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/lerna.json b/lerna.json
index 475cb2a55..8a042ac7b 100644
--- a/lerna.json
+++ b/lerna.json
@@ -11,7 +11,7 @@
     "examples/demo-react-native-jest",
     "generation"
   ],
-  "version": "12.11.2",
+  "version": "12.11.3",
   "npmClient": "npm",
   "command": {
     "publish": {


Test patch:
diff --git a/detox/ios/Detox/TestRunner.m b/detox/ios/Detox/TestRunner.m
index 802ed7d97..1e3f71941 100644
--- a/detox/ios/Detox/TestRunner.m
+++ b/detox/ios/Detox/TestRunner.m
@@ -9,6 +9,7 @@
 #import "TestRunner.h"
 #import "DTXMethodInvocation.h"
 #import "TestFailureHandler.h"
+#import <EarlGrey/GREYError.h>
 
 @interface TestRunner()
 
@@ -22,59 +23,70 @@
 
 - (void)initEarlGrey
 {
-    [EarlGrey setFailureHandler:self.failureHandler];
-    [[GREYConfiguration sharedInstance] setValue:@(NO) forConfigKey:kGREYConfigKeyAnalyticsEnabled];
-    //[[GREYConfiguration sharedInstance] setValue:@".*localhost.*" forConfigKey:kGREYConfigKeyURLBlacklistRegex];
+	[EarlGrey setFailureHandler:self.failureHandler];
+	[[GREYConfiguration sharedInstance] setValue:@(NO) forConfigKey:kGREYConfigKeyAnalyticsEnabled];
+	//[[GREYConfiguration sharedInstance] setValue:@".*localhost.*" forConfigKey:kGREYConfigKeyURLBlacklistRegex];
 }
 
 - (void)cleanupEarlGrey
 {
-    // this triggers grey_tearDown in GREYAutomationSetup
-    [[NSNotificationCenter defaultCenter] postNotificationName:@"GREYXCTestCaseInstanceDidFinish"
-                                                        object:self
-                                                      userInfo:nil];
+	// this triggers grey_tearDown in GREYAutomationSetup
+	[[NSNotificationCenter defaultCenter] postNotificationName:@"GREYXCTestCaseInstanceDidFinish"
+														object:self
+													  userInfo:nil];
 }
 
 - (instancetype)init
 {
-    self = [super init];
-    if (self == nil) return nil;
-    
-    self.failureHandler = [[TestFailureHandler alloc] init];
-    self.failureHandler.delegate = self;
-    self.currentMessageId = nil;
-    [self initEarlGrey];
-    
-    return self;
+	self = [super init];
+	if (self == nil) return nil;
+	
+	self.failureHandler = [[TestFailureHandler alloc] init];
+	self.failureHandler.delegate = self;
+	self.currentMessageId = nil;
+	[self initEarlGrey];
+	
+	return self;
 }
 
 - (void)cleanup
 {
-    [self cleanupEarlGrey];
+	[self cleanupEarlGrey];
 }
 
 - (void)onTestFailed:(NSString *)details {
-    if (self.currentMessageId != nil)
-    {
-        if (self.delegate) [self.delegate testRunnerOnTestFailed:details withMessageId:self.currentMessageId];
-        self.currentMessageId = nil;
-    }
+	if (self.currentMessageId != nil)
+	{
+		if (self.delegate) [self.delegate testRunnerOnTestFailed:details withMessageId:self.currentMessageId];
+		self.currentMessageId = nil;
+	}
 }
 
+extern GREYError* _dtx_elementMatcherError;
+
 - (void)invoke:(NSDictionary*)params withMessageId:(NSNumber *)messageId
 {
 	self.currentMessageId = messageId;
-    grey_execute_async(^{
-        id res = [DTXMethodInvocation invoke:params onError:^(NSString *error)
-        {
-            if (self.delegate) [self.delegate testRunnerOnError:error withMessageId:messageId];
-        }];
-        if (self.currentMessageId != nil)
-        {
-            if (self.delegate) [self.delegate testRunnerOnInvokeResult:res withMessageId:messageId];
-            self.currentMessageId = nil;
-        }
-    });
+	grey_execute_async(^{
+		id res = [DTXMethodInvocation invoke:params onError:^(NSString *error)
+				  {
+					  if (self.delegate) [self.delegate testRunnerOnError:error withMessageId:messageId];
+				  }];
+		
+		NSString* currentSelector = [params objectForKey:@"method"];
+		
+		if([currentSelector isEqualToString:@"waitWithTimeout:"] && [res boolValue] == NO)
+		{
+			if (self.delegate) [self.delegate testRunnerOnTestFailed:[NSString stringWithFormat:@"%@\n\nElement matcher: %@", _dtx_elementMatcherError.localizedDescription, _dtx_elementMatcherError.errorInfo[@"Element Matcher"]] withMessageId:messageId];
+			_dtx_elementMatcherError = nil;
+		}
+		else if (self.currentMessageId != nil)
+		{
+			if (self.delegate) [self.delegate testRunnerOnInvokeResult:res withMessageId:messageId];
+		}
+		
+		self.currentMessageId = nil;
+	});
 }
 
 /*
@@ -94,6 +106,6 @@
  
  });
  
-*/
+ */
 
 @end
diff --git a/detox/src/devices/android/ADB.test.js b/detox/src/devices/android/ADB.test.js
index 4759e7987..42e3846c3 100644
--- a/detox/src/devices/android/ADB.test.js
+++ b/detox/src/devices/android/ADB.test.js
@@ -1,9 +1,11 @@
+
 describe('ADB', () => {
   const mockAndroidSDKPath = '/Android/sdk-mock';
+  const adbBinPath = `${mockAndroidSDKPath}/platform-tools/adb`;
 
+  let mockEmulatorTelnet;
   let ADB;
   let adb;
-  let EmulatorTelnet;
   let exec;
 
   beforeEach(() => {
@@ -12,9 +14,23 @@ describe('ADB', () => {
       getAndroidSDKPath: () => mockAndroidSDKPath,
     }));
 
-    jest.mock('./EmulatorTelnet');
-    EmulatorTelnet = require('./EmulatorTelnet');
+    jest.mock('../../utils/encoding', () => ({
+      encodeBase64: (text) => `base64(${text})`,
+    }));
 
+    mockEmulatorTelnet = {
+      connect: jest.fn(),
+      quit: jest.fn(),
+      avdName: jest.fn(),
+    };
+    class MockEmulatorTelnet {
+      constructor() {
+        this.connect = mockEmulatorTelnet.connect;
+        this.quit = mockEmulatorTelnet.quit;
+        this.avdName = mockEmulatorTelnet.avdName;
+      }
+    }
+    jest.mock('./EmulatorTelnet', () => MockEmulatorTelnet);
 
     jest.mock('../../utils/exec', () => {
       const exec = jest.fn();
@@ -27,10 +43,46 @@ describe('ADB', () => {
     adb = new ADB();
   });
 
-  it(`devices`, async () => {
-    await adb.devices();
-    expect(exec).toHaveBeenCalledWith(`${mockAndroidSDKPath}/platform-tools/adb  devices`, { verbosity: 'high' }, undefined, 1);
-    expect(exec).toHaveBeenCalledTimes(1);
+  describe('devices', () => {
+    it(`should invoke ADB`, async () => {
+      await adb.devices();
+      expect(exec).toHaveBeenCalledWith(`${adbBinPath}  devices`, { verbosity: 'high' }, undefined, 1);
+      expect(exec).toHaveBeenCalledTimes(1);
+    });
+
+    it(`should parse emulator devices`, async () => {
+      exec.mockReturnValue({
+        stdout: 'List of devices attached\nemulator-5554\tdevice\nemulator-5555\tdevice\n'
+      });
+
+      await adb.devices();
+      expect(mockEmulatorTelnet.connect).toHaveBeenCalledWith("5554");
+    });
+
+    it(`should abort if port can't be parsed`, async () => {
+      const adbDevicesResult = 'List of devices attached\nemulator-\tdevice\n';
+      exec.mockReturnValue({
+        stdout: adbDevicesResult
+      });
+
+      try {
+        await adb.devices();
+        fail('Expected an error');
+      } catch (error) {
+        expect(mockEmulatorTelnet.connect).not.toHaveBeenCalled();
+        expect(error.message).toContain(`Failed to determine telnet port for emulator device 'emulator-'!`);
+        expect(error.message).toContain(`base64(${adbDevicesResult})`);
+      }
+    });
+
+    it(`should skip if no devices are available`, async () => {
+      exec.mockReturnValue({
+        stdout: 'List of devices attached\n'
+      });
+
+      await adb.devices();
+      expect(mockEmulatorTelnet.connect).not.toHaveBeenCalled();
+    });
   });
 
   it(`install`, async () => {
diff --git a/detox/test/e2e/02.matchers.test.js b/detox/test/e2e/02.matchers.test.js
index 5f1bf1fa3..1f458061b 100644
--- a/detox/test/e2e/02.matchers.test.js
+++ b/detox/test/e2e/02.matchers.test.js
@@ -19,6 +19,11 @@ describe('Matchers', () => {
     await element(by.text('Index')).atIndex(index).tap();
     await expect(element(by.text('First button pressed!!!'))).toBeVisible();
   });
+  
+  it('should be able to swipe elements matched by index', async () => {
+    const index = device.getPlatform() === 'ios' ? 2 : 0;
+    await element(by.text('Index')).atIndex(index).swipe('down', 'fast', 0.7); //No need to do here anything, just let it not crash.
+  });
 
   it('should match elements by type (native class)', async () => {
     const byType = device.getPlatform() === 'ios' ? by.type('RCTImageView') : by.type('android.widget.ImageView');
diff --git a/detox/test/e2e/05.waitfor.test.js b/detox/test/e2e/05.waitfor.test.js
index 23651c7fa..a4ab97cd0 100644
--- a/detox/test/e2e/05.waitfor.test.js
+++ b/detox/test/e2e/05.waitfor.test.js
@@ -11,7 +11,6 @@ describe('WaitFor', () => {
     await expect(element(by.id('createdAndVisibleText'))).toNotExist();
     await element(by.id('GoButton')).tap();
     await waitFor(element(by.id('createdAndVisibleText'))).toExist().withTimeout(20000);
-    await expect(element(by.id('createdAndVisibleText'))).toExist();
   });
 
   it('should wait until an element is removed', async () => {
@@ -38,6 +37,11 @@ describe('WaitFor', () => {
     await expect(element(by.text('Text5'))).toBeVisible();
   });
 
+  it('should fail test after waiting for element to exist but it doesn\'t at the end', async () => {
+    await expect(element(by.id('neverAppearingText'))).toNotExist();
+    await expectToThrow(() => waitFor(element(by.id('neverAppearingText'))).toExist().withTimeout(1000));
+  });
+
   it('should abort scrolling if element was not found', async () => {
     await element(by.id('GoButton')).tap();
     await expectToThrow(() => waitFor(element(by.text('Text1000'))).toBeVisible().whileElement(by.id('ScrollView')).scroll(50, 'down'));
diff --git a/detox/test/index.ios.js b/detox/test/index.ios.js
index 2c8e6b96f..7941e1569 100644
--- a/detox/test/index.ios.js
+++ b/detox/test/index.ios.js
@@ -22,4 +22,5 @@ class exampleIos extends example {
   }
 }
 
+console.disableYellowBox = true;
 AppRegistry.registerComponent('example', () => exampleIos);
\ No newline at end of file
diff --git a/detox/test/package.json b/detox/test/package.json
index eed59e1f4..1fb3c8435 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "12.11.2",
+  "version": "12.11.3",
   "private": true,
   "engines": {
     "node": ">=8.3.0"
@@ -21,11 +21,11 @@
   "dependencies": {
     "moment": "^2.24.0",
     "react": "16.8.3",
-    "react-native": "0.59.8"
+    "react-native": "0.59.9"
   },
   "devDependencies": {
     "@babel/core": "^7.4.5",
-    "detox": "^12.11.2",
+    "detox": "^12.11.3",
     "express": "^4.15.3",
     "jest": "24.6.x",
     "lodash": "^4.14.1",
diff --git a/detox/test/src/app.js b/detox/test/src/app.js
index 978e50e8b..a5114c6e7 100644
--- a/detox/test/src/app.js
+++ b/detox/test/src/app.js
@@ -53,6 +53,7 @@ class example extends Component {
   async componentDidMount() {
     const url = await Linking.getInitialURL();
     if (url) {
+      console.log('App@didMount: Found pending URL', url);
       this.setState({url: url});
     }
   }
@@ -63,7 +64,7 @@ class example extends Component {
 
   render() {
     if (this.state.notification) {
-      console.log("notification:", this.state.notification);
+      console.log("App@render: rendering a notification", this.state.notification);
       if (this.state.notification.title) {
         return this.renderText(this.state.notification.title);
       } else {
@@ -73,12 +74,12 @@ class example extends Component {
     }
 
     else if (this.state.url) {
-      console.log("url:", this.state.url);
+      console.log("App@render: rendering a URL:", this.state.url);
       return this.renderText(this.state.url);
     }
 
     if (!this.state.screen) {
-		console.log("JS rendering");
+		  console.log("App@render: JS rendering main screen");
       return (
         <View style={{flex: 1, paddingTop: 10, justifyContent: 'center', alignItems: 'center'}}>
           <Text style={{fontSize: 20, marginBottom: 10}}>
@@ -115,12 +116,12 @@ class example extends Component {
   }
 
   _onNotification(notification) {
-    console.log("onNotification:", notification);
+    console.log('App@onNotification:', notification);
     this.setState({notification: notification.getAlert()});
   }
 
   _handleOpenURL(params) {
-    console.log("handleOpenURL:", params);
+    console.log('App@handleOpenURL:', params);
     this.setState({url: params.url});
   }
 }