🙌

【UI Mode & codegen】Playwrightの超便利なツールやTips

2024/09/30に公開

PlaywrightでE2Eテストを実装してみたのですが、初めてで手探りということもあり書いている中で色々と躓いては調べて解決してを繰り返して時間を浪費してしまいました…

その問題と解決法を共有することで、皆さんの時間を節約出来たらと思いますので記事としてご紹介したいと思います!特にUI Modeでのテスト実行やcodegenによる自動コード生成はめちゃくちゃ便利なので是非そこだけでも目を通して下さい!

コマンドでのテスト実行ってわかりにくい

テストの実行は基本的にはコマンドを打って実行する人が多い(自分もそうでした)と思うのですが、コマンドを打つのは面倒。特にE2Eテストとなるとテキストベースで表示されても何がなんだかわけがわからない…

解決策

PlaywrightはUI Modeというツールを提供してくれています!

これを利用すると以下のような感じでGUI上でテストが実行できます。

こちらの方法だと、特定のテストファイルを指定しての実行が簡単に出来たり、テスト過程がグラフィカルに表示されたりするので、こちらの方法でテストを確認するのがオススメです。

UI Modeの使い方は、以下のコマンドを実行するだけです!

npx playwright test --ui

ただ1点注意として、環境変数はUI Mode起動時の物が使用されます。ですので環境変数を変更した場合はUI Modeを再起動するのをお忘れなく!

参考

UI Mode

E2Eテスト、そもそも書くのが面倒

E2Eテストのコードの大半は、「フォームに〜って入力して、〜っていうテキストのボタンを押して」という正直書いててあまり面白くないコードの割に大量に書かないといけなくて書くのが億劫…

解決策

Playwrightはブラウザでの操作内容をテストコードとして出力するツールを提供しています!

以下のコマンドで記録用のブラウザが起動されますので、後はそのブラウザを操作をするとコードが出力されていきます。

npx playwright codegen [テスト対象URL]

テストを書く際はまず一連の操作をこの方法で出力して、それをベースに補完していくやり方がオススメです!

ただ注意点として、生成されるコードでたまに以下のような物が出力されるケースがあります。(Next.jsプロジェクト想定)

await page.locator('.Modal_close_5e25yd').click() // モーダルの閉じるボタンをクリックする操作

.Modal_close_5e25yd_5e25ydCSS Modules を利用している際に付与されるサフィックスで、ビルド毎に変わる可能性があります。なのでこのコードをそのまま利用してしまうと、テストを書いた段階では通っていても、すぐに落ちてしまう可能性が高いです。それに、そもそもサフィックスが無くとも、CSSクラス名による要素の指定は実装に依存したコードになってしまうので、E2Eテストのコードとしてはあまり良くありません。

Playwrightが生成するコードのlocatorは、role text test id を利用したものが優先して生成されますが、前述のどの方法でも難しい場合はCSSクラス等を利用したlocatorが生成されてしまいます。その場合は、test id を付与して getByTestId を利用する方法に書き換える等する必要があります。生成されたコードは一通りチェックして、問題ないか確認しましょう。

参考

Generating tests

なんかテストが成功したり失敗したりするんだけど パート1

作成ボタンを押した後「作成に成功しました」というメッセージが出ることを確認したくて以下のコードを書いたものの、何故か頻繁に失敗してしまう…

await page.getByRole('button', { name: '作成' }).click()
const messageIsVisible = await page.getByText('作成に成功しました').isVisible()
expect(messageIsVisible).toBeTruthy()

解決策

await expect を利用しましょう!

上のコードが頻繁に失敗する理由は、二行目の実行が作成ボタンを押した直後(数ms~数十ms程度後)のため、メッセージが出る前に実行された結果 false となってしまうからです。

このように、E2Eテストでは通信処理等の関係で表示のチェックの前に待機時間が必要なケースが頻繁にあります。

これは以下のように await expect を利用することで解決出来ます。

await page.getByRole('button', { name: '作成' }).click()
await expect(page.getByText('作成に成功しました')).toBeVisible()

await expect(~).toBeVisible() は、「数秒(デフォルト設定では5秒)以内にexpect内の要素が見える状態になったら成功」といった動作をしてくれます。なので、通信処理等待機時間が必要な場合もカバー出来ます。

この方法でテストをすることにデメリットは無いと思いますので、どうしても使えない場合以外は await expect を使用してテストを書きましょう。

なんかテストが成功したり失敗したりするんだけど パート2

「ブログシステムで記事作成ボタンを押すとHTTPリクエストが発生し、成功すると即座に記事一覧に表示される」っていう動作を確認するコードを以下のように書いたら頻繁に失敗するんだけど…

await page.getByRole('button', { name: '作成' }).click()

await page.goto('/posts') // 記事一覧ページに移動

await expect(page.getByText('作成した記事タイトル')).toBeVisible()

解決策

HTTPリクエストが発生する操作を行った際は、その成功を確認してから次の操作を進めましょう!

