◆ Playwright でも Jest と同じ expect() を使える
◆ ユニットテストにも使えるなら Jest はなくてもいい?
◆ モック機能は Jest が便利なのであったほうがいいかも
◆ Playwright のセレクタで React のコンポーネントが使えるけど minify してるとダメ

Playwright と Jest

ちょっと前に Playwright 使おうかなーと書いて最近使ってみてました

Puppeteer の場合はブラウザを操作するだけなので テストに関する機能は別のツールを使う必要があります
ユニットテストで Jest を使っているならそのまま Jest を使えます

Playwright だとそういう機能が組み込まれています
便利だけどユニットテストで Jest を使ってると共存するのはどうするのがいいんでしょう?

探してみると jest-playwright というライブラリがありました
https://github.com/playwright-community/jest-playwright

ですが REAMDME の最初に

⚠️ We recommend the official Playwright test-runner (@playwright/test) ⚠️
It's more flexible, lightweight, optimized for Playwright, and has TypeScript support out of the box. This doesn't mean, that we stop with maintaining this package.

という注意書きがあります

Playwright 公式のテストランナーを推奨するそうです
このパッケージのメンテナンスを辞めるわけではないらしいので 使ってもいいのでしょうけど 公式のテストランナーを勧めてるのであればそっちを使いたいです

とりあえず深く考えずに両方を入れてみました

そのままだとダメそうに思いつつもとりあえず動かしてみると 予想通り Jest 用のテストを Playwright で実行したり その逆もあったりでエラーになりました
.test.js と .spec.js (jsx,ts,tsx も) はどっちもデフォルトでテスト対象にしてるようです
設定で変更はできたので Jest は roots オプション Playwright は testDir オプションでそれぞれのフォルダを設定して動かせるようにできました

Playwright にも通常の assert 機能はある

とりあえずは両方動きますし これでいいのかなと思っていましたが Playwright の expect って Jest のものだったようです
https://playwright.dev/docs/test-assertions

Playwright Test uses expect library for test assertions.

と書かれていてリンク先が Jest の expect です
それならユニットテストとしても使えるのじゃないでしょうか

expect(1 + 1).toBe(2)

という風に書いてみると問題なく動きます
Playwright のブラウザ系のオブジェクト以外にも使えます

Jest いる?

Playwright でユニットテストができるなら Jest はインストールせずに Playwright だけでいいような気もします
依存関係的に Jest のほうがインストールに時間がかかって重たいですし Playwright のほうが結果表示がブラウザで見れて便利です
それに テスト用の .js ファイルの書き方的にも Playwright のほうが好みです
Jest は自動で jest オブジェクトや test 関数がグローバルに存在することになっています
毎回インポートすることになるのでコード量が減って便利ではあるのですが こういうのはあまり好きではないです
Playwright は明示的にインポートする必要があり 通常の JavaScript として読めます

const { test, expect } = require("@playwright/test")

インポートしなければ存在しないのでグローバルにデフォルトで何があるかを知る必要はないです

モック

じゃあもう全部 Playwright にしよ そう思ったもののすぐに困ったのがモック機能でした
Jest はデフォルトで便利なモック機能がありますが Playwright にはありません
一応モックに関するドキュメントページはあるのですが E2E テスト用のもので ユニットテスト用のものではないです
ページ内の JavaScript の前にスクリプトを埋め込んで実行し ビルトイン関数をモック関数に置き換えておくようなことをしてます
https://playwright.dev/docs/mock

Jest 以前の Mocha や Jasmine もモック機能はなかったと思うので それらと一緒に使われていたライブラリを使うことはできます
しかし モジュール自体を差し替えるものではなく 単純にオブジェクトのプロパティを差し替えるようなもので モジュール自体を差し替えたい場合は制限が付きます

例えば

log.js
const fs = require("fs")
module.exports = obj => fs.writeFileSync("log.txt", JSON.stringify(obj))

というモジュールがあって エクスポートした関数のテストで fs.writeFileSync が実際にファイルへ書き込まず この関数が呼び出され引数が文字列であることを確認したいとします
プロパティを置き換えるものでも

const fs = require("fs")
const log = require("./log.js")

test("test1", () => {
const backup = fs.writeFileSync

const args = []
fs.writeFileSync = (...a) => args.push(a)

log({ foo: "bar" })

expect(args).toEqual([["log.txt", `{"foo":"bar"}`]])

fs.writeFileSync = backup
})

