Playwright Locator完全ガイド|get_by_role・filter・strict modeの使い方

Playwright Locatorは、画面上の要素を指定するための仕組みです。get_by_role()だけで書き始めたものの、「同じボタンが複数あってエラーになる」「リストの特定の行だけ操作したい」といった場面で手が止まる方は少なくありません。この記事では、Locatorの遅延評価の仕組み、strict mode、filter()によるチェーン、or_()・and_()、iframeまでを、実務で起きやすい失敗例と合わせて整理します。

最大のポイントは「Locatorは探し方の定義であり、操作やアサーションの実行時に、現在のDOMから対象要素が解決される」という点です。ここを押さえると、strict modeのエラーも、.firstで逃げてしまう癖も、整理しやすくなります。

📌 この記事はこんな方におすすめ

  • get_by_role()やget_by_text()は使えるが、複数一致のエラーでよく詰まるPlaywright(Python)ユーザー
  • テーブルやリストの「特定の行の中のボタン」を安定して操作したい方
  • .firstや.nth(0)に頼ったテストが、あとから壊れて困っている方
  • ロケーター入門は読んだので、Playwright固有の機能をもう一段深く知りたい方

✅ この記事を読むと得られること

  • Locatorが「遅延評価」で動く理由と、Seleniumのfind_elementとの違い
  • strict modeのエラーを、.firstに頼らず直す方法
  • filter()・チェーン・or_()・and_()の使いどころ
  • locator.all()で起きやすい「検証が空振りする」失敗の避け方

👤 この記事を書いた人

QAエンジニア・テスト自動化エンジニアとして15年以上の実務経験を持つ Yoshi が執筆。SeleniumからPlaywrightへの移行や、壊れにくいテスト設計を実務で担当してきました。

📌 結論

  • Playwright Locatorは遅延評価され、操作やアサーションの実行時に現在のDOMから対象要素を解決する。そのため、再描画後に古い要素を保持したまま操作する形になりにくい
  • 複数一致のエラー(strict mode)は、.firstではなくfilter()やスコープ指定で絞るほうが壊れにくい
  • locator.all()は待機しないため、動的なリストでは件数の検証を先に行ってからループする

Locatorを使い始めると、次のような場面に遭遇しやすくなります。

  • strict mode violationというエラーで、クリックが実行されない
  • テーブルの「田中さんの行」の編集ボタンだけを押したいのに、書き方がわからない
  • エラーを避けるために.firstを付けたら、別の要素を操作していた
  • for item in locator.all()で回したら、ループが1回も実行されずテストが通ってしまった

これらは、Locatorの性質を理解すると整理しやすくなります。CSSセレクターやXPathの基本は、既存の入門記事に譲り、この記事ではPlaywright固有の機能に絞ります。

Playwright Locatorとは

Locatorは、「どの要素を指すか」という探し方の定義を表すオブジェクトです。page.locator()やpage.get_by_role()を書いた時点では、まだ画面を探しにいきません。クリックや入力などの操作、またはアサーションを実行したタイミングで、現在のDOMから対象要素を解決します。操作の場合は、操作可能になるまで待機します。

from playwright.sync_api import Page, expect

def test_save(page: Page) -> None:
    page.goto("/profile")

    # この時点では、まだ要素を探していない(探し方を決めただけ)
    save_button = page.get_by_role("button", name="保存")

    # 操作した時点で探し、操作可能になるまで待つ
    save_button.click()

    # 画面が再描画されても、次の操作のたびに現在のDOMから解決される
    expect(page.get_by_text("保存しました")).to_be_visible()
観点Selenium(find_element)Playwright(Locator)
探すタイミング呼び出した瞬間操作を実行した瞬間
戻り値要素そのもの(WebElement)探し方の定義(Locator)
再描画後の扱い古い要素を指してStaleElementReferenceExceptionになりうる操作のたびに現在のDOMから解決するため、影響を受けにくい
💡 補足:page.query_selector()が返すElementHandleは、SeleniumのWebElementに近い性質を持ち、公式ドキュメントでもLocatorの利用が推奨されています。新しく書くテストでは、Locatorを使うのが基本です。

① get_by_*の選び方

メソッド狙うものメリットデメリット
get_by_role()ボタン・リンクなどの役割と名前ユーザーの見え方に近く、実装変更に強いロール名やnameの仕様を知る必要がある。ラベル文言が変わると影響を受ける
get_by_label()フォーム入力欄のラベル入力欄を意図どおりに指定しやすいラベルが紐付いていない画面では使えない
get_by_placeholder()入力欄のプレースホルダーラベルがない入力欄でも指定できる文言変更に弱い
get_by_text()表示テキストメッセージ表示の確認に向く部分一致が既定で、意図しない要素に一致しやすい。多言語画面にも弱い
get_by_alt_text() / get_by_title()画像のalt、title属性画像やアイコンを指定できる属性が設定されていないと使えない
get_by_test_id()data-testid属性文言やデザインの変更に左右されにくい開発側にテスト用属性の追加を依頼する必要がある
# name は既定で「部分一致・大文字小文字を区別しない」
page.get_by_role("button", name="保存")              # 「下書き保存」にも一致しうる

