diff --git a/react-native/RealmReact.h b/react-native/RealmReact.h index 8875e8e8..d0f4113d 100644 --- a/react-native/RealmReact.h +++ b/react-native/RealmReact.h @@ -5,8 +5,21 @@ #import #import -extern JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executor, bool create); +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (^RealmReactEventHandler)(id message); + +JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executor, bool create); @interface RealmReact : NSObject +- (void)addListenerForEvent:(NSString *)eventName handler:(RealmReactEventHandler)handler; +- (void)removeListenerForEvent:(NSString *)eventName handler:(RealmReactEventHandler)handler; + @end + +#ifdef __cplusplus +} +#endif diff --git a/react-native/RealmReact.mm b/react-native/RealmReact.mm index f2c7a0be..6715a88c 100644 --- a/react-native/RealmReact.mm +++ b/react-native/RealmReact.mm @@ -2,20 +2,29 @@ * Proprietary and Confidential */ -extern "C" { #import "RealmReact.h" #import "RCTBridge.h" -#import +#import "js_init.h" +#import "shared_realm.hpp" + #import #import +#if DEBUG +#import +#import +#import +#import +#import "rpc.hpp" +#endif + @interface NSObject () - (instancetype)initWithJSContext:(void *)context; - (JSGlobalContextRef)ctx; @end -JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executor, bool create) { +extern "C" JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executor, bool create) { Ivar contextIvar = class_getInstanceVariable([executor class], "_context"); if (!contextIvar) { return NULL; @@ -42,34 +51,20 @@ JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executor, bool cre return [rctJSContext ctx]; } -} -#import "shared_realm.hpp" - -#if DEBUG -#import -#import -#import -#import -#import - -@interface RealmReact () { - GCDWebServer *_webServer; - std::unique_ptr _rpcServer; -} +@interface RealmReact () @end -#endif -@interface RealmReact () { +@implementation RealmReact { + NSMutableDictionary *_eventHandlers; __weak NSThread *_currentJSThread; __weak NSRunLoop *_currentJSRunLoop; + +#if DEBUG + GCDWebServer *_webServer; + std::unique_ptr _rpcServer; +#endif } -@end - -static __weak RealmReact *s_currentRealmModule = nil; - - -@implementation RealmReact @synthesize bridge = _bridge; @@ -88,6 +83,37 @@ static __weak RealmReact *s_currentRealmModule = nil; return @"Realm"; } +- (instancetype)init { + self = [super init]; + if (self) { + _eventHandlers = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (dispatch_queue_t)methodQueue { + return dispatch_get_main_queue(); +} + +- (void)addListenerForEvent:(NSString *)eventName handler:(RealmReactEventHandler)handler { + NSMutableOrderedSet *handlers = _eventHandlers[eventName]; + if (!handlers) { + handlers = _eventHandlers[eventName] = [[NSMutableOrderedSet alloc] init]; + } + [handlers addObject:handler]; +} + +- (void)removeListenerForEvent:(NSString *)eventName handler:(RealmReactEventHandler)handler { + NSMutableOrderedSet *handlers = _eventHandlers[eventName]; + [handlers removeObject:handler]; +} + +RCT_REMAP_METHOD(emit, emitEvent:(NSString *)eventName withObject:(id)object) { + for (RealmReactEventHandler handler in [_eventHandlers[eventName] copy]) { + handler(object); + } +} + #if DEBUG - (void)startRPC { [GCDWebServer setLogLevel:3]; @@ -139,11 +165,9 @@ static __weak RealmReact *s_currentRealmModule = nil; _webServer = nil; _rpcServer.reset(); } - - #endif -- (void)shutdown { +- (void)invalidate { #if DEBUG // shutdown rpc if in chrome debug mode [self shutdownRPC]; @@ -152,7 +176,7 @@ static __weak RealmReact *s_currentRealmModule = nil; // block until JS thread exits NSRunLoop *runLoop = _currentJSRunLoop; if (runLoop) { - CFRunLoopStop([_currentJSRunLoop getCFRunLoop]); + CFRunLoopStop([runLoop getCFRunLoop]); while (_currentJSThread && !_currentJSThread.finished) { [NSThread sleepForTimeInterval:0.01]; } @@ -162,18 +186,18 @@ static __weak RealmReact *s_currentRealmModule = nil; } - (void)dealloc { - [self shutdown]; + [self performSelectorOnMainThread:@selector(invalidate) withObject:nil waitUntilDone:YES]; } - (void)setBridge:(RCTBridge *)bridge { _bridge = bridge; - // shutdown the last instance of this module - [s_currentRealmModule shutdown]; - s_currentRealmModule = self; + static __weak RealmReact *s_currentModule = nil; + [s_currentModule invalidate]; + s_currentModule = self; + + id executor = [bridge valueForKey:@"javaScriptExecutor"]; - Ivar executorIvar = class_getInstanceVariable([bridge class], "_javaScriptExecutor"); - id executor = object_getIvar(bridge, executorIvar); if ([executor isKindOfClass:NSClassFromString(@"RCTWebSocketExecutor")]) { #if DEBUG [self startRPC]; @@ -183,13 +207,16 @@ static __weak RealmReact *s_currentRealmModule = nil; } else { __weak __typeof__(self) weakSelf = self; + [executor executeBlockOnJavaScriptQueue:^{ - RealmReact *self = weakSelf; + __typeof__(self) self = weakSelf; if (!self) { return; } + self->_currentJSThread = [NSThread currentThread]; self->_currentJSRunLoop = [NSRunLoop currentRunLoop]; + JSGlobalContextRef ctx = RealmReactGetJSGlobalContextForExecutor(executor, true); RJSInitializeInContext(ctx); }]; diff --git a/tests/react-test-app/.eslintrc b/tests/react-test-app/.eslintrc new file mode 100644 index 00000000..48979dc5 --- /dev/null +++ b/tests/react-test-app/.eslintrc @@ -0,0 +1,33 @@ +{ + "env": { + "commonjs": true, + "es6": true + }, + "ecmaFeatures": { + "jsx": true + }, + "globals": { + "cancelAnimationFrame": false, + "clearImmediate": false, + "clearInterval": false, + "clearTimeout": false, + "console": false, + "global": false, + "requestAnimationFrame": false, + "setImmediate": false, + "setInterval": false, + "setTimeout": false + }, + "plugins": [ + "react" + ], + "rules": { + "no-console": 0, + "react/jsx-no-duplicate-props": 2, + "react/jsx-no-undef": 2, + "react/jsx-uses-react": 2, + "react/no-direct-mutation-state": 1, + "react/prefer-es6-class": 1, + "react/react-in-jsx-scope": 2 + } +} diff --git a/tests/react-test-app/index.ios.js b/tests/react-test-app/index.ios.js index 0a9a070e..de6bb819 100644 --- a/tests/react-test-app/index.ios.js +++ b/tests/react-test-app/index.ios.js @@ -4,18 +4,37 @@ 'use strict'; -var React = require('react-native'); -var Realm = require('realm'); -var RealmTests = require('realm-tests'); +const React = require('react-native'); +const Realm = require('realm'); +const RealmTests = require('realm-tests'); -var { +const { AppRegistry, + NativeAppEventEmitter, + NativeModules, StyleSheet, Text, TouchableHighlight, View, } = React; +// Listen for event to run a particular test. +NativeAppEventEmitter.addListener('realm-run-test', (test) => { + let error; + try { + RealmTests.runTest(test.suite, test.name); + } catch (e) { + error = '' + e; + } + + NativeModules.Realm.emit('realm-test-finished', error); +}); + +// Inform the native test harness about the test suite once it's ready. +setTimeout(() => { + NativeModules.Realm.emit('realm-test-names', RealmTests.getTestNames()); +}, 0); + function runTests() { let testNames = RealmTests.getTestNames(); @@ -46,7 +65,7 @@ function runTests() { } } -var ReactTests = React.createClass({ +class ReactTests extends React.Component { render() { return ( @@ -60,7 +79,7 @@ var ReactTests = React.createClass({ ); } -}); +} var styles = StyleSheet.create({ container: { diff --git a/tests/react-test-app/ios/ReactTests/AppDelegate.m b/tests/react-test-app/ios/ReactTests/AppDelegate.m index 520edd82..817550db 100644 --- a/tests/react-test-app/ios/ReactTests/AppDelegate.m +++ b/tests/react-test-app/ios/ReactTests/AppDelegate.m @@ -18,7 +18,7 @@ static NSString * const RCTDevMenuKey = @"RCTDevMenu"; NSMutableDictionary *settings = [([defaults dictionaryForKey:RCTDevMenuKey] ?: @{}) mutableCopy]; NSMutableDictionary *domain = [[defaults volatileDomainForName:NSArgumentDomain] mutableCopy]; - settings[@"executorClass"] = [defaults boolForKey:RealmReactEnableChromeDebuggingKey] ? @"RCTWebSocketExecutor" : @"RCTContextExecutor"; + settings[@"executorClass"] = [defaults boolForKey:RealmReactEnableChromeDebuggingKey] ? @"RCTWebSocketExecutor" : @"RCTJSCExecutor"; domain[RCTDevMenuKey] = settings; // Re-register the arguments domain (highest precedent and volatile) with our new overridden settings. diff --git a/tests/react-test-app/ios/ReactTests/RealmReactTests.m b/tests/react-test-app/ios/ReactTests/RealmReactTests.m index df321edd..50a3c9c4 100644 --- a/tests/react-test-app/ios/ReactTests/RealmReactTests.m +++ b/tests/react-test-app/ios/ReactTests/RealmReactTests.m @@ -6,12 +6,18 @@ #import "RCTJavaScriptExecutor.h" #import "RCTBridge.h" #import "RCTDevMenu.h" +#import "RCTEventDispatcher.h" @import RealmReact; extern void JSGlobalContextSetIncludesNativeCallStackWhenReportingExceptions(JSGlobalContextRef ctx, bool includesNativeCallStack); extern NSMutableArray *RCTGetModuleClasses(void); +@interface RCTBridge () ++ (instancetype)currentBridge; +- (void)setUp; +@end + @interface RealmReactTests : RealmJSTests @end @@ -26,41 +32,40 @@ extern NSMutableArray *RCTGetModuleClasses(void); } + (Class)executorClass { - return NSClassFromString(@"RCTContextExecutor"); + return NSClassFromString(@"RCTJSCExecutor"); } + (NSString *)classNameSuffix { return @""; } -+ (id)currentExecutor { ++ (RCTBridge *)currentBridge { Class executorClass = [self executorClass]; if (!executorClass) { return nil; } - static RCTBridge *s_bridge; - if (!s_bridge) { - [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; + RCTBridge *bridge = [RCTBridge currentBridge]; + if (!bridge.valid) { + [self waitForNotification:RCTJavaScriptDidLoadNotification]; + bridge = [RCTBridge currentBridge]; } - if (!s_bridge.valid) { - NSNotification *notification = [self waitForNotification:RCTJavaScriptDidLoadNotification]; - s_bridge = notification.userInfo[@"bridge"]; - assert(s_bridge); + if (bridge.executorClass != executorClass) { + bridge.executorClass = executorClass; + + RCTBridge *parentBridge = [bridge valueForKey:@"parentBridge"]; + [parentBridge invalidate]; + [parentBridge setUp]; + + return [self currentBridge]; } - if (s_bridge.executorClass != executorClass) { - s_bridge.executorClass = executorClass; - [s_bridge reload]; + return bridge; +} - // The [RCTBridge reload] method does a dispatch_async that we must run before trying again. - [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - - return [self currentExecutor]; - } - - return [s_bridge valueForKey:@"javaScriptExecutor"]; ++ (id)currentExecutor { + return [[self currentBridge] valueForKey:@"javaScriptExecutor"]; } + (XCTestSuite *)defaultTestSuite { @@ -78,11 +83,9 @@ extern NSMutableArray *RCTGetModuleClasses(void); JSGlobalContextSetIncludesNativeCallStackWhenReportingExceptions(ctx, false); } - NSError *error; - NSDictionary *testCaseNames = [self invokeMethod:@"getTestNames" arguments:nil error:&error]; - - if (error || !testCaseNames.count) { - NSLog(@"Error from calling getTestNames() - %@", error ?: @"None returned"); + NSDictionary *testCaseNames = [self waitForEvent:@"realm-test-names"]; + if (!testCaseNames.count) { + NSLog(@"ERROR: No test names were provided by the JS"); exit(1); } @@ -128,34 +131,24 @@ extern NSMutableArray *RCTGetModuleClasses(void); } } -+ (id)invokeMethod:(NSString *)method arguments:(NSArray *)arguments error:(NSError * __strong *)outError { - id executor = [self currentExecutor]; ++ (id)waitForEvent:(NSString *)eventName { + __weak RealmReact *realmModule = [[self currentBridge] moduleForClass:[RealmReact class]]; + assert(realmModule); __block BOOL condition = NO; __block id result; + __block RealmReactEventHandler handler; - [executor executeJSCall:@"realm-tests/index.js" method:method arguments:(arguments ?: @[]) callback:^(id json, NSError *error) { - // The React Native debuggerWorker.js very bizarrely returns an array five empty arrays to signify an error. - if ([json isKindOfClass:[NSArray class]] && [json isEqualToArray:@[@[], @[], @[], @[], @[]]]) { - json = nil; + __block __weak RealmReactEventHandler weakHandler = handler = ^(id object) { + [realmModule removeListenerForEvent:eventName handler:weakHandler]; - if (!error) { - error = [NSError errorWithDomain:@"JS" code:1 userInfo:@{NSLocalizedDescriptionKey: @"unknown JS error"}]; - } - } + condition = YES; + result = object; + }; - dispatch_async(dispatch_get_main_queue(), ^{ - condition = YES; - result = json; - - if (error && outError) { - *outError = error; - } - }); - }]; + [realmModule addListenerForEvent:eventName handler:handler]; [self waitForCondition:&condition]; - return result; } @@ -167,12 +160,12 @@ extern NSMutableArray *RCTGetModuleClasses(void); module = [module substringToIndex:(module.length - suffix.length)]; } - NSError *error; - [self.class invokeMethod:@"runTest" arguments:@[module, method] error:&error]; + RCTBridge *bridge = [self.class currentBridge]; + [bridge.eventDispatcher sendAppEventWithName:@"realm-run-test" body:@{@"suite": module, @"name": method}]; + id error = [self.class waitForEvent:@"realm-test-finished"]; if (error) { - // TODO: Parse and use localizedFailureReason info once we can source map the failure location in JS. - [self recordFailureWithDescription:error.localizedDescription inFile:@(__FILE__) atLine:__LINE__ expected:YES]; + [self recordFailureWithDescription:[error description] inFile:@(__FILE__) atLine:__LINE__ expected:YES]; } } diff --git a/tests/react-test-app/package.json b/tests/react-test-app/package.json index 0bfa7449..a0a9159e 100644 --- a/tests/react-test-app/package.json +++ b/tests/react-test-app/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "react-native": "^0.18.0-rc", - "realm": "file:../../lib", + "realm": "file:../..", "realm-tests": "file:../lib" } }