Repository: wix/Detox (js)

Issue:
AVDsResolver not able to find exact emulator on Windows machine
### Did you test using the latest Detox?

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

### Did your test fail and you're not sure why?

- [x] I have read the troubleshooting guide and it didn't help me.

### What happened?

Running `detox test --configuration android.emu.debug` fails with correct emulator name with error message:
  ```
  DetoxRuntimeError: Cannot boot Android Emulator with the name: 'a15'

    ,w14r Make sure you choose one of the available emulators: A9
```
Running emulator list returning this device, so it's not an issue with missing device:
```
>emulator -list-avds
A9
a15
w14r
```

Since issue is reproducing only on Windows machine, I assume it's related to [AVDResolver ](https://github.com/wix/Detox/blob/master/detox/src/devices/allocation/drivers/android/emulator/AVDsResolver.js#L10) using LF instead of CRLF for splitting device names

### What was the expected behaviour?

Expected to work with device in place and all environment variables set up

### Help us reproduce this issue!

To avoid issue quick fix should work:
```
const { ListAVDsCommand } = require('../../../../common/drivers/android/emulator/exec/EmulatorExec');
const { EOL } = require('os');

class AVDsResolver {
  constructor(emulatorExec) {
    this._emulatorExec = emulatorExec;
  }

  async resolve() {
    const output = await this._emulatorExec.exec(new ListAVDsCommand());
    const avds = output.trim().split(EOL); // Using OS native end of line to split emulators list
    return avds;
  }
}

module.exports = AVDsResolver;

```

### In what environment did this happen?

Detox version: "^20.39.0"
Test-runner (select one): jest

### Detox logs

<details><summary>Detox logs</summary>

```
● Test suite failed to run
                                                                                                                                                        
    ,w14rRuntimeError: avds:A9

    HINT: avdName:a15

      at AVDValidator._assertAVDMatch (node_modules/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js:38:13)
      at AVDValidator.validate (node_modules/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js:18:16)
      at async EmulatorAllocDriver.allocate (node_modules/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js:59:5)
      at async C:/Projects/PROJECT_NAME/node_modules/detox/src/devices/allocation/DeviceAllocator.js:44:22
```

</details>

### Device logs

<details><summary>Device logs</summary>

```
paste your device.log here!
```

</details>

### More data, please!

_No response_

Pull request:
fix: handle Windows CRLF in AVDsResolver output parsing
Resolves #4775

Patch:
diff --git a/detox/package.json b/detox/package.json
index a4db7cd67..b1628d5eb 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -98,7 +98,7 @@
     "stream-json": "^1.7.4",
     "strip-ansi": "^6.0.1",
     "telnet-client": "1.2.8",
-    "tempfile": "^2.0.0",
+    "tmp": "^0.2.1",
     "trace-event-lib": "^1.3.1",
     "which": "^1.3.1",
     "ws": "^7.0.0",
diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js
index ae39d6512..24f4a61cf 100644
--- a/detox/src/android/core/NativeElement.js
+++ b/detox/src/android/core/NativeElement.js
@@ -1,13 +1,14 @@
 const path = require('path');
 
 const fs = require('fs-extra');
-const tempfile = require('tempfile');
+
 
 const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 const invoke = require('../../invoke');
 const { removeMilliseconds } = require('../../utils/dateUtils');
 const { actionDescription } = require('../../utils/invocationTraceDescriptions');
 const mapLongPressArguments = require('../../utils/mapLongPressArguments');
+const tempfile = require('../../utils/tempfile');
 const actions = require('../actions/native');
 const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
 const { ActionInteraction } = require('../interactions/native');
diff --git a/detox/src/artifacts/templates/artifact/FileArtifact.js b/detox/src/artifacts/templates/artifact/FileArtifact.js
index 7f0704029..65d23d4a8 100644
--- a/detox/src/artifacts/templates/artifact/FileArtifact.js
+++ b/detox/src/artifacts/templates/artifact/FileArtifact.js
@@ -2,9 +2,10 @@
 const path = require('path');
 
 const fs = require('fs-extra');
-const tempfile = require('tempfile');
 
 const appendFile = require('../../../utils/appendFile');
+const tempfile = require('../../../utils/tempfile');
+
 
 const Artifact = require('./Artifact');
 
diff --git a/detox/src/artifacts/utils/temporaryPath.js b/detox/src/artifacts/utils/temporaryPath.js
index c55f0a435..faca7ff66 100644
--- a/detox/src/artifacts/utils/temporaryPath.js
+++ b/detox/src/artifacts/utils/temporaryPath.js
@@ -3,9 +3,10 @@ const { promisify } = require('util');
 
 const glob = require('glob');
 const _ = require('lodash');