# 完全一致にしたい場合は exact=True
page.get_by_role("button", name="保存", exact=True)  # 「保存」だけに一致

# テスト用属性の名前は、プロジェクトに合わせて変更できる
# (data-testid 以外の属性を使っている場合は、公式ドキュメントで設定方法を確認)
🔍 実務あるある:get_by_text()やget_by_role()のnameは、デフォルトでは部分一致です。「保存」を指定すると、「下書き保存」「保存して閉じる」などにも一致して、複数ヒットになることがあります。

② strict modeとは:複数一致で止まる仕組み

Locatorでclick()やfill()などの操作を行うとき、一致した要素が2つ以上あるとエラーになります。これがstrict modeです。「どの要素を操作するのか曖昧なまま進まない」ための安全装置で、意図しない要素を押してしまう事故を防いでくれます。

# 画面に「削除」ボタンが3つある場合
page.get_by_role("button", name="削除").click()
# Error: strict mode violation: get_by_role("button", name="削除") resolved to 3 elements

エラーの直し方:絞り込みの優先順位

対処書き方の例評価
スコープで絞る行やカードの中から探す(後述)意図が読み取れて壊れにくい
filter()で絞る.filter(has_text="田中")内容で特定できて読みやすい
exact=Truename="保存", exact=True部分一致が原因のときに有効
.first / .nth(n).first.click()最後の手段。表示順が変わると別要素を操作する
⚠️ 注意:.firstは、エラーを消すには手軽な方法です。ただし「何番目か」に意味を持たせるため、並び順が変わった日にテストが別の要素を操作しはじめます。しかもエラーにならないので、気づくのが遅れがちです。

③ filter()で内容から絞り込む

# テキストで絞り込む
product = page.get_by_role("listitem").filter(has_text="ノートPC")

# 中に特定の要素を持つものだけに絞り込む
in_stock = page.get_by_role("listitem").filter(
    has=page.get_by_role("button", name="カートに入れる")
)

# 特定のテキストを含まないものに絞り込む(has_not_text は比較的新しい引数)
available = page.get_by_role("listitem").filter(has_not_text="売り切れ")
💡 実務Tip:has_not_textやhas_notなど、後から追加された引数があります。使えるかどうかは、プロジェクトで固定しているPlaywrightのバージョンと公式ドキュメントで確認してください。

④ チェーンとスコープ指定:「この行の、このボタン」

一覧画面で「田中さんの行の編集ボタン」を押したいときは、先に行を特定し、その中でボタンを探す形にします。Locatorは、get_by_*やlocator()をつなげて範囲を絞れます。

def test_edit_user(page: Page) -> None:
    page.goto("/users")

    # ① 行を特定する
    row = page.get_by_role("row").filter(has_text="田中")

    # ② その行の中だけで、ボタンを探す
    row.get_by_role("button", name="編集").click()

    expect(page.get_by_role("heading", name="ユーザー編集")).to_be_visible()
✅ ポイント:rowはLocatorなので、画面が更新されても操作のたびに現在のDOMから解決されます。Page Objectに「行を返すメソッド」を用意しておくと、画面の変更にも追従しやすくなります。

⑤ or_()・and_()と、件数・全件の扱い

or_():どちらかが出ればよい場合

# ログイン後、「ダッシュボード」または「初回ガイド」のどちらかが表示される
dashboard = page.get_by_role("heading", name="ダッシュボード")
guide = page.get_by_role("heading", name="はじめにお読みください")

expect(dashboard.or_(guide)).to_be_visible()
⚠️ 注意:or_()は、どちらか一方が表示されるケースで便利です。ただし、両方が存在する可能性がある場合は2要素に一致して、strict modeのエラーになることがあります。目的に応じて、.firstなどで扱いを決めてください。

and_():複数の条件を同時に満たす場合

# 「ボタンである」かつ「title が購読」である要素
subscribe = page.get_by_role("button").and_(page.get_by_title("購読"))

件数の検証と、locator.all()の落とし穴

# ❌ 検索ボタンを押した直後に all() で回す
page.get_by_role("button", name="検索").click()
for item in page.get_by_role("listitem").all():   # all() は待機しない
    assert "ノートPC" in item.inner_text()
# 結果が表示される前だと、リストが空でループが1回も実行されず、テストが通ってしまう

# ✅ 先に件数を検証してから回す(件数の検証は、条件を満たすまで待機してくれる)
items = page.get_by_role("listitem")
expect(items).to_have_count(5)
for item in items.all():
    expect(item).to_contain_text("ノートPC")
