Skip to content

Commit

Permalink
Handling native crashes gracefully
Browse files Browse the repository at this point in the history
  • Loading branch information
rotemmiz committed Feb 6, 2018
1 parent 86e7951 commit fb9fea8
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 13 deletions.
12 changes: 12 additions & 0 deletions detox/src/Detox.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const log = require('npmlog');
const DetoxError = require('./client/DetoxError');
const Device = require('./devices/Device');
const IosDriver = require('./devices/IosDriver');
const SimulatorDriver = require('./devices/SimulatorDriver');
Expand Down Expand Up @@ -92,14 +93,25 @@ class Detox {
const testArtifactsPath = this._artifactsPathsProvider.createPathForTest(this._currentTestNumber, ...testNameComponents);
this.device.setArtifactsDestination(testArtifactsPath);
}

await this._handleAppCrash(testNameComponents[1]);
}

async afterEach(suiteName, testName) {
if(this._artifactsPathsProvider !== undefined) {
await this.device.finalizeArtifacts();
}

await this._handleAppCrash(testName);
}

async _handleAppCrash(testName) {
const pendingAppCrash = this.client.getPendingCrashAndReset();
if (pendingAppCrash) {
log.error('',`App crashed in test '${testName}', here's the native stack trace: \n${pendingAppCrash}`);
await this.device.launchApp({newInstance:true});
}
}
async _getSessionConfig() {
const session = this.userSession || await configuration.defaultSession();

Expand Down
27 changes: 24 additions & 3 deletions detox/src/client/AsyncWebSocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class AsyncWebSocket {
this.url = url;
this.ws = undefined;
this.inFlightPromises = {};
this.eventCallbacks = {};
this.messageIdCounter = 0;
}

Expand All @@ -32,11 +33,15 @@ class AsyncWebSocket {

this.ws.onmessage = (response) => {
log.verbose(`ws`, `onMessage: ${response.data}`);
let pendingId = JSON.parse(response.data).messageId;
let pendingPromise = this.inFlightPromises[pendingId];
let messageId = JSON.parse(response.data).messageId;
let pendingPromise = this.inFlightPromises[messageId];
if (pendingPromise) {
pendingPromise.resolve(response.data);
delete this.inFlightPromises[pendingId];
delete this.inFlightPromises[messageId];
}
let eventCallback = this.eventCallbacks[messageId];
if (eventCallback) {
eventCallback(response.data);
}
};

Expand All @@ -58,6 +63,10 @@ class AsyncWebSocket {
});
}

setEventCallback(eventId, callback) {
this.eventCallbacks[eventId] = callback;
}

