mirror of
https://github.com/status-im/react-native.git
synced 2025-01-10 17:45:59 +00:00
9909a4243f
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
215 lines
8.0 KiB
Objective-C
215 lines
8.0 KiB
Objective-C
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTTestRunner.h"
|
|
|
|
#import <React/RCTAssert.h>
|
|
#import <React/RCTBridge+Private.h>
|
|
#import <React/RCTDevSettings.h>
|
|
#import <React/RCTLog.h>
|
|
#import <React/RCTRootView.h>
|
|
#import <React/RCTUIManager.h>
|
|
#import <React/RCTUtils.h>
|
|
|
|
#import "FBSnapshotTestController.h"
|
|
#import "RCTTestModule.h"
|
|
|
|
static const NSTimeInterval kTestTimeoutSeconds = 120;
|
|
|
|
@implementation RCTTestRunner
|
|
{
|
|
FBSnapshotTestController *_testController;
|
|
RCTBridgeModuleListProvider _moduleProvider;
|
|
NSString *_appPath;
|
|
}
|
|
|
|
- (instancetype)initWithApp:(NSString *)app
|
|
referenceDirectory:(NSString *)referenceDirectory
|
|
moduleProvider:(RCTBridgeModuleListProvider)block
|
|
scriptURL:(NSURL *)scriptURL
|
|
{
|
|
RCTAssertParam(app);
|
|
RCTAssertParam(referenceDirectory);
|
|
|
|
if ((self = [super init])) {
|
|
if (!referenceDirectory.length) {
|
|
referenceDirectory = [[NSBundle bundleForClass:self.class].resourcePath stringByAppendingPathComponent:@"ReferenceImages"];
|
|
}
|
|
|
|
NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"];
|
|
sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"];
|
|
_testController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName];
|
|
_testController.referenceImagesDirectory = referenceDirectory;
|
|
_moduleProvider = [block copy];
|
|
_appPath = app;
|
|
|
|
if (scriptURL != nil) {
|
|
_scriptURL = scriptURL;
|
|
} else {
|
|
[self updateScript];
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|
|
|
- (void)updateScript
|
|
{
|
|
if (getenv("CI_USE_PACKAGER") || _useBundler) {
|
|
_scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true", _appPath]];
|
|
} else {
|
|
_scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"];
|
|
}
|
|
RCTAssert(_scriptURL != nil, @"No scriptURL set");
|
|
}
|
|
|
|
- (void)setRecordMode:(BOOL)recordMode
|
|
{
|
|
_testController.recordMode = recordMode;
|
|
}
|
|
|
|
- (BOOL)recordMode
|
|
{
|
|
return _testController.recordMode;
|
|
}
|
|
|
|
- (void)setUseBundler:(BOOL)useBundler
|
|
{
|
|
_useBundler = useBundler;
|
|
[self updateScript];
|
|
}
|
|
|
|
- (void)runTest:(SEL)test module:(NSString *)moduleName
|
|
{
|
|
[self runTest:test module:moduleName initialProps:nil configurationBlock:nil expectErrorBlock:nil];
|
|
}
|
|
|
|
- (void)runTest:(SEL)test module:(NSString *)moduleName
|
|
initialProps:(NSDictionary<NSString *, id> *)initialProps
|
|
configurationBlock:(void(^)(RCTRootView *rootView))configurationBlock
|
|
{
|
|
[self runTest:test module:moduleName initialProps:initialProps configurationBlock:configurationBlock expectErrorBlock:nil];
|
|
}
|
|
|
|
- (void)runTest:(SEL)test module:(NSString *)moduleName
|
|
initialProps:(NSDictionary<NSString *, id> *)initialProps
|
|
configurationBlock:(void(^)(RCTRootView *rootView))configurationBlock
|
|
expectErrorRegex:(NSString *)errorRegex
|
|
{
|
|
BOOL(^expectErrorBlock)(NSString *error) = ^BOOL(NSString *error){
|
|
return [error rangeOfString:errorRegex options:NSRegularExpressionSearch].location != NSNotFound;
|
|
};
|
|
|
|
[self runTest:test module:moduleName initialProps:initialProps configurationBlock:configurationBlock expectErrorBlock:expectErrorBlock];
|
|
}
|
|
|
|
- (void)runTest:(SEL)test module:(NSString *)moduleName
|
|
initialProps:(NSDictionary<NSString *, id> *)initialProps
|
|
configurationBlock:(void(^)(RCTRootView *rootView))configurationBlock
|
|
expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
|
|
{
|
|
__weak RCTBridge *batchedBridge;
|
|
NSNumber *rootTag;
|
|
|
|
@autoreleasepool {
|
|
__block NSMutableArray<NSString *> *errors = nil;
|
|
RCTLogFunction defaultLogFunction = RCTGetLogFunction();
|
|
RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
|
|
defaultLogFunction(level, source, fileName, lineNumber, message);
|
|
if (level >= RCTLogLevelError) {
|
|
if (errors == nil) {
|
|
errors = [NSMutableArray new];
|
|
}
|
|
[errors addObject:message];
|
|
}
|
|
});
|
|
|
|
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_scriptURL
|
|
moduleProvider:_moduleProvider
|
|
launchOptions:nil];
|
|
[bridge.devSettings setIsDebuggingRemotely:_useJSDebugger];
|
|
batchedBridge = [bridge batchedBridge];
|
|
|
|
UIViewController *vc = RCTSharedApplication().delegate.window.rootViewController;
|
|
vc.view = [UIView new];
|
|
|
|
RCTTestModule *testModule = [bridge moduleForClass:[RCTTestModule class]];
|
|
RCTAssert(_testController != nil, @"_testController should not be nil");
|
|
testModule.controller = _testController;
|
|
testModule.testSelector = test;
|
|
testModule.testSuffix = _testSuffix;
|
|
|
|
@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
|
|
|
|
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;
|
|
}
|
|
|
|
RCTSetLogFunction(defaultLogFunction);
|
|
|
|
#if RCT_DEV
|
|
NSArray<UIView *> *nonLayoutSubviews = [vc.view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id subview, NSDictionary *bindings) {
|
|
return ![NSStringFromClass([subview class]) isEqualToString:@"_UILayoutGuide"];
|
|
}]];
|
|
|
|
RCTAssert(nonLayoutSubviews.count == 0, @"There shouldn't be any other views: %@", nonLayoutSubviews);
|
|
#endif
|
|
|
|
if (expectErrorBlock) {
|
|
RCTAssert(expectErrorBlock(errors[0]), @"Expected an error but the first one was missing or did not match.");
|
|
} else {
|
|
RCTAssert(errors == nil, @"RedBox errors: %@", errors);
|
|
RCTAssert(testModule.status != RCTTestStatusPending, @"Test didn't finish within %0.f seconds", kTestTimeoutSeconds);
|
|
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];
|
|
}
|
|
|
|
// 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
|