Repository: wix/Detox
Pull request #2448 (2020-11-03 10:49:45)

Support debugSynchronization in .detoxrc
- [x] This change has been discussed in issue #1306 and the solution has been agreed upon with maintainers.

---

**Description:**

Extends `session` config, so it can hold a default `--debug-synchronization` value inside `detoxrc`, giving more flexibility for projects that use `.detoxrc.js` (dynamic configs).


```json
"session": {
  "debugSynchronization": 20000
},
```


Patch:
diff --git a/detox/src/client/Client.js b/detox/src/client/Client.js
index 8c9c80837..8afa238ab 100644
--- a/detox/src/client/Client.js
+++ b/detox/src/client/Client.js
@@ -11,7 +11,7 @@ class Client {
     this.configuration = config;
     this.ws = new AsyncWebSocket(config.server);
     this.slowInvocationStatusHandler = null;
-    this.slowInvocationTimeout = argparse.getArgValue('debug-synchronization');
+    this.slowInvocationTimeout = config.debugSynchronization;
     this.successfulTestRun = true; // flag for cleanup
     this.pandingAppCrash;
 
diff --git a/detox/src/configuration/composeSessionConfig.js b/detox/src/configuration/composeSessionConfig.js
index 56bbf4ab8..a8b430b05 100644
--- a/detox/src/configuration/composeSessionConfig.js
+++ b/detox/src/configuration/composeSessionConfig.js
@@ -1,5 +1,6 @@
 const getPort = require('get-port');
 const uuid = require('../utils/uuid');
+const isValidWebsocketURL = require('../utils/isValidWebsocketURL');
 
 /**
  *
@@ -7,22 +8,45 @@ const uuid = require('../utils/uuid');
  * @param {*} detoxConfig
  * @param {*} deviceConfig
  */
-async function composeSessionConfig({ errorBuilder, detoxConfig, deviceConfig }) {
-  const session = deviceConfig.session || detoxConfig.session || {
-    autoStart: true,
-    server: `ws://localhost:${await getPort()}`,
-    sessionId: uuid.UUID(),
+async function composeSessionConfig({ errorBuilder, cliConfig, detoxConfig, deviceConfig }) {
+  const session = {
+    ...detoxConfig.session,
+    ...deviceConfig.session,
   };
 
-  if (!session.server) {
-    throw errorBuilder.missingServerProperty();
+  if (session.server != null) {
+    const value = session.server;
+    if (typeof value !== 'string' || !isValidWebsocketURL(value)) {
+      throw errorBuilder.invalidServerProperty();
+    }
+  }
+
+  if (session.sessionId != null) {
+    const value = session.sessionId;
+    if (typeof value !== 'string' || value.length === 0) {
+      throw errorBuilder.invalidSessionIdProperty();
+    }
+  }
+
+  if (session.debugSynchronization != null) {
+    const value = session.debugSynchronization;
+    if (typeof value !== 'number' || value < 0) {
+      throw errorBuilder.invalidDebugSynchronizationProperty();
+    }
   }
 
-  if (!session.sessionId) {
-    throw errorBuilder.missingSessionIdProperty();
+  if (cliConfig.debugSynchronization > 0) {
+    session.debugSynchronization = +cliConfig.debugSynchronization;
   }
 
-  return session;
+  return {
+    autoStart: !session.server,
+    server: `ws://localhost:${await getPort()}`,
+    sessionId: uuid.UUID(),
+    debugSynchronization: false,
+
+    ...session,
+  };
 }
 
 module.exports = composeSessionConfig;
diff --git a/detox/src/configuration/index.js b/detox/src/configuration/index.js
index e6567e654..ed3f4c41f 100644
--- a/detox/src/configuration/index.js
+++ b/detox/src/configuration/index.js
@@ -67,8 +67,10 @@ async function composeDetoxConfig({
   });
 
   const sessionConfig = await composeSessionConfig({
+    cliConfig,
     detoxConfig,
     deviceConfig,
+    errorBuilder,
   });
 
   return {
diff --git a/detox/src/errors/DetoxConfigErrorBuilder.js b/detox/src/errors/DetoxConfigErrorBuilder.js
index b775a4677..40ac374f0 100644
--- a/detox/src/errors/DetoxConfigErrorBuilder.js
+++ b/detox/src/errors/DetoxConfigErrorBuilder.js
@@ -124,9 +124,21 @@ class DetoxConfigErrorBuilder {
     });
   }
 
-  missingServerProperty() {
+  invalidServerProperty() {
     return new DetoxConfigError({
-      message: `session.server property is missing, should hold the server address`,
+      message: `session.server property is not a valid WebSocket URL`,
+      hint: `Expected something like "ws://localhost:8099".\nCheck that in your Detox config${_atPath(this.filepath)}`,
+      inspectOptions: { depth: 3 },
+      debugInfo: _.omitBy({
+        session: _.get(this.contents, ['session']),
+        ...this._focusOnConfiguration(c => _.pick(c, ['session'])),
+      }, _.isEmpty),
+    });
+  }
+
+  invalidSessionIdProperty() {
+    return new DetoxConfigError({
+      message: `session.sessionId property should be a non-empty string`,
       hint: `Check that in your Detox config${_atPath(this.filepath)}`,
       inspectOptions: { depth: 3 },
       debugInfo: _.omitBy({
@@ -136,9 +148,9 @@ class DetoxConfigErrorBuilder {
     });
   }
 
-  missingSessionIdProperty() {
+  invalidDebugSynchronizationProperty() {
     return new DetoxConfigError({
-      message: `session.sessionId property is missing, should hold the server session id`,
+      message: `session.debugSynchronization should be a positive number`,
       hint: `Check that in your Detox config${_atPath(this.filepath)}`,
       inspectOptions: { depth: 3 },
       debugInfo: _.omitBy({
diff --git a/detox/src/utils/isValidWebsocketURL.js b/detox/src/utils/isValidWebsocketURL.js
new file mode 100644
index 000000000..64a5c3727
--- /dev/null
+++ b/detox/src/utils/isValidWebsocketURL.js
@@ -0,0 +1,12 @@
+const { URL } = require('url');
+
+function isValidWebsocketURL(url) {
+  try {
+    const { protocol } = new URL(url);
+    return protocol === 'ws:' || protocol === 'wss:';
+  } catch (err) {
+    return false;
+  }
+}
+
+module.exports = isValidWebsocketURL;
diff --git a/docs/APIRef.Configuration.md b/docs/APIRef.Configuration.md
index 531e9b5e8..738465951 100644
--- a/docs/APIRef.Configuration.md
+++ b/docs/APIRef.Configuration.md
@@ -38,6 +38,9 @@ Please find the [Detox example app](/examples/demo-react-native/detox.config.js)
   // ...
   "detox": {
     // ...
+    "session": {
+      "debugSynchronization": 20000
+    },
     "configurations": {
       "ios.sim.debug": {
         "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
@@ -193,12 +196,12 @@ Detox can either initialize a server using a generated configuration, or can be
 ```json
 {
   "session": {
-  "server": "ws://localhost:8099",
-  "sessionId": "YourProjectSessionId"
-}
+    "server": "ws://localhost:8099",
+    "sessionId": "YourProjectSessionId"
+  }
 ```
 
-Session can also be set per configuration:
+Session can be set also per configuration — then it takes a higher priority over the global session config:
 
 ```json
 {  
@@ -207,14 +210,25 @@ Session can also be set per configuration:
       ...
       "session": {
         "server": "ws://localhost:8099",
-        "sessionId": "YourProjectSessionId"
+        "sessionId": "YourProjectSessionId",
       }
     }
   }
 }
 ```
 
+Also, you can specify an optional numeric `debugSynchronization` parameter
+(see also `--debug-synchronization` in [APIRef.DetoxCLI.md#test](APIRef.DetoxCLI.md#test)).
+When an action/expectation takes a significant amount time, use this option to print device synchronization status.
+The status will be printed if the action takes more than _[N]_ ms to complete.
 
+```json
+{
+    "session": {
+      "debugSynchronization": 20000
+    }
+}
+```
 
 ## detox-cli
 


Test patch:
diff --git a/detox/src/client/Client.test.js b/detox/src/client/Client.test.js
index 7bf32c681..f8ddc0b36 100644
--- a/detox/src/client/Client.test.js
+++ b/detox/src/client/Client.test.js
@@ -1,24 +1,25 @@
 const tempfile = require('tempfile');
 const actions = require('./actions/actions');
-const config = require('../configuration/configurations.mock').validOneDeviceAndSession.session;
 const invoke = require('../invoke');
 const sleep = require('../utils/sleep');
+const { validOneDeviceAndSession } = require('../configuration/configurations.mock');
 
 describe('Client', () => {
-  let argparse;
   let WebSocket;
   let Client;
   let client;
   let bunyan;
   let log;
+  let sessionConfig;
 
   beforeEach(() => {
+    sessionConfig = {
+      ...validOneDeviceAndSession.session,
+    };
+
     jest.mock('../utils/logger');
     WebSocket = jest.mock('./AsyncWebSocket');
 
-    jest.mock('../utils/argparse');
-    argparse = require('../utils/argparse');
-
     jest.mock('bunyan')
     bunyan = require('bunyan');
 
@@ -169,7 +170,7 @@ describe('Client', () => {
   });
 
   it(`cleanup() - if not connected should do nothing`, async () => {
-    client = new Client(config);
+    client = new Client(sessionConfig);
     client.ws.send.mockReturnValueOnce(response("cleanupDone", {}, 1));
     await client.cleanup();
 
@@ -215,7 +216,7 @@ describe('Client', () => {
     });
 
     it(`execute() - fast invocation should not trigger "slowInvocationStatus"`, async () => {
-      argparse.getArgValue.mockReturnValue(2); // set debug-slow-invocations
+      sessionConfig.debugSynchronization = 2;
       await connect();
       await executeWithSlowInvocation(1);
       expect(client.ws.send).toHaveBeenLastCalledWith({"params": {"args": ["test"], "method": "matcherForAccessibilityLabel:", "target": {"type": "Class", "value": "GREYMatchers"}}, "type": "invoke"}, undefined);
@@ -223,7 +224,7 @@ describe('Client', () => {
     });
 
     it(`execute() - slow invocation should trigger "slowInvocationStatus:`, async () => {
-      argparse.getArgValue.mockReturnValue(2); // set debug-slow-invocations
+      sessionConfig.debugSynchronization = 2;
       await connect();
       await executeWithSlowInvocation(3);
       expect(client.ws.send).toHaveBeenLastCalledWith({"params": {}, "type": "currentStatus"}, undefined);
@@ -231,7 +232,7 @@ describe('Client', () => {
     });
 
     it(`execute() - slow invocation should do nothing if ws was closed`, async () => {
-      argparse.getArgValue.mockReturnValue(2); // set debug-slow-invocations
+      sessionConfig.debugSynchronization = 2;
       await connect();
       client.ws.isOpen.mockReturnValue(false);
       await executeWithSlowInvocation(4);
@@ -242,7 +243,7 @@ describe('Client', () => {
     // NOTE: this test prevents geometrical progression of currentStatus calls
     // when the device gets really busy. Otherwise, we can get 1000 pending currentStatus calls.
     it(`slowInvocationStatus() - should not schedule currentStatus thrice`, async () => {
-      argparse.getArgValue.mockReturnValue(2); // set debug-slow-invocations
+      sessionConfig.debugSynchronization = 2;
       await connect();
 
       jest.spyOn(client, 'slowInvocationStatus');
@@ -440,7 +441,7 @@ describe('Client', () => {
   });
 
   async function connect() {
-    client = new Client(config);
+    client = new Client(sessionConfig);
     client.ws.send.mockReturnValueOnce(response("loginSuccess", {}, 1));
     await client.connect();
     client.ws.isOpen.mockReturnValue(true);
diff --git a/detox/src/configuration/composeSessionConfig.test.js b/detox/src/configuration/composeSessionConfig.test.js
index b440051a3..eff14c342 100644
--- a/detox/src/configuration/composeSessionConfig.test.js
+++ b/detox/src/configuration/composeSessionConfig.test.js
@@ -2,70 +2,201 @@ const DetoxConfigErrorBuilder = require('../errors/DetoxConfigErrorBuilder');
 
 describe('composeSessionConfig', () => {
   let composeSessionConfig;
-  let detoxConfig, deviceConfig;
+  let cliConfig, detoxConfig, deviceConfig;
   /** @type {DetoxConfigErrorBuilder} */
   let errorBuilder;
 
   beforeEach(() => {
     composeSessionConfig = require('./composeSessionConfig');
     errorBuilder = new DetoxConfigErrorBuilder();
+    cliConfig = {};
     detoxConfig = {};
     deviceConfig = {};
   });
 
   const compose = () => composeSessionConfig({
+    cliConfig,
     detoxConfig,
     deviceConfig,
     errorBuilder,
   });
 
   it('should generate a default config', async () => {
-    const sessionConfig = await compose();
-
-    expect(sessionConfig).toMatchObject({
+    expect(await compose()).toEqual({
       autoStart: true,
-      server: expect.stringMatching(/^ws:.*localhost:/),
-      sessionId: expect.stringMatching(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i),
+      debugSynchronization: false,
+      server: expect.any(String),
+      sessionId: expect.any(String),
+    });
+  });
+
+  describe('sessionId', function() {
+    describe('by default', () => {
+      it('should be autogenerated GUID', async () => {
+        expect(await compose()).toMatchObject({
+          sessionId: expect.stringMatching(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i),
+        });
+      });
+    });
+
+    it('should pass validations', async () => {
+      detoxConfig.session = { sessionId: 1234 };
+      await expect(compose()).rejects.toThrowError(errorBuilder.invalidSessionIdProperty());
+
+      detoxConfig.session = { sessionId: '' };
+      await expect(compose()).rejects.toThrowError(errorBuilder.invalidSessionIdProperty());
+    });
+
+    describe('when defined in global config', () => {
+      beforeEach(() => {
+        detoxConfig.session = { sessionId: 'someSessionId' };
+      });
+
+      it('should use the specified value', async () => {
+        expect((await compose()).sessionId).toBe('someSessionId');
+      });
+
+      describe('and in device config', () => {
+        beforeEach(() => {
+          deviceConfig.session = { sessionId: 'otherSessionId' };
+        });
+
+        it('should use the specified value', async () => {
+          expect((await compose()).sessionId).toBe('otherSessionId');
+        });
+      });
+    });
+  });
+
+  describe('server', function() {
+    describe('by default', () => {
+      it('should be autogenerated', async () => {
+        expect(await compose()).toMatchObject({
+          server: expect.stringMatching(/^ws:.*localhost:/),
+        });
+      });
+    });
+
+    it('should pass validations', async () => {
+      detoxConfig.session = { server: 1234 };
+      await expect(compose()).rejects.toThrowError(errorBuilder.invalidServerProperty());
+
+      detoxConfig.session = { server: 'http://invalid-protocol.com' };
+      await expect(compose()).rejects.toThrowError(errorBuilder.invalidServerProperty());
+    });
+
+    describe('when defined in global config', () => {
+      beforeEach(() => {
+        detoxConfig.session = { server: 'ws://myserver:1100' };
+      });
+
+      it('should use the specified value', async () => {
+        expect(await compose()).toMatchObject({
+          server: 'ws://myserver:1100',
+        });
+      });
+
+      describe('and in device config', () => {
+        beforeEach(() => {
+          deviceConfig.session = { server: 'ws://otherserver:1100' };
+        });
+
+        it('should use the specified value', async () => {
+          expect(await compose()).toMatchObject({
+            server: 'ws://otherserver:1100',
+          });
+        });
+      });
     });
   });
 
-  describe('if detoxConfig.session is defined', function() {
-    beforeEach(() => {
-      detoxConfig.session = {
-        server: 'ws://localhost:9999',
-        sessionId: 'someSessionId',
-      };
-    })
+  describe('autoStart', function() {
+    describe('by default', () => {
+      it('should be true', async () => {
+        expect(await compose()).toMatchObject({ autoStart: true });
+      });
+    });
+
+    describe('when autoStart is explicitly false', function() {
+      beforeEach(() => {
+        detoxConfig.session = { autoStart: false };
+      });
+
+      it('should override the value', async () => {
+        expect(await compose()).toMatchObject({ autoStart: false });
+      });
+    });
+
+    describe('when server is defined', () => {
+      beforeEach(() => {
+        detoxConfig.session = { server: 'ws://localhost:1100' };
+      });
+
+      it('should be false', async () => {
+        expect(await compose()).toMatchObject({ autoStart: false });
+      });
 
-    it('should return detoxConfig.session', async () => {
-      expect(await compose()).toEqual({
-        server: 'ws://localhost:9999',
-        sessionId: 'someSessionId',
+      describe('when autoStart is explicitly true', function() {
+        beforeEach(() => {
+          detoxConfig.session.autoStart = true;
+        });
+
+        it('should override the value', async () => {
+          expect(await compose()).toMatchObject({ autoStart: true });
+        });
       });
     });
+  });
 
-    test(`providing empty server config should throw`, () => {
-      delete detoxConfig.session.server;
-      expect(compose()).rejects.toThrowError(errorBuilder.missingServerProperty());
+  describe('debugSynchronization', function () {
+    describe('by default', () => {
+      it('should be false', async () => {
+        expect(await compose()).toMatchObject({
+          debugSynchronization: false,
+        });
+      });
     });
 
-    test(`providing server config with no session should throw`, () => {
-      delete detoxConfig.session.sessionId;
-      expect(compose()).rejects.toThrowError(errorBuilder.missingSessionIdProperty());
+    it('should pass validations', async () => {
+      detoxConfig.session = { debugSynchronization: -1 };
+      await expect(compose()).rejects.toThrowError(errorBuilder.invalidDebugSynchronizationProperty());
+
+      detoxConfig.session = { debugSynchronization: '3000' };
+      await expect(compose()).rejects.toThrowError(errorBuilder.invalidDebugSynchronizationProperty());
     });
 
-    describe('if deviceConfig.session is defined', function() {
+    describe('when defined in global config', () => {
       beforeEach(() => {
-        detoxConfig.session = {
-          server: 'ws://localhost:1111',
-          sessionId: 'anotherSession',
-        };
+        detoxConfig.session = { debugSynchronization: 10000 };
       });
 
-      it('should return deviceConfig.session instead of detoxConfig.session', async () => {
-        expect(await compose()).toEqual({
-          server: 'ws://localhost:1111',
-          sessionId: 'anotherSession',
+      it('should use that value', async () => {
+        expect(await compose()).toMatchObject({
+          debugSynchronization: 10000,
+        });
+      });
+
+      describe('and in device config', () => {
+        beforeEach(() => {
+          deviceConfig.session = { debugSynchronization: 20000 };
+        });
+
+        it('should use that value', async () => {
+          expect(await compose()).toMatchObject({
+            debugSynchronization: 20000,
+          });
+        });
+
+        describe('and in CLI config', () => {
+          beforeEach(() => {
+            cliConfig.debugSynchronization = 3000;
+          });
+
+          it('should use that value', async () => {
+            expect(await compose()).toMatchObject({
+              debugSynchronization: 3000,
+            });
+          });
         });
       });
     });
diff --git a/detox/src/errors/DetoxConfigErrorBuilder.test.js b/detox/src/errors/DetoxConfigErrorBuilder.test.js
index c6ad942e6..d8daa9530 100644
--- a/detox/src/errors/DetoxConfigErrorBuilder.test.js
+++ b/detox/src/errors/DetoxConfigErrorBuilder.test.js
@@ -257,13 +257,13 @@ describe('DetoxConfigErrorBuilder', () => {
     });
   });
 
-  describe('.missingServerProperty', () => {
+  describe('.invalidServerProperty', () => {
     beforeEach(() => {
-      build = () => builder.missingServerProperty();
+      build = () => builder.invalidServerProperty();
       builder.setConfigurationName('android.release');
       builder.setDetoxConfig({
         session: {
-          sessionId: '234589798234',
+          server: 'localhost',
         },
         configurations: {
           'android.release': {
@@ -286,9 +286,9 @@ describe('DetoxConfigErrorBuilder', () => {
     });
   });
 
-  describe('.missingSessionId', () => {
+  describe('.invalidSessionId', () => {
     beforeEach(() => {
-      build = () => builder.missingSessionIdProperty();
+      build = () => builder.invalidSessionIdProperty();
       builder.setConfigurationName('android.release');
       builder.setDetoxConfig({
         configurations: {
@@ -298,7 +298,48 @@ describe('DetoxConfigErrorBuilder', () => {
               avdName: 'Pixel_2_API_29',
             },
             session: {
-              server: 'ws://localhost:12837',
+              sessionId: 234589798234,
+            },
+          }
+        }
+      });
+    });
+
+    it('should create a generic error, if the config location is not known', () => {
+      expect(build()).toMatchSnapshot();
+    });
+
+    it('should create an error with a hint, if the config location is known', () => {
+      builder.setDetoxConfigPath('/home/detox/myproject/.detoxrc.json');
+      expect(build()).toMatchSnapshot();
+    });
+
+    it('should point to global session if there is one', () => {
+      builder.setDetoxConfig({
+        session: {
+          server: 'ws://localhost:12837',
+        },
+        configurations: {},
+      })
+
+      builder.setDetoxConfigPath('/home/detox/myproject/.detoxrc.json');
+      expect(build()).toMatchSnapshot();
+    });
+  });
+
+  describe('.invalidDebugSynchronizationProperty', () => {
+    beforeEach(() => {
+      build = () => builder.invalidDebugSynchronizationProperty();
+      builder.setConfigurationName('android.release');
+      builder.setDetoxConfig({
+        configurations: {
+          'android.release': {
+            type: 'android.emulator',
+            device: {
+              avdName: 'Pixel_2_API_29',
+            },
+            session: {
+              debugSynchronization: '3000',
             },
           }
         }
diff --git a/detox/src/errors/__snapshots__/DetoxConfigErrorBuilder.test.js.snap b/detox/src/errors/__snapshots__/DetoxConfigErrorBuilder.test.js.snap
index 974d54b56..48fb6f09c 100644
--- a/detox/src/errors/__snapshots__/DetoxConfigErrorBuilder.test.js.snap
+++ b/detox/src/errors/__snapshots__/DetoxConfigErrorBuilder.test.js.snap
@@ -29,6 +29,131 @@ exports[`DetoxConfigErrorBuilder .failedToReadConfiguration should create a simp
 EISDIR: illegal operation on a directory, read]
 `;
 
+exports[`DetoxConfigErrorBuilder .invalidDebugSynchronizationProperty should create a generic error, if the config location is not known 1`] = `
+[DetoxConfigError: session.debugSynchronization should be a positive number
+
+HINT: Check that in your Detox config.
+
+{
+  configurations: {
+    'android.release': {
+      session: {
+        debugSynchronization: '3000'
+      }
+    }
+  }
+}]
+`;
+
+exports[`DetoxConfigErrorBuilder .invalidDebugSynchronizationProperty should create an error with a hint, if the config location is known 1`] = `
+[DetoxConfigError: session.debugSynchronization should be a positive number
+
+HINT: Check that in your Detox config at path:
+/home/detox/myproject/.detoxrc.json
+
+{
+  configurations: {
+    'android.release': {
+      session: {
+        debugSynchronization: '3000'
+      }
+    }
+  }
+}]
+`;
+
+exports[`DetoxConfigErrorBuilder .invalidDebugSynchronizationProperty should point to global session if there is one 1`] = `
+[DetoxConfigError: session.debugSynchronization should be a positive number
+
+HINT: Check that in your Detox config at path:
+/home/detox/myproject/.detoxrc.json
+
+{
+  session: {
+    server: 'ws://localhost:12837'
+  }
+}]
+`;
+
+exports[`DetoxConfigErrorBuilder .invalidServerProperty should create a generic error, if the config location is not known 1`] = `
+[DetoxConfigError: session.server property is not a valid WebSocket URL
+
+HINT: Expected something like "ws://localhost:8099".
+Check that in your Detox config.
+
+{
+  session: {
+    server: 'localhost'
+  },
+  configurations: {
+    'android.release': {}
+  }
+}]
+`;
+
+exports[`DetoxConfigErrorBuilder .invalidServerProperty should create an error with a hint, if the config location is known 1`] = `
+[DetoxConfigError: session.server property is not a valid WebSocket URL
+
+HINT: Expected something like "ws://localhost:8099".
+Check that in your Detox config at path:
+/home/detox/myproject/.detoxrc.json
+
+{
+  session: {
+    server: 'localhost'
+  },
+  configurations: {
+    'android.release': {}
+  }
+}]
+`;
+
+exports[`DetoxConfigErrorBuilder .invalidSessionId should create a generic error, if the config location is not known 1`] = `
+[DetoxConfigError: session.sessionId property should be a non-empty string
+
+HINT: Check that in your Detox config.
+
+{
+  configurations: {
+    'android.release': {
+      session: {
+        sessionId: 234589798234
+      }
+    }
+  }
+}]
+`;
+
+exports[`DetoxConfigErrorBuilder .invalidSessionId should create an error with a hint, if the config location is known 1`] = `
+[DetoxConfigError: session.sessionId property should be a non-empty string
+
+HINT: Check that in your Detox config at path:
+/home/detox/myproject/.detoxrc.json
+
+{
+  configurations: {
+    'android.release': {
+      session: {
+        sessionId: 234589798234
+      }
+    }
+  }
+}]
+`;
+
+exports[`DetoxConfigErrorBuilder .invalidSessionId should point to global session if there is one 1`] = `
+[DetoxConfigError: session.sessionId property should be a non-empty string
+
+HINT: Check that in your Detox config at path:
+/home/detox/myproject/.detoxrc.json
+
+{
+  session: {
+    server: 'ws://localhost:12837'
+  }
+}]
+`;
+
 exports[`DetoxConfigErrorBuilder .malformedAppLaunchArgs should create an error with specifying the config name 1`] = `
 [DetoxConfigError: Invalid type of "launchArgs" property in detox.configurations["android.release"]
 Expected an object.
@@ -175,83 +300,6 @@ HINT: Check that in your Detox config at path:
 }]
 `;
 
-exports[`DetoxConfigErrorBuilder .missingServerProperty should create a generic error, if the config location is not known 1`] = `
-[DetoxConfigError: session.server property is missing, should hold the server address
-
-HINT: Check that in your Detox config.
-
-{
-  session: {
-    sessionId: '234589798234'
-  },
-  configurations: {
-    'android.release': {}
-  }
-}]
-`;
-
-exports[`DetoxConfigErrorBuilder .missingServerProperty should create an error with a hint, if the config location is known 1`] = `
-[DetoxConfigError: session.server property is missing, should hold the server address
-
-HINT: Check that in your Detox config at path:
-/home/detox/myproject/.detoxrc.json
-
-{
-  session: {
-    sessionId: '234589798234'
-  },
-  configurations: {
-    'android.release': {}
-  }
-}]
-`;
-
-exports[`DetoxConfigErrorBuilder .missingSessionId should create a generic error, if the config location is not known 1`] = `
-[DetoxConfigError: session.sessionId property is missing, should hold the server session id
-
-HINT: Check that in your Detox config.
-
-{
-  configurations: {
-    'android.release': {
-      session: {
-        server: 'ws://localhost:12837'
-      }
-    }
-  }
-}]
-`;
-
-exports[`DetoxConfigErrorBuilder .missingSessionId should create an error with a hint, if the config location is known 1`] = `
-[DetoxConfigError: session.sessionId property is missing, should hold the server session id
-
-HINT: Check that in your Detox config at path:
-/home/detox/myproject/.detoxrc.json
-
-{
-  configurations: {
-    'android.release': {
-      session: {
-        server: 'ws://localhost:12837'
-      }
-    }
-  }
-}]
-`;
-
-exports[`DetoxConfigErrorBuilder .missingSessionId should point to global session if there is one 1`] = `
-[DetoxConfigError: session.sessionId property is missing, should hold the server session id
-
-HINT: Check that in your Detox config at path:
-/home/detox/myproject/.detoxrc.json
-
-{
-  session: {
-    server: 'ws://localhost:12837'
-  }
-}]
-`;
-
 exports[`DetoxConfigErrorBuilder .noConfigurationAtGivenPath should create an error with the attempted config path 1`] = `
 [DetoxConfigError: Failed to find Detox config at:
 /home/detox/myproject/.detoxrc.json
diff --git a/detox/src/utils/isValidWebsocketURL.test.js b/detox/src/utils/isValidWebsocketURL.test.js
new file mode 100644
index 000000000..6e545246d
--- /dev/null
+++ b/detox/src/utils/isValidWebsocketURL.test.js
@@ -0,0 +1,14 @@
+const isValidWebsocketURL = require('./isValidWebsocketURL');
+
+describe('isValidWebsocketURL', () => {
+  it.each([
+    [true, 'ws://localhost'],
+    [true, 'wss://localhost:3428'],
+    [false, 'http://localhost:3428'],
+    [false, '3428'],
+    [false, 3428],
+    [false, null],
+  ])('should return %j for %s', (expected, url) => {
+    expect(isValidWebsocketURL(url)).toBe(expected);
+  });
+});
diff --git a/detox/test/package.json b/detox/test/package.json
index 5527640f6..1135d9d72 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -11,7 +11,7 @@
     "start": "react-native start",
     "packager": "react-native start",
     "detox-server": "detox run-server",
-    "e2e:ios": "detox test -c ios.sim.release --debug-synchronization",
+    "e2e:ios": "detox test -c ios.sim.release",
     "e2e:android": "detox test -c android.emu.release",
     "e2e:android-debug": "detox test -c android.emu.debug",
     "e2e:ios-ci": "npm run e2e:ios -- --workers 3 --retries 1",
@@ -93,6 +93,9 @@
         "type": "ios.simulator",
         "device": {
           "type": "iPhone 11 Pro"
+        },
+        "session": {
+          "debugSynchronization": 3000
         }
       },
       "ios.none": {