Cucumber+Playwright+Bunでフロントエンドのe2eテストを行ったメモ

# 概要
[前回](https://qiita.com/hibohiboo/items/b368e41bf7826ecdd118) は Cucumberのみを使ってドメインの単体テストを行った。
今回は Playwright を使ってe2eテストを行った。bunでmonorepo構成のためか、`npm init playwright@latest`に失敗したので手動で設定したメモを残す。

[ソースコード](https://github.com/hibohiboo/odyssage/blob/dfbe2ce32c182bfe1aa58e705a2676c8d0fadd64/apps/frontend/cucumber.mjs)

## 準備

```
$ bun i -d @playwright/test @cucumber/cucumber ts-node
$ npx playwright install
```

## ソースコード

```diff:apps/frontend/package.json
{
  "name": "@odyssage/frontend",
-  "type": "module",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "bunx --bun vite",
    "build": "tsc -b && bunx --bun vite build",
    // 省略
+    "e2e-test": "cucumber-js --config cucumber.mjs --exit"
  },
  "dependencies": {
    "@odyssage/core": "workspace:*",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@cucumber/cucumber": "^11.0.1",
    "@odyssage/eslint-config-custom": "workspace:*",
    "@odyssage/tsconfig": "workspace:*",
    "@playwright/test": "^1.48.2",
    "ts-node": "^10.9.2",
    // 省略
    "vite": "^5.4.11"
  }
}

```

`"type": "module"`を指定していると`Error: Cucumber expected a CommonJS module`が発生。

```js:apps/frontend/cucumber.mjs
export default {
  paths: ['**/features/*.feature'],
  require: ['**/tests/*.steps.ts'],
  requireModule : ['ts-node/register']
};
```
```feature:apps/frontend/e2e/features/characterList.feature
Feature: Character List Management

  Background:
    Given アプリが起動している

  Scenario: キャラクターリストが空の場合
    When キャラクターリストページを表示する
    Then "キャラクターが見つかりませんでした" と表示される

  Scenario: キャラクターを追加する
    Given キャラクターリストが空である
    When "John Doe" という名前のキャラクターを追加する
    Then キャラクターリストに "John Doe" が表示される

  Scenario: キャラクターを複数追加して表示する
    Given キャラクターリストが空である
    When 以下のキャラクターを追加する:
      | name     |
      | Alice    |
      | Bob      |
    Then キャラクターリストに "Alice" と "Bob" が表示される
```

```ts:apps/frontend/e2e/tests/characterList.steps.ts
import { Given, When, Then, Before } from '@cucumber/cucumber';
import { expect, chromium, Page } from '@playwright/test';

let page: Page;

Before(async () => {
  const browser = await chromium.launch({ headless: false }); // headless: true にするとブラウザが表示されない
  const context = await browser.newContext();
  page = await context.newPage();
});
Given('アプリが起動している', async () => {
  await page.goto('http://localhost:5173');
});

Given('キャラクターリストが空である', async () => {
  await page.evaluate(() => localStorage.clear());
  await page.reload();
});

When('キャラクターリストページを表示する', async () => {
  await page.click('text=キャラクターリスト');
});

Then('"キャラクターが見つかりませんでした" と表示される', async () => {
  const message = await page.locator('text=キャラクターが見つかりませんでした');
  await expect(message).toBeVisible();
});

When('{string} という名前のキャラクターを追加する', async (name) => {
  await page.fill('input[placeholder="キャラクター名を入力"]', name);
  await page.click('button:has-text("キャラクターを追加")');
});

Then('キャラクターリストに {string} が表示される', async (name) => {
  const character = await page.locator(`text=${name}`);
  await expect(character).toBeVisible();
});

When('以下のキャラクターを追加する:', async (table) => {
  for (const { name } of table.hashes()) {
    await page.fill('input[placeholder="キャラクター名を入力"]', name);
    await page.click('button:has-text("キャラクターを追加")');
  }
});

Then(
  'キャラクターリストに {string} と {string} が表示される',
  async (name1, name2) => {
    const character1 = await page.locator(`text=${name1}`);
    const character2 = await page.locator(`text=${name2}`);
    await expect(character1).toBeVisible();
    await expect(character2).toBeVisible();
  },
);

```

```diff:apps/frontend/eslint.config.ts
import customConfig from '@odyssage/eslint-config-custom/frontend.js';
import tseslint from 'typescript-eslint';

export default tseslint.config({
  extends: [
    ...customConfig,
+    {
+      files: ['**/tests/**'],
+      rules: {
+        'import/extensions': ['off'],
+        'import/no-extraneous-dependencies': ['off'],
+        'import/no-unresolved': ['off'],
+        'sonarjs/slow-regex': ['off'],
+        'lintsonarjs/no-empty-test-file': ['off'],
+        'no-restricted-syntax': ['off'],
+        'no-await-in-loop': ['off'],
+      },
+    },
  ],

  settings: {
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
      typescript: {
        project: './tsconfig.app.json',
      },
    },
  },
});
```

# テスト実行
```
bun run dev
```

```
bun run e2e-test
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/14e1abae-d3f6-5bcc-206d-28414cb95b8a.png)

# 2024.11.16 追記 "type": "module" を削除せずにESMで動作させる
もとのviteの構成をあまり変えたくない場合。

まずは commonJSからesmにcucumberの設定を変更。

```diff:apps/frontend/cucumber.mjs
export default {
  paths: ['**/features/*.feature'],
-  require: ['**/tests/*.steps.ts'],
-  requireModule : ['ts-node/register']
+  import: ['**/tests/*.steps.ts'],
+  loader : ['ts-node/esm']
};
```

cucumberで使用するts-nodeが[references](https://typescriptbook.jp/reference/advanced-topics/project-references)に対応していない ( [*ts-node issue](https://github.com/TypeStrong/ts-node/issues/1057) ) ので[compilerOptions:module](https://qiita.com/urasaku77/items/b07f1b012d3985fffdd6#module)でESM(Nodenext)を指定する。

```diff:apps/frontend/tsconfig.ts
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" },
  ],
+  "compilerOptions": {
+    "module": "NodeNext"
+  }
}
```

これで`"type":"module"`をもとに戻しても大丈夫


```diff:apps/frontend/package.json
{
  "name": "@odyssage/frontend",
+  "type": "module",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "bunx --bun vite",
    "build": "tsc -b && bunx --bun vite build",
    // 省略
    "e2e-test": "cucumber-js --config cucumber.mjs --exit"
  },
  "dependencies": {
    "@odyssage/core": "workspace:*",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@cucumber/cucumber": "^11.0.1",
    "@odyssage/eslint-config-custom": "workspace:*",
    "@odyssage/tsconfig": "workspace:*",
    "@playwright/test": "^1.48.2",
    "ts-node": "^10.9.2",
    // 省略
    "vite": "^5.4.11"
  }
}