⚠️ 注意:locator.all()は、その時点で見つかっている要素を返すだけで、要素の出現は待ちません。公式ドキュメントでも、要素が動的に変わる場合は不安定になりうると説明されています。動的に追加されるリストでは、まずexpect(...).to_have_count()などで期待する状態になるまで待ってから、all()を使うのが安全です。

⑥ iframeとShadow DOMの扱い

# iframe の中の要素:frame_locator() で入り口を作ってから、通常どおり探す
payment = page.frame_locator("#payment-frame")
payment.get_by_label("カード番号").fill("4242 4242 4242 4242")
payment.get_by_role("button", name="支払う").click()
  • iframe:frame_locator()を挟まないと、iframe内の要素は見つかりません。Seleniumのswitch_to.frame()のような「切り替え」は不要です。
  • Shadow DOM:PlaywrightのCSSセレクターなどは、open状態のShadow DOMを自動で貫通します。一方、XPathは貫通しません。closedのShadow DOMは外から操作できないため、開発側にテスト用の仕組みを相談する必要があります。詳細な仕様は公式ドキュメントで確認してください。

⑦ ロケーターが決まらないときのデバッグ方法

# 画面を操作しながら、ロケーターの候補を出してくれる
#   $ playwright codegen https://example.com
#   → 「Pick locator」で要素を選ぶと、候補のLocatorが表示される

# テストの途中で止めて、Inspector で確認する
page.pause()

# 該当要素が画面上のどこかを、目で確認する
page.get_by_role("button", name="削除").highlight()

# 何件に一致しているかを確認する
print(page.get_by_role("button", name="削除").count())
💡 実務Tip:codegenが出力するLocatorは、そのまま使えることも多い一方で、文言や順序に依存した書き方になることもあります。コピーしたあとに、filter()やスコープ指定で意図が伝わる形に直しておくと、保守が楽になります。

実務でよくある失敗パターン

① エラーを消すために.firstを付ける

並び順や件数が変わったとき、別の要素を操作しながらテストが通り続けます。まずスコープやfilter()で絞れないかを考えます。

② locator.all()の直後にループして、検証が空振りする

要素が出る前だとリストが空になり、ループの中のassertが1回も実行されません。to_have_count()などで件数を先に確認します。

③ 部分一致(既定)で、別の要素に当たる

「保存」が「下書き保存」にも一致します。get_by_role()のnameも同じく部分一致なので、必要に応じてexact=Trueを付けます。

④ 文言ベースのLocatorが、多言語対応や文言変更で壊れる

日本語・英語を切り替える画面では、name="保存"が英語表示で一致しなくなります。重要な操作にはget_by_test_id()の併用を検討します。

⑤ 変数に入れたElementHandleを使い回す

画面の再描画後に古い要素を指して、操作に失敗します。使い回すなら、Locatorの形で持っておきます。

FAQ

Q. get_by_role()とget_by_test_id()は、どちらを優先すべきですか?

公式ドキュメントでは、ユーザーの見え方に近いget_by_role()などが推奨されています。ただし、文言が頻繁に変わる画面や多言語対応の画面では、get_by_test_id()のほうが安定する場合もあります。プロジェクトの事情に合わせて、併用するのが現実的です。

Q. .firstや.nth()は使ってはいけませんか?

禁止ではありません。「検索結果の先頭を確認したい」のように、順序そのものが仕様である場合には適しています。避けたいのは、エラーを消す目的だけで付けるケースです。

Q. Seleniumのfind_elementとの一番大きな違いは何ですか?

find_elementは呼んだ瞬間に探して要素を返しますが、Locatorは操作する瞬間に探します。そのため、再描画後の古い要素エラーが起きにくく、待機も自動で行われます。

Q. strict modeをオフにすることはできますか?

Locatorの操作では、複数一致を許可する設定は用意されていません。複数の要素を扱いたいときは、count()・all()・nth()を使うか、絞り込みを見直します。

Q. data-testid以外の属性名を使えますか?

テスト用属性の名前は変更できます。設定方法はバージョンによって異なる場合があるため、使用中のバージョンの公式ドキュメントで確認してください。

📋 まとめ

  • Playwright Locatorは遅延評価で、操作やアサーションの実行時に現在のDOMから解決される
  • 複数一致のエラー(strict mode)は、スコープ指定やfilter()で絞って直す
  • .firstは最後の手段にして、順序に意味がある場合だけ使う
  • locator.all()の前に、to_have_count()で件数を検証しておく
  • iframeはframe_locator()を挟み、Shadow DOMはopen/closedの違いに注意する

まずは、いま.firstや.nth(0)を使っている箇所を1つ探して、スコープ指定かfilter()に置き換えてみてください。意図がコードから読み取れるだけで、テストの保守がかなり楽になります。

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