-const tempfile = require('tempfile');
 
 const { useForwardSlashes } = require('../../utils/shellUtils');
+const tempfile = require('../../utils/tempfile');
+
 
 const globSync = glob.sync;
 const globAsync = promisify(glob);
diff --git a/detox/src/devices/allocation/drivers/android/emulator/AVDsResolver.js b/detox/src/devices/allocation/drivers/android/emulator/AVDsResolver.js
index 2eb466528..3a21749e4 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/AVDsResolver.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/AVDsResolver.js
@@ -7,7 +7,7 @@ class AVDsResolver {
 
   async resolve() {
     const output = await this._emulatorExec.exec(new ListAVDsCommand());
-    const avds = output.trim().split('\n');
+    const avds = output.trim().split('\n').map(x => x.trim());
     return avds;
   }
 }
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index f63a10114..4c1c55651 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -3,7 +3,7 @@ const path = require('path');
 
 const fs = require('fs-extra');
 const _ = require('lodash');
-const tempfile = require('tempfile');
+
 
 const { assertTraceDescription, assertEnum, assertNormalized } = require('../utils/assertArgument');
 const { removeMilliseconds } = require('../utils/dateUtils');
@@ -11,6 +11,7 @@ const { actionDescription, expectDescription } = require('../utils/invocationTra
 const { isRegExp } = require('../utils/isRegExp');
 const log = require('../utils/logger').child({ cat: 'ws-client, ws' });
 const mapLongPressArguments = require('../utils/mapLongPressArguments');
+const tempfile = require('../utils/tempfile');
 const traceInvocationCall = require('../utils/traceInvocationCall').bind(null, log);
 
 const { systemElement, systemMatcher, systemExpect, isSystemElement } = require('./system');
diff --git a/detox/src/utils/tempfile.js b/detox/src/utils/tempfile.js
new file mode 100644
index 000000000..c86a09196
--- /dev/null
+++ b/detox/src/utils/tempfile.js
@@ -0,0 +1,18 @@
+const tmp = require('tmp');
+
+tmp.setGracefulCleanup();
+
+/**
+ * Creates a temporary file path. If extension is provided, it will be appended to the path.
+ * @param {string} [extension] - Optional file extension to append to the temporary file path
+ * @returns {string} Path to a temporary file
+ */
+module.exports = function(extension) {
+  const _extension = (extension && extension.startsWith('.'))
+    ? extension
+    : (extension && `.${extension}`);
+
+  return tmp.tmpNameSync({
+    template: `detox-${process.pid}-XXXXXX${_extension || ''}`,
+  });
+};


Test patch:
diff --git a/detox/__tests__/helpers.js b/detox/__tests__/helpers.js
index 397ff89fe..e1d1f4c21 100644
--- a/detox/__tests__/helpers.js
+++ b/detox/__tests__/helpers.js
@@ -2,9 +2,10 @@ const path = require('path');
 
 const fs = require('fs-extra');
 const _ = require('lodash');
-const tempfile = require('tempfile');
 const yargs = require('yargs');
 
+const tempfile = require('../src/utils/tempfile');
+
 function callCli(modulePath, cmd) {
   return new Promise((resolve, reject) => {
     const originalModule = require(path.join(__dirname, '../local-cli', modulePath));
diff --git a/detox/local-cli/build.test.js b/detox/local-cli/build.test.js
index e6d7d86a9..889f5d363 100644
--- a/detox/local-cli/build.test.js
+++ b/detox/local-cli/build.test.js
@@ -1,6 +1,6 @@
-const tempfile = require('tempfile');
-
 const { callCli } = require('../__tests__/helpers');
+const tempfile = require('../src/utils/tempfile');
+
 
 describe('build', () => {
   let execSync, detox;
diff --git a/detox/local-cli/test.test.js b/detox/local-cli/test.test.js
index 511209848..04fbfd59c 100644
--- a/detox/local-cli/test.test.js
+++ b/detox/local-cli/test.test.js
@@ -659,7 +659,8 @@ describe('CLI', () => {
   // Helpers
 
   function tempfile(extension, content) {
-    const tempFilePath = require('tempfile')(extension);
+    const _tempfile = require('../src/utils/tempfile');
+    const tempFilePath = _tempfile(extension);
 
     fs.ensureFileSync(tempFilePath);
     if (content) {
diff --git a/detox/src/android/AndroidExpect.test.js b/detox/src/android/AndroidExpect.test.js
index c2d988184..1e2caeff8 100644
--- a/detox/src/android/AndroidExpect.test.js
+++ b/detox/src/android/AndroidExpect.test.js
@@ -9,7 +9,7 @@ describe('AndroidExpect', () => {
 
   beforeEach(() => {
     jest.mock('../utils/logger');
-    jest.mock('tempfile');
+    jest.mock('../utils/tempfile');
     jest.mock('fs-extra');
 
     mockExecutor = new MockExecutor();
@@ -341,7 +341,7 @@ describe('AndroidExpect', () => {
         mockExecutor.executeResult = Promise.resolve(invokeResultInBase64);
 
         fs = require('fs-extra');
-        tempfile = require('tempfile');
+        tempfile = require('../utils/tempfile');
         tempfile.mockReturnValue(tempFilePath);
 
         _element = e.element(e.by.id('FancyElement'));
diff --git a/detox/src/artifacts/log/ios/SimulatorLogPlugin.test.js b/detox/src/artifacts/log/ios/SimulatorLogPlugin.test.js
index cf31e6ee8..967360d1a 100644
--- a/detox/src/artifacts/log/ios/SimulatorLogPlugin.test.js
+++ b/detox/src/artifacts/log/ios/SimulatorLogPlugin.test.js
@@ -5,9 +5,10 @@ const path = require('path');
 
 const fs = require('fs-extra');
 const _ = require('lodash');
-const tempfile = require('tempfile');
 
 const childProcess = require('../../../utils/childProcess');
+const tempfile = require('../../../utils/tempfile');
+
 
 describe('SimulatorLogPlugin', () => {
   async function majorWorkflow() {
diff --git a/detox/src/artifacts/templates/artifact/FileArtifact.test.js b/detox/src/artifacts/templates/artifact/FileArtifact.test.js
index 5778a4579..16b8bd0df 100644
--- a/detox/src/artifacts/templates/artifact/FileArtifact.test.js
+++ b/detox/src/artifacts/templates/artifact/FileArtifact.test.js
@@ -1,6 +1,7 @@
 const fs = require('fs-extra');
 const _ = require('lodash');
-const tempfile = require('tempfile');
+
+const tempfile = require('../../../utils/tempfile');
 
 describe('FileArtifact', () => {
   let FileArtifact, fileArtifact, logger, temporaryPath, temporaryData, destinationPath;
diff --git a/detox/src/artifacts/utils/temporaryPath.test.js b/detox/src/artifacts/utils/temporaryPath.test.js
index 719b9a47d..8d34dea8c 100644
--- a/detox/src/artifacts/utils/temporaryPath.test.js
+++ b/detox/src/artifacts/utils/temporaryPath.test.js
@@ -1,7 +1,8 @@
 const path = require('path');
 
 const fs = require('fs-extra');
-const tempfile = require('tempfile');
+
+const tempfile = require('../../utils/tempfile');
 
 const temporaryPath = require('./temporaryPath');
 
diff --git a/detox/src/client/Client.test.js b/detox/src/client/Client.test.js
index 317c4c8da..bdee2cd8a 100644
--- a/detox/src/client/Client.test.js
+++ b/detox/src/client/Client.test.js
@@ -2,10 +2,11 @@
 jest.useFakeTimers('modern');
 
 const { serializeError } = require('serialize-error');
-const tempfile = require('tempfile');
+
 
 const { validSession } = require('../configuration/configurations.mock');
 const Deferred = require('../utils/Deferred');
+const tempfile = require('../utils/tempfile');
 
 const actions = require('./actions/actions');
 
diff --git a/detox/src/devices/allocation/DeviceRegistry.test.js b/detox/src/devices/allocation/DeviceRegistry.test.js
index 9f2c05d83..8267fc86f 100644
--- a/detox/src/devices/allocation/DeviceRegistry.test.js
+++ b/detox/src/devices/allocation/DeviceRegistry.test.js
@@ -1,5 +1,6 @@
 const fs = require('fs-extra');
-const tempfile = require('tempfile');
+
+const tempfile = require('../../utils/tempfile');
 
 const DeviceRegistry = require('./DeviceRegistry');  // Adjust the path to your actual file
 
diff --git a/detox/src/ios/expectTwo.test.js b/detox/src/ios/expectTwo.test.js
index b969fa036..e4984d5cb 100644
--- a/detox/src/ios/expectTwo.test.js
+++ b/detox/src/ios/expectTwo.test.js
@@ -11,8 +11,8 @@ describe('expectTwo', () => {
 
   beforeEach(() => {
     jest.mock('../utils/logger');
+    jest.mock('../utils/tempfile');
     jest.mock('fs-extra');
-    jest.mock('tempfile');
 
     fs = require('fs-extra');
     const IosExpect = require('./expectTwo');
@@ -690,7 +690,7 @@ describe('expectTwo', () => {
         screenshotPath: deviceTmpFilePath,
       });
 
-      require('tempfile').mockReturnValue(tmpFilePath);
+      require('../utils/tempfile').mockReturnValue(tmpFilePath);
       result = await e.element(e.by.id('uniqueId')).takeScreenshot(imageName);
     });
 
diff --git a/detox/src/logger/DetoxLogger.test.js b/detox/src/logger/DetoxLogger.test.js
index ad1d6fc68..de029099e 100644
--- a/detox/src/logger/DetoxLogger.test.js
+++ b/detox/src/logger/DetoxLogger.test.js
@@ -8,9 +8,10 @@ const os = require('os');
 
 const fs = require('fs-extra');
 const _ = require('lodash');
-const tempfile = require('tempfile');
 
 const sleep = require('../utils/sleep');
+const tempfile = require('../utils/tempfile');
+
 
 jest.retryTimes(2);
 
diff --git a/detox/src/logger/utils/DetoxLogFinalizer.test.js b/detox/src/logger/utils/DetoxLogFinalizer.test.js
index 019132701..8f8ada0ad 100644
--- a/detox/src/logger/utils/DetoxLogFinalizer.test.js
+++ b/detox/src/logger/utils/DetoxLogFinalizer.test.js
@@ -5,9 +5,9 @@ const { PassThrough } = require('stream');
 
 const fs = require('fs-extra');
 const glob = require('glob');
-const tempfile = require('tempfile');
 
 const temporary = require('../../artifacts/utils/temporaryPath');
+const tempfile = require('../../utils/tempfile');
 const DetoxLogger = require('../DetoxLogger');
 
 const DetoxLogFinalizer = require('./DetoxLogFinalizer');
diff --git a/detox/src/logger/utils/streams/BunyanTransformer.test.js b/detox/src/logger/utils/streams/BunyanTransformer.test.js
index bfd749897..c1208639e 100644
--- a/detox/src/logger/utils/streams/BunyanTransformer.test.js
+++ b/detox/src/logger/utils/streams/BunyanTransformer.test.js
@@ -1,5 +1,6 @@
 const fs = require('fs-extra');
-const tempfile = require('tempfile');
+
+const tempfile = require('../../../utils/tempfile');
 
 describe('BunyanTransformer', () => {
   /** @type {import('./BunyanTransformer')} */
diff --git a/detox/src/utils/ExclusiveLockfile.test.js b/detox/src/utils/ExclusiveLockfile.test.js
index 3c6e6a8d8..8f2995721 100644
--- a/detox/src/utils/ExclusiveLockfile.test.js
+++ b/detox/src/utils/ExclusiveLockfile.test.js
@@ -2,9 +2,10 @@ jest.mock('proper-lockfile');
 
 const fs = require('fs-extra');
 const plock = require('proper-lockfile');
-const tempfile = require('tempfile');
 
 const ExclusiveLockFile = require('./ExclusiveLockfile');
+const tempfile = require('./tempfile');
+
 
 describe('ExclusiveLockFile', () => {
   let filePath;
diff --git a/detox/src/utils/appendFile.test.js b/detox/src/utils/appendFile.test.js
index 735a50bd8..53ef04541 100644
--- a/detox/src/utils/appendFile.test.js
+++ b/detox/src/utils/appendFile.test.js
@@ -1,7 +1,8 @@
 const fs = require('fs-extra');
-const tempfile = require('tempfile');
 
 const appendFile = require('./appendFile');
+const tempfile = require('./tempfile');
+
 
 describe('appendFile', () => {
   let src, dest;
diff --git a/detox/src/utils/environment.test.js b/detox/src/utils/environment.test.js
index d4b3875c6..5b90efed9 100644
--- a/detox/src/utils/environment.test.js
+++ b/detox/src/utils/environment.test.js
@@ -3,7 +3,8 @@ const path = require('path');
 
 const fs = require('fs-extra');
 const _ = require('lodash');
-const tempfile = require('tempfile');
+
+const tempfile = require('./tempfile');
 
 describe('Environment', () => {
   let Environment;
diff --git a/detox/src/utils/fsext.test.js b/detox/src/utils/fsext.test.js
index ebb5cdd5b..e6ad0408e 100644
--- a/detox/src/utils/fsext.test.js
+++ b/detox/src/utils/fsext.test.js
@@ -1,9 +1,10 @@
 const path = require('path');
 
 const fs = require('fs-extra');
-const tempfile = require('tempfile');
 
 const fsext = require('./fsext');
+const tempfile = require('./tempfile');
+
 
 test('isDirEmptySync', async () => {
   const tempDir = tempfile();
diff --git a/detox/src/utils/tempfile.test.js b/detox/src/utils/tempfile.test.js
new file mode 100644
index 000000000..5bb7fe681
--- /dev/null
+++ b/detox/src/utils/tempfile.test.js
@@ -0,0 +1,72 @@
+describe('tempfile', () => {
+  let tmp;
+  let tempfile;
+
+  describe('in full integration with tmp module', () => {
+    beforeEach(() => {
+      jest.restoreAllMocks();
+      tempfile = require('./tempfile');
+    });
+
+    it('should work with the real tmp module', () => {
+      const tmpdir = require('node:os').tmpdir();
+      const path = require('node:path');
+
+      const expectedFile = path.join(tmpdir, `detox-${process.pid}-[a-zA-Z0-9]+.log`).replace(/\\/g, '\\\\');
+      const expectedResult = new RegExp(`^${expectedFile}$`);
+
+      const result = tempfile('.log');
+      expect(result).toMatch(expectedResult);
+    });
+  });
+
+  describe('with mocked tmp module', () => {
+    beforeEach(() => {
+      jest.mock('tmp');
+      tmp = require('tmp');
+      tempfile = require('./tempfile');
+    });
+
+    const expectTmpCalled = ({ withExtension = '' } = {}) => {
+      const template = `detox-${process.pid}-XXXXXX${withExtension}`;
+      expect(tmp.tmpNameSync).toHaveBeenCalledWith({ template });
+    };
+
+    it(`should enable tmp's graceful cleanup`, () => {
+      expect(tmp.setGracefulCleanup).toHaveBeenCalled();
+    });
+
+    it('should return the value from tmp.tmpNameSync', () => {
+      const mockPath = '/tmp/detox-123-abc123';
+      tmp.tmpNameSync.mockReturnValueOnce(mockPath);
+
+      const result = tempfile();
+      expect(result).toEqual(mockPath);
+    });
+
+    it('should create a temporary file path without extension', () => {
+      tempfile();
+      expectTmpCalled();
+    });
+
+    it('should create a temporary file path with extension', () => {
+      tempfile('txt');
+      expectTmpCalled({ withExtension: '.txt' });
+    });
+
+    it('should handle extension with leading dot', () => {
+      tempfile('.txt');
+      expectTmpCalled({ withExtension: '.txt' });
+    });
+
+    it('should handle empty extension', () => {
+      tempfile('');
+      expectTmpCalled();
+    });
+
+    it('should handle undefined extension', () => {
+      tempfile();
+      expectTmpCalled();
+    });
+  });
+});
diff --git a/detox/test/integration/stub/StubRuntimeDriver.js b/detox/test/integration/stub/StubRuntimeDriver.js
index bfa400155..54f540503 100644
--- a/detox/test/integration/stub/StubRuntimeDriver.js
+++ b/detox/test/integration/stub/StubRuntimeDriver.js
@@ -1,6 +1,6 @@
 const temporaryPath = require('detox/src/artifacts/utils/temporaryPath');
 const DeviceDriverBase = require('detox/src/devices/runtime/drivers/DeviceDriverBase');
-const tempfile = require('tempfile');
+const tempfile = require('../../../src/utils/tempfile');
 
 const {
   sleepSomeTime,
diff --git a/detox/test/integration/timeline-artifact.test.js b/detox/test/integration/timeline-artifact.test.js
index bfcc9dbc0..3f092caff 100644
--- a/detox/test/integration/timeline-artifact.test.js
+++ b/detox/test/integration/timeline-artifact.test.js
@@ -1,7 +1,7 @@
 const path = require('path');
 
 const _ = require('lodash');
-const tempfile = require('tempfile');
+const tempfile = require('../../src/utils/tempfile');
 const fs = require('fs-extra');
 const { promisify } = require('util');
 const { execCommand } = require('./utils/exec');