Nuxt3 で VRT を導入する（Playwright 編）

## 前提
今回の Nuxt3 における VRT ではコンポーネント単位の VRT ではなく、ページ単位の VRT になります。
そのため、コンポーネント単位で VRT を実施したい場合は、本記事はあまり参考にならないので、ご注意ください。

## はじめに
現在フロント側のテスト戦略を検討しているのですが、そちらに対し、いろいろ検証が必要でした。
その検証の１つとして、今回の Playwrigth での VRT があり、そちらにとても苦労したので、記事にしようと思いました。
Nuxt3 での VRT はあまり記事がないので、もし参考にされる方は参考にしていただければと思います。

## 成果物
こちらは下記のリポジトリになります。

https://github.com/noriyuki-shimizu/article-nuxt3

## VRT 導入完了後の所感
正直に言って、**Playwright での VRT 導入は、まだ早いかも？** と思いました。
というのも、ハマりポイントが多すぎたためとなります。
どれほど多かったのかは、この後に説明させていただきます。

## 完了までの要点
こちらは下記になります。

* Playwright での VRT では Nuxt の SSR モードを off にする
* Playwright の Docker image を使用して VRT 用のスクリーンショット画像生成する
  * Github Actions で VRT を実行する際に差分検知させないため
* VRT で使用する Rest API のデータは MSW で実装
* VRT の対象はページの画面単位

上記の要点を踏まえて、導入手順をまとめていきたいと思います。

## Playwright のインストール
[公式](https://playwright.dev/docs/intro)にある通りの手順でインストールを実施します。

```bash
npm init playwright@latest
```

自分の実行時は playwright のバージョンは `v1.42.0` でした
上記コマンドを実行すると下記のファイル達が生成されます。（デフォルトで生成されるファイル達）

```
playwright.config.ts
package.json
package-lock.json
tests/
  example.spec.ts
tests-examples/
  demo-todo-app.spec.ts
```

デフォルトで、作成されるテストファイルなどは丸っと削除しました笑（元々テストのディレクトリ構成は決めていたため）

## Playwright の設定ファイルを追加
設定は下記のようにしました。

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

export default defineConfig({
  /* ファイル内のテストを並行して実行する */
  fullyParallel: true,
  /* 誤ってソースコードにtest.onlyを残してしまった場合、CIでのビルドに失敗する。*/
  forbidOnly: !!process.env.CI,
  /* CIのみリトライ */
  retries: process.env.CI ? 2 : 0,
  /* CIで並列テストをオプトアウトする。*/
  workers: process.env.CI ? 1 : undefined,
  /* 使用するレポーター。https://playwright.dev/docs/test-reporters を参照。 */
  reporter: 'html',
  /* 以下の全プロジェクトで設定を共有。https://playwright.dev/docs/api/class-testoptions。 */
  use: {
    /* `await page.goto('/')` のようなアクションで使用するベースURL。 */
    baseURL: 'http://127.0.0.1:3009',
    /* 失敗したテストを再試行するときにトレースを収集します。https://playwright.dev/docs/trace-viewer を参照。 */
    trace: 'on-first-retry',
    /* ユーザーのタイムゾーンをエミュレートする。 */
    timezoneId: 'Asia/Tokyo',
    /* コンテキストのすべてのページで使用されるビューポート。 */
    viewport: { width: 1_680, height: 1_050 }
  },
  /* 主要なブラウザ用にプロジェクトを構成する */
  projects: [
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    },
    {
      name: 'safari',
      use: { ...devices['Desktop Safari'] }
    },
    /* ブランドのブラウザでテストする。 */
    {
      name: 'edge',
      use: { ...devices['Desktop Edge'], channel: 'msedge' }
    },
    {
      name: 'chrome',
      use: { ...devices['Desktop Chrome'], channel: 'chrome' }
    }
  ],
  /* テストを開始する前に、ローカルの開発サーバーを実行する。 */
  webServer: {
    command: 'PORT=3009 npm run preview',
    url: 'http://127.0.0.1:3009',
    reuseExistingServer: !process.env.CI
  }
})
```

この時点で、結構 Playwright に感動してました。
というのも Web Server の設定欄が設けられていたり、テストしたいブラウザの種類も豊富だったり、機能が充実しているなぁと思うことが多かったためです。

上記は Playwright の共通設定ファイルとしました。
Playwright は E2E なども実装できるので、今後そちらを導入した時に E2E 用の設定ファイルなどを作成できるようにするためとなります。
今回は VRT の設定ファイルになるので、下記のファイルを追加しました。

```ts:playwright-vrt.config.ts
import { defineConfig } from '@playwright/test'
import commonConfig from './playwright.config'

