Update React Tests to support React Native 0.18

We had to shift to using an event-based architecture to communicate with the page from native code.
This commit is contained in:
Scott Kyle 2016-01-06 15:24:00 -08:00
parent 381dbbe7ba
commit d7f80e22c8
7 changed files with 175 additions and 93 deletions

View File

@ -5,8 +5,21 @@
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
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

View File

@ -2,20 +2,29 @@
* Proprietary and Confidential
*/
extern "C" {
#import "RealmReact.h"
#import "RCTBridge.h"
#import <RealmJS/js_init.h>
#import "js_init.h"
#import "shared_realm.hpp"
#import <objc/runtime.h>
#import <dlfcn.h>
#if DEBUG
#import <GCDWebServer/Core/GCDWebServer.h>
#import <GCDWebServer/Requests/GCDWebServerDataRequest.h>
#import <GCDWebServer/Responses/GCDWebServerDataResponse.h>
#import <GCDWebServer/Responses/GCDWebServerErrorResponse.h>
#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 <GCDWebServer/Core/GCDWebServer.h>
#import <GCDWebServer/Requests/GCDWebServerDataRequest.h>
#import <GCDWebServer/Responses/GCDWebServerDataResponse.h>
#import <GCDWebServer/Responses/GCDWebServerErrorResponse.h>
#import <RealmJS/rpc.hpp>
@interface RealmReact () {
GCDWebServer *_webServer;
std::unique_ptr<realm_js::RPCServer> _rpcServer;
}
@interface RealmReact () <RCTBridgeModule>
@end
#endif
@interface RealmReact () <RCTBridgeModule> {
@implementation RealmReact {
NSMutableDictionary *_eventHandlers;
__weak NSThread *_currentJSThread;
__weak NSRunLoop *_currentJSRunLoop;
#if DEBUG
GCDWebServer *_webServer;
std::unique_ptr<realm_js::RPCServer> _rpcServer;
#endif
}
@end
static __weak RealmReact *s_currentRealmModule = nil;
@implementation RealmReact
@synthesize bridge = _bridge;
@ -88,6 +83,34 @@ 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] ?: (_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 +162,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 +173,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 +183,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<RCTJavaScriptExecutor> 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 +204,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);
}];

View File

@ -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
}
}

View File

@ -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 (
<View style={styles.container}>
@ -60,7 +79,7 @@ var ReactTests = React.createClass({
</View>
);
}
});
}
var styles = StyleSheet.create({
container: {

View File

@ -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.

View File

@ -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<RCTJavaScriptExecutor>)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];
// 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 bridge;
}
return [s_bridge valueForKey:@"javaScriptExecutor"];
+ (id<RCTJavaScriptExecutor>)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<RCTJavaScriptExecutor> 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"}];
}
}
dispatch_async(dispatch_get_main_queue(), ^{
condition = YES;
result = json;
result = object;
};
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];
}
}

View File

@ -6,7 +6,7 @@
"start": "react-native start"
},
"dependencies": {
"react-native": "0.16.0",
"react-native": "0.18.0-rc",
"realm": "file:../..",
"realm-tests": "file:../lib"
}