From 9909a4243f7154c8f817489599702e8155c2beb2 Mon Sep 17 00:00:00 2001 From: Kevin Gozali Date: Tue, 24 Apr 2018 02:00:09 -0700 Subject: [PATCH] iOS: RCTTestRunner should deallocate rootview before invalidating the bridge Summary: There are cases of race condition where the react component being mounted is calling a nativemodule from JS *right after* the test runner starts invalidating the bridge. This causes assertion failure deep in the RCTModuleData such that the bridge doesn't complete the invalidation. To avoid this, unmount and deallocate the RCTRootView before invalidating the bridge. Reviewed By: sahrens Differential Revision: D7727249 fbshipit-source-id: 8b82edc3b795ceb2e32441f16e225d723fcd9be1 --- Libraries/RCTTest/RCTTestRunner.m | 63 ++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 00e971372..315371f4a 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -12,6 +12,7 @@ #import #import #import +#import #import #import "FBSnapshotTestController.h" @@ -113,6 +114,7 @@ configurationBlock:(void(^)(RCTRootView *rootView))configurationBlock expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock { __weak RCTBridge *batchedBridge; + NSNumber *rootTag; @autoreleasepool { __block NSMutableArray *errors = nil; @@ -133,36 +135,43 @@ expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock [bridge.devSettings setIsDebuggingRemotely:_useJSDebugger]; batchedBridge = [bridge batchedBridge]; - RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProps]; -#if TARGET_OS_TV - rootView.frame = CGRectMake(0, 0, 1920, 1080); // Standard screen size for tvOS -#else - rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices -#endif + UIViewController *vc = RCTSharedApplication().delegate.window.rootViewController; + vc.view = [UIView new]; - RCTTestModule *testModule = [rootView.bridge moduleForClass:[RCTTestModule class]]; + RCTTestModule *testModule = [bridge moduleForClass:[RCTTestModule class]]; RCTAssert(_testController != nil, @"_testController should not be nil"); testModule.controller = _testController; testModule.testSelector = test; testModule.testSuffix = _testSuffix; - testModule.view = rootView; - UIViewController *vc = RCTSharedApplication().delegate.window.rootViewController; - vc.view = [UIView new]; - [vc.view addSubview:rootView]; // Add as subview so it doesn't get resized + @autoreleasepool { + // The rootView needs to be deallocated after this @autoreleasepool block exits. + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProps]; +#if TARGET_OS_TV + rootView.frame = CGRectMake(0, 0, 1920, 1080); // Standard screen size for tvOS +#else + rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices +#endif - if (configurationBlock) { - configurationBlock(rootView); + rootTag = rootView.reactTag; + testModule.view = rootView; + + [vc.view addSubview:rootView]; // Add as subview so it doesn't get resized + + if (configurationBlock) { + configurationBlock(rootView); + } + + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:kTestTimeoutSeconds]; + while (date.timeIntervalSinceNow > 0 && testModule.status == RCTTestStatusPending && errors == nil) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + + [rootView removeFromSuperview]; + testModule.view = nil; } - NSDate *date = [NSDate dateWithTimeIntervalSinceNow:kTestTimeoutSeconds]; - while (date.timeIntervalSinceNow > 0 && testModule.status == RCTTestStatusPending && errors == nil) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - } - - [rootView removeFromSuperview]; - RCTSetLogFunction(defaultLogFunction); #if RCT_DEV @@ -181,15 +190,25 @@ expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock RCTAssert(testModule.status == RCTTestStatusPassed, @"Test failed"); } + // Wait for the rootView to be deallocated completely before invalidating the bridge. + RCTUIManager *uiManager = [bridge moduleForClass:[RCTUIManager class]]; + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:5]; + while (date.timeIntervalSinceNow > 0 && [uiManager viewForReactTag:rootTag]) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + RCTAssert([uiManager viewForReactTag:rootTag] == nil, @"RootView should have been deallocated after removed."); + [bridge invalidate]; } - // Give the bridge a chance to disappear before continuing to the next test. + // Wait for the bridge to disappear before continuing to the next test. NSDate *invalidateTimeout = [NSDate dateWithTimeIntervalSinceNow:30]; while (invalidateTimeout.timeIntervalSinceNow > 0 && batchedBridge != nil) { [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; } + RCTAssert(batchedBridge == nil, @"Bridge should be deallocated after the test"); } @end