Expo + DetoxでE2Eテストが動くまで

久々に個人で作っているレシピ管理アプリRicettaをアップデートしようと思ったので、色々とライブラリのアップデートも必要になってくるわけなんですが、あちこち手動で動作確認するのも大変なので、E2Eテスト書いてコアな機能が一通り動くことを自動で検証出来たら非常にありがたいなあと思い、ReactNative用？のE2EテストライブラリのDetoxを導入することにした。

ひとまず、ブランクのExpoアプリケーションに、Detox入れてビルド〜テストが通るところまでやりました。なかなか手間取ったのでログを残しておきます。環境はMacです。

## まずはExpoのブランクアプリが動くまで

Expoプロジェクトを生成して起動してみるも動かない。やれやれ。

```
$ expo init expo-detox
$ yarn install
$ expo start

┌─────────────────────────────────────────────────────────────────────────┐
│                                                                         │
│   There is a new version of expo-cli available (6.3.8).                 │
│   You are currently using expo-cli 6.0.8                                │
│   Install expo-cli globally using the package manager of your choice;   │
│   for example: `npm install -g expo-cli` to get the latest version      │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
› Port 19000 is running Ricetta in another window
  /Users/aharada/source/Ricetta (pid 16732)
✔ Use port 19002 instead? … yes

This command is being executed with the global Expo CLI. Learn more.
To use the local CLI instead (recommended in SDK 46 and higher), run:
› npx expo start

Starting project at /Users/aharada/source/expo-detox

✔ It looks like you're trying to use TypeScript but don't have the required dependencies installed. Would you like to install
@types/react-native? … yes

✖ Failed to install @types/react-native with error: yarnpkg exited with non-zero code: 1
yarnpkg exited with non-zero code: 1
Error: yarnpkg exited with non-zero code: 1
    at ChildProcess.completionListener (/Users/aharada/.volta/tools/image/packages/expo-cli/lib/node_modules/expo-cli/node_modules/@expo/spawn-async/src/spawnAsync.ts:65:13)
    at Object.onceWrapper (events.js:422:26)
    at ChildProcess.emit (events.js:315:20)
    at maybeClose (internal/child_process.js:1051:16)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:287:5)
    ...
    at spawnAsync (/Users/aharada/.volta/tools/image/packages/expo-cli/lib/node_modules/expo-cli/node_modules/@expo/spawn-async/src/spawnAsync.ts:26:19)
    at YarnPackageManager._runAsync (/Users/aharada/.volta/tools/image/packages/expo-cli/lib/node_modules/expo-cli/node_modules/@expo/package-manager/src/NodePackageManagers.ts:314:31)
    at YarnPackageManager.addDevAsync (/Users/aharada/.volta/tools/image/packages/expo-cli/lib/node_modules/expo-cli/node_modules/@expo/package-manager/src/NodePackageManagers.ts:270:16)
    at installPackagesAsync (/Users/aharada/.volta/tools/image/packages/expo-cli/lib/node_modules/expo-cli/src/commands/utils/typescript/ensureTypeScriptSetup.ts:185:5)
    at ensureRequiredDependenciesAsync (/Users/aharada/.volta/tools/image/packages/expo-cli/lib/node_modules/expo-cli/src/commands/utils/typescript/ensureTypeScriptSetup.ts:121:7)
    at ensureTypeScriptSetupAsync (/Users/aharada/.volta/tools/image/packages/expo-cli/lib/node_modules/expo-cli/src/commands/utils/typescript/ensureTypeScriptSetup.ts:37:3)
```

nodeのバージョンを変更してみたり、node_modulesを削除・再インストールしてみたりしても動こない。

最終的には、yarnでinstallしていたexpo-cliをnpmでinstallしてプロジェクトを生成し直したら動いた。。なんでやねん...

```
$ npm install -g expo-cli
$ rm -rf expo-detox
$ expo init expo-detox
$ cd expo-detox
$ expo install
$ expo start
```

![Simulator Screenshot - iPhone SE (3rd generation) - 2023-06-21 at 12.35.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47677/db6a2112-0c9f-0443-fa47-ddfe4ceed0ba.png)

## Detoxを入れてテストが通るようにする

必要なライブラリなどをインストールする。

