//////////////////////////////////////////////////////////////////////////// // // Copyright 2016 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////// #import "RealmJSTests.h" #import "RCTJavaScriptExecutor.h" #import "RCTBridge.h" #import "RCTDevMenu.h" #import "RCTEventDispatcher.h" #import "RCTJavaScriptLoader.h" #import "RCTLog.h" @import RealmReact; extern void JSGlobalContextSetIncludesNativeCallStackWhenReportingExceptions(JSGlobalContextRef ctx, bool includesNativeCallStack); extern NSMutableArray *RCTGetModuleClasses(void); @interface RCTBridge () + (instancetype)currentBridge; - (void)setUp; @end @interface RCTDevMenuDisabler : RCTDevMenu @end @interface RealmReactTests : RealmJSTests @end @interface RealmReactChromeTests : RealmReactTests @end @implementation RCTDevMenuDisabler + (void)load { // +[RCTDevMenu load] is guaranteed to have been called since it's the superclass. // We remove it since it interferes with us fully controlling the executor class. NSMutableArray *moduleClasses = RCTGetModuleClasses(); [moduleClasses removeObject:[RCTDevMenu class]]; } @end @implementation RealmReactTests + (void)load { RCTAddLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { NSAssert(level < RCTLogLevelError, RCTFormatLog(nil, level, fileName, lineNumber, message)); }); } + (Class)executorClass { return NSClassFromString(@"RCTJSCExecutor"); } + (NSString *)classNameSuffix { return @""; } + (RCTBridge *)currentBridge { Class executorClass = [self executorClass]; if (!executorClass) { return nil; } RCTBridge *bridge = [RCTBridge currentBridge]; if (!bridge.valid) { [self waitForNotification:RCTJavaScriptDidLoadNotification]; bridge = [RCTBridge currentBridge]; } if (bridge.executorClass != executorClass) { bridge.executorClass = executorClass; RCTBridge *parentBridge = [bridge valueForKey:@"parentBridge"]; [parentBridge invalidate]; [parentBridge setUp]; return [self currentBridge]; } return bridge; } + (id)currentExecutor { return [[self currentBridge] valueForKey:@"javaScriptExecutor"]; } + (XCTestSuite *)defaultTestSuite { XCTestSuite *suite = [super defaultTestSuite]; id executor = [self currentExecutor]; // The executor may be nil if the executorClass was not found (i.e. release build). if (!executor) { return suite; } // FIXME: Remove this nonsense once the crashes go away when a test fails! JSGlobalContextRef ctx = RealmReactGetJSGlobalContextForExecutor(executor, false); if (ctx) { JSGlobalContextSetIncludesNativeCallStackWhenReportingExceptions(ctx, false); } NSDictionary *testCaseNames = [self waitForEvent:@"realm-test-names"]; NSAssert(testCaseNames.count, @"No test names were provided by the JS"); NSString *nameSuffix = [self classNameSuffix]; if (nameSuffix.length) { NSMutableDictionary *renamedTestCaseNames = [[NSMutableDictionary alloc] init]; for (NSString *name in testCaseNames) { renamedTestCaseNames[[name stringByAppendingString:nameSuffix]] = testCaseNames[name]; } testCaseNames = renamedTestCaseNames; } for (XCTestSuite *testSuite in [self testSuitesFromDictionary:testCaseNames]) { [suite addTest:testSuite]; } return suite; } + (NSNotification *)waitForNotification:(NSString *)notificationName { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; __block BOOL condition = NO; __block NSNotification *notification; id token = [nc addObserverForName:notificationName object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { condition = YES; notification = note; }]; @try { [self waitForCondition:&condition description:notificationName]; } @finally { [nc removeObserver:token]; } return notification; } + (void)waitForCondition:(BOOL *)condition description:(NSString *)description { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:30.0]; while (!*condition) { if ([timeout timeIntervalSinceNow] < 0) { @throw [NSException exceptionWithName:@"ConditionTimeout" reason:[NSString stringWithFormat:@"Timed out waiting for: %@", description] userInfo:nil]; } @autoreleasepool { [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [runLoop runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [NSThread sleepForTimeInterval:0.01]; // Bad things may happen without some sleep. } } } + (id)waitForEvent:(NSString *)eventName { __weak RealmReact *realmModule = [[self currentBridge] moduleForClass:[RealmReact class]]; NSAssert(realmModule, @"RealmReact module not found"); __block BOOL condition = NO; __block id result; __block RealmReactEventHandler handler; __block __weak RealmReactEventHandler weakHandler = handler = ^(id object) { [realmModule removeListenerForEvent:eventName handler:weakHandler]; condition = YES; result = object; }; [realmModule addListenerForEvent:eventName handler:handler]; [self waitForCondition:&condition description:eventName]; return result; } - (void)invokeTest { RCTLogFunction logFunction = RCTGetLogFunction(); // Fail when React Native logs an error. RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { RCTDefaultLogFunction(level, source, fileName, lineNumber, message); if (level >= RCTLogLevelError) { NSString *type = (source == RCTLogSourceJavaScript) ? @"JS" : @"Native"; XCTFail(@"%@ Error: %@", type, RCTFormatLog(nil, level, fileName, lineNumber, message)); } }); [super invokeTest]; RCTSetLogFunction(logFunction); } - (void)invokeMethod:(NSString *)method { NSString *module = NSStringFromClass(self.class); NSString *suffix = [self.class classNameSuffix]; if (suffix.length && [module hasSuffix:suffix]) { module = [module substringToIndex:(module.length - suffix.length)]; } RCTBridge *bridge = [self.class currentBridge]; [bridge.eventDispatcher sendAppEventWithName:@"realm-run-test" body:@{@"suite": module, @"name": method}]; id error; @try { error = [self.class waitForEvent:@"realm-test-finished"]; } @catch (id exception) { error = exception; } if (error) { [self recordFailureWithDescription:[error description] inFile:@(__FILE__) atLine:__LINE__ expected:YES]; } } @end @implementation RealmReactChromeTests + (Class)executorClass { return NSClassFromString(@"RCTWebSocketExecutor"); } + (NSString *)classNameSuffix { return @"_Chrome"; } @end