async close() {
return new Promise(async(resolve, reject) => {
if (this.ws) {
Expand All @@ -83,6 +92,18 @@ class AsyncWebSocket {
}
return this.ws.readyState === WebSocket.OPEN;
}

rejectAll(error) {
_.forEach(this.inFlightPromises, (promise, messageId) => {
let pendingPromise = this.inFlightPromises[messageId];
if (pendingPromise) {
pendingPromise.reject(error);
delete this.inFlightPromises[messageId];
}
});


}
}

module.exports = AsyncWebSocket;
10 changes: 10 additions & 0 deletions detox/src/client/AsyncWebSocket.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ describe('AsyncWebSocket', () => {
}
});

it(`eventCallback should be triggered on a registered messageId when sent from testee`, async () => {
const mockCallback = jest.fn();
const mockedResponse = generateResponse('onmessage', -10000);
await connect(client);
client.setEventCallback(-10000, mockCallback);

client.ws.onmessage(mockedResponse);
expect(mockCallback).toHaveBeenCalledWith(mockedResponse.data);
});

async function connect(client) {
const result = {};
const promise = client.open();
Expand Down
36 changes: 30 additions & 6 deletions detox/src/client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ class Client {
this.slowInvocationStatusHandler = null;
this.slowInvocationTimeout = argparse.getArgValue('debug-synchronization');
this.successfulTestRun = true; // flag for cleanup
this.pandingAppCrash;

this.setActionListener(new actions.AppWillTerminateWithError(), (response) => {
this.pandingAppCrash = response.params.errorDetails;
this.ws.rejectAll(this.pandingAppCrash);
});
}

async connect() {
Expand All @@ -18,22 +24,21 @@ class Client {
}

async reloadReactNative() {
await this.sendAction(new actions.ReloadReactNative(), -1000);
await this.sendAction(new actions.ReloadReactNative());
}

async sendUserNotification(params) {
await this.sendAction(new actions.SendUserNotification(params));
}

async waitUntilReady() {
await this.sendAction(new actions.Ready(), -1000);
await this.sendAction(new actions.Ready());
this.isConnected = true;
}

async cleanup() {
clearTimeout(this.slowInvocationStatusHandler);

if (this.isConnected) {
if (this.isConnected && !this.pandingAppCrash) {
await this.sendAction(new actions.Cleanup(this.successfulTestRun));
this.isConnected = false;
}
Expand Down Expand Up @@ -68,8 +73,27 @@ class Client {
clearTimeout(this.slowInvocationStatusHandler);
}

async sendAction(action, messageId) {
const response = await this.ws.send(action, messageId);
getPendingCrashAndReset() {
const crash = this.pandingAppCrash;
this.pandingAppCrash = undefined;

return crash;
}

setActionListener(action, clientCallback) {
this.ws.setEventCallback(action.messageId, (response) => {
const parsedResponse = JSON.parse(response);
action.handle(parsedResponse);

/* istanbul ignore next */
if (clientCallback) {
clientCallback(parsedResponse);
}
});
}

async sendAction(action) {
const response = await this.ws.send(action, action.messageId);
const parsedResponse = JSON.parse(response);
await action.handle(parsedResponse);
return parsedResponse;
Expand Down
14 changes: 14 additions & 0 deletions detox/src/client/Client.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const DetoxError = require('./DetoxError');
const config = require('../configurations.mock').validOneDeviceAndSession.session;
const invoke = require('../invoke');

Expand Down Expand Up @@ -150,6 +151,19 @@ describe('Client', () => {
}
});

it(`Throw error if AppWillTerminateWithError event is sent to tester`, async () => {
client.ws.setEventCallback = jest.fn();
await connect();

const event = JSON.stringify({
type: "AppWillTerminateWithError",
params: {errorDetails: "someDetails"},
messageId: -10000
});

expect(() => client.ws.setEventCallback.mock.calls[0][1](event)).toThrow();
});

async function connect() {
client = new Client(config);
client.ws.send.mockReturnValueOnce(response("loginSuccess", {}, 1));
Expand Down
11 changes: 11 additions & 0 deletions detox/src/client/DetoxError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class DetoxError extends Error {
constructor(message) {
super(message);
Error.stackTraceLimit = 0;

Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
}
}

module.exports = DetoxError;
18 changes: 17 additions & 1 deletion detox/src/client/actions/actions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const _ = require('lodash');
const log = require('npmlog');
const DetoxError = require('../DetoxError');

class Action {
constructor(type, params = {}) {
Expand Down Expand Up @@ -32,6 +33,7 @@ class Login extends Action {
class Ready extends Action {
constructor() {
super('isReady');
this.messageId = -1000;
}

async handle(response) {
Expand All @@ -42,6 +44,7 @@ class Ready extends Action {
class ReloadReactNative extends Action {
constructor() {
super('reactNativeReload');
this.messageId = -1000;
}

async handle(response) {
Expand Down Expand Up @@ -117,6 +120,18 @@ class CurrentStatus extends Action {
}
}

class AppWillTerminateWithError extends Action {
constructor(params) {
super(params);
this.messageId = -10000;
}

handle(response) {
this.expectResponseOfType(response, 'AppWillTerminateWithError');
return response.params.errorDetails;
}
}

module.exports = {
Login,
Ready,
Expand All @@ -125,5 +140,6 @@ module.exports = {
Cleanup,
openURL,
SendUserNotification,
CurrentStatus
CurrentStatus,
AppWillTerminateWithError
};
2 changes: 1 addition & 1 deletion detox/test/e2e/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ beforeEach(async function() {
});

afterEach(async function() {
await detox.afterEach();
await detox.afterEach(this.currentTest.parent.title, this.currentTest.title);
});
24 changes: 24 additions & 0 deletions detox/test/e2e/p-crash-handling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

describe.only('Crash Handling, not to be ran on CI', () => {

beforeEach(async () => {
await device.launchApp({newInstance: false});
});

it('Should throw error upon app crash', async () => {
let failed = false;

await element(by.text('Crash')).tap();
try {
await expect(element(by.text('Crash'))).toBeVisible();
} catch(ex) {
failed = true;
}

if (!failed) throw new Error('Test should have thrown an error, but did not');
});

it('Should recover from app crash', async () => {
await expect(element(by.text('Sanity'))).toBeVisible();
});
});
11 changes: 9 additions & 2 deletions detox/test/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,22 @@ class example extends Component {
};
}

renderScreenButton(title, component) {
renderButton(title, onPressCallback) {
return (
<TouchableOpacity onPress={() => {
this.setState({screen: component});
onPressCallback();
}}>
<Text style={{color: 'blue', marginBottom: 20}}>{title}</Text>
</TouchableOpacity>
);
}

renderScreenButton(title, component) {
return this.renderButton(title, () => {
this.setState({screen: component});
});
}

renderText(text) {
return (
<View style={{flex: 1, paddingTop: 20, justifyContent: 'center', alignItems: 'center'}}>
Expand Down Expand Up @@ -78,6 +84,7 @@ class example extends Component {
{this.renderScreenButton('Network', Screens.NetworkScreen)}
{this.renderScreenButton('Animations', Screens.AnimationsScreen)}
{this.renderScreenButton('Location', Screens.LocationScreen)}
{this.renderButton('Crash', () => {throw new Error('Simulated Crash')})}
</View>
);
}
Expand Down

0 comments on commit fb9fea8

Please sign in to comment.