[tests] new test infra - start of bridge cleanup
This commit is contained in:
parent
31efb51751
commit
cbe5d27c2a
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -12,3 +12,7 @@ before(async () => {
|
|||
after(async () => {
|
||||
await detox.cleanup();
|
||||
});
|
||||
|
||||
bridge.beforeContextReset = () => {
|
||||
console.log('reset');
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue