diff --git a/Jenkinsfile b/Jenkinsfile index 1bb6472f..5206c78b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,8 +68,8 @@ stage('build') { macos_node_release: doMacBuild('node Release'), macos_realmjs_debug: doMacBuild('realmjs Debug'), macos_realmjs_release: doMacBuild('realmjs Release'), - macos_react_tests_debug: doMacBuild('react-tests Debug'), - macos_react_tests_release: doMacBuild('react-tests Release'), + macos_react_tests_debug: doReactBuild('react-tests Debug'), + macos_react_tests_release: doReactBuild('react-tests Release'), macos_react_example_debug: doMacBuild('react-example Debug'), macos_react_example_release: doMacBuild('react-example Release'), android_react_tests: doAndroidBuild('react-tests-android', { @@ -184,3 +184,18 @@ def doMacBuild(target, postStep = null) { } } } + +def doReactBuild(target, postStep = null) { + return { + node('xamarin-mac') { + try { + lock("${env.NODE_NAME} iOS Simulator") { + doInside("./scripts/test.sh", target, postStep) + } + } finally { + deleteDir() + } + } + } +} + diff --git a/lib/browser/collections.js b/lib/browser/collections.js index fcc9b8f5..c3d89c0b 100644 --- a/lib/browser/collections.js +++ b/lib/browser/collections.js @@ -19,7 +19,8 @@ 'use strict'; import { keys } from './constants'; -import { getterForProperty, setterForProperty } from './util'; +import { getterForProperty } from './util'; +import { getProperty, setProperty } from './rpc'; let mutationListeners = {}; @@ -52,9 +53,65 @@ export function fireMutationListeners(realmId) { } } +function isIndex(propertyName) { + return typeof propertyName === 'number' || (typeof propertyName === 'string' && /^\d+$/.test(propertyName)); +} + +const mutable = Symbol('mutable'); + +const traps = { + get(collection, property, receiver) { + if (isIndex(property)) { + return getProperty(collection[keys.realm], collection[keys.id], property); + } + + return Reflect.get(collection, property, collection); + }, + set(collection, property, value, receiver) { + if (isIndex(property)) { + if (!collection[mutable]) { + return false; + } + + setProperty(collection[keys.realm], collection[keys.id], property, value); + + // If this isn't a primitive value, then it might create a new object in the Realm. + if (value && typeof value == 'object') { + fireMutationListeners(collection[keys.realm]); + } + + return true; + } + + return Reflect.set(collection, property, value, collection); + }, + ownKeys(collection) { + return Reflect.ownKeys(collection).concat(Array.from({ length: collection.length }, (value, key) => String(key))); + }, + getOwnPropertyDescriptor(collection, property) { + if (isIndex(property)) { + let descriptor = { + enumerable: true, + configurable: true, + writable: collection[mutable] + }; + Reflect.defineProperty(descriptor, "value", { get: () => this.get(collection, property) }); + return descriptor; + } + + return Reflect.getOwnPropertyDescriptor(collection, property); + }, + has(collection, property) { + if (isIndex(property)) { + return true; + } + + return Reflect.has(collection, property); + } +}; + export function createCollection(prototype, realmId, info, _mutable) { let collection = Object.create(prototype); - let size; Object.defineProperties(collection, { 'length': { @@ -65,65 +122,10 @@ export function createCollection(prototype, realmId, info, _mutable) { }, }); - let resize = function(length) { - if (length == null) { - length = collection.length; - } - if (length == size) { - return; // Nothing has changed. - } - if (size == null) { - size = 0; // This is first pass. - } - - let props = {}; - - if (length > size) { - for (let i = size; i < length; i++) { - props[i] = { - get: getterForProperty(i), - set: setterForProperty(i), - enumerable: true, - configurable: true, - }; - } - } - else if (length < size) { - for (let i = size; i >= length; i--) { - delete collection[i]; - } - } - - // Helpfully throw an exception on attempts to set to one past the last index. - props[length] = { - get: getterForProperty(length), - set: setterForProperty(length), - configurable: true, - }; - - Object.defineProperties(collection, props); - - size = length; - }; - collection[keys.realm] = realmId; collection[keys.id] = info.id; collection[keys.type] = info.type; + collection[mutable] = _mutable; - resize(info.size); - - addMutationListener(realmId, function listener() { - try { - resize(); - } catch (e) { - // If the error indicates the collection was deleted, then remove this listener. - if (e.message.indexOf('Access to invalidated') == 0) { - removeMutationListener(realmId, listener); - } else { - throw e; - } - } - }); - - return collection; + return new Proxy(collection, traps); } diff --git a/lib/browser/constants.js b/lib/browser/constants.js index 367ee967..6cfc5485 100644 --- a/lib/browser/constants.js +++ b/lib/browser/constants.js @@ -27,7 +27,7 @@ export const propTypes = {}; 'realm', 'type', ].forEach(function(name) { - keys[name] = Symbol(); + keys[name] = Symbol(name); }); [ diff --git a/scripts/test.sh b/scripts/test.sh index 19d71074..e629f8d3 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -224,11 +224,14 @@ cleanup trap cleanup EXIT # Use a consistent version of Node if possible. -if [ -s "${HOME}/.nvm/nvm.sh" ]; then - # shellcheck disable=SC1090 - . "${HOME}/.nvm/nvm.sh" - nvm use 5.12 || true +if [ -f "$NVM_DIR/nvm.sh" ]; then + . "$NVM_DIR/nvm.sh" +elif [ -x "$(command -v brew)" ] && [ -f "$(brew --prefix nvm)/nvm.sh" ]; then + # we must be on mac and nvm was installed with brew + # TODO: change the mac slaves to use manual nvm installation + . "$(brew --prefix nvm)/nvm.sh" fi +[[ "$(command -v nvm)" ]] && nvm use 6.5.0 || true # Remove cached packages rm -rf ~/.yarn-cache/npm-realm-* diff --git a/tests/react-test-app/ios/ReactTests/RealmReactTests.m b/tests/react-test-app/ios/ReactTests/RealmReactTests.m index 0aaf5977..746e31a6 100644 --- a/tests/react-test-app/ios/ReactTests/RealmReactTests.m +++ b/tests/react-test-app/ios/ReactTests/RealmReactTests.m @@ -125,6 +125,7 @@ extern NSMutableArray *RCTGetModuleClasses(void); JSGlobalContextSetIncludesNativeCallStackWhenReportingExceptions(ctx, false); } + [self.currentBridge.eventDispatcher sendAppEventWithName:@"realm-test-names" body:nil]; NSDictionary *testCaseNames = [self waitForEvent:@"realm-test-names"]; NSAssert(testCaseNames.count, @"No test names were provided by the JS"); diff --git a/tests/react-test-app/tests/index.js b/tests/react-test-app/tests/index.js index 64392474..ebaabcba 100644 --- a/tests/react-test-app/tests/index.js +++ b/tests/react-test-app/tests/index.js @@ -26,6 +26,11 @@ RealmTests.registerTests({ ListViewTest, }); +// Listen for event signalling native is ready to receive test names +NativeAppEventEmitter.addListener('realm-test-names', () => { + NativeModules.Realm.emit('realm-test-names', getTestNames()); +}); + // Listen for event to run a particular test. NativeAppEventEmitter.addListener('realm-run-test', async ({suite, name}) => { let error; @@ -38,14 +43,6 @@ NativeAppEventEmitter.addListener('realm-run-test', async ({suite, name}) => { NativeModules.Realm.emit('realm-test-finished', error); }); -// Inform the native test harness about the test suite once it's ready. -setTimeout(() => { - // The emit() method only exists on iOS, for now. - if (NativeModules.Realm.emit) { - NativeModules.Realm.emit('realm-test-names', getTestNames()); - } -}, 500); - export function getTestNames() { return RealmTests.getTestNames(); }