1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-11-14

概要

前回 は Cucumberのみを使ってドメインの単体テストを行った。
今回は Playwright を使ってe2eテストを行った。bunでmonorepo構成のためか、npm init playwright@latestに失敗したので手動で設定したメモを残す。

ソースコード

準備

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

ソースコード

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が発生。

apps/frontend/cucumber.mjs
export default {
  paths: ['**/features/*.feature'],
  require: ['**/tests/*.steps.ts'],
  requireModule : ['ts-node/register']
};
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" が表示される
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();
  },
);

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

2024.11.16 追記 "type": "module" を削除せずにESMで動作させる

もとのviteの構成をあまり変えたくない場合。

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

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に対応していない ( *ts-node issue ) のでcompilerOptions:moduleでESM(Nodenext)を指定する。

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

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

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"
  }
}

発生していたエラー

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?

参考

参考: CucumberとPlaywright+生成AIを使ってTypeScriptで自動テストを書こう!WebアプリやAndroidネイティブアプリもBDDで!
公式 : Installation
公式 : github
github - Playwright with Typescript - Cucumber - BDD
package.json で type: "module" を設定していると、TypeScript による Playwright のテストが、その実行時に構文エラー
Cucumber.js + TypeScriptではまった話

2024.11.16 追記
ViteとTypeScriptを5系にバージョンアップする
サバイバルTypescript - プロジェクト参照 (project references)
CompilerOptions - module

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?