[tests] new test infra - start of bridge cleanup

This commit is contained in:
Salakar 2018-03-25 02:23:46 +01:00
parent 31efb51751
commit cbe5d27c2a
9 changed files with 264 additions and 84 deletions

View File

@ -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 (
<View>
<Text testID="tap">{this.state.message}</Text>
<Text testID="messageText">{this.state.message}</Text>
</View>
);
}

21
tests-new/bridge/env/node/console.js vendored Normal file
View File

@ -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);
},
};
};

74
tests-new/bridge/env/node/context.js vendored Normal file
View File

@ -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;
},
},
});
},
};

View File

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

17
tests-new/bridge/env/node/ready.js vendored Normal file
View File

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

View File

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

View File

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

View File

@ -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();

View File

@ -12,3 +12,7 @@ before(async () => {
after(async () => {
await detox.cleanup();
});
bridge.beforeContextReset = () => {
console.log('reset');
};