Playwrightでクリックしていると、Selenium時代に見慣れた「element click intercepted」に近い失敗が起きることがあります。
ただしPlaywrightは、クリックを実行する前に「本当にその要素へクリックが届くか」を自動で確認します。つまり、失敗は面倒な例外ではなく、テストが画面の状態を正しく見つけたサインです。
今回は、Seleniumのクリック失敗とPlaywrightのactionability checksの違いを整理します。最後に、force: trueをいつ使ってよいかも決めます。
Seleniumのelement click interceptedとは
Seleniumで次のように書いたとします。
driver.find_element(By.CSS_SELECTOR, "button.submit").click()
対象のボタンがDOMに存在していても、別の要素が上に重なっているとクリックできません。Cookieの同意バナー、ローディング画面、アニメーション中のモーダルなどがよくある原因です。
人間が見れば「まだ画面が準備中」と分かります。しかし、要素を見つけただけの処理では、そのボタンがいまクリックできるかまでは分かりません。
Playwrightはクリック前に5つ確認する
Playwrightのlocator.click()は、要素を見つけたら即座にイベントを送る処理ではありません。必要な条件がそろうまで待ってからクリックします。
公式ドキュメントでは、クリックに対して次の条件が確認されます。
- locatorが1つの要素に解決する
- 要素が見えている(Visible)
- 要素が動いていない(Stable)
- クリック位置が別の要素に覆われていない(Receives Events)
- 要素が有効になっている(Enabled)
特に大切なのが、Receives Eventsです。対象のボタンが見えていても、クリックする座標にオーバーレイがあれば、実際にクリックを受け取るのはオーバーレイです。Playwrightはそこまで確認します。
最初に書くコードはlocatorと通常のclick
では、まずユーザーが見る名前を使ってクリックしてみましょう。
import { test, expect } from '@playwright/test';
test('注文を送信できる', async ({ page }) => {
await page.goto('/checkout');
await page.getByRole('button', { name: '注文を確定' }).click();
await expect(page.getByRole('status')).toHaveText('注文を受け付けました');
});
ここでタイムアウトになったなら、待ち時間を適当に増やす前に、どの条件で止まっているかを見ます。
force: trueは原因を隠す
Playwrightには次の書き方があります。
await page.getByRole('button', { name: '注文を確定' }).click({ force: true });
force: trueはactionability checksのうち、必要不可欠ではない確認を省略します。公式ドキュメントの例では、対象が別の要素に覆われていてイベントを受け取れない状態でもクリックを進められます。
これはテストを通す魔法ではありません。ボタンが見えていない、無効になっている、別の要素にクリックを奪われている状態をそのまま許す指定です。
本番ユーザーがそのボタンを押せないのに、テストだけが成功する。これがforce: trueを常用したときの怖さです。
forceを使ってよいケース
では、絶対に使ってはいけないのでしょうか。そうでもありません。アプリの仕様として、ホバーすると別の要素が重なるなど、通常のクリックとは違う動作を意図的にテストしたい場合があります。
test('ホバー中のメニューを強制的に選択する', async ({ page }) => {
await page.goto('/menu');
const menuItem = page.getByRole('menuitem', { name: '設定' });
await menuItem.hover();
await menuItem.click({ force: true });
});
この場合でも、なぜ通常のクリックではなくforceなのかをテスト名かコメントに残してください。「とりあえず通す」ためのforceと、UIの仕様を明示したforceは別物です。
失敗したときの切り分け順
クリックで失敗したら、次の順番で確認すると原因を追いやすくなります。
1. locatorが複数要素を選んでいないか
同じ名前のボタンが画面の裏側にもあると、対象が一意になりません。getByRoleのnameや、必要ならlocator('button').filter()で対象を絞ります。
const buttons = page.getByRole('button', { name: '保存' });
await expect(buttons).toHaveCount(1);
await buttons.click();
2. 見えているか、表示領域に入っているか
ゼロサイズの要素やdisplay: noneの要素は、ユーザーには見えません。toBeVisible()やtoBeInViewport()は、原因を切り分けるために使えます。
const submit = page.getByRole('button', { name: '注文を確定' });
await expect(submit).toBeVisible();
await expect(submit).toBeInViewport();
await submit.click();
3. ボタンが有効になる条件を確認する
入力チェックが終わるまでボタンがdisabledになっている画面はよくあります。ここで固定時間のwaitForTimeout()を足すより、ユーザーが押せる条件をassertionにします。
await page.getByLabel('メールアドレス').fill('user@example.com');
await expect(submit).toBeEnabled();
await submit.click();
4. オーバーレイを調べる
Receives Eventsで失敗した場合は、Cookieバナー、ローディングスピナー、固定ヘッダー、モーダルの背面などを疑います。テスト側で無理にクリックするのではなく、まずアプリ側の表示完了条件を待つべきです。
await expect(page.getByRole('progressbar')).toBeHidden();
await expect(page.getByRole('button', { name: '注文を確定' })).toBeVisible();
await page.getByRole('button', { name: '注文を確定' }).click();
trialでactionabilityだけ確認する
クリックせずに「いまクリックできるか」だけを確認したいときは、trial: trueが使えます。
const submit = page.getByRole('button', { name: '注文を確定' });
await submit.click({ trial: true });
await submit.click();
ただし、通常は一度のclick()に自動待機させれば十分です。trialを増やしすぎると、同じ条件を二度確認するテストになりやすいので、診断や特殊な操作フローで使ってください。
まとめ:forceではなく画面の状態を直す
Seleniumで見てきたelement click interceptedは、Playwrightではactionability checksとしてより具体的に扱われます。見えているだけではなく、動いていないか、イベントを受け取れるか、有効かまで確認してからクリックします。
クリックで失敗したときの基本方針は次のとおりです。
- locatorを一意にする
- 表示・安定・有効の条件を確認する
- オーバーレイやローディングをアプリ側の状態として待つ
force: trueは仕様上必要な場合だけ使う
Playwrightの自動待機を黙らせると、テストは通るかもしれません。しかし、ユーザーが押せない画面を見逃します。失敗を消すより、なぜクリックできないのかを直す方が、長い目で見ると安いです。


コメント