Repository: wix/Detox
Pull request #3780 (2022-12-23 10:26:10)

refactor(tests): reusable cli mock
## Description

This PR prepares ground for the `detox start` PR #3766.

It makes it simple to use the mock `executable` script for integration tests in `local-cli` subpackage when we're testing that the arguments and environment are passed correctly to the child processes.


Patch:
diff --git a/detox/local-cli/__mocks__/executable b/detox/local-cli/__mocks__/executable
index db749e27f..11f0cd997 100755
--- a/detox/local-cli/__mocks__/executable
+++ b/detox/local-cli/__mocks__/executable
@@ -4,22 +4,32 @@ const fs = require('fs');
 
 const DETOX_REGEXP = /^DETOX_(?!DISABLE_POD_INSTALL|DISABLE_POSTINSTALL)/;
 
-const env = Object.keys(process.env)
-  .filter(k => DETOX_REGEXP.test(k.toUpperCase()))
-  .reduce((acc, key) => {
-    acc[key] = process.env[key];
-    return acc;
-  }, {});
+function main() {
+    const [, ...argv] = process.argv;
+    const env = Object.keys(process.env)
+        .filter(k => DETOX_REGEXP.test(k.toUpperCase()))
+        .reduce((acc, key) => {
+            acc[key] = process.env[key];
+            return acc;
+        }, {});
 
-const [, ...argv] = process.argv;
+    const output = JSON.stringify({ env, argv }) + '\n';
 
-const output = JSON.stringify({ env, argv }) + '\n';
-if (process.env.CLI_TEST_STDOUT) {
-    fs.appendFileSync(process.env.CLI_TEST_STDOUT, output);
-} else {
-    process.stdout.write(output);
+    if (process.env.CLI_TEST_STDOUT) {
+        fs.appendFileSync(process.env.CLI_TEST_STDOUT, output);
+    } else {
+        process.stdout.write(output);
+    }
+
+    if (process.env.CLI_EXIT_CODE) {
+        process.exit(+process.env.CLI_EXIT_CODE);
+    } else {
+        process.exit(0);
+    }
 }
 
-if (process.env.CLI_EXIT_CODE) {
-    process.exit(+process.env.CLI_EXIT_CODE);
+if (process.env.CLI_SLEEP > 0) {
+    setTimeout(main, +process.env.CLI_SLEEP);
+} else {
+    main();
 }
diff --git a/detox/package.json b/detox/package.json
index f10440c02..037193971 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -41,6 +41,7 @@
     "@types/ws": "^7.4.0",
     "@typescript-eslint/eslint-plugin": "^5.4.0",
     "@typescript-eslint/parser": "^5.4.0",
+    "cross-env": "^7.0.3",
     "eslint": "^8.3.0",
     "eslint-plugin-import": "^2.23.3",
     "eslint-plugin-no-only-tests": "^2.6.0",


Test patch:
diff --git a/detox/__tests__/helpers.js b/detox/__tests__/helpers.js
index 020eb46f7..4669bb0d3 100644
--- a/detox/__tests__/helpers.js
+++ b/detox/__tests__/helpers.js
@@ -1,6 +1,8 @@
 const path = require('path');
 
+const fs = require('fs-extra');
 const _ = require('lodash');
+const tempfile = require('tempfile');
 const yargs = require('yargs');
 
 function callCli(modulePath, cmd) {
@@ -27,14 +29,58 @@ function callCli(modulePath, cmd) {
         'camel-case-expansion': false,
         'dot-notation': false,
         'duplicate-arguments-array': false,
+        'populate--': true,
       })
       .command(spiedModule)
+      .wrap(null)
       .exitProcess(false)
       .fail((msg, err) => reject(err || msg))
       .parse(cmd, (err) => err && reject(err));
   });
 }
 
+function buildMockCommand(options = {}) {
+  if (!options.stdout) {
+    options.stdout = tempfile('.txt');
+  }
+
+  return {
+    get cmd() {
+      const env = [
+        options.exitCode ? `CLI_EXIT_CODE=${options.exitCode}` : undefined,
+        options.sleep ? `CLI_SLEEP=${options.sleep}` : undefined,
+        `CLI_TEST_STDOUT=${options.stdout}`,
+      ].filter(Boolean).join(' ');
+
+      return `cross-env ${env} node ${path.join(__dirname, '../local-cli/__mocks__/executable')}`;
+    },
+
+    options,
+
+    _calls: undefined,
+
+    get calls() {
+      if (this._calls === undefined) {
+        if (fs.existsSync(options.stdout)) {
+          this._calls = fs.readFileSync(options.stdout, 'utf8')
+            .trim()
+            .split('\n')
+            .map(c => JSON.parse(c));
+        } else {
+          this._calls = [];
+        }
+      }
+
+      return this._calls;
+    },
+
+    async clean() {
+      await fs.remove(options.stdout);
+    },
+  };
+}
+
+exports.buildMockCommand = buildMockCommand;
 exports.callCli = callCli;
 exports.latestInstanceOf = (clazz) => _.last(clazz.mock.instances);
 exports.lastCallTo = (mocked) => _.last(mocked.mock.calls);
