Playwrightのlocator(ロケーター)は、ボタンや入力欄を「画面の利用者から見える役割と名前」で指定する仕組みです。この記事では、getByRole・getByLabel・getByTestIdの書き方、filterやwithinで一覧の1行に絞る手順、テストが「strict mode violation」で落ちたときの直し方を、そのまま動かせるTypeScriptのコードで解説します。版はPlaywright 1.65.1(2026年11月時点のnpm最新)で、1.63〜1.65で入った変更も反映しました。Playwright自体の位置づけはPlaywright(プレイライト)とは?仕組み・できること・Seleniumとの違いで整理しています。
まとめ:Playwrightのlocatorは役割と名前で書き、1要素に絞り込む
要素の指定は、まずgetByRoleで役割(button・textbox・rowなど)とアクセシブルネームを書きます。入力欄はgetByLabel、役割も名前も付けられない要素だけgetByTestIdに回し、CSSやXPathは最後の手段にします。
strict mode violationは「locatorが2件以上に一致した」という知らせです。first()やnth()で黙らせず、名前の完全一致、親要素での絞り込み、テストIDの順で1件にします。codegenが出した候補もこの基準で読み直し、ESLintのルールでレビューを機械化すると、チームの書き方がそろいます。
Playwrightのlocatorが自動待機と再検索で壊れにくい理由
locatorは操作のたびに要素を探し直す遅延評価のオブジェクト
locatorは「要素そのもの」ではなく「要素の探し方」を保持します。ElementHandleの公式ページは、ElementHandleが特定の要素を指すのに対し、Locatorは要素を取り出すロジックを保持すると説明し、ElementHandleの使用を非推奨としています。
この差が、再描画に強いかどうかを分けます。ReactやVueで一覧が描き直されても、locatorはclickやfillの直前に探し直すため、古い要素を掴んだまま失敗することがありません。操作前には表示・有効・安定などの条件がそろうまで自動で待ちます。
import { test, expect } from '@playwright/test';
test('ログインしてダッシュボードを開く', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('メールアドレス').fill(process.env.E2E_EMAIL!);
await page.getByLabel('パスワード').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'ログイン' }).click();
await expect(page.getByRole('heading', { level: 1 })).toHaveText('ダッシュボード');
});
認証情報は環境変数から渡し、コードに直接書かないでください。テストの初期化から実行まではPlaywrightの使い方入門:E2Eテストを初期化から実行・CIまで書く手順にまとめています。
公式の推奨順はgetByRoleが先頭で、CSSとXPathは最後の手段
公式のLocatorsガイドは、組み込みのlocatorを次の順で紹介しています。上から順に検討し、合わなければ次へ進むのが基本の使い方です。
| メソッド | 何で探すか | 向く要素 |
|---|---|---|
| getByRole | ARIAの役割と名前 | ボタン・リンク・見出し・表 |
| getByLabel | labelの文言 | フォームの入力欄 |
| getByPlaceholder | placeholder属性 | label無しの入力欄 |
| getByText | 表示テキスト | 操作しない文言の確認 |
| getByAltText | alt属性 | 画像 |
| getByTitle | title属性 | アイコンボタン |
| getByTestId | data-testid属性 | 役割も名前も無い要素 |
| locator | CSS・XPath | 上記で取れない要素 |
CSSやXPathはDOMの構造やクラス名に依存するため、デザイン変更で壊れます。公式も長いCSS・XPathの連結を悪い書き方として挙げています。
getByRoleとgetByLabelでボタン・入力欄・見出しを指定する書き方
getByRoleのnameは部分一致が既定、exactの指定で完全一致
page.getByRoleのAPIリファレンスによると、nameは既定で大文字小文字を区別しない部分一致です。exact: trueを付けると、大文字小文字を区別した文字列全体の一致に変わります。
// 「保存」と「保存して閉じる」の両方に一致してしまう
page.getByRole('button', { name: '保存' });
// 「保存」だけに一致する
page.getByRole('button', { name: '保存', exact: true });
// 正規表現も使える(exactは無視される)
page.getByRole('link', { name: /^請求書\s*\d+$/ });
アクセシブルネームは要素の中身・aria-label・関連付けたlabelから決まります。計算の規則はWAI-ARIA 1.2の仕様と関連仕様が元です。1.65のリリースノートでは、描画されないテキストをnameの計算から外す変更が告知されており、非表示の補足文を含めて一致させていたlocatorは結果が変わる可能性があります。
checked・level・expandedで同じ役割の要素を区別する指定
同じ役割の要素が並ぶ画面では、状態のオプションで区別できます。getByRoleが受け付けるのはchecked・disabled・expanded・includeHidden・level・pressed・selected・name・description・exactで、descriptionは1.60で追加されました。
// h2の見出しだけ
page.getByRole('heading', { level: 2, name: '請求先' });
// チェック済みのチェックボックス
page.getByRole('checkbox', { checked: true });
// 開いているアコーディオンのボタン
page.getByRole('button', { expanded: true });
この指定が効くのは、aria-checkedやaria-expandedが正しく出力されているときだけです。自作のトグルでこれらの属性が付いていない場合、locatorではなく部品側を直すほうが、支援技術の利用者にも効く修正になります。
data-testidを使う場面とtestIdAttributeでの属性名変更
アイコンだけのボタンや、文言が日替わりで変わるバナーのように、役割も名前も安定しない要素にはテスト専用の属性を付けます。既定の属性名はdata-testidで、既存の命名に合わせたい場合は設定ファイルのuseオプションのtestIdAttributeで変えられます。
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
testIdAttribute: 'data-qa',
},
});
// テスト側:data-qa="cart-badge" の要素を指定する
await expect(page.getByTestId('cart-badge')).toHaveText('3');
テストIDは利用者に見えない契約です。付けすぎるとgetByRoleで書けた箇所までIDに頼るようになり、アクセシビリティの崩れをテストが検知できなくなります。
filter・and・or・withinで一覧の中の1行と1ボタンに絞る手順
filterのhasTextとhasで請求書一覧の特定行を指定するチェーン
一覧画面では「どの行の」削除ボタンかを指定しないと、ボタンが行数ぶん一致します。LocatorのAPIリファレンスにあるfilterで行を絞り、その内側でボタンを探します。filterで指定できる条件は、hasText・hasNotText・has・hasNot・visibleです。
// テキストで行を絞る
const row = page.getByRole('row').filter({ hasText: 'INV-2026-1042' });
await row.getByRole('button', { name: '削除' }).click();
// 子要素の有無で絞る:「未払い」バッジを持つ行だけ
const unpaid = page.getByRole('row').filter({
has: page.getByText('未払い', { exact: true }),
});
await expect(unpaid).toHaveCount(2);
hasに渡すlocatorは外側の要素を起点に評価されるため、page全体を探すわけではありません。表示中の要素だけに絞る場合は、1.63で追加されたvisible()が短く書けます。
and・orの組み合わせと1.64で入ったwithinでの列指定
and(1.34)は2つの条件の両方に一致する要素、or(1.33)はどちらかに一致する要素を返します。orは「新規メール画面か確認ダイアログのどちらかが出る」画面で、先に出たほうを待つ用途に向きます。
// 役割とtitleの両方で一致
const subscribe = page.getByRole('button').and(page.getByTitle('Subscribe'));
// どちらかが表示されるまで待つ(両方出るとstrict違反になるのでfirst())
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog).first()).toBeVisible();
// 各行の3列目のセルだけ(1.64以降)
const thirdColumn = page.getByRole('cell').nth(2).within(page.getByRole('row'));
withinでは、nth()やfirst()が親要素ごとに解決されます。表全体の3番目ではなく「各行の3番目」になるため、列単位の検証が1行で書けます。
iframe内とShadow DOM内の要素をlocatorで扱うときの制約
決済フォームや埋め込み地図のようなiframeの中身は、contentFrame()(1.43)でフレーム用のlocatorに切り替えてから探します。
const payment = page.getByTitle('決済フォーム').contentFrame();
await payment.getByLabel('カード番号').fill(process.env.E2E_CARD_NUMBER!);
Shadow DOMは既定で中まで探せるため、特別な書き方は要りません。例外は2つで、XPathはshadow rootを越えられず、closedモードのshadow rootには対応していません。Web Componentsを使う画面でXPathを書いているなら、getByRoleへの置き換えが先決です。
strict mode violationが出たときの原因の読み方と直す順番
エラーのresolved to N elementsに並ぶ候補から重複の理由を読む
Playwrightのlocatorは厳密(strict)で、clickやfillのように1要素を対象にする操作で2件以上に一致すると例外を投げます。メッセージは「strict mode violation」に続けて「resolved to 2 elements」のように件数を示し、一致した要素の一覧を出します。
直す前に、この一覧を読んでください。同じ名前のボタンがヘッダーとフッターにある、モーダルの裏に同じフォームが残っている、レスポンシブ用にPC版とSP版の両方がDOMにある、の3つが典型です。count()のように複数件を扱う操作では例外になりません。
first・nthで黙らせず、名前・親要素・テストIDの順で絞る判断
公式のLocatorsガイドは、first()・last()・nth()を「推奨しない」と明記しています。ページが変わると、黙って別の要素を操作するからです。直す順番は次のとおりです。
- nameにexact: trueを付ける、または名前を足して1件にする
- dialog・navigation・rowなど親の役割で範囲を絞り、その内側で探す
- 非表示の重複(SP版メニュー等)はvisible()で外す
- それでも区別できない要素だけgetByTestIdにする
// 修正前:ヘッダーとモーダルの両方の「保存」に一致して落ちる
await page.getByRole('button', { name: '保存' }).click();
// 修正後:開いているダイアログの中に限定する
await page.getByRole('dialog', { name: '請求先の編集' })
.getByRole('button', { name: '保存', exact: true })
.click();
first()を使ってよいのは、orで「どちらかが出た」ことを確かめる場面のように、どの要素でも結果が同じときだけです。
locator.allと表示待ちの組み合わせで起きる不安定なテスト
一覧の全行を順に検証したいときに使うall()(1.29)は、要素の出現を待たず、その瞬間にある要素だけを返します。APIリファレンスも、一覧が動的に変わる場合は結果が予測できず不安定になると注意しています。
const rows = page.getByRole('row').filter({ hasText: '未払い' });
// 先に件数がそろうまで待ってから列挙する
await expect(rows).toHaveCount(3);
for (const row of await rows.all()) {
await expect(row.getByRole('cell').nth(3)).not.toBeEmpty();
}
toHaveCountで件数を待ってからall()を呼ぶと、読み込み途中の行数で検証が走る事故を防げます。
codegenとPick locatorで生成したlocatorを読み直して直す作業
npx playwright codegenが出す候補の優先順と手直しの基準
テストジェネレーター(codegen)は、画面を操作するとlocator付きのコードを生成します。公式のベストプラクティスによると、生成はrole・テキスト・テストIDを優先し、複数に一致する場合は1件に絞れるよう改良されます。
npx playwright codegen http://localhost:3000/invoices
生成結果はそのまま採用せず、次の2点を確かめます。1つは、getByTextで取れたボタンをgetByRoleに書き換えられないか。もう1つは、「INV-2026-1042」のようなテストデータ依存の文言がnameに入っていないかです。データ依存の文言は、テストの前提データが変わった時点で壊れます。CLIの他のオプションはPlaywright CLIコマンド一覧にあります。
describeとhighlightでトレースとデバッグ時の対象を確認する方法
describe()(1.53)はlocatorに説明を付け、その説明がトレースビューアやレポートに表示されます。highlight()(1.20)は対象要素を画面上で強調するデバッグ用の機能で、公式はこれを使ったコードをコミットしないよう注意しています。
const submit = page.getByTestId('btn-sub').describe('請求書の確定ボタン');
await submit.highlight({ style: 'outline: 2px dashed red' }); // デバッグ時だけ
await submit.click();
VS Codeの拡張からPick locatorで要素を拾い、候補を確かめる操作はPlaywright VS Code拡張の使い方:テスト実行・デバッグ・Pick locatorと録画の手順で画面付きで解説しています。
受託開発でlocatorの方針を決める条件とgetByRoleを見送る場面
マークアップを直せる自社開発はgetByRole、直せない画面はテストID
判断の軸は、画面のHTMLを自分たちで直せるかどうかです。自社開発の画面ならgetByRoleを原則にし、一致しない要素はlocatorではなくマークアップ側にlabelやaria属性を足して直します。テストがアクセシビリティの回帰検知を兼ねるので、追加の費用がかかりません。
getByRoleを見送るのは、次の3つの場面です。canvasで描かれたグラフや地図のように役割が存在しない要素、改修予算の無い旧画面でラベルが一切付いていない場合、日英の表示切り替えで同じテストを両言語で流す場合です。3つ目は文言が言語ごとに変わるため、テストIDのほうが保守の手間が少なくなります。逆に、テストIDを全要素に一律で振る方針は採りません。利用者から見えない契約が増えるだけで、壊れにくさは得られないからです。
ESLintで位置指定と生locatorを検知するCIのレビュー自動化
書き方の方針は、コードレビューの目視に頼ると崩れます。eslint-plugin-playwrightには、first()・last()・nth()を禁止するno-nth-methods、page.locator()より組み込みlocatorを勧めるprefer-native-locators(自動修正あり)があり、どちらも推奨設定には含まれないため明示的に有効にします。
// eslint.config.js
import playwright from 'eslint-plugin-playwright';
export default [
{
...playwright.configs['flat/recommended'],
files: ['tests/**'],
rules: {
...playwright.configs['flat/recommended'].rules,
'playwright/no-nth-methods': 'error',
'playwright/prefer-native-locators': 'error',
'playwright/no-raw-locators': 'warn',
},
},
];
CIのlintでこれを止めておくと、strict違反をnth()で黙らせる修正はプルリクエストの段階で止まる仕組みです。E2Eテストの導入からCIへの組み込み、ルールの運用までまとめて任せたい場合は、一創のDevOps・CI/CD導入支援で相談を受けています。locatorを画面ごとのクラスにまとめる設計はPage Object Model(POM)の実装例と保守コストの実測が参考になります。
よくある質問
Playwrightのlocatorについて、書き始めと移行の段階で出やすい疑問をまとめました。
page.$やElementHandleで書いた古いテストは書き換えるべきですか?
書き換えを勧めます。公式が案内しているのは、非推奨のElementHandleに代えてLocatorとweb-firstアサーションを使う方法です。ElementHandleは取得した時点の要素を指し続けるため、再描画の後に古い要素を操作して失敗しやすくなります。eslint-plugin-playwrightのno-element-handleルール(推奨設定に含まれる)で残りを洗い出せます。
getByTextとgetByRoleはどちらを使えばよいですか?
ボタンやリンクのように操作する要素はgetByRoleを使います。getByTextは役割を見ないため、同じ文言の見出しやツールチップにも一致し、strict違反の原因になりがちです。getByTextは、操作しない文言の表示確認や、行を絞るfilterの条件に向いています。
XPathはPlaywrightで使えないのですか?
使えます。page.locator()に「xpath=」付き、または「//」で始まる式を渡すとXPathとして解釈されます。ただしDOM構造に依存して壊れやすく、Shadow DOMの中は探せません。Seleniumからの移行で残すのは一時的な措置とし、移行後にgetByRoleへ置き換える計画を立ててください。
Python版のPlaywrightでも同じ書き方ができますか?
Python版でも同じ考え方で記述が可能です。Python版のLocatorsガイドのとおり、メソッド名がスネークケースになり、page.get_by_role(“button”, name=”ログイン”)のように書きます。filterのhas_textやexactなどのオプションも同じ意味で使えます。
aria snapshotとlocatorはどう使い分けますか?
locatorは「どの要素を操作・検証するか」を指定する道具で、aria snapshotは「画面のアクセシビリティツリー全体が期待どおりか」をまとめて比べる道具です。ボタンを押す・値を入れるにはlocatorを使い、画面の構造の崩れを1回で検知したいときにaria snapshotを足します。書き方はPlaywright aria snapshotの使い方にあります。
関連記事
- Playwrightの使い方入門:E2Eテストを初期化から実行・CIまで書く手順【1.65対応】:locatorを書く前のテスト環境の作り方と実行方法
- Playwright VS Code拡張の使い方:テスト実行・デバッグ・Pick locatorと録画の手順【1.65対応】:エディタ上でlocatorを拾って確かめる操作
- Page Object Model(POM)とは?Playwright・Seleniumの実装例と保守コストの実測:locatorを画面ごとのクラスにまとめる設計
- Playwright aria snapshotの使い方|toMatchAriaSnapshotの書き方と更新手順【1.64対応】:アクセシビリティツリー全体を期待値にする検証
- React Testing Libraryのuser-event v14とクエリ実践|setup()・getByRole・queryBy・fireEventの使い分け:単体テスト側のgetByRoleとの書き方の違い