Repository: wix/Detox (js)

Issue:
`headless` not respected by AVDValidator emulator version validator
### What happened?

the `headless` config option is not respected by the emulator version validator (it is respected by other commands however)

Example error observed on ubuntu:
```
09:33:44.784 detox[10068] ERROR: [EXEC_FAIL, #1] ""/usr/local/lib/android/sdk/emulator/emulator" -version " failed with error = ChildProcessError: Command failed: "/usr/local/lib/android/sdk/emulator/emulator" -version 
/usr/local/lib/android/sdk/emulator/qemu/linux-x86_64/qemu-system-x86_64: error while loading shared libraries: libpulse.so.0: cannot open shared object file: No such file or directory
 `"/usr/local/lib/android/sdk/emulator/emulator" -version ` (exited with error code 127) (code=127), stdout and stderr:

0[9](https://github.com/letsbuilders/LbSiteDiary/actions/runs/3091392392/jobs/5012531600#step:8:10):33:44.785 detox[[10](https://github.com/letsbuilders/LbSiteDiary/actions/runs/3091392392/jobs/5012531600#step:8:11)068] ERROR: [EXEC_FAIL, #1] 
09:33:44.785 detox[10068] ERROR: [EXEC_FAIL, #1] /usr/local/lib/android/sdk/emulator/qemu/linux-x86_64/qemu-system-x86_64: error while loading shared libraries: libpulse.so.0: cannot open shared object file: No such file or directory
```

### What was the expected behaviour?

_No response_

### Was it tested on latest Detox?

- [X] I have tested this issue on the latest Detox release and it still reproduces.

### Did your test throw out a timeout?