上のテストが頻繁に失敗する理由は作成ボタン押下後、HTTPリクエストの成功を確認せずに次の操作に移っているのが原因です。その結果記事一覧ページ表示タイミングではまだ記事作成の処理が完了しておらず記事一覧に表示されないパターンが発生してしまっています。操作間の間隔は環境によって変わってしまうので、ローカルではテストが問題なく通った結果CIで落ちるまで気づかないパターンが頻出します。

HTTPリクエストをする操作の場合、成功時にはページ移動したりトーストが出たりと何らかの変化が発生するはずなので、それの発生を確認してから次の操作に移る事を意識しましょう。

今回のケースの場合、作成完了後記事詳細画面へ自動的に移るとすると以下のようになります。

await page.getByRole('button', { name: '作成' }).click()

// 記事詳細ページへの遷移を確認
await expect(sharedPage).toHaveURL(/\/posts\/[^/]+$/)

await page.goto('/posts') // 記事一覧ページに移動

await expect(page.getByText('作成した記事タイトル')).toBeVisible()

このように成功を確認してから次の操作に移るようにすれば、タイミングに依存しないテストになって失敗しなくなります。「とりあえず何らかの処理を実行する操作を行ったら成功を確認してから次に移る」を意識してテストを書くのが良いと思います。

ユーザー一覧テーブルの対象ユーザー行にあるボタンを押したい

ユーザー情報編集に関するテストを書きたいので、ユーザー一覧テーブルの対象ユーザー行にある編集ボタンを押したいが、どうコードを書けばいいかわからない…

解決策

filter を使えばスマートに書けます!

例えば、今回のケースの場合は、以下のように書けます。

const targetRowLocator = page.locate('tr').filter({ has: page.locate(`td:has-text("対象アカウント名")`) })
const targetButtonLocator = targetRowLocator.getByRole('button', { name: '編集' })
await targetButtonLocator.click();

page.locate('tr') のみだと、全ての tr が対象となってしまいますが、 filter({ has: ~ }) を追加することで、「特定要素を子要素として持つ tr」を対象とすることが出来ます。

filterhas 以外にも hasNot 特定子要素を持たない要素のフィルタリングや hasText でテキストによるフィルタリングが出来ます。

参考

Filtering Locators

処理時間に幅がある動作の確認を効率的にやるにはどうすれば?

「ブログシステムで、作成した記事が記事一覧に表示されることのテスト」を書きたい…ただ、作成後表示されるまで5~30秒程度のタイムラグがあるので次のように書いた。

await page.getByRole('button', { name: '作成' }).click()
await page.goto('/posts') // 記事一覧ページに移動

await page.waitForTimeout(30_000)

await page.reload()
await expect(page.getByText('記事タイトル')).toBeVisible()

テスト自体はある程度まともに動くけど、以下の点で良く無さそう…

  • タイムラグが数秒で済んだ場合でも30秒待ってしまうので、時間効率が悪い
  • タイムラグが30秒を超えた場合、テストが失敗する

解決策

toPass を利用して書くと解決します!

今回のケースでは以下のような感じです。

await page.getByRole('button', { name: '作成' }).click()
await page.goto('/posts') // 記事一覧ページに移動

await expect(async () => {
    await page.reload()
    await expect(page.getByText('記事タイトル')).toBeVisible()
}).toPass({
    intervals: [1_000, 2_000, 4_000, 8_000, 16_000, 32_000]
})

toPass は、 expect 内のテストが成功するまで一定回数繰り返し試行してくれる関数です。試行間の待機時間は intervals で指定します。

この方法でテストを書けば、タイムラグが短い場合はテストも短時間で終えることが出来るので、無駄の少ないテストにすることが出来ます。

参考

expect.toPass

ダウンロードしたファイルの中身の検証ってどうやるの?

CSVファイルのダウンロードボタンがあって、ダウンロードしたファイルの内容が正しいかテストで検証したいんだけど一体どうすれば出来るのかわからない…

解決策

ダウンロードしたファイルの内容を見るのは、以下のコードで出来ます!

const getTextFileContent = async (download: Download): Promise<string> => {
  return new Promise(async (resolve, reject) => {
    const readStream = await download.createReadStream()
    const rows: string[] = []
    readStream.on('data', (data) => rows.push(data))
    readStream.on('end', () => resolve(rows.join('\n')))
    readStream.on('error', (err) => reject(err))
  })
}

const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'ダウンロード' })
const download = await downloadPromise
const text = await getTextFileContent(download) // ダウンロードしたテキストファイルの中身が取得できる

公式ではファイルを一旦保存するやり方が紹介されていますが、保存の必要がない場合はこの download.createReadStream を使うやり方が楽でオススメです!

ローカルストレージってどうやっていじるの?

ローカルストレージに特定の値が入っているケースのテストをしたいんだけど、ローカルストレージの弄り方がわからない…

解決策

evaluate を利用するとローカルストレージをいじれます!

await page.evaluate(() => {
  localStorage.setItem('key', 'value')
})

evaluate の引数に渡された関数は、テストを実行しているブラウザで実行されます。ローカルストレージ以外でも活用できるケースはありそうなので覚えておくと便利かなと思います。

参考

Evaluating JavaScript

まとめ

以上、私が遭遇した問題と解決策を紹介させていただきました!

この記事がお役に立てればなによりです。それでは!🫡

R&Dテックブログ

Discussion