QR Send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

Seleniumのelement click interceptedはPlaywrightだとなぜ起きないのか:force: trueで黙らせてはいけない理由

ブラウザ自動化

Seleniumでブラウザを動かしていると、element click intercepted に出会うことがあります。

要素は見つかっています。ですが、クリックする瞬間に別の要素が上に重なっている。これがこのエラーの典型です。

Playwrightに移行すると、このエラーに悩む時間がかなり減ります。Playwrightがクリック前に、要素が表示されているか、動いていないか、クリックを受け取れるかを自動で確認してくれるからです。

今回は、Seleniumで起きるクリック失敗と、Playwrightのactionability checksの違いを見ていきます。最後にforce: trueを安易に使ってはいけない理由も説明します。

「要素はあるのにクリックできない」

まず、Seleniumでありがちなコードを見てみましょう。

button = driver.find_element(By.CSS_SELECTOR, "button.submit")
button.click()

これでボタンをクリックできます。ところが、ページの表示直後やモーダルが閉じる途中だと、次のようなエラーになることがあります。

ElementClickInterceptedException:
  element click intercepted: Element <button class="submit">...

この場合、find_elementは成功しています。要素がDOMに存在することと、今クリックできることは別です。

たとえば、Cookieバナーがボタンの上に残っているかもしれません。画面上はボタンが見えていても、クリック位置に別の要素があればブラウザはその要素へクリックを渡します。

sleepを足せば直るのでしょうか

最初に思いつくのは、少し待つことです。

time.sleep(2)
driver.find_element(By.CSS_SELECTOR, "button.submit").click()

たまたま動くことはあります。ですが、2秒後に必ずクリックできる保証はありません。

ネットワークが速い日は0.5秒でモーダルが消えるかもしれません。遅い日は3秒かかるかもしれません。2秒待つコードは、速い日には無駄に待ち、遅い日にはまた失敗します。

では、Playwrightで同じ操作を書いてみましょう。

await page.locator("button.submit").click();

Playwrightのlocator.click()は、見つけた要素へすぐ座標を送るだけではありません。クリックしてよい状態になるまで、必要な確認をしてから操作します。

Playwrightがクリック前に確認していること

Playwrightのactionability checksは、アクションごとに異なります。クリックでは主に次の確認が行われます。

  • 要素がDOMに接続されている
  • 要素が表示されている
  • 要素が動いていない
  • 要素が有効になっている
  • クリック位置が別の要素に覆われていない

つまり、要素を見つけるだけではなく、「ユーザーが今クリックできる状態か」を待ちます。

特に大事なのが、最後の「クリック位置が別の要素に覆われていない」です。Cookieバナーやローディング画面が残っていると、Playwrightはそれを検出して待ちます。

それでも条件が満たされなければ、タイムアウト時にどの状態が問題だったかを含んだエラーになります。原因を隠さずに失敗してくれるので、テストの修正方針を立てやすいです。

実際にオーバーレイを閉じてみる

クリックを邪魔しているものがあるなら、待ち時間を増やすのではなく、その要素を閉じるのが正解です。

const cookieBanner = page.locator("[data-testid='cookie-banner']");

if (await cookieBanner.isVisible()) {
  await cookieBanner.getByRole("button", { name: "Accept" }).click();
}

await page.getByRole("button", { name: "Submit" }).click();

このコードでは、Cookieバナーが見えている時だけ閉じています。閉じた後にSubmitをクリックするので、テストの手順と実際のユーザー操作が一致します。

ここで、単にisVisible()を確認すれば十分とは限りません。バナーが見えなくなるアニメーション中なら、Playwrightのクリック側の待機も働きます。

テストが落ちたら、まず次を確認してください。

  1. エラーメッセージに、どのactionability checkが失敗したかを見る
  2. 画面を覆っている要素やローディング表示を特定する
  3. その要素を閉じる、またはテストデータを直す
  4. 最後の手段として、待機時間ではなく状態の変化を待つ

force: trueで無理に通すのは危険です

Playwrightには、actionability checksを一部省略する方法があります。

await page.locator("button.submit").click({ force: true });

これでテストが通ることはあります。ですが、テストが通ったことと、ユーザーがクリックできることは別です。

ボタンの上にCookieバナーがあるのにforce: trueでクリックすると、テストは「クリックした」ことにして先へ進みます。実際のユーザーなら押せない画面なのに、テストだけが成功する状態です。

これは、テストの信頼性を下げます。UIの不具合を見つけるためのテストが、UIの不具合を見逃すテストになってしまいます。

force: trueを使うなら、なぜ通常のクリックができないのかを確認した後にしてください。たとえば、意図的に透明なレイヤーの下を操作する特殊なUIなど、ユーザー操作との違いを説明できる場合だけです。

それでも失敗する時の切り分け

Playwrightを使ってもクリックに失敗することはあります。自動待機があるから、すべてのUIが勝手に安定するわけではありません。

まず、locatorが複数要素に一致していないか確認します。

const submit = page.getByRole("button", { name: "Submit" });
console.log(await submit.count());

2個以上なら、画面上のどのSubmitなのかを絞る必要があります。テスト用にnth(0)を足す前に、フォームやダイアログの単位でlocatorを狭くしてください。

const dialog = page.getByRole("dialog", { name: "Account" });
await dialog.getByRole("button", { name: "Submit" }).click();

次に、ボタンが本当に有効になる条件を確認します。disabledのままなら、入力値やバリデーションエラーを直す必要があります。

await page.getByLabel("Email").fill("user@example.test");
await page.getByLabel("Password").fill("pass-for-test");
await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();
await page.getByRole("button", { name: "Submit" }).click();

このように、待つべきものを「2秒」ではなく「ボタンが有効になること」として書くと、テストの意図が残ります。

Seleniumから移行する時の考え方

Seleniumでは、要素を探す処理と、クリックできる状態を待つ処理を自分で組み合わせることが多くあります。

wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit")))
driver.find_element(By.CSS_SELECTOR, "button.submit").click()

この書き方自体は悪くありません。ただし、element_to_be_clickableが確認する条件と、実際にクリックを邪魔するオーバーレイの状態が一致しないことがあります。

Playwrightでは、locatorに操作を任せることで、待機と操作が分離しません。クリック直前の状態を見て、条件が崩れたらlocatorを再評価します。

もちろん、テストコードが短くなればすべて解決するわけではありません。locatorが曖昧なら曖昧なままですし、アプリ側のオーバーレイ制御が壊れていればテストは止まります。

ですが、sleepを散りばめたり、JavaScriptで強制クリックしたりする前に、画面の状態を直す方向へ進める。この判断がしやすくなります。

まとめ

element click interceptedは、要素が存在しないエラーではありません。クリック位置に別の要素が重なっている、または要素がまだ動いているという状態のエラーです。

Playwrightのlocator.click()は、表示・安定・有効・クリック可能かを確認してから操作します。だから、固定秒数のsleepを減らせます。

クリックできないなら、まず邪魔している要素やdisabledの理由を直してください。force: trueでテストだけを通すのは最後です。

Seleniumの明示的な待機に慣れている人ほど、Playwrightでは「何秒待つか」ではなく「何が起きたら次へ進めるか」をlocatorとexpectで書くと、テストが読みやすくなります。

これ、意外と使えます。クリックが不安定なテストを見つけたら、まずforce: trueを足す前に、画面を覆っている要素を探してみてください。

コメント

タイトルとURLをコピーしました