- [ ] I have followed the instructions under [Identifying which synchronization mechanism causes us to wait too much](https://wix.github.io/Detox/docs/troubleshooting/synchronization#identifying-which-synchronization-mechanism-causes-us-to-wait-too-much).

### Help us reproduce this issue!

_No response_

### In what environment did this happen?

Detox version: 19.12.1
React Native version: N/A 
Node version: 16
Device model: N/A
Android version: N/A
Test-runner (select one): N/A

### Detox logs

<details><summary>Detox logs</summary>

```
paste logs here!
```

</details>

### Device logs

<details><summary>Device logs</summary>

```
paste logs here!
```

</details>

### More data, please!

_No response_

Pull request:
bugfix: avd validator emulator version should obey `headless`
## Description
This is a bugfix to address the `headless` config when running tests (invoked as `emulator -version` by the `AVDValidator`s emulator version validator.

- Resolves #3613 

In this pull request, I have forwarded the `headless` config option to the `AVDValidator` so that the existing emulator version query can operate as expected and in accordance to the configuration provided by the user



Patch:
diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/IdlingResourceDescription.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/IdlingResourceDescription.kt
index d074ecd54..12a21e538 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/IdlingResourceDescription.kt
+++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/IdlingResourceDescription.kt
@@ -1,20 +1,26 @@
 package com.wix.detox.reactnative.idlingresources
 
 class IdlingResourceDescription private constructor(
-  private val name: String,
-  private val description: Map<String, Any>) {
-    fun json(): Map<String, Any> = mutableMapOf<String, Any>("name" to name)
-      .apply { if (description.isNotEmpty()) put("description", description) }
+    private val name: String,
+    private val description: Map<String, Any>) {
 
-    override fun equals(other: Any?): Boolean = other is IdlingResourceDescription &&
-        other.json() == this.json()
+    fun json() =
+        mutableMapOf<String, Any>("name" to name)
+          .apply {
+            if (description.isNotEmpty()) put("description", description)
+          }.toMap()
 
-    data class Builder(
-      var name: String = "unknown",
-      var description: MutableMap<String, Any> = mutableMapOf()) {
+    override fun equals(other: Any?) =
+      (other is IdlingResourceDescription && other.json() == this.json())
+
+    override fun hashCode(): Int =
+      (31 * name.hashCode() + description.hashCode())
 
-        fun name(name: String) = apply { this.name = name }
-        fun addDescription(key: String, value: Any) = apply { this.description[key] = value }
-        fun build() = IdlingResourceDescription(name, description)
-    }
+    data class Builder(
+        var name: String = "unknown",
+        var description: MutableMap<String, Any> = mutableMapOf()) {
+            fun name(value: String) = apply { name = value }
+            fun addDescription(key: String, value: Any) = apply { description[key] = value }
+            fun build() = IdlingResourceDescription(name, description)
+      }
 }
diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResource.java b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResource.java
index 630785615..93cd62f34 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResource.java
+++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResource.java
@@ -69,42 +69,13 @@ public class NetworkIdlingResource extends DetoxBaseIdlingResource implements Ch
 
     @NotNull
     @Override
-    public IdlingResourceDescription getDescription() {
+    public synchronized IdlingResourceDescription getDescription() {
         return new IdlingResourceDescription.Builder()
                 .name("network")
                 .addDescription("urls", new ArrayList<>(busyResources))
                 .build();
     }
 
-    @Override
-    protected boolean checkIdle() {
-        boolean idle = true;
-        busyResources.clear();
-        List<Call> calls = dispatcher.runningCalls();
-        for (Call call : calls) {
-            idle = false;
-            String url = call.request().url().toString();
-            for (Pattern pattern : blacklist) {
-                if (pattern.matcher(url).matches()) {
-                    idle = true;
-                    break;
-                }
-            }
-            if (!idle) {
-                busyResources.add(call.request().url().toString());
-            }
-        }
-        if (!idle) {
-            Choreographer.getInstance().postFrameCallback(this);
-            Log.i(LOG_TAG, "Network is busy");
-        } else {
-            if (callback != null) {
-                callback.onTransitionToIdle();
-            }
-        }
-        return idle;
-    }
-
     @Override
     public void registerIdleTransitionCallback(ResourceCallback callback) {
         this.callback = callback;
@@ -116,10 +87,42 @@ public class NetworkIdlingResource extends DetoxBaseIdlingResource implements Ch
         isIdleNow();
     }
 
+    @Override
+    protected synchronized boolean checkIdle() {
+        busyResources.clear();
+
+        List<Call> calls = dispatcher.runningCalls();
+        for (Call call: calls) {
+            final String url = call.request().url().toString();
+
+            if (!isUrlBlacklisted(url)) {
+                busyResources.add(url);
+            }
+        }
+
+        if (!busyResources.isEmpty()) {
+            Log.i(LOG_TAG, "Network is busy, with " + busyResources.size() + " in-flight calls");
+            Choreographer.getInstance().postFrameCallback(this);
+            return false;
+        }
+
+        notifyIdle();
+        return true;
+    }
+
     @Override
     protected void notifyIdle() {
         if (callback != null) {
             callback.onTransitionToIdle();
         }
     }
+
+    private boolean isUrlBlacklisted(String url) {
+        for (Pattern pattern: blacklist) {
+            if (pattern.matcher(url).matches()) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/detox/package.json b/detox/package.json
index c497b77b9..bf939973a 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "19.12.1",
+  "version": "19.12.3",
   "bin": {
     "detox": "local-cli/cli.js"
   },
diff --git a/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js b/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js
index 95ad52d0f..07d57f07e 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js
@@ -12,11 +12,11 @@ class AVDValidator {
     this._emulatorVersionResolver = emulatorVersionResolver;
   }
 
-  async validate(avdName) {
+  async validate(avdName, isHeadless) {
     const avds = await this._avdsResolver.resolve(avdName);
     this._assertAVDs(avds);
     await this._assertAVDMatch(avds, avdName);
-    await this._validateEmulatorVer();
+    await this._validateEmulatorVer(isHeadless);
   }
 
   _assertAVDs(avds) {
@@ -37,8 +37,8 @@ class AVDValidator {
     }
   }
 
-  async _validateEmulatorVer() {
-    const emulatorVersion = await this._emulatorVersionResolver.resolve();
+  async _validateEmulatorVer(isHeadless) {
+    const emulatorVersion = await this._emulatorVersionResolver.resolve(isHeadless);
     if (!emulatorVersion) {
       logger.warn({ event: 'AVD_VALIDATION' }, 'Emulator version detection failed (See previous logs)');
       return;
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
index 74272f96e..ce0bac2ca 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
@@ -31,7 +31,7 @@ class EmulatorAllocDriver extends AllocationDriverBase {
   async allocate(deviceConfig) {
     const avdName = deviceConfig.device.avdName;
 
-    await this._avdValidator.validate(avdName);
+    await this._avdValidator.validate(avdName, deviceConfig.headless);
     await this._fixAvdConfigIniSkinNameIfNeeded(avdName, deviceConfig.headless);
 
     const allocResult = await this._allocationHelper.allocateDevice(avdName);
diff --git a/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js b/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
index 4fd9e466f..585c99b8e 100644
--- a/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
+++ b/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
@@ -132,6 +132,30 @@ class AppleSimUtils {
   }
 
   async sendToHome(udid) {
+    if (await this._isSpringBoardInaccessible(udid)) {
+      // SpringBoard is not directly accessible by Simctl on iOS 16.0 and above, therefore we launch and terminate the
+      // Settings app instead. This sends the currently open app to the background and brings the home screen to the
+      // foreground.
+      await this._launchAndTerminateSettings(udid);
+      return;
+    }
+
+    await this._launchSpringBoard(udid);
+  }
+
+  async _isSpringBoardInaccessible(udid) {
+    const device = await this._findDeviceByUDID(udid);
+    const majorIOSVersion = parseInt(device.os.version.split('.')[0]);
+    return majorIOSVersion >= 16;
+  }
+
+  async _launchAndTerminateSettings(udid) {
+    const bundleId = 'com.apple.Preferences';
+    await this._execSimctl({ cmd: `launch ${udid} ${bundleId}`, retries: 10 });
+    await this._execSimctl({ cmd: `terminate ${udid} ${bundleId}`, retries: 10 });
+  }
+
+  async _launchSpringBoard(udid) {
     await this._execSimctl({ cmd: `launch ${udid} com.apple.springboard`, retries: 10 });
   }
 
@@ -246,7 +270,7 @@ class AppleSimUtils {
       // ```
       // This workaround is done to ignore the error above, as we do not care if the app was running before, we just
       // want to make sure it isn't now.
-      if (err.code === 3 && 
+      if (err.code === 3 &&
           (err.stderr.includes(`the app is not currently running`) ||
            err.stderr.includes(`The operation couldn’t be completed. found nothing to terminate`))) {
         return;
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index 8f08de197..8310f014b 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": "19.12.1",
+  "version": "19.12.3",
   "private": true,
   "scripts": {
     "packager": "react-native start",
@@ -8,7 +8,7 @@
     "e2e": "mocha e2e --opts ./e2e/mocha.opts"
   },
   "devDependencies": {
-    "detox": "^19.12.1",
+    "detox": "^19.12.3",
     "mocha": "^6.1.3"
   },
   "detox": {}
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index 461594d49..53e43eac7 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": "19.12.1",
+  "version": "19.12.3",
   "private": true,
   "devDependencies": {
-    "detox": "^19.12.1",
+    "detox": "^19.12.3",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index 1c11d110b..c1984b551 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -1,12 +1,12 @@
 {
   "name": "demo-plugin",
-  "version": "19.12.1",
+  "version": "19.12.3",
   "private": true,
   "scripts": {
     "test:plugin": "detox test --configuration plugin -l verbose"
   },
   "devDependencies": {
-    "detox": "^19.12.1",
+    "detox": "^19.12.3",
     "jest": "^28.0.0"
   },
   "detox": {
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index 48a64a177..8920fa3bb 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": "19.12.1",
+  "version": "19.12.3",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^19.12.1",
+    "detox": "^19.12.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 a75ffa14d..897513d0b 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": "19.12.1",
+  "version": "19.12.3",
   "private": true,
   "scripts": {
     "test:ios-release": "detox test --configuration ios.sim.release -l verbose",
@@ -12,7 +12,7 @@
   },
   "devDependencies": {
     "@types/jest": "^27.0.3",
-    "detox": "^19.12.1",
+    "detox": "^19.12.3",
     "jest": "^28.0.0",
     "sanitize-filename": "^1.6.1",
     "ts-jest": "^28.0.0",
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index 3586ecfe7..6c89fe5c0 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "19.12.1",
+  "version": "19.12.3",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -31,7 +31,7 @@
   },
   "devDependencies": {
     "@types/mocha": "^8.2.0",
-    "detox": "^19.12.1",
+    "detox": "^19.12.3",
     "mocha": "^6.1.3",
     "ts-node": "^9.1.1",
     "typescript": "^4.1.3"
diff --git a/lerna.json b/lerna.json
index aca9dcea2..4d5f00840 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,7 +13,7 @@
     "generation",
     "."
   ],
-  "version": "19.12.1",
+  "version": "19.12.3",
   "npmClient": "npm",
   "command": {
     "publish": {
diff --git a/package.json b/package.json
index 738db54ec..f67757784 100644
--- a/package.json
+++ b/package.json
@@ -54,5 +54,5 @@
     "shell-utils": "1.x.x",
     "unified": "^10.1.0"
   },
-  "version": "19.12.1"
+  "version": "19.12.3"
 }


Test patch:
diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResourcesTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResourcesTest.kt
new file mode 100644
index 000000000..64b07dd5b
--- /dev/null
+++ b/detox/android/detox/src/testFull/java/com/wix/detox/reactnative/idlingresources/NetworkIdlingResourcesTest.kt
@@ -0,0 +1,61 @@
+package com.wix.detox.reactnative.idlingresources
+
+import com.wix.detox.UTHelpers.yieldToOtherThreads
+import org.assertj.core.api.Assertions.assertThat
+
+import okhttp3.Dispatcher
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import java.util.concurrent.Executors
+
+@RunWith(RobolectricTestRunner::class)
+class NetworkIdlingResourcesTest {
+    lateinit var dispatcher: Dispatcher
+    lateinit var uut: NetworkIdlingResource
+
+    @Before
+    fun setup() {
+        dispatcher = Dispatcher()
+        uut = NetworkIdlingResource(dispatcher)
+    }
+
+    // Note: Ideally, we should test that the list of busy resources is protected,
+    // rather than testing thread-safety as a whole.
+    @Test
+    fun `should return the description in a thread-safe way`() {
+        val localExecutor = Executors.newSingleThreadExecutor()
+        var description: IdlingResourceDescription? = null
+
+        synchronized(uut) {
+            localExecutor.submit {
+                description = uut.getDescription()
+            }
+
+            yieldToOtherThreads(localExecutor)
+            assertThat(description).isNull()
+        }
+        yieldToOtherThreads(localExecutor)
+        assertThat(description).isNotNull()
+    }
+
+    // Note: Ideally, we should test that the list of busy resources is protected,
+    // rather than testing thread-safety as a whole.
+    @Test
+    fun `should check for idle in a thread-safe way`() {
+        val localExecutor = Executors.newSingleThreadExecutor()
+        var idle = false
+
+        synchronized(uut) {
+            localExecutor.submit {
+                idle = uut.isIdleNow
+            }
+
+            yieldToOtherThreads(localExecutor)
+            assertThat(idle).isFalse
+        }
+        yieldToOtherThreads(localExecutor)
+        assertThat(idle).isTrue
+    }
+}
diff --git a/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.test.js b/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.test.js
index 6f5c15ea1..127bce7d6 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.test.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.test.js
@@ -111,4 +111,13 @@ describe('AVD validator', () => {
       'Emulator version detection failed (See previous logs)'
       );
   });
+
+  it('should run emulator version resolver as headless if required', async () => {
+    givenExpectedAVD();
+    givenProperEmulatorVersion();
+
+    await uut.validate('mock-avd-name', true);
+
+    expect(versionResolver.resolve).toHaveBeenCalledWith(true);
+  });
 });
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js
index dd927f674..d17ba5efd 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js
@@ -156,7 +156,13 @@ describe('Allocation driver for Google emulators', () => {
     it('should pre-validate proper AVD configuration', async () => {
       givenValidAVD();
       await allocDriver.allocate(deviceConfig);
-      expect(avdValidator.validate).toHaveBeenCalledWith(avdName);
+      expect(avdValidator.validate).toHaveBeenCalledWith(avdName, undefined);
+    });
+
+    it('should respect headless avd configuration during AVD validation', async () => {
+      givenValidAVD();
+      await allocDriver.allocate({ ...deviceConfig, headless: true });
+      expect(avdValidator.validate).toHaveBeenCalledWith(avdName, true);
     });
 
     it('should throw if AVD configuration is invalid', async () => {
diff --git a/detox/test/package.json b/detox/test/package.json
index b103ce8c1..db94b02c2 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "19.12.1",
+  "version": "19.12.3",
   "private": true,
   "engines": {
     "node": ">=8.3.0"
@@ -52,7 +52,7 @@
     "@types/react-native": "0.69.5",
     "@typescript-eslint/eslint-plugin": "^5.4.0",
     "@typescript-eslint/parser": "^5.4.0",
-    "detox": "^19.12.1",
+    "detox": "^19.12.3",
     "eslint": "^7.32.0",
     "eslint-plugin-unicorn": "^39.0.0",
     "express": "^4.15.3",