Skip to content

Commit

Permalink
iOS: RCTTestRunner should deallocate rootview before invalidating the…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
fkgozali authored and facebook-github-bot committed Apr 24, 2018
1 parent 37d28be commit 9909a42
Showing 1 changed file with 40 additions and 21 deletions.
61 changes: 40 additions & 21 deletions Libraries/RCTTest/RCTTestRunner.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -113,6 +114,7 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName
expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
{
__weak RCTBridge *batchedBridge;
NSNumber *rootTag;

@autoreleasepool {
__block NSMutableArray<NSString *> *errors = nil;
Expand All @@ -133,35 +135,42 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName
[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;

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]];
}
[vc.view addSubview:rootView]; // Add as subview so it doesn't get resized

if (configurationBlock) {
configurationBlock(rootView);
}

[rootView removeFromSuperview];
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;
}

RCTSetLogFunction(defaultLogFunction);

Expand All @@ -181,15 +190,25 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName
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

0 comments on commit 9909a42

Please sign in to comment.