Nuxt3 に Playwright で E2E Snapshot Test を導入してみた

# 前提条件

- Mac book
- Nuxt .. v3.0.0-rc.6
- Playwright, @playwright/test .. v1.25.1
- yarn .. v1.22.17

Nuxt の構成
- SSR モード
- Server Api あり (./server/api)
    - ./server/api 内で、バックエンドから取ってきたデータを ViewModel に変換してブラウザに返すような構成
  

# 1. MockServer 化

まず、統一した実行結果がほしいので、ViewModel を返却する Server Api をモックサーバ化してみる

Serve Api は nitro で動いているので、 nuxt.config.ts を以下のように書き換えるとうまくいった。

```ts:nuxt.config.ts
  hooks: {
    'nitro:config': (config: NitroConfig) => {
      // nitro の option 側に設定する場合、 scanDirs が nuxt 固有の設定を含んでしまうため、 mock モードではそれをここで上書きするよ
      // isDebugMode は TEST=true で true を返却するメソッドがどこかで定義されてるよ
      if (isDebugMode()) {
        const testServer = resolve(__dirname, './test/server')

        config.srcDir = testServer
        config.scanDirs = [testServer]
      }
    }
  },
```

それから、 `test/server/api` 配下を、 `server/api` 配下と同じ構成、同じ型の返却となるように構成するよ。

基本的には、jsonの固定データが変えるような実装ができればいい感じ。
大体以下のようなコードになるね。超シンプル。

```ts:test/server/api/example.get.ts
import type { ExampleViewModel } from '~/lib/models/example'

import mock from '~~/test/server/datas/examples.json'

export default defineEventHandler<ExampleViewModel[]>(async (event) => {
  return mock
})
```

これで `TEST=true yarn dev` `TEST=true　yarn build` で、モックサーバを含む Nuxt サーバの起動・ビルドができるようになった。


# 2. Playwright を導入

とりあえずインストールしなければ始まらないね。

```sh
yarn add playwright @playwright/test -D
```

インストール後は、プロジェクトのルートに 以下のような感じで playwright.config.ts を記述してみる

作成中の web アプリは ipad の横画面で見る想定の作りをしているので、それに対応するように設定した。

```ts:playwright.config.ts
import { PlaywrightConfig, devices } from '@playwright/test'

function definePlaywrightConfig(config: PlaywrightConfig): PlaywrightConfig {
  return config
}

export default definePlaywrightConfig({
  projects: [
    {
      name: 'ipad pro 11',
      use: { ...devices['iPad Pro 11 landscape'] }
    },
    {
      name: 'ipad mini',
      use: { ...devices['iPad Mini landscape'] }
    }
  ]
})
```

devices 配下に様々なブラウザやプラットフォーム向けの設定があるようで、それをそのまま設定値に突っ込めばいいっぽい。

# 3. テストコードを書く

手始めに、 "ログインページを初回描画してスナップショットを撮る"　だけのテストを作ってみる

```ts:test/e2e/pages/login.spec.ts
import { test, expect } from '@playwright/test'

test.describe('/login', () => {
  test('初回表示', async ({ page }) => {
    await page.goto('http://localhost:3000/login')
    await page.waitForLoadState('domcontentloaded')
    await expect(page).toHaveScreenshot()
  })
})
```

これだけで完了。

ページを表示　→ 描画完了を待つ → スナップショットテスト
の３行のみの構成になっている。テストは一旦これでいい。


# 4. スナップショットを作成

`TEST=true yarn dev` を別のターミナルで実行している状態で、 以下のコマンドを実行する

```sh
yarn run playwright test ./test/e2e/ --update-snapshots
```

すると、`login.spec.ts` と同じ階層に `login.spec.ts-snapshots` ディレクトリが作られており、中に
- `-login-SSR-画面表示-1-ipad-mini-darwin.png`
- `-login-SSR-画面表示-1-ipad-pro-11-darwin.png`
ができていた。

`-login-SSR-画面表示-1-ipad-mini-darwin.png`
![-login-SSR-画面表示-1-ipad-mini-darwin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067027/ea45cbf9-40f6-8c00-1ce9-bb36c862e190.png)

`-login-SSR-画面表示-1-ipad-pro-11-darwin.png`
![-login-SSR-画面表示-1-ipad-pro-11-darwin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067027/ac1f3a95-1480-5139-88b4-886a6d728811.png)

# 5. 試しにテストを落としてみる

"Hoge アプリ" のキャプションを試しに "Fuga アプリ"　に変えた状態でテストしてみる。 

テストを走らせるには以下のコマンドを実行する 

```sh
yarn run playwright test ./test/e2e/
```


想定通り落ちてくれた。