```
$ brew tap wix/brew
$ brew install applesimutils
$ yarn global add detox-cli

Usage Error: The 'yarn global' commands have been removed in 2.x - consider using 'yarn dlx' or a third-party plugin instead
```

ん？しばらく見ないうちに `yarn global add` がオワコンになっていたようだ。代わりに `yarn dlx detox-cli` する。

```
$ yarn dlx detox-cli
$ yarn add "jest@^29" --dev
$ npm install detox --save-dev
$ detox init
```

detoxコマンドを実行してみるとエラー。

```
$ detox
Failed to find Detox executable at path: /Users/aharada/source/node_modules/.bin/detox

Possible solutions:
1. Make sure your current working directory is correct.
2. Run "npm install" to ensure your "node_modules" directory is up-to-date.
3. Run "npm install detox --save-dev" for the fresh Detox installation in your project
```

その後も色々やってもこのエラーが解消出来なかったが、どうやらyarnでインストールしているのがダメっぽい。これまでexpoでの開発はyarn使っていたのでちょっと気になるが、npmでインストール＆ライブラリ管理する。

```
$ rm -rf node_modules
$ rm yarn.lock

$ npm install
$ npm install detox-cli --global
$ npm install "jest@^29" --save-dev
$ npm install detox --save-dev
```

これでdetoxコマンドが使えるようになったので、detoxでテストを実行するための初期設定をする。

```
$ detox init
```

.detoxrc.js のアプリ名の部分を置き換える。

```.detoxrc.js 
...
  apps: {
    'ios.debug': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/expodetox.app',
      build: 'xcodebuild -workspace ios/expodetox.xcworkspace -scheme expodetox -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
    },
    'ios.release': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/expodetox.app',
      build: 'xcodebuild -workspace ios/expodetox.xcworkspace -scheme expodetox -configuration Release -sdk iphonesimulator -derivedDataPath ios/build'
    },
    'android.debug': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
      reversePorts: [
        8081
      ]
...
```

ビルドしてみるもエラー。

```
$ detox build --configuration ios.sim.debug

...
xcodebuild: error: 'ios/expo-detox.xcworkspace' does not exist.


Important: 'detox build' is a convenience shortcut for calling your own build command, as provided in the config file.
Failures in this build command are not the responsibility of Detox. You are responsible for maintaining this command.

Command failed: xcodebuild -workspace ios/expo-detox.xcworkspace -scheme expo-detox -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build
```

こちらの記事によると、detoxはmanaged workflowでは動かない？的なことが書いてある。ほんまかいな
https://chrisgriffing.com/blog/e2e-test-expo-apps-with-detox

まあいい。深く考えずbare workflowにしてしまおう。

```
$ expo eject
$ cd ios
$ pod install
$ cd ..
```

再びビルドするも、よくわからんエラー。

```
$ detox build --configuration ios.sim.debug                 
    
    ** BUILD FAILED **


The following build commands failed:
	CompileSwift normal x86_64 (in target 'ExpoModulesCore' from project 'Pods')
	CompileSwiftSources normal x86_64 com.apple.xcode.tools.swift.compiler (in target 'ExpoModulesCore' from project 'Pods')
	CompileSwift normal arm64 (in target 'ExpoModulesCore' from project 'Pods')
	CompileSwiftSources normal arm64 com.apple.xcode.tools.swift.compiler (in target 'ExpoModulesCore' from project 'Pods')
(4 failures)
```

調べてみると、Xcodeのバージョンの問題っぽい。Xcodeをバージョンアップするには、MacOS自体のアップデートが強いられるようなので、MacOSをVenturaにしてから、App StoreでXcodeをアップデート。

detoxのテストを実行してみるもエラー。道のりは険しいなおい。。。

```
$ detox test --configuration ios.sim.debug
12:39:00.356 detox[14942] B jest --config e2e/jest.config.js
 FAIL  e2e/starter.test.js run...
  ● Test suite failed to run

    DetoxRuntimeError: /Users/aharada/Library/Detox/ios/861165bcfef42334b197b5e21188c9265459c337/Detox.framework could not be found, this means either you changed a version of Xcode or Detox postinstall script was unsuccessful.
            To attempt a fix try running 'detox clean-framework-cache && detox build-framework-cache'

      at IosSimulatorEnvValidator.validate (node_modules/detox/src/validation/ios/IosSimulatorEnvValidator.js:12:13)

12:39:02.093 detox[14942] E Command failed with exit code = 1:
jest --config e2e/jest.config.js
```

