Repository: wix/Detox
Pull request #1467 (2019-06-21 10:16:55)

fix(ws): explicit error handling
Fixes the issue we began getting after bumping `ws` to `^3.3.1`.
 
- [x] This is a small change 

---

**Description:**

Private builds on TeamCity began failing with unhandled `read ECONNRESET` error in logs, and the domino effect led to failures in consequent tests.

To my great displeasure, I could not reproduce it consistently in Detox e2e suite, but the fix has been verified locally on the private build.

More details here: https://github.com/websockets/ws/issues/1256


Patch:
diff --git a/detox/src/client/actions/actions.js b/detox/src/client/actions/actions.js
index c3f761f0c..1d060cb46 100644
--- a/detox/src/client/actions/actions.js
+++ b/detox/src/client/actions/actions.js
@@ -83,14 +83,14 @@ class ReloadReactNative extends Action {
 
 class Cleanup extends Action {
   constructor(stopRunner) {
-    const params = {
-      stopRunner: stopRunner
-    };
-    super('cleanup', params);
+    super('cleanup', { stopRunner });
+    this.messageId = -0xc1ea;
   }
 
   async handle(response) {
-    this.expectResponseOfType(response, 'cleanupDone');
+    if (response.type !== 'testeeDisconnected') {
+      this.expectResponseOfType('cleanupDone');
+    }
   }
 }
 
diff --git a/detox/src/server/DetoxServer.js b/detox/src/server/DetoxServer.js
index 7f748502c..ea7d23366 100644
--- a/detox/src/server/DetoxServer.js
+++ b/detox/src/server/DetoxServer.js
@@ -45,10 +45,18 @@ class DetoxServer {
         }
       });
 
+      ws.on('error', (e) => {
+        this.log.warn({ event: 'WEBSOCKET_ERROR', role, sessionId }, `${e && e.message} (role=${role}, session=${sessionId})`);
+      });
+
       ws.on('close', () => {
         if (sessionId && role) {
           this.log.debug({ event: 'DISCONNECT' }, `role=${role}, sessionId=${sessionId}`);
 
+          if (role === ROLE_TESTEE) {
+            this.sendToOtherRole(sessionId, role, { type: 'testeeDisconnected', messageId: -0xc1ea });
+          }
+
           if (this.standalone && role === ROLE_TESTER) {
             this.sendToOtherRole(sessionId, role, { type: 'testerDisconnected', messageId: -1 });
           }
@@ -70,6 +78,13 @@ class DetoxServer {
       this.sendAction(ws, action);
     } else {
       this.log.debug({ event: 'CANNOT_FORWARD' }, `role=${otherRole} not connected, cannot fw action (sessionId=${sessionId})`);
+
+      if (role === ROLE_TESTER && action.type === 'cleanup') {
+        this.sendToOtherRole(sessionId, otherRole, {
+          type: 'testeeDisconnected',
+          messageId: action.messageId,
+        });
+      }
     }
   }
 


Test patch:
diff --git a/detox/src/client/Client.test.js b/detox/src/client/Client.test.js
index 709e53109..d15c69262 100644
--- a/detox/src/client/Client.test.js
+++ b/detox/src/client/Client.test.js
@@ -110,6 +110,16 @@ describe('Client', () => {
     expect(client.ws.send).toHaveBeenCalledTimes(3);
   });
 
+  it(`cleanup() - if connected should accept testeeDisconnected action too`, async () => {
+    await connect();
+    client.ws.send.mockReturnValueOnce(response("ready", {}, 1));
+    await client.waitUntilReady();
+    client.ws.send.mockReturnValueOnce(response("testeeDisconnected", {}, 2));
+    await client.cleanup();
+
+    expect(client.ws.send).toHaveBeenCalledTimes(3);
+  });
+
   it(`cleanup() - if not connected should do nothing`, async () => {
     client = new Client(config);
     client.ws.send.mockReturnValueOnce(response("cleanupDone", {}, 1));
diff --git a/detox/test/e2e/23.flows.test.js b/detox/test/e2e/23.flows.test.js
new file mode 100644
index 000000000..2b6351e3d
--- /dev/null
+++ b/detox/test/e2e/23.flows.test.js
@@ -0,0 +1,6 @@
+describe('Flows', () => {
+  it('should exit without timeouts if app was terminated inside test', async () => {
+    await device.launchApp();
+    await device.terminateApp();
+  });
+});