export default defineConfig({
  ...commonConfig,
  /* 設定しているファイルパターンにマッチするもののみテスト対象となる */
  testMatch: 'index.vrt.ts',
  /* VRT 実行時に保存するディレクトリとファイル名の定義 */
  snapshotPathTemplate:
    '{testFileDir}/__screenshots__{/projectName}/{arg}{ext}'
})
```

上記の設定における `snapshotPathTemplate` に関しては、例えば下記のディレクトリにある VRT テストを実行した場合

```ts:./pages/index.vue
import { test, expect } from '@playwright/test'

test.describe('トップページ', () => {
  test('初期表示', async ({ page }) => {
    await page.goto('/')
    await page.waitForLoadState('domcontentloaded')

    await expect(page).toHaveScreenshot()
  })
})

test.afterEach(async ({ page }) => {
  await page.close()
})
```

下記のようなスナップショットファイルが保存されます。

`pages/__screenshots__/{chrome|edge|firefox|safari}/トップページ-初期表示-1.png`

つまり、`snapshotPathTemplate` は下記の構成になります。

* testFileDir
  * テスト実行時のディレクトリパス
* projectName
  * ブラウザ種別
* arg
  * 要はテストケースをつなげたもの
* ext
  * 画像の拡張子

そのほかにもいろいろパラメータがあるので、気になる方は見ていただければと思います。

https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template

## スクリプトの追加
上記設定までできたら package.json にスクリプトを追加します。

```json:package.json
  "scripts": {
    "test:vrt": "playwright test -c playwright-vrt.config.ts",
    "test:vrt:us": "npm run test:vrt -- --update-snapshots",
    "test:vrt:ui": "npm run test:vrt -- --ui"
  }
```

## MSW の追加
最新の記事情報を表示する本アプリケーションでは、画面のスクリーンショットを取得する際、データはモックデータでないと、毎回違いスクリーンショットになってしまうため、モックデータを使用する必要がありました。

### インストール
こちらは[公式](https://mswjs.io/docs/getting-started)に従ってインストールを実施します。

```bash
npm install msw@latest --save-dev
```

次に、ワーカースクリプトの作成を行います。
こちらも[公式](https://mswjs.io/docs/cli/init/)にある通り、実施を行います。

```bash
npx msw init ./public --save
```

こうすることで、public ディレクトリにワーカースクリプトが配置され、package.json に msw に対して、ワーカースクリプトの配置場所を指定するソースが自動挿入されます。

### playwright-msw のインストール
playwright で msw を使用できるようにするためのパッケージとなります。

https://github.com/valendres/playwright-msw

あんまりメンテナンスされていなさそうなパッケージでもあったので導入するか迷いましたが、「上記パッケージ以外でいいものがない」＋「自作するにはあまりにも時間がかかりそう」と言った点で、使用することにしました。
導入自体は非常に簡単で、まずパッケージをインストールする

```bash
npm install playwright-msw --save-dev
```

導入後、playwright の拡張を実施する

```ts:tests/playwright.ts
import { test as base, expect } from '@playwright/test'
import type { MockServiceWorker } from 'playwright-msw'
import { createWorkerFixture } from 'playwright-msw'

/** playwright に msw の機能を拡張 */
const test = base.extend<{
  worker: MockServiceWorker;
}>({
  worker: createWorkerFixture()
})

export { test, expect }
```

ここまでできたら、実際にモックしたい API に対しモックデータ・ハンドラーを作成して、テスト実施時にそちらを読み込ませればOKとなります。（実際のモックデータやハンドラーの作成方法は msw の公式などを確認していただければです）

```ts:pages/index.vrt.ts
import { handleGetArticles } from '../../../infrastructures/rest/qiita.com/api/v2/items/__mock__/msw'
import { test, expect } from '../../../tests/playwright'

