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=True | name="保存", 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="売り切れ")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())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()に置き換えてみてください。意図がコードから読み取れるだけで、テストの保守がかなり楽になります。