diff --git a/detox/local-cli/test.test.js b/detox/local-cli/test.test.js
index 5dbc81fe4..d21bf84ea 100644
--- a/detox/local-cli/test.test.js
+++ b/detox/local-cli/test.test.js
@@ -11,17 +11,16 @@ jest.mock('./utils/jestInternals');
 const cp = require('child_process');
 const cpSpawn = cp.spawn;
 const os = require('os');
-const path = require('path');
 const util = require('util');
 
 const fs = require('fs-extra');
 const _ = require('lodash');
-const yargs = require('yargs');
+
+const { buildMockCommand, callCli } = require('../__tests__/helpers');
 
 const { DEVICE_LAUNCH_ARGS_DEPRECATION } = require('./testCommand/warnings');
 
 describe('CLI', () => {
-  let _cliCallDump;
   let _env;
   let logger;
   let _temporaryFiles;
@@ -31,26 +30,25 @@ describe('CLI', () => {
   let GenyDeviceRegistryFactory;
   let jestInternals;
 
+  let mockExecutable;
+
   afterEach(() => {
     cp.spawn = cpSpawn;
   });
 
   beforeEach(() => {
-    _cliCallDump = undefined;
     _env = process.env;
     _temporaryFiles = [];
 
-    process.env = {
-      ..._env,
-      CLI_TEST_STDOUT: tempfile('.txt'),
-    };
+    process.env = { ..._env };
 
-    const executable = path.relative(process.cwd(), path.join(__dirname, '__mocks__/executable'));
+    mockExecutable = buildMockCommand();
+    _temporaryFiles.push(mockExecutable.options.stdout);
 
     detoxConfig = {
       testRunner: {
         args: {
-          $0: os.platform() === 'win32' ? `node ${executable}` : executable,
+          $0: mockExecutable.cmd,
           config: 'e2e/config.json'
         },
         forwardEnv: true,
@@ -98,7 +96,7 @@ describe('CLI', () => {
   describe('by default', () => {
     test('by default, should attempt to load config from package.json or .detoxrc', async () => {
       const expectedError = /^Cannot run Detox without a configuration/;
-      await expect(runRaw('test')).rejects.toThrowError(expectedError);
+      await expect(callCli('./test', 'test')).rejects.toThrowError(expectedError);
     });
   });
 
@@ -519,45 +517,6 @@ describe('CLI', () => {
     return tempFilePath;
   }
 
-  async function runRaw(...command) {
-    let memArgv;
-
-    try {
-      memArgv = process.argv.splice(2, Infinity, ...command);
-
-      return await new Promise((resolve, reject) => {
-        const testCommand = require('./test');
-        const originalHandler = testCommand.handler;
-
-        const parser = yargs()
-          .scriptName('detox')
-          .parserConfiguration({
-            'boolean-negation': false,
-            'camel-case-expansion': false,
-            'dot-notation': false,
-            'duplicate-arguments-array': false,
-            'populate--': true,
-          })
-          .command({
-            ...testCommand,
-            async handler(argv) {
-              try {
-                await originalHandler(argv);
-                resolve();
-              } catch (e) {
-                reject(e);
-              }
-            },
-          })
-          .wrap(null);
-
-        parser.parse(command, err => err && reject(err));
-      });
-    } finally {
-      memArgv && process.argv.splice(2, Infinity, ...memArgv);
-    }
-  }
-
   async function run(...args) {
     let contents = `module.exports = ${util.inspect(detoxConfig, { depth: Infinity })};`;
     if (detoxConfig.testRunner && detoxConfig.testRunner.inspectBrk) {
@@ -566,18 +525,11 @@ describe('CLI', () => {
 
     detoxConfigPath = tempfile('.js', contents);
     const __configPath = Math.random() > 0.5 ? '-C' : '--config-path';
-    return runRaw('test', __configPath, detoxConfigPath, ...args);
+    await callCli('./test', ['test', __configPath, detoxConfigPath, ...args]);
   }
 
   function cliCall(index = 0) {
-    if (!_cliCallDump) {
-      _cliCallDump = fs.readFileSync(process.env.CLI_TEST_STDOUT, 'utf8')
-        .split('\n')
-        .filter(Boolean)
-        .map(line => JSON.parse(line));
-    }
-
-    const mockCall = _cliCallDump[index];
+    const mockCall = mockExecutable.calls[index];
     if (!mockCall) {
       return null;
     }
@@ -597,6 +549,7 @@ describe('CLI', () => {
   }
 
   function mockExitCode(code) {
-    process.env.CLI_EXIT_CODE = code;
+    mockExecutable.options.exitCode = code;
+    detoxConfig.testRunner.args.$0 = mockExecutable.cmd;
   }
 });