Repository: wix/Detox
Pull request #2644 (2021-02-17 12:24:46)

Fix launch-arguments editing bug
## Description

Fix the following bug in apps' launch-arguments editing:
In some cases, an app's config can be generated with no associated `launchArgs` property (typically, users do not predefined ones).

When this happens, all mutations made via the `device.appLaunchArgs` APIs (e.g. `device.appLaunchArgs.modify()`) have no effect on the actual arguments passed to the app using `launchApp()`.


Patch:
diff --git a/detox/src/devices/Device.js b/detox/src/devices/Device.js
index 045f7003f..f49e6c328 100644
--- a/detox/src/devices/Device.js
+++ b/detox/src/devices/Device.js
@@ -1,5 +1,5 @@
 const _ = require('lodash');
-const LaunchArgs = require('./LaunchArgs');
+const LaunchArgsEditor = require('./LaunchArgsEditor');
 const debug = require('../utils/debug'); // debug utils, leave here even if unused
 const { traceCall } = require('../utils/trace');
 
@@ -21,7 +21,7 @@ class Device {
     this._errorComposer = runtimeErrorComposer;
 
     this._currentApp = null;
-    this._cachedAppLaunchArgs = null;
+    this._currentAppLaunchArgs = null;
     this._deviceId = undefined;
     this._processes = {};
 
@@ -43,11 +43,10 @@ class Device {
   }
 
   get appLaunchArgs() {
-    if (!this._cachedAppLaunchArgs) {
-      this._cachedAppLaunchArgs = new LaunchArgs(this._getCurrentApp().launchArgs);
+    if (!this._currentAppLaunchArgs) {
+      this._currentAppLaunchArgs = this._getCurrentAppsLaunchArgs();
     }
-
-    return this._cachedAppLaunchArgs;
+    return this._currentAppLaunchArgs;
   }
 
   async prepare() {
@@ -82,7 +81,7 @@ class Device {
     }
 
     this._currentApp = appConfig;
-    this._cachedAppLaunchArgs = null;
+    this._currentAppLaunchArgs = null;
     await this._inferBundleIdFromBinary();
   }
 
@@ -274,7 +273,6 @@ class Device {
     if (!this._currentApp) {
       throw this._errorComposer.appNotSelected();
     }
-
     return this._currentApp;
   }
 
@@ -403,6 +401,12 @@ class Device {
       this._currentApp.bundleId = await this.deviceDriver.getBundleIdFromBinary(binaryPath);
     }
   }
+
+  _getCurrentAppsLaunchArgs() {
+    const currentApp = this._getCurrentApp();
+    currentApp.launchArgs = currentApp.launchArgs || {};
+    return new LaunchArgsEditor(currentApp.launchArgs);
+  }
 }
 
 module.exports = Device;
diff --git a/detox/src/devices/LaunchArgs.js b/detox/src/devices/LaunchArgsEditor.js
similarity index 70%
rename from detox/src/devices/LaunchArgs.js
rename to detox/src/devices/LaunchArgsEditor.js
index 5f04cbe94..66b5f3da0 100644
--- a/detox/src/devices/LaunchArgs.js
+++ b/detox/src/devices/LaunchArgsEditor.js
@@ -1,7 +1,11 @@
 const _ = require('lodash');
 