```

<details><summary>発生していたエラー</summary><div>

tsconfig.tsに`references`をつけただけの時のエラー

```
$ bun run e2e-test
$ cucumber-js --config cucumber.mjs --exit
(node:25960) [DEP0180] DeprecationWarning: fs.Stats constructor is deprecated.
(Use `node --trace-deprecation ...` to show where the warning was created)
ReferenceError: exports is not defined in ES module scope
    at file:///D:/projects/odyssage/packages/bdd-e2e-test/e2e/step-definitions/characterList.steps.ts:38:23
    at ModuleJob.run (node:internal/modules/esm/module_job:268:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:543:26)
    at async getSupportCodeLibrary (D:\projects\odyssage\node_modules\@cucumber\cucumber\lib\api\support.js:32:9)
    at async runCucumber (D:\projects\odyssage\node_modules\@cucumber\cucumber\lib\api\run_cucumber.js:49:11)
    at async Cli.run (D:\projects\odyssage\node_modules\@cucumber\cucumber\lib\cli\index.js:56:29)
    at async Object.run [as default] (D:\projects\odyssage\node_modules\@cucumber\cucumber\lib\cli\run.js:29:18)
error: script "e2e-test" exited with code 1
```


loaderに ts-node/esm を cliオプションで指定した時のエラー `--loader ts-node/esm`

```
(node:4356) [DEP0180] DeprecationWarning: fs.Stats constructor is deprecated.
(Use `node --trace-deprecation ...` to show where the warning was created)
e2e/tests/characterList.steps.ts(1,43): error TS2792: Cannot find module '@cucumber/cucumber'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?
e2e/tests/characterList.steps.ts(2,34): error TS2792: Cannot find module '@playwright/test'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?
e2e/tests/characterList.steps.ts(3,27): error TS2792: Cannot find module '@playwright/test'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?

```

</div></details>

# 参考
[参考: CucumberとPlaywright＋生成AIを使ってTypeScriptで自動テストを書こう！WebアプリやAndroidネイティブアプリもBDDで！](https://note.shiftinc.jp/n/n1fa2e3780f73)  
[公式 : Installation](https://playwright.dev/docs/intro)  
[公式 : github](https://github.com/microsoft/playwright)  
[github - Playwright with Typescript - Cucumber - BDD](https://github.com/adamcegielka/playwright-cucumber-bdd-typescript/tree/main)  
[package.json で type: "module" を設定していると、TypeScript による Playwright のテストが、その実行時に構文エラー](https://devadjust.exblog.jp/29186437/)
[Cucumber.js + TypeScriptではまった話](https://qiita.com/abcaaa/items/31c1a3dd9d083b66a574)

2024.11.16 追記
[ViteとTypeScriptを5系にバージョンアップする](https://qiita.com/KentaYoshimura/items/7756e39c954bd849c23d)
[サバイバルTypescript - プロジェクト参照 (project references)](https://typescriptbook.jp/reference/advanced-topics/project-references)
[CompilerOptions - module](https://qiita.com/urasaku77/items/b07f1b012d3985fffdd6#module)