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
This commit is contained in:
Kevin Gozali 2018-04-24 02:00:09 -07:00 committed by Facebook Github Bot
parent 37d28be2d9
commit 9909a4243f
1 changed files with 41 additions and 22 deletions

View File

@ -12,6 +12,7 @@
#import <React/RCTDevSettings.h>
#import <React/RCTLog.h>
#import <React/RCTRootView.h>
#import <React/RCTUIManager.h>
#import <React/RCTUtils.h>
#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<NSString *> *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