Repository: wix/Detox
Pull request #3929 (2023-02-12 13:25:04)

[Genymotion SaaS] Update gmsaas start flag to fix wrong device timeout
## Description

<!--
Thank you for contributing!

Step 1: Before submitting a pull request that introduces a new functionality or fixes a bug,
please open an issue where we could discuss the suggestion, problem - and potential ways to fix.
-->

- This pull request addresses the issue described here: N/A

<!--
Step 2: Provide an overview of how your fix / enhancement works.
If possible, provide screenshots of the before and after states (even for simple command line options - show the terminal).
-->

In this pull request, I have repeated #3926 by @lilinor with the necessary unit-test fixes.

<!--
Step 3: Please review the checklist below.
-->

---


> _For features/enhancements:_
 - [ ] I have added/updated the relevant references in the [documentation](https://github.com/wix/Detox/tree/master/docs) files.

> _For API changes:_
 - [ ] I have made the necessary changes in the [types index](https://github.com/wix/Detox/blob/master/detox/index.d.ts) file.



Patch:
diff --git a/.buildkite/jobs/pipeline.android_demo_app_rn_69.yml b/.buildkite/jobs/pipeline.android_demo_app_rn_70.yml
similarity index 75%
rename from .buildkite/jobs/pipeline.android_demo_app_rn_69.yml
rename to .buildkite/jobs/pipeline.android_demo_app_rn_70.yml
index 7dbb2a7b0..dec0e9b0f 100644
--- a/.buildkite/jobs/pipeline.android_demo_app_rn_69.yml
+++ b/.buildkite/jobs/pipeline.android_demo_app_rn_70.yml
@@ -1,9 +1,9 @@
-  - label: ":android::react: RN.69 + Android: Demo app"
+  - label: ":android::react: RN .70 + Android: Demo app"
     command:
       - "nvm install"
       - "./scripts/demo-projects.android.sh"
     env:
-      REACT_NATIVE_VERSION: 0.69.7
+      REACT_NATIVE_VERSION: 0.70.7
       DETOX_DISABLE_POD_INSTALL: true
       DETOX_DISABLE_POSTINSTALL: true
     artifact_paths:
diff --git a/.buildkite/jobs/pipeline.ios_demo_app_rn_69.yml b/.buildkite/jobs/pipeline.ios_demo_app_rn_70.yml
similarity index 70%
rename from .buildkite/jobs/pipeline.ios_demo_app_rn_69.yml
rename to .buildkite/jobs/pipeline.ios_demo_app_rn_70.yml
index e61e6f93c..188adfb37 100644
--- a/.buildkite/jobs/pipeline.ios_demo_app_rn_69.yml
+++ b/.buildkite/jobs/pipeline.ios_demo_app_rn_70.yml
@@ -1,9 +1,9 @@
-  - label: ":ios::react: RN.69 + iOS: Demo app"
+  - label: ":ios::react: RN. 70 + iOS: Demo app"
     command:
       - "nvm install"
       - "./scripts/demo-projects.ios.sh"
     env:
-      REACT_NATIVE_VERSION: 0.69.7
+      REACT_NATIVE_VERSION: 0.70.7
     artifact_paths:
       - "/Users/builder/work/coverage/**/*.lcov"
       - "/Users/builder/work/artifacts*.tar.gz"
diff --git a/.buildkite/pipeline_common.sh b/.buildkite/pipeline_common.sh
index b4875d7e1..0b8507294 100755
--- a/.buildkite/pipeline_common.sh
+++ b/.buildkite/pipeline_common.sh
@@ -7,7 +7,7 @@ cat .buildkite/jobs/pipeline.ios_rn_70.yml
 cat .buildkite/jobs/pipeline.android_rn_68.yml
 cat .buildkite/jobs/pipeline.android_rn_70.yml
 cat .buildkite/jobs/pipeline.android_demo_app_rn_68.yml
-cat .buildkite/jobs/pipeline.android_demo_app_rn_69.yml
+cat .buildkite/jobs/pipeline.android_demo_app_rn_70.yml
 cat .buildkite/jobs/pipeline.ios_demo_app_rn_68.yml
-cat .buildkite/jobs/pipeline.ios_demo_app_rn_69.yml
+cat .buildkite/jobs/pipeline.ios_demo_app_rn_70.yml
 cat .buildkite/pipeline.post_processing.yml
diff --git a/README.md b/README.md
index 755760596..c20719854 100644
--- a/README.md
+++ b/README.md
@@ -56,21 +56,28 @@ The most difficult part of automated testing on mobile is the tip of the testing
 
 ### Environment
 
-- **OS**: macOS 10.15 (Catalina) or higher; Ubuntu Linux 20.04 or higher; Windows 10 Version 1803 or higher
-- **Xcode**: 11.0 or higher
-  - **iOS Simulator Runtime**: iOS 13.0 or higher
+**OS:**
+
+- **macOS:** v10.15 (Catalina) or higher
+  - **Xcode**: v11.0 or higher
+  - **iOS Simulator Runtime**: iOS v13.0 or higher
+- **Ubuntu linux:** v20.04 or higher
+- **Windows 10:** Version 1803 or higher
 
 ### React Native
 
 Detox is built from the ground up to support React Native projects as well as pure native ones.
 
-The following React Native versions have been tested:
+Detox works, at least officially, with the following React Native versions:
+
+| iOS             | Android                                                                                                                |
+| --------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| 0.68.x - 0.70.7 | 0.68.x - 0.70.7 -<br/>Visibility edge-case: see this [RN issue](https://github.com/facebook/react-native/issues/23870) |
 
-| iOS             | Android                                                                                                                   |
-| --------------- | ------------------------------------------------------------------------------------------------------------------------- |
-| 0.68.x - 0.69.7 | 0.68.x - 0.69.7 -<br/>Visibility edge-case: see this [RN issue](https://github.com/facebook/react-native/issues/23870) \* |
+Newer versions, as well as React Native's ["New Architecture"](https://reactnative.dev/docs/the-new-architecture/landing-page), are most likely supported, but have not been tested out yet.
+Please [submit an issue](https://github.com/wix/Detox/issues/new/choose) if you find specific problems with more recent React Native versions.
 
-Future versions are most likely supported, but have not been tested yet. Please open issues if you find specific issues with newer React Native versions.
+For support for React Native 0.71.x, please track [this issue](https://github.com/wix/Detox/issues/3884).
 
 ## Get Started with Detox
 
diff --git a/detox/android/build.gradle b/detox/android/build.gradle
index fd17bf188..08251da36 100644
--- a/detox/android/build.gradle
+++ b/detox/android/build.gradle
@@ -23,7 +23,7 @@ buildscript {
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:7.1.1'
+        classpath 'com.android.tools.build:gradle:7.2.1'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
         classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion"
 
diff --git a/detox/android/gradle/wrapper/gradle-wrapper.properties b/detox/android/gradle/wrapper/gradle-wrapper.properties
index 4d1256f9f..a36e959a9 100644
--- a/detox/android/gradle/wrapper/gradle-wrapper.properties
+++ b/detox/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
 
diff --git a/detox/local-cli/run-server.js b/detox/local-cli/run-server.js
index c85ff1aa5..61653d1fb 100644
--- a/detox/local-cli/run-server.js
+++ b/detox/local-cli/run-server.js
@@ -1,5 +1,8 @@
-const DetoxRuntimeError = require('../src/errors/DetoxRuntimeError');
+const collectCliConfig = require('../src/configuration/collectCliConfig');
+const composeLoggerConfig = require('../src/configuration/composeLoggerConfig');
+const { DetoxRuntimeError } = require('../src/errors');
 const DetoxServer = require('../src/server/DetoxServer');
+const logger = require('../src/utils/logger');
 
 module.exports.command = 'run-server';
 module.exports.desc = 'Start a standalone Detox server';
@@ -28,6 +31,14 @@ module.exports.handler = async function runServer(argv) {
     throw new DetoxRuntimeError(`The port should be between 1 and 65535, got ${argv.port}`);
   }
 
+  await logger.setConfig(composeLoggerConfig({
+    // @ts-ignore
+    globalConfig: {},
+    // @ts-ignore
+    localConfig: {},
+    cliConfig: collectCliConfig({ argv }),
+  }));
+
   await new DetoxServer({
     port: +argv.port,
     standalone: true,
diff --git a/detox/package.json b/detox/package.json
index 7689d1490..7d1de186f 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "20.1.3",
+  "version": "20.1.5",
   "bin": {
     "detox": "local-cli/cli.js"
   },
@@ -199,4 +199,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/detox/src/android/core/NativeWaitFor.js b/detox/src/android/core/NativeWaitFor.js
index 7e4c7bcfa..6928e9b4d 100644
--- a/detox/src/android/core/NativeWaitFor.js
+++ b/detox/src/android/core/NativeWaitFor.js
@@ -65,6 +65,14 @@ class NativeWaitForElement extends NativeWaitFor {
   toNotHaveValue(value) {
     return this.not.toHaveValue(value);
   }
+
+  toBeFocused() {
+    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.FocusMatcher().not : new matchers.FocusMatcher());
+  }
+
+  toBeNotFocused() {
+    return this.not.toBeFocused();
+  }
 }
 
 module.exports = {
diff --git a/detox/src/devices/common/drivers/android/genycloud/exec/GenyCloudExec.js b/detox/src/devices/common/drivers/android/genycloud/exec/GenyCloudExec.js
index 07eb7ce9f..3948f7716 100644
--- a/detox/src/devices/common/drivers/android/genycloud/exec/GenyCloudExec.js
+++ b/detox/src/devices/common/drivers/android/genycloud/exec/GenyCloudExec.js
@@ -28,7 +28,7 @@ class GenyCloudExec {
   }
 
   startInstance(recipeUUID, instanceName) {
-    return this._exec(`instances start --stop-when-inactive --no-wait ${recipeUUID} "${instanceName}"`, { retries: 0 });
+    return this._exec(`instances start --no-wait ${recipeUUID} "${instanceName}"`, { retries: 0 });
   }
 
   adbConnect(instanceUUID) {
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index 8d4c8428e..3415b420b 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -545,6 +545,16 @@ class WaitFor {
     return this;
   }
 
+  toBeFocused() {
+    this.expectation = this.expectation.toBeFocused();
+    return this;
+  }
+
+  toBeNotFocused() {
+    this.expectation = this.expectation.toBeNotFocused();
+    return this;
+  }
+
   get not() {
     this.expectation.not;
     return this;
diff --git a/detox/src/utils/logger.js b/detox/src/utils/logger.js
index 6804ea131..6a75edcc3 100644
--- a/detox/src/utils/logger.js
+++ b/detox/src/utils/logger.js
@@ -1,4 +1,4 @@
 /**
- * @type {Detox.Logger}
+ * @type {import('../logger/DetoxLogger')}
  */
 module.exports = require('../../internals').log;
diff --git a/docs/api/actions.md b/docs/api/actions.md
index 61a2ef774..f0c83c633 100644
--- a/docs/api/actions.md
+++ b/docs/api/actions.md
@@ -233,7 +233,7 @@ Sets the date-picker’s date to the specified date and time.
 `dateString`—The date to set. Should match the format provided by `dateFormat`.<br/>
 `dateFormat`—The format of `dateString`. Should be either [`'ISO8601'`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString), or an explicit date representation format, as supported by [`NSDateFormatter`] on iOS / [`DateTimeFormatter`] on Android (e.g. `'yyyy/MM/dd'`).
 
->_The recommended `dateFormat` is `ISO8601`._
+> _The recommended `dateFormat` is `ISO8601`._
 
 Examples:
 
@@ -362,9 +362,15 @@ await element(by.id('PinchableScrollView')).pinchWithAngle('outward', 'slow', 0)
 ```
 
 [`testID`]: ../guide/test-id.mdx
+
 [`by.type`]: ../api/matchers.md#bytypeclassname
+
 [`Date.prototype.toISOString()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
+
 [`NSDateFormatter`]: https://developer.apple.com/documentation/foundation/dateformatter
+
 [`DateTimeFormatter`]: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html
+
 [`@react-native-community/datetimepicker`]: https://www.npmjs.com/package/@react-native-community/datetimepicker
+
 [PR datetimepicker#705]: https://github.com/react-native-datetimepicker/datetimepicker/pull/705
diff --git a/docs/guide/android-dev-env.md b/docs/guide/android-dev-env.md
index 4bbdd925c..545b26573 100644
--- a/docs/guide/android-dev-env.md
+++ b/docs/guide/android-dev-env.md
@@ -64,7 +64,7 @@ _<sup>\* Inspect the content of your `ANDROID_SDK_ROOT` and `ANDROID_HOME` envir
 
 ## Android (AOSP) Emulators
 
-Mobile-apps' automation needs an Android device to run on. If you haven’t already done so, you should  [set up an Emulator](https://developer.android.com/studio/run/emulator). But, wait - don’t go and install the default one: read through, first.
+Mobile-apps’ automation needs an Android device to run on. If you haven’t already done so, you should  [set up an Emulator](https://developer.android.com/studio/run/emulator). But, wait - don’t go and install the default one: read through, first.
 
 We’ve long proven that for automation - which requires a stable and deterministic environment, Google’s emulators running with Google APIs simply don’t deliver what’s needed. Be it the preinstalled Google play-services - which tend to take up a lot of CPU, or even Google’s `gboard` Keyboard - which is full-featured but overly bloated: These encourage flakiness in tests, which we are desperate to avoid in automation.
 
@@ -81,7 +81,7 @@ While it’s possible to do this using Android Studio, we’ll focus on the comm
 1. Locate your 'Android home' folder - typically set in the `ANDROID_HOME` environment variable, or in its predecessor - `ANDROID_SDK_ROOT`. If `ANDROID_HOME` isn’t set, either set it yourself or run the following commands after `cd`-ing into the home folder.
 
 1. Preliminary: Upgrade your `emulator` executable to the latest version.
-   _Note: It is OK if the emulator’s version is not aligned with the SDK or platform-tools' version you currently have installed (e.g. 30.x.x vs. SDK 29)_
+   _Note: It is OK if the emulator’s version is not aligned with the SDK or platform-tools’ version you currently have installed (e.g. 30.x.x vs. SDK 29)_
 
    ```sh
    $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install emulator
@@ -92,7 +92,7 @@ While it’s possible to do this using Android Studio, we’ll focus on the comm
    In previous Android Studio versions, the SDK-Manager path was located at `$ANDROID_HOME/tools/bin/sdkmanager`.
    We highly recommend working with the latest version, however, in case you are working with an old version, and this command fails, try this path instead.
 
-   The command might also fail if the command-line tools aren't installed. To install the tools using Android Studio, click `Tools > SDK Manager` and select the `SDK Tools` tab. Ensure that `Android SDK Command-line Tools (latest)` is checked and applied.
+   The command might also fail if the command-line tools aren’t installed. To install the tools using Android Studio, click `Tools > SDK Manager` and select the `SDK Tools` tab. Ensure that `Android SDK Command-line Tools (latest)` is checked and applied.
 
    :::
 
@@ -140,7 +140,7 @@ Also, be sure to upgrade your emulator executable to the latest version: If it i
 
 ![Upgrade emulator in AS](../img/android/upgrade-emulator-as.png)
 
-**Note:** _It is OK if the emulator’s version is not aligned with the SDK or platform-tools' version you currently have installed (e.g. 30.x.x vs. SDK 29)_
+**Note:** _It is OK if the emulator’s version is not aligned with the SDK or platform-tools’ version you currently have installed (e.g. 30.x.x vs. SDK 29)_
 
 ## Emulator Quick-Boot
 
@@ -218,7 +218,7 @@ The library part can be easily achieved as explained there (i.e. by using Gradle
 
 #### Solution 1: Prebaked Images
 
-If you have control over the emulators' snapshots, simply download (see test-butler’s guide) and install the test-butler APK once (e.g. use `adb install -r -t path/to/test-butler-app.apk`), and save an updated version of the snapshot. This is the best solution.
+If you have control over the emulators’ snapshots, simply download (see test-butler’s guide) and install the test-butler APK once (e.g. use `adb install -r -t path/to/test-butler-app.apk`), and save an updated version of the snapshot. This is the best solution.
 
 > Note: you will have to reiterate this if you upgrade to a newer version of Test-Butler, in the future.
 
diff --git a/docs/guide/cucumber-js-integration.md b/docs/guide/cucumber-js-integration.md
new file mode 100644
index 000000000..03a9e382f
--- /dev/null
+++ b/docs/guide/cucumber-js-integration.md
@@ -0,0 +1,93 @@
+# Cucumber.js Integration
+
+:::note Community resource
+
+This is an article generously contributed [by the community][initial PR].
+Feel free to [contribute to it][Contributing] by submitting a pull request
+if you find any errors or have suggestions for improvements.
+If you have any questions, please get in touch with the author directly.
+
+:::
+
+[Cucumber] is a tool for running automated tests written in plain language.
+It can be integrated with Detox to run end-to-end tests for mobile applications
+if you manage [Detox Internals API] from [Cucumber.js hooks].
+
+In your `init.js` or `init.ts` file, please import `detox/internals` on top of the file:
+
+```js
+import detox from 'detox/internals'
+```
+
+Also import the hooks we'll be using just a bit later:
+
+```js
+import { Before, BeforeAll, AfterAll, After,  ITestCaseHookParameter } from '@cucumber/cucumber'
+```
+
+Define the earliest [Cucumber.js] hook, `BeforeAll`, where you’ll [initialize][`detox.init()`] Detox and launch the app:
+
+```js
+BeforeAll({ timeout: 120 * 1000 }, async () => {
+    await detox.init()
+    await device.launchApp()
+})
+```
+
+You must also tell Detox explicitly when [Cucumber.js] starts a test via calling [`detox.onTestStart()`] inside `Before` hook.
+Otherwise, Detox won't be able to _start recording_ logs, screenshots, videos, and other artifacts:
+
+```js
+Before(async (message: ITestCaseHookParameter) => {
+    const { pickle } = message
+    await detox.onTestStart({
+        title: pickle.uri,
+        fullName: pickle.name,
+        status: 'running',
+    })
+})
+```
+
+The symmetrical step is to inform Detox that the current test is over via calling [`detox.onTestDone()`] inside `After` hook.
+Otherwise, Detox won't be able to _save_ logs, screenshots, videos, and other artifacts:
+
+```js
+After(async (message: ITestCaseHookParameter) => {
+    const { pickle, result } = message
+    await detox.onTestDone({
+        title: pickle.uri,
+        fullName: pickle.name,
+        status: result ? 'passed' : 'failed',
+    })
+})
+```
+
+After the tests are over, you should call [`detox.cleanup()`] inside `AfterAll` hook so that Detox can clean up all the resources it has allocated:
+
+```js
+AfterAll(async () => {
+    await detox.cleanup();
+})
+```
+
+For more information on [Cucumber.js] please check out this [documentation][Cucumber.js].
+
+[Cucumber]: https://cucumber.io/
+
+[Cucumber.js]: https://github.com/cucumber/cucumber-js#documentation
+
+[Cucumber.js hooks]: https://github.com/cucumber/cucumber-js/blob/main/docs/support_files/hooks.md
+
+[Initial PR]: https://github.com/wix/Detox/pull/3858
+
+[Detox Internals API]: ../api/internals.mdx
+
+[`detox.init()`]: ../api/internals.mdx#initoptions-promise
+
+[`detox.cleanup()`]: ../api/internals.mdx#cleanup-promise
+
+[`detox.onTestStart()`]: ../api/internals.mdx#onteststartevent-promise
+
+[`detox.onTestDone()`]: ../api/internals.mdx#ontestdoneevent-promise
+
+[Contributing]: ../contributing.md
diff --git a/docs/introduction/getting-started.mdx b/docs/introduction/getting-started.mdx
index 90e0a1ca7..7f8d90600 100644
--- a/docs/introduction/getting-started.mdx
+++ b/docs/introduction/getting-started.mdx
@@ -1,6 +1,7 @@
 import Tabs from '@theme/Tabs';
 import TabItem from '@theme/TabItem';
 
+import IntroRN from './partials/_getting-started-rn.md';
 import IntroExpo from './partials/_getting-started-expo.md';
 import IntroOther from './partials/_getting-started-other.mdx';
 
@@ -16,7 +17,9 @@ Bear with us - it is all necessary, and once set up, it is easy to move forward
 Please select type of your mobile application before you start the tutorial:
 
 <Tabs groupId="appType">
-    <TabItem value="start-rn" label="React Native" default />
+    <TabItem value="start-rn" label="React Native" default>
+        <IntroRN />
+    </TabItem>
     <TabItem value="start-expo" label="Expo">
         <IntroExpo />
     </TabItem>
diff --git a/docs/introduction/partials/_getting-started-rn.md b/docs/introduction/partials/_getting-started-rn.md
new file mode 100644
index 000000000..9a858fa4b
--- /dev/null
+++ b/docs/introduction/partials/_getting-started-rn.md
@@ -0,0 +1,16 @@
+<!-- markdownlint-configure-file { "first-line-h1": 0 } -->
+
+:::info Note
+
+Mind that Detox works, at least officially, with the following React Native versions:
+
+| iOS             | Android                                                                                                                |
+| --------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| 0.68.x - 0.70.7 | 0.68.x - 0.70.7 -<br/>Visibility edge-case: see this [RN issue](https://github.com/facebook/react-native/issues/23870) |
+
+Newer versions, as well as React Native's ["New Architecture"](https://reactnative.dev/docs/the-new-architecture/landing-page), are most likely supported, but have not been tested out yet.
+Please [submit an issue](https://github.com/wix/Detox/issues/new/choose) if you find specific problems with more recent React Native versions.
+
+For support for React Native 0.71.x, please track [this issue](https://github.com/wix/Detox/issues/3884).
+
+:::
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index cd2eb24fd..ce55a7701 100644
--- a/examples/demo-native-android/package.json
+++ b/examples/demo-native-android/package.json
@@ -1,13 +1,13 @@
 {
   "name": "detox-demo-native-android",
-  "version": "20.1.3",
+  "version": "20.1.5",
   "private": true,
   "scripts": {
     "packager": "react-native start",
     "detox-server": "detox run-server"
   },
   "devDependencies": {
-    "detox": "^20.1.3"
+    "detox": "^20.1.5"
   },
   "detox": {}
 }
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index 91b7a665b..1c00b8ca2 100644
--- a/examples/demo-native-ios/package.json
+++ b/examples/demo-native-ios/package.json
@@ -1,9 +1,9 @@
 {
   "name": "detox-demo-native-ios",
-  "version": "20.1.3",
+  "version": "20.1.5",
   "private": true,
   "devDependencies": {
-    "detox": "^20.1.3"
+    "detox": "^20.1.5"
   },
   "detox": {
     "specs": "",
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index e3886a989..4e4d32ceb 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -1,12 +1,12 @@
 {
   "name": "demo-plugin",
-  "version": "20.1.3",
+  "version": "20.1.5",
   "private": true,
   "scripts": {
     "test:plugin": "detox test --configuration plugin -l verbose"
   },
   "devDependencies": {
-    "detox": "^20.1.3",
+    "detox": "^20.1.5",
     "jest": "^28.0.0"
   },
   "detox": {
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index 5b4dd029c..e609225c1 100644
--- a/examples/demo-react-native-detox-instruments/package.json
+++ b/examples/demo-react-native-detox-instruments/package.json
@@ -1,10 +1,10 @@
 {
   "name": "demo-react-native-detox-instruments",
-  "version": "20.1.3",
+  "version": "20.1.5",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^20.1.3"
+    "detox": "^20.1.5"
   },
   "detox": {
     "configurations": {
diff --git a/examples/demo-react-native/ios/Podfile b/examples/demo-react-native/ios/Podfile
index a5e135cdf..471dbf0c0 100644
--- a/examples/demo-react-native/ios/Podfile
+++ b/examples/demo-react-native/ios/Podfile
@@ -13,7 +13,15 @@ target 'example' do
   )
 
   post_install do |installer|
-    react_native_post_install(installer)
+      if ENV["REACT_NATIVE_VERSION"] && ENV["REACT_NATIVE_VERSION"].match(/0.6[6,7,8,9].*/)
+        react_native_post_install(installer)
+      else
+        react_native_post_install(
+          installer,
+            # Set `mac_catalyst_enabled` to `true` in order to apply patches
+            # necessary for Mac Catalyst builds
+            :mac_catalyst_enabled => false
+        )
+      end
   end
 end
-
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index 99f0f669c..4206f2f26 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "20.1.3",
+  "version": "20.1.5",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -23,14 +23,14 @@
   },
   "dependencies": {
     "@react-native-async-storage/async-storage": "^1.17.3",
-    "react": "18.0.0",
-    "react-native": "0.69.7",
+    "react": "18.1.0",
+    "react-native": "0.70.7",
     "tslib": "^2.0.3"
   },
   "devDependencies": {
     "@types/fs-extra": "^9.0.13",
     "@types/jest": "^29.2.1",
-    "detox": "^20.1.3",
+    "detox": "^20.1.5",
     "fs-extra": "^9.1.0",
     "jest": "^29.2.1",
     "ts-jest": "^29.0.3",
diff --git a/lerna.json b/lerna.json
index 022a3ad59..eedbd16d3 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,7 +13,7 @@
     "generation",
     "."
   ],
-  "version": "20.1.3",
+  "version": "20.1.5",
   "npmClient": "npm",
   "command": {
     "publish": {
diff --git a/package.json b/package.json
index d6027b30a..67b49e48a 100644
--- a/package.json
+++ b/package.json
@@ -54,5 +54,5 @@
     "shell-utils": "1.x.x",
     "unified": "^10.1.0"
   },
-  "version": "20.1.3"
+  "version": "20.1.5"
 }
diff --git a/website/sidebars.js b/website/sidebars.js
index 97983621b..c933e01bd 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -42,6 +42,7 @@ const sidebars = {
         'guide/developing-while-writing-tests',
         'guide/android-dev-env',
         'guide/proguard-configuration',
+        'guide/cucumber-js-integration',
         'guide/uninstalling',
       ]
     },
diff --git a/website/src/css/_navbar.scss b/website/src/css/_navbar.scss
index 136202c7f..7fbcde140 100644
--- a/website/src/css/_navbar.scss
+++ b/website/src/css/_navbar.scss
@@ -22,3 +22,53 @@
   padding: 5px;
   font-weight: 300;
 }
+/*===========*/
+
+/* Restyle logo & hamburger button */
+
+.navbar {
+  height: 70px;
+
+  &__items {
+    &--right {
+      position: relative;
+    }
+  }
+
+  &__logo {
+    height: 26px;
+    margin: 0 10px 0 8px;
+  }
+
+  &__toggle {
+    width: 40px;
+    height: 40px;
+    background-color: var(--detox-color-a3);
+    border-radius: 10px;
+    justify-content: center;
+    align-items: center;
+  }
+}
+
+@media (max-width: 768px) {
+  .navbar__toggle {
+    position: absolute;
+    margin: 0;
+    top: 15px;
+    right: 25px;
+  }
+}
+
+/* Restyle search button */
+
+@media (max-width: 768px) {
+  button.DocSearch-Button {
+    background-color: var(--detox-color-a3);
+    height: 40px;
+    border-radius: 10px;
+    padding: 0 10px;
+  }
+  .DocSearch-Search-Icon {
+    opacity: 0.6;
+  }
+}
diff --git a/website/src/theme/Icon/Menu/index.js b/website/src/theme/Icon/Menu/index.js
new file mode 100644
index 000000000..abfd02495
--- /dev/null
+++ b/website/src/theme/Icon/Menu/index.js
@@ -0,0 +1,10 @@
+import React from 'react';
+export default function IconMenu({ width = 19, height = 12, className, ...restProps }) {
+  return (
+    <svg className={className} width={width} height={height} viewBox="0 0 19 12" fill="currentColor" aria-hidden="true" {...restProps}>
+      <rect x="0.5" y="5.5" width="18" height="1" rx="0.5" stroke="currentColor" strokeLinecap="round" />
+      <rect x="0.5" y="10.5" width="18" height="1" rx="0.5" stroke="currentColor" strokeLinecap="round" />
+      <rect x="0.5" y="0.5" width="18" height="1" rx="0.5" stroke="currentColor" strokeLinecap="round" />
+    </svg>
+  );
+}
diff --git a/website/src/theme/Navbar/Search/index.js b/website/src/theme/Navbar/Search/index.js
new file mode 100644
index 000000000..ff575981d
--- /dev/null
+++ b/website/src/theme/Navbar/Search/index.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import Search from '@theme-original/Navbar/Search';
+import styles from './styles.module.scss';
+
+export default function SearchWrapper(props) {
+  return (
+    <>
+      <Search className={styles.searchWrapper} {...props} />
+    </>
+  );
+}
diff --git a/website/src/theme/Navbar/Search/styles.module.scss b/website/src/theme/Navbar/Search/styles.module.scss
new file mode 100644
index 000000000..ce3009060
--- /dev/null
+++ b/website/src/theme/Navbar/Search/styles.module.scss
@@ -0,0 +1,5 @@
+@media (max-width: 768px) {
+  .searchWrapper {
+    right: 64px;
+  }
+}
diff --git a/website/versioned_docs/version-20.x/api/actions.md b/website/versioned_docs/version-20.x/api/actions.md
index 61a2ef774..f0c83c633 100644
--- a/website/versioned_docs/version-20.x/api/actions.md
+++ b/website/versioned_docs/version-20.x/api/actions.md
@@ -233,7 +233,7 @@ Sets the date-picker’s date to the specified date and time.
 `dateString`—The date to set. Should match the format provided by `dateFormat`.<br/>
 `dateFormat`—The format of `dateString`. Should be either [`'ISO8601'`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString), or an explicit date representation format, as supported by [`NSDateFormatter`] on iOS / [`DateTimeFormatter`] on Android (e.g. `'yyyy/MM/dd'`).
 
->_The recommended `dateFormat` is `ISO8601`._
+> _The recommended `dateFormat` is `ISO8601`._
 
 Examples:
 
@@ -362,9 +362,15 @@ await element(by.id('PinchableScrollView')).pinchWithAngle('outward', 'slow', 0)
 ```
 
 [`testID`]: ../guide/test-id.mdx
+
 [`by.type`]: ../api/matchers.md#bytypeclassname
+
 [`Date.prototype.toISOString()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
+
 [`NSDateFormatter`]: https://developer.apple.com/documentation/foundation/dateformatter
+
 [`DateTimeFormatter`]: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html
+
 [`@react-native-community/datetimepicker`]: https://www.npmjs.com/package/@react-native-community/datetimepicker
+
 [PR datetimepicker#705]: https://github.com/react-native-datetimepicker/datetimepicker/pull/705
diff --git a/website/versioned_docs/version-20.x/guide/android-dev-env.md b/website/versioned_docs/version-20.x/guide/android-dev-env.md
index 4bbdd925c..545b26573 100644
--- a/website/versioned_docs/version-20.x/guide/android-dev-env.md
+++ b/website/versioned_docs/version-20.x/guide/android-dev-env.md
@@ -64,7 +64,7 @@ _<sup>\* Inspect the content of your `ANDROID_SDK_ROOT` and `ANDROID_HOME` envir
 
 ## Android (AOSP) Emulators
 
-Mobile-apps' automation needs an Android device to run on. If you haven’t already done so, you should  [set up an Emulator](https://developer.android.com/studio/run/emulator). But, wait - don’t go and install the default one: read through, first.
+Mobile-apps’ automation needs an Android device to run on. If you haven’t already done so, you should  [set up an Emulator](https://developer.android.com/studio/run/emulator). But, wait - don’t go and install the default one: read through, first.
 
 We’ve long proven that for automation - which requires a stable and deterministic environment, Google’s emulators running with Google APIs simply don’t deliver what’s needed. Be it the preinstalled Google play-services - which tend to take up a lot of CPU, or even Google’s `gboard` Keyboard - which is full-featured but overly bloated: These encourage flakiness in tests, which we are desperate to avoid in automation.
 
@@ -81,7 +81,7 @@ While it’s possible to do this using Android Studio, we’ll focus on the comm
 1. Locate your 'Android home' folder - typically set in the `ANDROID_HOME` environment variable, or in its predecessor - `ANDROID_SDK_ROOT`. If `ANDROID_HOME` isn’t set, either set it yourself or run the following commands after `cd`-ing into the home folder.
 
 1. Preliminary: Upgrade your `emulator` executable to the latest version.
-   _Note: It is OK if the emulator’s version is not aligned with the SDK or platform-tools' version you currently have installed (e.g. 30.x.x vs. SDK 29)_
+   _Note: It is OK if the emulator’s version is not aligned with the SDK or platform-tools’ version you currently have installed (e.g. 30.x.x vs. SDK 29)_
 
    ```sh
    $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install emulator
@@ -92,7 +92,7 @@ While it’s possible to do this using Android Studio, we’ll focus on the comm
    In previous Android Studio versions, the SDK-Manager path was located at `$ANDROID_HOME/tools/bin/sdkmanager`.
    We highly recommend working with the latest version, however, in case you are working with an old version, and this command fails, try this path instead.
 
-   The command might also fail if the command-line tools aren't installed. To install the tools using Android Studio, click `Tools > SDK Manager` and select the `SDK Tools` tab. Ensure that `Android SDK Command-line Tools (latest)` is checked and applied.
+   The command might also fail if the command-line tools aren’t installed. To install the tools using Android Studio, click `Tools > SDK Manager` and select the `SDK Tools` tab. Ensure that `Android SDK Command-line Tools (latest)` is checked and applied.
 
    :::
 
@@ -140,7 +140,7 @@ Also, be sure to upgrade your emulator executable to the latest version: If it i
 
 ![Upgrade emulator in AS](../img/android/upgrade-emulator-as.png)
 
-**Note:** _It is OK if the emulator’s version is not aligned with the SDK or platform-tools' version you currently have installed (e.g. 30.x.x vs. SDK 29)_
+**Note:** _It is OK if the emulator’s version is not aligned with the SDK or platform-tools’ version you currently have installed (e.g. 30.x.x vs. SDK 29)_
 
 ## Emulator Quick-Boot
 
@@ -218,7 +218,7 @@ The library part can be easily achieved as explained there (i.e. by using Gradle
 
 #### Solution 1: Prebaked Images
 
-If you have control over the emulators' snapshots, simply download (see test-butler’s guide) and install the test-butler APK once (e.g. use `adb install -r -t path/to/test-butler-app.apk`), and save an updated version of the snapshot. This is the best solution.
+If you have control over the emulators’ snapshots, simply download (see test-butler’s guide) and install the test-butler APK once (e.g. use `adb install -r -t path/to/test-butler-app.apk`), and save an updated version of the snapshot. This is the best solution.
 
 > Note: you will have to reiterate this if you upgrade to a newer version of Test-Butler, in the future.
 
diff --git a/website/versioned_docs/version-20.x/introduction/getting-started.mdx b/website/versioned_docs/version-20.x/introduction/getting-started.mdx
index 90e0a1ca7..7f8d90600 100644
--- a/website/versioned_docs/version-20.x/introduction/getting-started.mdx
+++ b/website/versioned_docs/version-20.x/introduction/getting-started.mdx
@@ -1,6 +1,7 @@
 import Tabs from '@theme/Tabs';
 import TabItem from '@theme/TabItem';
 
+import IntroRN from './partials/_getting-started-rn.md';
 import IntroExpo from './partials/_getting-started-expo.md';
 import IntroOther from './partials/_getting-started-other.mdx';
 
@@ -16,7 +17,9 @@ Bear with us - it is all necessary, and once set up, it is easy to move forward
 Please select type of your mobile application before you start the tutorial:
 
 <Tabs groupId="appType">
-    <TabItem value="start-rn" label="React Native" default />
+    <TabItem value="start-rn" label="React Native" default>
+        <IntroRN />
+    </TabItem>
     <TabItem value="start-expo" label="Expo">
         <IntroExpo />
     </TabItem>
diff --git a/website/versioned_docs/version-20.x/introduction/partials/_getting-started-rn.md b/website/versioned_docs/version-20.x/introduction/partials/_getting-started-rn.md
new file mode 100644
index 000000000..9a858fa4b
--- /dev/null
+++ b/website/versioned_docs/version-20.x/introduction/partials/_getting-started-rn.md
@@ -0,0 +1,16 @@
+<!-- markdownlint-configure-file { "first-line-h1": 0 } -->
+
+:::info Note
+
+Mind that Detox works, at least officially, with the following React Native versions:
+
+| iOS             | Android                                                                                                                |
+| --------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| 0.68.x - 0.70.7 | 0.68.x - 0.70.7 -<br/>Visibility edge-case: see this [RN issue](https://github.com/facebook/react-native/issues/23870) |
+
+Newer versions, as well as React Native's ["New Architecture"](https://reactnative.dev/docs/the-new-architecture/landing-page), are most likely supported, but have not been tested out yet.
+Please [submit an issue](https://github.com/wix/Detox/issues/new/choose) if you find specific problems with more recent React Native versions.
+
+For support for React Native 0.71.x, please track [this issue](https://github.com/wix/Detox/issues/3884).
+
+:::


Test patch:
diff --git a/detox/runners/jest/testEnvironment/listeners/DetoxCoreListener.js b/detox/runners/jest/testEnvironment/listeners/DetoxCoreListener.js
index d2be075ec..50e1c1ad8 100644
--- a/detox/runners/jest/testEnvironment/listeners/DetoxCoreListener.js
+++ b/detox/runners/jest/testEnvironment/listeners/DetoxCoreListener.js
@@ -12,6 +12,7 @@ const log = detoxInternals.log.child({ cat: 'lifecycle,jest-environment' });
 class DetoxCoreListener {
   constructor({ env }) {
     this._startedTests = new Set();
+    this._skippedTests = new Set();
     this._testsFailedBeforeStart = new Set();
     this._env = env;
     this._circusRetryTimes = 1;
@@ -46,11 +47,7 @@ class DetoxCoreListener {
       this._testsFailedBeforeStart.add(test);
     }
 
-    const logTrace = this._isTestSkipped(test)
-      ? log.trace
-      : log.trace.begin;
-
-    logTrace({
+    log.trace.begin({
       context: 'test',
       status: metadata.status,
       fullName: metadata.fullName,
@@ -61,6 +58,16 @@ class DetoxCoreListener {
     this._circusRetryTimes = isNaN(circusRetryTimes) ? 1 : 1 + circusRetryTimes;
   }
 
+  async test_skip({ test }) {
+    log.trace.end({ status: 'skip' }, getFullTestName(test));
+    this._skippedTests.add(test);
+  }
+
+  async test_todo({ test }) {
+    log.trace.end({ status: 'todo' }, getFullTestName(test));
+    this._skippedTests.add(test);
+  }
+
   async hook_start(event, state) {
     await this._onBeforeActualTestStart(state.currentlyRunningTest);
     log.trace.begin({ functionCode: event.hook.fn.toString() }, event.hook.type);
@@ -122,7 +129,7 @@ class DetoxCoreListener {
   }
 
   _isTestSkipped(test) {
-    return test && (test.mode === 'skip' || test.mode === 'todo');
+    return test && this._skippedTests.has(test);
   }
 
   _getTestMetadata(test) {
@@ -140,16 +147,12 @@ class DetoxCoreListener {
     return result;
   }
 
-  /** @returns { 'failed' | 'passed' | 'running' | 'skip' | 'todo' } */
+  /** @returns { 'failed' | 'passed' | 'running'} */
   _getTestStatus(test) {
     if (!_.isEmpty(test.errors)) {
       return 'failed';
     }
 
-    if (this._isTestSkipped(test)) {
-      return test.mode;
-    }
-
     if (test.status === 'done') {
       return 'passed';
     } else {
diff --git a/detox/src/android/AndroidExpect.test.js b/detox/src/android/AndroidExpect.test.js
index 6252c4684..8b26a716c 100644
--- a/detox/src/android/AndroidExpect.test.js
+++ b/detox/src/android/AndroidExpect.test.js
@@ -165,6 +165,8 @@ describe('AndroidExpect', () => {
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50, 'down');
       await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50);
 
+      await e.waitFor(e.element(e.by.id('id'))).toBeFocused().withTimeout(0);
+      await e.waitFor(e.element(e.by.id('id'))).toBeNotFocused().withTimeout(0);
     });
 
     it(`waitFor (element) with wrong parameters should throw`, async () => {
diff --git a/detox/src/devices/common/drivers/android/genycloud/exec/GenyCloudExec.test.js b/detox/src/devices/common/drivers/android/genycloud/exec/GenyCloudExec.test.js
index 02ca38a6c..28d3689ff 100644
--- a/detox/src/devices/common/drivers/android/genycloud/exec/GenyCloudExec.test.js
+++ b/detox/src/devices/common/drivers/android/genycloud/exec/GenyCloudExec.test.js
@@ -69,7 +69,7 @@ describe('Genymotion-cloud executable', () => {
     {
       commandName: 'Start Instance',
       commandExecFn: () => uut.startInstance(recipeUUID, instanceName),
-      expectedExec: `"mock/path/to/gmsaas" --format compactjson instances start --stop-when-inactive --no-wait ${recipeUUID} "${instanceName}"`,
+      expectedExec: `"mock/path/to/gmsaas" --format compactjson instances start --no-wait ${recipeUUID} "${instanceName}"`,
       expectedExecOptions: { retries: 0 },
     },
     {
diff --git a/detox/src/ios/expectTwoApiCoverage.test.js b/detox/src/ios/expectTwoApiCoverage.test.js
index 4284d56da..f70e6a568 100644
--- a/detox/src/ios/expectTwoApiCoverage.test.js
+++ b/detox/src/ios/expectTwoApiCoverage.test.js
@@ -223,6 +223,8 @@ describe('expectTwo API Coverage', () => {
       await e.waitFor(e.element(e.by.id('id'))).toNotHaveLabel('value');
       await e.waitFor(e.element(e.by.id('id'))).toHaveValue('value');
       await e.waitFor(e.element(e.by.id('id'))).toNotHaveValue('value');
+      await e.waitFor(e.element(e.by.id('id'))).toBeFocused().withTimeout(0);
+      await e.waitFor(e.element(e.by.id('id'))).toBeNotFocused().withTimeout(0);
     });
 
     it(`waitFor (element) with wrong parameters should throw`, async () => {
diff --git a/detox/test/e2e/05.waitfor.test.js b/detox/test/e2e/05.waitfor.test.js
index 4aa095bd6..6efd4234a 100644
--- a/detox/test/e2e/05.waitfor.test.js
+++ b/detox/test/e2e/05.waitfor.test.js
@@ -1,49 +1,75 @@
 const custom = require('./utils/custom-it');
 const {expectToThrow} = require('./utils/custom-expects');
 
+const expectToFinishBeforeTimeout = async (block, timeout) => {
+  const startTime = new Date().getTime();
+  await block();
+  const endTime = new Date().getTime();
+
+  const expiredAfter = endTime - startTime;
+  if (expiredAfter > timeout) {
+    throw new Error(`Action not expired even after a timeout, took ${expiredAfter}ms`);
+  }
+}
+
 describe('WaitFor', () => {
+  const goButton = element(by.id('goButton'));
+  const timeout = 5000;
+
   beforeEach(async() => {
     await device.reloadReactNative();
     await element(by.text('WaitFor')).tap();
   });
 
-  it('should wait until an element is created and exists in layout', async () => {
-    await expect(element(by.id('createdAndVisibleText'))).not.toExist();
-    await element(by.id('GoButton')).tap();
-    await waitFor(element(by.id('createdAndVisibleText'))).toExist().withTimeout(20000);
+  it('should wait until an element is exists / removed in layout', async () => {
+    let testElement = element(by.id('changeExistenceByToggle'));
+    await expect(testElement).not.toExist();
+
+    await goButton.tap();
+
+    await expectToFinishBeforeTimeout(async () => {
+      await waitFor(testElement).toExist().withTimeout(timeout);
+    }, timeout);
+
+    await goButton.tap();
+
+    await expectToFinishBeforeTimeout(async () => {
+      await waitFor(testElement).not.toExist().withTimeout(timeout);
+    }, timeout);
   });
 
-  it('should wait until an element is removed', async () => {
-    const timeout = 20000;
+  it('should wait until an element is focused / unfocused', async () => {
+    let testElement = element(by.id('changeFocusByToggle'));
+    await expect(testElement).not.toBeFocused();
 
-    await expect(element(by.id('deletedFromHierarchyText'))).toBeVisible();
-    await element(by.id('GoButton')).tap();
+    await goButton.tap();
 
-    const startTime = new Date().getTime();
-    await waitFor(element(by.id('deletedFromHierarchyText'))).not.toBeVisible().withTimeout(timeout);
-    const endTime = new Date().getTime();
+    await expectToFinishBeforeTimeout(async () => {
+      await waitFor(testElement).toBeFocused().withTimeout(timeout);
+    }, timeout);
 
-    if (endTime - startTime > timeout) {
-      throw new Error(`Action not expired even after a timeout`);
-    }
+    await goButton.tap();
 
-    await expect(element(by.id('deletedFromHierarchyText'))).not.toBeVisible();
+    await expectToFinishBeforeTimeout(async () => {
+      await waitFor(testElement).not.toBeFocused().withTimeout(timeout);
+    }, timeout);
   });
 
   custom.it.withFailureIf.android('should find element by scrolling until it is visible', async () => {
     await expect(element(by.text('Text5'))).not.toBeVisible();
-    await element(by.id('GoButton')).tap();
+
+    await goButton.tap();
     await waitFor(element(by.text('Text5'))).toBeVisible().whileElement(by.id('ScrollView')).scroll(50, 'down');
     await expect(element(by.text('Text5'))).toBeVisible();
   });
 
   it('should fail test after waiting for element to exist but it doesn\'t at the end', async () => {
     await expect(element(by.id('neverAppearingText'))).not.toExist();
-    await expectToThrow(() => waitFor(element(by.id('neverAppearingText'))).toExist().withTimeout(1000));
+    await expectToThrow(() => waitFor(element(by.id('neverAppearingText'))).toExist().withTimeout(500));
   });
 
   it('should abort scrolling if element was not found', async () => {
-    await element(by.id('GoButton')).tap();
+    await goButton.tap();
     await expectToThrow(() => waitFor(element(by.text('Text1000'))).toBeVisible().whileElement(by.id('ScrollView')).scroll(50, 'down'));
     await expect(element(by.text('Text1000'))).not.toBeVisible();
   });
diff --git a/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap b/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
index 022d76c75..7e4498061 100644
--- a/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
+++ b/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Timeline integration test should deterministically produce a timeline artifact 1`] = `
+exports[`Timeline integration test Flaky test should deterministically produce a timeline artifact 1`] = `
 [
   {
     "args": {
@@ -815,3 +815,815 @@ Detox CLI is going to restart the test runner with those files...
   },
 ]
 `;
+
+exports[`Timeline integration test Focused test should deterministically produce a timeline artifact 1`] = `
+[
+  {
+    "args": {
+      "cwd": "$CWD",
+      "data": {},
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "detox test -c stub --config integration/e2e/config.js -a integration_artifacts/ -R 1 focused",
+    "ph": "B",
+    "pid": 0,
+    "tid": 0,
+    "ts": 0,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Serialized the session state at: $TMPDIR/$SESSION_ID.detox.json",
+    "ph": "i",
+    "pid": 0,
+    "tid": 0,
+    "ts": 1,
+  },
+  {
+    "args": {
+      "env": {},
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle,cli",
+    "name": "nyc jest --config integration/e2e/config.js focused",
+    "ph": "B",
+    "pid": 0,
+    "tid": 0,
+    "ts": 2,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "integration/e2e/focused.test.js",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 3,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "set up environment",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 4,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 5,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "focused.test.js is assigned to Stub #StubDevice#1",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 6,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "run the tests",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 7,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "Focused",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 8,
+  },
+  {
+    "args": {
+      "context": "test",
+      "fullName": "Focused Only test",
+      "invocations": 1,
+      "level": 10,
+      "status": "running",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "Only test",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 9,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Focused: Only test",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 10,
+  },
+  {
+    "args": {
+      "functionCode": "() => {
+    if (config.resetModules) {
+      runtime.resetModules();
+    }
+    if (config.clearMocks) {
+      runtime.clearAllMocks();
+    }
+    if (config.resetMocks) {
+      runtime.resetAllMocks();
+      if (
+        config.fakeTimers.enableGlobally &&
+        config.fakeTimers.legacyFakeTimers
+      ) {
+        // during setup, this cannot be null (and it's fine to explode if it is)
+        environment.fakeTimers.useFakeTimers();
+      }
+    }
+    if (config.restoreMocks) {
+      runtime.restoreAllMocks();
+    }
+  }",
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "beforeEach",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 11,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 12,
+  },
+  {
+    "args": {
+      "functionCode": "async () => {
+    // Checking that skipped tests are also traced
+  }",
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "test_fn",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 13,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 14,
+  },
+  {
+    "args": {
+      "level": 10,
+      "status": "passed",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 15,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Focused: Only test [OK]",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 16,
+  },
+  {
+    "args": {
+      "context": "test",
+      "fullName": "Focused Skipped test",
+      "invocations": 1,
+      "level": 10,
+      "status": "running",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "Skipped test",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 17,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Focused: Skipped test",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 18,
+  },
+  {
+    "args": {
+      "level": 10,
+      "status": "skip",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 19,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Focused: Skipped test [SKIPPED]",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 20,
+  },
+  {
+    "args": {
+      "functionCode": "async () => {
+    // Reproducing when hook_start is called after test_start
+  }",
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "afterAll",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 21,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 22,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 23,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 24,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "tear down environment",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 25,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 26,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 27,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,cli",
+    "ph": "E",
+    "pid": 0,
+    "tid": 0,
+    "ts": 28,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "ph": "E",
+    "pid": 0,
+    "tid": 0,
+    "ts": 29,
+  },
+]
+`;
+
+exports[`Timeline integration test Skipped test should deterministically produce a timeline artifact 1`] = `
+[
+  {
+    "args": {
+      "cwd": "$CWD",
+      "data": {},
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "detox test -c stub --config integration/e2e/config.js -a integration_artifacts/ -R 1 skipped",
+    "ph": "B",
+    "pid": 0,
+    "tid": 0,
+    "ts": 0,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Serialized the session state at: $TMPDIR/$SESSION_ID.detox.json",
+    "ph": "i",
+    "pid": 0,
+    "tid": 0,
+    "ts": 1,
+  },
+  {
+    "args": {
+      "env": {},
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle,cli",
+    "name": "nyc jest --config integration/e2e/config.js skipped",
+    "ph": "B",
+    "pid": 0,
+    "tid": 0,
+    "ts": 2,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "integration/e2e/passing-skipped.test.js",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 3,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "set up environment",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 4,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 5,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "passing-skipped.test.js is assigned to Stub #StubDevice#1",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 6,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "run the tests",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 7,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "Suite with skipped tests",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 8,
+  },
+  {
+    "args": {
+      "context": "test",
+      "fullName": "Suite with skipped tests Skipped test",
+      "invocations": 1,
+      "level": 10,
+      "status": "running",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "Skipped test",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 9,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Suite with skipped tests: Skipped test",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 10,
+  },
+  {
+    "args": {
+      "level": 10,
+      "status": "skip",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 11,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Suite with skipped tests: Skipped test [SKIPPED]",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 12,
+  },
+  {
+    "args": {
+      "context": "test",
+      "fullName": "Suite with skipped tests Regular test",
+      "invocations": 1,
+      "level": 10,
+      "status": "running",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "Regular test",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 13,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Suite with skipped tests: Regular test",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 14,
+  },
+  {
+    "args": {
+      "functionCode": "() => {
+    if (config.resetModules) {
+      runtime.resetModules();
+    }
+    if (config.clearMocks) {
+      runtime.clearAllMocks();
+    }
+    if (config.resetMocks) {
+      runtime.resetAllMocks();
+      if (
+        config.fakeTimers.enableGlobally &&
+        config.fakeTimers.legacyFakeTimers
+      ) {
+        // during setup, this cannot be null (and it's fine to explode if it is)
+        environment.fakeTimers.useFakeTimers();
+      }
+    }
+    if (config.restoreMocks) {
+      runtime.restoreAllMocks();
+    }
+  }",
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "beforeEach",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 15,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 16,
+  },
+  {
+    "args": {
+      "functionCode": "async () => {
+    // should be traced as usual
+  }",
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "test_fn",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 17,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 18,
+  },
+  {
+    "args": {
+      "level": 10,
+      "status": "passed",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 19,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Suite with skipped tests: Regular test [OK]",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 20,
+  },
+  {
+    "args": {
+      "context": "test",
+      "fullName": "Suite with skipped tests Check that todo tests are also traced",
+      "invocations": 1,
+      "level": 10,
+      "status": "running",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "Check that todo tests are also traced",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 21,
+  },
+  {
+    "args": {
+      "level": 30,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "name": "Suite with skipped tests: Check that todo tests are also traced",
+    "ph": "i",
+    "pid": 1,
+    "tid": 1,
+    "ts": 22,
+  },
+  {
+    "args": {
+      "level": 10,
+      "status": "todo",
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 23,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 24,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 25,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "name": "tear down environment",
+    "ph": "B",
+    "pid": 1,
+    "tid": 1,
+    "ts": 26,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 27,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,jest-environment",
+    "ph": "E",
+    "pid": 1,
+    "tid": 1,
+    "ts": 28,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "lifecycle,cli",
+    "ph": "E",
+    "pid": 0,
+    "tid": 0,
+    "ts": 29,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "lifecycle",
+    "ph": "E",
+    "pid": 0,
+    "tid": 0,
+    "ts": 30,
+  },
+]
+`;
+
diff --git a/detox/test/integration/e2e/focused.test.js b/detox/test/integration/e2e/focused.test.js
new file mode 100644
index 000000000..474ff1800
--- /dev/null
+++ b/detox/test/integration/e2e/focused.test.js
@@ -0,0 +1,13 @@
+describe('Focused', () => {
+  afterAll(async () => {
+    // Reproducing when hook_start is called after test_start
+  });
+
+  it.only('Only test', async () => {
+    // Checking that skipped tests are also traced
+  });
+
+  it('Skipped test', async () => {
+    // Causes issue in the previous implementation
+  });
+});
diff --git a/detox/test/integration/e2e/passing-sandbox.js b/detox/test/integration/e2e/passing-sandbox.test.js
similarity index 100%
rename from detox/test/integration/e2e/passing-sandbox.js
rename to detox/test/integration/e2e/passing-sandbox.test.js
diff --git a/detox/test/integration/e2e/passing-skipped.js b/detox/test/integration/e2e/passing-skipped.test.js
similarity index 56%
rename from detox/test/integration/e2e/passing-skipped.js
rename to detox/test/integration/e2e/passing-skipped.test.js
index 71b844cfb..5ac513a94 100644
--- a/detox/test/integration/e2e/passing-skipped.js
+++ b/detox/test/integration/e2e/passing-skipped.test.js
@@ -1,7 +1,11 @@
-describe('Skipped', () => {
+describe('Suite with skipped tests', () => {
   it.skip('Skipped test', async () => {
     // Checking that skipped tests are also traced
   });
 
+  it('Regular test', async () => {
+    // should be traced as usual
+  });
+
   it.todo('Check that todo tests are also traced');
 });
diff --git a/detox/test/integration/initialization-test.test.js b/detox/test/integration/initialization-test.test.js
index 0d53cf11e..6a8957448 100644
--- a/detox/test/integration/initialization-test.test.js
+++ b/detox/test/integration/initialization-test.test.js
@@ -4,11 +4,11 @@ describe('Initialization (context) tests', () => {
   test.each([
     `cross-env DETOX_CONFIGURATION=stub node integration/utils/simplistic-runner.js`,
 
-    `detox test -c stub --config integration/e2e/config.js --runInBand --retries 1 flaky passing`,
-    `cross-env DETOX_CONFIGURATION=stub jest --config integration/e2e/config.js --runInBand passing`,
+    `detox test -c stub --config integration/e2e/config.js --runInBand --retries 1 flaky passing-simple`,
+    `cross-env DETOX_CONFIGURATION=stub jest --config integration/e2e/config.js --runInBand passing-simple`,
 
-    `detox test -c stub --config integration/e2e/config.js --maxWorkers 2 --retries 1 flaky passing`,
-    `cross-env DETOX_CONFIGURATION=stub jest --config integration/e2e/config.js --maxWorkers 2 passing`,
+    `detox test -c stub --config integration/e2e/config.js --maxWorkers 2 --retries 1 flaky passing-simple`,
+    `cross-env DETOX_CONFIGURATION=stub jest --config integration/e2e/config.js --maxWorkers 2 passing-simple`,
   ])('should run: %s', async (cmd) => {
     const handle = execa.commandSync(cmd, { stdio: 'inherit', shell: true });
     expect(handle.exitCode).toBe(0);
diff --git a/detox/test/integration/timeline-artifact.test.js b/detox/test/integration/timeline-artifact.test.js
index 9ad8e72a3..bfcc9dbc0 100644
--- a/detox/test/integration/timeline-artifact.test.js
+++ b/detox/test/integration/timeline-artifact.test.js
@@ -15,52 +15,78 @@ describe('Timeline integration test', () => {
   const timelineArtifactFilename = 'detox.trace.json';
   const timelineArtifactPath = path.join(artifactsDirectory, timelineArtifactFilename);
   const clearAllArtifacts = () => remove(artifactsDirectory);
+  const readTac = async () => {
+    tac = JSON.parse(await readFile(timelineArtifactPath, 'utf8'));
+  };
 
   let tac;
 
-  beforeAll(clearAllArtifacts);
-
-  beforeAll(async () => {
-    await execCommand(`detox test -c stub --config integration/e2e/config.js -a ${artifactsDirectory} -R 1 flaky`);
-    tac = JSON.parse(await readFile(timelineArtifactPath, 'utf8'));
+  describe('Flaky test', () => {
+    beforeAll(clearAllArtifacts);
+    beforeAll(() => execCommand(`detox test -c stub --config integration/e2e/config.js -a ${artifactsDirectory} -R 1 flaky`));
+    beforeAll(readTac);
+
+    it('should deterministically produce a timeline artifact', verifyTimelineSnapshot);
+
+    it('should have balanced begin/end events', assertBalancedEvents);
+
+    it('should have a credible list of categories', async () => {
+      const cats = _.chain(tac)
+        .flatMap(e => e.cat ? `${e.cat}`.split(',', 1) : [])
+        .uniq()
+        .sort()
+        .value();
+
+      expect(cats).toEqual([
+        'artifacts-manager',
+        'device',
+        'ipc',
+        'lifecycle',
+        'user',
+        'ws-client',
+        'ws-server',
+      ]);
+    });
+
+    it('should have a credible process and thread ids', async () => {
+      const unique = _.mapValues({
+        pid: tac.map(e => e.pid),
+        tid: tac.map(e => e.tid),
+        pid_tid: tac.map(e => `${e.pid}:${e.tid}`),
+      }, v => _.uniq(v).sort());
+
+      expect(unique.pid.length).toBe(3);
+      expect(unique.tid.length).toBeLessThan(unique.pid_tid.length);
+      expect(unique.pid_tid.length).toBeGreaterThan(10);
+    });
   });
 
-  it('should deterministically produce a timeline artifact', async () => {
-    const sanitizeContext = { pid: new Map(), tid: new Map(), sessionId: '', cwd: '' };
-    expect(tac.filter(isLifecycleEvent).map(sanitizeEvent.bind(sanitizeContext))).toMatchSnapshot();
-  });
+  describe('Skipped test', () => {
+    beforeAll(clearAllArtifacts);
+    beforeAll(() => execCommand(`detox test -c stub --config integration/e2e/config.js -a ${artifactsDirectory} -R 1 skipped`));
+    beforeAll(readTac);
+
+    it('should deterministically produce a timeline artifact', verifyTimelineSnapshot);
 
-  it('should have a credible list of categories', async () => {
-    const cats = _.chain(tac)
-      .flatMap(e => e.cat ? `${e.cat}`.split(',', 1) : [])
-      .uniq()
-      .sort()
-      .value();
-
-    expect(cats).toEqual([
-      'artifacts-manager',
-      'device',
-      'ipc',
-      'lifecycle',
-      'user',
-      'ws-client',
-      'ws-server',
-    ]);
+    it('should have balanced begin/end events', assertBalancedEvents);
   });
 
-  it('should have a credible process and thread ids', async () => {
-    const unique = _.mapValues({
-      pid: tac.map(e => e.pid),
-      tid: tac.map(e => e.tid),
-      pid_tid: tac.map(e => `${e.pid}:${e.tid}`),
-    }, v => _.uniq(v).sort());
+  describe('Focused test', () => {
+    beforeAll(clearAllArtifacts);
+    beforeAll(() => execCommand(`detox test -c stub --config integration/e2e/config.js -a ${artifactsDirectory} -R 1 focused`));
+    beforeAll(readTac);
 
-    expect(unique.pid.length).toBe(3);
-    expect(unique.tid.length).toBeLessThan(unique.pid_tid.length);
-    expect(unique.pid_tid.length).toBeGreaterThan(10);
+    it('should deterministically produce a timeline artifact', verifyTimelineSnapshot);
+
+    it('should have balanced begin/end events', assertBalancedEvents);
   });
 
-  it('should have balanced begin/end events', () => {
+  function verifyTimelineSnapshot() {
+    const sanitizeContext = { pid: new Map(), tid: new Map(), sessionId: '', cwd: '' };
+    expect(tac.filter(isLifecycleEvent).map(sanitizeEvent.bind(sanitizeContext))).toMatchSnapshot();
+  }
+
+  function assertBalancedEvents() {
     for (const lane of _(tac).groupBy('tid').values()) {
       const beginCount = _.filter(lane, { ph: 'B' }).length;
       const endCount = _.filter(lane, { ph: 'E' }).length;
@@ -69,7 +95,7 @@ describe('Timeline integration test', () => {
         expect(`imbalanced begin (${beginCount}) vs end (${endCount}) events in thread ${tid} (category: ${cat})`).toBeNull();
       }
     }
-  });
+  }
 });
 
 function isLifecycleEvent(e) {
diff --git a/detox/test/ios/Podfile b/detox/test/ios/Podfile
index a593d3a08..e9e490196 100644
--- a/detox/test/ios/Podfile
+++ b/detox/test/ios/Podfile
@@ -4,8 +4,6 @@ require_relative '../node_modules/@react-native-community/cli-platform-ios/nativ
 platform :ios, '12.4'
 install! 'cocoapods', :deterministic_uuids => false
 
-production = ENV["PRODUCTION"] == "1"
-
 def shared_pods
   config = use_native_modules!
 
diff --git a/detox/test/package.json b/detox/test/package.json
index 825ffc232..4b48c5abd 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "20.1.3",
+  "version": "20.1.5",
   "private": true,
   "engines": {
     "node": ">=14.5.0"
@@ -55,7 +55,7 @@
     "@typescript-eslint/eslint-plugin": "^5.4.0",
     "@typescript-eslint/parser": "^5.4.0",
     "cross-env": "^7.0.3",
-    "detox": "^20.1.3",
+    "detox": "^20.1.5",
     "eslint": "^7.32.0",
     "eslint-plugin-unicorn": "^39.0.0",
     "execa": "^5.1.1",
@@ -82,4 +82,4 @@
   "detox": {
     "extends": "./e2e/detox.config"
   }
-}
\ No newline at end of file
+}
diff --git a/detox/test/src/Screens/WaitForScreen.js b/detox/test/src/Screens/WaitForScreen.js
index 62b961343..b87dad809 100644
--- a/detox/test/src/Screens/WaitForScreen.js
+++ b/detox/test/src/Screens/WaitForScreen.js
@@ -4,19 +4,32 @@ import {
   View,
   ScrollView,
   Animated,
-  TouchableOpacity
+  TouchableOpacity,
+  TextInput
 } from 'react-native';
 
 export default class WaitForScreen extends Component {
-
   constructor(props) {
     super(props);
+    this.textInputRef = React.createRef();
     this.state = {
-      clicked: false,
+      toggle: false,
       becomeVisibleLeft: new Animated.Value(-500)
     };
   }
 
+  componentDidUpdate(prevProps, prevState, _) {
+    if (prevState.toggle === this.state.toggle) {
+      return;
+    }
+
+    if (this.state.toggle) {
+      this.textInputRef.current.focus();
+    } else {
+      this.textInputRef.current.blur();
+    }
+  }
+
   render() {
     return (
       <View style={{flex: 1, paddingTop: 40, justifyContent: 'flex-start'}}>
@@ -34,16 +47,21 @@ export default class WaitForScreen extends Component {
           </ScrollView>
         </View>
 
-        {this.state.clicked ? false :
-         <Text testID='deletedFromHierarchyText' style={{marginBottom: 20, textAlign: 'center', color: 'red'}}>I am being removed 2 sec after click</Text>
+        {!this.state.toggle ? false :
+          <Text testID='changeExistenceByToggle' style={{marginBottom: 20, textAlign: 'center', color: 'red'}}>
+            I am being exist / removed 2 sec after click
+          </Text>
         }
 
-        {!this.state.clicked ? false :
-         <Text testID='createdAndVisibleText' style={{marginBottom: 20, textAlign: 'center'}}>I am being created 2 sec after click</Text>
-        }
+        <TextInput
+          testID='changeFocusByToggle'
+          style={{marginBottom: 20}}
+          value='I am focused / unfocused 2 sec after click'
+          ref={this.textInputRef}
+        />
 
         <TouchableOpacity onPress={this.onGoButtonPress.bind(this)}>
-          <Text testID='GoButton' style={{color: 'blue', textAlign: 'center'}}>Go</Text>
+          <Text testID='goButton' style={{color: 'blue', textAlign: 'center'}}>Go</Text>
         </TouchableOpacity>
 
       </View>
@@ -52,8 +70,10 @@ export default class WaitForScreen extends Component {
 
   onGoButtonPress() {
     setTimeout(() => {
+      let previousState = this.state.toggle;
+
       this.setState({
-        clicked: true
+        toggle: !previousState
       });
     }, 2000);
   }
diff --git a/docs/config/testRunner.mdx b/docs/config/testRunner.mdx
index 5de27bac9..ca7638d5f 100644
--- a/docs/config/testRunner.mdx
+++ b/docs/config/testRunner.mdx
@@ -192,7 +192,7 @@ In other words, if you’re implementing (or using) a custom integration with so
 
 #### `testRunner.jest.setupTimeout` \[number]
 
-Default: `300000` (5 minutes).
+Default: `120000` (2 minutes).
 
 As a part of the [environment setup](https://jestjs.io/docs/configuration/#testenvironment-string)), Detox boots the device and installs the apps.
 If that takes longer than the specified value, the entire test suite will be considered as failed, e.g.:
@@ -201,7 +201,7 @@ If that takes longer than the specified value, the entire test suite will be con
  FAIL  e2e/starter.test.js
   ● Test suite failed to run
 
-    Exceeded timeout of 300000ms while setting up Detox environment
+    Exceeded timeout of 120000ms while setting up Detox environment
 ```
 
 #### `testRunner.jest.teardownTimeout` \[number]
diff --git a/website/versioned_docs/version-20.x/config/testRunner.mdx b/website/versioned_docs/version-20.x/config/testRunner.mdx
index 5de27bac9..ca7638d5f 100644
--- a/website/versioned_docs/version-20.x/config/testRunner.mdx
+++ b/website/versioned_docs/version-20.x/config/testRunner.mdx
@@ -192,7 +192,7 @@ In other words, if you’re implementing (or using) a custom integration with so
 
 #### `testRunner.jest.setupTimeout` \[number]
 
-Default: `300000` (5 minutes).
+Default: `120000` (2 minutes).
 
 As a part of the [environment setup](https://jestjs.io/docs/configuration/#testenvironment-string)), Detox boots the device and installs the apps.
 If that takes longer than the specified value, the entire test suite will be considered as failed, e.g.:
@@ -201,7 +201,7 @@ If that takes longer than the specified value, the entire test suite will be con
  FAIL  e2e/starter.test.js
   ● Test suite failed to run
 
-    Exceeded timeout of 300000ms while setting up Detox environment
+    Exceeded timeout of 120000ms while setting up Detox environment
 ```
 
 #### `testRunner.jest.teardownTimeout` \[number]