Repository: wix/Detox
Pull request #3790 (2022-12-29 04:05:48)

feat(android): support setDatePickerDate
## Description
- This pull request addresses the issue described here: #3789 

In this pull request, I have extended the setDatePickerDate action to work on android as well.

setDatePickerDate is already working for android in this pr and I also updated one relevant test.
I have yet to investigate whether more tests need changes. 
I also have yet to update the docs.

Feel free to comment on how to improve this implementation.

> _For features/enhancements:_
 - [x] I have added/updated the relevant references in the [documentation](https://github.com/wix/Detox/tree/master/docs) files.

> _For API changes:_
 - [x] I have made the necessary changes in the [types index](https://github.com/wix/Detox/blob/master/detox/index.d.ts) file.


noomorph: Looks good to me, at least at the first glance!

There is some missing unit test coverage, but I hope it is trivial enough to add it.

Could you post here a screenshot of the updated DatePicker screen?

If you need some help or have question, let us know.
mauricedoepke: @noomorph 

I think my most recent PR should fix the unit test coverage and I also adjusted the docs to reflect setDatePickerDate working on android.

This is how the Android DatePicker screen looks like now. The ios one is unchanged.

<img src="https://user-images.githubusercontent.com/16628813/209962953-95f533d6-999f-4372-9f3d-45e2d26c3e27.png" height="800">
<img src="https://user-images.githubusercontent.com/16628813/209962956-072a542c-c990-499e-893c-94fa9e45c11c.png" height="800">

Patch:
diff --git a/.editorconfig b/.editorconfig
index c4298e306..e1e863d41 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -18,7 +18,7 @@ tab_width = 2
 indent_size = 4
 tab_width = 4
 
-[{*.cjs,*.js}]
+[{*.cjs,*.js,*.mjs}]
 indent_size = 2
 tab_width = 2
 
@@ -26,5 +26,5 @@ tab_width = 2
 indent_size = 2
 tab_width = 2
 
-[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}]
+[{*.css, *.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}]
 indent_size = 2
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT_ANDROID.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT_ANDROID.yml
index 1843e6bd9..6380e1ea6 100644
--- a/.github/ISSUE_TEMPLATE/BUG_REPORT_ANDROID.yml
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT_ANDROID.yml
@@ -45,6 +45,7 @@ body:
       description: "Note: the test runner is Jest by default, unless overridden via testRunner property in your detox configuration file (e.g. package.json, detox.config)."
       value: "Detox version:
       \nReact Native version:
+      \nHas Fabric (React Native's new rendering system) enabled: (yes/no)
       \nNode version:
       \nDevice model:
       \nAndroid version:
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT_IOS.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT_IOS.yml
index c38d7721a..9f6c29116 100644
--- a/.github/ISSUE_TEMPLATE/BUG_REPORT_IOS.yml
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT_IOS.yml
@@ -45,6 +45,7 @@ body:
       description: "Note: the test runner is Jest by default, unless overridden via testRunner property in your detox configuration file (e.g. package.json, detox.config)."
       value: "Detox version:
       \nReact Native version:
+      \nHas Fabric (React Native's new rendering system) enabled: (yes/no)
       \nNode version:
       \nDevice model:
       \niOS version:
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT_OTHER.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT_OTHER.yml
index 89e2d85f7..022a143d8 100644
--- a/.github/ISSUE_TEMPLATE/BUG_REPORT_OTHER.yml
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT_OTHER.yml
@@ -40,6 +40,7 @@ body:
       description: "Note: the test runner is Jest by default, unless overridden via testRunner property in your detox configuration file (e.g. package.json, detox.config)."
       value: "Detox version:
       \nReact Native version:
+      \nHas Fabric (React Native's new rendering system) enabled: (yes/no)
       \nNode version:
       \nTest-runner (select one): jest / other"
   - type: textarea
diff --git a/.github/workflows/looking-for-contributors.yml b/.github/workflows/looking-for-contributors.yml
new file mode 100644
index 000000000..a48d2fbb7
--- /dev/null
+++ b/.github/workflows/looking-for-contributors.yml
@@ -0,0 +1,31 @@
+name: Looking for Contributors
+on:
+    issues:
+        types:
+            - labeled
+jobs:
+    add-comment:
+        if: contains(github.event.label.name, 'looking for contributors')
+        runs-on: ubuntu-latest
+        permissions:
+            issues: write
+        steps:
+            - name: Add comment
+              uses: peter-evans/create-or-update-comment@v2
+              with:
+                  issue-number: ${{ github.event.issue.number }}
+                  body: |
+                      Hello! We appreciate you bringing this issue to our attention.
+                      It looks like this could be a valuable addition or fix to our project.
+                      We believe that this feature would benefit greatly from contributions from the community,
+                      even from first-time contributors.
+
+                      If you're interested in contributing to this feature,
+                      please take a look at our [contribution guide](https://wix.github.io/Detox/docs/next/contributing).
+                      It has all the information you need to know about how to submit a pull request and contribute to our project.
+
+                      You're also welcome to join our [Discord server](https://discord.gg/CkD5QKheF5)
+                      and discuss this feature with the collaborators or other contributors under the channel 'contributions'.
+
+                      Please feel free to reach out to us if you have any questions, or need help with anything.
+                      We appreciate your feedback and look forward to working with you!
diff --git a/.github/workflows/reproduction-required.yml b/.github/workflows/reproduction-required.yml
new file mode 100644
index 000000000..0d70bf469
--- /dev/null
+++ b/.github/workflows/reproduction-required.yml
@@ -0,0 +1,27 @@
+name: Reproduction Required
+on:
+    issues:
+        types:
+            - labeled
+jobs:
+    add-comment:
+        if: contains(github.event.label.name, 'reproduction required')
+        runs-on: ubuntu-latest
+        permissions:
+            issues: write
+        steps:
+            - name: Add comment
+              uses: peter-evans/create-or-update-comment@v2
+              with:
+                  issue-number: ${{ github.event.issue.number }}
+                  body: |
+                      Hello! We appreciate you bringing this issue to our attention.
+
+                      To assist our collaborators or contributors in identifying and resolving the problem,
+                      it would be helpful if you could provide a minimal reproduction of the issue.
+
+                      We recommend using our React Native template,
+                      which can be found at https://github.com/wix-incubator/detoxtemplate,
+                      but any minimal reproduction project would be appreciated.
+
+                      Your help is greatly appreciated. Thank you for your support!
diff --git a/.github/workflows/support.yml b/.github/workflows/support.yml
index 3e21d45c1..0f61c1c55 100644
--- a/.github/workflows/support.yml
+++ b/.github/workflows/support.yml
@@ -20,7 +20,7 @@ jobs:
             In order to get help, please either ask a question on Stack Overflow with the [`detox`](https://stackoverflow.com/questions/tagged/detox) tag,
             or simply join our [Discord](https://discord.com/invite/CkD5QKheF5).
 
-            **Feel free to post your Stack Overflow question here for more visibility!** We'll take a look at it ASAP.
+            **Feel free to post your Stack Overflow question here for more visibility!**
 
             For more information about our policy on issues, refer to [this discussion](https://github.com/wix/Detox/issues/1305).
           close-issue: true
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml
index c1d194779..13df06fc4 100644
--- a/.github/workflows/website.yml
+++ b/.github/workflows/website.yml
@@ -3,8 +3,16 @@ name: Website
 on:
   pull_request:
     branches: [master]
+    paths:
+      - '.github/workflows/website.yml'
+      - 'docs/**'
+      - 'website/**'
   push:
     branches: [master]
+    paths:
+      - '.github/workflows/website.yml'
+      - 'docs/**'
+      - 'website/**'
 
 jobs:
   checks:
@@ -15,11 +23,23 @@ jobs:
       - uses: actions/setup-node@v1
         with:
           node-version: '16.x'
+      - uses: dorny/paths-filter@v2
+        id: changes
+        with:
+          filters: |
+            website:
+              - 'website/**'
       - name: Test Build
+        working-directory: ./website
         run: |
-          cd website/
           npm i
           npm run build
+      - name: Run Percy Snapshot
+        working-directory: ./website
+        if: steps.changes.outputs.website == 'true'
+        run: npm run percy
+        env:
+          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
   gh-release:
     if: github.event_name != 'pull_request'
     permissions:
@@ -37,10 +57,28 @@ jobs:
         env:
           USE_SSH: true
           GIT_USER: git
+          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
         run: |
           git config --global user.email "mobile1@wix.com"
           git config --global user.name "mobilewix"
           cd website/
           npm i
           npm run build
+          npm run percy
           npx docusaurus deploy
+  performance:
+    needs: gh-release
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - run: mkdir -p ${{ github.workspace }}/tmp/artifacts
+      - name: Lighthouse
+        uses: foo-software/lighthouse-check-action@master
+        with:
+          urls: 'https://wix.github.io/Detox/,https://wix.github.io/Detox/docs/next/demo/'
+          outputDirectory: ${{ github.workspace }}/tmp/artifacts
+      - name: Upload artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          name: Lighthouse reports
+          path: ${{ github.workspace }}/tmp/artifacts
diff --git a/detox/android/detox/build.gradle b/detox/android/detox/build.gradle
index 9185a26a0..11ade3552 100644
--- a/detox/android/detox/build.gradle
+++ b/detox/android/detox/build.gradle
@@ -95,6 +95,10 @@ dependencies {
     api('androidx.test.espresso:espresso-web:3.4.0') {
         because 'Web-View testing'
     }
+    api('androidx.test.espresso:espresso-contrib:3.4.0') {
+        because 'Android datepicker support'
+        exclude group: "org.checkerframework", module: "checker"
+    }
     api('androidx.test:rules:1.4.0') {
         because 'of ActivityTestRule. Needed by users *and* internally used by Detox.'
     }
diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java
index ab19f2704..fbf9b704e 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java
+++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java
@@ -1,6 +1,7 @@
 package com.wix.detox.espresso;
 
 import android.view.View;
+import android.os.Build;
 
 import com.wix.detox.common.DetoxErrors.DetoxRuntimeException;
 import com.wix.detox.common.DetoxErrors.StaleActionException;
@@ -19,6 +20,11 @@ import com.wix.detox.espresso.scroll.ScrollHelper;
 import com.wix.detox.espresso.scroll.SwipeHelper;
 
 import org.hamcrest.Matcher;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.ZonedDateTime;
+import java.util.Calendar;
+import java.util.Date;
 
 import androidx.test.espresso.UiController;
 import androidx.test.espresso.ViewAction;
@@ -26,10 +32,12 @@ import androidx.test.espresso.action.CoordinatesProvider;
 import androidx.test.espresso.action.GeneralClickAction;
 import androidx.test.espresso.action.GeneralLocation;
 import androidx.test.espresso.action.Press;
+import androidx.test.espresso.contrib.PickerActions;
 
 import static androidx.test.espresso.action.ViewActions.actionWithAssertions;
 import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+
 import static org.hamcrest.Matchers.allOf;
 
 
@@ -39,6 +47,8 @@ import static org.hamcrest.Matchers.allOf;
 
 public class DetoxAction {
     private static final String LOG_TAG = "detox";
+    private static final String ISO8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
+    private static final String ISO8601_FORMAT_NO_TZ = "yyyy-MM-dd'T'HH:mm:ss";
 
     private DetoxAction() {
         // static class
@@ -149,6 +159,19 @@ public class DetoxAction {
         return new ScrollToIndexAction(index);
     }
 
+    public static ViewAction setDatePickerDate(String dateString, String formatString) throws ParseException {
+        Date date;
+        if (formatString.equals("ISO8601")) {
+            date = parseDateISO8601(dateString);
+        } else {
+            date = new SimpleDateFormat(formatString).parse(dateString);
+        }
+
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        return PickerActions.setDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH));
+    }
+
     public static ViewAction adjustSliderToPosition(final double newPosition) {
         return new AdjustSliderToPositionAction(newPosition);
     }
@@ -179,4 +202,12 @@ public class DetoxAction {
             }
         };
     }
+
+    private static Date parseDateISO8601(String dateString) throws ParseException {
+        try {
+            return new SimpleDateFormat(ISO8601_FORMAT).parse(dateString);
+        } catch (ParseException e) {
+            return new SimpleDateFormat(ISO8601_FORMAT_NO_TZ).parse(dateString);
+        }
+    }
 }
diff --git a/detox/index.d.ts b/detox/index.d.ts
index 874ebe3f8..28f0cecba 100644
--- a/detox/index.d.ts
+++ b/detox/index.d.ts
@@ -1365,12 +1365,15 @@ declare global {
             setColumnToValue(column: number, value: string): Promise<void>;
 
             /**
-             * Sets the date of a date picker to a date generated from the provided string and date format. (iOS only)
-             * @param dateString string representing a date in the supplied `dateFormat`
-             * @param dateFormat format for the `dateString` supplied
+             * Sets the date of a date-picker according to the specified date-string and format.
+             * @param dateString Textual representation of a date (e.g. '2023/01/01'). Should be in coherence with the format specified by `dateFormat`.
+             * @param dateFormat Format of `dateString`: Generally either 'ISO8601' or an explicitly specified format (e.g. 'yyyy/MM/dd'); It should
+             *      follow the rules of NSDateFormatter for iOS and DateTimeFormatter for Android.
+             * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
              * @example
-             * await expect(element(by.id('datePicker'))).toBeVisible();
-             * await element(by.id('datePicker')).setDatePickerDate('2019-02-06T05:10:00-08:00', "yyyy-MM-dd'T'HH:mm:ssZZZZZ");
+             * await element(by.id('datePicker')).setDatePickerDate('2023-01-01T00:00:00Z', 'ISO8601');
+             * await element(by.id('datePicker')).setDatePickerDate(new Date().toISOString(), 'ISO8601');
+             * await element(by.id('datePicker')).setDatePickerDate('2023/01/01', 'yyyy/MM/dd');
              */
             setDatePickerDate(dateString: string, dateFormat: string): Promise<void>;
 
diff --git a/detox/local-cli/cli.js b/detox/local-cli/cli.js
index 52d648be2..7c6f3d56b 100755
--- a/detox/local-cli/cli.js
+++ b/detox/local-cli/cli.js
@@ -7,6 +7,8 @@ const yargs = require('yargs');
 const logger = require('../internals').log.child({ cat: 'cli' });
 const DetoxError = require('../src/errors/DetoxError');
 
+const { isErrorAlreadyLogged } = require('./utils/cliErrorHandling');
+
 yargs
   .scriptName('detox')
   .parserConfiguration({
@@ -28,9 +30,8 @@ yargs
   .wrap(yargs.terminalWidth() * 0.9)
   .fail(function(msg, err, program) {
     if (err) {
-      const message = DetoxError.format(err);
-      if (message) {
-        logger.error(message);
+      if (!isErrorAlreadyLogged(err)) {
+        logger.error(DetoxError.format(err));
         process.stderr.write('\n');
       }
 
diff --git a/detox/local-cli/utils/cliErrorHandling.js b/detox/local-cli/utils/cliErrorHandling.js
new file mode 100644
index 000000000..99d32804b
--- /dev/null
+++ b/detox/local-cli/utils/cliErrorHandling.js
@@ -0,0 +1,15 @@
+const loggedErrors = new WeakSet();
+
+function markErrorAsLogged(error) {
+  loggedErrors.add(error);
+  return error;
+}
+
+function isErrorAlreadyLogged(error) {
+  return loggedErrors.has(error);
+}
+
+module.exports = {
+  markErrorAsLogged,
+  isErrorAlreadyLogged,
+};
diff --git a/detox/package.json b/detox/package.json
index 1fb2e19d8..3b1994019 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.1",
+  "version": "20.1.2",
   "bin": {
     "detox": "local-cli/cli.js"
   },
@@ -69,7 +69,7 @@
     "glob": "^8.0.3",
     "ini": "^1.3.4",
     "json-cycle": "^1.3.0",
-    "lodash": "^4.17.5",
+    "lodash": "^4.17.11",
     "multi-sort-stream": "^1.0.3",
     "multipipe": "^4.0.0",
     "node-ipc": "^9.2.1",
diff --git a/detox/src/android/actions/native.js b/detox/src/android/actions/native.js
index d0596f955..25de514e5 100644
--- a/detox/src/android/actions/native.js
+++ b/detox/src/android/actions/native.js
@@ -124,6 +124,13 @@ class ScrollToIndex extends Action {
   }
 }
 
+class SetDatePickerDateAction extends Action {
+  constructor(dateString, formatString) {
+    super();
+    this._call = invoke.callDirectly(DetoxActionApi.setDatePickerDate(dateString, formatString));
+  }
+}
+
 class AdjustSliderToPosition extends Action {
   constructor(newPosition) {
     super();
@@ -155,5 +162,6 @@ module.exports = {
   SwipeAction,
   TakeElementScreenshot,
   ScrollToIndex,
+  SetDatePickerDateAction,
   AdjustSliderToPosition,
 };
diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js
index aa75bcfb8..404946451 100644
--- a/detox/src/android/core/NativeElement.js
+++ b/detox/src/android/core/NativeElement.js
@@ -5,6 +5,7 @@ const tempfile = require('tempfile');
 
 const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 const invoke = require('../../invoke');
+const { removeMilliseconds } = require('../../utils/dateUtils');
 const { actionDescription } = require('../../utils/invocationTraceDescriptions');
 const actions = require('../actions/native');
 const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
@@ -112,6 +113,16 @@ class NativeElement {
     return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
   }
 
+  async setDatePickerDate(rawDateString, formatString) {
+    const dateString = formatString === 'ISO8601'
+      ? removeMilliseconds(rawDateString)
+      : rawDateString;
+
+    const action = new actions.SetDatePickerDateAction(dateString, formatString);
+    const traceDescription = actionDescription.setDatePickerDate(dateString, formatString);
+    return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
+  }
+
   /**
    * @param {'up' | 'right' | 'down' | 'left'} direction
    * @param {'slow' | 'fast'} [speed]
diff --git a/detox/src/android/espressoapi/DetoxAction.js b/detox/src/android/espressoapi/DetoxAction.js
index 580215d5e..3628b193b 100644
--- a/detox/src/android/espressoapi/DetoxAction.js
+++ b/detox/src/android/espressoapi/DetoxAction.js
@@ -194,6 +194,19 @@ class DetoxAction {
     };
   }
 
+  static setDatePickerDate(dateString, formatString) {
+    if (typeof dateString !== "string") throw new Error("dateString should be a string, but got " + (dateString + (" (" + (typeof dateString + ")"))));
+    if (typeof formatString !== "string") throw new Error("formatString should be a string, but got " + (formatString + (" (" + (typeof formatString + ")"))));
+    return {
+      target: {
+        type: "Class",
+        value: "com.wix.detox.espresso.DetoxAction"
+      },
+      method: "setDatePickerDate",
+      args: [dateString, formatString]
+    };
+  }
+
   static adjustSliderToPosition(newPosition) {
     if (typeof newPosition !== "number") throw new Error("newPosition should be a number, but got " + (newPosition + (" (" + (typeof newPosition + ")"))));
     return {
@@ -220,6 +233,18 @@ class DetoxAction {
     };
   }
 
+  static parseDateISO8601(dateString) {
+    if (typeof dateString !== "string") throw new Error("dateString should be a string, but got " + (dateString + (" (" + (typeof dateString + ")"))));
+    return {
+      target: {
+        type: "Class",
+        value: "com.wix.detox.espresso.DetoxAction"
+      },
+      method: "parseDateISO8601",
+      args: [dateString]
+    };
+  }
+
 }
 
 module.exports = DetoxAction;
\ No newline at end of file
diff --git a/detox/src/artifacts/utils/temporaryPath.js b/detox/src/artifacts/utils/temporaryPath.js
index 7225f925b..c55f0a435 100644
--- a/detox/src/artifacts/utils/temporaryPath.js
+++ b/detox/src/artifacts/utils/temporaryPath.js
@@ -2,16 +2,14 @@ const path = require('path');
 const { promisify } = require('util');
 
 const glob = require('glob');
+const _ = require('lodash');
 const tempfile = require('tempfile');
 
 const { useForwardSlashes } = require('../../utils/shellUtils');
 
 const globSync = glob.sync;
 const globAsync = promisify(glob);
-
-function getRoot() {
-  return path.dirname(tempfile());
-}
+const getRoot = _.once(() => path.dirname(tempfile()));
 
 function createGlobber(ext) {
   const fullExt = `.detox.${ext}`;
diff --git a/detox/src/configuration/composeLoggerConfig.js b/detox/src/configuration/composeLoggerConfig.js
index 0c7c19314..52738d9c1 100644
--- a/detox/src/configuration/composeLoggerConfig.js
+++ b/detox/src/configuration/composeLoggerConfig.js
@@ -35,7 +35,7 @@ function composeLoggerConfig(opts) {
         options: typeof options === 'function' ? options(acc) : options
       });
     },
-    items.reduce((a, b) => _.merge(a, _.omit(b, 'options')))
+    items.reduce((a, b) => _.merge(a, _.omit(b, 'options')), {})
   );
 }
 
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index c5467f3da..8d4c8428e 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -7,6 +7,7 @@ const _ = require('lodash');
 const tempfile = require('tempfile');
 
 const { assertEnum, assertNormalized } = require('../utils/assertArgument');
+const { removeMilliseconds } = require('../utils/dateUtils');
 const { actionDescription, expectDescription } = require('../utils/invocationTraceDescriptions');
 const log = require('../utils/logger').child({ cat: 'ws-client, ws' });
 const traceInvocationCall = require('../utils/traceInvocationCall').bind(null, log);
@@ -275,6 +276,9 @@ class Element {
   setDatePickerDate(dateString, dateFormat) {
     if (typeof dateString !== 'string') throw new Error('dateString should be a string, but got ' + (dateString + (' (' + (typeof dateString + ')'))));
     if (typeof dateFormat !== 'string') throw new Error('dateFormat should be a string, but got ' + (dateFormat + (' (' + (typeof dateFormat + ')'))));
+    if (dateFormat === 'ISO8601') {
+      dateString = removeMilliseconds(dateString);
+    }
 
     const traceDescription = actionDescription.setDatePickerDate(dateString, dateFormat);
     return this.withAction('setDatePickerDate', traceDescription, dateString, dateFormat);
diff --git a/detox/src/logger/DetoxLogger.js b/detox/src/logger/DetoxLogger.js
index 512a51bf9..042343461 100644
--- a/detox/src/logger/DetoxLogger.js
+++ b/detox/src/logger/DetoxLogger.js
@@ -4,7 +4,7 @@ const _ = require('lodash');
 
 const { DetoxInternalError, DetoxError } = require('../errors');
 const { shortFormat } = require('../utils/dateUtils');
-const isPromise = require('../utils/isPromise');
+const { isPromiseLike } = require('../utils/isPromise');
 
 const BunyanLogger = require('./utils/BunyanLogger');
 const CategoryThreadDispatcher = require('./utils/CategoryThreadDispatcher');
@@ -263,7 +263,7 @@ class DetoxLogger {
         ? action()
         : action;
 
-      if (!isPromise(result)) {
+      if (!isPromiseLike(result)) {
         end({ success: true });
       } else {
         result.then(
diff --git a/detox/src/logger/utils/BunyanLogger.js b/detox/src/logger/utils/BunyanLogger.js
index e02770a51..162e0c612 100644
--- a/detox/src/logger/utils/BunyanLogger.js
+++ b/detox/src/logger/utils/BunyanLogger.js
@@ -1,4 +1,5 @@
 const { PassThrough } = require('stream');
+const { promisify } = require('util');
 
 const bunyan = require('bunyan');
 const bds = require('bunyan-debug-stream');
@@ -74,12 +75,14 @@ class BunyanLogger {
   }
 
   async closeFileStreams() {
-    const { _closeStream } = BunyanLogger;
     const internalBunyanStreams = this._bunyan['streams'];
     const openFileStreams = _.filter(internalBunyanStreams, this._isOpenFileStream);
     _.remove(internalBunyanStreams, openFileStreams);
 
-    await Promise.all(openFileStreams.map(_closeStream));
+    await Promise.all(openFileStreams.map(bunyanStream => {
+      const stream = bunyanStream.stream;
+      return promisify(stream.end.bind(stream))();
+    }));
   }
 
   /** @private */
@@ -97,20 +100,6 @@ class BunyanLogger {
         return stream.fd > 2 && !stream.closed;
     }
   };
-
-  /** @private */
-  static _closeStream(bunyanStream) {
-    return new Promise((resolve, reject) => {
-      bunyanStream.stream.end((err) => {
-        /* istanbul ignore next */
-        if (err) {
-          reject(err);
-        } else {
-          resolve();
-        }
-      });
-    });
-  }
 }
 
 module.exports = BunyanLogger;
diff --git a/detox/src/logger/utils/streams/DetoxJSONLParser.js b/detox/src/logger/utils/streams/DetoxJSONLParser.js
index a6e82346a..d94743237 100644
--- a/detox/src/logger/utils/streams/DetoxJSONLParser.js
+++ b/detox/src/logger/utils/streams/DetoxJSONLParser.js
@@ -14,7 +14,7 @@ class DetoxJSONLParser {
       .on('error', (err) => {
         /* istanbul ignore else */
         if (err instanceof SyntaxError) {
-          log.debug({ err });
+          log.debug({ err }, 'Failed to parse log line:');
           readable.end();
         } else {
           readable.emit('error', err);
diff --git a/detox/src/utils/dateUtils.js b/detox/src/utils/dateUtils.js
index cb0a527cc..5588307be 100644
--- a/detox/src/utils/dateUtils.js
+++ b/detox/src/utils/dateUtils.js
@@ -10,6 +10,11 @@ function shortFormat(date) {
   return `${HH}:${MM}:${ss}.${milli}`;
 }
 
+function removeMilliseconds(isoDate) {
+  return isoDate.replace(/(T\d\d:\d\d:\d\d)(\.\d\d\d)/, '$1');
+}
+
 module.exports = {
   shortFormat,
+  removeMilliseconds,
 };
diff --git a/detox/src/utils/isPromise.js b/detox/src/utils/isPromise.js
index 711b95e69..f52ee33b6 100644
--- a/detox/src/utils/isPromise.js
+++ b/detox/src/utils/isPromise.js
@@ -2,4 +2,11 @@ function isPromise(value) {
   return Promise.resolve(value) === value;
 }
 
-module.exports = isPromise;
\ No newline at end of file
+function isPromiseLike(value) {
+  return value ? typeof value.then === 'function' : false;
+}
+
+module.exports = {
+  isPromise,
+  isPromiseLike,
+};
diff --git a/docs/api/actions.md b/docs/api/actions.md
index fdb144771..61a2ef774 100644
--- a/docs/api/actions.md
+++ b/docs/api/actions.md
@@ -22,7 +22,7 @@ Use [expectations](expect.md) to verify element states.
 - [`.tapReturnKey()`](#tapreturnkey)
 - [`.tapBackspaceKey()`](#tapbackspacekey)
 - [`.setColumnToValue()`](#setcolumntovaluecolumn-value--ios-only) **iOS only**
-- [`.setDatePickerDate()`](#setdatepickerdatedatestring-dateformat--ios-only) **iOS only**
+- [`.setDatePickerDate()`](#setdatepickerdatedatestring-dateformat)
 - [`.adjustSliderToPosition()`](#adjustslidertopositionnormalizedposition)
 - [`.getAttributes()`](#getattributes)
 - [`.takeScreenshot(name)`](#takescreenshotname)
@@ -214,7 +214,7 @@ Sets the element’s specified column to the specified value, using the system
 
 Values accepted by this method are strings only, and the system will do its best to match complex picker view cells to the string.
 
-This function does not support date pickers. Use [`.setDatePickerDate()`](#setdatepickerdatedatestring-dateformat--ios-only) instead.
+This function does not support date pickers. Use [`.setDatePickerDate()`](#setdatepickerdatedatestring-dateformat) instead.
 
 `column`—the element’s column to set (valid input: number, 0 and above) <br/>
 `value`—the string value to set (valid input: string)
@@ -226,20 +226,40 @@ await element(by.id('pickerView')).setColumnToValue(2, "Hello World");
 
 > **Note:** When working with date pickers, you should always set an explicit locale when launching your app in order to prevent flakiness from different date and time styles. See [here](device.md#9-languageandlocalelaunch-with-a-specific-language-andor-local-ios-only) for more information.
 
-### `setDatePickerDate(dateString, dateFormat)`  iOS only
+### `setDatePickerDate(dateString, dateFormat)`
 
-Sets the element’s date to the specified date string, parsed using the specified date format.
+Sets the date-picker’s date to the specified date and time.
 
-The specified date string is converted by the system to an [`NSDate`](https://developer.apple.com/documentation/foundation/nsdate) object, using [`NSDateFormatter`](https://developer.apple.com/documentation/foundation/dateformatter) with the specified date format, or [`NSISO8601DateFormatter`](https://developer.apple.com/documentation/foundation/iso8601dateformatter) in case of ISO 8601 date strings. If you use JavaScript’s [Date.toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) or otherwise provide a valid ISO 8601 date string, set the date format to `"ISO8601"`, which is supported as a special case.
+`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'`).
 
-`dateString`—the date to set (valid input: valid, parsable date string) <br/>
-`dateFormat`—the date format of `dateString` (valid input: `"ISO8601"` or a valid, parsable date format supported by [`NSDateFormatter`](https://developer.apple.com/documentation/foundation/dateformatter))
+>_The recommended `dateFormat` is `ISO8601`._
+
+Examples:
+
+```js
+const datePicker = element(by.id('datePicker'));
+
+// ISO8601:
+await datePicker.setDatePickerDate('2019-02-06T05:10:00-08:00', 'ISO8601');
+await datePicker.setDatePickerDate(new Date().toISOString(), 'ISO8601'); // toISOString returns an ISO8601 format with no timezone (UTC-0)
+
+// Explicit format:
+await datePicker.setDatePickerDate('2019/02/06', "yyyy/MM/dd");
+```
+
+:::info
+
+As far as element-matching is concerned, on Android, older versions of the popular [`@react-native-community/datetimepicker`] package don’t allow for the specification of your own [`testID`] prop for the date-picker component. Therefore, you'd have to either upgrade your package  to a newer version containing [PR datetimepicker#705] inside, or use Detox's [`by.type`] matcher as a workaround. For example:
 
 ```js
-await element(by.id('datePicker')).setDatePickerDate('2019-02-06T05:10:00-08:00', "ISO8601");
-await element(by.id('datePicker')).setDatePickerDate('2019/02/06', "yyyy/MM/dd");
+const datePicker = device.getPlatform() === 'android'
+  ? element(by.type('android.widget.DatePicker'))
+  : element(by.id('datePicker'));
 ```
 
+:::
+
 ### `adjustSliderToPosition(normalizedPosition)`
 
 Manipulates the UI to change the displayed value of the slider element to a new value, based on a normalized position.
@@ -340,3 +360,11 @@ Simulates a pinch on the element with the provided options.
 ```js
 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/contributing.md b/docs/contributing.md
index 469cf5324..42b12edaf 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -190,3 +190,8 @@ To update a specific version with the latest changes:
 
 1. Remove the version from `versions.json`.
 1. Run `npm run docusaurus docs:version <version>`.
+
+##### Inspect Documentation Style Changes
+
+This [demo page](demo.mdx) serves as a visual representation of the styling and formatting of our documentation.
+If you are changing the stylesheets, check with it to prevent undesirable visual regressions.
diff --git a/docs/demo.mdx b/docs/demo.mdx
new file mode 100644
index 000000000..f9ea82c9b
--- /dev/null
+++ b/docs/demo.mdx
@@ -0,0 +1,258 @@
+---
+title: Internal Demo Page for Styling
+---
+
+import CodeBlock from '@theme/CodeBlock';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+<head>
+  <meta name="robots" content="noindex, nofollow" />
+</head>
+
+:::note
+
+This page is for internal use only. If you are a contributor to the Detox project, you can use this page to test your changes to the website documentation pages
+styling. If you are not a contributor, you can ignore this page. Follow this link to the [Detox Home Page](/).
+
+:::
+
+## Headings section
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+### Third level heading
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+### `id` \[string \| number\]
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+#### Fourth level heading
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+#### `id` \[string \| number\]
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+##### Fifth level heading
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+##### `id` \[string \| number\]
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+###### Sixth level heading
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+###### `id` \[string \| number\]
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+## Paragraphs section
+
+Dummy double-spaced paragraph example, consectetur adipiscing elit. Curabitur euismod, arcu eu commodo tincidunt, est eros auctor augue, eget bibendum nibh tellus a mi.<br/>Integer blandit, enim quis malesuada fringilla, elit augue pellentesque justo, eget euismod elit sapien id magna. Sed molestie ultrices metus, a congue erat.
+
+This is a regular text, an _italic text_, a **bold text**, a ***both italic and bold text***, a ~~strikethrough text~~, an `inline code text`, a [simple link](#), and a [`link with inline code`](#).
+
+```plain text
+Here is an example of a code block with plain text.
+// highlight-next-line
+And this is a highlighted line.
+```
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur euismod, arcu eu commodo tincidunt, est eros auctor augue, eget bibendum nibh tellus a mi. Integer blandit, enim quis malesuada fringilla, elit augue pellentesque justo, eget euismod elit sapien id magna. Sed molestie ultrices metus, a congue erat.
+
+![This is a placeholder image.](https://via.placeholder.com/728x450.png?text=Isquix+ConsecteturAdipiscing.ips+Siquis+lorem)
+
+- Lorem ipsum dolor sit amet, consectetur adipiscing elit:
+  - `dummy.config.js` – dummy config file;
+  - `dummy.test.js` – dummy test file;
+- Donec in dui eget metus aliquet facilisis ac eget tortor.
+- Donec scelerisque ex lacinia, commodo eros eget, pretium leo.
+
+## Table section
+
+A simple table:
+
+| Dummy Params | Details of Dummy Text Data                                                                                                                                                                                                          |
+| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `type`       | **Required.** String Literal. Dummy property to discern device types: dummy.simulator, dummy.emulator, dummy.attached, dummy.genydummy etc.                                                                                         |
+| device       | **Required.** Object. Device query, e.g. `{ "byType": "dummy_phone" }` for dummy simulator, `{ "avdName": "dummy_avd" }` for dummy emulator or `{ "adbName": "<pattern>" }` for attached dummy device with name matching the regex. |
+| `bootArgs`   | _Optional. String. Supported by dummy.simulator and dummy.emulator only_ <br/> Supply an extra String of arguments to dummy_simctl boot ... or emulator -verbose ... @AVD_Name.                                                     |
+
+A workaround for displaying images side by side:
+
+| ![Left image](https://via.placeholder.com/728x450.png?text=Left+image) | ![Right image](https://via.placeholder.com/728x450.png?text=Right+image) |
+| ---------------------------------------------------------------------- | ------------------------------------------------------------------------ |
+
+## Admonitions section
+
+:::caution
+
+Rhoncus magna eget, pellentesque auguerhoncus magna eget, pellentesque augue **Android** due to DUMMY issues:
+
+- [rhoncus magna eget, pellentesque auguerhoncus magna eget](#)
+- [Curabitur euismod, arcu eu commodo tincidunt, est eros auctor augue](#)
+- [pellentesque auguerhoncus magna eget](#)
+
+:::
+
+:::danger DANGER
+
+This is a DANGER admonition SAMPLE with a custom background color, and a border color.
+
+:::
+
+:::info
+
+If your app Dummy uses [DUMMYPods] (all modern React DUMMY projects do since `0.999.0`), make sure
+to run `DUMMY pod install` in your `ios/` folder before building.
+
+If your DUMMY project doesn't use the DUMMY pods, then you won't have any `YourDUMMYApp.xdummyworkkspace` in your iOS DUMMY Project.
+You should DUMMY search instead for something like `YourDUmmyApp.dummy` there, and see your DUMMY build command accordingly:
+
+```diff title="apps.diff"
+   apps: {
+     'dummy.debug': {
+       type: 'dummy.app',
+-      binaryPath: 'dummy/build/Products/${conf}-dummyplatform/YOUR_APP.dummy',
++      binaryPath: 'dummy/build/Products/${conf}-dummyplatform/YOUR_APP.dummy',
+-      build: 'dummy_command -workspace dummy/YOUR_APP.dummyworkspace -scheme YOUR_APP -configuration ${conf} -sdk  dummyplatform -derivedDataPath dummy/build'
++      build: 'dummy_command -workspace dummy/example.dummyworkspace -scheme example -configuration ${conf} -sdk  dummyplatform -derivedDataPath dummy/build'
+```
+
+:::
+
+:::note NOTE
+
+This is a NOTE admonition SAMPLE with a custom background color, and a border color.
+
+:::
+
+:::tip
+
+Lorem ipsum `dolor sit amet`, (Sed porttitor purus ac risus bibendum), PELLENTESQUES back to
+[Fusce tempor nisl euismod](#) rhoncus magna eget, pellentesque auguerhoncus magna eget,
+pellentesque augue **Fusce tempor nisl euismod**, rhoncus pellentesques magna tempus augue mollis.
+
+Donec in nulla `AUCTOR_SED_MALESUADA` (or `AMALESUADA_MASSA`) eros posuere
+Curabitur elementum mi massa, non molestie ipsum velit vel:
+
+<Tabs groupId="whateverId">
+  <TabItem value="tab1" label="Tab 1">
+    <CodeBlock language="bash">$ANDACISUS_SED_EGET/rhoncus/tempus -augue-mollis</CodeBlock>
+  </TabItem>
+  <TabItem value="tab2" label="Tab 2">
+    <CodeBlock language="bash">$ANDACISUS_SED_EGET/rhoncus/tempus -augue-mollis</CodeBlock>
+  </TabItem>
+</Tabs>
+
+:::
+
+## Syntax highlighting section
+
+### npm
+
+```bash npm2yarn
+npm install "dummy_package" --save
+```
+
+### Shell
+
+```shell
+// highlight-start
+# Check the exit status of the last command
+if [ $? -eq 0 ]; then
+    echo "dummy_command succeeded"
+// highlight-end
+else
+    echo "dummy_command failed"
+fi
+```
+
+### JSX
+
+```jsx title="src/DummyExample.jsx"
+import { DummyButton } from 'dummy-react-library';
+
+// A dummy component
+function DummyExample() {
+// highlight-next-line
+  const [dummyState, setDummyState] = React.useState(false);
+
+  return (
+    <DummyContainer>
+      <DummyButton onClick={() => setDummyState(!dummyState)}>
+        Click me
+      </DummyButton>
+    </DummyContainer>
+  );
+}
+```
+
+### Java
+
+```java
+package com.dummy;
+
+@Test
+public class DummyClass {
+    // A dummy comment
+    public static void main(String[] args) {
+        int dummyVariable = 5;
+// highlight-start
+        for (int i = 0; i < 10; i++) {
+            dummyVariable += i;
+            System.out.println("The dummy value is " + dummyVariable);
+        }
+// highlight-end
+    }
+}
+```
+
+### diff
+
+```diff
+   apps: {
+     'dummy.debug': {
+// highlight-start
+       type: 'dummy.app',
+-      binaryPath: 'dummy/build/Products/${conf}-dummyplatform/YOUR_APP.dummy',
++      binaryPath: 'dummy/build/Products/${conf}-dummyplatform/YOUR_APP.dummy',
+// highlight-end
+-      build: 'dummy_command -workspace dummy/YOUR_APP.dummyworkspace -scheme YOUR_APP -configuration ${conf} -sdk  dummyplatform -derivedDataPath dummy/build'
++      build: 'dummy_command -workspace dummy/example.dummyworkspace -scheme example -configuration ${conf} -sdk  dummyplatform -derivedDataPath dummy/build'
+```
+
+### JSON
+
+```json
+{
+  "items": {
+    "object1": {
+      "property1": "dummy_type",
+      "property2": "dummy_device",
+// highlight-next-line
+      "property3": ["dummy_path/to/dummy_file.dummy"]
+    }
+  }
+}
+```
+
+### YAML
+
+```yaml
+actions:
+  - dummy install example
+    - push_dummytext: "*"
+
+  - dummy install example --save global
+  # Lorem ipsum dolor sit amet, consectetur SampleCo is using ($DUMMY/lorem-text)
+```
+
+Thanks for checking with this visual regression suite!
diff --git a/docs/guide/android-dev-env.md b/docs/guide/android-dev-env.md
index f2fbee15f..4bbdd925c 100644
--- a/docs/guide/android-dev-env.md
+++ b/docs/guide/android-dev-env.md
@@ -78,7 +78,7 @@ _Here’s a visual comparison between the two - an SDK 28 (Android 9) AOSP emula
 
 While it’s possible to do this using Android Studio, we’ll focus on the command line, as it is also good for _headless_ CI machines.
 
-1. Locate your 'Android home' folder - typically set in the `ANDROID_HOME` environment variable, or in its successor - `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. 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)_
@@ -92,6 +92,8 @@ 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.
+
    :::
 
 1. Install an emulator image without Google APIs:
@@ -104,6 +106,7 @@ While it’s possible to do this using Android Studio, we’ll focus on the comm
    > - With `;android-28;`, we assumed SDK 28 here, but other APIs are supported just the same.
    > - The `;default;` part replaces `;google_apis;`, which is the default, and is what matters here.
    > - If you are running on a M1 you must install an arm64 architecture i.e. `system-images;android-28;default;arm64-v8a`
+   > - To get a list of available images, run `$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --list`
 
 1. Create an emulator (i.e. AVD - Android Virtual Device):
 
diff --git a/docs/guide/mocking-user-notifications.md b/docs/guide/mocking-user-notifications.md
index bc2374f00..41a4f6abb 100644
--- a/docs/guide/mocking-user-notifications.md
+++ b/docs/guide/mocking-user-notifications.md
@@ -63,7 +63,7 @@ User notifications are passed as JSON objects to Detox. The JSON object needs to
 | `subtitle`          | No                        | String     | iOS           | A secondary description of the reason for the alert.                                                                                                                                     |
 | `body`              | No                        | String     | iOS           | The body of the notification.                                                                                                                                                            |
 | `badge`             | No                        | Integer    | iOS           | The number to display as the app’s icon badge.                                                                                                                                           |
-| `payload`           | iOS: No<br />Android: Yes | Object     | iOS & Android | An object of custom information associated with the notification.<br />Android: see [full description below](#Payload)                                                                   |
+| `payload`           | iOS: No<br />Android: Yes | Object     | iOS & Android | An object of custom information associated with the notification.<br />Android: see [full description below](#payload)                                                                   |
 | `category`          | No                        | String     | iOS           | The identifier of the app-defined category object.                                                                                                                                       |
 | `content-available` | No                        | Integer    | iOS           | Include this key with a value of 1 to configure a silent notification.                                                                                                                   |
 | `user-text`         | No                        | String     | iOS           | The text response provided by the user.                                                                                                                                                  |
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index 9e29c9ee7..77c3a0832 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.1",
+  "version": "20.1.2",
   "private": true,
   "scripts": {
     "packager": "react-native start",
     "detox-server": "detox run-server"
   },
   "devDependencies": {
-    "detox": "^20.1.1"
+    "detox": "^20.1.2"
   },
   "detox": {}
 }
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index 3fbdc151b..f6b71bb73 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.1",
+  "version": "20.1.2",
   "private": true,
   "devDependencies": {
-    "detox": "^20.1.1"
+    "detox": "^20.1.2"
   },
   "detox": {
     "specs": "",
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index eabb7e9e0..396103c3c 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -1,12 +1,12 @@
 {
   "name": "demo-plugin",
-  "version": "20.1.1",
+  "version": "20.1.2",
   "private": true,
   "scripts": {
     "test:plugin": "detox test --configuration plugin -l verbose"
   },
   "devDependencies": {
-    "detox": "^20.1.1",
+    "detox": "^20.1.2",
     "jest": "^28.0.0"
   },
   "detox": {
diff --git a/examples/demo-pure-native-android/app/build.gradle b/examples/demo-pure-native-android/app/build.gradle
index 19564c2f1..0bb5f0e2b 100644
--- a/examples/demo-pure-native-android/app/build.gradle
+++ b/examples/demo-pure-native-android/app/build.gradle
@@ -45,7 +45,6 @@ dependencies {
     implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
     testImplementation 'junit:junit:4.13.2'
     androidTestImplementation 'com.wix:detox:999.999.999'
-    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
     androidTestImplementation 'com.google.truth:truth:1.1.3'
 }
 
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index 016cee5fb..0ac6e9686 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.1",
+  "version": "20.1.2",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^20.1.1"
+    "detox": "^20.1.2"
   },
   "detox": {
     "configurations": {
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index 413dc9ef3..d65a9fd3f 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "20.1.1",
+  "version": "20.1.2",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -30,7 +30,7 @@
   "devDependencies": {
     "@types/fs-extra": "^9.0.13",
     "@types/jest": "^29.2.1",
-    "detox": "^20.1.1",
+    "detox": "^20.1.2",
     "fs-extra": "^9.1.0",
     "jest": "^29.2.1",
     "ts-jest": "^29.0.3",
diff --git a/lerna.json b/lerna.json
index 010282ad0..fe90ea9a1 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,7 +13,7 @@
     "generation",
     "."
   ],
-  "version": "20.1.1",
+  "version": "20.1.2",
   "npmClient": "npm",
   "command": {
     "publish": {
diff --git a/package.json b/package.json
index 7cf36ee3c..d0d6d0f48 100644
--- a/package.json
+++ b/package.json
@@ -54,5 +54,5 @@
     "shell-utils": "1.x.x",
     "unified": "^10.1.0"
   },
-  "version": "20.1.1"
+  "version": "20.1.2"
 }
diff --git a/website/README.md b/website/README.md
new file mode 100644
index 000000000..fe33cb3be
--- /dev/null
+++ b/website/README.md
@@ -0,0 +1,13 @@
+# Detox Website
+
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/66fcbcad/detox-website)
+
+Explore further about using Detox from our new **[website](https://wix.github.io/Detox/)**.
+This project is tested with [Browserstack](https://browserstack.com).
+
+[![Website Preview](https://user-images.githubusercontent.com/1962469/214340717-7ac3114b-8f22-4e94-960a-47dc21b6a88b.png)](https://wix.github.io/Detox)
+
+## Contributing
+
+See our [Contributing Guide](https://wix.github.io/Detox/docs/contributing#detox-documentation-website).
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index 9667b502d..a02a87776 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -15,6 +15,7 @@ const config = {
   favicon: '/img/favicon.ico',
   organizationName: 'wix',
   projectName: 'Detox',
+  plugins: ['docusaurus-plugin-sass'],
 
   presets: [
     [
@@ -53,7 +54,7 @@ const config = {
           remarkPlugins: [[require('@docusaurus/remark-plugin-npm2yarn'), { sync: true }]]
         },
         theme: {
-          customCss: require.resolve('./src/css/custom.css')
+          customCss: require.resolve('./src/css/custom.scss')
         }
       })
     ]
@@ -84,7 +85,7 @@ const config = {
           {
             to: 'blog',
             label: 'Blog',
-            position: 'left',
+            position: 'left'
           },
           {
             to: 'showcase',
diff --git a/website/package.json b/website/package.json
index f1efea809..3635395c4 100644
--- a/website/package.json
+++ b/website/package.json
@@ -3,6 +3,7 @@
   "version": "0.0.0",
   "private": true,
   "scripts": {
+    "percy": "zx percy.mjs",
     "docusaurus": "docusaurus",
     "start": "docusaurus start",
     "build": "docusaurus build",
@@ -19,14 +20,19 @@
     "@docusaurus/remark-plugin-npm2yarn": "^2.2.0",
     "@docusaurus/theme-search-algolia": "^2.2.0",
     "@mdx-js/react": "^1.6.21",
+    "@percy/cli": "^1.17.0",
     "@svgr/webpack": "^5.5.0",
     "clsx": "^1.1.1",
+    "docusaurus-plugin-sass": "^0.2.3",
     "file-loader": "^6.2.0",
     "prism-react-renderer": "^1.2.1",
     "react": "^17.0.1",
     "react-dom": "^17.0.1",
     "react-loader-spinner": "^5.3.4",
-    "url-loader": "^4.1.1"
+    "sass": "^1.57.1",
+    "sass-loader": "^13.2.0",
+    "url-loader": "^4.1.1",
+    "zx": "^7.1.1"
   },
   "browserslist": {
     "production": [
diff --git a/website/percy.mjs b/website/percy.mjs
new file mode 100644
index 000000000..b610d1564
--- /dev/null
+++ b/website/percy.mjs
@@ -0,0 +1,78 @@
+#!/usr/bin/env zx
+
+import 'zx/globals';
+
+class Server {
+  constructor() {
+    this.childPromise = null;
+  }
+
+  async isDown() {
+    try {
+      await fetch('http://localhost:3000', { method: 'HEAD' });
+      return false;
+    } catch (e) {
+      if (e.code === 'ECONNREFUSED') {
+        return true;
+      }
+
+      throw e;
+    }
+  }
+
+  async isUp() {
+    return !(await this.isDown());
+  }
+
+  async start() {
+    if (await this.isUp()) {
+      return;
+    }
+
+    this.childPromise = $`docusaurus serve --no-open`;
+
+    await new Promise((resolve, reject) => {
+      const handle = setTimeout(() => {
+        reject(new Error('Server timed out'));
+      }, 20000).unref();
+
+      this.childPromise.stdout.on('data', (data) => {
+        if (`${data}`.startsWith('[SUCCESS] Serving')) {
+          clearTimeout(handle);
+          resolve();
+        }
+      });
+    });
+
+    if (await this.isDown()) {
+      throw new Error('Server is not available after start')
+    }
+  }
+
+  async stop() {
+    if (!this.childPromise) {
+      return;
+    }
+
+    if (typeof this.childPromise.exitCode === 'number') {
+      return;
+    }
+
+    this.childPromise.catch((e) => {
+      if (e.signal !== 'SIGTERM') {
+        throw e;
+      }
+    });
+
+    await this.childPromise.kill('SIGTERM');
+  }
+}
+
+const server = new Server();
+try {
+  await server.start();
+  await $`percy snapshot snapshots.yml`
+} finally {
+  await server.stop();
+}
+
diff --git a/website/snapshots.yml b/website/snapshots.yml
new file mode 100644
index 000000000..16556fb36
--- /dev/null
+++ b/website/snapshots.yml
@@ -0,0 +1,32 @@
+- name: Home page dark theme
+  url: http://localhost:3000/Detox/
+  execute: document.documentElement.setAttribute("data-theme", "dark")
+
+- name: Home page light theme
+  url: http://localhost:3000/Detox/
+  execute: document.documentElement.setAttribute("data-theme", "light")
+
+- name: Demo page dark theme
+  url: http://localhost:3000/Detox/docs/next/demo
+  execute: document.documentElement.setAttribute("data-theme", "dark")
+
+- name: Demo page light theme
+  url: http://localhost:3000/Detox/docs/next/demo
+  execute: document.documentElement.setAttribute("data-theme", "light")
+
+- name: Blog page dark theme
+  url: http://localhost:3000/Detox/blog
+  execute: document.documentElement.setAttribute("data-theme", "dark")
+
+- name: Blog page light theme
+  url: http://localhost:3000/Detox/blog
+  execute: document.documentElement.setAttribute("data-theme", "light")
+
+- name: Showcase page dark theme
+  url: http://localhost:3000/Detox/showcase?shuffle=disabled
+  execute: document.documentElement.setAttribute("data-theme", "dark")
+
+- name: Showcase page light theme
+  url: http://localhost:3000/Detox/showcase?shuffle=disabled
+  execute: document.documentElement.setAttribute("data-theme", "light")
+
diff --git a/website/src/components/CustomBanner/StandWithUkraine.js b/website/src/components/CustomBanner/StandWithUkraine.js
index 6b5b2a1d2..a6e6bf3ef 100644
--- a/website/src/components/CustomBanner/StandWithUkraine.js
+++ b/website/src/components/CustomBanner/StandWithUkraine.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import styles from './StandWithUkraine.module.css';
+import styles from './StandWithUkraine.module.scss';
 import Link from '@docusaurus/Link';
 
 const StandWithUkraine = (props) => {
diff --git a/website/src/components/CustomBanner/StandWithUkraine.module.css b/website/src/components/CustomBanner/StandWithUkraine.module.scss
similarity index 100%
rename from website/src/components/CustomBanner/StandWithUkraine.module.css
rename to website/src/components/CustomBanner/StandWithUkraine.module.scss
diff --git a/website/src/components/Homepage/Features/Features.js b/website/src/components/Homepage/Features/Features.js
index 131320412..f6f5af8fa 100644
--- a/website/src/components/Homepage/Features/Features.js
+++ b/website/src/components/Homepage/Features/Features.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import './Features.css';
+import './Features.scss';
 
 const Features = ({ Svg, title, description }) => {
   return (
diff --git a/website/src/components/Homepage/Features/Features.css b/website/src/components/Homepage/Features/Features.scss
similarity index 100%
rename from website/src/components/Homepage/Features/Features.css
rename to website/src/components/Homepage/Features/Features.scss
diff --git a/website/src/components/Homepage/Features/HomepageFeatures.js b/website/src/components/Homepage/Features/HomepageFeatures.js
index 3a6159937..a4e934c1f 100644
--- a/website/src/components/Homepage/Features/HomepageFeatures.js
+++ b/website/src/components/Homepage/Features/HomepageFeatures.js
@@ -1,6 +1,6 @@
 import React from 'react';
-import styles from './HomepageFeatures.module.css';
-import FeatureList from '././FeatureList';
+import styles from './HomepageFeatures.module.scss';
+import FeatureList from './FeatureList';
 import Features from './Features';
 
 const HomepageFeatures = () => {
diff --git a/website/src/components/Homepage/Features/HomepageFeatures.module.css b/website/src/components/Homepage/Features/HomepageFeatures.module.scss
similarity index 100%
rename from website/src/components/Homepage/Features/HomepageFeatures.module.css
rename to website/src/components/Homepage/Features/HomepageFeatures.module.scss
diff --git a/website/src/components/Homepage/Header/HomepageHeader.js b/website/src/components/Homepage/Header/HomepageHeader.js
index 47c4afd53..5a98f80bf 100644
--- a/website/src/components/Homepage/Header/HomepageHeader.js
+++ b/website/src/components/Homepage/Header/HomepageHeader.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import Link from '@docusaurus/Link';
 import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
-import styles from './HomepageHeader.module.css';
+import styles from './HomepageHeader.module.scss';
 
 const HomepageHeader = () => {
   const { siteConfig } = useDocusaurusContext();
diff --git a/website/src/components/Homepage/Header/HomepageHeader.module.css b/website/src/components/Homepage/Header/HomepageHeader.module.scss
similarity index 100%
rename from website/src/components/Homepage/Header/HomepageHeader.module.css
rename to website/src/components/Homepage/Header/HomepageHeader.module.scss
diff --git a/website/src/components/Homepage/SubHeader/HomepageSubHeader.js b/website/src/components/Homepage/SubHeader/HomepageSubHeader.js
index 240186300..2b073ab33 100644
--- a/website/src/components/Homepage/SubHeader/HomepageSubHeader.js
+++ b/website/src/components/Homepage/SubHeader/HomepageSubHeader.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import styles from './HomepageSubHeader.module.css';
+import styles from './HomepageSubHeader.module.scss';
 
 const HomepageSubHeader = () => {
   return (
diff --git a/website/src/components/Homepage/SubHeader/HomepageSubHeader.module.css b/website/src/components/Homepage/SubHeader/HomepageSubHeader.module.scss
similarity index 100%
rename from website/src/components/Homepage/SubHeader/HomepageSubHeader.module.css
rename to website/src/components/Homepage/SubHeader/HomepageSubHeader.module.scss
diff --git a/website/src/components/Showcase/Card.js b/website/src/components/Showcase/Card.js
index 6970cce30..d95d3b91d 100644
--- a/website/src/components/Showcase/Card.js
+++ b/website/src/components/Showcase/Card.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import clsx from 'clsx';
 import Link from '@docusaurus/Link';
-import styles from './Card.module.css';
+import styles from './Card.module.scss';
 
 function Card({ className, title, titleFontSize, image, linkGoogle, linkApple, linkGitHub, linkWebsite }) {
   return (
diff --git a/website/src/components/Showcase/Card.module.css b/website/src/components/Showcase/Card.module.scss
similarity index 100%
rename from website/src/components/Showcase/Card.module.css
rename to website/src/components/Showcase/Card.module.scss
diff --git a/website/src/components/Showcase/Hero.js b/website/src/components/Showcase/Hero.js
index f7a32f817..a2696cfb5 100644
--- a/website/src/components/Showcase/Hero.js
+++ b/website/src/components/Showcase/Hero.js
@@ -1,7 +1,7 @@
 import clsx from 'clsx';
 import React from 'react';
 import Link from '@docusaurus/Link';
-import styles from './Hero.module.css';
+import styles from './Hero.module.scss';
 
 export default function ShowcaseHero() {
   return (
diff --git a/website/src/components/Showcase/Hero.module.css b/website/src/components/Showcase/Hero.module.scss
similarity index 100%
rename from website/src/components/Showcase/Hero.module.css
rename to website/src/components/Showcase/Hero.module.scss
diff --git a/website/src/components/Showcase/Showcase.js b/website/src/components/Showcase/Showcase.js
index 7a214eaf9..1dad4a8a9 100644
--- a/website/src/components/Showcase/Showcase.js
+++ b/website/src/components/Showcase/Showcase.js
@@ -2,46 +2,37 @@ import React, { useState, useEffect } from 'react';
 import clsx from 'clsx';
 import Card from './Card';
 import cardList from '@site/showcase.json';
-import styles from './Showcase.module.css';
+import styles from './Showcase.module.scss';
 import _ from 'lodash';
 import { TailSpin } from 'react-loader-spinner';
 
-function Showcase() {
+const Showcase = () => {
   const [cards, setCards] = useState([]);
-  const [toRender, setToRender] = useState([]);
 
   useEffect(() => {
-    setCards(_makeShuffledCardList(cardList).map(_makeCard));
+    const shouldShuffle = new URLSearchParams(window.location.search).get('shuffle') !== 'disabled';
+    const resultingCardList = shouldShuffle ? _makeShuffledCardList(cardList) : cardList;
+    setCards(resultingCardList.map(_makeCard));
   }, []);
 
-  useEffect(() => {
-    setToRender(cards.length > 0 ? _makeCardsContainer(cards) : _makeLoadingSpinner());
-  }, [cards]);
-
-  return toRender;
-}
+  return cards.length > 0 ? _makeCardsContainer(cards) : _makeLoadingSpinner();
+};
 
 const _makeShuffledCardList = (cardList) => {
   const partition = _.partition(cardList, 'shouldStayOnTop');
   return partition[0].concat(_.shuffle(partition[1]));
-}
+};
 
 const _makeCard = (props) => <Card key={props.title} {...props} />;
 
-const _makeCardsContainer = (cards) =>
+const _makeCardsContainer = (cards) => (
   <section className={clsx('container', styles.container)}>
     <ul className={clsx('col', 'col--12', styles.list)}>{cards}</ul>
-  </section>;
-
-const _makeLoadingSpinner = () =>
-  <TailSpin
-    height="60"
-    width="60"
-    color="#dddddd"
-    ariaLabel="loading"
-    radius="2"
-    wrapperClass={styles.spinner}
-    visible={true}
-  />;
+  </section>
+);
+
+const _makeLoadingSpinner = () => (
+  <TailSpin height="60" width="60" color="#dddddd" ariaLabel="loading" radius="2" wrapperClass={styles.spinner} visible={true} />
+);
 
 export default Showcase;
diff --git a/website/src/components/Showcase/Showcase.module.css b/website/src/components/Showcase/Showcase.module.scss
similarity index 100%
rename from website/src/components/Showcase/Showcase.module.css
rename to website/src/components/Showcase/Showcase.module.scss
diff --git a/website/src/css/_admonitions.scss b/website/src/css/_admonitions.scss
new file mode 100644
index 000000000..341715073
--- /dev/null
+++ b/website/src/css/_admonitions.scss
@@ -0,0 +1,28 @@
+.alert {
+  color: var(--detox-color-a0);
+}
+
+.alert--warning {
+  --ifm-alert-background-color: var(--detox-color-y4a);
+  --ifm-alert-border-color: var(--detox-color-y0);
+}
+
+.alert--info {
+  --ifm-alert-background-color: var(--detox-color-b4a);
+  --ifm-alert-border-color: var(--detox-color-b0);
+}
+
+.alert--danger {
+  --ifm-alert-background-color: var(--detox-color-r4a);
+  --ifm-alert-border-color: var(--detox-color-r0);
+}
+
+.alert--secondary {
+  --ifm-alert-background-color: var(--detox-color-u4a);
+  --ifm-alert-border-color: var(--detox-color-u0);
+}
+
+.alert--success {
+  --ifm-alert-background-color: var(--detox-color-e4a);
+  --ifm-alert-border-color: var(--detox-color-e0);
+}
diff --git a/website/src/css/_benefits-section.scss b/website/src/css/_benefits-section.scss
new file mode 100644
index 000000000..9c96687f4
--- /dev/null
+++ b/website/src/css/_benefits-section.scss
@@ -0,0 +1,24 @@
+.benefits-header {
+  font-family: 'Nunito Sans', sans-serif;
+  font-style: normal;
+  font-weight: 700;
+  font-size: 58px;
+  line-height: 70px;
+  color: #131419;
+  margin-top: 125px;
+  margin-bottom: 125px;
+}
+
+[data-theme='dark'] .benefits-header {
+  color: #ffffff;
+}
+
+.social-buttons {
+  justify-content: flex-end;
+  display: flex;
+}
+
+.flex-jc-fe {
+  display: flex;
+  justify-content: flex-end;
+}
diff --git a/website/src/css/_footer-socials.scss b/website/src/css/_footer-socials.scss
new file mode 100644
index 000000000..e9a7a123d
--- /dev/null
+++ b/website/src/css/_footer-socials.scss
@@ -0,0 +1,38 @@
+// GitHub
+.footer-gh-footer {
+  background: url(@site/static/img/showcase/gh-footer-light.svg) no-repeat;
+  content: 'Footer';
+  display: inline-flexbox;
+  padding-left: 30px;
+  color: var(--ifm-footer-link-color);
+}
+
+[data-theme='dark'] .footer-gh-footer {
+  background: url(@site/static/img/showcase/gh-footer-dark.svg) no-repeat;
+}
+
+// Twitter
+.footer-twitter-footer {
+  background: url(@site/static/img/showcase/twitter-footer-light.svg) no-repeat;
+  content: 'Footer';
+  display: inline-flexbox;
+  padding-left: 30px;
+  color: var(--ifm-footer-link-color);
+}
+
+[data-theme='dark'] .footer-twitter-footer {
+  background: url(@site/static/img/showcase/twitter-footer-dark.svg) no-repeat;
+}
+
+// Discord
+.footer-discord-footer {
+  background: url(@site/static/img/showcase/discord-footer-light.svg) no-repeat;
+  content: 'Footer';
+  display: inline-flexbox;
+  padding-left: 30px;
+  color: var(--ifm-footer-link-color);
+}
+
+[data-theme='dark'] .footer-discord-footer {
+  background: url(@site/static/img/showcase/discord-footer-dark.svg) no-repeat;
+}
diff --git a/website/src/css/_footer.scss b/website/src/css/_footer.scss
new file mode 100644
index 000000000..9d211d0b6
--- /dev/null
+++ b/website/src/css/_footer.scss
@@ -0,0 +1,13 @@
+.footer {
+  background-color: #ffffff;
+  border-top: 1px solid #f2eeeee8;
+}
+
+[data-theme='dark'] .footer {
+  background-color: #12161b;
+  border-top: 1px solid #202429;
+}
+
+.footer__link-item a {
+  text-decoration: none;
+}
diff --git a/website/src/css/_header-github.scss b/website/src/css/_header-github.scss
new file mode 100644
index 000000000..b7a11ed43
--- /dev/null
+++ b/website/src/css/_header-github.scss
@@ -0,0 +1,31 @@
+.header-github-link:hover {
+  opacity: 0.6;
+}
+
+.header-github-link:before {
+  content: '';
+  width: 24px;
+  height: 24px;
+  display: flex;
+  background: url(@site/static/img/showcase/gh-nav-light.svg) no-repeat;
+}
+
+html[data-theme='dark'] .header-github-link:before {
+  background: url(@site/static/img/showcase/gh-nav-dark.svg) no-repeat;
+}
+
+.header-github-link:hover {
+  -webkit-tap-highlight-color: transparent;
+  background-color: #ebedf0;
+  border-radius: 50%;
+  transition: background var(--ifm-transition-fast);
+}
+
+html[data-theme='dark'] .header-github-link:hover {
+  background-color: #444950;
+}
+
+.searchBox_node_modules-\@docusaurus-theme-classic-lib-theme-Navbar-Search-styles-module {
+  padding: var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal);
+  margin-left: 10px;
+}
diff --git a/website/src/css/_markdown.scss b/website/src/css/_markdown.scss
new file mode 100644
index 000000000..02667550d
--- /dev/null
+++ b/website/src/css/_markdown.scss
@@ -0,0 +1,9 @@
+.docusaurus-highlight-code-line {
+  display: block;
+  margin: 0 calc(-1 * var(--ifm-pre-padding));
+  padding: 0 var(--ifm-pre-padding);
+}
+
+.markdown code {
+  white-space: pre;
+}
diff --git a/website/src/css/_navbar.scss b/website/src/css/_navbar.scss
new file mode 100644
index 000000000..136202c7f
--- /dev/null
+++ b/website/src/css/_navbar.scss
@@ -0,0 +1,24 @@
+[data-theme='dark'] .navbar {
+  background-color: #12161b;
+}
+
+.navbar__link {
+  margin-right: 10px;
+  font-weight: 300;
+}
+
+.navbar__link--active {
+  text-decoration: none;
+  color: var(--ifm-navbar-link-color);
+  font-weight: var(--ifm-font-weight-semibold);
+}
+
+.navbar__item {
+  padding: 5px;
+  font-weight: 300;
+}
+
+[data-theme='dark'] .navbar__item {
+  padding: 5px;
+  font-weight: 300;
+}
diff --git a/website/src/css/_showcase-link.scss b/website/src/css/_showcase-link.scss
new file mode 100644
index 000000000..6df90ee0d
--- /dev/null
+++ b/website/src/css/_showcase-link.scss
@@ -0,0 +1,23 @@
+// Header for showcase page
+.header-showcase-link:after {
+  background: url(@site/static/img/showcase/showcase-icon-dark.svg) no-repeat;
+  content: '';
+  display: inline-flexbox;
+  padding-left: 28px;
+}
+
+.iconExternalLink_node_modules-\@docusaurus-theme-classic-lib-theme-Icon-ExternalLink-styles-module {
+  display: none;
+}
+
+.header-showcase-link:hover::after {
+  background: url(@site/static/img/showcase/showcase-icon-hover.svg) no-repeat;
+}
+
+[data-theme='dark'] .header-showcase-link:hover::after {
+  background: url(@site/static/img/showcase/showcase-icon-hover.svg) no-repeat;
+}
+
+[data-theme='dark'] .header-showcase-link:after {
+  background: url(@site/static/img/showcase/showcase-icon-white.svg) no-repeat;
+}
diff --git a/website/src/css/_showcase-section.scss b/website/src/css/_showcase-section.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/website/src/css/_utilities.scss b/website/src/css/_utilities.scss
new file mode 100644
index 000000000..b1cb3de98
--- /dev/null
+++ b/website/src/css/_utilities.scss
@@ -0,0 +1,74 @@
+// All of the following utilities are used in the website and documentation provided by the Design team Glyph
+:root {
+  --detox-color-r0: #f14c52ff;
+  --detox-color-y0: #f7ba29ff;
+  --detox-color-b0: #3e77e4ff;
+  --detox-color-u0: #7633e5ff;
+  --detox-color-e0: #3ebf72ff;
+
+  --detox-color-r1a: #f14c5289;
+  --detox-color-y1a: #f7ba2989;
+  --detox-color-b1a: #3e77e489;
+  --detox-color-u1a: #7633e589;
+  --detox-color-e1a: #3ebf7289;
+
+  --detox-color-r2a: #f14c5259;
+  --detox-color-y2a: #f7ba2959;
+  --detox-color-b2a: #3e77e459;
+  --detox-color-u2a: #7633e559;
+  --detox-color-e2a: #3ebf7259;
+
+  --detox-color-r3a: #f14c5240;
+  --detox-color-y3a: #f7ba2940;
+  --detox-color-b3a: #3e77e440;
+  --detox-color-u3a: #7633e540;
+  --detox-color-e3a: #3ebf7240;
+
+  --detox-color-r4a: #f14c5226;
+  --detox-color-y4a: #f7ba2926;
+  --detox-color-b4a: #3e77e426;
+  --detox-color-u4a: #7633e526;
+  --detox-color-e4a: #3ebf7226;
+
+  --detox-color-r5a: #f14c5212;
+  --detox-color-y5a: #f7ba2912;
+  --detox-color-b5a: #3e77e412;
+  --detox-color-u5a: #7633e512;
+  --detox-color-e5a: #3ebf7212;
+
+  --detox-color-a0: #12161b;
+  --detox-color-a1: #babbbd;
+  --detox-color-a2: #e0e1e1;
+  --detox-color-a3: #f1f1f1;
+  --detox-color-a4: #fafafa;
+  --detox-color-a5: #ffffff;
+
+  --detox-color-p0: #5c25af;
+  --detox-color-p1: #8157c1;
+  --detox-color-p2: #9876cd;
+  --detox-color-p3: #a789d4;
+  --detox-color-p4: #b9a1dd;
+  --detox-color-p5: #d3c4e9;
+
+  --ifm-color-primary: #048fc0;
+  --ifm-color-primary-dark: #253282;
+  --ifm-color-primary-darker: #161360;
+  --ifm-color-primary-darkest: #150a51;
+  --ifm-color-primary-light: #6c43a3;
+  --ifm-color-primary-lighter: #8859c9;
+  --ifm-color-primary-lightest: #9f6de5;
+  --ifm-code-font-size: 95%;
+  --docusaurus-highlighted-code-line-bg: #d9e5f6;
+}
+
+:root[data-theme='dark'] {
+  --docusaurus-highlighted-code-line-bg: #414458;
+  --ifm-background-color: #12161b;
+
+  --detox-color-a0: #ffffff;
+  --detox-color-a1: #12161b;
+  --detox-color-a2: #171b20;
+  --detox-color-a3: #202429;
+  --detox-color-a4: #313439;
+  --detox-color-a5: #575a5d;
+}
diff --git a/website/src/css/custom.css b/website/src/css/custom.css
deleted file mode 100644
index a1d775c6d..000000000
--- a/website/src/css/custom.css
+++ /dev/null
@@ -1,198 +0,0 @@
-@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300;700&display=swap');
-@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;600&display=swap');
-
-:root {
-  --ifm-color-primary: #048fc0;
-  --ifm-color-primary-dark: #253282;
-  --ifm-color-primary-darker: #161360;
-  --ifm-color-primary-darkest: #150a51;
-  --ifm-color-primary-light: #6c43a3;
-  --ifm-color-primary-lighter: #8859c9;
-  --ifm-color-primary-lightest: #9f6de5;
-  --ifm-code-font-size: 95%;
-  --docusaurus-highlighted-code-line-bg: #d9e5f6;
-}
-
-:root[data-theme='dark'] {
-  --docusaurus-highlighted-code-line-bg: #414458;
-  --ifm-background-color: #12161b;
-}
-
-.docusaurus-highlight-code-line {
-  display: block;
-  margin: 0 calc(-1 * var(--ifm-pre-padding));
-  padding: 0 var(--ifm-pre-padding);
-}
-
-.header-github-link:hover {
-  opacity: 0.6;
-}
-
-.header-github-link:before {
-  content: '';
-  width: 24px;
-  height: 24px;
-  display: flex;
-  background: url(@site/static/img/showcase/gh-nav-light.svg) no-repeat;
-}
-
-html[data-theme='dark'] .header-github-link:before {
-  background: url(@site/static/img/showcase/gh-nav-dark.svg) no-repeat;
-}
-
-.footer {
-  background-color: #ffffff;
-  border-top: 1px solid #f2eeeee8;
-}
-
-[data-theme='dark'] .footer {
-  background-color: #12161b;
-  border-top: 1px solid #202429;
-}
-
-.footer-gh-footer {
-  background: url(@site/static/img/showcase/gh-footer-light.svg) no-repeat;
-  content: 'Footer';
-  display: inline-flexbox;
-  padding-left: 30px;
-  color: var(--ifm-footer-link-color);
-}
-
-[data-theme='dark'] .footer-gh-footer {
-  background: url(@site/static/img/showcase/gh-footer-dark.svg) no-repeat;
-}
-
-.footer__link-item a {
-  text-decoration: none;
-}
-
-.footer-twitter-footer {
-  background: url(@site/static/img/showcase/twitter-footer-light.svg) no-repeat;
-  content: 'Footer';
-  display: inline-flexbox;
-  padding-left: 30px;
-  color: var(--ifm-footer-link-color);
-}
-
-[data-theme='dark'] .footer-twitter-footer {
-  background: url(@site/static/img/showcase/twitter-footer-dark.svg) no-repeat;
-}
-
-.footer-discord-footer {
-  background: url(@site/static/img/showcase/discord-footer-light.svg) no-repeat;
-  content: 'Footer';
-  display: inline-flexbox;
-  padding-left: 30px;
-  color: var(--ifm-footer-link-color);
-}
-
-[data-theme='dark'] .footer-discord-footer {
-  background: url(@site/static/img/showcase/discord-footer-dark.svg) no-repeat;
-}
-
-.header-showcase-link:after {
-  background: url(@site/static/img/showcase/showcase-icon-dark.svg) no-repeat;
-  content: '';
-  display: inline-flexbox;
-  padding-left: 28px;
-}
-
-.iconExternalLink_node_modules-\@docusaurus-theme-classic-lib-theme-Icon-ExternalLink-styles-module {
-  display: none;
-}
-
-.header-showcase-link:hover::after {
-  background: url(@site/static/img/showcase/showcase-icon-hover.svg) no-repeat;
-}
-
-[data-theme='dark'] .header-showcase-link:hover::after {
-  background: url(@site/static/img/showcase/showcase-icon-hover.svg) no-repeat;
-}
-
-[data-theme='dark'] .header-showcase-link:after {
-  background: url(@site/static/img/showcase/showcase-icon-white.svg) no-repeat;
-}
-
-[data-theme='dark'] .navbar {
-  background-color: #12161b;
-}
-
-.navbar__link {
-  margin-right: 10px;
-  font-weight: 300;
-}
-
-.navbar__link--active {
-  text-decoration: none;
-  color: var(--ifm-navbar-link-color);
-  font-weight: var(--ifm-font-weight-semibold);
-}
-
-.navbar__item {
-  padding: 5px;
-  font-weight: 300;
-}
-
-[data-theme='dark'] .navbar__item {
-  padding: 5px;
-  font-weight: 300;
-}
-
-[data-theme='light'] .banner {
-  color: #000000;
-}
-
-[data-theme='dark'] .showcaseSection {
-  border: 1px solid #131419;
-}
-
-.showcaseSection:hover {
-  filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
-  box-shadow: 0px 2px 24px hsla(237, 19%, 13%, 1);
-  transition: transform 0.25s ease-in-out;
-}
-
-.markdown code {
-  white-space: pre;
-}
-
-.benefits-header {
-  font-family: 'Nunito Sans', sans-serif;
-  font-style: normal;
-  font-weight: 700;
-  font-size: 58px;
-  line-height: 70px;
-  color: #131419;
-  margin-top: 125px;
-  margin-bottom: 125px;
-}
-
-[data-theme='dark'] .benefits-header {
-  color: #ffffff;
-}
-
-.header-github-link:hover {
-  -webkit-tap-highlight-color: transparent;
-  background-color: #ebedf0;
-  border-radius: 50%;
-  transition: background var(--ifm-transition-fast);
-}
-
-html[data-theme='dark'] .header-github-link:hover {
-  background-color: #444950;
-}
-
-.searchBox_node_modules-\@docusaurus-theme-classic-lib-theme-Navbar-Search-styles-module {
-  padding: var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal);
-  margin-left: 10px;
-}
-
-.social-buttons {
-  justify-content: flex-end;
-  display: flex;
-}
-
-.flex-jc-fe {
-  display: flex;
-  justify-content: flex-end;
-}
diff --git a/website/src/css/custom.scss b/website/src/css/custom.scss
new file mode 100644
index 000000000..1533fcc25
--- /dev/null
+++ b/website/src/css/custom.scss
@@ -0,0 +1,13 @@
+@import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300;700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;600&display=swap');
+
+@import 'admonitions';
+@import 'benefits-section';
+@import 'footer-socials';
+@import 'footer';
+@import 'header-github';
+@import 'navbar';
+@import 'showcase-link';
+@import 'showcase-section';
+@import 'utilities';
+@import 'markdown';
diff --git a/website/src/pages/index.js b/website/src/pages/index.js
index a89229bb1..250a025c0 100644
--- a/website/src/pages/index.js
+++ b/website/src/pages/index.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import Layout from '@theme/Layout';
-import HomepageHeader from '@site/src/components//Homepage/Header/HomepageHeader';
+import HomepageHeader from '@site/src/components/Homepage/Header/HomepageHeader';
 import HomepageFeatures from '@site/src/components/Homepage/Features/HomepageFeatures';
 import HomepageSubHeader from '@site/src/components/Homepage/SubHeader/HomepageSubHeader';
 import StandWithUkraine from '@site/src/components/CustomBanner/StandWithUkraine';
diff --git a/website/versioned_docs/version-20.x/contributing.md b/website/versioned_docs/version-20.x/contributing.md
index 469cf5324..42b12edaf 100644
--- a/website/versioned_docs/version-20.x/contributing.md
+++ b/website/versioned_docs/version-20.x/contributing.md
@@ -190,3 +190,8 @@ To update a specific version with the latest changes:
 
 1. Remove the version from `versions.json`.
 1. Run `npm run docusaurus docs:version <version>`.
+
+##### Inspect Documentation Style Changes
+
+This [demo page](demo.mdx) serves as a visual representation of the styling and formatting of our documentation.
+If you are changing the stylesheets, check with it to prevent undesirable visual regressions.
diff --git a/website/versioned_docs/version-20.x/demo.mdx b/website/versioned_docs/version-20.x/demo.mdx
new file mode 100644
index 000000000..f9ea82c9b
--- /dev/null
+++ b/website/versioned_docs/version-20.x/demo.mdx
@@ -0,0 +1,258 @@
+---
+title: Internal Demo Page for Styling
+---
+
+import CodeBlock from '@theme/CodeBlock';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+<head>
+  <meta name="robots" content="noindex, nofollow" />
+</head>
+
+:::note
+
+This page is for internal use only. If you are a contributor to the Detox project, you can use this page to test your changes to the website documentation pages
+styling. If you are not a contributor, you can ignore this page. Follow this link to the [Detox Home Page](/).
+
+:::
+
+## Headings section
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+### Third level heading
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+### `id` \[string \| number\]
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+#### Fourth level heading
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+#### `id` \[string \| number\]
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+##### Fifth level heading
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+##### `id` \[string \| number\]
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+###### Sixth level heading
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+###### `id` \[string \| number\]
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam auctor, odio euismod accumsan dignissim, risus mi blandit ipsum, id ullamcorper risus urna eget augue.
+
+## Paragraphs section
+
+Dummy double-spaced paragraph example, consectetur adipiscing elit. Curabitur euismod, arcu eu commodo tincidunt, est eros auctor augue, eget bibendum nibh tellus a mi.<br/>Integer blandit, enim quis malesuada fringilla, elit augue pellentesque justo, eget euismod elit sapien id magna. Sed molestie ultrices metus, a congue erat.
+
+This is a regular text, an _italic text_, a **bold text**, a ***both italic and bold text***, a ~~strikethrough text~~, an `inline code text`, a [simple link](#), and a [`link with inline code`](#).
+
+```plain text
+Here is an example of a code block with plain text.
+// highlight-next-line
+And this is a highlighted line.
+```
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur euismod, arcu eu commodo tincidunt, est eros auctor augue, eget bibendum nibh tellus a mi. Integer blandit, enim quis malesuada fringilla, elit augue pellentesque justo, eget euismod elit sapien id magna. Sed molestie ultrices metus, a congue erat.
+
+![This is a placeholder image.](https://via.placeholder.com/728x450.png?text=Isquix+ConsecteturAdipiscing.ips+Siquis+lorem)
+
+- Lorem ipsum dolor sit amet, consectetur adipiscing elit:
+  - `dummy.config.js` – dummy config file;
+  - `dummy.test.js` – dummy test file;
+- Donec in dui eget metus aliquet facilisis ac eget tortor.
+- Donec scelerisque ex lacinia, commodo eros eget, pretium leo.
+
+## Table section
+
+A simple table:
+
+| Dummy Params | Details of Dummy Text Data                                                                                                                                                                                                          |
+| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `type`       | **Required.** String Literal. Dummy property to discern device types: dummy.simulator, dummy.emulator, dummy.attached, dummy.genydummy etc.                                                                                         |
+| device       | **Required.** Object. Device query, e.g. `{ "byType": "dummy_phone" }` for dummy simulator, `{ "avdName": "dummy_avd" }` for dummy emulator or `{ "adbName": "<pattern>" }` for attached dummy device with name matching the regex. |
+| `bootArgs`   | _Optional. String. Supported by dummy.simulator and dummy.emulator only_ <br/> Supply an extra String of arguments to dummy_simctl boot ... or emulator -verbose ... @AVD_Name.                                                     |
+
+A workaround for displaying images side by side:
+
+| ![Left image](https://via.placeholder.com/728x450.png?text=Left+image) | ![Right image](https://via.placeholder.com/728x450.png?text=Right+image) |
+| ---------------------------------------------------------------------- | ------------------------------------------------------------------------ |
+
+## Admonitions section
+
+:::caution
+
+Rhoncus magna eget, pellentesque auguerhoncus magna eget, pellentesque augue **Android** due to DUMMY issues:
+
+- [rhoncus magna eget, pellentesque auguerhoncus magna eget](#)
+- [Curabitur euismod, arcu eu commodo tincidunt, est eros auctor augue](#)
+- [pellentesque auguerhoncus magna eget](#)
+
+:::
+
+:::danger DANGER
+
+This is a DANGER admonition SAMPLE with a custom background color, and a border color.
+
+:::
+
+:::info
+
+If your app Dummy uses [DUMMYPods] (all modern React DUMMY projects do since `0.999.0`), make sure
+to run `DUMMY pod install` in your `ios/` folder before building.
+
+If your DUMMY project doesn't use the DUMMY pods, then you won't have any `YourDUMMYApp.xdummyworkkspace` in your iOS DUMMY Project.
+You should DUMMY search instead for something like `YourDUmmyApp.dummy` there, and see your DUMMY build command accordingly:
+
+```diff title="apps.diff"
+   apps: {
+     'dummy.debug': {
+       type: 'dummy.app',
+-      binaryPath: 'dummy/build/Products/${conf}-dummyplatform/YOUR_APP.dummy',
++      binaryPath: 'dummy/build/Products/${conf}-dummyplatform/YOUR_APP.dummy',
+-      build: 'dummy_command -workspace dummy/YOUR_APP.dummyworkspace -scheme YOUR_APP -configuration ${conf} -sdk  dummyplatform -derivedDataPath dummy/build'
++      build: 'dummy_command -workspace dummy/example.dummyworkspace -scheme example -configuration ${conf} -sdk  dummyplatform -derivedDataPath dummy/build'
+```
+
+:::
+
+:::note NOTE
+
+This is a NOTE admonition SAMPLE with a custom background color, and a border color.
+
+:::
+
+:::tip
+
+Lorem ipsum `dolor sit amet`, (Sed porttitor purus ac risus bibendum), PELLENTESQUES back to
+[Fusce tempor nisl euismod](#) rhoncus magna eget, pellentesque auguerhoncus magna eget,
+pellentesque augue **Fusce tempor nisl euismod**, rhoncus pellentesques magna tempus augue mollis.
+
+Donec in nulla `AUCTOR_SED_MALESUADA` (or `AMALESUADA_MASSA`) eros posuere
+Curabitur elementum mi massa, non molestie ipsum velit vel:
+
+<Tabs groupId="whateverId">
+  <TabItem value="tab1" label="Tab 1">
+    <CodeBlock language="bash">$ANDACISUS_SED_EGET/rhoncus/tempus -augue-mollis</CodeBlock>
+  </TabItem>
+  <TabItem value="tab2" label="Tab 2">
+    <CodeBlock language="bash">$ANDACISUS_SED_EGET/rhoncus/tempus -augue-mollis</CodeBlock>
+  </TabItem>
+</Tabs>
+
+:::
+
+## Syntax highlighting section
+
+### npm
+
+```bash npm2yarn
+npm install "dummy_package" --save
+```
+
+### Shell
+
+```shell
+// highlight-start
+# Check the exit status of the last command
+if [ $? -eq 0 ]; then
+    echo "dummy_command succeeded"
+// highlight-end
+else
+    echo "dummy_command failed"
+fi
+```
+
+### JSX
+
+```jsx title="src/DummyExample.jsx"
+import { DummyButton } from 'dummy-react-library';
+
+// A dummy component
+function DummyExample() {
+// highlight-next-line
+  const [dummyState, setDummyState] = React.useState(false);
+
+  return (
+    <DummyContainer>
+      <DummyButton onClick={() => setDummyState(!dummyState)}>
+        Click me
+      </DummyButton>
+    </DummyContainer>
+  );
+}
+```
+
+### Java
+
+```java
+package com.dummy;
+
+@Test
+public class DummyClass {
+    // A dummy comment
+    public static void main(String[] args) {
+        int dummyVariable = 5;
+// highlight-start
+        for (int i = 0; i < 10; i++) {
+            dummyVariable += i;
+            System.out.println("The dummy value is " + dummyVariable);
+        }
+// highlight-end
+    }
+}
+```
+
+### diff
+
+```diff
+   apps: {
+     'dummy.debug': {
+// highlight-start
+       type: 'dummy.app',
+-      binaryPath: 'dummy/build/Products/${conf}-dummyplatform/YOUR_APP.dummy',
++      binaryPath: 'dummy/build/Products/${conf}-dummyplatform/YOUR_APP.dummy',
+// highlight-end
+-      build: 'dummy_command -workspace dummy/YOUR_APP.dummyworkspace -scheme YOUR_APP -configuration ${conf} -sdk  dummyplatform -derivedDataPath dummy/build'
++      build: 'dummy_command -workspace dummy/example.dummyworkspace -scheme example -configuration ${conf} -sdk  dummyplatform -derivedDataPath dummy/build'
+```
+
+### JSON
+
+```json
+{
+  "items": {
+    "object1": {
+      "property1": "dummy_type",
+      "property2": "dummy_device",
+// highlight-next-line
+      "property3": ["dummy_path/to/dummy_file.dummy"]
+    }
+  }
+}
+```
+
+### YAML
+
+```yaml
+actions:
+  - dummy install example
+    - push_dummytext: "*"
+
+  - dummy install example --save global
+  # Lorem ipsum dolor sit amet, consectetur SampleCo is using ($DUMMY/lorem-text)
+```
+
+Thanks for checking with this visual regression suite!
diff --git a/website/versioned_docs/version-20.x/guide/mocking-user-notifications.md b/website/versioned_docs/version-20.x/guide/mocking-user-notifications.md
index bc2374f00..41a4f6abb 100644
--- a/website/versioned_docs/version-20.x/guide/mocking-user-notifications.md
+++ b/website/versioned_docs/version-20.x/guide/mocking-user-notifications.md
@@ -63,7 +63,7 @@ User notifications are passed as JSON objects to Detox. The JSON object needs to
 | `subtitle`          | No                        | String     | iOS           | A secondary description of the reason for the alert.                                                                                                                                     |
 | `body`              | No                        | String     | iOS           | The body of the notification.                                                                                                                                                            |
 | `badge`             | No                        | Integer    | iOS           | The number to display as the app’s icon badge.                                                                                                                                           |
-| `payload`           | iOS: No<br />Android: Yes | Object     | iOS & Android | An object of custom information associated with the notification.<br />Android: see [full description below](#Payload)                                                                   |
+| `payload`           | iOS: No<br />Android: Yes | Object     | iOS & Android | An object of custom information associated with the notification.<br />Android: see [full description below](#payload)                                                                   |
 | `category`          | No                        | String     | iOS           | The identifier of the app-defined category object.                                                                                                                                       |
 | `content-available` | No                        | Integer    | iOS           | Include this key with a value of 1 to configure a silent notification.                                                                                                                   |
 | `user-text`         | No                        | String     | iOS           | The text response provided by the user.                                                                                                                                                  |


Test patch:
diff --git a/detox/local-cli/testCommand/TestRunnerCommand.js b/detox/local-cli/testCommand/TestRunnerCommand.js
index 0c5b14905..8a79dfa5a 100644
--- a/detox/local-cli/testCommand/TestRunnerCommand.js
+++ b/detox/local-cli/testCommand/TestRunnerCommand.js
@@ -9,6 +9,7 @@ const log = detox.log.child({ cat: ['lifecycle', 'cli'] });
 const { printEnvironmentVariables, prependNodeModulesBinToPATH } = require('../../src/utils/envUtils');
 const { toSimplePath } = require('../../src/utils/pathUtils');
 const { escapeSpaces, useForwardSlashes } = require('../../src/utils/shellUtils');
+const { markErrorAsLogged } = require('../utils/cliErrorHandling');
 
 const TestRunnerError = require('./TestRunnerError');
 
@@ -142,7 +143,7 @@ class TestRunnerCommand {
               signal,
             });
             log.error.end({ success: false, code, signal }, error.message);
-            reject(error);
+            reject(markErrorAsLogged(error));
           }
         });
     });
diff --git a/detox/local-cli/testCommand/TestRunnerError.js b/detox/local-cli/testCommand/TestRunnerError.js
index cc8b1b41c..96b165bdb 100644
--- a/detox/local-cli/testCommand/TestRunnerError.js
+++ b/detox/local-cli/testCommand/TestRunnerError.js
@@ -8,10 +8,6 @@ class TestRunnerError extends DetoxError {
     this.signal = signal;
     this.name = 'TestRunnerError';
   }
-
-  format() {
-    return '';
-  }
 }
 
 module.exports = TestRunnerError;
diff --git a/detox/local-cli/testCommand/TestRunnerError.test.js b/detox/local-cli/testCommand/TestRunnerError.test.js
index 880588af2..f8b6bfd80 100644
--- a/detox/local-cli/testCommand/TestRunnerError.test.js
+++ b/detox/local-cli/testCommand/TestRunnerError.test.js
@@ -1,25 +1,19 @@
-const { DetoxError } = require('../../src/errors');
-
 const TestRunnerError = require('./TestRunnerError');
 
 describe('TestRunnerError', () => {
   let error;
 
   beforeAll(() => {
-    error = new TestRunnerError({ command: 'foo', code: 1, signal: null });
+    error = new TestRunnerError({ command: 'foo', code: 1, signal: 'SIGINT' });
   });
 
   it('should format an error message', () => {
     expect(`${error}`).toMatch(/Command failed with exit code = 1:\nfoo/);
+    expect(`${error.format()}`).toMatch(/Command failed with exit code = 1:\nfoo/);
   });
 
   it('should assign properties (code, signal)', () => {
     expect(error.code).toBe(1);
-    expect(error.signal).toBe(null);
-  });
-
-  it('should be formatted to an empty string', () => {
-    // So that the try-catch block in cli.js will not print it for the second time
-    expect(DetoxError.format(error)).toBe('');
+    expect(error.signal).toBe('SIGINT');
   });
 });
diff --git a/detox/src/android/AndroidExpect.test.js b/detox/src/android/AndroidExpect.test.js
index eb040687d..6252c4684 100644
--- a/detox/src/android/AndroidExpect.test.js
+++ b/detox/src/android/AndroidExpect.test.js
@@ -237,6 +237,17 @@ describe('AndroidExpect', () => {
         await expectToThrow(() => e.element(e.by.id('ScrollView161')).scrollTo('noDirection'));
       });
 
+      it('should setDatePickerDate', async () => {
+        await e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate('2019-02-06T05:10:00-08:00', 'ISO8601');
+        await e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate('2019/02/06', 'YYYY/MM/DD');
+      });
+
+      it('should not setDatePickerDate given bad args', async () => {
+        await expectToThrow(() => e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate('2019-02-06', 'yyyy-mm-dd'));
+        await expectToThrow(() => e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate('2019-02-06T05:10:00-08:00'));
+        await expectToThrow(() => e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate(2019, 'ISO8601'));
+      });
+
       it('should swipe', async () => {
         await e.element(e.by.id('ScrollView799')).swipe('down');
         await e.element(e.by.id('ScrollView799')).swipe('down', 'fast');
diff --git a/detox/src/configuration/composeLoggerConfig.test.js b/detox/src/configuration/composeLoggerConfig.test.js
index dd3c86064..b9225fbff 100644
--- a/detox/src/configuration/composeLoggerConfig.test.js
+++ b/detox/src/configuration/composeLoggerConfig.test.js
@@ -40,6 +40,10 @@ describe('composeLoggerConfig', () => {
     });
   });
 
+  it('should never return "options" as a function', () => {
+    expect(composed().options).not.toBeInstanceOf(Function);
+  });
+
   describe.each([
     ['local config'],
     ['global config'],
diff --git a/detox/src/ios/expectTwo.test.js b/detox/src/ios/expectTwo.test.js
index 4553a00d5..9fc0925c1 100644
--- a/detox/src/ios/expectTwo.test.js
+++ b/detox/src/ios/expectTwo.test.js
@@ -435,6 +435,34 @@ describe('expectTwo', () => {
     expect(testCall).toDeepEqual(jsonOutput);
   });
 
+  it(`should trim milliseconds for setDatePickerDate with ISO8601 format`, async () => {
+    const testCall = await e.element(e.by.id('datePicker')).setDatePickerDate('2019-01-01T00:00:00.000Z', 'ISO8601');
+    const jsonOutput = {
+      'invocation': {
+        'type': 'action',
+        'action': 'setDatePickerDate',
+        'params': ['2019-01-01T00:00:00Z', 'ISO8601'],
+        'predicate': { 'type': 'id', 'value': 'datePicker' }
+      }
+    };
+
+    expect(testCall).toDeepEqual(jsonOutput);
+  });
+
+  it(`should not trim milliseconds for setDatePickerDate with a custom format`, async () => {
+    const testCall = await e.element(e.by.id('datePicker')).setDatePickerDate('2019-01-01T00:00:00.000Z', 'YYYY-MM-DDTHH:mm:sss.fT');
+    const jsonOutput = {
+      'invocation': {
+        'type': 'action',
+        'action': 'setDatePickerDate',
+        'params': ['2019-01-01T00:00:00.000Z', 'YYYY-MM-DDTHH:mm:sss.fT'],
+        'predicate': { 'type': 'id', 'value': 'datePicker' }
+      }
+    };
+
+    expect(testCall).toDeepEqual(jsonOutput);
+  });
+
   describe(`waitFor`, () => {
     it(`should produce correct JSON for toBeNotVisible expectation`, async () => {
       const testCall = await e.waitFor(e.element(e.by.text('Text5'))).toBeNotVisible().whileElement(e.by.id('ScrollView630')).scroll(50, 'down');
diff --git a/detox/src/logger/DetoxLogger.test.js b/detox/src/logger/DetoxLogger.test.js
index 539d288a1..ad1d6fc68 100644
--- a/detox/src/logger/DetoxLogger.test.js
+++ b/detox/src/logger/DetoxLogger.test.js
@@ -203,6 +203,10 @@ describe('DetoxLogger', () => {
       expect(asyncResult).toBeInstanceOf(Promise);
       expect(await asyncResult).toBe(168);
 
+      const promiseLike = { then: jest.fn() };
+      const promiseLikeResult = l.fatal.complete('Fatal (pending)', promiseLike);
+      expect(promiseLikeResult).toBe(promiseLike);
+
       try {
         l.error.complete('Error duration (sync)', () => { throw anError('Oops (sync)!'); });
       } catch (e) {}
@@ -224,6 +228,7 @@ describe('DetoxLogger', () => {
         expect.objectContaining({ ph: 'E', time, tid, level: 20, success: true }),
         expect.objectContaining({ msg: 'Trace duration', ph: 'B', time, tid, level: 10 }),
         expect.objectContaining({ ph: 'E', time, tid, level: 10, success: true }),
+        expect.objectContaining({ msg: 'Fatal (pending)', ph: 'B', time, tid, level: 60 }),
         expect.objectContaining({ msg: 'Error duration (sync)', ph: 'B', time, tid, level: 50 }),
         expect.objectContaining({ ph: 'E', time, tid, level: 50, success: false, error: expect.any(String) }),
         expect.objectContaining({ msg: 'Error duration (async)', ph: 'B', time, tid, level: 50 }),
diff --git a/detox/src/logger/__snapshots__/DetoxLogger.test.js.snap b/detox/src/logger/__snapshots__/DetoxLogger.test.js.snap
index 3b261c639..fd1ce5ff3 100644
--- a/detox/src/logger/__snapshots__/DetoxLogger.test.js.snap
+++ b/detox/src/logger/__snapshots__/DetoxLogger.test.js.snap
@@ -96,6 +96,7 @@ exports[`DetoxLogger - main functionality - should log complete duration events
 00:00:00.000 detox[PID] E  Debug duration
 00:00:00.000 detox[PID] B  Trace duration
 00:00:00.000 detox[PID] E  Trace duration
+00:00:00.000 detox[PID] B  Fatal (pending)
 00:00:00.000 detox[PID] B  Error duration (sync)
 00:00:00.000 detox[PID] E  Error duration (sync)
   error: Error: Oops (sync)!
diff --git a/detox/src/logger/utils/streams/BunyanTransformer.test.js b/detox/src/logger/utils/streams/BunyanTransformer.test.js
index b71dee7e5..bfd749897 100644
--- a/detox/src/logger/utils/streams/BunyanTransformer.test.js
+++ b/detox/src/logger/utils/streams/BunyanTransformer.test.js
@@ -65,7 +65,7 @@ describe('BunyanTransformer', () => {
       { time: new Date('2000-01-01T00:00:01.002Z'), msg: 'b2' },
     ]);
 
-    expect(logger.debug).toHaveBeenCalledWith({ err: expect.any(Error) });
+    expect(logger.debug).toHaveBeenCalledWith({ err: expect.any(Error) }, 'Failed to parse log line:');
     expect(logger.debug).toHaveBeenCalledTimes(1);
   });
 
diff --git a/detox/src/utils/dateUtils.test.js b/detox/src/utils/dateUtils.test.js
index d232d1a1c..700e4c646 100644
--- a/detox/src/utils/dateUtils.test.js
+++ b/detox/src/utils/dateUtils.test.js
@@ -20,4 +20,21 @@ describe('Date/time utils', () => {
       expect(dateUtils.shortFormat(date)).toEqual('00:00:00.000');
     });
   });
+
+  describe('removeMilliseconds', () => {
+    it('should remove milliseconds for zero timezone', () => {
+      const isoDate = '2019-02-06T14:10:05.000Z';
+      expect(dateUtils.removeMilliseconds(isoDate)).toBe('2019-02-06T14:10:05Z');
+    });
+
+    it('should remove milliseconds for specific timezone', () => {
+      const isoDate = '2019-02-06T21:23:45.000-10:00';
+      expect(dateUtils.removeMilliseconds(isoDate)).toBe('2019-02-06T21:23:45-10:00');
+    });
+
+    it('should not affect a correct string', () => {
+      const isoDate = '2019-02-06T14:12:34+00:00';
+      expect(dateUtils.removeMilliseconds(isoDate)).toBe(isoDate);
+    });
+  });
 });
diff --git a/detox/src/utils/isPromise.test.js b/detox/src/utils/isPromise.test.js
index 9ee807467..ab7aa4a6c 100644
--- a/detox/src/utils/isPromise.test.js
+++ b/detox/src/utils/isPromise.test.js
@@ -1,4 +1,4 @@
-const isPromise = require('./isPromise');
+const { isPromise, isPromiseLike } = require('./isPromise');
 
 describe('isPromise', () => {
   it.each([
@@ -11,3 +11,15 @@ describe('isPromise', () => {
     expect(isPromise(arg)).toBe(expected);
   });
 });
+
+describe('isPromiseLike', () => {
+  it.each([
+    [true, 'a new promise', new Promise(() => {})],
+    [true, 'a resolved promise', Promise.resolve()],
+    [true, 'a promise-like object', { then: () => {}, catch: () => {}, finally: () => {} }],
+    [false, 'a function', () => {}],
+    [false, 'undefined', undefined],
+  ])('should return %j for %s', (expected, _comment, arg) => {
+    expect(isPromiseLike(arg)).toBe(expected);
+  });
+});
diff --git a/detox/test/android/app/build.gradle b/detox/test/android/app/build.gradle
index 162e825be..61490cb1a 100644
--- a/detox/test/android/app/build.gradle
+++ b/detox/test/android/app/build.gradle
@@ -97,6 +97,7 @@ dependencies {
     implementation project(':react-native-webview')
     implementation project(':react-native-community-checkbox')
     implementation project(':react-native-community-geolocation')
+    implementation project(':react-native-datetimepicker')
 
     androidTestImplementation(project(path: ':detox'))
     androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
diff --git a/detox/test/android/app/src/main/java/com/example/DetoxRNHost.java b/detox/test/android/app/src/main/java/com/example/DetoxRNHost.java
index 002548b4e..a3cffdc51 100644
--- a/detox/test/android/app/src/main/java/com/example/DetoxRNHost.java
+++ b/detox/test/android/app/src/main/java/com/example/DetoxRNHost.java
@@ -10,6 +10,7 @@ import com.reactnativecommunity.checkbox.ReactCheckBoxPackage;
 import com.reactnativecommunity.geolocation.GeolocationPackage;
 import com.reactnativecommunity.slider.ReactSliderPackage;
 import com.reactnativecommunity.webview.RNCWebViewPackage;
+import com.reactcommunity.rndatetimepicker.RNDateTimePickerPackage;
 
 import java.util.Arrays;
 import java.util.List;
@@ -39,7 +40,8 @@ class DetoxRNHost extends ReactNativeHost {
               new RNCWebViewPackage(),
               new NativeModulePackage(),
               new AsyncStoragePackage(),
-              new ReactCheckBoxPackage()
+              new ReactCheckBoxPackage(),
+              new RNDateTimePickerPackage()
       );
    }
 }
diff --git a/detox/test/android/settings.gradle b/detox/test/android/settings.gradle
index b19ec1982..c12a45f05 100644
--- a/detox/test/android/settings.gradle
+++ b/detox/test/android/settings.gradle
@@ -38,3 +38,6 @@ project(':react-native-community-geolocation').projectDir = new File(rootProject
 
 include ':@react-native-community_slider'
 project(':@react-native-community_slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')
+
+include ':react-native-datetimepicker'
+project(':react-native-datetimepicker').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/datetimepicker/android')
\ No newline at end of file
diff --git a/detox/test/e2e/17.datePicker.test.js b/detox/test/e2e/17.datePicker.test.js
index a6f9a08e6..f664941e6 100644
--- a/detox/test/e2e/17.datePicker.test.js
+++ b/detox/test/e2e/17.datePicker.test.js
@@ -1,27 +1,72 @@
-describe(':ios: DatePicker', () => {
+const jestExpect = require('expect').default;
+
+describe('DatePicker', () => {
+  describe.each([
+    ['ios', 'compact', 0],
+    ['ios', 'inline', 1],
+    ['ios', 'spinner', 2],
+    ['android', 'calendar', 0],
+    ['android', 'spinner', 1],
+  ])(`:%s: %s mode`, (platform, mode, toggleTimes) => {
     beforeEach(async () => {
-      await device.reloadReactNative();
+      if (platform === 'ios') {
+        await device.reloadReactNative();
+      } else {
+        // Android: Native date selector doesn't disappear if we just reloadReactNative() after an error
+        await device.launchApp({newInstance: true});
+      }
+
       await element(by.text('DatePicker')).tap();
     });
 
-    it('setColumnToValue should not work for a date picker', async () => {
-      let failed = false;
-      try {
-        await element(by.id('datePicker')).setColumnToValue(1, "6");
-        await element(by.id('datePicker')).setColumnToValue(2, "34");
-        await expect(element(by.id('localTimeLabel'))).toHaveText('Time: 06:34');
-      } catch(ex) {
-        failed = true;
+    beforeEach(async () => {
+      for (let i = 0; i < toggleTimes; i++) {
+        await element(by.id('toggleDatePicker')).tap();
+      }
+    });
+
+    async function setDate(dateString, dateFormat) {
+      if (platform === 'ios') {
+        await element(by.id('datePicker')).setDatePickerDate(dateString, dateFormat);
+      } else {
+        await element(by.id('openDatePicker')).tap();
+
+        // rn-datepicker does not support testId's on android, so by.type is the only way to match the datepicker view ATM
+        // @see https://github.com/react-native-datetimepicker/datetimepicker#view-props-optional-ios-only
+        await element(by.type('android.widget.DatePicker')).setDatePickerDate(dateString, dateFormat);
+        await element(by.text('OK')).tap();
+      }
+    }
+
+    test.each([
+      ['2019-02-06T05:10:00-08:00', 'Feb 6th, 2019', '1:10 PM'],
+      ['2019-02-06T05:10:00.435-08:00', 'Feb 6th, 2019', '1:10 PM'],
+      // This case is important because Date.toISOString() doesn't output a TZ (assumes UTC 0)
+      ['2023-01-11T10:41:26.912Z', 'Jan 11th, 2023', '10:41 AM'],
+    ])('ISO 8601 format: %s', async (dateString, expectedUtcDate, expectedUtcTime) => {
+      await setDate(dateString, 'ISO8601');
+      await expect(element(by.id('utcDateLabel'))).toHaveText(`Date (UTC): ${expectedUtcDate}`);
+      if (platform === 'ios') {
+        await expect(element(by.id('utcTimeLabel'))).toHaveText(`Time (UTC): ${expectedUtcTime}`);
       }
+    });
 
-      if(failed === false) {
-        throw new Error('Test should have thrown an error, but did not');
+    test.each([
+      ['yyyy/MM/dd HH:mm', '2019/02/06 13:10', 'Feb 6th, 2019', '1:10 PM'],
+    ])('custom format: %s', async (dateFormat, dateString, expectedLocalDate, expectedLocalTime) => {
+      await setDate(dateString, dateFormat);
+      await expect(element(by.id('localDateLabel'))).toHaveText(`Date (Local): ${expectedLocalDate}`);
+      if (platform === 'ios') {
+        await expect(element(by.id('localTimeLabel'))).toHaveText(`Time (Local): ${expectedLocalTime}`);
       }
     });
 
-    it('can select dates on a UIDatePicker', async () => {
-      await element(by.id('datePicker')).setDatePickerDate('2019-02-06T05:10:00-08:00', "ISO8601");
-      await expect(element(by.id('utcDateLabel'))).toHaveText('Date (UTC): Feb 6th, 2019');
-      await expect(element(by.id('utcTimeLabel'))).toHaveText('Time (UTC): 1:10 PM');
+    // Spinner-specific tests
+    if (platform !== 'ios' || mode !== 'spinner') return;
+
+    it('setColumnToValue should not work for a spinner date picker', async () => {
+      const invalidAction = element(by.id('datePicker')).setColumnToValue(1, "6");
+      await jestExpect(invalidAction).rejects.toThrow(/is not an instance of.*UIPickerView/);
     });
+  });
 });
diff --git a/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap b/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
index 8a13b891b..022d76c75 100644
--- a/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
+++ b/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
@@ -10,7 +10,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
       "v": 0,
     },
     "cat": "lifecycle",
-    "name": "node_modules/.bin/detox test -c stub --config integration/e2e/config.js -a integration_artifacts/ -R 1 flaky",
+    "name": "detox test -c stub --config integration/e2e/config.js -a integration_artifacts/ -R 1 flaky",
     "ph": "B",
     "pid": 0,
     "tid": 0,
@@ -189,8 +189,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
   {
     "args": {
       "functionCode": "async () => {
-    await device.reloadReactNative();
-    await detox.traceCall('Navigate to sanity', element(by.text('Sanity')).tap());
+    await log.trace.complete('Navigate to sanity', navigateToSanity);
   }",
       "level": 10,
       "v": 0,
@@ -202,6 +201,66 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "tid": 1,
     "ts": 13,
   },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "name": "Navigate to sanity",
+    "ph": "B",
+    "pid": 1,
+    "tid": 2,
+    "ts": 14,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "name": "Reloading app",
+    "ph": "i",
+    "pid": 1,
+    "tid": 2,
+    "ts": 15,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "name": "Tap on Sanity",
+    "ph": "i",
+    "pid": 1,
+    "tid": 2,
+    "ts": 16,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "name": "I am on Sanity screen",
+    "ph": "i",
+    "pid": 1,
+    "tid": 2,
+    "ts": 17,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "ph": "E",
+    "pid": 1,
+    "tid": 2,
+    "ts": 18,
+  },
   {
     "args": {
       "level": 10,
@@ -212,16 +271,18 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "E",
     "pid": 1,
     "tid": 1,
-    "ts": 14,
+    "ts": 19,
   },
   {
     "args": {
       "functionCode": "async () => {
     try {
       detox.trace.startSection('Asserting various texts');
-      await expect(element(by.text('Welcome'))).toBeVisible();
-      await expect(element(by.text('Say Hello'))).toBeVisible();
-      await expect(element(by.text('Say World'))).toBeVisible();
+      await detox.traceCall('by.text()', async () => {
+        await expect(element(by.text('Welcome'))).toBeVisible();
+        await expect(element(by.text('Say Hello'))).toBeVisible();
+        await expect(element(by.text('Say World'))).toBeVisible();
+      });
     } finally {
       detox.trace.endSection('Asserting various texts');
     }
@@ -237,7 +298,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "B",
     "pid": 1,
     "tid": 1,
-    "ts": 15,
+    "ts": 20,
   },
   {
     "args": {
@@ -250,7 +311,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "E",
     "pid": 1,
     "tid": 1,
-    "ts": 16,
+    "ts": 21,
   },
   {
     "args": {
@@ -263,7 +324,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "E",
     "pid": 1,
     "tid": 1,
-    "ts": 17,
+    "ts": 22,
   },
   {
     "args": {
@@ -275,7 +336,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "i",
     "pid": 1,
     "tid": 1,
-    "ts": 18,
+    "ts": 23,
   },
   {
     "args": {
@@ -286,7 +347,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "E",
     "pid": 1,
     "tid": 1,
-    "ts": 19,
+    "ts": 24,
   },
   {
     "args": {
@@ -297,7 +358,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "E",
     "pid": 1,
     "tid": 1,
-    "ts": 20,
+    "ts": 25,
   },
   {
     "args": {
@@ -309,7 +370,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "B",
     "pid": 1,
     "tid": 1,
-    "ts": 21,
+    "ts": 26,
   },
   {
     "args": {
@@ -320,7 +381,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "E",
     "pid": 1,
     "tid": 1,
-    "ts": 22,
+    "ts": 27,
   },
   {
     "args": {
@@ -332,7 +393,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "E",
     "pid": 1,
     "tid": 1,
-    "ts": 23,
+    "ts": 28,
   },
   {
     "args": {
@@ -346,7 +407,7 @@ exports[`Timeline integration test should deterministically produce a timeline a
     "ph": "E",
     "pid": 0,
     "tid": 0,
-    "ts": 24,
+    "ts": 29,
   },
   {
     "args": {
@@ -362,7 +423,7 @@ Detox CLI is going to restart the test runner with those files...
     "ph": "i",
     "pid": 0,
     "tid": 0,
-    "ts": 25,
+    "ts": 30,
   },
   {
     "args": {
@@ -375,7 +436,7 @@ Detox CLI is going to restart the test runner with those files...
     "ph": "B",
     "pid": 0,
     "tid": 0,
-    "ts": 26,
+    "ts": 31,
   },
   {
     "args": {
@@ -386,8 +447,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "integration/e2e/flaky.test.js",
     "ph": "B",
     "pid": 2,
-    "tid": 2,
-    "ts": 27,
+    "tid": 3,
+    "ts": 32,
   },
   {
     "args": {
@@ -398,8 +459,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "set up environment",
     "ph": "B",
     "pid": 2,
-    "tid": 2,
-    "ts": 28,
+    "tid": 3,
+    "ts": 33,
   },
   {
     "args": {
@@ -410,8 +471,8 @@ Detox CLI is going to restart the test runner with those files...
     "cat": "lifecycle,jest-environment",
     "ph": "E",
     "pid": 2,
-    "tid": 2,
-    "ts": 29,
+    "tid": 3,
+    "ts": 34,
   },
   {
     "args": {
@@ -422,8 +483,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "flaky.test.js is assigned to Stub #StubDevice#1",
     "ph": "i",
     "pid": 2,
-    "tid": 2,
-    "ts": 30,
+    "tid": 3,
+    "ts": 35,
   },
   {
     "args": {
@@ -434,8 +495,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "run the tests",
     "ph": "B",
     "pid": 2,
-    "tid": 2,
-    "ts": 31,
+    "tid": 3,
+    "ts": 36,
   },
   {
     "args": {
@@ -446,8 +507,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "Flaky",
     "ph": "B",
     "pid": 2,
-    "tid": 2,
-    "ts": 32,
+    "tid": 3,
+    "ts": 37,
   },
   {
     "args": {
@@ -462,8 +523,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "should have welcome screen",
     "ph": "B",
     "pid": 2,
-    "tid": 2,
-    "ts": 33,
+    "tid": 3,
+    "ts": 38,
   },
   {
     "args": {
@@ -474,8 +535,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "Flaky: should have welcome screen",
     "ph": "i",
     "pid": 2,
-    "tid": 2,
-    "ts": 34,
+    "tid": 3,
+    "ts": 39,
   },
   {
     "args": {
@@ -507,8 +568,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "beforeEach",
     "ph": "B",
     "pid": 2,
-    "tid": 2,
-    "ts": 35,
+    "tid": 3,
+    "ts": 40,
   },
   {
     "args": {
@@ -519,14 +580,13 @@ Detox CLI is going to restart the test runner with those files...
     "cat": "lifecycle,jest-environment",
     "ph": "E",
     "pid": 2,
-    "tid": 2,
-    "ts": 36,
+    "tid": 3,
+    "ts": 41,
   },
   {
     "args": {
       "functionCode": "async () => {
-    await device.reloadReactNative();
-    await detox.traceCall('Navigate to sanity', element(by.text('Sanity')).tap());
+    await log.trace.complete('Navigate to sanity', navigateToSanity);
   }",
       "level": 10,
       "v": 0,
@@ -535,8 +595,68 @@ Detox CLI is going to restart the test runner with those files...
     "name": "beforeEach",
     "ph": "B",
     "pid": 2,
-    "tid": 2,
-    "ts": 37,
+    "tid": 3,
+    "ts": 42,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "name": "Navigate to sanity",
+    "ph": "B",
+    "pid": 2,
+    "tid": 4,
+    "ts": 43,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "name": "Reloading app",
+    "ph": "i",
+    "pid": 2,
+    "tid": 4,
+    "ts": 44,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "name": "Tap on Sanity",
+    "ph": "i",
+    "pid": 2,
+    "tid": 4,
+    "ts": 45,
+  },
+  {
+    "args": {
+      "level": 10,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "name": "I am on Sanity screen",
+    "ph": "i",
+    "pid": 2,
+    "tid": 4,
+    "ts": 46,
+  },
+  {
+    "args": {
+      "level": 10,
+      "success": true,
+      "v": 0,
+    },
+    "cat": "user,lifecycle",
+    "ph": "E",
+    "pid": 2,
+    "tid": 4,
+    "ts": 47,
   },
   {
     "args": {
@@ -547,17 +667,19 @@ Detox CLI is going to restart the test runner with those files...
     "cat": "lifecycle,jest-environment",
     "ph": "E",
     "pid": 2,
-    "tid": 2,
-    "ts": 38,
+    "tid": 3,
+    "ts": 48,
   },
   {
     "args": {
       "functionCode": "async () => {
     try {
       detox.trace.startSection('Asserting various texts');
-      await expect(element(by.text('Welcome'))).toBeVisible();
-      await expect(element(by.text('Say Hello'))).toBeVisible();
-      await expect(element(by.text('Say World'))).toBeVisible();
+      await detox.traceCall('by.text()', async () => {
+        await expect(element(by.text('Welcome'))).toBeVisible();
+        await expect(element(by.text('Say Hello'))).toBeVisible();
+        await expect(element(by.text('Say World'))).toBeVisible();
+      });
     } finally {
       detox.trace.endSection('Asserting various texts');
     }
@@ -572,8 +694,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "test_fn",
     "ph": "B",
     "pid": 2,
-    "tid": 2,
-    "ts": 39,
+    "tid": 3,
+    "ts": 49,
   },
   {
     "args": {
@@ -584,8 +706,8 @@ Detox CLI is going to restart the test runner with those files...
     "cat": "lifecycle,jest-environment",
     "ph": "E",
     "pid": 2,
-    "tid": 2,
-    "ts": 40,
+    "tid": 3,
+    "ts": 50,
   },
   {
     "args": {
@@ -596,8 +718,8 @@ Detox CLI is going to restart the test runner with those files...
     "cat": "lifecycle,jest-environment",
     "ph": "E",
     "pid": 2,
-    "tid": 2,
-    "ts": 41,
+    "tid": 3,
+    "ts": 51,
   },
   {
     "args": {
@@ -608,8 +730,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "Flaky: should have welcome screen [OK]",
     "ph": "i",
     "pid": 2,
-    "tid": 2,
-    "ts": 42,
+    "tid": 3,
+    "ts": 52,
   },
   {
     "args": {
@@ -619,8 +741,8 @@ Detox CLI is going to restart the test runner with those files...
     "cat": "lifecycle,jest-environment",
     "ph": "E",
     "pid": 2,
-    "tid": 2,
-    "ts": 43,
+    "tid": 3,
+    "ts": 53,
   },
   {
     "args": {
@@ -630,8 +752,8 @@ Detox CLI is going to restart the test runner with those files...
     "cat": "lifecycle,jest-environment",
     "ph": "E",
     "pid": 2,
-    "tid": 2,
-    "ts": 44,
+    "tid": 3,
+    "ts": 54,
   },
   {
     "args": {
@@ -642,8 +764,8 @@ Detox CLI is going to restart the test runner with those files...
     "name": "tear down environment",
     "ph": "B",
     "pid": 2,
-    "tid": 2,
-    "ts": 45,
+    "tid": 3,
+    "ts": 55,
   },
   {
     "args": {
@@ -653,8 +775,8 @@ Detox CLI is going to restart the test runner with those files...
     "cat": "lifecycle,jest-environment",
     "ph": "E",
     "pid": 2,
-    "tid": 2,
-    "ts": 46,
+    "tid": 3,
+    "ts": 56,
   },
   {
     "args": {
@@ -665,8 +787,8 @@ Detox CLI is going to restart the test runner with those files...
     "cat": "lifecycle,jest-environment",
     "ph": "E",
     "pid": 2,
-    "tid": 2,
-    "ts": 47,
+    "tid": 3,
+    "ts": 57,
   },
   {
     "args": {
@@ -678,7 +800,7 @@ Detox CLI is going to restart the test runner with those files...
     "ph": "E",
     "pid": 0,
     "tid": 0,
-    "ts": 48,
+    "ts": 58,
   },
   {
     "args": {
@@ -689,7 +811,7 @@ Detox CLI is going to restart the test runner with those files...
     "ph": "E",
     "pid": 0,
     "tid": 0,
-    "ts": 49,
+    "ts": 59,
   },
 ]
 `;
diff --git a/detox/test/integration/e2e/flaky.test.js b/detox/test/integration/e2e/flaky.test.js
index efb336684..90bec721d 100644
--- a/detox/test/integration/e2e/flaky.test.js
+++ b/detox/test/integration/e2e/flaky.test.js
@@ -1,17 +1,19 @@
 const { session } = require('detox/internals');
+const log = detox.log.child({ cat: 'lifecycle' });
 
 describe('Flaky', () => {
   beforeEach(async () => {
-    await device.reloadReactNative();
-    await detox.traceCall('Navigate to sanity', element(by.text('Sanity')).tap());
+    await log.trace.complete('Navigate to sanity', navigateToSanity);
   });
 
   it('should have welcome screen', async () => {
     try {
       detox.trace.startSection('Asserting various texts');
-      await expect(element(by.text('Welcome'))).toBeVisible();
-      await expect(element(by.text('Say Hello'))).toBeVisible();
-      await expect(element(by.text('Say World'))).toBeVisible();
+      await detox.traceCall('by.text()', async () => {
+        await expect(element(by.text('Welcome'))).toBeVisible();
+        await expect(element(by.text('Say Hello'))).toBeVisible();
+        await expect(element(by.text('Say World'))).toBeVisible();
+      });
     } finally {
       detox.trace.endSection('Asserting various texts');
     }
@@ -21,4 +23,11 @@ describe('Flaky', () => {
     }
   });
 
+  async function navigateToSanity() {
+    log.trace('Reloading app');
+    await device.reloadReactNative();
+    log.trace('Tap on Sanity');
+    await element(by.text('Sanity')).tap();
+    log.trace('I am on Sanity screen');
+  }
 });
diff --git a/detox/test/integration/timeline-artifact.test.js b/detox/test/integration/timeline-artifact.test.js
index 22ca32145..9ad8e72a3 100644
--- a/detox/test/integration/timeline-artifact.test.js
+++ b/detox/test/integration/timeline-artifact.test.js
@@ -10,12 +10,6 @@ const TMPDIR = path.dirname(tempfile());
 const readFile = promisify(fs.readFile);
 const remove = promisify(fs.remove);
 
-// TODO: investigate why the test fails
-if (require('os').platform() === 'win32') {
-  // eslint-disable-next-line no-global-assign
-  describe = describe.skip;
-}
-
 describe('Timeline integration test', () => {
   const artifactsDirectory = 'integration_artifacts/'
   const timelineArtifactFilename = 'detox.trace.json';
@@ -90,7 +84,7 @@ function sanitizeEvent(e, ts) {
   if (ts === 0) {
     this.sessionId = r.args.data.id;
     this.cwd = r.args.cwd;
-    this.name = r.name.replace(/^.*(?=jest)/, '');
+    r.name = r.name.replace(/^[^ ]*/, 'detox');
   }
 
   r.pid = (this.pid.has(e.pid) ? this.pid : this.pid.set(e.pid, this.pid.size)).get(e.pid);
@@ -103,6 +97,7 @@ function sanitizeEvent(e, ts) {
       .replace(this.sessionId, '$SESSION_ID')
       .replace(this.cwd, '$CWD')
       .replace(this.cwd.replace(/\\/g, '/'), '$CWD')
+      .replace(/\\/g, '/')
       .replace(/DETOX_[A-Z_]+=\S*\s*/g, '');
   }
 
diff --git a/detox/test/package.json b/detox/test/package.json
index e628964b1..d07c484b8 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "20.1.1",
+  "version": "20.1.2",
   "private": true,
   "engines": {
     "node": ">=14.5.0"
@@ -33,6 +33,7 @@
   "dependencies": {
     "@react-native-async-storage/async-storage": "^1.17.3",
     "@react-native-community/checkbox": "0.5.12",
+    "@react-native-community/datetimepicker": "^6.7.1",
     "@react-native-community/geolocation": "^2.0.2",
     "@react-native-community/slider": "4.2.4",
     "@react-native-picker/picker": "^2.1.0",
@@ -54,7 +55,7 @@
     "@typescript-eslint/eslint-plugin": "^5.4.0",
     "@typescript-eslint/parser": "^5.4.0",
     "cross-env": "^7.0.3",
-    "detox": "^20.1.1",
+    "detox": "^20.1.2",
     "eslint": "^7.32.0",
     "eslint-plugin-unicorn": "^39.0.0",
     "execa": "^5.1.1",
diff --git a/detox/test/src/Screens/DatePickerScreen.js b/detox/test/src/Screens/DatePickerScreen.js
index 829888c43..dbdd05b7a 100644
--- a/detox/test/src/Screens/DatePickerScreen.js
+++ b/detox/test/src/Screens/DatePickerScreen.js
@@ -1,26 +1,46 @@
+import upperFirst from 'lodash/upperFirst';
 import moment from 'moment';
 import React, { Component } from 'react';
-import { Text, View, StyleSheet, DatePickerIOS } from 'react-native';
+import { Text, View, StyleSheet, Button, Platform } from 'react-native';
+import { default as DatePicker } from "@react-native-community/datetimepicker";
+
+const shouldHideDatePicker = Platform.OS === 'android';
 
 export default class DatePickerScreen extends Component {
   constructor(props) {
     super(props);
 
     this.state = {
-      chosenDate: new Date()
+      chosenDate: new Date(),
+      datePickerVisible: !shouldHideDatePicker,
+      datePickerDisplay: DatePickerScreen.MODES[0],
     };
-
-    this.setDate = this.setDate.bind(this);
   }
 
-  setDate(newDate) {
+  _toggleDatePicker = () => {
+    const currentIndex = DatePickerScreen.MODES.indexOf(this.state.datePickerDisplay);
     this.setState({
-      chosenDate: newDate
+      datePickerDisplay: DatePickerScreen.MODES[(currentIndex + 1) % DatePickerScreen.MODES.length],
     });
   }
 
+  _openDatePicker = () => {
+    this.setState({ datePickerVisible: true });
+  }
+
+  setDate = (e, newDate) => {
+    this.setState({
+      chosenDate: newDate,
+      datePickerVisible: !shouldHideDatePicker,
+    });
+  };
+
   getTimeLocal() {
-    return moment(this.state.chosenDate).format("hh:mm");
+    return moment(this.state.chosenDate).format("h:mm A");
+  }
+
+  getDateLocal() {
+    return moment(this.state.chosenDate).format("MMM Do, YYYY");
   }
 
   getTimeUTC() {
@@ -40,25 +60,54 @@ export default class DatePickerScreen extends Component {
         <Text style={styles.dateText} testID='utcTimeLabel'>
           {"Time (UTC): " + this.getTimeUTC()}
         </Text>
+        <Text style={styles.dateText} testID="localDateLabel">
+          {"Date (Local): " + this.getDateLocal()}
+        </Text>
         <Text style={styles.dateText} testID='localTimeLabel'>
-          {"Time: " + this.getTimeLocal()}
+          {"Time (Local): " + this.getTimeLocal()}
         </Text>
-        <DatePickerIOS testID="datePicker" style={styles.datePicker} date={this.state.chosenDate} onDateChange={this.setDate} />
+        <View style={styles.container}>
+          <Button
+            title={`${upperFirst(this.state.datePickerDisplay)} Date Picker`}
+            testID='toggleDatePicker'
+            onPress={this._toggleDatePicker}
+          />
+        </View>
+        {shouldHideDatePicker && (
+          <View style={styles.container}>
+            <Button
+              title="Open Date Picker"
+              testID='openDatePicker'
+              onPress={this._openDatePicker}
+            />
+          </View>
+        )}
+        {this.state.datePickerVisible && (
+          <DatePicker
+            testID="datePicker"
+            mode='date'
+            // @ts-ignore
+            display={this.state.datePickerDisplay}
+            value={this.state.chosenDate}
+            onChange={this.setDate} />
+          )}
       </View>
     );
   }
+
+  static MODES = Platform.OS === 'ios'
+    ? ['compact', 'inline', 'spinner']
+    : ['calendar', 'spinner'];
 }
 
 const styles = StyleSheet.create({
   container: {
     flex: 1,
-    paddingTop: 20,
-    justifyContent: 'center',
-    alignItems: 'center'
-  },
-  datePicker: {
-    width:'100%',
-    height:200
+    paddingTop: 100,
+    paddingBottom: 50,
+    flexDirection: 'column',
+    alignItems: 'center',
+    justifyContent: 'flex-start',
   },
   dateText: {
     textAlign:'center'
diff --git a/detox/test/src/app.js b/detox/test/src/app.js
index 21833aae0..775396279 100644
--- a/detox/test/src/app.js
+++ b/detox/test/src/app.js
@@ -120,7 +120,7 @@ export default class example extends Component {
         {this.renderScreenButton('Device', Screens.DeviceScreen)}
         {isIos && this.renderScreenButton('Overlay', Screens.OverlayScreen)}
         {this.renderScreenButton('Location', Screens.LocationScreen)}
-        {isIos && this.renderScreenButton('DatePicker', Screens.DatePickerScreen)}
+        {this.renderScreenButton('DatePicker', Screens.DatePickerScreen)}
         {isIos && this.renderScreenButton('Picker', Screens.PickerViewScreen)}
         {isAndroid && this.renderScreenButton('WebView', Screens.WebViewScreen)}
         {this.renderScreenButton('Attributes', Screens.AttributesScreen)}