Repository: wix/Detox
Pull request #1269 (2019-04-10 16:47:05)

feat(log): dump pending network requests on test timeout
**Description:**

Currently, it is not easy to track what requests to `testee` have never been resolved, leading to timeout errors in tests.

To make it Detox debug-friendly in those cases, I suggest dumping pending network requests if:

1. a test has failed with a timeout (mocha offers this as an API, in Jest I scan for timeouts in failed expectations)
2. in `detox.cleanup()`, before we clean up the client.

Although it does not cover all the cases (e.g. cannot catch infinite `detox._client.cleanup()` but looks like a good start IMO.

LeoNatan: @d4vidi 
LeoNatan: I think this is a bad approach. Sometimes, as we have discovered, things take more than 60 seconds. For example, first launch of an app that has to go through the packager and that takes a long time in case of a large app. Detox already has a configurable timeout setting, and that should be respected, rather than a hardcoded number.
noomorph: Oh, I have another idea on how to rewrite this logic while keeping the feature. We might want to run a self-check on `detox.cleanup()` which will print out the stuck requests.
noomorph: I found how to do it right. :smiley: 
I'll implement soon.
noomorph: @LeoNatan, I've completely changed the approach.

Now we just dump pending network requests if any, in 2 cases:
1) a test has failed with a timeout (mocha offers this as an API, in Jest I scan for timeouts in failed expectations)
2) in `detox.cleanup()`, before we clean up the client.

It does not cover all the cases (e.g. cannot catch infinite `detox._client.cleanup()` but looks like a good start to me.

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 000000000..49e334a61
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,38 @@
+version: 2
+
+detox-suite-template: &detox-suite-template
+  macos:
+    xcode: "10.2.1"
+  steps:
+    - checkout
+    - run: 
+        name: Install Dependencies
+        command: scripts/install.ios.sh        
+    - run:
+        name: Detox iOS CI
+        command: scripts/ci.ios.sh
+    - store_artifacts:
+        path: detox/test/artifacts
+
+jobs:
+  detox-suite-rn59-9:
+    enviroment:
+      REACT_NATIVE_VERSION: "0.59.9" 
+    <<: *detox-suite-template
+  detox-suite-rn58-6:      
+    enviroment:
+      REACT_NATIVE_VERSION: "0.58.6"   
+    <<: *detox-suite-template
+      
+workflows:
+  version: 2
+  test-detox-suite:
+    jobs:
+      - detox-suite-rn59-9:
+          filters:
+            branches:
+              only: TravisCITest    
+      - detox-suite-rn58-6:
+          filters:
+            branches:
+              only: TravisCITest
\ No newline at end of file
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 821f6c811..05ab29d4e 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -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/runners/jest/DetoxJestAdapter.js b/detox/runners/jest/DetoxJestAdapter.js
index 5ff0dd043..d27b8d5b0 100644
--- a/detox/runners/jest/DetoxJestAdapter.js
+++ b/detox/runners/jest/DetoxJestAdapter.js
@@ -1,3 +1,4 @@
+const _ = require('lodash');
 const DetoxRuntimeError = require('../../src/errors/DetoxRuntimeError');
 
 class DetoxJestAdapter /* implements JasmineReporter */ {
@@ -60,11 +61,20 @@ class DetoxJestAdapter /* implements JasmineReporter */ {
       title: result.description,
       fullName: result.fullName,
       status: result.status,
+      timedOut: this._hasTimedOut(result),
     };
 
     this._enqueue(() => this._afterEach(spec));
   }
 
+  _hasTimedOut(result) {
+    return _.chain(result.failedExpectations)
+      .map('error')
+      .compact()
+      .some(e => _.includes(e.message, 'Timeout'))
+      .value();
+  }
+
   _enqueue(fn) {
     this._todos.push(fn);
   }
diff --git a/detox/runners/mocha/DetoxMochaAdapter.js b/detox/runners/mocha/DetoxMochaAdapter.js
index 4a8a65e22..e07cabb74 100644
--- a/detox/runners/mocha/DetoxMochaAdapter.js
+++ b/detox/runners/mocha/DetoxMochaAdapter.js
@@ -16,6 +16,7 @@ class DetoxMochaAdapter {
       title: context.currentTest.title,
       fullName: context.currentTest.fullTitle(),
       status: this._mapStatus(context, true),
+      timedOut: context.currentTest.timedOut,
     });
   }
 
@@ -31,4 +32,4 @@ class DetoxMochaAdapter {
   }
 }
 
