Repository: wix/Detox
Pull request #2395 (2020-10-08 17:33:27)

DETOX_ARGV_OVERRIDE env var
**Description:**

E.g., you have a failing build on CI, but there's:
1. --loglevel verbose, but you want `--loglevel trace`.
2. there is one suite failing and you want to re-run it ASAP, e.g.: `e2e/03.actions.test.js`
3. your build is hanging, and without adding `--forceExit` to Jest you are unable to get the artifacts from CI.

With this `$DETOX_ARGV_OVERRIDE` environment variable, now all the above is possible in one click:

![image](https://user-images.githubusercontent.com/1962469/95493670-7315e280-09a5-11eb-979b-c10f3e3ecd56.png)

The `detox test` command will merge in those args from $DETOX_ARGV_OVERRIDE variable, as if you were typing that from a script.

See the excerpt for the updated CLI guide:

#### DETOX_ARGV_OVERRIDE

If you happen to be troubleshooting Detox tests inside a complex script, or a failing CI build (e.g., on TeamCity or Jenkins), there is an escape-hatch feature for running Detox with some extra CLI args just by setting the `DETOX_ARGV_OVERRIDE` environment variable before re-running it again.

```
> export DETOX_ARGV_OVERRIDE="--forceExit -w 1 --testNamePattern='that hanging test' e2e/sanity/login.test.js"
> bash scripts/ci.e2e.sh
  # ... some output ...
  > detox test -c ios.sim.release -l verbose --workers 3
    # ...
    configuration=ios.sim.release ... jest --maxWorkers 1 --forceExit --testNamePattern='that hanging test' e2e/sanity/login.test.js
```

Consider the example above, where `DETOX_ARGV_OVERRIDE` forces Detox to run Jest in a single worker mode with a forceful exit (after 1 second) only for a selected test in a specific file.

As you might see, the idea of `DETOX_ARGV_OVERRIDE` is quite similar to [NODE_OPTIONS](https://nodejs.org/api/cli.html#cli_node_options_options) except for the fact you use it not for regular flows, but for forced ad-hoc patching of a failing Detox configuration to save your time.

Please avoid using it in your regular flows – instead, use Detox configuration files (`.detoxrc.js`) as your primary choice.


noomorph: Hmm... maybe I'll rename it to $DETOX_OPTS, to allude to NODE_OPTS. 🤔 
d4vidi: Oh geez. This would make a very powerful feature... perhaps _too_ powerful. Was there a particular use case to have driven this?

Patch:
diff --git a/detox/local-cli/utils/deprecation.js b/detox/local-cli/utils/deprecation.js
deleted file mode 100644
index 348dcf181..000000000
--- a/detox/local-cli/utils/deprecation.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const chalk = require('chalk');
-const log = require('../../src/utils/logger').child({ __filename });
-
-function coerceDeprecation(option) {
-  return function coerceDeprecationFn(value) {
-    log.warn(`Beware: ${option} will be removed in the next version of Detox.`);
-
-    return value;
-  };
-}
-
-module.exports = {
-  coerceDeprecation,
-};
diff --git a/detox/local-cli/utils/shellQuote.js b/detox/local-cli/utils/shellQuote.js
index 8174e9c86..091e02bef 100644
--- a/detox/local-cli/utils/shellQuote.js
+++ b/detox/local-cli/utils/shellQuote.js
@@ -1,9 +1,30 @@
+const _ = require('lodash');
+const shellQuote = require('shell-quote');
 const { autoEscape } = require('../../src/utils/shellUtils');
 
 function quote(argv) {
   return argv.map(arg => autoEscape(arg)).join(' ');
 }
 
+function parse(str) {
+  return _.chain('')
+    .thru(() => shellQuote.parse(str, process.env))
+    .map(arg => {
+      if (_.isObject(arg)) {
+        if (arg.op === 'glob') {
+          return arg.pattern;
+        }
+
+        return null;
+      }
+
+      return arg;
+    })
+    .compact()
+    .value();
+}
+
 module.exports = {
+  parse,
   quote,
 };
diff --git a/detox/local-cli/utils/warnings.js b/detox/local-cli/utils/warnings.js
new file mode 100644
index 000000000..9fd6803e2
--- /dev/null
+++ b/detox/local-cli/utils/warnings.js
@@ -0,0 +1,35 @@
+const chalk = require('chalk');
+const log = require('../../src/utils/logger').child({ __filename });
+
+function coerceDeprecation(option) {
+  return function coerceDeprecationFn(value) {
+    log.warn(`Beware: ${option} will be removed in the next version of Detox.`);
+
+    return value;
+  };
+}
+
+const DETOX_ARGV_OVERRIDE_NOTICE = `
+   _____ _____ ___________
+  /  ___|_   _|  _  | ___ \\  $DETOX_ARGV_OVERRIDE is detected
+  \\ '--.  | | | | | | |_/ /
+   '--. \\ | | | | | |  __/   This feature is designed solely
+  /\\__/ / | | \\ \\_/ / |      for ad-hoc troubleshooting of
+  \\____/  \\_/  \\___/\\_|      failing builds.
+
+  Do not use this feature in scripts and do not rely on it
+  for anything that is not a quick-and-dirty rerun of your
+  failing E2E script.
+
+  Use it sparingly for emergency Detox launches with extra
+  debugging flags and features, to speed up your investigation
+  and collect additional test artifacts. For more details, see:
+
+  https://github.com/wix/Detox/blob/master/docs/APIRef.DetoxCLI.md#detox_argv_override
+
+`;
+
+module.exports = {
+  coerceDeprecation,
+  DETOX_ARGV_OVERRIDE_NOTICE,
+};
diff --git a/detox/package.json b/detox/package.json
index 61c0f80d6..f452b95ef 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -55,6 +55,7 @@
     "proper-lockfile": "^3.0.2",
     "resolve-from": "^5.0.0",
     "sanitize-filename": "^1.6.1",
+    "shell-quote": "^1.7.2",
     "signal-exit": "^3.0.3",
     "tail": "^2.0.0",
     "telnet-client": "1.2.8",
diff --git a/docs/APIRef.Artifacts.md b/docs/APIRef.Artifacts.md
index 134c2e72a..29588d6a8 100644
--- a/docs/APIRef.Artifacts.md
+++ b/docs/APIRef.Artifacts.md
@@ -20,7 +20,7 @@ Artifacts are disabled by default. Two things are required to enable them:
 Alternatively, you might leverage the [device.takeScreenshot()](APIRef.DeviceObjectAPI.md#devicetakescreenshotname) API for manual control.
 
 * To change artifacts root directory location (by default it is `./artifacts`), add `--artifacts-location <path>`.  
-**NOTE:** <a id="slash-convention">There</a> is a slightly obscure convention. If you want to create automatically a subdirectory with timestamp and configuration name (to avoid file overwrites upon consquent re-runs), specify a path to directory that does not end with a slash. Otherwise, if you want to put artifacts straight to the specified directory (in a case where you make a single run only, e.g. on CI), add a slash (or a backslash) to the end.
+**NOTE:** <a id="slash-convention">There</a> is a slightly obscure convention. If you want to create automatically a subdirectory with timestamp and configuration name (to avoid file overwrites upon consquent reruns), specify a path to directory that does not end with a slash. Otherwise, if you want to put artifacts straight to the specified directory (in a case where you make a single run only, e.g. on CI), add a slash (or a backslash) to the end.
 
 ```sh
 detox test --artifacts-location /tmp/detox_artifacts  # will also append /android.emu.release.2018-06-14 08:54:11Z
diff --git a/docs/APIRef.Configuration.md b/docs/APIRef.Configuration.md
index 82d4132aa..531e9b5e8 100644
--- a/docs/APIRef.Configuration.md
+++ b/docs/APIRef.Configuration.md
@@ -91,6 +91,7 @@ Detox can store artifacts such as transient screenshots and device logs. You can
     "plugins": {
       "instruments": { "enabled": false },
       "log": { "enabled": true },
+      "uiHierarchy": "enabled",
       "screenshot": {
         "shouldTakeAutomaticSnapshots": true,
         "keepOnlyFailedTestsArtifacts": true,
diff --git a/docs/APIRef.DetoxCLI.md b/docs/APIRef.DetoxCLI.md
index a8caa02d7..0dd8b4a5e 100644
--- a/docs/APIRef.DetoxCLI.md
+++ b/docs/APIRef.DetoxCLI.md
@@ -91,7 +91,33 @@ Initiating your test suite. <sup>[[1]](#notice-passthrough)</sup>
 | --inspect-brk | Uses [node's --inspect-brk](https://nodejs.org/en/docs/guides/debugging-getting-started/#enable-inspector) flag to let users debug the jest/mocha test runner <br />*Default: false* |
 | --help                                        | Show help |
 
-##### Notices
+#### DETOX_ARGV_OVERRIDE
+
+If you happen to be troubleshooting Detox tests inside a complex script, or a failing CI build
+(e.g., on TeamCity or Jenkins), there is an escape-hatch feature for running Detox with
+some extra CLI args just by setting the `DETOX_ARGV_OVERRIDE` environment variable before
+rerunning it again.
+
+```
+> export DETOX_ARGV_OVERRIDE="--forceExit -w 1 --testNamePattern='that hanging test' e2e/sanity/login.test.js"
+> bash scripts/ci.e2e.sh
+  # ... some output ...
+  > detox test -c ios.sim.release -l verbose --workers 3
+    # ...
+    configuration=ios.sim.release ... jest --maxWorkers 1 --forceExit --testNamePattern='that hanging test' e2e/sanity/login.test.js
+```
+
+Consider the example above, where `DETOX_ARGV_OVERRIDE` forces Detox to run Jest in a single worker
+mode with a forceful exit (after 1 second) only for a selected test in a specific file.
+
+As you might see, the idea of `DETOX_ARGV_OVERRIDE` is quite similar to [NODE_OPTIONS](https://nodejs.org/api/cli.html#cli_node_options_options)
+except for the fact you use it not for regular flows, but for forced ad-hoc patching of a failing Detox configuration to
+save your time.
+
+Please avoid using it in your regular flows – instead, use Detox configuration files (`.detoxrc.js`)
+as your primary choice.
+
+#### Notices
 
 1. <a name="notice-passthrough">It</a> should be noted that `detox test` is a convenience method to trigger an execution
 of a supported test runner, so for the most part it reads configuration from CLI args and `package.json` and remaps it
diff --git a/docs/Guide.Migration.md b/docs/Guide.Migration.md
index 8b8bf0bde..34699a111 100644
--- a/docs/Guide.Migration.md
+++ b/docs/Guide.Migration.md
@@ -18,7 +18,7 @@ From now on, Detox explicitly depends on `jest-cli` package (marked as a peerDep
 Cannot find module 'jest-cli/build/cli/args'
 ```
 
-~You should add `jest-cli` to your `package.json`'s `devDependencies` and re-run `npm install`, e.g.:~
+~You should add `jest-cli` to your `package.json`'s `devDependencies` and rerun `npm install`, e.g.:~
 
 **UPD**: since `detox@17.5.2` you can ignore this advice. The problem should go away **without** these edits:
 
@@ -550,7 +550,7 @@ Previously, to enable log recording you just had to specify `--artifact-location
 
 Notice that `--artifact-location` became sensitive to whether you end your directory path with a slash or not. It has the next convention:
 
-* If you want to create automatically a subdirectory with timestamp and configuration name (to avoid file overwrites upon consquent re-runs), specify a path to directory that *does not end* with a slash.
+* If you want to create automatically a subdirectory with timestamp and configuration name (to avoid file overwrites upon consquent reruns), specify a path to directory that *does not end* with a slash.
 * Otherwise, if you want to put artifacts straight to the specified directory (in a case where you make a single run only, e.g. on CI), *add a slash* to the end.
 
 For more information see [CLI documentation](APIRef.DetoxCLI.md).
diff --git a/docs/Guide.Uninstalling.md b/docs/Guide.Uninstalling.md
index dbe3db52b..f0e0d4d1e 100644
--- a/docs/Guide.Uninstalling.md
+++ b/docs/Guide.Uninstalling.md
@@ -27,7 +27,7 @@ On every test run, Detox rewrites a few temporary files in `DETOX_LIBRARY_ROOT_P
 1. The respective iOS and Android lockfiles to tell apart the busy and the available devices for use with multiple workers:
     * `device.registry.state.lock`
     * `android-device.registry.state.lock`.
-1. A plain text file with the line-separated list of last failed test files to re-run via `-R <retries-count>` CLI feature:
+1. A plain text file with the line-separated list of last failed test files to rerun via `-R <retries-count>` CLI feature:
     * `last-failed.txt`.
 
 The location of `DETOX_LIBRARY_ROOT_PATH` may vary depending on the operating system:


Test patch:
diff --git a/detox/local-cli/test.js b/detox/local-cli/test.js
index 6e60c9198..bd07669ed 100644
--- a/detox/local-cli/test.js
+++ b/detox/local-cli/test.js
@@ -2,7 +2,7 @@ const _ = require('lodash');
 const cp = require('child_process');
 const path = require('path');
 const unparse = require('yargs-unparser');
-const { quote } = require('./utils/shellQuote');
+const { parse, quote } = require('./utils/shellQuote');
 const splitArgv = require('./utils/splitArgv');
 const DetoxRuntimeError = require('../src/errors/DetoxRuntimeError');
 const DeviceRegistry = require('../src/devices/DeviceRegistry');
@@ -11,6 +11,7 @@ const { composeDetoxConfig } = require('../src/configuration');
 const log = require('../src/utils/logger').child({ __filename });
 const { getPlatformSpecificString, printEnvironmentVariables } = require('./utils/misc');
 const { prependNodeModulesBinToPATH } = require('./utils/misc');
+const { DETOX_ARGV_OVERRIDE_NOTICE } = require('./utils/warnings');
 
 module.exports.command = 'test';
 module.exports.desc = 'Run your test suite with the test runner specified in package.json';
@@ -48,6 +49,21 @@ module.exports.handler = async function test(argv) {
   await runTestRunnerWithRetries(forwardedArgs, retries);
 };
 
+module.exports.middlewares = [
+  function applyEnvironmentVariableAddendum(argv, yargs) {
+    if (process.env.DETOX_ARGV_OVERRIDE) {
+      log.warn(DETOX_ARGV_OVERRIDE_NOTICE);
+
+      return yargs.parse([
+        ...process.argv.slice(2),
+        ...parse(process.env.DETOX_ARGV_OVERRIDE),
+      ]);
+    }
+
+    return argv;
+  }
+];
+
 function choosePrepareArgs({ cliConfig, detoxArgs, runner }) {
   if (runner === 'mocha') {
     if (hasMultipleWorkers(cliConfig)) {
diff --git a/detox/local-cli/test.test.js b/detox/local-cli/test.test.js
index 90157fb3d..220d99072 100644
--- a/detox/local-cli/test.test.js
+++ b/detox/local-cli/test.test.js
@@ -14,8 +14,12 @@ describe('CLI', () => {
   let detoxConfig;
   let detoxConfigPath;
   let DeviceRegistry;
+  let _env;
 
   beforeEach(() => {
+    _env = process.env;
+    process.env = { ..._env };
+
     detoxConfig = {
       configurations: {
         single: {
@@ -33,6 +37,8 @@ describe('CLI', () => {
   });
 
   afterEach(async () => {
+    process.env = _env;
+
     await Promise.all(temporaryFiles.map(name => fs.remove(name)));
   });
 
@@ -637,6 +643,15 @@ describe('CLI', () => {
       await run('--inspect-brk');
       expect(cliCall().command).toMatch(RegExp(`^node --inspect-brk ${testRunner}`));
     });
+
+    test('should append $DETOX_ARGV_OVERRIDE to detox test ... command and print a warning', async () => {
+      process.env.PLATFORM = 'ios';
+      process.env.DETOX_ARGV_OVERRIDE = '--inspect-brk --testNamePattern "[$PLATFORM] tap" e2e/sanity/*.test.js';
+      await run();
+
+      expect(cliCall().command).toMatch(/^node --inspect-brk.* --testNamePattern '\[ios\] tap'.* e2e\/sanity\/\*\.test.js$/);
+      expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('$DETOX_ARGV_OVERRIDE is detected'));
+    });
   });
 
   test('should fail for unrecognized test runner', async () => {
@@ -659,34 +674,42 @@ describe('CLI', () => {
   }
 
   async function runRaw(command = '') {
-    return 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));
-    });
+    let argv;
+
+    try {
+      argv = process.argv.splice(2, Infinity, ...command.trim().split(' '));
+
+      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 {
+      argv && process.argv.splice(2, Infinity, ...argv);
+    }
   }
 
   async function run(command = '') {
diff --git a/docs/Troubleshooting.RunningTests.md b/docs/Troubleshooting.RunningTests.md
index 7d9b3f000..18b3ad6c3 100644
--- a/docs/Troubleshooting.RunningTests.md
+++ b/docs/Troubleshooting.RunningTests.md
@@ -10,6 +10,7 @@
 * [Compare to a working setup](#compare-to-a-working-setup)
 * [Take a look at past issues](#take-a-look-at-past-issues)
 * [How to open a new issue](#how-to-open-a-new-issue)
+* [A component is not visible](#a-component-is-not-visible)
 
 ### Enable trace mode
 
@@ -179,6 +180,30 @@ in `package.json` (no deprecation warnings starting from `12.4.0`), as shown bel
 Please mind that if your e2e tests are located at the default path (`e2e`),
 then you don't need to add `"specs"` property explicitly to `package.json`.
 
+### A component is not visible
+
+On iOS, you may run in a situation, when one of the interactions (tap, scroll, etc.) with some element fails with an error like:
+
+```
+Test Failed: View "<RCTScrollView: 0x7f8d32296d70>" is not visible: view does not pass visibility threshold (0% visible of 75% required)
+```
+
+1. Try retriving `ui.viewhierarchy` artifact — sometimes examining the hierarchy visually can give you insights.
+Add a line `"uiHierarchy": "enabled"` to your artifacts configuration in `.detoxrc.js`, like in an example here: [Artifacts Configuration](https://github.com/wix/Detox/blob/master/docs/APIRef.Configuration.md#artifacts-configuration). After a rerun, you should find
+a `ui.viewhierarchy` in a folder of your failing test. Please mind that you need Xcode 12 at least to open `*.viewhierarchy` files.
+
+2. Besides, you can rerun your iOS app with `-detoxDebugVisibility YES` launch argument (do not confuse with Detox CLI arguments). What will happen is, for each view that fails visibility, two images will be saved in your `~/Desktop` folder (e.g., `DETOX_VISIBILITY_CustomView_0x7ffe9f50df10_TEST.png`):
+
+| Visible | Internals |
+|:-------:|:---------:|
+|<img alt="visible" src="https://user-images.githubusercontent.com/2270433/94178906-68872380-fea4-11ea-845c-c55c25b68c0e.png" height=480>|<img alt="internals" src="https://user-images.githubusercontent.com/2270433/94178919-6c1aaa80-fea4-11ea-8db6-64f1f4b21361.png" height=480>|
+
+The first is supposed to show what is seen on screen. The second shows the internal buffer that Detox tests for visibility; if the pixels aren't transparent enough and less than 75% of tested region is not transparent, the view is not considered visible.
+
+If you are developing a React Native app, then the following applies. If, for instance, you see that tapping fails due to a view with `pointerEvents="none"` obscuring your target — well, the only solutions are: either to make the obscurer a descendant of your target (instead of being a sibling), or to tap on the parent container.
+
+If you see that your issue cannot be solved via testID replacement or a simple hierarchy rearrangment, then there's a chance this is a bug in Detox. Make sure to provide your `ui.viewhierarchy` artifact, the pictures generated with `-detoxDebugVisiblity` option and a comprehensive description of the issue backed up with cold and strong arguments.
+
 ### Debug view hierarchy
 
 **Issue:** I added the `testID` prop but I still can't find the view by id in my tests.