Skip to content

Commit

Permalink
Improve content of native errors shown on the JS test runner (wix#2427)
Browse files Browse the repository at this point in the history
  • Loading branch information
d4vidi authored Oct 19, 2020
1 parent 1cbb538 commit 2ca7cc3
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.wix.detox
import android.content.Context
import android.util.Log
import androidx.test.espresso.IdlingResource
import com.wix.detox.common.extractRootCause
import com.wix.detox.instruments.DetoxInstrumentsException
import com.wix.detox.instruments.DetoxInstrumentsManager
import com.wix.invoke.MethodInvocation
Expand Down Expand Up @@ -47,22 +48,33 @@ class InvokeActionHandler(
private val errorParse: (e: Throwable?) -> String)
: DetoxActionHandler {

private val VIEW_HIERARCHY_TEXT = "View Hierarchy:"

override fun handle(params: String, messageId: Long) {
try {
val invocationResult = methodInvocation.invoke(params)
wsClient.sendAction("invokeResult", mapOf<String, Any?>("result" to invocationResult), messageId)
} catch (e: InvocationTargetException) {
Log.i(LOG_TAG, "Test exception", e)
val message = e.targetException.message
val error = message?.substringBefore("View Hierarchy:")?.trim()
val viewHierarchy = message?.substringAfter("View Hierarchy:")
wsClient.sendAction("testFailed", mapOf<String, Any?>("details" to "${error}\n",
"viewHierarchy" to viewHierarchy), messageId)
val payload = extractFailurePayload(e)
wsClient.sendAction("testFailed", payload, messageId)
} catch (e: Exception) {
Log.e(LOG_TAG, "Exception", e)
wsClient.sendAction("error", mapOf<String, Any?>("error" to "${errorParse(e)}\nCheck device logs for full details!\n"), messageId)
}
}

private fun extractFailurePayload(e: InvocationTargetException): Map<String, Any?>
= e.targetException.message?.let { message: String ->
if (message.contains(VIEW_HIERARCHY_TEXT)) {
val error = message.substringBefore(VIEW_HIERARCHY_TEXT).trim()
val viewHierarchy = message.substringAfter(VIEW_HIERARCHY_TEXT).trim()
mapOf<String, Any?>("details" to "${error}\n", "viewHierarchy" to viewHierarchy)
} else {
val error = extractRootCause(e.targetException)
mapOf<String, Any?>("details" to error.message)
}
} ?: emptyMap()
}

class CleanupActionHandler(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.wix.detox.common

fun extractRootCause(t: Throwable): Throwable {
var ex: Throwable = t
while (ex.cause != null) {
ex = ex.cause!!
}
return ex
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,19 +138,40 @@ object DetoxActionHandlersSpec : Spek({
eq(messageId))
}

it("should handle an InvocationTargetException") {
val targetException = Exception("mock-error-reason")
it("should handle an InvocationTargetException and extract view hierarchy") {
val targetException = Exception("before View Hierarchy: after")
val exception = InvocationTargetException(targetException)
whenever(methodInvocationMock.invoke(isA<String>())).thenThrow(exception)

uut().handle(params, messageId)

verify(wsClient).sendAction(
eq("testFailed"),
argThat { size == 2 && this["details"] is String &&
this["viewHierarchy"] is String },
argThat {
this["details"] == "before\n" &&
this["viewHierarchy"] == "after" &&
size == 2
},
eq(messageId))
}

it("should handle a non-view-hierarchy InvocationTargetException") {
val rootException = RuntimeException("root-exception-mock")
val targetException = Exception("target-exception-mock", rootException)
val exception = InvocationTargetException(targetException)
whenever(methodInvocationMock.invoke(isA<String>())).thenThrow(exception)

uut().handle(params, messageId)

verify(wsClient).sendAction(
eq("testFailed"),
argThat {
this["details"] == "root-exception-mock" &&
size == 1
},
eq(messageId))

}
}

describe("InstrumentsRecording recording state actions") {
Expand Down
36 changes: 34 additions & 2 deletions detox/src/client/Client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('Client', () => {
let WebSocket;
let Client;
let client;
let bunyan;
let log;

beforeEach(() => {
Expand All @@ -18,8 +19,12 @@ describe('Client', () => {
jest.mock('../utils/argparse');
argparse = require('../utils/argparse');

jest.mock('bunyan')
bunyan = require('bunyan');

Client = require('./Client');
log = require('../utils/logger');
log.level.mockReturnValue(bunyan.DEBUG)
});

it(`reloadReactNative() - should receive ready from device and resolve`, async () => {
Expand Down Expand Up @@ -279,14 +284,41 @@ describe('Client', () => {
expect(client.ws.send).toHaveBeenCalledTimes(2);
});

it(`execute() - "testFailed" result should throw`, async () => {
it(`execute() - "testFailed" result should throw with view hierarchy`, async () => {
await connect();
client.ws.send.mockReturnValueOnce(response("testFailed", {details: "this is an error", viewHierarchy: 'mock-hierarchy'}, 1));
const call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityLabel:', 'test');
try {
await client.execute(call);
} catch (ex) {
expect(ex).toBeDefined();
expect(ex.toString()).toContain('View Hierarchy:\nmock-hierarchy');
}
});

it(`execute() - "testFailed" result should throw with view-hierarchy hint`, async () => {
log.level.mockReturnValue(bunyan.INFO);

await connect();
client.ws.send.mockReturnValueOnce(response("testFailed", {details: "this is an error", viewHierarchy: 'mock-hierarchy'}, 1));
const call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityLabel:', 'test');
try {
await client.execute(call);
} catch (ex) {
expect(ex).toBeDefined();
expect(ex.toString()).toContain('use log-level verbose or higher');
}
});

it(`execute() - "testFailed" result should throw without a view hierarchy`, async () => {
await connect();
client.ws.send.mockReturnValueOnce(response("testFailed", {details: "this is an error"}, 1));
client.ws.send.mockReturnValueOnce(response("testFailed", {details: "this is an error", viewHierarchy: undefined}, 1));
const call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityLabel:', 'test');
try {
await client.execute(call);
} catch (ex) {
expect(ex).toBeDefined();
expect(ex.toString()).not.toContain('View Hierarchy:');
}
});

Expand Down
12 changes: 8 additions & 4 deletions detox/src/client/actions/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,15 @@ class Invoke extends Action {
async handle(response) {
switch (response.type) {
case 'testFailed':
throw new Error('Test Failed: ' + response.params.details +
let message = 'Test Failed: ' + response.params.details;
if (response.params.viewHierarchy) {
/* istanbul ignore next */
(log.level() <= bunyan.DEBUG ?
'\nView Hierarchy:\n' + response.params.viewHierarchy :
'\nTIP: To print view hierarchy on failed actions/matches, use loglevel verbose and above.'));
message += (log.level() <= bunyan.DEBUG ?
'\nView Hierarchy:\n' + response.params.viewHierarchy :
'\nTIP: To print view hierarchy on failed actions/matches, use log-level verbose or higher.');
}

throw new Error(message);
case 'invokeResult':
return response.params;
case 'error':
Expand Down
1 change: 1 addition & 0 deletions detox/src/utils/__mocks__/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class FakeLogger {
this.opts = opts;
this.log = jest.fn();
this.reinitialize = jest.fn();
this.level = jest.fn();

for (const method of METHODS) {
this[method] = jest.fn().mockImplementation((...args) => {
Expand Down
3 changes: 3 additions & 0 deletions detox/test/e2e/03.actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ describe('Actions', () => {
await driver.sluggishTapElement.tap();
} catch (e) {
console.log('Got an expected error', e);
if (!e.toString().includes('Tap handled too slowly, and turned into a long-tap!')) {
throw new Error('Error content isn\'t as expected!');
}
return;
}

Expand Down

0 comments on commit 2ca7cc3

Please sign in to comment.