react-native/Libraries/RCTTest/RCTTestRunner.m

170 lines
6.8 KiB
Mathematica
Raw Normal View History

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTTestRunner.h"
2015-03-24 17:20:13 +00:00
#import "FBSnapshotTestController.h"
#import "RCTAssert.h"
#import "RCTLog.h"
#import "RCTRootView.h"
#import "RCTTestModule.h"
#import "RCTUtils.h"
#import "RCTJSCExecutor.h"
static const NSTimeInterval kTestTimeoutSeconds = 60;
static const NSTimeInterval kTestTeardownTimeoutSeconds = 30;
@implementation RCTTestRunner
2015-03-24 17:20:13 +00:00
{
FBSnapshotTestController *_testController;
RCTBridgeModuleProviderBlock _moduleProvider;
2015-03-24 17:20:13 +00:00
}
- (instancetype)initWithApp:(NSString *)app
referenceDirectory:(NSString *)referenceDirectory
moduleProvider:(RCTBridgeModuleProviderBlock)block
{
RCTAssertParam(app);
RCTAssertParam(referenceDirectory);
2015-03-24 17:20:13 +00:00
if ((self = [super init])) {
if (!referenceDirectory.length) {
referenceDirectory = [[NSBundle bundleForClass:self.class].resourcePath stringByAppendingPathComponent:@"ReferenceImages"];
}
2015-03-24 17:20:13 +00:00
NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"];
sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"];
_testController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName];
_testController.referenceImagesDirectory = referenceDirectory;
_moduleProvider = [block copy];
#if RUNNING_ON_CI
_scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"];
RCTAssert(_scriptURL != nil, @"Could not locate main.jsBundle");
#else
_scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true", app]];
#endif
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
2015-03-24 17:20:13 +00:00
- (void)setRecordMode:(BOOL)recordMode
{
_testController.recordMode = recordMode;
2015-03-24 17:20:13 +00:00
}
- (BOOL)recordMode
{
return _testController.recordMode;
}
2015-03-24 17:20:13 +00:00
- (void)runTest:(SEL)test module:(NSString *)moduleName
{
[self runTest:test module:moduleName initialProps:nil configurationBlock:nil expectErrorBlock:nil];
2015-03-24 17:20:13 +00:00
}
- (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
2015-03-24 17:20:13 +00:00
{
BOOL(^expectErrorBlock)(NSString *error) = ^BOOL(NSString *error){
2015-04-11 22:08:00 +00:00
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 id weakJSContext;
@autoreleasepool {
__block NSString *error = nil;
RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
if (level >= RCTLogLevelError) {
error = message;
}
});
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_scriptURL
moduleProvider:_moduleProvider
launchOptions:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProps];
rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
RCTTestModule *testModule = [rootView.bridge moduleForClass:[RCTTestModule class]];
RCTAssert(_testController != nil, @"_testController should not be nil");
testModule.controller = _testController;
testModule.testSelector = test;
testModule.view = rootView;
UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController;
vc.view = [UIView new];
[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 && error == nil) {
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
[[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
// Take a weak reference to the JS context, so we track its deallocation later
// (we can only do this now, since it's been lazily initialized)
id jsExecutor = [bridge valueForKeyPath:@"batchedBridge.javaScriptExecutor"];
if ([jsExecutor isKindOfClass:[RCTJSCExecutor class]]) {
weakJSContext = [jsExecutor valueForKey:@"context"];
}
[rootView removeFromSuperview];
RCTSetLogFunction(RCTDefaultLogFunction);
2015-03-26 01:59:42 +00:00
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);
if (expectErrorBlock) {
RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched.");
} else {
RCTAssert(error == nil, @"RedBox error: %@", error);
RCTAssert(testModule.status != RCTTestStatusPending, @"Test didn't finish within %0.f seconds", kTestTimeoutSeconds);
RCTAssert(testModule.status == RCTTestStatusPassed, @"Test failed");
}
[bridge invalidate];
}
// Wait for the executor to have shut down completely before returning
NSDate *teardownTimeout = [NSDate dateWithTimeIntervalSinceNow:kTestTeardownTimeoutSeconds];
while (teardownTimeout.timeIntervalSinceNow > 0 && weakJSContext) {
2015-05-06 16:33:20 +00:00
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
[[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
RCTAssert(!weakJSContext, @"JS context was not deallocated after being invalidated");
}
@end