Playwrightでpage.waitForTimeoutは使うと遅くなるので使ってはいけない

page.waitForTimeoutは使ってはいけない

Playwrightのコードでpage.waitForTimeoutが使われているコードを見ることがあります。

page.waitForTimeoutを使用するとテストが完了したとしても、無駄に待ち時間が長いテストになってしまいます。

さらに処理が間に合わないと失敗し、間に合っていれば通るという、テストの再現性が低下してしまいます。

example.spec.js
await page.click('#submit')
await page.waitForTimeout(2000) // 1秒で送信完了した場合、無駄な待ち時間が発生
await expect(page.getByText('送信完了')).toBeVisible()

仮にpage.waitForTimeoutで100回中100回成功したとしても、パソコンの性能・ネットワーク状況・バックエンド側の変更などによって、処理速度が変わることがある点にも注意が必要です。

page.waitForTimeoutの代替手段

Playwrightにはテストの安定性や正確性を保つために「待機」機能(waitFor系メソッド)が多数用意されています。

これらをちゃんと使いこなしていれば、page.waitForTimeoutを使わなくてはいけないケースはほぼなくなります。

以下は代表的なwaitFor系のメソッドとその用途です。

page.waitForNavigationは非推奨

page.waitForNavigation()はページ遷移を待機しますが、現在はDISCOURAGED(非推奨)となっており、page.waitForURL()の使用が推奨されていますので使わないでください。

page.waitForURL

指定したURLを待機します。

URLをそのまま指定するか、以下のようにワイルドカードで指定できます。

example.spec.js
await page.click('a.delayed-navigation')
await page.waitForURL('**/target.html')

page.waitForLoadState(state)

load, domcontentloaded, networkidleなどのページ状態まで待機する。

networkidleはDISCOURAGED(非推奨)となっているので、使用するのはload, domcontentloadedのどちらかにしてください。

domcontentloadedはDOMツリーが構築された状態で、loadはCSSや画像を含むすべてのリソースの読み込み完了なので、page.screenshotを使用してスクリーンショットを保存する場合はpage.waitForLoadState()を使用したほうが良いです。

example.spec.js
await page.waitForLoadState() // デフォルトは 'load' なので省略可
await page.waitForLoadState('domcontentloaded')
await page.waitForLoadState('networkidle') // 非推奨

page.waitForSelector

page.waitForSelectorは指定した要素がDOM上に現れるまで処理を止めて待機するために使います。

example.spec.js
await page.goto('https://example.com')
await page.waitForSelector('h1')

stateと組み合わせることで以下のようにどのような状態を待つか設定できます。

example.spec.js
// ローディングの要素がDOMにあればOK
await page.waitForSelector('.loading')

// ローディングが表示されるのを待つ
await page.waitForSelector('.loading', { state: 'visible' })

// ローディングが非表示になるのを待つ
await page.waitForSelector('.loading', { state: 'hidden' })

// ローディングがDOMから削除されて消えるのを待つ
await page.waitForSelector('.loading', { state: 'detached' })

page.waitForEvent

特定のイベントが発火するのを待機するためのメソッドです。

例えば、window.openやaタグのtarget="_blank"で別タブで開いたときのテストには「page.waitForEvent('popup')」を使用します。

example.spec.js
import { test, expect } from '@playwright/test'

test('別タブで開いた先のURLが正しいかテスト', async ({ page }) => {
  await page.setContent(`
    <button onclick="window.open('https://example.com/')">open</button>
    <a href="https://example.com/" target="_blank">link</a>
  `)

  for (const selector of ['button', 'a']) {
    const [newPage] = await Promise.all([
      page.waitForEvent('popup'),
      page.click(selector)
    ])
    expect(newPage.url()).toBe('https://example.com/')
  }
})

Playwright 別タブで開いた先のURLが正しいかテスト

また、ファイルのダウンロードのときにはpage.waitForEvent('download')を使用します。

example.spec.js
import { test, expect } from '@playwright/test'

test('ファイルダウンロードのテスト', async ({ page }) => {
  await page.goto('https://iwb.jp/s/download.html')

  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.click('a.download')
  ])

  const fileName = download.suggestedFilename()
  await download.path()
  console.log(`${fileName}のダウンロード完了`)
  // sample.jpgのダウンロード完了
})

Playwright ファイルダウンロードのテスト

この2つはpage.waitForEventでよく使われますので、必ず覚えておいたほうが良いです。

page.waitForRequestとpage.waitForResponse

その名の通り、リクエストおよびレスポンスを待機します。

URLのパラメーターが変わる場合は以下のようにワイルドカードを指定してテストします。

example.spec.js
test('DummyJSONボタンのクリックとリクエスト検証', async ({ page }) => {
  // ページに移動
  await page.goto('https://iwb.jp/s/download.html')

  // ボタンが表示されるのを待機
  const button = page.getByRole('button', { name: 'DummyJSON' })
  await expect(button).toBeVisible()

  // リクエストの監視を開始(URLパラメータを動的に検証)
  const requestPromise = page.waitForRequest('https://dummyjson.com/test?*')

  // ボタンをクリック
  await button.click()

  // リクエストの完了を待機
  const request = await requestPromise
  console.log('リクエストURL:', request.url())
  // リクエストURL: https://dummyjson.com/test?delay=1000

  // レスポンスの待機と検証(URLパラメータを動的に検証)
  const response = await page.waitForResponse('https://dummyjson.com/test?*')
  const responseData = await response.json()
  console.log('レスポンスデータ:', responseData)
  // レスポンスデータ: { status: 'ok', method: 'GET' }

  // レスポンスデータの検証
  expect(responseData).toEqual({
    status: 'ok',
    method: 'GET'
  })
})

waitForFunction

ページ内で特定の条件が真になるまで待機します。

page.waitForSelectorではテストできない場合はこちらを使用します。

example.spec.js
test('要素の待機と検証', async ({ page }) => {
  await page.goto('https://www.google.com/');

  const result = await page.waitForFunction(async () => {
    return !!document.querySelector('textarea[name="q"]')
  });
  console.log(await result.jsonValue()) // true

  const result2 = await page.waitForFunction(async () => {
    return !!document.querySelector('foo[name="q"]')
  });
  console.log(await result2.jsonValue()) // false
})

まとめ

page.waitForTimeoutは「テストの信頼性・効率・保守性」が著しく低下するため、使用してはいけません。

page.waitForNavigationも非推奨なので使わないでください。

Playwrightにはpageオブジェクトにおける waitForXXX メソッドが他にも以下の7つが用意されているため、待機したい場合はこれらのいずれかを使用してください。

メソッド名説明
waitForURL(url)指定のURLにナビゲーションされるまで待機(部分一致や正規表現も可)
waitForLoadState(state?)ページのロード状態になるまで待機
waitForSelector(selector)指定したセレクターの要素が表示されるまで待機
waitForEvent(event)waitForEvent(event)
waitForRequest(url)指定のURLまたは条件に一致するリクエストが発生するまで待機
waitForResponse(url)指定のURLまたは条件に一致するレスポンスを受け取るまで待機
waitForFunction(fn)指定した関数がtrueを返すまで待機