この投稿を参考に`detox rebuild-framework-cache`して、detoxのテストを実行してみるもやはりエラー。

https://github.com/wix/Detox/issues/2302

```
$ detox rebuild-framework-cache
Removing framework binaries from /Users/aharada/Library/Detox
Extracting Detox framework...
Done

$ detox test --configuration ios.sim.debug
17:29:23.489 detox[16610] B jest --config e2e/jest.config.js
 FAIL  e2e/starter.test.js run...
  ● Test suite failed to run

    DetoxRuntimeError: Failed to find a device by type = "iPhone 12"

    HINT: Run 'applesimutils --list' to list your supported devices. It is advised only to specify a device type, e.g., "iPhone Xʀ" and avoid explicit search by OS version.

      at SimulatorAllocDriver._queryDevices (node_modules/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js:115:13)
      at SimulatorAllocDriver._groupDevicesByStatus (node_modules/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js:94:27)
      at SimulatorAllocDriver._findOrCreateDevice (node_modules/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js:81:29)
      at node_modules/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js:31:14
      at safeAsync (node_modules/detox/src/utils/safeAsync.js:4:13)
      at node_modules/detox/src/devices/DeviceRegistry.js:73:24
      at ExclusiveLockfile.exclusively (node_modules/detox/src/utils/ExclusiveLockfile.js:41:15)
      at SimulatorAllocDriver.allocate (node_modules/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js:30:18)

17:29:25.592 detox[16610] E Command failed with exit code = 1:
jest --config e2e/jest.config.js
```

今度は単に、テスト用に指定されているiPhoneシミュレータがインストールされてないだけっぽいので、指定するシミュレータを変更。

```.detoxrc.js
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: {
        type: 'iPhone 14'
      }
    },
```

再びdetoxのテストを実行。

```
$ detox test --configuration ios.sim.debug
```

シミュレータとアプリは起動したが、エラー画面が出ている。

```
No bundle URL present.

Make sure you're running a packager server or have included a .jsbundle file in your application bundle.
```

![Simulator Screenshot - iPhone 14 - 2023-06-21 at 17.34.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47677/9eae72b5-ba9d-4ecb-5c9b-f97f55388e03.png)

こちらの投稿を参考に一つ一つ試す。
https://stackoverflow.com/questions/42610070/what-is-the-meaning-of-no-bundle-url-present-in-react-native

最終的にこれでいけた！ライセンスはGUIでagreeしてたと思うんだがな。。

```
$ sudo xcodebuild -license
```

いけたあああああああ！あとはテストコード自体が生成されたままなので、実際のexpoのブランクアプリの実装にしたがってテストが通るように直す。

```starter.test.js
describe('Example', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should have welcome screen', async () => {
    await expect(element(by.text('Open up App.tsx to start working on your app!'))).toBeVisible();
  });

  // it('should show hello screen after tap', async () => {
  //   await element(by.id('hello_button')).tap();
  //   await expect(element(by.text('Hello!!!'))).toBeVisible();
  // });

  // it('should show world screen after tap', async () => {
  //   await element(by.id('world_button')).tap();
  //   await expect(element(by.text('World!!!'))).toBeVisible();
  // });
});
```

テストが通ったああああああああ！！！

```
$ detox test --configuration ios.sim.debug                                                                       ✘ 1
09:39:03.554 detox[32882] B jest --config e2e/jest.config.js
09:39:08.991 detox[32883] i starter.test.js is assigned to 1D6229EF-4969-4A07-9E1B-CAF04E5554FE (iPhone 14)
09:39:10.600 detox[32883] i com.a.harada.expodetox launched. To watch simulator logs, run:
        /usr/bin/xcrun simctl spawn 1D6229EF-4969-4A07-9E1B-CAF04E5554FE log stream --level debug --style compact --predicate 'process == "expodetox"'
09:39:12.167 detox[32883] i Example: should have welcome screen
09:39:13.378 detox[32883] i Example: should have welcome screen [OK]

 PASS  e2e/starter.test.js (8.964 s)
  Example
    ✓ should have welcome screen (1209 ms)
```

大変長い道のりだった。。
次は既存アプリにDetoxを乗っけようと思うけど、Expoのバージョンも古かったりするし、難関な気がする。。