Repository: wix/Detox
Pull request #2242 (2020-07-29 18:31:47)

feat(jest): support artifact collection for multiple retries with jest.retryTimes(n)
- [x] This change has been discussed in issue #2073 and the solution has been agreed upon with maintainers.

---

**Description:**

Adds support `jest.retryTimes(n)` when used with `jest-circus`.

For example, if a `Flaky test` fails 2 times and then passes on the third, the artifacts will be saved like this:

```
artifacts/<auto-generated-dir>/✗ Flaky test/...
artifacts/<auto-generated-dir>/✗ Flaky test (2)/...
artifacts/<auto-generated-dir>/✓ Flaky test (3)/...
```

This pull request has unit and E2E tests for the feature.





Patch:
diff --git a/README.md b/README.md
index 14a5d80e0..aaad5dc11 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ The following React Native versions have been tested:
 | iOS    | Android                                                      |
 | ------ | ------------------------------------------------------------ |
 | <=0.62 | <=0.56 - Full support                                        |
-|        | >=0.57 <=0.62 - Visibility edge-case: see this [RN issue](https://github.com/facebook/react-native/issues/23870)* |
+|        | >=0.57 <=0.63 - Visibility edge-case: see this [RN issue](https://github.com/facebook/react-native/issues/23870)* |
 
 Future versions are most likely supported, but have not been tested yet. Please open issues if you find specific issues with newer React Native versions.
 
diff --git a/detox/android/build.gradle b/detox/android/build.gradle
index 2c512ce4b..b71b6906e 100644
--- a/detox/android/build.gradle
+++ b/detox/android/build.gradle
@@ -15,7 +15,7 @@ buildscript {
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.5.2'
+        classpath 'com.android.tools.build:gradle:3.5.3'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
         classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion"
 
diff --git a/detox/android/detox/build.gradle b/detox/android/detox/build.gradle
index 71c8e8ac6..41afb0a45 100644
--- a/detox/android/detox/build.gradle
+++ b/detox/android/detox/build.gradle
@@ -3,10 +3,10 @@ apply plugin: 'kotlin-android'
 apply plugin: 'kotlin-android-extensions'
 
 def _ext = rootProject.ext
-def _compileSdkVersion = _ext.has('compileSdkVersion') ? _ext.compileSdkVersion : 28
-def _buildToolsVersion = _ext.has('buildToolsVersion') ? _ext.buildToolsVersion : '28.0.3'
+def _compileSdkVersion = _ext.has('compileSdkVersion') ? _ext.compileSdkVersion : 29
+def _targetSdkVersion = _ext.has('targetSdkVersion') ? _ext.targetSdkVersion : 29
+def _buildToolsVersion = _ext.has('buildToolsVersion') ? _ext.buildToolsVersion : '29.0.3'
 def _minSdkVersion = _ext.has('minSdkVersion') ? _ext.minSdkVersion : 18
-def _targetSdkVersion = _ext.has('targetSdkVersion') ? _ext.targetSdkVersion : 28
 def _kotlinVersion = _ext.has('detoxKotlinVersion') ? _ext.detoxKotlinVersion : '1.2.0'
 def _kotlinStdlib = _ext.has('detoxKotlinStdlib') ? _ext.detoxKotlinStdlib : 'kotlin-stdlib-jdk8'
 
@@ -91,7 +91,7 @@ dependencies {
 
 // Third-party deps.
 dependencies {
-    implementation 'org.apache.commons:commons-lang3:3.7' // Needed by invoke.
+    implementation 'org.apache.commons:commons-lang3:3.7' // Needed by invoke. Warning: Upgrading to newer versions is not seamless.
     implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'
 }
 
@@ -100,8 +100,8 @@ dependencies {
     // noinspection GradleDynamicVersion
     testImplementation 'com.facebook.react:react-native:+'
     testImplementation 'org.json:json:20140107'
-    testImplementation 'junit:junit:4.12'
-    testImplementation 'org.assertj:assertj-core:3.8.0'
+    testImplementation 'junit:junit:4.13'
+    testImplementation 'org.assertj:assertj-core:3.16.1'
     testImplementation "org.jetbrains.kotlin:kotlin-test:$_kotlinVersion"
     testImplementation 'org.apache.commons:commons-io:1.3.2'
     testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
@@ -126,8 +126,8 @@ if (rootProject.hasProperty('isOfficialDetoxLib') ||
     }
 
     dependencies {
-        testImplementation 'org.spekframework.spek2:spek-dsl-jvm:2.0.9'
-        testImplementation 'org.spekframework.spek2:spek-runner-junit5:2.0.9'
+        testImplementation 'org.spekframework.spek2:spek-dsl-jvm:2.0.12'
+        testImplementation 'org.spekframework.spek2:spek-runner-junit5:2.0.12'
         testImplementation "org.jetbrains.kotlin:kotlin-reflect:$_kotlinVersion"
     }
 }
diff --git a/detox/android/detox/proguard-rules.pro b/detox/android/detox/proguard-rules.pro
index 761cfe016..89b923c83 100644
--- a/detox/android/detox/proguard-rules.pro
+++ b/detox/android/detox/proguard-rules.pro
@@ -12,3 +12,14 @@
 -dontnote org.joor.**
 -dontnote org.hamcrest.**
 -dontnote com.facebook.**
+
+# Detox dynamic access to declared methods of RN's timing module
+
+-keep class com.facebook.react.modules.core.TimingModule { *; } # RN >= .63
+-keep class com.facebook.react.modules.core.Timing { *; } # RN <= .62
+-dontnote com.wix.detox.reactnative.idlingresources.timers.**
+
+# Detox profiler (optional)
+
+-keep class com.wix.detoxprofiler.** { *; }
+-dontnote  com.wix.detox.instruments.reflected.**
diff --git a/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeExtension.kt b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeExtension.kt
index 8885bd1d8..1e9452697 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeExtension.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeExtension.kt
@@ -106,7 +106,7 @@ object ReactNativeExtension {
     }
 
     private fun hackRN50OrHigherWaitForReady() {
-        if (ReactNativeInfo.rnVersion().minor >= 50) {
+        if (ReactNativeInfo.rnVersion().minor in 50..62) {
             try {
                 //TODO- Temp hack to make Detox usable for RN>=50 till we find a better sync solution.
                 Thread.sleep(1000)
diff --git a/detox/android/detox/src/main/java/com/wix/detox/reactnative/idlingresources/timers/DefaultIdleInterrogationStrategy.kt b/detox/android/detox/src/main/java/com/wix/detox/reactnative/idlingresources/timers/DefaultIdleInterrogationStrategy.kt
index a1c24a5d0..31b1c63af 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/reactnative/idlingresources/timers/DefaultIdleInterrogationStrategy.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/reactnative/idlingresources/timers/DefaultIdleInterrogationStrategy.kt
@@ -2,14 +2,12 @@
 
 package com.wix.detox.reactnative.idlingresources.timers
 
-import android.util.Log
 import com.facebook.react.bridge.NativeModule
 import com.facebook.react.bridge.ReactContext
 import com.wix.detox.common.RNDropSupportTodo
 import org.joor.Reflect
 import java.util.*
 
-private const val LOG_TAG = "DefaultTimingStrategy"
 private const val BUSY_WINDOW_THRESHOLD = 1500
 
 private class TimerReflected(timer: Any) {
@@ -74,7 +72,6 @@ class DefaultIdleInterrogationStrategy
             try {
                 val timingClass: Class<NativeModule> = Class.forName("com.facebook.react.modules.core.TimingModule") as Class<NativeModule>
                 if (!reactContext.hasNativeModule(timingClass)) {
-                    Log.d(LOG_TAG, "create(): new class - no native module")
                     return null
                 }
 
@@ -82,7 +79,6 @@ class DefaultIdleInterrogationStrategy
                 val timersManager = Reflect.on(timingModule).get<Any>("mJavaTimerManager")
                 return DefaultIdleInterrogationStrategy(timersManager)
             } catch (ex: Exception) {
-                Log.d(LOG_TAG, "create(): no new timing-module class implementation", ex)
             }
 
             // RN < 0.62
@@ -90,7 +86,6 @@ class DefaultIdleInterrogationStrategy
                 val timingClass: Class<NativeModule> = Class.forName("com.facebook.react.modules.core.Timing") as Class<NativeModule>
                 return DefaultIdleInterrogationStrategy(reactContext.getNativeModule(timingClass))
             } catch (ex: Exception) {
-                Log.d(LOG_TAG, "create(): no old timing-module class implementation", ex)
             }
 
             return null
diff --git a/detox/android/detox/src/main/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategy.kt b/detox/android/detox/src/main/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategy.kt
index 7923f3bb1..af3e33279 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategy.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/reactnative/idlingresources/timers/DelegatedIdleInterrogationStrategy.kt
@@ -1,12 +1,10 @@
 package com.wix.detox.reactnative.idlingresources.timers
 
-import android.util.Log
 import com.facebook.react.bridge.NativeModule
 import com.facebook.react.bridge.ReactContext
 import com.wix.detox.common.RNDropSupportTodo
 import org.joor.Reflect
 
-private const val LOG_TAG = "DelegatedTimingStrategy"
 private const val BUSY_WINDOW_THRESHOLD = 1500L
 
 private class RN62TimingModuleReflected(private val timingModule: NativeModule) {
@@ -19,9 +17,9 @@ private class RN62TimingModuleReflected(private val timingModule: NativeModule)
  * of RN v0.62 (followed by a previous refactor and rename of the class).
  */
 @RNDropSupportTodo(62, """
-    When min RN version supported by Detox is 0.62.x (or higher),
-    | can (and should) remove any usage of reflection here. That
-    | includes the unit test's stub being used for that reason in particular.
+    When min RN version supported by Detox is 0.62.x (or higher), can (and
+    should) remove any usage of reflection here.
+    That includes the unit test's stub being used for that reason in particular.
     """)
 class DelegatedIdleInterrogationStrategy(timingModule: NativeModule): IdleInterrogationStrategy {
     private val timingModuleReflected = RN62TimingModuleReflected(timingModule)
@@ -34,19 +32,16 @@ class DelegatedIdleInterrogationStrategy(timingModule: NativeModule): IdleInterr
             try {
                 moduleClass = Class.forName("com.facebook.react.modules.core.TimingModule") as Class<NativeModule>
             } catch (ex: Exception) {
-                Log.d(LOG_TAG, "create(): no class", ex)
                 return null
             }
 
             if (!reactContext.hasNativeModule(moduleClass)) {
-                Log.d(LOG_TAG, "create(): no native module")
                 return null
             }
 
             try {
                 moduleClass.getDeclaredMethod("hasActiveTimersInRange", Long::class.java)
             } catch (ex: Exception) {
-                Log.d(LOG_TAG, "create(): no dedicated idle-interrogation method", ex)
                 return null
             }
 
diff --git a/detox/android/gradle.properties b/detox/android/gradle.properties
index 1af85ec65..aac7c9b46 100644
--- a/detox/android/gradle.properties
+++ b/detox/android/gradle.properties
@@ -15,6 +15,3 @@ org.gradle.jvmargs=-Xmx1536m
 # This option should only be used with decoupled projects. More details, visit
 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
 # org.gradle.parallel=true
-
-# Not ready for this yet
-android.enableR8=false
diff --git a/detox/android/gradle/wrapper/gradle-wrapper.properties b/detox/android/gradle/wrapper/gradle-wrapper.properties
index 66ab81bfa..372e68a2f 100644
--- a/detox/android/gradle/wrapper/gradle-wrapper.properties
+++ b/detox/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
diff --git a/detox/package.json b/detox/package.json
index 9e4ec9057..2189d4c84 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "17.3.2",
+  "version": "17.3.3",
   "bin": {
     "detox": "local-cli/cli.js"
   },
diff --git a/detox/runners/jest-circus/listeners/DetoxCoreListener.js b/detox/runners/jest-circus/listeners/DetoxCoreListener.js
index e8384380e..9a1e0d165 100644
--- a/detox/runners/jest-circus/listeners/DetoxCoreListener.js
+++ b/detox/runners/jest-circus/listeners/DetoxCoreListener.js
@@ -64,6 +64,7 @@ class DetoxCoreListener {
       title: test.name,
       fullName: getFullTestName(test),
       status: 'running',
+      invocations: test.invocations,
     });
   }
 
@@ -73,6 +74,7 @@ class DetoxCoreListener {
         title: test.name,
         fullName: getFullTestName(test),
         status: test.errors.length ? 'failed' : 'passed',
+        invocations: test.invocations,
         timedOut: hasTimedOut(test)
       });
     }
diff --git a/detox/src/artifacts/utils/ArtifactPathBuilder.js b/detox/src/artifacts/utils/ArtifactPathBuilder.js
index 3da60a5b3..cad5e2025 100644
--- a/detox/src/artifacts/utils/ArtifactPathBuilder.js
+++ b/detox/src/artifacts/utils/ArtifactPathBuilder.js
@@ -32,8 +32,9 @@ class ArtifactPathBuilder {
   }
 
   _constructDirectoryNameForCurrentRunningTest(testSummary) {
-    const testDirectoryPrefix = this._buildTestDirectoryPrefix(testSummary);
-    const testArtifactsDirname = constructSafeFilename(testDirectoryPrefix, testSummary.fullName);
+    const prefix = this._buildTestDirectoryPrefix(testSummary);
+    const suffix = testSummary.invocations > 1 ? ` (${testSummary.invocations})` : '';
+    const testArtifactsDirname = constructSafeFilename(prefix, testSummary.fullName, suffix);
 
     return testArtifactsDirname;
   }
@@ -43,6 +44,8 @@ class ArtifactPathBuilder {
   }
 
   _getStatusSign(testSummary) {
+    const invocations = testSummary.invocations || 1;
+
     switch (testSummary.status) {
       case 'passed': return '✓ ';
       case 'failed': return '✗ ';
diff --git a/docs/APIRef.Artifacts.md b/docs/APIRef.Artifacts.md
index b49f4e869..de104c850 100644
--- a/docs/APIRef.Artifacts.md
+++ b/docs/APIRef.Artifacts.md
@@ -51,10 +51,74 @@ The `artifacts` object has the following properties:
 | Property    | Example values                  | Default value | Description |
 |-------------|---------------------------------|---------------|-------------|
 | rootDir     | `".artifacts/"`                 | `./artifacts` | A directory, where all the recorded artifacts will be placed in. Please note that there is a trailing slash convention [described above](#slash-convention). |
-| pathBuilder | `"./e2e/config/pathbuilder.js"` | `undefined`   | Path to a module that implements `PathBuilder` interface[<sup>\[a\]</sup>](#pathBuilder) |
+| pathBuilder | `"./e2e/config/pathbuilder.js"` | `undefined`   | Path to a module that exports a custom `PathBuilder` [<sup>\[a\]</sup>](#pathBuilder) |
 | plugins     | `{ ... }`                       | ... see below | ... see below |
 
-<a id=pathBuilder><sup>a</sup> PathBuilder</a> should be an object with a method `buildPathForTestArtifact` or a class. The `buildPathForTestArtifact` method has a signature: `(artifactName: string, testSummary?: { title: string; fullName: string; status: 'running' | 'passed' | 'failed' }) => string`, where it accepts a suggested artifact name (e.g., `testDone.png`, `device.log`), a current test summary with its name and status, and it is expected to return a full path to the custom artifact location. If it is a class, its constructor also accepts `{ rootDir }` configuration object. Search for `ArtifactPathBuilder.js` in Detox source code for a technical reference.
+<a id=pathBuilder><sup>a</sup><code>PathBuilder</code></a> should be either an _object_ with a method `buildPathForTestArtifact` or a _class_ &mdash; see the corresponding interfaces below:
+
+```typescript
+interface PathBuilder {
+    buildPathForTestArtifact(artifactPath: string, testSummary?: TestSummary): string;
+}
+
+interface PathBuilderClass {
+    new(opts: { rootDir: string; }): PathBuilder;
+}
+```
+
+As one can see, if a custom implementation of `PathBuilder` exports a class instead of an object, then the class constructor can also get and save `rootDir` location:
+
+```js
+class MyPathBuilder {
+  constructor({ rootDir }) {
+    this._rootDir = rootDir;
+  }
+
+  buildPathForTestArtifact(artifactName, testSummary) {
+    /* ... use this._rootDir ... */
+  }
+}
+
+module.exports = MyPathBuilder;
+```
+
+Its main method, `buildPathForTestArtifact` should return a full path to the custom artifact location, when called with a suggested artifact name (e.g., `testDone.png`, `device.log`) and the current `TestSummary`, where `TestSummary` is:
+
+```typescript
+interface TestSummary {
+    /**
+     * Name of the current test, e.g., for:
+     * describe('that screen', () =>
+     *   it('should have a menu', () =>
+     * The expected string would be: "should have a menu".
+     */
+    title: string;
+    /**
+     * Full name of the current test, usually preceeded by a suite name, e.g.:
+     * describe('that screen', () =>
+     *   it('should have a menu', () =>
+     * The expected string would be: "that screen should have a menu".
+     */
+    fullName: string;
+    /**
+     * Status of the current test. Free-form strings are not allowed.
+     */
+    status: 'running' | 'passed' | 'failed';
+    /**
+     * Clarifies the reason for why the test has failed.
+     * Expected to coincide only with status: 'failed'.
+     */
+    timedOut?: boolean;
+    /**
+     * If the test runner is capable of retrying failed tests, then
+     * this property indicates for which time this test is running.
+     * When the property is undefined, its value is considered to be 1.
+     * */
+    invocations?: number;
+}
+```
+
+For more technical details, search for `ArtifactPathBuilder.js` in Detox source code.
 
 The further subsections describe the `plugins` object structure.
 
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index 293406b5e..7e875575b 100644
--- a/examples/demo-native-android/package.json
+++ b/examples/demo-native-android/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-demo-native-android",
-  "version": "17.3.2",
+  "version": "17.3.3",
   "private": true,
   "scripts": {
     "packager": "react-native start",
@@ -8,7 +8,7 @@
     "e2e": "mocha e2e --opts ./e2e/mocha.opts"
   },
   "devDependencies": {
-    "detox": "^17.3.2",
+    "detox": "^17.3.3",
     "mocha": "^6.1.3"
   },
   "detox": {}
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index 8d7dfd29c..354ef01b2 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": "17.3.2",
+  "version": "17.3.3",
   "private": true,
   "devDependencies": {
-    "detox": "^17.3.2",
+    "detox": "^17.3.3",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index 317fd634d..0ffb703d6 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -1,12 +1,12 @@
 {
   "name": "demo-plugin",
-  "version": "17.3.2",
+  "version": "17.3.3",
   "private": true,
   "scripts": {
     "test:plugin": "detox test --configuration plugin -l verbose"
   },
   "devDependencies": {
-    "detox": "^17.3.2",
+    "detox": "^17.3.3",
     "jest": "26.x.x"
   },
   "detox": {
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index a8933b6f5..e9b9dd54e 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": "17.3.2",
+  "version": "17.3.3",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^17.3.2",
+    "detox": "^17.3.3",
     "mocha": "6.x.x"
   },
   "detox": {
diff --git a/examples/demo-react-native-jest/package.json b/examples/demo-react-native-jest/package.json
index 2b774ca61..bc2021108 100644
--- a/examples/demo-react-native-jest/package.json
+++ b/examples/demo-react-native-jest/package.json
@@ -1,6 +1,6 @@
 {
   "name": "demo-react-native-jest",
-  "version": "17.3.2",
+  "version": "17.3.3",
   "private": true,
   "scripts": {
     "test:ios-release": "detox test --configuration ios.sim.release -l verbose",
@@ -9,7 +9,7 @@
     "test:android-release-ci": "detox test --configuration android.emu.release -l verbose --workers 2 --headless --record-logs all --take-screenshots all"
   },
   "devDependencies": {
-    "detox": "^17.3.2",
+    "detox": "^17.3.3",
     "jest": "26.x.x",
     "jest-circus": "26.x.x",
     "sanitize-filename": "^1.6.1"
diff --git a/examples/demo-react-native/android/build.gradle b/examples/demo-react-native/android/build.gradle
index f847c7e7a..88276feb2 100644
--- a/examples/demo-react-native/android/build.gradle
+++ b/examples/demo-react-native/android/build.gradle
@@ -1,7 +1,7 @@
 buildscript {
     ext {
         kotlinVersion = '1.3.41'
-        buildToolsVersion = '29.0.0'
+        buildToolsVersion = '29.0.3'
         compileSdkVersion = 29
         targetSdkVersion = 29
         minSdkVersion = 18
@@ -12,8 +12,8 @@ buildscript {
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.5.2'
-        classpath 'de.undercouch:gradle-download-task:3.4.3'
+        classpath 'com.android.tools.build:gradle:3.5.3'
+        classpath 'de.undercouch:gradle-download-task:4.0.2'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
     }
 }
diff --git a/examples/demo-react-native/android/gradle.properties b/examples/demo-react-native/android/gradle.properties
index d86695a57..9516cf599 100644
--- a/examples/demo-react-native/android/gradle.properties
+++ b/examples/demo-react-native/android/gradle.properties
@@ -16,6 +16,3 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro
 # This option should only be used with decoupled projects. More details, visit
 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
 # org.gradle.parallel=true
-
-# Not ready for this yet
-android.enableR8=false
diff --git a/examples/demo-react-native/android/gradle/wrapper/gradle-wrapper.properties b/examples/demo-react-native/android/gradle/wrapper/gradle-wrapper.properties
index 4aa53223a..ede3b4fa3 100644
--- a/examples/demo-react-native/android/gradle/wrapper/gradle-wrapper.properties
+++ b/examples/demo-react-native/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Sun Apr 05 21:47:34 IDT 2020
+#Wed Jul 29 13:31:38 IDT 2020
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index 585b2aaa9..6fb27810b 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "17.3.2",
+  "version": "17.3.3",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -14,14 +14,15 @@
     "test:android-release-ci": "detox test --configuration android.emu.release -l verbose --headless --record-logs all --take-screenshots all",
     "e2e:ios": "npm run build:ios && npm run test:ios",
     "e2e:android-debug": "npm run build:android-debug && npm run test:android-debug",
-    "e2e:android-release": "npm run build:android-release && npm run test:android-release"
+    "e2e:android-release": "npm run build:android-release && npm run test:android-release",
+    "clean:android": "pushd android && ./gradlew clean && popd"
   },
   "dependencies": {
     "react": "16.11.x",
     "react-native": "0.62.x"
   },
   "devDependencies": {
-    "detox": "^17.3.2",
+    "detox": "^17.3.3",
     "mocha": "^6.1.3"
   }
 }
diff --git a/lerna.json b/lerna.json
index e3fb59c2d..a833775a3 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,7 +13,7 @@
     "generation",
     "."
   ],
-  "version": "17.3.2",
+  "version": "17.3.3",
   "npmClient": "npm",
   "command": {
     "publish": {
diff --git a/package.json b/package.json
index e18a47415..e644e610b 100644
--- a/package.json
+++ b/package.json
@@ -18,5 +18,5 @@
     "semver": "5.x.x",
     "shell-utils": "1.x.x"
   },
-  "version": "17.3.2"
+  "version": "17.3.3"
 }


Test patch:
diff --git a/detox/src/artifacts/utils/ArtifactPathBuilder.test.js b/detox/src/artifacts/utils/ArtifactPathBuilder.test.js
index 1f7a22746..90660dfd8 100644
--- a/detox/src/artifacts/utils/ArtifactPathBuilder.test.js
+++ b/detox/src/artifacts/utils/ArtifactPathBuilder.test.js
@@ -73,6 +73,20 @@ describe(ArtifactPathBuilder, () => {
 
       expect(asPosixPath(artifactPath)).toBe('/tmp/subdir/✗ test/1.log');
     });
+
+    it('should append (2) invocations count for a test that fails for the second time', () => {
+      const testSummary = {title: '', fullName: 'test', status: 'failed', invocations: 2 };
+      const artifactPath = pathBuilder.buildPathForTestArtifact('1.log', testSummary);
+
+      expect(asPosixPath(artifactPath)).toBe('/tmp/subdir/✗ test (2)/1.log');
+    });
+
+    it('should append (3) invocations count for a test that passes for the third time', () => {
+      const testSummary = {title: '', fullName: 'test', status: 'passed', invocations: 3 };
+      const artifactPath = pathBuilder.buildPathForTestArtifact('1.log', testSummary);
+
+      expect(asPosixPath(artifactPath)).toBe('/tmp/subdir/✓ test (3)/1.log');
+    });
   });
 
   function asPosixPath(maybeWin32Path) {
diff --git a/detox/test/android/build.gradle b/detox/test/android/build.gradle
index b94c29ba0..fb0a41397 100644
--- a/detox/test/android/build.gradle
+++ b/detox/test/android/build.gradle
@@ -7,7 +7,7 @@ buildscript {
     ext {
         isOfficialDetoxApp = true
         kotlinVersion = '1.3.41'
-        buildToolsVersion = '29.0.0'
+        buildToolsVersion = '29.0.3'
         compileSdkVersion = 29
         targetSdkVersion = 29
         minSdkVersion = 18
@@ -22,7 +22,7 @@ buildscript {
 
     dependencies {
         classpath "com.android.tools.build:gradle:$androidGradlePluginVersion"
-        classpath 'de.undercouch:gradle-download-task:3.4.3'
+        classpath 'de.undercouch:gradle-download-task:4.0.2'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
 
         // Needed by Spek (https://spekframework.org/setup-android)
diff --git a/detox/test/android/gradle.properties b/detox/test/android/gradle.properties
index a04627452..4bd11de57 100644
--- a/detox/test/android/gradle.properties
+++ b/detox/test/android/gradle.properties
@@ -16,6 +16,3 @@
 # This option should only be used with decoupled projects. More details, visit
 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
 # org.gradle.parallel=true
-
-# Not ready for this yet
-android.enableR8=false
diff --git a/detox/test/android/gradle/wrapper/gradle-wrapper.properties b/detox/test/android/gradle/wrapper/gradle-wrapper.properties
index ab1fa7789..4ac1a408c 100644
--- a/detox/test/android/gradle/wrapper/gradle-wrapper.properties
+++ b/detox/test/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Thu Apr 02 15:32:17 IDT 2020
+#Wed Jul 29 13:31:05 IDT 2020
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
diff --git a/detox/test/e2e/27.retries.test.js b/detox/test/e2e/27.retries.test.js
new file mode 100644
index 000000000..fd38bde19
--- /dev/null
+++ b/detox/test/e2e/27.retries.test.js
@@ -0,0 +1,30 @@
+jest.retryTimes(3);
+
+const {
+  assertArtifactExists,
+  waitUntilArtifactsManagerIsIdle,
+} = require('./utils/artifactUtils');
+
+describe('jest.retryTimes() support', () => {
+  let counter = 3;
+
+  beforeAll(async () => {
+    await device.launchApp({ newInstance: true });
+  });
+
+  it('should fail twice and pass once', async () => {
+    const matcher = --counter > 0
+      ? by.text('Not existing')
+      : by.text('Sanity');
+
+    await element(matcher).tap();
+  });
+
+  afterAll(async () => {
+    await waitUntilArtifactsManagerIsIdle();
+
+    assertArtifactExists('✗ jest.retryTimes() support should fail twice and pass once');
+    assertArtifactExists('✗ jest.retryTimes() support should fail twice and pass once (2)');
+    assertArtifactExists('✓ jest.retryTimes() support should fail twice and pass once (3)');
+  });
+});
diff --git a/detox/test/e2e/config.js b/detox/test/e2e/config.js
index f561d0545..f6c9b861d 100644
--- a/detox/test/e2e/config.js
+++ b/detox/test/e2e/config.js
@@ -2,7 +2,7 @@ module.exports = {
   "rootDir": "../..",
   "testEnvironment": "./test/e2e/environment.js",
   "testRunner": "./test/node_modules/jest-circus/runner",
-  "setupFilesAfterEnv": ['./test/e2e/init-coverage.js'],
+  "setupFilesAfterEnv": [],
   "globalSetup": "./test/e2e/global-setup.js",
   "testTimeout": 120000,
   "reporters": process.env.DISABLE_JUNIT_REPORTER === '1'
diff --git a/detox/test/e2e/environment.js b/detox/test/e2e/environment.js
index 363ba9a41..ad3e2b701 100644
--- a/detox/test/e2e/environment.js
+++ b/detox/test/e2e/environment.js
@@ -17,11 +17,19 @@ class CustomDetoxEnvironment extends DetoxCircusEnvironment {
   }
 
   async initDetox() {
+    let instance;
+
     if (process.env.TIMEOUT_E2E_TEST) {
-      return this._initDetoxWithHangingServer();
+      instance = await this._initDetoxWithHangingServer();
     } else {
-      return super.initDetox();
+      instance = await super.initDetox();
     }
+
+    this.global.detox.__waitUntilArtifactsManagerIsIdle__ = () => {
+      return instance._artifactsManager._idlePromise;
+    };
+
+    return instance;
   }
 
   async _initDetoxWithHangingServer() {
diff --git a/detox/test/e2e/init-coverage.js b/detox/test/e2e/init-coverage.js
deleted file mode 100644
index 98c671ac5..000000000
--- a/detox/test/e2e/init-coverage.js
+++ /dev/null
@@ -1,1 +0,0 @@
-require('detox'); // forces Jest to instrument Detox codebase
diff --git a/detox/test/e2e/utils/artifactUtils.js b/detox/test/e2e/utils/artifactUtils.js
new file mode 100644
index 000000000..fed145a0e
--- /dev/null
+++ b/detox/test/e2e/utils/artifactUtils.js
@@ -0,0 +1,37 @@
+const _ = require('lodash');
+const fs = require('fs');
+const path = require('path');
+
+function getLatestArtifactsDir() {
+  if (!fs.existsSync('artifacts')) {
+    return null;
+  }
+
+  const sessionDirNames = fs.readdirSync('artifacts');
+  const sessionDirs = sessionDirNames.map(name => path.join('artifacts', name));
+  const stats = _.zipObject(sessionDirs, sessionDirs.map(dir => fs.statSync(dir)));
+
+  return _(sessionDirs)
+    .filter(dir => stats[dir].isDirectory())
+    .maxBy((dir) => stats[dir].mtime);
+}
+
+function assertArtifactExists(name) {
+  const artifactsRootDir = getLatestArtifactsDir();
+  const artifactPath = path.join(artifactsRootDir, name);
+  if (!fs.existsSync(artifactPath)) {
+    throw new Error('Assertion failed.\nFailed to find an artifact at path: ' + artifactPath);
+  }
+}
+
+async function waitUntilArtifactsManagerIsIdle() {
+  if (typeof detox !== 'undefined') {
+    await detox.__waitUntilArtifactsManagerIsIdle__();
+  }
+}
+
+module.exports = {
+  getLatestArtifactsDir,
+  assertArtifactExists,
+  waitUntilArtifactsManagerIsIdle,
+};
\ No newline at end of file
diff --git a/detox/test/package.json b/detox/test/package.json
index 44c27ea8a..d0ece76ab 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "17.3.2",
+  "version": "17.3.3",
   "private": true,
   "engines": {
     "node": ">=8.3.0"
@@ -33,8 +33,8 @@
     "react-native": "0.62.x"
   },
   "devDependencies": {
-    "@babel/core": "^7.4.5",
-    "detox": "^17.3.2",
+    "@babel/core": "^7.8.7",
+    "detox": "^17.3.3",
     "express": "^4.15.3",
     "jest": "26.x.x",
     "jest-circus": "26.x.x",