-module.exports = DetoxMochaAdapter;
\ No newline at end of file
+module.exports = DetoxMochaAdapter;
diff --git a/detox/src/Detox.js b/detox/src/Detox.js
index 2045f71f3..6c7a1be43 100644
--- a/detox/src/Detox.js
+++ b/detox/src/Detox.js
@@ -90,6 +90,7 @@ class Detox {
     await this._artifactsManager.onAfterAll();
 
     if (this._client) {
+      this._client.dumpPendingRequests();
       await this._client.cleanup();
     }
 
@@ -109,7 +110,10 @@ class Detox {
   async beforeEach(testSummary) {
     this._validateTestSummary(testSummary);
     this._logTestRunCheckpoint('DETOX_BEFORE_EACH', testSummary);
-    await this._handleAppCrashIfAny(testSummary.fullName);
+    await this._dumpUnhandledErrorsIfAny({
+      pendingRequests: false,
+      testName: testSummary.fullName,
+    });
     await this._artifactsManager.onBeforeEach(testSummary);
   }
 
@@ -117,7 +121,10 @@ class Detox {
     this._validateTestSummary(testSummary);
     this._logTestRunCheckpoint('DETOX_AFTER_EACH', testSummary);
     await this._artifactsManager.onAfterEach(testSummary);
-    await this._handleAppCrashIfAny(testSummary.fullName);
+    await this._dumpUnhandledErrorsIfAny({
+      pendingRequests: testSummary.timedOut,
+      testName: testSummary.fullName,
+    });
   }
 
   _logTestRunCheckpoint(event, { status, fullName }) {
@@ -150,7 +157,11 @@ class Detox {
     }
   }
 
-  async _handleAppCrashIfAny(testName) {
+  async _dumpUnhandledErrorsIfAny({ testName, pendingRequests }) {
+    if (pendingRequests) {
+      this._client.dumpPendingRequests({testName});
+    }
+
     const pendingAppCrash = this._client.getPendingCrashAndReset();
 
     if (pendingAppCrash) {
diff --git a/detox/src/client/AsyncWebSocket.js b/detox/src/client/AsyncWebSocket.js
index 686f2b4cd..008c1fcc3 100644
--- a/detox/src/client/AsyncWebSocket.js
+++ b/detox/src/client/AsyncWebSocket.js
@@ -59,7 +59,7 @@ class AsyncWebSocket {
 
     return new Promise(async(resolve, reject) => {
       message.messageId = messageId || this.messageIdCounter++;
-      this.inFlightPromises[message.messageId] = {resolve, reject};
+      this.inFlightPromises[message.messageId] = {message, resolve, reject};
       const messageAsString = JSON.stringify(message);
       this.log.trace({ event: 'WEBSOCKET_SEND' }, `${messageAsString}`);
       this.ws.send(messageAsString);
@@ -96,6 +96,12 @@ class AsyncWebSocket {
     return this.ws.readyState === WebSocket.OPEN;
   }
 
+  resetInFlightPromises() {
+    _.forEach(this.inFlightPromises, (_, messageId) => {
+      delete this.inFlightPromises[messageId];
+    });
+  }
+
   rejectAll(error) {
     _.forEach(this.inFlightPromises, (promise, messageId) => {
       let pendingPromise = this.inFlightPromises[messageId];
diff --git a/detox/src/client/Client.js b/detox/src/client/Client.js
index 60ea30217..e5ce64b2d 100644
--- a/detox/src/client/Client.js
+++ b/detox/src/client/Client.js
@@ -2,6 +2,7 @@ const _ = require('lodash');
 const AsyncWebSocket = require('./AsyncWebSocket');
 const actions = require('./actions/actions');
 const argparse = require('../utils/argparse');
+const log = require('../utils/logger').child({ __filename });
 
 class Client {
   constructor(config) {
@@ -135,6 +136,30 @@ class Client {
       }
     }, this.slowInvocationTimeout);
   }
+
+  dumpPendingRequests({testName} = {}) {
+    const messages = _.values(this.ws.inFlightPromises)
+      .map(p => p.message)
+      .filter(m => m.type !== 'currentStatus');
+
+    if (_.isEmpty(messages)) {
+      return;
+    }
+
+    let dump = 'App has not responded to the network requests below:';
+    for (const msg of messages) {
+      dump += `\n  (id = ${msg.messageId}) ${msg.type}: ${JSON.stringify(msg.params)}`;
+    }
+
+    const notice = testName
+      ? `That might be the reason why the test "${testName}" has timed out.`
+      : `Unresponded network requests might result in timeout errors in Detox tests.`;
+
+    dump += `\n\n${notice}\n`;
+
+    log.warn({ event: 'PENDING_REQUESTS'}, dump);
+    this.ws.resetInFlightPromises();
+  }
 }
 
 module.exports = Client;
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.MockingOpenFromURL.md b/docs/APIRef.MockingOpenFromURL.md
index 6282f7e46..beb2fd374 100644
--- a/docs/APIRef.MockingOpenFromURL.md
+++ b/docs/APIRef.MockingOpenFromURL.md
@@ -5,15 +5,19 @@ You can mock opening the app from URL to test your app's deep link handling mech
 #### Mocking App Launch from a URL
 
 ```js
-await device.relaunchApp({url: url, sourceApp: bundleId}); //sourceApp is optional
+await device.launchApp({newInstance: true, url: url, sourceApp: bundleId}); //sourceApp is optional
 ```
 
 **Example:**
 
 ```js
-describe('relaunchApp', () => {
+describe('launch app from URL', () => {
     before(async () => {
-      await device.relaunchApp({url: 'scheme://some.url', sourceApp: 'com.apple.mobilesafari'});
+      await device.launchApp({
+        newInstance: true,
+        url: 'scheme://some.url',
+        sourceApp: 'com.apple.mobilesafari'
+      });
     });
 
     it('should tap successfully', async () => {
diff --git a/docs/APIRef.MockingUserActivity.md b/docs/APIRef.MockingUserActivity.md
index 888107135..f352ff672 100644
--- a/docs/APIRef.MockingUserActivity.md
+++ b/docs/APIRef.MockingUserActivity.md
@@ -37,7 +37,7 @@ await device.sendUserActivity(activity)
 describe('Foreground user activity', () => {
 
 beforeEach(async () => {
-  await device.relaunchApp();
+  await device.launchApp({newInstance: true});
 });
 
 it('User activity from inside the app', async () => {
diff --git a/docs/APIRef.MockingUserNotifications.md b/docs/APIRef.MockingUserNotifications.md
index 59dfabbf2..3a2e05a40 100644
--- a/docs/APIRef.MockingUserNotifications.md
+++ b/docs/APIRef.MockingUserNotifications.md
@@ -6,22 +6,26 @@ Detox supports mocking user notifications for iOS apps.
 
 ### Mocking App Launch with a Notification
 
-Using `relaunchApp()` with custom params will trigger the mocking mechanism.
+Using `launchApp()` with custom params (e.g., `userNotification`) will trigger the mocking mechanism.
 
 ```js
-await device.relaunchApp({userNotification: notification});
+await device.launchApp({newInstance: true, userNotification: notification});
 ```
+
 **Example:**
 
 ```js
 describe('Background push notification', () => {
-	beforeEach(async () => {
-	  await device.relaunchApp({userNotification: userNotificationPushTrigger})
-	});
-
-	it('push notification from background', async () => {
-	  await expect(element(by.text('From push'))).toBeVisible();
-	});
+  beforeEach(async () => {
+    await device.launchApp({
+      newInstance: true,
+      userNotification: userNotificationPushTrigger,
+    });
+  });
+
+  it('push notification from background', async () => {
+    await expect(element(by.text('From push'))).toBeVisible();
+  });
 });
 ```
 
@@ -40,7 +44,7 @@ await device.sendUserNotification(notification)
 describe('Foreground user notifications', () => {
 
 beforeEach(async () => {
-  await device.relaunchApp();
+  await device.launchApp();
 });
 
 it('Local notification from inside the app', async () => {
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();
 ```


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/Detox.test.js b/detox/src/Detox.test.js
index e8310a965..cbcd15ddb 100644
--- a/detox/src/Detox.test.js
+++ b/detox/src/Detox.test.js
@@ -166,6 +166,27 @@ describe('Detox', () => {
     expect(device.launchApp).toHaveBeenCalledTimes(1);
   });
 
+  it(`handleAppCrash should not dump pending requests if testSummary has no timeout flag`, async () => {
+    Detox = require('./Detox');
+    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
+    const testSummary = { title: 'test', fullName: 'suite - test', status: 'failed' };
+
+    await detox.init();
+    await detox.afterEach(testSummary);
+
+    expect(detox._client.dumpPendingRequests).not.toHaveBeenCalled();
+  });
+
+  it(`handleAppCrash should dump pending requests if testSummary has timeout flag`, async () => {
+    Detox = require('./Detox');
+    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
+    const testSummary = { title: 'test', fullName: 'suite - test', status: 'failed', timedOut: true };
+
+    await detox.init();
+    await detox.afterEach(testSummary);
+    expect(detox._client.dumpPendingRequests).toHaveBeenCalled();
+  });
+
   describe('.artifactsManager', () => {
     let artifactsManager;
 
diff --git a/detox/src/client/AsyncWebSocket.test.js b/detox/src/client/AsyncWebSocket.test.js
index 4c87db554..79da8a95c 100644
--- a/detox/src/client/AsyncWebSocket.test.js
+++ b/detox/src/client/AsyncWebSocket.test.js
@@ -186,6 +186,16 @@ describe('AsyncWebSocket', () => {
     await expect(message2).rejects.toEqual(error);
   });
 
+  it(`resetInFlightPromises should erase all pending promises`, async () => {
+    await connect(client);
+    client.send(generateRequest());
+    client.send(generateRequest());
+
+    expect(_.size(client.inFlightPromises)).toBe(2);
+    client.resetInFlightPromises();
+    expect(_.size(client.inFlightPromises)).toBe(0);
+  });
+
   async function connect(client) {
     const result = {};
     const promise = client.open();
diff --git a/detox/src/client/Client.test.js b/detox/src/client/Client.test.js
index d15c69262..0dcfb94bc 100644
--- a/detox/src/client/Client.test.js
+++ b/detox/src/client/Client.test.js
@@ -1,4 +1,5 @@
 const tempfile = require('tempfile');
+const actions = require('./actions/actions');
 const config = require('../configurations.mock').validOneDeviceAndSession.session;
 const invoke = require('../invoke');
 
@@ -7,6 +8,7 @@ describe('Client', () => {
   let WebSocket;
   let Client;
   let client;
+  let log;
 
   beforeEach(() => {
     jest.mock('../utils/logger');
@@ -16,6 +18,7 @@ describe('Client', () => {
     argparse = require('../utils/argparse');
 
     Client = require('./Client');
+    log = require('../utils/logger');
   });
 
   it(`reloadReactNative() - should receive ready from device and resolve`, async () => {
@@ -228,6 +231,53 @@ describe('Client', () => {
     }
   });
 
+  it(`dumpPendingRequests() - should not dump if no pending requests`, async () => {
+    await connect();
+    client.dumpPendingRequests();
+    expect(log.warn).not.toHaveBeenCalled();
+  });
+
+  it(`dumpPendingRequests() - should not dump if there are only currentStatus requests (debug-synchronization)`, async () => {
+    await connect();
+
+    const currentStatus = { message: new actions.CurrentStatus(), resolve: jest.fn(), reject: jest.fn() };
+    client.ws.inFlightPromises = {
+      [currentStatus.message.messageId]: currentStatus
+    };
+
+    client.dumpPendingRequests();
+    expect(log.warn).not.toHaveBeenCalled();
+  });
+
+  describe('dumpPendingRequests() - if there are pending requests -', () => {
+    beforeEach(async () => {
+      await connect();
+
+      const cleanup = { message: new actions.Cleanup(), resolve: jest.fn(), reject: jest.fn() };
+      client.ws.inFlightPromises = {
+        [cleanup.message.messageId]: cleanup
+      };
+    });
+
+    it(`should dump generic message if not testName is specified`, async () => {
+      client.dumpPendingRequests();
+      expect(log.warn.mock.calls[0]).toMatchSnapshot();
+      expect(log.warn.mock.calls[0][1]).toMatch(/Unresponded network requests/);
+    });
+
+    it(`should dump specific message if testName is specified`, async () => {
+      client.dumpPendingRequests({testName: "Login screen should log in"});
+      expect(log.warn.mock.calls[0]).toMatchSnapshot();
+      expect(log.warn.mock.calls[0][1]).toMatch(/Login screen should log in/);
+    });
+
+    it(`should reset in flight promises`, async () => {
+      expect(client.ws.resetInFlightPromises).not.toHaveBeenCalled();
+      client.dumpPendingRequests();
+      expect(client.ws.resetInFlightPromises).toHaveBeenCalled();
+    });
+  });
+
   it(`save a pending error if AppWillTerminateWithError event is sent to tester`, async () => {
     client.ws.setEventCallback = jest.fn();
     await connect();
diff --git a/detox/src/client/__snapshots__/Client.test.js.snap b/detox/src/client/__snapshots__/Client.test.js.snap
new file mode 100644
index 000000000..2957aa609
--- /dev/null
+++ b/detox/src/client/__snapshots__/Client.test.js.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Client dumpPendingRequests() - if there are pending requests - should dump generic message if not testName is specified 1`] = `
+Array [
+  Object {
+    "event": "PENDING_REQUESTS",
+  },
+  "App has not responded to the network requests below:
+  (id = -49642) cleanup: {}
+
+Unresponded network requests might result in timeout errors in Detox tests.
+",
+]
+`;
+
+exports[`Client dumpPendingRequests() - if there are pending requests - should dump specific message if testName is specified 1`] = `
+Array [
+  Object {
+    "event": "PENDING_REQUESTS",
+  },
+  "App has not responded to the network requests below:
+  (id = -49642) cleanup: {}
+
+That might be the reason why the test \\"Login screen should log in\\" has timed out.
+",
+]
+`;
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'));