diff --git a/examples/ReactExample/.eslintrc b/examples/ReactExample/.eslintrc index 98b5f8ba..2e863dc5 100644 --- a/examples/ReactExample/.eslintrc +++ b/examples/ReactExample/.eslintrc @@ -6,10 +6,24 @@ "ecmaFeatures": { "jsx": true }, + "globals": { + "cancelAnimationFrame": false, + "clearImmediate": false, + "clearInterval": false, + "clearTimeout": false, + "console": false, + "global": false, + "requestAnimationFrame": false, + "setImmediate": false, + "setInterval": false, + "setTimeout": false + }, + "parser": "babel-eslint", "plugins": [ "react" ], "rules": { + "strict": 0, "react/jsx-no-duplicate-props": 2, "react/jsx-no-undef": 2, "react/jsx-uses-react": 2, diff --git a/examples/ReactExample/components/realm.js b/examples/ReactExample/components/realm.js index d3f44bed..6640e717 100644 --- a/examples/ReactExample/components/realm.js +++ b/examples/ReactExample/components/realm.js @@ -4,9 +4,9 @@ 'use strict'; -const Realm = require('realm'); +import Realm from 'realm'; -module.exports = new Realm({ +export default new Realm({ schema: [ { name: 'Todo', diff --git a/examples/ReactExample/components/styles.js b/examples/ReactExample/components/styles.js index 9ab48593..b8807129 100644 --- a/examples/ReactExample/components/styles.js +++ b/examples/ReactExample/components/styles.js @@ -4,14 +4,16 @@ 'use strict'; -const React = require('react-native'); +import { + Navigator, + Platform, + StyleSheet +} from 'react-native'; -const { Navigator, Platform, StyleSheet } = React; const { NavBarHeight, TotalNavHeight } = Navigator.NavigationBar.Styles.General; - const iOS = (Platform.OS == 'ios'); -module.exports = StyleSheet.create({ +export default StyleSheet.create({ container: { flex: 1, justifyContent: 'center', diff --git a/examples/ReactExample/components/todo-app.js b/examples/ReactExample/components/todo-app.js index 69936fdc..375163ae 100644 --- a/examples/ReactExample/components/todo-app.js +++ b/examples/ReactExample/components/todo-app.js @@ -4,21 +4,21 @@ 'use strict'; -const React = require('react-native'); -const TodoItem = require('./todo-item'); -const TodoListView = require('./todo-listview'); -const realm = require('./realm'); -const styles = require('./styles'); - -const { +import React, { + Component, Navigator, StatusBarIOS, Text, TouchableOpacity, View, -} = React; +} from 'react-native'; -class TodoApp extends React.Component { +import TodoItem from './todo-item'; +import TodoListView from './todo-listview'; +import realm from './realm'; +import styles from './styles'; + +export default class TodoApp extends Component { constructor(props) { super(props); @@ -150,8 +150,11 @@ class TodoApp extends React.Component { } _setEditingRow(rowIndex) { + let listView = this.currentListView; + // Update the state on the currently displayed TodoList to edit this new item. - this.currentListView.setState({editingRow: rowIndex}); + listView.setState({editingRow: rowIndex}); + listView.updateDataSource(); } } @@ -200,5 +203,3 @@ const RouteMapper = { ); }, }; - -module.exports = TodoApp; diff --git a/examples/ReactExample/components/todo-item.js b/examples/ReactExample/components/todo-item.js index b8306d73..76b8c572 100644 --- a/examples/ReactExample/components/todo-item.js +++ b/examples/ReactExample/components/todo-item.js @@ -4,14 +4,17 @@ 'use strict'; -const React = require('react-native'); -const TodoListItem = require('./todo-list-item'); -const realm = require('./realm'); -const styles = require('./styles'); +import React, { + Text, + TouchableWithoutFeedback, + View, +} from 'react-native'; -const { Text, TouchableWithoutFeedback, View } = React; +import TodoListItem from './todo-list-item'; +import realm from './realm'; +import styles from './styles'; -class TodoItem extends TodoListItem { +export default class TodoItem extends TodoListItem { constructor(props) { super(props); @@ -56,5 +59,3 @@ class TodoItem extends TodoListItem { this.forceUpdate(); } } - -module.exports = TodoItem; diff --git a/examples/ReactExample/components/todo-list-item.js b/examples/ReactExample/components/todo-list-item.js index ac82b878..fe4e43f0 100644 --- a/examples/ReactExample/components/todo-list-item.js +++ b/examples/ReactExample/components/todo-list-item.js @@ -4,21 +4,20 @@ 'use strict'; -const React = require('react-native'); -const realm = require('./realm'); -const styles = require('./styles'); - -const { +import React, { Platform, Text, TextInput, TouchableWithoutFeedback, View, -} = React; +} from 'react-native'; + +import realm from './realm'; +import styles from './styles'; const iOS = (Platform.OS == 'ios'); -class TodoListItem extends React.Component { +export default class TodoListItem extends React.Component { constructor(props) { super(props); @@ -123,5 +122,3 @@ class TodoListItem extends React.Component { } } } - -module.exports = TodoListItem; diff --git a/examples/ReactExample/components/todo-listview.js b/examples/ReactExample/components/todo-listview.js index 3e3ec407..ad4f2945 100644 --- a/examples/ReactExample/components/todo-listview.js +++ b/examples/ReactExample/components/todo-listview.js @@ -4,26 +4,38 @@ 'use strict'; -const React = require('react-native'); -const TodoListItem = require('./todo-list-item'); -const realm = require('./realm'); -const styles = require('./styles'); +import React, { + Text, + View, +} from 'react-native'; -const { ListView, Text, View } = React; +import { ListView } from 'realm/react-native'; +import TodoListItem from './todo-list-item'; +import realm from './realm'; +import styles from './styles'; -class TodoListView extends React.Component { +export default class TodoListView extends React.Component { constructor(props) { super(props); - this.dataSource = new ListView.DataSource({ - sectionHeaderHasChanged: () => false, - rowHasChanged: (row1, row2) => row1 !== row2 + let dataSource = new ListView.DataSource({ + rowHasChanged(a, b) { + // Always re-render TodoList items. + return a.done !== b.done || a.text !== b.text || a.items || b.items; + } }); - this.state = {}; + this.state = { + dataSource: this._cloneDataSource(dataSource, props), + }; + this.renderRow = this.renderRow.bind(this); } + componentWillReceiveProps(props) { + this.updateDataSource(props); + } + componentDidUpdate() { let items = this.props.items; let editingRow = this.state.editingRow; @@ -43,19 +55,9 @@ class TodoListView extends React.Component { } render() { - // Clone the items into a new Array to prevent unexpected errors from changes in length. - let sections = [Array.from(this.props.items)]; - let extraItems = this.props.extraItems; - - if (extraItems && extraItems.length) { - sections.push(extraItems); - } - - let dataSource = this.dataSource.cloneWithRowsAndSections(sections); - return ( - + Press Cmd+R to reload,{'\n'} Cmd+D for dev menu @@ -85,6 +87,24 @@ class TodoListView extends React.Component { ); } + updateDataSource(props=this.props) { + this.setState({ + dataSource: this._cloneDataSource(this.state.dataSource, props), + }); + } + + _cloneDataSource(dataSource, props) { + let items = props.items; + let extraItems = props.extraItems; + let sections = [items ? items.snapshot() : []]; + + if (extraItems && extraItems.length) { + sections.push(extraItems); + } + + return dataSource.cloneWithRowsAndSections(sections); + } + _onPressRow(item, sectionIndex, rowIndex) { let onPressItem = this.props.onPressItem; if (onPressItem) { @@ -100,12 +120,13 @@ class TodoListView extends React.Component { _onPressDeleteRow(item) { this._deleteItem(item); - this.forceUpdate(); + this.updateDataSource(); } _onEndEditingRow(item, rowIndex) { - this._deleteItemIfEmpty(item); - + if (this._deleteItemIfEmpty(item)) { + this.updateDataSource(); + } if (this.state.editingRow == rowIndex) { this.setState({editingRow: null}); } @@ -155,5 +176,3 @@ class TodoListExtraItem extends TodoListItem { return null; } } - -module.exports = TodoListView; diff --git a/examples/ReactExample/index.android.js b/examples/ReactExample/index.android.js index c506bc04..77d06c34 100644 --- a/examples/ReactExample/index.android.js +++ b/examples/ReactExample/index.android.js @@ -1,10 +1,7 @@ -/* Copyright 2015 Realm Inc - All Rights Reserved +/* Copyright 2016 Realm Inc - All Rights Reserved * Proprietary and Confidential */ 'use strict'; -const React = require('react-native'); -const TodoApp = require('./components/todo-app'); - -React.AppRegistry.registerComponent('ReactExample', () => TodoApp); +import './main'; diff --git a/examples/ReactExample/index.ios.js b/examples/ReactExample/index.ios.js index c506bc04..b9c970ad 100644 --- a/examples/ReactExample/index.ios.js +++ b/examples/ReactExample/index.ios.js @@ -1,10 +1,9 @@ -/* Copyright 2015 Realm Inc - All Rights Reserved +/* Copyright 2016 Realm Inc - All Rights Reserved * Proprietary and Confidential */ 'use strict'; -const React = require('react-native'); -const TodoApp = require('./components/todo-app'); - -React.AppRegistry.registerComponent('ReactExample', () => TodoApp); +// Allow our test harness to test this app. +import './tests'; +import './main'; diff --git a/examples/ReactExample/ios/ReactExample.xcodeproj/project.pbxproj b/examples/ReactExample/ios/ReactExample.xcodeproj/project.pbxproj index 05d27062..4a7a9dfa 100644 --- a/examples/ReactExample/ios/ReactExample.xcodeproj/project.pbxproj +++ b/examples/ReactExample/ios/ReactExample.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; - 027798491BBB2F1000C96559 /* ReactExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 027798481BBB2F1000C96559 /* ReactExampleTests.m */; }; 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; @@ -24,6 +23,8 @@ 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; F6C4656F1C48DBE900E79896 /* RealmReact.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6C465551C48D4C300E79896 /* RealmReact.framework */; }; F6C465701C48DBF700E79896 /* RealmReact.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F6C465551C48D4C300E79896 /* RealmReact.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + F6DC9DE51C519CFF00ED587E /* RealmJSTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F6DC9DE41C519CFF00ED587E /* RealmJSTests.mm */; }; + F6DC9DE71C519D2300ED587E /* RealmReactTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F6DC9DE61C519D2300ED587E /* RealmReactTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -170,7 +171,6 @@ 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = "../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj"; sourceTree = ""; }; 0270BC9E1B7D04D700010E03 /* RealmJS.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RealmJS.xcodeproj; path = ../../../RealmJS.xcodeproj; sourceTree = ""; }; 027798461BBB2F1000C96559 /* ReactExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 027798481BBB2F1000C96559 /* ReactExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactExampleTests.m; sourceTree = ""; }; 0277984A1BBB2F1000C96559 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = ""; }; 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; @@ -184,6 +184,9 @@ 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; + F6DC9DE31C519CFF00ED587E /* RealmJSTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RealmJSTests.h; path = ../../../../tests/ios/RealmJSTests.h; sourceTree = ""; }; + F6DC9DE41C519CFF00ED587E /* RealmJSTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = RealmJSTests.mm; path = ../../../../tests/ios/RealmJSTests.mm; sourceTree = ""; }; + F6DC9DE61C519D2300ED587E /* RealmReactTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RealmReactTests.m; path = "../../../../tests/react-test-app/ios/ReactTests/RealmReactTests.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -258,7 +261,9 @@ 027798471BBB2F1000C96559 /* ReactExampleTests */ = { isa = PBXGroup; children = ( - 027798481BBB2F1000C96559 /* ReactExampleTests.m */, + F6DC9DE31C519CFF00ED587E /* RealmJSTests.h */, + F6DC9DE41C519CFF00ED587E /* RealmJSTests.mm */, + F6DC9DE61C519D2300ED587E /* RealmReactTests.m */, 0277984A1BBB2F1000C96559 /* Info.plist */, ); path = ReactExampleTests; @@ -644,7 +649,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "../node_modules/react-native/packager/react-native-xcode.sh"; + shellScript = "[ \"$PLATFORM_NAME\" = \"iphonesimulator\" ] && exit\n../node_modules/react-native/packager/react-native-xcode.sh"; }; /* End PBXShellScriptBuildPhase section */ @@ -653,7 +658,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 027798491BBB2F1000C96559 /* ReactExampleTests.m in Sources */, + F6DC9DE71C519D2300ED587E /* RealmReactTests.m in Sources */, + F6DC9DE51C519CFF00ED587E /* RealmJSTests.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -698,7 +704,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_TESTABILITY = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = ReactExampleTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; diff --git a/examples/ReactExample/ios/ReactExampleTests/ReactExampleTests.m b/examples/ReactExample/ios/ReactExampleTests/ReactExampleTests.m deleted file mode 100644 index 1d9d12eb..00000000 --- a/examples/ReactExample/ios/ReactExampleTests/ReactExampleTests.m +++ /dev/null @@ -1,64 +0,0 @@ -/* Copyright 2015 Realm Inc - All Rights Reserved - * Proprietary and Confidential - */ - -#import -#import - -#import "RCTLog.h" -#import "RCTRootView.h" - -#define TIMEOUT_SECONDS 10 -#define TEXT_TO_LOOK_FOR @"Todo Items" - -@interface ReactExampleTests : XCTestCase - -@end - -@implementation ReactExampleTests - -- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test -{ - if (test(view)) { - return YES; - } - for (UIView *subview in [view subviews]) { - if ([self findSubviewInView:subview matching:test]) { - return YES; - } - } - return NO; -} - -- (void)testLaunched -{ - UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; - NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; - BOOL foundElement = NO; - - __block NSString *redboxError = nil; - RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { - if (level >= RCTLogLevelError) { - redboxError = message; - } - }); - - while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - - foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { - if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { - return YES; - } - return NO; - }]; - } - - RCTSetLogFunction(RCTDefaultLogFunction); - - XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); - //XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); -} - -@end diff --git a/examples/ReactExample/main.js b/examples/ReactExample/main.js new file mode 100644 index 00000000..693f6378 --- /dev/null +++ b/examples/ReactExample/main.js @@ -0,0 +1,10 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +'use strict'; + +import { AppRegistry } from 'react-native'; +import TodoApp from './components/todo-app'; + +AppRegistry.registerComponent('ReactExample', () => TodoApp); diff --git a/examples/ReactExample/tests/example-test.js b/examples/ReactExample/tests/example-test.js new file mode 100644 index 00000000..0fe02e94 --- /dev/null +++ b/examples/ReactExample/tests/example-test.js @@ -0,0 +1,16 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +'use strict'; + +import { + getRootComponent, + assertChildExists, +} from './util'; + +export default { + async testTodoAppRendered() { + assertChildExists(await getRootComponent(), 'TodoApp'); + }, +} \ No newline at end of file diff --git a/examples/ReactExample/tests/index.js b/examples/ReactExample/tests/index.js new file mode 100644 index 00000000..03211a76 --- /dev/null +++ b/examples/ReactExample/tests/index.js @@ -0,0 +1,56 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +'use strict'; + +import { + NativeAppEventEmitter, + NativeModules, +} from 'react-native'; + +import ExampleTest from './example-test'; + +const TESTS = { + ExampleTest, +}; + +const SPECIAL_METHODS = { + beforeEach: true, + afterEach: true, +}; + +// Listen for event to run a particular test. +NativeAppEventEmitter.addListener('realm-run-test', async ({suite, name}) => { + let testSuite = TESTS[suite]; + let testMethod = testSuite && testSuite[name]; + let error; + + try { + if (testMethod) { + await testMethod.call(testSuite); + } else if (!testSuite || !(name in SPECIAL_METHODS)) { + throw new Error('Missing test: ' + suite + '.' + 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', getTestNames()); +}, 0); + +function getTestNames() { + let testNames = {}; + + for (let suiteName in TESTS) { + let testSuite = TESTS[suiteName]; + testNames[suiteName] = Object.keys(testSuite); + } + + return testNames; +} diff --git a/examples/ReactExample/tests/util.js b/examples/ReactExample/tests/util.js new file mode 100644 index 00000000..eceeb51b --- /dev/null +++ b/examples/ReactExample/tests/util.js @@ -0,0 +1,59 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +'use strict'; + +import React from 'react-native'; + +const rootComponentPromise = new Promise((resolve) => { + // Require internal module here so the promise is rejected if there is an error. + let Mount = require('react-native/Libraries/ReactNative/ReactNativeMount'); + let renderComponent = Mount.renderComponent; + + Mount.renderComponent = function() { + let component = renderComponent.apply(this, arguments); + + resolve(component); + return component; + }; +}); + +export function getRootComponent() { + return rootComponentPromise; +} + +export function assertChildExists(component, name) { + if (!findChildComponent(component, name)) { + throw new Error(name + ' not rendered'); + } +} + +export function findChildComponent(component, name) { + for (let child of traverseChildren(component)) { + if (child.type.name == name) { + return child; + } + } + return null; +} + +export function* traverseChildren(component) { + let props = component.props; + + // The hacky TopLevelWrapper has its props set to the root element. + if (props.props) { + props = props.props; + } + + let children = props.children; + if (!children) { + return; + } + + // ReactNative is missing React.Children.toArray() + for (let child of React.Children.map(children, (x) => x)) { + yield child; + yield* traverseChildren(child); + } +} diff --git a/lib/.eslintrc b/lib/.eslintrc index 50abf59d..432f0525 100644 --- a/lib/.eslintrc +++ b/lib/.eslintrc @@ -2,7 +2,7 @@ "env": { "commonjs": true, "browser": true, - "es6": true, + "es6": true }, "ecmaFeatures": { "forOf": false diff --git a/react-native/.eslintrc b/react-native/.eslintrc new file mode 100644 index 00000000..c027a69c --- /dev/null +++ b/react-native/.eslintrc @@ -0,0 +1,21 @@ +{ + "env": { + "commonjs": true, + "es6": true + }, + "ecmaFeatures": { + "forOf": false, + "jsx": true + }, + "plugins": [ + "react" + ], + "rules": { + "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/react-native/index.js b/react-native/index.js new file mode 100644 index 00000000..eade9a84 --- /dev/null +++ b/react-native/index.js @@ -0,0 +1,7 @@ +/* Copyright 2015 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +'use strict'; + +exports.ListView = require('./listview'); diff --git a/react-native/listview.js b/react-native/listview.js new file mode 100644 index 00000000..bf826c3a --- /dev/null +++ b/react-native/listview.js @@ -0,0 +1,190 @@ +/* Copyright 2015 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +'use strict'; + +const React = require('react-native'); + +function hashObjects(array) { + let hash = Object.create(null); + for (let i = 0, len = array.length; i < len; i++) { + hash[array[i]] = true; + } + return hash; +} + +class ListViewDataSource extends React.ListView.DataSource { + cloneWithRowsAndSections(inputData, sectionIds, rowIds) { + let data = {}; + + for (let sectionId in inputData) { + let items = inputData[sectionId]; + let copy; + + // Realm Results and List objects have a snapshot() method. + if (typeof items.snapshot == 'function') { + copy = items.snapshot(); + } else if (Array.isArray(items)) { + copy = items.slice(); + } else { + copy = Object.assign({}, items); + } + + data[sectionId] = copy; + } + + if (!sectionIds) { + sectionIds = Object.keys(data); + } + if (!rowIds) { + rowIds = sectionIds.map((sectionId) => { + let items = data[sectionId]; + if (typeof items.snapshot != 'function') { + return Object.keys(items); + } + + // Efficiently get the keys of the Realm collection, since they're never sparse. + let count = items.length; + let indexes = new Array(count); + for (let i = 0; i < count; i++) { + indexes[i] = i; + } + return indexes; + }); + } + + // Copy this object with the same parameters initially passed into the constructor. + let newSource = new this.constructor({ + getRowData: this._getRowData, + getSectionHeaderData: this._getSectionHeaderData, + rowHasChanged: this._rowHasChanged, + sectionHeaderHasChanged: this._sectionHeaderHasChanged, + }); + + newSource._cachedRowCount = rowIds.reduce((n, a) => n + a.length, 0); + newSource._dataBlob = data; + newSource.sectionIdentities = sectionIds; + newSource.rowIdentities = rowIds; + + let prevSectionIds = this.sectionIdentities; + let prevRowIds = this.rowIdentities; + let prevRowHash = {}; + for (let i = 0, len = prevRowIds.length; i < len; i++) { + prevRowHash[prevSectionIds[i]] = hashObjects(prevRowIds[i]); + } + + // These properties allow lazily calculating if rows and section headers should update. + newSource._prevDataBlob = this._dataBlob; + newSource._prevSectionHash = hashObjects(prevSectionIds); + newSource._prevRowHash = prevRowHash; + + return newSource; + } + + getRowData() { + // The React.ListView calls this for *every* item during each render, which is quite + // premature since this can be mildly expensive and memory inefficient since it keeps + // the result of this alive through a bound renderRow function. + return null; + } + + getRow(sectionId, rowId) { + // This new method is provided as a convenience for those wishing to be memory efficient. + return this._getRowData(this._dataBlob, sectionId, rowId); + } + + sectionHeaderShouldUpdate(sectionIndex) { + let dirtySections = this._dirtySections; + let dirty; + + if ((dirty = dirtySections[sectionIndex]) != null) { + // This was already calculated before. + return dirty; + } + + let sectionId = this.sectionIdentities[sectionIndex]; + let sectionHeaderHasChanged = this._sectionHeaderHasChanged; + if (this._prevSectionHash[sectionId] && sectionHeaderHasChanged) { + dirty = sectionHeaderHasChanged( + this._getSectionHeaderData(this._prevDataBlob, sectionId), + this._getSectionHeaderData(this._dataBlob, sectionId) + ); + } + + // Unless it's explicitly *not* dirty, then this section header should update. + return (dirtySections[sectionIndex] = dirty !== false); + } + + rowShouldUpdate(sectionIndex, rowIndex) { + let dirtyRows = this._dirtyRows[sectionIndex]; + let dirty; + + if (!dirtyRows) { + dirtyRows = this._dirtyRows[sectionIndex] = []; + } else if ((dirty = dirtyRows[rowIndex]) != null) { + // This was already calculated before. + return dirty; + } + + let sectionId = this.sectionIdentities[sectionIndex]; + if (this._prevSectionHash[sectionId]) { + let rowId = this.rowIdentities[sectionIndex][rowIndex]; + if (this._prevRowHash[sectionId][rowId]) { + let prevItem = this._getRowData(this._prevDataBlob, sectionId, rowId); + if (prevItem) { + let item = this._getRowData(this._dataBlob, sectionId, rowId); + if (item) { + dirty = this._rowHasChanged(prevItem, item); + } + } + } + } + + // Unless it's explicitly *not* dirty, then this row should update. + return (dirtyRows[rowIndex] = dirty !== false); + } +} + +class ListView extends React.Component { + constructor(props) { + super(props); + + this.renderRow = this.renderRow.bind(this); + } + + render() { + return ( + + ); + } + + renderRow(_, sectionId, rowId, ...args) { + let props = this.props; + let item = props.dataSource.getRow(sectionId, rowId); + + // The item could be null because our data is a snapshot and it was deleted. + return item ? props.renderRow(item, sectionId, rowId, ...args) : null; + } + + getInnerViewNode() { + return this.refs.listView.getInnerViewNode(); + } + + scrollTo(...args) { + this.refs.listView.scrollTo(...args); + } + + setNativeProps(props) { + this.refs.listView.setNativeProps(props); + } +} + +ListView.propTypes = { + dataSource: React.PropTypes.instanceOf(ListViewDataSource).isRequired, + renderRow: React.PropTypes.func.isRequired, +}; + +ListView.DataSource = ListViewDataSource; + +module.exports = ListView; diff --git a/scripts/test.sh b/scripts/test.sh index c6dc2355..8db02259 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -97,10 +97,11 @@ case "$TARGET" in fi npm update react-native + open_chrome start_packager pushd ios - xcodebuild -scheme ReactExample -configuration "$CONFIGURATION" -sdk iphonesimulator build $DESTINATION + xcodebuild -scheme ReactExample -configuration "$CONFIGURATION" -sdk iphonesimulator $DESTINATION build test ;; "react-tests-android") if [[ $CONFIGURATION == 'Debug' ]]; then diff --git a/src/js_list.cpp b/src/js_list.cpp index f998c51f..b99f4be0 100644 --- a/src/js_list.cpp +++ b/src/js_list.cpp @@ -15,13 +15,10 @@ using namespace realm; JSValueRef ListGetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* jsException) { try { - // index subscripting List *list = RJSGetInternal(object); - size_t size = list->size(); - std::string indexStr = RJSStringForJSString(propertyName); if (indexStr == "length") { - return JSValueMakeNumber(ctx, size); + return JSValueMakeNumber(ctx, list->size()); } return RJSObjectCreate(ctx, Object(list->realm(), list->get_object_schema(), list->get(RJSValidatedPositiveIndex(indexStr)))); diff --git a/src/js_results.cpp b/src/js_results.cpp index bd92fca0..fb242ce3 100644 --- a/src/js_results.cpp +++ b/src/js_results.cpp @@ -13,13 +13,10 @@ using namespace realm; JSValueRef ResultsGetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* jsException) { try { - // index subscripting Results *results = RJSGetInternal(object); - size_t size = results->size(); - std::string indexStr = RJSStringForJSString(propertyName); if (indexStr == "length") { - return JSValueMakeNumber(ctx, size); + return JSValueMakeNumber(ctx, results->size()); } auto row = results->get(RJSValidatedPositiveIndex(indexStr)); diff --git a/tests/ios/RJSModuleLoader.m b/tests/ios/RJSModuleLoader.m index a188ff5c..6c633edc 100644 --- a/tests/ios/RJSModuleLoader.m +++ b/tests/ios/RJSModuleLoader.m @@ -33,8 +33,7 @@ static NSString * const RJSModuleLoaderErrorDomain = @"RJSModuleLoaderErrorDomai self.globalModules[name] = [JSValue valueWithObject:object inContext:self.context]; } -- (JSValue *)loadModule:(NSString *)name relativeToURL:(NSURL *)baseURL error:(NSError **)error -{ +- (JSValue *)loadModule:(NSString *)name relativeToURL:(NSURL *)baseURL error:(NSError **)error { if (![name hasPrefix:@"./"] && ![name hasPrefix:@"../"]) { return [self loadGlobalModule:name relativeToURL:baseURL error:error]; } diff --git a/tests/ios/RealmJSCoreTests.m b/tests/ios/RealmJSCoreTests.m index ad97d689..6e511469 100644 --- a/tests/ios/RealmJSCoreTests.m +++ b/tests/ios/RealmJSCoreTests.m @@ -26,23 +26,15 @@ [moduleLoader addGlobalModuleObject:realmConstructor forName:@"realm"]; NSError *error; - JSValue *testObjects = [moduleLoader loadModuleFromURL:scriptURL error:&error]; + JSValue *testObject = [moduleLoader loadModuleFromURL:scriptURL error:&error]; + NSAssert(testObject, @"%@", error); - if (!testObjects) { - NSLog(@"%@", error); - exit(1); - } - - NSDictionary *testCaseNames = [[testObjects invokeMethod:@"getTestNames" withArguments:nil] toDictionary]; - - if (!testCaseNames.count) { - NSLog(@"No test case names from getTestNames() JS method!"); - exit(1); - } + NSDictionary *testCaseNames = [[testObject invokeMethod:@"getTestNames" withArguments:nil] toDictionary]; + NSAssert(testCaseNames.count, @"No test names were provided by the JS"); for (XCTestSuite *testSuite in [self testSuitesFromDictionary:testCaseNames]) { for (RealmJSCoreTests *test in testSuite.tests) { - test.testObject = testObjects[testSuite.name]; + test.testObject = testObject; } [suite addTest:testSuite]; @@ -57,15 +49,10 @@ - (void)invokeMethod:(NSString *)method { JSValue *testObject = self.testObject; - - if (![testObject hasProperty:method]) { - return; - } - JSContext *context = testObject.context; context.exception = nil; - [testObject invokeMethod:method withArguments:nil]; + [testObject invokeMethod:@"runTest" withArguments:@[NSStringFromClass(self.class), method]]; JSValue *exception = context.exception; if (exception) { diff --git a/tests/ios/RealmJSTests.mm b/tests/ios/RealmJSTests.mm index 7fea54d3..7bf3acf3 100644 --- a/tests/ios/RealmJSTests.mm +++ b/tests/ios/RealmJSTests.mm @@ -10,12 +10,17 @@ + (NSArray *)testSuitesFromDictionary:(NSDictionary *)testCaseNames { NSMutableArray *testSuites = [[NSMutableArray alloc] init]; + NSSet *specialNames = [NSSet setWithObjects:@"beforeEach", @"afterEach", nil]; for (NSString *suiteName in testCaseNames) { XCTestSuite *testSuite = [[XCTestSuite alloc] initWithName:suiteName]; Class testClass = objc_allocateClassPair(self, suiteName.UTF8String, 0); for (NSString *testName in testCaseNames[suiteName]) { + if ([specialNames containsObject:testName]) { + continue; + } + XCTestCase *testCase = [[testClass alloc] initWithTestName:testName]; [testSuite addTest:testCase]; } diff --git a/tests/lib/base-test.js b/tests/lib/base-test.js index ffe08365..f9fca2ae 100644 --- a/tests/lib/base-test.js +++ b/tests/lib/base-test.js @@ -14,11 +14,6 @@ exports.extend = function(object) { }; Object.defineProperties(prototype, { - // TODO: Remove once missing undefined check is fixed inside RCTContextExecutor. - beforeEach: { - value: function() {} - }, - afterEach: { value: function() { Realm.clearTestState(); diff --git a/tests/lib/index.js b/tests/lib/index.js index 3ef1db86..4446b82f 100644 --- a/tests/lib/index.js +++ b/tests/lib/index.js @@ -4,37 +4,46 @@ 'use strict'; -exports.ListTests = require('./list-tests'); -exports.ObjectTests = require('./object-tests'); -exports.RealmTests = require('./realm-tests'); -exports.ResultsTests = require('./results-tests'); -exports.QueryTests = require('./query-tests'); +var TESTS = { + ListTests: require('./list-tests'), + ObjectTests: require('./object-tests'), + RealmTests: require('./realm-tests'), + ResultsTests: require('./results-tests'), + QueryTests: require('./query-tests'), +}; var SPECIAL_METHODS = { beforeEach: true, afterEach: true, }; -// Only the test suites should be iterable members of exports. -Object.defineProperties(exports, { - getTestNames: { - value: function() { - var testNames = {}; +exports.getTestNames = function() { + var testNames = {}; - for (var suiteName in exports) { - var testSuite = exports[suiteName]; + for (var suiteName in TESTS) { + var testSuite = TESTS[suiteName]; - testNames[suiteName] = Object.keys(testSuite).filter(function(testName) { - return !(testName in SPECIAL_METHODS) && typeof testSuite[testName] == 'function'; - }); - } + testNames[suiteName] = Object.keys(testSuite).filter(function(testName) { + return !(testName in SPECIAL_METHODS) && typeof testSuite[testName] == 'function'; + }); + } - return testNames; - } - }, - runTest: { - value: function(suiteName, testName) { - exports[suiteName][testName](); - } - }, -}); + return testNames; +}; + +exports.registerTests = function(tests) { + for (var suiteName in tests) { + TESTS[suiteName] = tests[suiteName]; + } +}; + +exports.runTest = function(suiteName, testName) { + var testSuite = TESTS[suiteName]; + var testMethod = testSuite && testSuite[testName]; + + if (testMethod) { + testMethod.call(testSuite); + } else if (!testSuite || !(testName in SPECIAL_METHODS)) { + throw new Error('Missing test: ' + suiteName + '.' + testName); + } +}; diff --git a/tests/react-test-app/index.android.js b/tests/react-test-app/index.android.js index a7e2342e..f95f31f8 100644 --- a/tests/react-test-app/index.android.js +++ b/tests/react-test-app/index.android.js @@ -1,52 +1,49 @@ -/** - * Sample React Native App - * https://github.com/facebook/react-native +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential */ 'use strict'; -var React = require('react-native'); -var { - AppRegistry, - StyleSheet, - Image, - Text, - View, - TouchableNativeFeedback, + +const React = require('react-native'); +const Realm = require('realm'); +const RealmTests = require('realm-tests'); +const builder = require('xmlbuilder'); +const RNFS = require('react-native-fs'); + +const { + AppRegistry, + StyleSheet, + Image, + Text, + View, + TouchableNativeFeedback, } = React; -var Realm = require('realm'); -var RealmTests = require('realm-tests'); -var builder = require('xmlbuilder'); -var RNFS = require('react-native-fs'); +RealmTests.registerTests({ + ListViewTest: require('./tests/listview-test'), +}); function runTests() { - var rootXml = builder.create('testsuites'); + let rootXml = builder.create('testsuites'); let testNames = RealmTests.getTestNames(); for (let suiteName in testNames) { - var itemTestsuite = rootXml.ele('testsuite'); + let itemTestsuite = rootXml.ele('testsuite'); let nbrTests = 0; let nbrFailures = 0; - let testSuite = RealmTests[suiteName]; - console.log('Starting suite ' + suiteName); - var suiteTestNames = testNames[suiteName]; - for (var index in suiteTestNames) { + testNames[suiteName].forEach((testName) => { nbrTests++; - var testName = suiteTestNames[index]; - var itemTest = itemTestsuite.ele('testcase'); + let itemTest = itemTestsuite.ele('testcase'); itemTest.att('name', testName); console.log('Starting ' + testName); - - if (testSuite.beforeEach) { - testSuite.beforeEach(); - } + RealmTests.runTest(suiteName, 'beforeEach'); try { - testSuite[testName](); + RealmTests.runTest(suiteName, testName); console.log('+ ' + testName); } catch (e) { @@ -57,11 +54,9 @@ function runTests() { nbrFailures++; } finally { - if (testSuite.afterEach) { - testSuite.afterEach(); - } + RealmTests.runTest(suiteName, 'afterEach'); } - } + }); // update Junit XML report itemTestsuite.att('name', suiteName); diff --git a/tests/react-test-app/index.ios.js b/tests/react-test-app/index.ios.js index de6bb819..eb549b28 100644 --- a/tests/react-test-app/index.ios.js +++ b/tests/react-test-app/index.ios.js @@ -1,4 +1,4 @@ -/* Copyright 2015 Realm Inc - All Rights Reserved +/* Copyright 2016 Realm Inc - All Rights Reserved * Proprietary and Confidential */ @@ -6,70 +6,21 @@ const React = require('react-native'); const Realm = require('realm'); -const RealmTests = require('realm-tests'); +const tests = require('./tests'); 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(); - - for (let suiteName in testNames) { - let testSuite = RealmTests[suiteName]; - - console.log('Starting ' + suiteName); - - for (let testName of testNames[suiteName]) { - if (testSuite.beforeEach) { - testSuite.beforeEach(); - } - - try { - testSuite[testName](); - console.log('+ ' + testName); - } - catch (e) { - console.log('- ' + testName); - console.warn(e.message); - } - finally { - if (testSuite.afterEach) { - testSuite.afterEach(); - } - } - } - } -} - class ReactTests extends React.Component { render() { return ( - + Tap to Run Tests diff --git a/tests/react-test-app/ios/ReactTests.xcodeproj/project.pbxproj b/tests/react-test-app/ios/ReactTests.xcodeproj/project.pbxproj index 7ba00bcf..bec981ec 100644 --- a/tests/react-test-app/ios/ReactTests.xcodeproj/project.pbxproj +++ b/tests/react-test-app/ios/ReactTests.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 008F07F31AC5B25A0029DE68 /* main.jsbundle in Resources */ = {isa = PBXBuildFile; fileRef = 008F07F21AC5B25A0029DE68 /* main.jsbundle */; }; 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; @@ -158,7 +157,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; }; 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = "../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj"; sourceTree = ""; }; 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = ""; }; @@ -172,12 +170,12 @@ 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = ""; }; 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* ReactTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = ReactTests/AppDelegate.h; sourceTree = ""; }; - 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = ReactTests/AppDelegate.m; sourceTree = ""; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; - 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactTests/Images.xcassets; sourceTree = ""; }; - 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactTests/Info.plist; sourceTree = ""; }; - 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ReactTests/main.m; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; @@ -283,7 +281,6 @@ 13B07FAE1A68108700A75B9A /* ReactTests */ = { isa = PBXGroup; children = ( - 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, @@ -291,7 +288,7 @@ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 13B07FB71A68108700A75B9A /* main.m */, ); - name = ReactTests; + path = ReactTests; sourceTree = ""; }; 146834001AC3E56700842450 /* Products */ = { @@ -382,10 +379,11 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactTestApp" */; buildPhases = ( - F65C4BCA1BC72C5A000A1793 /* ShellScript */, + F65C4BCA1BC72C5A000A1793 /* Install Realm Node Modules */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, + F6EDE5BF1C49007200B1085F /* Bundle React Native code and images */, F6F405C61BCE565100A1E24F /* Embed Frameworks */, ); buildRules = ( @@ -404,7 +402,7 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 0710; - ORGANIZATIONNAME = Facebook; + ORGANIZATIONNAME = Realm; TargetAttributes = { 00E356ED1AD99517003FC87E = { CreatedOnToolsVersion = 6.2; @@ -597,7 +595,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 008F07F31AC5B25A0029DE68 /* main.jsbundle in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, ); @@ -606,19 +603,34 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - F65C4BCA1BC72C5A000A1793 /* ShellScript */ = { + F65C4BCA1BC72C5A000A1793 /* Install Realm Node Modules */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); + name = "Install Realm Node Modules"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "[ -s \"${HOME}/.nvm/nvm.sh\" ] && . \"${HOME}/.nvm/nvm.sh\" \nrm -rf ../node_modules/realm ../node_modules/realm-tests\nnpm install realm realm-tests\ncp ../../../src/object-store/parser/queryTests.json ../node_modules/realm-tests/query-tests.json\n"; }; + F6EDE5BF1C49007200B1085F /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "[ \"$PLATFORM_NAME\" = \"iphonesimulator\" ] && exit\n../node_modules/react-native/packager/react-native-xcode.sh"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -657,7 +669,6 @@ 13B07FB21A68108700A75B9A /* Base */, ); name = LaunchScreen.xib; - path = ReactTests; sourceTree = ""; }; /* End PBXVariantGroup section */ @@ -679,9 +690,7 @@ "$(SRCROOT)/../node_modules/react-native/React/Modules", ); INFOPLIST_FILE = ../../ios/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ReactTests.app/ReactTests"; }; @@ -700,9 +709,7 @@ "$(SRCROOT)/../node_modules/react-native/React/Modules", ); INFOPLIST_FILE = ../../ios/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ReactTests.app/ReactTests"; }; @@ -715,7 +722,6 @@ INFOPLIST_FILE = ReactTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = ReactTests; }; name = Debug; @@ -727,7 +733,6 @@ INFOPLIST_FILE = ReactTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = ReactTests; }; name = Release; @@ -776,6 +781,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "io.realm.$(PRODUCT_NAME:rfc1034identifier)"; SDKROOT = iphoneos; }; name = Debug; @@ -816,6 +822,7 @@ ); IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "io.realm.$(PRODUCT_NAME:rfc1034identifier)"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; diff --git a/tests/react-test-app/ios/ReactTests/AppDelegate.m b/tests/react-test-app/ios/ReactTests/AppDelegate.m index 817550db..f41768d0 100644 --- a/tests/react-test-app/ios/ReactTests/AppDelegate.m +++ b/tests/react-test-app/ios/ReactTests/AppDelegate.m @@ -32,9 +32,8 @@ static NSString * const RCTDevMenuKey = @"RCTDevMenu"; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *jsCodeLocation; +#if TARGET_OS_SIMULATOR /** - * Loading JavaScript code - uncomment the one you want. - * * OPTION 1 * Load from development server. Start the server from the repository root: * @@ -46,19 +45,21 @@ static NSString * const RCTDevMenuKey = @"RCTDevMenu"; * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"]; +#if DEBUG + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"]; +#else + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=false"]; +#endif +#else /** * OPTION 2 - * Load from pre-bundled file on disk. To re-generate the static bundle - * from the root of your project directory, run - * - * $ react-native bundle --minify - * - * see http://facebook.github.io/react-native/docs/runningondevice.html + * Load from pre-bundled file on disk. The static bundle is automatically + * generated by "Bundle React Native code and images" build step. */ - // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"ReactTests" diff --git a/tests/react-test-app/ios/ReactTests/RealmReactTests.m b/tests/react-test-app/ios/ReactTests/RealmReactTests.m index 50a3c9c4..f9f39106 100644 --- a/tests/react-test-app/ios/ReactTests/RealmReactTests.m +++ b/tests/react-test-app/ios/ReactTests/RealmReactTests.m @@ -7,6 +7,8 @@ #import "RCTBridge.h" #import "RCTDevMenu.h" #import "RCTEventDispatcher.h" +#import "RCTJavaScriptLoader.h" +#import "RCTLog.h" @import RealmReact; @@ -29,6 +31,10 @@ extern NSMutableArray *RCTGetModuleClasses(void); + (void)load { NSMutableArray *moduleClasses = RCTGetModuleClasses(); [moduleClasses removeObject:[RCTDevMenu class]]; + + RCTAddLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { + NSAssert(level < RCTLogLevelError, RCTFormatLog(nil, level, fileName, lineNumber, message)); + }); } + (Class)executorClass { @@ -84,10 +90,7 @@ extern NSMutableArray *RCTGetModuleClasses(void); } NSDictionary *testCaseNames = [self waitForEvent:@"realm-test-names"]; - if (!testCaseNames.count) { - NSLog(@"ERROR: No test names were provided by the JS"); - exit(1); - } + NSAssert(testCaseNames.count, @"No test names were provided by the JS"); NSString *nameSuffix = [self classNameSuffix]; if (nameSuffix.length) { @@ -115,25 +118,37 @@ extern NSMutableArray *RCTGetModuleClasses(void); notification = note; }]; - [self waitForCondition:&condition]; - [nc removeObserver:token]; + @try { + [self waitForCondition:&condition description:notificationName]; + } @finally { + [nc removeObserver:token]; + } return notification; } -+ (void)waitForCondition:(BOOL *)condition { ++ (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 distantFuture]]; + [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]]; - assert(realmModule); + NSAssert(realmModule, @"RealmReact module not found"); __block BOOL condition = NO; __block id result; @@ -148,10 +163,28 @@ extern NSMutableArray *RCTGetModuleClasses(void); [realmModule addListenerForEvent:eventName handler:handler]; - [self waitForCondition:&condition]; + [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]; @@ -163,7 +196,13 @@ extern NSMutableArray *RCTGetModuleClasses(void); RCTBridge *bridge = [self.class currentBridge]; [bridge.eventDispatcher sendAppEventWithName:@"realm-run-test" body:@{@"suite": module, @"name": method}]; - id error = [self.class waitForEvent:@"realm-test-finished"]; + 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]; } diff --git a/tests/react-test-app/ios/main.jsbundle b/tests/react-test-app/ios/main.jsbundle deleted file mode 100644 index b702b30c..00000000 --- a/tests/react-test-app/ios/main.jsbundle +++ /dev/null @@ -1,8 +0,0 @@ -// Offline JS -// To re-generate the offline bundle, run this from the root of your project: -// -// $ react-native bundle --minify -// -// See http://facebook.github.io/react-native/docs/runningondevice.html for more details. - -throw new Error('Offline JS file is empty. See iOS/main.jsbundle for instructions'); diff --git a/tests/react-test-app/tests/index.js b/tests/react-test-app/tests/index.js new file mode 100644 index 00000000..5fa3966a --- /dev/null +++ b/tests/react-test-app/tests/index.js @@ -0,0 +1,65 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +'use strict'; + +const React = require('react-native'); +const Realm = require('realm'); +const RealmTests = require('realm-tests'); + +RealmTests.registerTests({ + ListViewTest: require('./listview-test'), +}); + +const { + NativeAppEventEmitter, + NativeModules, +} = React; + +module.exports = { + runTests, +}; + +// 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(); + + for (let suiteName in testNames) { + let testSuite = RealmTests[suiteName]; + + console.log('Starting ' + suiteName); + + for (let testName of testNames[suiteName]) { + RealmTests.runTest(suiteName, 'beforeEach'); + + try { + RealmTests.runTest(suiteName, testName); + console.log('+ ' + testName); + } + catch (e) { + console.warn('- ' + testName); + console.warn(e.message); + } + finally { + RealmTests.runTest(suiteName, 'afterEach'); + } + } + } +} diff --git a/tests/react-test-app/tests/listview-test.js b/tests/react-test-app/tests/listview-test.js new file mode 100644 index 00000000..c7cf8852 --- /dev/null +++ b/tests/react-test-app/tests/listview-test.js @@ -0,0 +1,88 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +'use strict'; + +const Realm = require('realm'); +const { ListView } = require('realm/react-native'); +const { assertEqual, assertTrue } = require('realm-tests/asserts'); + +const OBJECT_SCHEMA = { + name: 'UniqueObject', + primaryKey: 'id', + properties: { + id: 'int', + } +}; + +function createRealm() { + let realm = new Realm({schema: [OBJECT_SCHEMA]}); + + realm.write(() => { + for (let i = 0; i < 10; i++) { + realm.create('UniqueObject', {id: i}); + } + }); + + return realm; +} + +function createDataSource() { + return new ListView.DataSource({ + rowHasChanged: (a, b) => a.id !== b.id, + }); +} + +module.exports = { + afterEach() { + Realm.clearTestState(); + }, + + testDataSource() { + let realm = createRealm(); + let objects = realm.objects('UniqueObject'); + objects.sortByProperty('id'); + + let dataSource = createDataSource().cloneWithRows(objects); + let count = objects.length; + + // Make sure the section header should update. + assertTrue(dataSource.sectionHeaderShouldUpdate(0)); + + // All rows should need to update. + for (let i = 0; i < count; i++) { + assertTrue(dataSource.rowShouldUpdate(0, i)); + } + + // Clone data source with no changes and make sure no rows need to update. + dataSource = dataSource.cloneWithRows(objects); + for (let i = 0; i < count; i++) { + assertTrue(!dataSource.rowShouldUpdate(0, i)); + } + + // Delete the second object and make sure current data source is unchanged. + realm.write(() => realm.delete(objects[1])); + for (let i = 0; i < count; i++) { + assertTrue(!dataSource.rowShouldUpdate(0, i)); + } + + // Getting the row data for the second row should return null. + assertEqual(dataSource.getRow('s1', 1), null); + + // Clone data source and make sure all rows after the first one need to update. + dataSource = dataSource.cloneWithRows(objects); + for (let i = 0; i < count - 1; i++) { + let changed = dataSource.rowShouldUpdate(0, i); + assertTrue(i == 0 ? !changed : changed); + } + + // Create an object at the ened and make sure only the last row needs to update. + realm.write(() => realm.create('UniqueObject', {id: count})); + dataSource = dataSource.cloneWithRows(objects); + for (let i = 0; i < count; i++) { + let changed = dataSource.rowShouldUpdate(0, i); + assertTrue(i < count - 1 ? !changed : changed); + } + }, +};