我が名は神龍……どんなテストもひとつだけ自動化してやろう (Playwright編)

## 概要
[我が名は神龍……どんなテストもひとつだけ自動化してやろう](https://qiita.com/tsuemura/items/56ba9942565963858d8f)をPlaywrightで試してみたメモ。

[ソースコード](https://github.com/hibohiboo/async-ttrpg/blob/382ec7fe0b9671f86a53ef2ace2e8a6228796f89/e2e/codecept.conf.ts)

**環境**
- Windows 11 Home 23H2 22631.3958
- docker-desktop v4.32.0 
- nodejs-lts v20.16.0 

## 『我が名は神龍……どんなテストもひとつだけ自動化してやろう』
じゃ、じゃあCRUD画面のブラウザテストの自動化をお願いします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/7dfd3409-971f-fc6e-6221-9f6a486479d5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/1bf8f9b0-43c9-7ec8-d76f-5b3c6c364bb2.png)

* Chromeで対象ページにアクセスして
* 「キャラクターを追加」ボタンをクリックして
* 「ぱんてぃ」と入力して
* 「Save」ボタンをクリックして
* 一覧に「ぱんてぃ」が追加されていることを確認

## 『よかろう……たやすい願いだ』
### まずはライブラリのインストールと初期設定をしてやろう……

```bash
# プロジェクトフォルダを作り、移動します
$ mkdir e2e
$ cd e2e
# [ライブラリのインストール]
# CodeceptJSをインストールします。nodeとnpmが必要ですので別途入れておいて下さい。
$ npx create-codeceptjs .
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/7da6f86c-6608-bfae-4134-a4fe047984ea.png)

```bash
# Playwrightをインストールします。 Chromiumのダウンロードが入るので通信量かさむ場合があります。テザリングとかしてる場合は要注意。
$ npx playwright install 

# [CodeceptJSの初期設定]
$ npx codeceptjs init
# 対話式で色々聞かれます。デフォルトから下記を変更しています
# TypeScript? Yes
# Base url http://localhost:5173
? Do you plan to write tests in TypeScript? Yes
? Where are your tests located? ./*_test.ts
? What helpers do you want to use? Playwright
? Where should logs, screenshots, and reports to be stored? ./output
? Do you want to enable localization for tests? http://bit.ly/3GNUBbh English (no localization)
Configure helpers...
? [Playwright] Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron chromium
? [Playwright] Base url of site to be tested http://localhost:5173
? [Playwright] Show browser window No
```

### 次に神龍語の翻訳ファイルを設定してやろう……
https://raw.githubusercontent.com/hibohiboo/async-ttrpg/382ec7fe0b9671f86a53ef2ace2e8a6228796f89/e2e/ja-SR.js からダウンロードしてやろう
これはコードの記述とテストレポート生成に使われる……

```
$ curl -o ja-SR.js https://raw.githubusercontent.com/hibohiboo/async-ttrpg/382ec7fe0b9671f86a53ef2ace2e8a6228796f89/e2e/ja-SR.js
```

<details><summary>ダウンロードしたファイルはこんな感じ</summary><div>

```js
module.exports = {
  I: '我が名は神龍',
  contexts: {
    Feature: 'テストの大枠を定義してやろう',
    Scenario: 'テストケースを定義してやろう',
    ScenarioOutline: '複数のデータセットを持つテストケースを定義してやろう',
    Before: 'テストの準備をしてやろう',
  },
  actions: {
    amOutsideAngularApp: 'Angularの外に出てやろう',
    amInsideAngularApp: 'Angularの中に入ってやろう',
    waitForElement: '要素が表示されるまで待ってやろう',
    waitForClickable: 'クリック可能になるまで待ってやろう',
    waitForVisible: '要素が見えるようになるまで待ってやろう',
    waitForText: 'テキストが表示されるまで待ってやろう',
    moveTo: 'ページを移動してやろう',
    refresh: 'ページを更新してやろう',
    haveModule: 'モジュールを持っているか確認してやろう',
    resetModule: 'モジュールをリセットしてやろう',
    amOnPage: 'URLにアクセスしてやろう',
    click: 'クリックしてやろう',
    doubleClick: 'ダブルクリックしてやろう',
    see: 'テキストがあるか確認してやろう',
    dontSee: 'テキストがないことを確認してやろう',
    selectOption: 'オプションを選んでやろう',
    fillField: 'フィールドに入力してやろう',
    pressKey: 'キー入力してやろう',
    triggerMouseEvent: 'マウスイベントを発火させてやろう',
    attachFile: 'ファイルを添付してやろう',
    seeInField: '入力フィールドに文字が入っているか確認してやろう',
    dontSeeInField: '入力フィールドに文字が入っていないか確認してやろう',
    appendField: '入力フィールドに文字を追加してやろう',
    checkOption: 'オプションをチェックしてやろう',
    seeCheckboxIsChecked: 'チェックされているか確認してやろう',
    dontSeeCheckboxIsChecked: 'チェックされていないことを確認してやろう',
    grabTextFrom: 'テキストを取得してやろう',
    grabValueFrom: '入力値を取得してやろう',
    grabAttributeFrom: '要素を取得してやろう',
    seeInTitle: 'タイトルに文字が含まれるか確認してやろう',
    dontSeeInTitle: 'タイトルに文字が含まれないことを確認してやろう',
    grabTitle: 'タイトルを取得してやろう',
    seeElement: '要素があるか確認してやろう',
    dontSeeElement: '要素がないことを確認してやろう',
    seeInSource: 'ソースにあるか確認してやろう',
    dontSeeInSource: 'ソースにないことを確認してやろう',
    executeScript: 'スクリプトを実行してやろう',
    executeAsyncScript: '非同期スクリプトを実行してやろう',
    seeInCurrentUrl: 'URLに含まれるか確認してやろう',
    dontSeeInCurrentUrl: 'URLに含まれないことを確認してやろう',
    seeCurrentUrlEquals: 'URLが等しいか確認してやろう',
    dontSeeCurrentUrlEquals: 'URLが等しくないことを確認してやろう',
    saveScreenshot: 'スクリーンショットを保存してやろう',
    setCookie: 'Cookieをセットしてやろう',
    clearCookie: 'Cookieをクリアしてやろう',
    seeCookie: 'Cookieに含まれることを確認してやろう',
    dontSeeCookie: 'Cookieに含まれないことを確認してやろう',
    grabCookie: 'Cookieを取得してやろう',
    resizeWindow: 'ウィンドウをリサイズしてやろう',
    wait: '待ってやろう',
  },
}
```

</div></details>

ダウンロードした翻訳ファイルを設定ファイルに追加してやろう……

```diff:e2e/codecept.conf.ts
import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';
// turn on headless mode when running with HEADLESS=true environment variable
// export HEADLESS=true && npx codeceptjs run
setHeadlessWhen(process.env.HEADLESS);

// enable all common plugins https://github.com/codeceptjs/configure#setcommonplugins
setCommonPlugins();

export const config: CodeceptJS.MainConfig = {
  tests: './form_test.js',
  output: './output',
  helpers: {
    Playwright: {
      browser: 'chromium',
      url: 'http://localhost:5173',
      show: false,
    },
  },
+  translation: './ja-SR.js',
  include: {
    I: './steps_file',
  },
  name: 'e2e',
};

```

### さあ　おまちかねのテストコードだ
このコードを `form_test.js` として保存するがいい……

```js:e2e/form_test.js
テストの大枠を定義してやろう('キャラクターのCRUD');

テストケースを定義してやろう('キャラクターの追加',  ({I}) => {
  const 我が名は神龍 = I;
  const 追加 = locate('a').withText('キャラクターを追加').as('追加')
  const Save = locate('button').withText('Save').as('Save')

  我が名は神龍.URLにアクセスしてやろう('/')
  我が名は神龍.クリックしてやろう(追加)
  我が名は神龍.フィールドに入力してやろう('CharacterName', 'ぱんてぃ')
  我が名は神龍.入力フィールドに文字が入っているか確認してやろう('CharacterName', 'ぱんてぃ')
  我が名は神龍.クリックしてやろう(Save)
  我が名は神龍.テキストが表示されるまで待ってやろう('キャラクターを追加')
  我が名は神龍.テキストがあるか確認してやろう('ぱんてぃ')
});

```

```
$ npm run codeceptjs
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/f049cbc4-9810-dc15-c0f4-42d9e46408c1.png)

## 『願いはかなった　さらばだ』
まってください！！ブラウザが開かずテスト結果だけ出てきても本当にテストされたのかわからないです！
結果が見えるようにしてもらえませんか！？！？！？？！？！？？！？！？
## 『いいだろう　たやすい願いだ』
### Playwright実行中にブラウザが開くようにしてやろう
Playwrightの起動オプションを `show: true` にするだけで良い
```diff
import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';
// turn on headless mode when running with HEADLESS=true environment variable
// export HEADLESS=true && npx codeceptjs run
setHeadlessWhen(process.env.HEADLESS);

// enable all common plugins https://github.com/codeceptjs/configure#setcommonplugins
setCommonPlugins();

export const config: CodeceptJS.MainConfig = {
  tests: './form_test.js',
  output: './output',
  helpers: {
    Playwright: {
      browser: 'chromium',
      url: 'http://localhost:5173',
-      show: false,
+      show: true,
    },
  },
  translation: './ja-SR.js',
  include: {
    I: './steps_file',
  },
  name: 'e2e',
};

```

## 『願いはかなった　さらばだ』

待って下さい！！！！！！！！
クライアントに提出するスクリーンショットが必要なんです！！お願いします！！！！！！！！！！！！！

## 『いいだろう　たやすい願いだ』
### スクリーンショットが自動で撮影されるようにしてやろう
`StepByStepReport` プラグインを有効にすると、各ステップのスクリーンショットが撮影され、いい感じのスライドにまとめられる

```diff:e2e/codecept.conf.ts
import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';
// turn on headless mode when running with HEADLESS=true environment variable
// export HEADLESS=true && npx codeceptjs run
setHeadlessWhen(process.env.HEADLESS);

// enable all common plugins https://github.com/codeceptjs/configure#setcommonplugins
setCommonPlugins();

export const config: CodeceptJS.MainConfig = {
  tests: './form_test.js',
  output: './output',
  helpers: {
    Playwright: {
      browser: 'chromium',
      url: 'http://localhost:5173',
       show: true,
    },
  },
  translation: './ja-SR.js',
  include: {
    I: './steps_file',
  },
+  plugins: {
+    stepByStepReport: {
+      enabled: true,
+      deleteSuccessful: false,
+    },
+  },  
  name: 'e2e',
};

```

## 『願いはかなった　さらばだ』
待って下さい！！！！！！！！！！！！！！！！
データベースのテーブルが汚れてしまって次回のテストがうまく行きません！
テーブルをきれいにしてからテストしてください！

## 『いいだろう　たやすい願いだ』
### テスト前にテーブルのデータを一つ残らず消してやろう

```diff:e2e/form_test.js
テストの大枠を定義してやろう('キャラクターのCRUD');
+ テストの準備をしてやろう(() => {
+   const exec = require('child_process').exec;
+   // Character テーブルのデータをすべて消してやろう
+   exec(
+     'docker exec -d mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P MyPassword@123 -C -d test -Q "TRUNCATE TABLE atrpg.Character;"',
+   );
+ });

テストケースを定義してやろう('キャラクターの追加',  ({I}) => {
  const 我が名は神龍 = I;
  const 追加 = locate('a').withText('キャラクターを追加').as('追加')
  const Save = locate('button').withText('Save').as('Save')

  我が名は神龍.URLにアクセスしてやろう('/')
  我が名は神龍.クリックしてやろう(追加)
  我が名は神龍.フィールドに入力してやろう('CharacterName', 'ぱんてぃ')
  我が名は神龍.入力フィールドに文字が入っているか確認してやろう('CharacterName', 'ぱんてぃ')
  我が名は神龍.クリックしてやろう(Save)
  我が名は神龍.テキストが表示されるまで待ってやろう('キャラクターを追加')
  我が名は神龍.テキストがあるか確認してやろう('ぱんてぃ')
});

```

## 『願いはかなった　さらばだ』
待って下さい！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！
せっかくTypescriptを入れたのでテストをTypescriptで書いて型補完が効くようにしたいです！

## 『いいだろう　たやすい願いだ』
### Typescriptでかけるようにしてやろう
`form_test.js`を`form_test.ts`に変更して下記のコードを上書きするといい……

```ts:e2e/form_test.ts
Feature('キャラクターのCRUD');

Before(() => {
  const exec = require('child_process').exec;
  exec(
    'docker exec -d mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P MyPassword@123 -C -d test -Q "TRUNCATE TABLE atrpg.Character;"',
  );
});

Scenario('キャラクターの追加', ({ I }) => {
  const 追加 = locate('a').withText('キャラクターを追加').as('追加');
  const Save = locate('button').withText('Save').as('Save');

  I.amOnPage('/');
  I.click(追加);
  I.fillField('CharacterName', 'ぱんてぃ');
  I.seeInField('CharacterName', 'ぱんてぃ');
  I.click(Save);
  I.waitForText('キャラクターを追加');
  I.see('ぱんてぃ');
});
```

configファイルも設定してやろう……

```diff:e2e/codecept.conf.ts
import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';
// turn on headless mode when running with HEADLESS=true environment variable
// export HEADLESS=true && npx codeceptjs run
setHeadlessWhen(process.env.HEADLESS);

// enable all common plugins https://github.com/codeceptjs/configure#setcommonplugins
setCommonPlugins();

export const config: CodeceptJS.MainConfig = {
-  tests: './form_test.js',
+  tests: './form_test.ts',
  output: './output',
  helpers: {
    Playwright: {
      browser: 'chromium',
      url: 'http://localhost:5173',
      show: true,
    },
  },
-   translation: './ja-SR.js',
+  translation: 'MyLang',
+  vocabularies: ['./ja-SR.js'],
  plugins: {
    stepByStepReport: {
      enabled: true,
      deleteSuccessful: false,
    },
  },
  include: {
    I: './steps_file',
  },
  name: 'e2e',
};
```


## 『願いはかなった　さらばだ』


ありがとうございます！！！！！！！！！！

## 参考

[我が名は神龍……どんなテストもひとつだけ自動化してやろう](https://qiita.com/tsuemura/items/56ba9942565963858d8f)
[CodeceptJS - quickstart](https://codecept.io/quickstart/)
[CodeceptJS - translation](https://codecept.io/translation/#how-it-works)

## おまけ：エラー
### フロントエンドのフォルダでcreate実行してエラー
```
 npx codeceptjs init

  Welcome to CodeceptJS initialization tool
  It will prepare and configure a test environment for you

 Useful links:

  👉 How to start testing ASAP: https://codecept.io/quickstart/#init
  👉 How to select helper: https://codecept.io/basics/#architecture
  👉 TypeScript setup: https://codecept.io/typescript/#getting-started

Installing to D:\projects\async-ttrpg\apps\pl-app
? Do you plan to write tests in TypeScript? Yes
? Where are your tests located? ./*_test.ts
? What helpers do you want to use? Playwright
? Where should logs, screenshots, and reports to be stored? ./output
? Do you want to enable localization for tests? http://bit.ly/3GNUBbh English (no localization)
Configure helpers...
? [Playwright] Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron chromium
? [Playwright] Base url of site to be tested http://localhost:5173
? [Playwright] Show browser window No

Steps file created at ./steps_file.ts
Config created at D:\projects\async-ttrpg\apps\pl-app\codecept.conf.ts
Directory for temporary output files created at './output'
tsconfig.json already exists at D:\projects\async-ttrpg\apps\pl-app\tsconfig.json
CodeceptJS should be installed locally
Installing packages:  typescript, ts-node, @types/node, codeceptjs

changed 2 packages, and audited 1288 packages in 6s

206 packages are looking for funding
  run `npm fund` for details

12 vulnerabilities (4 low, 2 moderate, 6 high)

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: D:\projects\async-ttrpg\apps\pl-app\codecept.conf.ts
require() of ES modules is not supported.
require() of D:\projects\async-ttrpg\apps\pl-app\codecept.conf.ts from D:\projects\async-ttrpg\node_modules\codeceptjs\lib\config.js is an ES module file as it is a .ts file whose nearest parent package.json contains "type": "module" which defines all .ts files in that package scope as ES modules.
Instead change the requiring code to use import(), or remove "type": "module" from \apps\pl-app\package.json.

    at createErrRequireEsm (\node_modules\ts-node\dist-raw\node-internal-errors.js:46:15)
    at assertScriptCanLoadAsCJSImpl (\node_modules\ts-node\dist-raw\node-internal-modules-cjs-loader.js:584:11)

```

### Playwrightいれわすれ

```
$ npm run codeceptjs

> codeceptjs-tests@0.1.0 codeceptjs
> codeceptjs run form_test.js --steps

CodeceptJS v3.6.4 #StandWithUkraine
Using test root "D:\projects\async-ttrpg\e2e"

作成のテスト --
  × "before all" hook: codeceptjs.beforeSuite for "test something" in 5ms
Error: browserType.launch: Executable doesn't exist at C:\Users\hisa\AppData\Local\ms-playwright\chromium-1124\chrome-win\chrome.exe
╔═════════════════════════════════════════════════════════════════════════╗
║ Looks like Playwright Test or Playwright was just installed or updated. ║
║ Please run the following command to download new browsers:              ║
║                                                                         ║
║     npx playwright install                                              ║
║                                                                         ║
║ <3 Playwright Team                                                      ║
╚═════════════════════════════════════════════════════════════════════════╝

-- FAILURES:

  1) 作成のテスト
       "before all" hook: codeceptjs.beforeSuite for "test something":
     browserType.launch: Executable doesn't exist at C:\Users\hisa\AppData\Local\ms-playwright\chromium-1124\chrome-win\chrome.exe
╔═════════════════════════════════════════════════════════════════════════╗
║ Looks like Playwright Test or Playwright was just installed or updated. ║
║ Please run the following command to download new browsers:              ║
║                                                                         ║
║     npx playwright install                                              ║
║                                                                         ║
║ <3 Playwright Team                                                      ║
╚═════════════════════════════════════════════════════════════════════════╝
  browserType.launch: Executable doesn't exist at C:\Users\hisa\AppData\Local\ms-playwright\chromium-1124\chrome-win\chrome.exe
  ╔═════════════════════════════════════════════════════════════════════════╗
  ║ Looks like Playwright Test or Playwright was just installed or updated. ║




  FAIL  | 0 passed, 1 failed   // 7ms
Run with --verbose flag to see complete NodeJS stacktrace

```

### Dockerでデータベース作るときに書きかたをミスってエラー
いっしょうつながらない
```
mssql  | Sqlcmd: Error: Microsoft ODBC Driver 18 for SQL Server : Login timeout expired.
mssql  | Sqlcmd: Error: Microsoft ODBC Driver 18 for SQL Server : TCP Provider: Error code 0x2749.
mssql  | Sqlcmd: Error: Microsoft ODBC Driver 18 for SQL Server : A network-related or instance-specific error has occurred while establishing a connection to mssql. Server is not found or not accessible. Check if instance name is correct and if SQL Server is configured to allow remote connections. For more information see SQL Server Books Online..                  
mssql exited with code 1
```

[Docker & Microsoft SQL Server(初期データ登録まで)](https://qiita.com/75ks/items/b156905968535e23278c) を参考にした`docker-compose.yml`で解決

### FunctionsからローカルのDBにつなげない
`trustServerCertificate=true`の指定が漏れていた。

```
    "DATABASE_URL":"sqlserver://localhost:1433;database=test;user=sa;password=MyPassword@123;trustServerCertificate=true;encrypt=true;schema=atrpg
```