From cbe5d27c2a22bebf611ee4223fe9a5ceef945ac8 Mon Sep 17 00:00:00 2001 From: Salakar Date: Sun, 25 Mar 2018 02:23:46 +0100 Subject: [PATCH] [tests] new test infra - start of bridge cleanup --- tests-new/app.js | 19 +++--- tests-new/bridge/env/node/console.js | 21 +++++++ tests-new/bridge/env/node/context.js | 74 +++++++++++++++++++++++ tests-new/bridge/env/node/index.js | 90 +++++++++++++++------------- tests-new/bridge/env/node/ready.js | 17 ++++++ tests-new/bridge/env/node/ws.js | 9 ++- tests-new/bridge/env/rn.js | 66 ++++++++++++-------- tests-new/e2e/bridge.spec.js | 48 +++++++++++++-- tests-new/e2e/init.js | 4 ++ 9 files changed, 264 insertions(+), 84 deletions(-) create mode 100644 tests-new/bridge/env/node/console.js create mode 100644 tests-new/bridge/env/node/context.js create mode 100644 tests-new/bridge/env/node/ready.js diff --git a/tests-new/app.js b/tests-new/app.js index ad5cccb4..51438121 100755 --- a/tests-new/app.js +++ b/tests-new/app.js @@ -4,32 +4,33 @@ * @flow */ -require('sinon'); -require('should-sinon'); -require('should'); - // must import before all else -import Bridge from './bridge/env/rn'; import React, { Component } from 'react'; import { AppRegistry, Text, View } from 'react-native'; +import bridge from './bridge/env/rn'; import firebase from './firebase'; +require('sinon'); +require('should-sinon'); +require('should'); + class Root extends Component { constructor(props) { super(props); this.state = { - message: 'React Native Firebase Test App', + message: '', }; - Bridge.provideRoot(this); - Bridge.provideModule(firebase); + + bridge.setBridgeProperty('root', this); + bridge.setBridgeProperty('module', firebase); } render() { return ( - {this.state.message} + {this.state.message} ); } diff --git a/tests-new/bridge/env/node/console.js b/tests-new/bridge/env/node/console.js new file mode 100644 index 00000000..105df4fb --- /dev/null +++ b/tests-new/bridge/env/node/console.js @@ -0,0 +1,21 @@ +module.exports = function consoleContext() { + return { + ...console, + /** + * Override console log so we can ignore certain logs like the application being started + * + * @param args + */ + log(...args) { + if ( + args[0] && + typeof args[0] === 'string' && + args[0].startsWith('Running application "') + ) { + return; + } + + console.log(...args); + }, + }; +}; diff --git a/tests-new/bridge/env/node/context.js b/tests-new/bridge/env/node/context.js new file mode 100644 index 00000000..683e4cf6 --- /dev/null +++ b/tests-new/bridge/env/node/context.js @@ -0,0 +1,74 @@ +/* eslint-disable guard-for-in,no-restricted-syntax */ +global.bridge.context = null; + +const consoleContext = require('./console'); +const { createContext } = require('vm'); + +let customBridgeProps = []; + +module.exports = { + /** + * Cleanup existing context - just some quick iterations over common fb/rn/bridge locations + * garbage collection will do the rest. This is probably not needed... + */ + async cleanup() { + if (global.bridge.context) { + if (global.bridge.beforeContextReset) { + await global.bridge.beforeContextReset(); + } + try { + for (const name in global.bridge.context.__fbBatchedBridge) { + global.bridge.context.__fbBatchedBridge[name] = undefined; + delete global.bridge.context.__fbBatchedBridge[name]; + } + + for (const name in global.bridge.context.__fbGenNativeModule) { + global.bridge.context.__fbGenNativeModule[name] = undefined; + delete global.bridge.context.__fbGenNativeModule[name]; + } + + for (const name in global.bridge.context.__fbBatchedBridgeConfig) { + global.bridge.context.__fbBatchedBridgeConfig[name] = undefined; + delete global.bridge.context.__fbBatchedBridgeConfig[name]; + } + + for (const name in global.bridge.context) { + global.bridge.context[name] = undefined; + delete global.bridge.context[name]; + } + } catch (e) { + // do nothing; + } + + global.bridge.context = undefined; + + // clear custom props and reset props track array + for (let i = 0; i < customBridgeProps.length; i++) { + global.bridge[customBridgeProps[i]] = undefined; + delete global.bridge[customBridgeProps[i]]; + } + + customBridgeProps = []; + } + }, + + /** + * Create a new context for a RN app to attach to, we addtionaly provide __bridgeNode for + * the counterpart RN bridge code to attach to and communicate back + */ + create() { + global.bridge.context = createContext({ + console: consoleContext(), + __bridgeNode: { + _ready() { + setTimeout(() => process.emit('bridge-attached'), 5); + }, + + setBridgeProperty(key, value) { + customBridgeProps.push(key); + global.bridge[key] = value; + }, + }, + }); + }, +}; diff --git a/tests-new/bridge/env/node/index.js b/tests-new/bridge/env/node/index.js index 53e1cddf..35f3e969 100644 --- a/tests-new/bridge/env/node/index.js +++ b/tests-new/bridge/env/node/index.js @@ -1,55 +1,59 @@ +/* eslint-disable no-param-reassign */ global.bridge = {}; const detox = require('detox'); - -require('./vm'); const ws = require('./ws'); +const ready = require('./ready'); -const detoxOriginalInit = detox.init.bind(detox); -const detoxOriginalCleanup = detox.cleanup.bind(detox); +/* --------------------- + * DEVICE OVERRIDES + * --------------------- */ -let bridgeReady = false; -process.on('rn-ready', () => { - bridgeReady = true; +let device; +Object.defineProperty(global, 'device', { + get() { + return device; + }, + set(originalDevice) { + // device.reloadReactNative({ ... }) + // todo detoxOriginalReloadReactNative currently broken it seems + // const detoxOriginalReloadReactNative = originalDevice.reloadReactNative.bind(originalDevice); + originalDevice.reloadReactNative = async () => { + ready.reset(); + global.bridge.reload(); + return ready.wait(); + }; + + // device.launchApp({ ... }) + const detoxOriginalLaunchApp = originalDevice.launchApp.bind( + originalDevice + ); + originalDevice.launchApp = async (...args) => { + ready.reset(); + await detoxOriginalLaunchApp(...args); + return ready.wait(); + }; + + device = originalDevice; + return originalDevice; + }, }); -function onceBridgeReady() { - if (bridgeReady) return Promise.resolve(); - return new Promise(resolve => { - process.once('rn-ready', resolve); - }); -} - -function shimDevice() { - // reloadReactNative - // todo detoxOriginalReloadReactNative currently broken - // const detoxOriginalReloadReactNative = device.reloadReactNative.bind(device); - device.reloadReactNative = async () => { - bridgeReady = false; - global.bridge.reload(); - return onceBridgeReady(); - }; - - // launchApp - const detoxOriginalLaunchApp = device.launchApp.bind(device); - device.launchApp = async (...args) => { - bridgeReady = false; - await detoxOriginalLaunchApp(...args); - return onceBridgeReady(); - }; - - // todo other device reloading related methods -} +/* ------------------- + * DETOX OVERRIDES + * ------------------- */ +// detox.init() +const detoxOriginalInit = detox.init.bind(detox); detox.init = async (...args) => { - bridgeReady = false; - return detoxOriginalInit(...args).then(() => { - shimDevice(); - return onceBridgeReady(); - }); + ready.reset(); + await detoxOriginalInit(...args); + return ready.wait(); }; -detox.cleanup = async (...args) => - detoxOriginalCleanup(...args).then(() => { - ws.close(); - }); +// detox.cleanup() +const detoxOriginalCleanup = detox.cleanup.bind(detox); +detox.cleanup = async (...args) => { + ws.close(); + await detoxOriginalCleanup(...args); +}; diff --git a/tests-new/bridge/env/node/ready.js b/tests-new/bridge/env/node/ready.js new file mode 100644 index 00000000..97426352 --- /dev/null +++ b/tests-new/bridge/env/node/ready.js @@ -0,0 +1,17 @@ +let ready = false; + +process.on('bridge-attached', () => { + ready = true; +}); + +module.exports = { + wait() { + if (ready) return Promise.resolve(); + return new Promise(resolve => { + process.once('bridge-attached', resolve); + }); + }, + reset() { + ready = false; + }, +}; diff --git a/tests-new/bridge/env/node/ws.js b/tests-new/bridge/env/node/ws.js index 99e26292..5c7829c0 100644 --- a/tests-new/bridge/env/node/ws.js +++ b/tests-new/bridge/env/node/ws.js @@ -1,10 +1,15 @@ +const vm = require('./vm'); const WebSocket = require('ws'); const ws = new WebSocket( 'ws://localhost:8081/debugger-proxy?role=debugger&name=Chrome' ); -ws.onmessage = message => process.emit('ws-message', JSON.parse(message.data)); -ws.onclose = event => (!event.wasClean ? console.log('WS close', event) : ''); +vm.reply = obj => ws.send(JSON.stringify(obj)); + +ws.onmessage = message => vm.message(JSON.parse(message.data)); + +ws.onclose = event => + !event.wasClean ? console.error('Bridge WS Error', event.message) : ''; module.exports = ws; diff --git a/tests-new/bridge/env/rn.js b/tests-new/bridge/env/rn.js index def9c6b2..4bd7d9ce 100644 --- a/tests-new/bridge/env/rn.js +++ b/tests-new/bridge/env/rn.js @@ -1,43 +1,59 @@ -import reactNative, { Platform, NativeModules } from 'react-native'; +import ReactNative from 'react-native'; import RNRestart from 'react-native-restart'; // Import package from node modules +const { Platform, NativeModules } = ReactNative; + const bridgeNode = global.__bridgeNode; +const INTERNAL_KEYS = ['context', 'rn', 'reload']; // https://github.com/facebook/react-native/blob/master/React/Modules/RCTDevSettings.mm if (Platform.OS === 'ios' && !bridgeNode) { NativeModules.RCTDevSettings.setIsDebuggingRemotely(true); +} else { + if (Platform.OS === 'android' && !bridgeNode) { + // TODO warn to add: + // getReactNativeHost().getReactInstanceManager().getDevSupportManager().getDevSettings().setRemoteJSDebugEnabled(true); + // to MainApplication onCreate + } + + if (bridgeNode) { + if (Platform.OS === 'ios') { + bridgeNode.setBridgeProperty( + 'reload', + NativeModules.RCTDevSettings.reload + ); + } else { + bridgeNode.setBridgeProperty('reload', RNRestart.Restart); + } + + bridgeNode.setBridgeProperty('rn', ReactNative); + + // keep alive + setInterval(() => { + // I don't do anything... + // BUT i am needed - otherwise RN's batched bridge starts to hang in detox... ??? + }, 60); + } } -if (bridgeNode) { - bridgeNode.provideReload(RNRestart.Restart); - bridgeNode.provideReactNativeModule(reactNative); - - // keep alive - setInterval(() => { - // I don't do anything... - // BUT i am needed - otherwise RN's batched bridge starts to hang in detox... ??? - }, 60); -} +let hasInitialized = false; export default { /** - * Makes the main module to be tested accessible to nodejs - * @param moduleExports + * Expose a property in node on the global.bridge object + * @param key + * @param value */ - provideModule(moduleExports) { + setBridgeProperty(key, value) { + if (INTERNAL_KEYS.includes(key)) return; if (bridgeNode) { - bridgeNode.provideModule(moduleExports); - bridgeNode.ready(); - } - }, + bridgeNode.setBridgeProperty(key, value); - /** - * Makes the root component accessible to nodejs - e.g. bridge.root.setState({ ... }); - * @param rootComponent - */ - provideRoot(rootComponent) { - if (bridgeNode) { - bridgeNode.provideRoot(rootComponent); + // notify ready on first setBridgeProp + if (!hasInitialized) { + bridgeNode._ready(); + hasInitialized = true; + } } }, }; diff --git a/tests-new/e2e/bridge.spec.js b/tests-new/e2e/bridge.spec.js index e9b007f9..782d6eb0 100755 --- a/tests-new/e2e/bridge.spec.js +++ b/tests-new/e2e/bridge.spec.js @@ -1,8 +1,9 @@ const should = require('should'); describe('bridge', () => { - beforeEach(async () => { + beforeEach(async function beforeEach() { await device.reloadReactNative(); + bridge.root.setState({ message: this.currentTest.title }); }); it('should provide -> global.bridge', () => { @@ -10,19 +11,49 @@ describe('bridge', () => { return Promise.resolve(); }); - it('should provide -> global.bridge.module', () => { + // main react-native module you're testing on + // in our case react-native-firebase + it('should provide -> bridge.module', () => { should(bridge.module).not.be.undefined(); return Promise.resolve(); }); - it('should provide -> global.bridge.rn', () => { + // react-native module access + it('should provide -> bridge.rn', () => { should(bridge.rn).not.be.undefined(); should(bridge.rn.Platform.OS).be.a.String(); should(bridge.rn.Platform.OS).equal(device.getPlatform()); return Promise.resolve(); }); - it('should provide -> global.reload and allow reloadReactNative usage', async () => { + // 'global' context of the app's JS environment + it('should provide -> bridge.context', () => { + should(bridge.context).not.be.undefined(); + should(bridge.context.setTimeout).be.a.Function(); + should(bridge.context.window).be.a.Object(); + // etc ... e.g. __coverage__ is here also if covering + return Promise.resolve(); + }); + + // the apps root component + // allows you to read and set state if required + it('should provide -> bridge.root', async () => { + should(bridge.root).not.be.undefined(); + should(bridge.root.setState).be.a.Function(); + should(bridge.root.state).be.a.Object(); + + // test setting state + await new Promise(resolve => + bridge.root.setState({ message: 'hello world' }, resolve) + ); + should(bridge.root.state.message).equal('hello world'); + return Promise.resolve(); + }); + + // we shim our own reloadReactNative functionality as the detox reloadReactNative built-in + // hangs often and seems unpredictable - todo: investigate & PR if solution found + // reloadReactNative is replaced on init with bridge.root automatically + it('should allow reloadReactNative usage without breaking remote debug', async () => { should(bridge.reload).be.a.Function(); // and check it works without breaking anything await device.reloadReactNative(); @@ -30,8 +61,15 @@ describe('bridge', () => { return Promise.resolve(); }); - it('should allow detox to launchApp without breaking remote debug', async () => { + it('should allow launchApp usage without breaking remote debug', async () => { + should(bridge.module).not.be.undefined(); + should(bridge.reload).be.a.Function(); + should(bridge.rn).not.be.undefined(); + should(bridge.rn.Platform.OS).be.a.String(); + should(bridge.rn.Platform.OS).equal(device.getPlatform()); + await device.launchApp({ newInstance: true }); + should(bridge.module).not.be.undefined(); should(bridge.reload).be.a.Function(); should(bridge.rn).not.be.undefined(); diff --git a/tests-new/e2e/init.js b/tests-new/e2e/init.js index 0e5f6c4a..6d43f60b 100755 --- a/tests-new/e2e/init.js +++ b/tests-new/e2e/init.js @@ -12,3 +12,7 @@ before(async () => { after(async () => { await detox.cleanup(); }); + +bridge.beforeContextReset = () => { + console.log('reset'); +};