Repository: wix/Detox (js)

Issue:
Upgrade test/demo projects to Jest 28
Probably boils down to our example projects (might be even just `demo-react-native-jest`)

Pull request:
chore: upgrade internally to jest@28
## Description

**Maintenance PR.**
Upgrades all the sub-projects internally to Jest 28 (fixes #3396)

It has a couple of issues, though:

1. Error stack traces have changed slightly in Jest 28, so I had to update some expectations in the unit tests related to our error mangling utilities.
2. There's an issue with dropping redundant `@types/jest` devDependencies at the moment. Trying to solve it here: https://github.com/kulshekhar/ts-jest/pull/3513
3. There's a weird import glitch for `require("yargs/yargs").Parser` – created a bug report to Jest: https://github.com/facebook/jest/issues/12843

All the issues have been resolved via various workarounds, but this makes me wary about merging this PR soon. I'd wait for some official responses above.

Patch:
diff --git a/detox/package.json b/detox/package.json
index bbdf3ce81..28a9db52e 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -43,8 +43,7 @@
     "eslint-plugin-no-only-tests": "^2.6.0",
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-unicorn": "^39.0.0",
-    "jest": "^27.0.0",
-    "jest-environment-node": "^27.0.0",
+    "jest": "^28.0.0",
     "mocha": ">=6.0.0",
     "mockdate": "^2.0.1",
     "prettier": "1.7.0",
@@ -78,6 +77,7 @@
     "which": "^1.3.1",
     "ws": "^7.0.0",
     "yargs": "^16.0.3",
+    "yargs-parser": "^20.2.2",
     "yargs-unparser": "^2.0.0"
   },
   "peerDependencies": {
diff --git a/detox/runners/jest-circus/environment.js b/detox/runners/jest-circus/environment.js
index 6eb9ad761..1408347f7 100644
--- a/detox/runners/jest-circus/environment.js
+++ b/detox/runners/jest-circus/environment.js
@@ -1,5 +1,5 @@
 // @ts-nocheck
-const maybeNodeEnvironment = require('jest-environment-node'); // eslint-disable-line node/no-unpublished-require
+const maybeNodeEnvironment = require('jest-environment-node'); // eslint-disable-line node/no-extraneous-require
 const NodeEnvironment = maybeNodeEnvironment.default || maybeNodeEnvironment;
 
 const DetoxError = require('../../src/errors/DetoxError');
diff --git a/detox/runners/jest/JestCircusEnvironment.js b/detox/runners/jest/JestCircusEnvironment.js
index a4ce90889..a412a00c8 100644
--- a/detox/runners/jest/JestCircusEnvironment.js
+++ b/detox/runners/jest/JestCircusEnvironment.js
@@ -1,4 +1,4 @@
-const maybeNodeEnvironment = require('jest-environment-node'); // eslint-disable-line node/no-unpublished-require
+const maybeNodeEnvironment = require('jest-environment-node'); // eslint-disable-line node/no-extraneous-require
 // @ts-ignore
 const NodeEnvironment = maybeNodeEnvironment.default || maybeNodeEnvironment;
 
diff --git a/detox/src/Detox.js b/detox/src/Detox.js
index 7d0c0624f..0ec8832cf 100644
--- a/detox/src/Detox.js
+++ b/detox/src/Detox.js
@@ -251,6 +251,10 @@ class Detox {
     for (const appName of appNames) {
       await this.device.selectApp(appName);
       await this.device.uninstallApp();
+    }
+
+    for (const appName of appNames) {
+      await this.device.selectApp(appName);
       await this.device.installApp();
     }
 
diff --git a/detox/src/artifacts/log/android/ADBLogcatRecording.js b/detox/src/artifacts/log/android/ADBLogcatRecording.js
index 95a53e04e..72022c87c 100644
--- a/detox/src/artifacts/log/android/ADBLogcatRecording.js
+++ b/detox/src/artifacts/log/android/ADBLogcatRecording.js
@@ -1,6 +1,5 @@
 const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const { interruptProcess } = require('../../../utils/childProcess');
-const log = require('../../../utils/logger').child({ __filename });
 const retry = require('../../../utils/retry');
 const sleep = require('../../../utils/sleep');
 const Artifact = require('../../templates/artifact/Artifact');
@@ -26,26 +25,18 @@ class ADBLogcatRecording extends Artifact {
     this._waitUntilLogFileIsCreated = null;
   }
 
-  get hasRecordedFile() {
-    return !!this._waitUntilLogFileIsCreated;
-  }
-
   async doStart() {
     const pid = this.pid.get();
 
-    if (pid > 0) {
-      this.processPromise = this.adb.logcat(this.deviceId, {
-        file: this.pathToLogOnDevice,
-        time: this.since.get(),
-        pid,
-      });
+    this.processPromise = this.adb.logcat(this.deviceId, {
+      file: this.pathToLogOnDevice,
+      time: this.since.get(),
+      pid: pid > 0 ? pid : 0,
+    });
 
-      this._waitUntilLogFileIsCreated = sleep(300).then(() => {
-        return retry(() => this._assertLogIsCreated());
-      });
-    } else {
-      log.debug('Ignoring a command to start recording, because PID of the app is missing');
-    }
+    this._waitUntilLogFileIsCreated = sleep(300).then(() => {
+      return retry(() => this._assertLogIsCreated());
+    });
   }
 
   async doStop() {
@@ -61,20 +52,12 @@ class ADBLogcatRecording extends Artifact {
   }
 
   async doSave(artifactPath) {
-    if (this.hasRecordedFile) {
-      await this.adb.pull(this.deviceId, this.pathToLogOnDevice, artifactPath);
-      await this.adb.rm(this.deviceId, this.pathToLogOnDevice);
-    } else {
-      log.debug(`Skipping saving artifact because the recording has not started: ${artifactPath}`);
-    }
+    await this.adb.pull(this.deviceId, this.pathToLogOnDevice, artifactPath);
+    await this.adb.rm(this.deviceId, this.pathToLogOnDevice);
   }
 
   async doDiscard() {
-    if (this.hasRecordedFile) {
-      await this.adb.rm(this.deviceId, this.pathToLogOnDevice);
-    } else {
-      log.debug(`Skipping discarding artifact due to a not started recording: ${this.pathToLogOnDevice}`);
-    }
+    await this.adb.rm(this.deviceId, this.pathToLogOnDevice);
   }
 
   async _assertLogIsCreated() {
diff --git a/detox/src/configuration/composeAppsConfig.js b/detox/src/configuration/composeAppsConfig.js
index 94e6dd710..f4562e00a 100644
--- a/detox/src/configuration/composeAppsConfig.js
+++ b/detox/src/configuration/composeAppsConfig.js
@@ -1,6 +1,6 @@
 // @ts-nocheck
 const _ = require('lodash');
-const parse = require('yargs/yargs').Parser;
+const parse = require('yargs-parser');
 
 const deviceAppTypes = require('./utils/deviceAppTypes');
 
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index fa328c6c3..15f7896e3 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -7,7 +7,7 @@
   },
   "devDependencies": {
     "detox": "^19.7.0",
-    "jest": "^27.0.0"
+    "jest": "^28.0.0"
   },
   "detox": {
     "test-runner": "jest",
diff --git a/examples/demo-react-native-jest/package.json b/examples/demo-react-native-jest/package.json
index 74873ed5f..b5c18b41b 100644
--- a/examples/demo-react-native-jest/package.json
+++ b/examples/demo-react-native-jest/package.json
@@ -13,9 +13,9 @@
   "devDependencies": {
     "@types/jest": "^27.0.3",
     "detox": "^19.7.0",
-    "jest": "^27.0.0",
+    "jest": "^28.0.0",
     "sanitize-filename": "^1.6.1",
-    "ts-jest": "^27.0.0",
+    "ts-jest": "^28.0.0",
     "typescript": "^4.1.3"
   },
   "dependencies": {


Test patch:
diff --git a/detox/src/Detox.test.js b/detox/src/Detox.test.js
index 5ffdea1d7..a3fb2dff4 100644
--- a/detox/src/Detox.test.js
+++ b/detox/src/Detox.test.js
@@ -244,10 +244,12 @@ describe('Detox', () => {
         expect(runtimeDevice.uninstallApp).toHaveBeenCalledTimes(2);
         expect(runtimeDevice.installApp).toHaveBeenCalledTimes(2);
 
-        expect(runtimeDevice.selectApp).toHaveBeenCalledTimes(3);
+        expect(runtimeDevice.selectApp).toHaveBeenCalledTimes(5);
         expect(runtimeDevice.selectApp.mock.calls[0]).toEqual(['default']);
         expect(runtimeDevice.selectApp.mock.calls[1]).toEqual(['extraApp']);
-        expect(runtimeDevice.selectApp.mock.calls[2]).toEqual([null]);
+        expect(runtimeDevice.selectApp.mock.calls[2]).toEqual(['default']);
+        expect(runtimeDevice.selectApp.mock.calls[3]).toEqual(['extraApp']);
+        expect(runtimeDevice.selectApp.mock.calls[4]).toEqual([null]);
       });
     });
 
diff --git a/detox/src/utils/callsites.test.js b/detox/src/utils/callsites.test.js
index 3fa1d7fba..b72f6e53e 100644
--- a/detox/src/utils/callsites.test.js
+++ b/detox/src/utils/callsites.test.js
@@ -27,20 +27,19 @@ describe('callsites', () => {
     const callStackDumpFromWrapperFn = (endFrame) => callsites.getStackDump(endFrame);
     const callStackDumpFromTwoWrapperFn = (endFrame) => callStackDumpFromWrapperFn(endFrame);
 
-    const expectedTopFrameRegExp = /^ {4}at callStackDumpFromWrapperFn \(src[\\/]utils[\\/]callsites\.test\.js:[0-9][0-9]?:[0-9][0-9]?\)/;
-    const expected2ndLineRegExp = /^ {4}at callStackDumpFromTwoWrapperFn \(src[\\/]utils[\\/]callsites\.test\.js:[0-9][0-9]?:[0-9][0-9]?\)/;
+    const expectedTopFrameRegExp = /^ {4}at (?:Object\.)?callStackDumpFromWrapperFn \(src[\\/]utils[\\/]callsites\.test\.js:[0-9][0-9]?:[0-9][0-9]?\)/;
+    const expected2ndLineRegExp = /^ {4}at (?:Object\.)?callStackDumpFromTwoWrapperFn \(src[\\/]utils[\\/]callsites\.test\.js:[0-9][0-9]?:[0-9][0-9]?\)/;
 
     it('should return a valid, multi-line, stack-dump string', () => {
-      const stackdump = callStackDumpFromTwoWrapperFn();
+      const [,line1,line2] = callStackDumpFromTwoWrapperFn().split('\n');
 
-      expect(stackdump).toEqual(expect.stringMatching(expectedTopFrameRegExp));
-      expect(stackdump).toEqual(expect.stringMatching(new RegExp(expected2ndLineRegExp, 'm')));
+      expect(line1).toEqual(expect.stringMatching(expectedTopFrameRegExp));
+      expect(line2).toEqual(expect.stringMatching(expected2ndLineRegExp));
     });
 
     it('should slice according to end-frame arg', () => {
-      const _expectedTopLineRegExp = expected2ndLineRegExp;
-      const stackdump = callStackDumpFromTwoWrapperFn(1);
-      expect(stackdump).toEqual(expect.stringMatching(_expectedTopLineRegExp));
+      const [,line1] = callStackDumpFromTwoWrapperFn(1).split('\n');
+      expect(line1).toEqual(expect.stringMatching(expected2ndLineRegExp));
     });
   });
 
diff --git a/detox/src/utils/errorUtils.test.js b/detox/src/utils/errorUtils.test.js
index f562dc500..ad4197e35 100644
--- a/detox/src/utils/errorUtils.test.js
+++ b/detox/src/utils/errorUtils.test.js
@@ -6,10 +6,14 @@ describe('sliceErrorStack(error, fromIndex)', () => {
   it('should clean up error stack by N first lines containing at:', () => {
     function innerFunction() { throw new Error('Source Error'); }
     function outerFunction() { innerFunction(); }
+    function attemptFunction() {
+      try { outerFunction(); } catch (e) { console.error('err', e); return e; }
+    }
+
     const slicer = at => (_line) => --at < 0;
-    const error0 = errorUtils.filterErrorStack(_.attempt(outerFunction), slicer(0));
-    const error2 = errorUtils.filterErrorStack(_.attempt(outerFunction), slicer(1));
-    const error3 = errorUtils.filterErrorStack(_.attempt(outerFunction), slicer(2));
+    const error0 = errorUtils.filterErrorStack(attemptFunction(), slicer(1));
+    const error2 = errorUtils.filterErrorStack(attemptFunction(), slicer(2));
+    const error3 = errorUtils.filterErrorStack(attemptFunction(), slicer(3));
     expect(error0.stack).toMatch(/at innerFunction/);
     expect(error0.stack).toMatch(/at outerFunction/);
     expect(error2.stack).not.toMatch(/at innerFunction/);
diff --git a/examples/demo-plugin/e2e/config.json b/examples/demo-plugin/e2e/config.json
index 52e4ca0d2..4c0efb521 100644
--- a/examples/demo-plugin/e2e/config.json
+++ b/examples/demo-plugin/e2e/config.json
@@ -1,6 +1,7 @@
 {
-  "setupFilesAfterEnv": ["./init.js"],
-  "testRunner": "jasmine2",
+  "maxWorkers": 1,
   "reporters": ["detox/runners/jest/streamlineReporter"],
+  "testEnvironment": "detox/runners/jest-circus/environment",
+  "testTimeout": 120000,
   "verbose": true
 }
diff --git a/examples/demo-plugin/e2e/init.js b/examples/demo-plugin/e2e/init.js
deleted file mode 100644
index 8007485c6..000000000
--- a/examples/demo-plugin/e2e/init.js
+++ /dev/null
@@ -1,30 +0,0 @@
-const detox = require('detox');
-const adapter = require('detox/runners/jest/adapter');
-const specReporter = require('detox/runners/jest/specReporter');
-const assignReporter = require('detox/runners/jest/assignReporter');
-
-jasmine.getEnv().addReporter(adapter);
-
-// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
-// This is strictly optional.
-jasmine.getEnv().addReporter(specReporter);
-
-// This will post which device has assigned to run a suite, which can be useful in a multiple-worker tests run.
-// This is strictly optional.
-jasmine.getEnv().addReporter(assignReporter);
-
-// Set the default timeout
-jest.setTimeout(90000);
-
-beforeAll(async () => {
-  await detox.init();
-}, 300000);
-
-beforeEach(async () => {
-  await adapter.beforeEach();
-});
-
-afterAll(async () => {
-  await adapter.afterAll();
-  await detox.cleanup();
-});