```sh
$ playwright test

Running 2 tests using 2 workers

  ✘  1 [ipad mini] › pages/login.spec.ts:4:3 › /login › [SSR] 画面表示 (1s)
  ✘  2 [ipad pro 11] › pages/login.spec.ts:4:3 › /login › [SSR] 画面表示 (1s)


  1) [ipad pro 11] › pages/login.spec.ts:4:3 › /login › [SSR] 画面表示 =================================

    Error: Screenshot comparison failed:

      496 pixels (ratio 0.01 of all image pixels) are different

    Call log:
      - expect.toHaveScreenshot with timeout 5000ms
      -   verifying given screenshot expectation
      - taking page screenshot
      -   disabled all CSS animations
      - 496 pixels (ratio 0.01 of all image pixels) are different
      - waiting 100ms before taking screenshot
      - taking page screenshot
      -   disabled all CSS animations
      - captured a stable screenshot
      - 496 pixels (ratio 0.01 of all image pixels) are different

    Expected: /Users/mewton/Workspace/sample-app/test-results/pages-login--login-SSR-画面表示-ipad-pro-11/-login-SSR-画面表示-1-expected.png
    Received: /Users/mewton/Workspace/sample-app/test-results/pages-login--login-SSR-画面表示-ipad-pro-11/-login-SSR-画面表示-1-actual.png
        Diff: /Users/mewton/Workspace/sample-app/test-results/pages-login--login-SSR-画面表示-ipad-pro-11/-login-SSR-画面表示-1-diff.png

       5 |     await page.goto('./login')
       6 |     await page.waitForLoadState('domcontentloaded')
    >  7 |     await expect(page).toHaveScreenshot()
         |                        ^
       8 |   })
       9 | })
      10 |

        at /Users/mewton/Workspace/sample-app/test/e2e/pages/login.spec.ts:7:24

    attachment #1: -login-SSR-画面表示-1-expected.png (image/png) --------------------------------------
    test-results/pages-login--login-SSR-画面表示-ipad-pro-11/-login-SSR-画面表示-1-expected.png
    ------------------------------------------------------------------------------------------------

    attachment #2: -login-SSR-画面表示-1-actual.png (image/png) ----------------------------------------
    test-results/pages-login--login-SSR-画面表示-ipad-pro-11/-login-SSR-画面表示-1-actual.png
    ------------------------------------------------------------------------------------------------

    attachment #3: -login-SSR-画面表示-1-diff.png (image/png) ------------------------------------------
    test-results/pages-login--login-SSR-画面表示-ipad-pro-11/-login-SSR-画面表示-1-diff.png
    ------------------------------------------------------------------------------------------------

  2) [ipad mini] › pages/login.spec.ts:4:3 › /login › [SSR] 画面表示 ===================================

    Error: Screenshot comparison failed:

      496 pixels (ratio 0.01 of all image pixels) are different

    Call log:
      - expect.toHaveScreenshot with timeout 5000ms
      -   verifying given screenshot expectation
      - taking page screenshot
      -   disabled all CSS animations
      - 496 pixels (ratio 0.01 of all image pixels) are different
      - waiting 100ms before taking screenshot
      - taking page screenshot
      -   disabled all CSS animations
      - captured a stable screenshot
      - 496 pixels (ratio 0.01 of all image pixels) are different

    Expected: /Users/mewton/Workspace/sample-app/test-results/pages-login--login-SSR-画面表示-ipad-mini/-login-SSR-画面表示-1-expected.png
    Received: /Users/mewton/Workspace/sample-app/test-results/pages-login--login-SSR-画面表示-ipad-mini/-login-SSR-画面表示-1-actual.png
        Diff: /Users/mewton/Workspace/sample-app/test-results/pages-login--login-SSR-画面表示-ipad-mini/-login-SSR-画面表示-1-diff.png

       5 |     await page.goto('./login')
       6 |     await page.waitForLoadState('domcontentloaded')
    >  7 |     await expect(page).toHaveScreenshot()
         |                        ^
       8 |   })
       9 | })
      10 |

        at /Users/mewton/Workspace/sample-app/test/e2e/pages/login.spec.ts:7:24

    attachment #1: -login-SSR-画面表示-1-expected.png (image/png) --------------------------------------
    test-results/pages-login--login-SSR-画面表示-ipad-mini/-login-SSR-画面表示-1-expected.png
    ------------------------------------------------------------------------------------------------

    attachment #2: -login-SSR-画面表示-1-actual.png (image/png) ----------------------------------------
    test-results/pages-login--login-SSR-画面表示-ipad-mini/-login-SSR-画面表示-1-actual.png
    ------------------------------------------------------------------------------------------------

    attachment #3: -login-SSR-画面表示-1-diff.png (image/png) ------------------------------------------
    test-results/pages-login--login-SSR-画面表示-ipad-mini/-login-SSR-画面表示-1-diff.png
    ------------------------------------------------------------------------------------------------


  2 failed
    [ipad pro 11] › pages/login.spec.ts:4:3 › /login › [SSR] 画面表示 ==================================
    [ipad mini] › pages/login.spec.ts:4:3 › /login › [SSR] 画面表示 ====================================
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

```

デフォルトだと test-result 配下にテスト結果として "expected" "actual" "diff" のそれぞれの画像を出力してくれる模様。

# 6. 出力結果をちょっと豪華にする

ここで、 `playwright.config.ts` を書き換えて、テスト結果の情報を多くしてみる

```ts:playwright.config.ts
export default definePlaywrightConfig({
  reporter: [
    ['line'],
    ['html', { open: 'always', outputFolder: coverageDir }]
  ],
  use: {
    video: 'on',
    screenshot: 'on',
  },
  projects: [
    {
      name: 'ipad pro 11',
      use: { ...devices['iPad Pro 11 landscape'] }
    },
    {
      name: 'ipad mini',
      use: { ...devices['iPad Mini landscape'] }
    }
  ]
})
```

この設定で再度テストを実行すると、テスト完了後にブラウザが立ち上がって、以下のページが表示された

![スクリーンショット 2022-08-31 18.38.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067027/f5c53a4c-a511-c18e-4f14-b9e6b07b229a.png)

１行目をクリックしてみると、テスト結果の詳細がコード・画像差分・動画つきで出力された。

![2022-08-31 18.43.49 127.0.0.1 baa59bbaa4ed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067027/6b3a9a4e-be30-37be-cbf6-cc5e819d4b73.png)

画像差分・動画付きはかなり嬉しい。

# まとめ

今回は Nuxt3 に playwright で E2E snapshot test を導入してみた。

ページの描画直後をスナップショットするだけでも十分テストになるので、
手間がかからなくてとっても良かった。

実はこの後、 GitHub Actions で動作させようとして、また一波乱あったため、こちらは別の記事として知見を残そうと思う。