diff --git a/SYNC-RESEARCH.md b/SYNC-RESEARCH.md new file mode 100644 index 0000000000..df1c428f3d --- /dev/null +++ b/SYNC-RESEARCH.md @@ -0,0 +1,89 @@ +GREYDispatchQueueIdlingResource: + + (instancetype)resourceWithDispatchQueue:(dispatch_queue_t)queue name:(NSString *)name; + we need dispatch_queue_t + + (instancetype)resourceWithNSOperationQueue:(NSOperationQueue *)queue name:(NSString *)name; + we need NSOperationQueue + +enqueueJSCall + +- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block +{ + if ([NSThread currentThread] != _javaScriptThread) { + [self performSelector:@selector(executeBlockOnJavaScriptQueue:) + onThread:_javaScriptThread withObject:block waitUntilDone:NO]; + } else { + block(); + } +} + +1. CFRunLoopIsWaiting ?? - the JS thread is a thread, it isn't a GCD queue so it's hard +This function is useful only to test the state of another thread’s run loop. When called with the current thread’s run loop, this function always returns false. +#import "Additions/NSRunLoop+GREYAdditions.h" + +http://tadeuzagallo.com/blog/react-native-bridge/ + + +RCTFrameUpdateObserver - a react native module can ask to receive a callback before every frame is drawn to screen (using CADisplayLink) + +2. RCTTiming.m +maybe listen in on all timers in the system and add EarlGrey monitors to them + +RCT_PROFILE_BEGIN_EVENT +RCT_PROFILE_END_EVENT +we can maybe enable profiling and listen on RCTProfileGetQueue() + +flushedQueue (js) + +dispatch_queue_create("com.facebook.react.RCTBridgeQueue", DISPATCH_QUEUE_CONCURRENT); + +_pendingCalls + +enqueueJSCall + +char *const RCTUIManagerQueueName = "com.facebook.react.ShadowQueue"; +RCTGetUIManagerQueue +_pendingUIBlocks + +if (![_bridge isBatchActive]) + +MessageQueue.js +flushedQueue() +Systrace.counterEvent('pending_js_to_native_queue', this._queue[0].length); + +RCTJSThread + +[self executeBlockOnJavaScriptQueue:^{ + BOOL enabled = [notification.name isEqualToString:RCTProfileDidStartProfiling]; + [_bridge enqueueJSCall:@"Systrace.setEnabled" args:@[enabled ? @YES : @NO]]; + }]; + +JSEvaluateScript + +addSynchronousHookWithName +// can make a JS function that when executes, runs native code synchronously and gets a return value from native + +InteractionManager.runAfterInteractions +* - requestAnimationFrame(): for code that animates a view over time. +* - setImmediate/setTimeout(): run code later, note this may delay animations. +* - runAfterInteractions(): run code later, without delaying active animations. + +BatchedBridge.js getEventLoopRunningTime() - see how much time passed after last flush + +JSTimersExecution.js +JSTimersExecution.Type.requestIdleCallback +callImmediatesPass() return JSTimersExecution.immediates.length > 0 + +requestAnimationFrame - is a polyfill from the browser that you might be familiar with. It accepts a function as its only argument and calls that function before the next repaint + +ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactIdleDetectionUtil.java +* Waits for both the UI thread and bridge to be idle. It determines this by waiting for the +* bridge to become idle, then waiting for the UI thread to become idle, then checking if the +* bridge is idle again (if the bridge was idle before and is still idle after running the UI +* thread to idle, then there are no more events to process in either place). + +NSRunLoop *targetRunLoop = [_javaScriptExecutor isKindOfClass:[RCTJSCExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop]; + [_displayLink addToRunLoop:targetRunLoop]; + +GREYUIWebViewIdlingResource.m +* this is an implementation by EarlGrey that waits on a webview until it becomes idle +* we can do a very similar concept diff --git a/detox/ios/Detox.xcodeproj/project.pbxproj b/detox/ios/Detox.xcodeproj/project.pbxproj index cd9323f289..8eaa6fc647 100644 --- a/detox/ios/Detox.xcodeproj/project.pbxproj +++ b/detox/ios/Detox.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ CC0F35231D460F26008BB94F /* EarlGrey.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CCFA7D8E1D11C4E400E15EDF /* EarlGrey.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CC38FC451D61DEB800589F1C /* ReactNativeBridgeIdlingResource.h in Headers */ = {isa = PBXBuildFile; fileRef = CC38FC431D61DEB800589F1C /* ReactNativeBridgeIdlingResource.h */; }; + CC38FC461D61DEB800589F1C /* ReactNativeBridgeIdlingResource.m in Sources */ = {isa = PBXBuildFile; fileRef = CC38FC441D61DEB800589F1C /* ReactNativeBridgeIdlingResource.m */; }; + CC38FC4F1D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.h in Headers */ = {isa = PBXBuildFile; fileRef = CC38FC4D1D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.h */; }; + CC38FC501D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.m in Sources */ = {isa = PBXBuildFile; fileRef = CC38FC4E1D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.m */; }; CCA99FFF1D227DBE00C762B8 /* ReactNativeSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA99FFD1D227DBE00C762B8 /* ReactNativeSupport.h */; }; CCA9A0001D227DBE00C762B8 /* ReactNativeSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA99FFE1D227DBE00C762B8 /* ReactNativeSupport.m */; }; CCE6D43B1D11A76500F81E39 /* Detox.h in Headers */ = {isa = PBXBuildFile; fileRef = CCE6D43A1D11A76500F81E39 /* Detox.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -84,6 +88,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + CC38FC431D61DEB800589F1C /* ReactNativeBridgeIdlingResource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReactNativeBridgeIdlingResource.h; sourceTree = ""; }; + CC38FC441D61DEB800589F1C /* ReactNativeBridgeIdlingResource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReactNativeBridgeIdlingResource.m; sourceTree = ""; }; + CC38FC481D61F36100589F1C /* ReactNativeHeaders.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactNativeHeaders.h; sourceTree = ""; }; + CC38FC4D1D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReactNativeUIManagerIdlingResource.h; sourceTree = ""; }; + CC38FC4E1D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReactNativeUIManagerIdlingResource.m; sourceTree = ""; }; CCA99FFD1D227DBE00C762B8 /* ReactNativeSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReactNativeSupport.h; sourceTree = ""; }; CCA99FFE1D227DBE00C762B8 /* ReactNativeSupport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReactNativeSupport.m; sourceTree = ""; }; CCE105591D48D82F00A40950 /* DetoxLoader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DetoxLoader.h; sourceTree = ""; }; @@ -148,6 +157,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + CC38FC471D61E3B500589F1C /* ReactNativeSupport */ = { + isa = PBXGroup; + children = ( + CC38FC481D61F36100589F1C /* ReactNativeHeaders.h */, + CCA99FFD1D227DBE00C762B8 /* ReactNativeSupport.h */, + CCA99FFE1D227DBE00C762B8 /* ReactNativeSupport.m */, + CC38FC431D61DEB800589F1C /* ReactNativeBridgeIdlingResource.h */, + CC38FC441D61DEB800589F1C /* ReactNativeBridgeIdlingResource.m */, + CC38FC4D1D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.h */, + CC38FC4E1D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.m */, + ); + name = ReactNativeSupport; + sourceTree = ""; + }; CCE6D42D1D11A76500F81E39 = { isa = PBXGroup; children = ( @@ -180,8 +203,7 @@ CCFA7E511D12CA1500E15EDF /* TestFailureHandler.m */, CCFA7E441D12191200E15EDF /* MethodInvocation.h */, CCFA7E451D12191200E15EDF /* MethodInvocation.m */, - CCA99FFD1D227DBE00C762B8 /* ReactNativeSupport.h */, - CCA99FFE1D227DBE00C762B8 /* ReactNativeSupport.m */, + CC38FC471D61E3B500589F1C /* ReactNativeSupport */, CCE6D43C1D11A76500F81E39 /* Info.plist */, CCFA7D921D11C63000E15EDF /* SocketRocket */, ); @@ -306,6 +328,7 @@ files = ( CCFA7DB51D11C63100E15EDF /* SRIOConsumer.h in Headers */, CCFA7DB91D11C63100E15EDF /* SRProxyConnect.h in Headers */, + CC38FC4F1D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.h in Headers */, CCFA7DC51D11C63100E15EDF /* NSRunLoop+SRWebSocket.h in Headers */, CCFA7E521D12CA1500E15EDF /* TestFailureHandler.h in Headers */, CCFA7E461D12191200E15EDF /* MethodInvocation.h in Headers */, @@ -321,6 +344,7 @@ CCFA7DBD1D11C63100E15EDF /* SRSecurityOptions.h in Headers */, CCFA7DB71D11C63100E15EDF /* SRIOConsumerPool.h in Headers */, CCFA7E4E1D12AA4C00E15EDF /* WebSocket.h in Headers */, + CC38FC451D61DEB800589F1C /* ReactNativeBridgeIdlingResource.h in Headers */, CCFA7DBF1D11C63100E15EDF /* SRError.h in Headers */, CCFA7DC31D11C63100E15EDF /* SRURLUtilities.h in Headers */, CCFA7DBB1D11C63100E15EDF /* SRRunLoopThread.h in Headers */, @@ -424,10 +448,12 @@ CCFA7E4B1D12A60600E15EDF /* TestRunner.m in Sources */, CCFA7DB61D11C63100E15EDF /* SRIOConsumer.m in Sources */, CCFA7DB81D11C63100E15EDF /* SRIOConsumerPool.m in Sources */, + CC38FC501D622AFB00589F1C /* ReactNativeUIManagerIdlingResource.m in Sources */, CCFA7DC21D11C63100E15EDF /* SRHash.m in Sources */, CCFA7DBE1D11C63100E15EDF /* SRSecurityOptions.m in Sources */, CCFA7DBC1D11C63100E15EDF /* SRRunLoopThread.m in Sources */, CCFA7E4F1D12AA4C00E15EDF /* WebSocket.m in Sources */, + CC38FC461D61DEB800589F1C /* ReactNativeBridgeIdlingResource.m in Sources */, CCFA7DB41D11C63100E15EDF /* SRDelegateController.m in Sources */, CCFA7DC81D11C63100E15EDF /* NSURLRequest+SRWebSocket.m in Sources */, ); diff --git a/detox/ios/Detox.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/detox/ios/Detox.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 629705ffc6..0000000000 --- a/detox/ios/Detox.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/detox/ios/Detox/ReactNativeBridgeIdlingResource.h b/detox/ios/Detox/ReactNativeBridgeIdlingResource.h new file mode 100644 index 0000000000..04a8bbd5ac --- /dev/null +++ b/detox/ios/Detox/ReactNativeBridgeIdlingResource.h @@ -0,0 +1,18 @@ +// +// ReactNativeBridgeIdlingResource.h +// Detox +// +// Created by Tal Kol on 8/15/16. +// Copyright © 2016 Wix. All rights reserved. +// + +#import +#import + +@interface ReactNativeBridgeIdlingResource : NSObject + ++ (instancetype)idlingResourceForBridge:(id)bridge name:(NSString *)name; ++ (void)deregister:(ReactNativeBridgeIdlingResource*)instance; +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/detox/ios/Detox/ReactNativeBridgeIdlingResource.m b/detox/ios/Detox/ReactNativeBridgeIdlingResource.m new file mode 100644 index 0000000000..d0dd8f0a49 --- /dev/null +++ b/detox/ios/Detox/ReactNativeBridgeIdlingResource.m @@ -0,0 +1,102 @@ +// +// ReactNativeBridgeIdlingResource.m +// Detox +// +// Created by Tal Kol on 8/15/16. +// Copyright © 2016 Wix. All rights reserved. +// + +#import "ReactNativeHeaders.h" +#import "ReactNativeBridgeIdlingResource.h" +#import "Common/GREYDefines.h" +#import "Common/GREYPrivate.h" + +typedef enum { + kWaiting, + kBusy, + kIdle +} IdlingCheckState; + +@implementation ReactNativeBridgeIdlingResource +{ + NSString *_name; + id _bridge; + IdlingCheckState _state; + int _consecutiveIdles; +} + ++ (instancetype)idlingResourceForBridge:(id)bridge name:(NSString *)name +{ + if (bridge == nil) + { + Class RCTBridge = NSClassFromString(@"RCTBridge"); + if (RCTBridge == nil) return nil; + bridge = [RCTBridge currentBridge]; + } + + if (bridge == nil) return nil; + ReactNativeBridgeIdlingResource *res = [[ReactNativeBridgeIdlingResource alloc] initWithBridge:bridge name:name]; + [[GREYUIThreadExecutor sharedInstance] registerIdlingResource:res]; + return res; +} + ++ (void)deregister:(ReactNativeBridgeIdlingResource*)instance +{ + [[GREYUIThreadExecutor sharedInstance] deregisterIdlingResource:instance]; +} + +- (instancetype)initWithBridge:(id)bridge name:(NSString *)name +{ + self = [super init]; + if (self) + { + _name = [name copy]; + _bridge = bridge; + _state = kBusy; + _consecutiveIdles = 0; + } + return self; +} + +#pragma mark - GREYIdlingResource + +- (NSString *)idlingResourceName { + return _name; +} + +- (NSString *)idlingResourceDescription { + return _name; +} + +- (BOOL)isIdleNow +{ + if (_bridge == nil) return NO; + if (![_bridge isValid] || [_bridge isLoading]) return NO; + id executor = [_bridge valueForKey:@"javaScriptExecutor"]; + if (executor == nil) return NO; + + if (_state == kIdle) + { + _consecutiveIdles++; + } + else + { + _consecutiveIdles = 0; + } + if (_state != kWaiting) + { + _state = kWaiting; + [executor executeBlockOnJavaScriptQueue:^{ + [executor flushedQueue:^(id json, NSError *error) { + if (error == nil && json != nil) _state = kBusy; + else _state = kIdle; + }]; + }]; + } + BOOL res = NO; + if (_consecutiveIdles > 2) res = YES; + // NSLog(@"ReactNativeBridgeIdlingResource: idle=%d (%d)", res, _consecutiveIdles); + return res; +} + +@end diff --git a/detox/ios/Detox/ReactNativeHeaders.h b/detox/ios/Detox/ReactNativeHeaders.h new file mode 100644 index 0000000000..e5423d8ed0 --- /dev/null +++ b/detox/ios/Detox/ReactNativeHeaders.h @@ -0,0 +1,35 @@ +// +// ReactNativeHeaders.h +// Detox +// +// Created by Tal Kol on 8/15/16. +// Copyright © 2016 Wix. All rights reserved. +// + +#ifndef ReactNativeHeaders_h +#define ReactNativeHeaders_h + +#import + +// we don't want detox to have a direct dependency on ReactNative + +typedef void (^RN_RCTJavaScriptCallback)(id json, NSError *error); + +@protocol RN_RCTUIManager +- (dispatch_queue_t)methodQueue; +@end + +@protocol RN_RCTBridge ++ (id)currentBridge; +- (id) uiManager; +- (id)valueForKey:(NSString*)key; +@property (nonatomic, readonly, getter=isLoading) BOOL loading; +@property (nonatomic, readonly, getter=isValid) BOOL valid; +@end + +@protocol RN_RCTJavaScriptExecutor +- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block; +- (void)flushedQueue:(RN_RCTJavaScriptCallback)onComplete; +@end + +#endif /* ReactNativeHeaders_h */ diff --git a/detox/ios/Detox/ReactNativeSupport.m b/detox/ios/Detox/ReactNativeSupport.m index ca5ecf3682..064cd6f2d7 100644 --- a/detox/ios/Detox/ReactNativeSupport.m +++ b/detox/ios/Detox/ReactNativeSupport.m @@ -7,10 +7,14 @@ // #import "ReactNativeSupport.h" +#import "ReactNativeBridgeIdlingResource.h" +#import "ReactNativeUIManagerIdlingResource.h" @interface ReactNativeSupport() @property (nonatomic, assign) BOOL javascriptJustLoaded; +@property (nonatomic, retain) ReactNativeBridgeIdlingResource *bridgeIdlingResource; +@property (nonatomic, retain) ReactNativeUIManagerIdlingResource *uiManagerIdlingResource; @end @@ -27,6 +31,8 @@ - (instancetype)init self = [super init]; if (self == nil) return nil; self.javascriptJustLoaded = NO; + self.bridgeIdlingResource = nil; + self.uiManagerIdlingResource = nil; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(javascriptDidLoad) @@ -48,6 +54,8 @@ - (void)dealloc - (void) reloadApp { + [self removeIdlingResources]; + // option 1: [[RCTBridge currentBridge] reload] // option 2: post notification @@ -59,6 +67,11 @@ - (void) reloadApp - (void) javascriptDidLoad { self.javascriptJustLoaded = YES; + + // install idling resources to help earlgrey sync with react native + [self removeIdlingResources]; + self.bridgeIdlingResource = [ReactNativeBridgeIdlingResource idlingResourceForBridge:nil name:@"ReactNative Bridge"]; + self.uiManagerIdlingResource = [ReactNativeUIManagerIdlingResource idlingResourceForBridge:nil name:@"ReactNative UIManager"]; } - (void) contentDidAppear @@ -70,5 +83,18 @@ - (void) contentDidAppear } } +- (void) removeIdlingResources +{ + if (self.bridgeIdlingResource != nil) + { + [ReactNativeBridgeIdlingResource deregister:self.bridgeIdlingResource]; + self.bridgeIdlingResource = nil; + } + if (self.uiManagerIdlingResource != nil) + { + [ReactNativeUIManagerIdlingResource deregister:self.uiManagerIdlingResource]; + self.uiManagerIdlingResource = nil; + } +} @end diff --git a/detox/ios/Detox/ReactNativeUIManagerIdlingResource.h b/detox/ios/Detox/ReactNativeUIManagerIdlingResource.h new file mode 100644 index 0000000000..95f467db95 --- /dev/null +++ b/detox/ios/Detox/ReactNativeUIManagerIdlingResource.h @@ -0,0 +1,19 @@ +// +// ReactNativeUIManagerIdlingResource.h +// Detox +// +// Created by Tal Kol on 8/15/16. +// Copyright © 2016 Wix. All rights reserved. +// + +#import +#import +#import + +@interface ReactNativeUIManagerIdlingResource : GREYDispatchQueueIdlingResource + ++ (instancetype)idlingResourceForBridge:(id)bridge name:(NSString *)name; ++ (void)deregister:(ReactNativeUIManagerIdlingResource*)instance; +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/detox/ios/Detox/ReactNativeUIManagerIdlingResource.m b/detox/ios/Detox/ReactNativeUIManagerIdlingResource.m new file mode 100644 index 0000000000..610d9f9d94 --- /dev/null +++ b/detox/ios/Detox/ReactNativeUIManagerIdlingResource.m @@ -0,0 +1,66 @@ +// +// ReactNativeUIManagerIdlingResource.m +// Detox +// +// Created by Tal Kol on 8/15/16. +// Copyright © 2016 Wix. All rights reserved. +// + +#import "ReactNativeHeaders.h" +#import "ReactNativeUIManagerIdlingResource.h" +#import "Common/GREYDefines.h" +#import "Common/GREYPrivate.h" + + +@interface GREYDispatchQueueIdlingResource (ReactNativeUIManagerIdlingResource) +- (instancetype)initWithDispatchQueue:(dispatch_queue_t)queue name:(NSString *)name; +@end + + +@implementation ReactNativeUIManagerIdlingResource +{ + id _bridge; +} + ++ (instancetype)idlingResourceForBridge:(id)bridge name:(NSString *)name +{ + if (bridge == nil) + { + Class RCTBridge = NSClassFromString(@"RCTBridge"); + if (RCTBridge == nil) return nil; + bridge = [RCTBridge currentBridge]; + } + + if (bridge == nil) return nil; + ReactNativeUIManagerIdlingResource *res = [[ReactNativeUIManagerIdlingResource alloc] initWithBridge:bridge name:name]; + [[GREYUIThreadExecutor sharedInstance] registerIdlingResource:res]; + return res; +} + ++ (void)deregister:(ReactNativeUIManagerIdlingResource*)instance +{ + [[GREYUIThreadExecutor sharedInstance] deregisterIdlingResource:instance]; +} + +- (instancetype)initWithBridge:(id)bridge name:(NSString *)name +{ + dispatch_queue_t queue = [[bridge uiManager] methodQueue]; + self = [super initWithDispatchQueue:queue name:name]; + if (self) + { + _bridge = bridge; + } + return self; +} + +- (BOOL)isIdleNow +{ + if (_bridge == nil) return NO; + if (![_bridge isValid] || [_bridge isLoading]) return NO; + + BOOL res = [super isIdleNow]; + // NSLog(@"ReactNativeUIManagerIdlingResource: idle=%d", res); + return res; +} + +@end