-class LaunchArgs {
-  constructor(launchArgs = {}) {
+class LaunchArgsEditor {
+  constructor(launchArgs) {
+    if (!launchArgs) {
+      throw new Error('Cannot instantiate a launch-arguments editor with no reference arguments-object!');
+    }
+
     this._args = launchArgs;
   }
 
@@ -30,4 +34,4 @@ class LaunchArgs {
   }
 }
 
-module.exports = LaunchArgs;
+module.exports = LaunchArgsEditor;
diff --git a/docs/APIRef.LaunchArgs.md b/docs/APIRef.LaunchArgs.md
index 492518772..90b7900ec 100644
--- a/docs/APIRef.LaunchArgs.md
+++ b/docs/APIRef.LaunchArgs.md
@@ -23,7 +23,7 @@ User-defined launch arguments specification is very flexible, and can be defined
 
 | Level                                     | Description                                                  |
 | ----------------------------------------- | ------------------------------------------------------------ |
-| 1. Static Configuration                   | As a part of a static [Detox configuration](APIRef.Configuration.md), using the `launchArg` property.<br />This is can sufficient, for example, if you only require one mock server instance, and can use the same static port throughout the entire testing execution session. |
+| 1. Static Configuration                   | As a part of a static [Detox configuration](APIRef.Configuration.md), using the `launchArgs` property.<br />This is can sufficient, for example, if you only require one mock server instance, and can use the same static port throughout the entire testing execution session. |
 | 2. Static via CLI                         | As arguments specified explicitly in the [command-line](APIRef.DetoxCLI.md) execution of `detox test`, using `--app-launch-args`. |
 | 3.`device.appLaunchArgs`                  | Dynamically, using the [`device.appLaunchArgs`](APIRef.DeviceObjectAPI.md#deviceapplaunchargs) API, which initially holds the static configuration, and then allows for the modification of it before applied through `device.launchApp()`.<br/>Mostly required in complex test environments, where the servers and ports are dynamic, and are deteremined via distinct software components (e.g. separate test kits). |
 | 4. `device.launchApp()` with `launchArgs` | Dynamically and explicitly, using on-site arguments specified in calls to [`device.launchApp()`](APIRef.DeviceObjectAPI.md#devicelaunchappparams) through the `launchArgs` parameter.<br />Ideal for fairly simple test environments, where the ports are dynamic but are in complete control of the user. |


Test patch:
diff --git a/detox/src/devices/LaunchArgs.test.js b/detox/src/devices/LaunchArgs.test.js
deleted file mode 100644
index 10ecd6c36..000000000
--- a/detox/src/devices/LaunchArgs.test.js
+++ /dev/null
@@ -1,83 +0,0 @@
-describe('Device launch-args', () => {
-  const initArgs = {
-    argX: 'valX',
-    argY: { value: 'Y' },
-  };
-
-  let LaunchArgs;
-  beforeEach(() => {
-    LaunchArgs = require('./LaunchArgs');
-  });
-
-  it('should initialize as an empty object', () => {
-    const launchArgs = new LaunchArgs();
-    expect(launchArgs.get()).toEqual({});
-  });
-
-  it('should allow for a custom initial set of args', () => {
-    const launchArgs = new LaunchArgs(initArgs);
-    expect(launchArgs.get()).toEqual(initArgs);
-  });
-
-  it('should allow for arguments setting', () => {
-    const someArgs = {
-      argZ: 'valZ',
-    };
-
-    const launchArgs = new LaunchArgs();
-    launchArgs.modify(someArgs);
-    expect(launchArgs.get()).toEqual(someArgs);
-  });
-
-  it('should return a non-reflecting representation of the launch-arguments via get()', () => {
-    const launchArgs = new LaunchArgs();
-    launchArgs.modify({
-      aLaunchArg: { value: 'aValue?' },
-    });
-
-    launchArgs.get().aLaunchArg.value = 'aValue!';
-    expect(launchArgs.get().aLaunchArg.value).toEqual('aValue?');
-  });
-
-  describe('with custom initial args', () => {
-    it('should merge set arguments into the initial ones', () => {
-      const someArgs = {
-        argZ: 'valZ',
-      };
-      const expectedArgs = {
-        ...initArgs,
-        argZ: 'valZ',
-      };
-
-      const launchArgs = new LaunchArgs(initArgs);
-      launchArgs.modify(someArgs);
-      expect(launchArgs.get()).toEqual(expectedArgs);
-    });
-
-    it('should allow for implicit arguments clearing using undefined as values', () => {
-      const argsModifier = {
-        argX: undefined,
-        argY: null,
-        argZ: 'valZ',
-      };
-      const expectedArgs = {
-        argZ: 'valZ',
-      }
-
-      const launchArgs = new LaunchArgs(initArgs);
-      launchArgs.modify(argsModifier);
-      expect(launchArgs.get()).toStrictEqual(expectedArgs);
-   });
-
-    it('should allow for a complete arguments reset', () => {
-      const someArgs = {
-        argZ: 'valZ',
-      };
-
-      const launchArgs = new LaunchArgs(initArgs);
-      launchArgs.modify(someArgs);
-      launchArgs.reset();
-      expect(launchArgs.get()).toStrictEqual({});
-    });
-  });
-});
diff --git a/detox/src/devices/LaunchArgsEditor.test.js b/detox/src/devices/LaunchArgsEditor.test.js
new file mode 100644
index 000000000..593b7067c
--- /dev/null
+++ b/detox/src/devices/LaunchArgsEditor.test.js
@@ -0,0 +1,83 @@
+describe('Device launch-args editor', () => {
+
+  let launchArgs;
+  let LaunchArgsEditor;
+  beforeEach(() => {
+    launchArgs = {
+      argX: 'valX',
+      argY: { value: 'Y' },
+    };
+    LaunchArgsEditor = require('./LaunchArgsEditor');
+  });
+
+  it('should require a reference plain-object', () => {
+      expect(() => new LaunchArgsEditor(undefined)).toThrow();
+  });
+
+  it('should allow for getting the referenced args-object', () => {
+    const launchArgsEditor = new LaunchArgsEditor(launchArgs);
+    expect(launchArgsEditor.get()).toEqual(launchArgs);
+  });
+
+  it('should allow for arguments setting', () => {
+    const someArgs = {
+      argZ: 'valZ',
+    };
+
+    const launchArgsEditor = new LaunchArgsEditor({});
+    launchArgsEditor.modify(someArgs);
+    expect(launchArgsEditor.get()).toEqual(someArgs);
+  });
+
+  it('should return a non-reflecting representation of the launch-arguments via get()', () => {
+    const launchArgsEditor = new LaunchArgsEditor(launchArgs);
+    launchArgsEditor.modify({
+      aLaunchArg: { value: 'aValue?' },
+    });
+
+    launchArgsEditor.get().aLaunchArg.value = 'aValue!';
+    expect(launchArgsEditor.get().aLaunchArg.value).toEqual('aValue?');
+  });
+
+  describe('with custom initial args', () => {
+    it('should merge set arguments into the initial ones', () => {
+      const someArgs = {
+        argZ: 'valZ',
+      };
+      const expectedArgs = {
+        ...launchArgs,
+        argZ: 'valZ',
+      };
+
+      const launchArgsEditor = new LaunchArgsEditor(launchArgs);
+      launchArgsEditor.modify(someArgs);
+      expect(launchArgsEditor.get()).toEqual(expectedArgs);
+    });
+
+    it('should allow for implicit arguments clearing using undefined as values', () => {
+      const argsModifier = {
+        argX: undefined,
+        argY: null,
+        argZ: 'valZ',
+      };
+      const expectedArgs = {
+        argZ: 'valZ',
+      }
+
+      const launchArgsEditor = new LaunchArgsEditor(launchArgs);
+      launchArgsEditor.modify(argsModifier);
+      expect(launchArgsEditor.get()).toStrictEqual(expectedArgs);
+   });
+
+    it('should allow for a complete arguments reset', () => {
+      const someArgs = {
+        argZ: 'valZ',
+      };
+
+      const launchArgsEditor = new LaunchArgsEditor(launchArgs);
+      launchArgsEditor.modify(someArgs);
+      launchArgsEditor.reset();
+      expect(launchArgsEditor.get()).toStrictEqual({});
+    });
+  });
+});