🔖

Nuxt3 で VRT を導入する(Playwright 編)

2024/03/04に公開

前提

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

はじめに

現在フロント側のテスト戦略を検討しているのですが、そちらに対し、いろいろ検証が必要でした。
その検証の1つとして、今回の 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 のインストール

公式にある通りの手順でインストールを実施します。

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 の設定ファイルを追加

設定は下記のようにしました。

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 の設定ファイルになるので、下記のファイルを追加しました。

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 テストを実行した場合

./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 にスクリプトを追加します。

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 の追加

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

インストール

こちらは公式に従ってインストールを実施します。

npm install msw@latest --save-dev

次に、ワーカースクリプトの作成を行います。
こちらも公式にある通り、実施を行います。

npx msw init ./public --save

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

playwright-msw のインストール

playwright で msw を使用できるようにするためのパッケージとなります。

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

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

npm install playwright-msw --save-dev

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

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 の公式などを確認していただければです)

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 を渡す
VRT=true npm run build
  • nuxt.config.ts に上記環境変数を使った ssr の設定を行う
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 上でスクリーンショットの取得を行う

こちらは公式にあるコマンドをちょっと変更し、Docker run を走らせるようにしました。

# イメージの取得
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 のイメージを使用したコンテナ内に入れます。
そこで下記コマンドを実行し、スクリーンショットの取得を行いました。

# 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 の更新を行うと下記コマンドも自動で動くようになっています。

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 のイメージを使用するように修正します。

  # ... 省略
  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 を確認してみるのもありだと思います。(もちろん、予算次第にはなりますが)

Discussion