のように fs.writeFileSync を書き換えてから log 関数を呼び出せば対応できます

しかし log.js で

const { writeFileSync } = require("fs")
module.exports = obj => writeFileSync("log.txt", JSON.stringify(obj))

のように writeFileSync が使われていると あとから fs.writeFileSync を一時的に置き換えても意味がありません
log.js を require する前に書き換えてしまえばできなくはないですが require の順番を考慮しないといけなくなりますし ES Modules で静的に import だとインポートの前に処理することができません
また require されたタイミングで writeFileSync が決まるのでテストの途中でモックの置き換えをするのが難しくなります
fs の場合はオブジェクトなのでプロパティ操作ができましたが もし関数だった場合は事前に置き換える事もできず require のキャッシュをいじるような手段になります

Jest ではテストファイルは自動で変換されて これらをうまくやってくれます
ES Modules は require に変換され モジュールのモック機能が動作するような順番で require とモック処理が行われます

そう考えると Jest はあったほうが良い気もします

Playwright と React

ここからは Jest は関係なくなって Playwright を使っていて気になった機能についてです
CSS のセレクタを使うのなら id や class を付けずに DOM を制御するものだと辛そうと思っていたのですが なんと React 用のセレクタがありました
https://playwright.dev/docs/selectors#react-selectors

_react=MyButton[enabled = false]

のように書けてコンポーネントと props で要素を特定できるようです
どうやってるのかと思ったら React DevTools を使ってるようです
Playwright に組み込まれてた Chromium を使ってるわけなので それにブラウザの拡張機能が入っててもおかしなことではないですね

試しに使ってみたのですが……動きません
まだ experimental なステータスですし 仕方ないのでしょうか
なんて思いましたが experimental とはいえ 自分のブラウザに依存するわけではなく ドキュメントに書いてる例が動かないことはそうないです

React DevTools が入ってるブラウザで確認してみると コンポーネント名が違ってました
Production ビルドをしていると minify されます
map ファイルから復元できそうにも思うのですが React DevTools 上では minify 済みの名前でした
ドキュメントを下の方まで見ると unminified のビルドでのみ動くって書いてましたし minify 済みは無理そうです

開発用の dev server では minify されていないので これを使うことはできますが クライアントアプリ開発用なので完全なアプリではありません
クライアントアプリで管理されていない静的ファイルやユーザがアップロードするような動的ファイルは見つかりません
完全なアプリケーション全体としての動作をテストするというよりもほぼクライアントアプリのみのテストになります

自分で webpack などビルドの設定をしていれば テスト用に minify のみ無効にしてビルドできなくもないです
ですが React は自分で設定するのは面倒なのでだいたい create-react-app 任せです
このツールだとあまり使われないような細かな設定はできないことが多いです
最近では minify での問題はめったに聞かないですが テストするなら最終的に実際に動く環境としてやりたいわけでテスト用に専用ビルドするのはどうなのかなという気はします

そもそも React は対応してますが その他の lit や hyperhtml などのマイナーなライブラリだと Playwright のセレクタで対応できませんし react セレクタに頼らなければいいともいえます

クライアントアプリのみでのテスト

完全な状態でのテストってそこまで丁寧しなくても画面がエラー無く開けてることを確認できれば十分な気もします
react セレクタを使うようなことはせず href 属性で a タグや中のテキストで button タグを見つけてクリックして各ページを 1 周できればそれでいいです

クライアント側だと React などのライブラリ部分はあまりテストしやすいと思いません
複雑でテストしておきたいロジックがあるなら コンポーネントに含めずに独立したモジュールに分けてそこだけでユニットテストしてコンポーネント部分はユニットテストしてないことも多いです
そういう場合のクライアントアプリ全体の動きとしてのテストには Playwright は良さそうでした
ブラウザを動かす E2E って遅いイメージでしたが Playwright は十分高速です
サーバ側のテストではないので ダミーサーバで API を用意します
Playwright のテストコードの JavaScript 中でダミーサーバを用意して サーバ側で受け取ったものを共有してテストできます

API を呼び出すパラメータの確認とテスト用のレスポンスを受け取ればいいなら fetch 関数を置き換えてしまうとか Service Worker でやるとかでもいいかもしれません
HTTP リクエストがいらないですし
Service Worker を使ったモックライブラリは少し前に話題になってたように思いますし それを使ってもいいかもですね