test.describe('Qiita記事一覧', () => {
  test('初期表示', async ({ page, worker }) => {
    // 使用するモックハンドラーを追加
    await worker.use(handleGetArticles())
    await page.goto('/articles/qiita')
    // ... 省略
```

### Nuxt の SSR で問題が・・・
上記の設定で問題なく動くだろうと思っていたのですが、Nuxt の SSR では上記の設定で正常に機能しません。
というのも playwright-msw は playwright のヘッドレスブラウザ上で msw のコンテキストを使用できるようなモジュールとなっており、Nuxt のサーバサイド実行では playwright のヘッドレスブラウザとは関係ないので、うまくモック化できません。

### 苦肉の策、、、 Nuxt の SSR モードを off にする
Nuxt 側でサーバサイド実行しないようにし、ブラウザでのみ Nuxt アプリケーションを動かすようにすることで playwright-msw を読み込ませるという策を実行しました。
案の定、うまく動作したので良かったのですが、SSR モードでの VRT 検証はできないので、そちらがなんとも言えない・・・
（何かいい方法があれば教えてほしいです）

他の参考サイトなどには、Nuxt の plugins に msw を読み込ませるサンプルなどがあったのですが、テストケースごとでモックデータを変えたいということもあり、そちらの案はぼつにしました。

SSR モードを off にするに関しては、下記のような構成で実装しました。

* ビルド時に環境変数に `VRT=true` を渡す
```bash
VRT=true npm run build
```

* nuxt.config.ts に上記環境変数を使った ssr の設定を行う
```ts:nuxt.config.ts
export default defineNuxtConfig({
  ssr: !process.env.VRT,
  // ... 省略
})
```


## スクリーンショットの取得
ここまでできたら、テストを作成し、スクリーンショットを取得します。
実際のテスト内容はそれぞれの画面に応じて異なると思うので、自サイトにあったテストの作成をしていただければと思います。
ただ、スクリーンショットの取得では `toHaveScreenshot` 関数が必要になるので、そちらは必須で定義しないといけません。

https://playwright.dev/docs/test-snapshots

### スクリーンショットの取得は Docker で行う
こちらは結構ハマりました・・・
ローカルの実行に対しては、何も問題なく実行できて、正常なスクリーンショットの取得ができたのですが、Github Actions と組み合わせるためには、ローカル環境での Docker で画像を取得する必要があったのです。
なぜかに関しては、**Playwright で取得されるスクリーンショットは OS プラットフォームに依存するためとなります。**
Github Actios では Linux で実行され、ローカルは、それとは異なります。
そうなると、生成されるスクリーンショットでフォントの差分等があり、VRT が絶対にこけてしまうためです。

そのため、対策としては下記のような対策を実施しました。

* ローカルで画像生成する際は Playwright のイメージを使用した Docker 上でスクリーンショットの取得を行う
* Github Actions でも同じ Playwright のイメージを使用して VRT を実行する

### ローカルで画像生成する際は Playwright のイメージを使用した Docker 上でスクリーンショットの取得を行う
こちらは[公式](https://playwright.dev/docs/docker)にあるコマンドをちょっと変更し、Docker run を走らせるようにしました。

```bash
# イメージの取得
docker pull mcr.microsoft.com/playwright:v1.41.1-jammy

# Docker Run
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
```

上記コマンドを実行すると Playwright のイメージを使用したコンテナ内に入れます。
そこで下記コマンドを実行し、スクリーンショットの取得を行いました。

```bash
# node_modules が生成されていない場合、下記を実行
npm ci

# VRT で使用するブラウザのインストール
npx playwright install --with-deps chrome msedge

# VRT モードでのビルドを実行
VRT=true npm run build

# VRT におけるスクリーンショットの取得
npm run test:vrt:us
```

上記コマンドでうまくいくと信じていましたが、そう甘くはありません。

#### 最初の `npm ci` が成功しない
`npm ci` における node_modules の生成自体は成功しますが、Nuxt3 から node_modules の更新を行うと下記コマンドも自動で動くようになっています。

```bash
nuxt prepare
```

上記の nuxt コマンドが失敗します。
こちらはいろいろ調査しました。そこで出会ったのが下記のサイトです。

https://zenn.dev/alcnaka/articles/1c26387722a371

上記のサイトを見ていただくとおり、なんと Docker Desktop のバージョンを 4.18 にダウングレードして、その Docker 側の設定を変更しないと nuxt コマンドや vite コマンド等は動かないとのことでした。（これは辛い）
そのため Docker Desktop のバージョンを 4.18 にダウングレードし、上記サイト通りに設定を変更したら、nuxt の cli や playwright の cli が動くようになりました。

以上で、VRT のスクリーンショットを取得することができました。
（この時点で、自分は現場のプロジェクトに Playwright の VRT を導入することをほぼ諦めてます笑）

### Github Actions でも同じ Playwright のイメージを使用して VRT を実行する
Github Actions の workflow に、VRT の実行を記載するのですが、その実行に関して、Docker でも使用した Playworight のイメージを使用するように修正します。

```yml
  # ... 省略
  vrt-with-playwright:
    container:
      image: mcr.microsoft.com/playwright:v1.41.1-jammy
  # ... 省略
```

上記設定で CI を動かすと、ローカルで作成したスクリーンショットとの差分もなしに CI が成功できました。

## 最終的なディレクトリ構成
ここまでで VRT の導入が完了しました。
この時点でのディレクトリ構成を下記に記載します。（関連のあるもののみ記載しています）

```
.
├── infrastructures
│   └── api
│       └── __mock__
│           ├── fixture.ts
│           └── msw.ts
├── pages
│   ├── __screenshots__
│   │   ├── chrome
│   │   ├── edge
│   │   ├── firefox
│   │   └── safari
│   ├── index.vrt.ts
│   └── index.vue
├── tests
│   └── playwright.ts
├── playwright-vrt.config.ts
├── playwright.config.ts
└── nuxt.config.ts
```

上記のようにすることで、画面単位の VRT のディレクトリ構成を作成することができ、いい感じになったなぁと思っています。


## まとめ
ここまで来るのに、業務時間を除いて約1週間かかりました笑
何かを追加するたびに闇にぶち当たったので、何度も諦めかけましたが、なんとかやり切れて良かったです笑

現場への導入はやめようかな〜と思っていたりするので、まだ完璧に実装完了できていませんが、ここまでの内容とさせていただきます。
そのほかのやり残しは下記となります。

* テストファイルに対し `@` などを使用した相対パスでの import の実現

もし、どうしても Playwright で VRT を導入したいなどがあれば、本記事を参考にしていただければと思います。
Playwright 自体は、実行速度等はとても早いので、導入はお勧めします。

以上、最後まで読んでいただきありがとうございました！

## おまけ
Nuxt3 で VRT を導入するというのは割と難しいです。
Playwright 以外でもし導入する場合、jest-image-snapshot などがあったりします。
そのほかには Storybook +reg-suit でコンポーネント単位での VRT もあったりしますが、現状 Storybook は Nuxt3 自体を正式にサポートしていなかったりするので、そちらの導入も厳しいと思われます。

そのため、自分の現場では Autify の導入を進めています。

https://www2.autify.com/lp/sem?utm_term=autify&utm_campaign=brand&utm_source=google&utm_medium=cpc&hsa_acc=5029101531&hsa_cam=17291921015&hsa_grp=138423658042&hsa_ad=598485513808&hsa_src=g&hsa_tgt=kwd-386293750300&hsa_kw=autify&hsa_mt=p&hsa_net=adwords&hsa_ver=3&gad_source=1&gclid=CjwKCAiAloavBhBOEiwAbtAJO4Xf-anh1h8rPegZzJPrvrloxGjj_b-1r6GA9KkTLqpzJqVnbKnP0xoCF6MQAvD_BwE

有料サービスになりますが、とてもいい機能が満載で、VRT もサポートしているので、もし気になる方は、Autify を確認してみるのもありだと思います。（もちろん、予算次第にはなりますが）