ロケーター
はじめに
ロケーターは、Playwrightのオートウェイトとリトライ可能性の中心的な要素です。簡単に言うと、ロケーターはいつでもページ上の要素を見つける方法を表します。
クイックガイド
これらは推奨される組み込みロケーターです。
- page.getByRole() は、明示的および暗黙的なアクセシビリティ属性で要素を特定します。
- page.getByText() は、テキストコンテンツで要素を特定します。
- page.getByLabel() は、関連するラベルのテキストでフォームコントロールを特定します。
- page.getByPlaceholder() は、プレースホルダーで入力を特定します。
- page.getByAltText() は、通常画像などの要素を、その代替テキストで特定します。
- page.getByTitle() は、そのタイトル属性で要素を特定します。
- page.getByTestId() は、その
data-testid
属性に基づいて要素を特定します(他の属性も設定可能です)。
await page.getByLabel('User Name').fill('John');
await page.getByLabel('Password').fill('secret-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome, John!')).toBeVisible();
要素の特定
Playwrightには複数の組み込みロケーターが付属しています。テストの回復力を高めるために、ユーザーに見える属性や、page.getByRole() のような明示的な契約を優先することをお勧めします。
例えば、次のDOM構造を考えてみましょう。
<button>Sign in</button>
「サインイン」という名前のbutton
の役割で要素を特定します。
await page.getByRole('button', { name: 'Sign in' }).click();
コードジェネレーターを使用してロケーターを生成し、必要に応じて編集してください。
ロケーターがアクションに使用されるたびに、最新のDOM要素がページ内で特定されます。以下のスニペットでは、基となるDOM要素は、各アクションの前に1回ずつ、計2回特定されます。これは、レンダリングによって呼び出しの間にDOMが変更された場合、ロケーターに対応する新しい要素が使用されることを意味します。
const locator = page.getByRole('button', { name: 'Sign in' });
await locator.hover();
await locator.click();
page.getByLabel() のようにロケーターを作成するすべてのメソッドは、Locator および FrameLocator クラスでも利用できるため、それらを連結してロケーターを段階的に絞り込むことができます。
const locator = page
.frameLocator('#my-frame')
.getByRole('button', { name: 'Sign in' });
await locator.click();
ロールによる特定
page.getByRole() ロケーターは、ユーザーや支援技術がページをどのように認識するか、例えば、ある要素がボタンかチェックボックスかなどを反映します。ロールで特定する場合、通常、アクセシブルな名前も渡すことで、ロケーターが正確な要素を指し示すようにします。
例えば、次のDOM構造を考えてみましょう。
サインアップ
<h3>Sign up</h3>
<label>
<input type="checkbox" /> Subscribe
</label>
<br/>
<button>Submit</button>
各要素をその暗黙の役割で特定できます。
await expect(page.getByRole('heading', { name: 'Sign up' })).toBeVisible();
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
await page.getByRole('button', { name: /submit/i }).click();
ロールロケーターには、ボタン、チェックボックス、見出し、リンク、リスト、テーブルなどが含まれ、ARIAロール、ARIA属性、アクセシブルネームに関するW3Cの仕様に準拠しています。<button>
のような多くのHTML要素には、ロールロケーターによって認識される暗黙的に定義されたロールがあることに注意してください。
ロールロケーターはアクセシビリティ監査や適合性テストを**置き換えるものではない**ことに注意してください。むしろ、ARIAガイドラインに関する早期のフィードバックを提供します。
要素の特定にはロールロケーターを優先することをお勧めします。これは、ユーザーや支援技術がページをどのように認識するかに最も近い方法だからです。
ラベルによる特定
ほとんどのフォームコントロールには通常、フォームとのインタラクションに便利に使える専用のラベルが付いています。この場合、page.getByLabel() を使用して、関連するラベルのテキストでコントロールを特定できます。
例えば、次のDOM構造を考えてみましょう。
<label>Password <input type="password" /></label>
ラベルのテキストで入力を特定した後、それを入力できます。
await page.getByLabel('Password').fill('secret');
フォームフィールドを特定する際にこのロケーターを使用します。
プレースホルダーによる特定
入力要素には、ユーザーに入力すべき値を示すためのプレースホルダー属性がある場合があります。page.getByPlaceholder() を使用して、このような入力要素を特定できます。
例えば、次のDOM構造を考えてみましょう。
<input type="email" placeholder="name@example.com" />
プレースホルダーテキストで入力を特定した後、それを入力できます。
await page
.getByPlaceholder('name@example.com')
.fill('playwright@microsoft.com');
ラベルを持たず、プレースホルダーテキストを持つフォーム要素を特定する際にこのロケーターを使用します。
テキストによる特定
要素が含むテキストで要素を検索します。page.getByText() を使用すると、部分文字列、正確な文字列、または正規表現で一致させることができます。
例えば、次のDOM構造を考えてみましょう。
<span>Welcome, John</span>
要素が含むテキストで要素を特定できます。
await expect(page.getByText('Welcome, John')).toBeVisible();
正確な一致を設定
await expect(page.getByText('Welcome, John', { exact: true })).toBeVisible();
正規表現で一致
await expect(page.getByText(/welcome, [A-Za-z]+$/i)).toBeVisible();
テキストによるマッチングは、厳密な一致であっても常に空白を正規化します。例えば、複数のスペースを1つにまとめ、改行をスペースに変換し、先頭と末尾の空白を無視します。
div
、span
、p
などの非対話型要素を見つけるにはテキストロケーターを使用することをお勧めします。button
、a
、input
などの対話型要素には、ロールロケーターを使用してください。
リスト内の特定のアイテムを見つけようとする場合に便利なテキストによるフィルターも可能です。
altテキストによる特定
すべての画像には画像を説明する alt
属性が必要です。page.getByAltText() を使用して、代替テキストに基づいて画像を特定できます。
例えば、次のDOM構造を考えてみましょう。
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />
代替テキストで画像を特定した後、それをクリックできます。
await page.getByAltText('playwright logo').click();
img
や area
要素など、altテキストをサポートする要素にこのロケーターを使用します。
タイトルによる特定
page.getByTitle() を使用して、一致するタイトル属性を持つ要素を特定します。
例えば、次のDOM構造を考えてみましょう。
<span title='Issues count'>25 issues</span>
タイトルテキストで特定した後、問題の数をチェックできます。
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
要素に title
属性がある場合にこのロケーターを使用します。
テストIDによる特定
テストIDによるテストは、テキストや属性のロールが変更されてもテストがパスするため、最も回復力のあるテスト方法です。QAと開発者は、明示的なテストIDを定義し、page.getByTestId() でそれらをクエリすべきです。ただし、テストIDによるテストはユーザーには見えません。ロールやテキスト値が重要である場合は、ロールやテキストロケーターなどのユーザーに見えるロケーターの使用を検討してください。
例えば、次のDOM構造を考えてみましょう。
<button data-testid="directions">Itinéraire</button>
テストIDで要素を特定できます。
await page.getByTestId('directions').click();
カスタムテストID属性の設定
デフォルトでは、page.getByTestId() は data-testid
属性に基づいて要素を特定しますが、テスト設定で設定するか、selectors.setTestIdAttribute() を呼び出すことで設定できます。
テストにカスタムデータ属性を使用するようにテストIDを設定します。
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
testIdAttribute: 'data-pw'
}
});
これでHTMLでは、デフォルトの data-testid
の代わりに data-pw
をテストIDとして使用できます。
<button data-pw="directions">Itinéraire</button>
そして、通常通り要素を特定します。
await page.getByTestId('directions').click();
CSSまたはXPathによる特定
CSSまたはXPathロケーターをどうしても使用する必要がある場合は、page.locator() を使用して、ページ内の要素を検索する方法を記述するセレクターを受け取るロケーターを作成できます。PlaywrightはCSSとXPathセレクターをサポートしており、css=
または xpath=
プレフィックスを省略すると自動検出します。
await page.locator('css=button').click();
await page.locator('xpath=//button').click();
await page.locator('button').click();
await page.locator('//button').click();
XPathとCSSセレクターは、DOM構造や実装に結びついている場合があります。これらのセレクターは、DOM構造が変更されると壊れる可能性があります。以下の長いCSSまたはXPathチェーンは、不安定なテストにつながる**悪い習慣**の例です。
await page.locator(
'#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input'
).click();
await page
.locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input')
.click();
CSSやXPathはDOMが頻繁に変わる可能性があり、テストが不安定になるため推奨されません。代わりに、ロールロケーターなどのユーザーがページを認識する方法に近いロケーターを考案するか、テストIDを使用して明示的なテスト契約を定義するようにしてください。
Shadow DOM内の特定
Playwrightのすべてのロケーターは**デフォルトで**Shadow DOM内の要素でも機能します。例外は以下の通りです。
- XPathによる特定はシャドウルーツを貫通しません。
- クローズドモードのシャドウルーツはサポートされていません。
カスタムWebコンポーネントの次の例を考えてみましょう。
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>
シャドウルートが存在しないかのように、同じ方法で特定できます。
<div>詳細</div>
をクリックするには
await page.getByText('Details').click();
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>
<x-details>
をクリックするには
await page.locator('x-details', { hasText: 'Details' }).click();
<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>
<x-details>
がテキスト「詳細」を含んでいることを確認するには
await expect(page.locator('x-details')).toContainText('Details');
ロケーターのフィルタリング
2番目の商品カードの購入ボタンをクリックしたい次のDOM構造を考えてみましょう。正しいロケーターを取得するためにロケーターをフィルタリングするためのいくつかのオプションがあります。
商品1
商品2
<ul>
<li>
<h3>Product 1</h3>
<button>Add to cart</button>
</li>
<li>
<h3>Product 2</h3>
<button>Add to cart</button>
</li>
</ul>
テキストでフィルター
ロケーターは locator.filter() メソッドでテキストによってフィルタリングできます。これは、要素のどこかに、おそらく子孫要素に、大文字と小文字を区別せずに特定の文字列を検索します。正規表現を渡すこともできます。
await page
.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button', { name: 'Add to cart' })
.click();
正規表現を使用する
await page
.getByRole('listitem')
.filter({ hasText: /Product 2/ })
.getByRole('button', { name: 'Add to cart' })
.click();
テキストを持たない要素でフィルター
あるいは、テキストを**持たない**要素でフィルタリングすることもできます。
// 5 in-stock items
await expect(page.getByRole('listitem').filter({ hasNotText: 'Out of stock' })).toHaveCount(5);
子/子孫でフィルター
ロケーターは、別のロケーターに一致する子孫を持つ要素、または持たない要素のみを選択するオプションをサポートしています。そのため、locator.getByRole()、locator.getByTestId()、locator.getByText() など、他のロケーターでフィルタリングできます。
商品1
商品2
<ul>
<li>
<h3>Product 1</h3>
<button>Add to cart</button>
</li>
<li>
<h3>Product 2</h3>
<button>Add to cart</button>
</li>
</ul>
await page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
.getByRole('button', { name: 'Add to cart' })
.click();
また、商品カードをアサートして、1つだけであることを確認することもできます。
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) }))
.toHaveCount(1);
フィルタリングするロケーターは、元のロケーターに対して**相対的である必要があり**、ドキュメントルートではなく、元のロケーターのマッチングからクエリされます。したがって、フィルタリングロケーターが元のロケーターによってマッチした <li>
リストアイテムの外にある <ul>
リスト要素からマッチングを開始するため、次のものは機能しません。
// ✖ WRONG
await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('list').getByText('Product 2') }))
.toHaveCount(1);
子/子孫を持たない要素でフィルター
内部に一致する要素を**持たない**要素でフィルタリングすることもできます。
await expect(page
.getByRole('listitem')
.filter({ hasNot: page.getByText('Product 2') }))
.toHaveCount(1);
内部ロケーターは、ドキュメントルートからではなく、外部ロケーターから照合されることに注意してください。
ロケーター演算子
ロケーター内部でのマッチング
page.getByText() や locator.getByRole() のようにロケーターを作成するメソッドをチェーンすることで、ページの特定の部分に検索を絞り込むことができます。
この例では、まず listitem
の役割を特定して product というロケーターを作成します。次に、テキストでフィルターします。product ロケーターを再度使用して、ボタンの役割を取得してクリックし、アサーションを使用して「商品2」というテキストを持つ製品が1つだけであることを確認できます。
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
await product.getByRole('button', { name: 'Add to cart' }).click();
await expect(product).toHaveCount(1);
また、2つのロケーターを連結して、たとえば特定のダイアログ内の「保存」ボタンを見つけることもできます。
const saveButton = page.getByRole('button', { name: 'Save' });
// ...
const dialog = page.getByTestId('settings-dialog');
await dialog.locator(saveButton).click();
2つのロケーターを同時にマッチング
メソッド locator.and() は、追加のロケーターをマッチングすることで、既存のロケーターを絞り込みます。例えば、page.getByRole() と page.getByTitle() を組み合わせて、役割とタイトルの両方でマッチングすることができます。
const button = page.getByRole('button').and(page.getByTitle('Subscribe'));
2つの代替ロケーターのいずれかにマッチング
2つ以上の要素のいずれかをターゲットにしたいが、どちらになるかわからない場合は、locator.or() を使用して、いずれかまたは両方の代替要素に一致するロケーターを作成します。
例えば、「新しいメール」ボタンをクリックしたいが、セキュリティ設定ダイアログが代わりに表示される場合があるシナリオを考えてみましょう。この場合、「新しいメール」ボタンまたはダイアログのいずれかを待ち、それに応じて行動することができます。
「新しいメール」ボタンとセキュリティダイアログの両方が画面に表示された場合、「or」ロケーターは両方に一致し、場合によっては「厳密モード違反」エラーをスローします。この場合、locator.first() を使用して、いずれか1つのみに一致させることができます。
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog).first()).toBeVisible();
if (await dialog.isVisible())
await page.getByRole('button', { name: 'Dismiss' }).click();
await newEmail.click();
表示されている要素のみにマッチング
通常、可視性をチェックするよりも、要素を一意に識別するより信頼性の高い方法を見つける方が良いでしょう。
ページに2つのボタンがあり、1つは非表示で、もう1つは表示されているとします。
<button style='display: none'>Invisible</button>
<button>Visible</button>
-
これは両方のボタンを見つけ、厳密性違反エラーをスローします。
await page.locator('button').click();
-
これは、表示されている2番目のボタンのみを見つけてクリックします。
await page.locator('button').filter({ visible: true }).click();
リスト
リスト内のアイテムの数を数える
リスト内のアイテムの数を数えるためにロケーターをアサートできます。
例えば、次のDOM構造を考えてみましょう。
- リンゴ
- バナナ
- オレンジ
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
カウントアサーションを使用して、リストに3つのアイテムがあることを確認します。
await expect(page.getByRole('listitem')).toHaveCount(3);
リスト内のすべてのテキストをアサートする
リスト内のすべてのテキストを見つけるためにロケーターをアサートできます。
例えば、次のDOM構造を考えてみましょう。
- リンゴ
- バナナ
- オレンジ
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
expect(locator).toHaveText() を使用して、リストに「apple」、「banana」、「orange」というテキストがあることを確認します。
await expect(page
.getByRole('listitem'))
.toHaveText(['apple', 'banana', 'orange']);
特定のアイテムを取得
リスト内の特定のアイテムを取得する方法はいくつかあります。
テキストで取得
page.getByText() メソッドを使用して、リスト内の要素をそのテキストコンテンツで特定し、それをクリックします。
例えば、次のDOM構造を考えてみましょう。
- リンゴ
- バナナ
- オレンジ
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
アイテムをテキストコンテンツで特定し、クリックします。
await page.getByText('orange').click();
テキストでフィルター
locator.filter() を使用して、リスト内の特定のアイテムを特定します。
例えば、次のDOM構造を考えてみましょう。
- リンゴ
- バナナ
- オレンジ
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>
「listitem」の役割を持つアイテムを特定し、「orange」のテキストでフィルターし、クリックします。
await page
.getByRole('listitem')
.filter({ hasText: 'orange' })
.click();
テストIDで取得
page.getByTestId() メソッドを使用して、リスト内の要素を特定します。テストIDがまだない場合は、HTMLを修正してテストIDを追加する必要があるかもしれません。
例えば、次のDOM構造を考えてみましょう。
- リンゴ
- バナナ
- オレンジ
<ul>
<li data-testid='apple'>apple</li>
<li data-testid='banana'>banana</li>
<li data-testid='orange'>orange</li>
</ul>
「orange」のテストIDを持つアイテムを特定し、クリックします。
await page.getByTestId('orange').click();
n番目のアイテムで取得
同一の要素のリストがあり、それらを区別する唯一の方法が順序である場合、locator.first()、locator.last()、または locator.nth() を使用して、リストから特定の要素を選択できます。
const banana = await page.getByRole('listitem').nth(1);
ただし、この方法は注意して使用してください。ページが変更されると、ロケーターが予期しない完全に異なる要素を指す可能性があります。代わりに、厳密性基準を満たす一意のロケーターを作成するベストプラクティスに従ってください。
フィルタの連結
さまざまな類似性を持つ要素がある場合、locator.filter() メソッドを使用して正しい要素を選択できます。複数のフィルターを連結して、選択範囲を絞り込むこともできます。
例えば、次のDOM構造を考えてみましょう。
- ジョン
- メアリー
- ジョン
- メアリー
<ul>
<li>
<div>John</div>
<div><button>Say hello</button></div>
</li>
<li>
<div>Mary</div>
<div><button>Say hello</button></div>
</li>
<li>
<div>John</div>
<div><button>Say goodbye</button></div>
</li>
<li>
<div>Mary</div>
<div><button>Say goodbye</button></div>
</li>
</ul>
「Mary」と「Say goodbye」を含む行のスクリーンショットを撮るには
const rowLocator = page.getByRole('listitem');
await rowLocator
.filter({ hasText: 'Mary' })
.filter({ has: page.getByRole('button', { name: 'Say goodbye' }) })
.screenshot({ path: 'screenshot.png' });
これで、プロジェクトのルートディレクトリに「screenshot.png」ファイルがあるはずです。
稀な使用例
リスト内の各要素に対して何かをする
要素を反復処理する
for (const row of await page.getByRole('listitem').all())
console.log(await row.textContent());
通常のforループを使用して反復処理する
const rows = page.getByRole('listitem');
const count = await rows.count();
for (let i = 0; i < count; ++i)
console.log(await rows.nth(i).textContent());
ページ内での評価
locator.evaluateAll() 内のコードはページ内で実行され、そこで任意のDOM APIを呼び出すことができます。
const rows = page.getByRole('listitem');
const texts = await rows.evaluateAll(
list => list.map(element => element.textContent));
厳密性
ロケーターは厳密です。これは、特定のDOM要素を暗示するロケーターに対するすべての操作が、複数の要素が一致する場合に例外をスローすることを意味します。例えば、DOMに複数のボタンがある場合、次の呼び出しは例外をスローします。
複数の要素がある場合にエラーをスローする
await page.getByRole('button').click();
一方、Playwrightは複数の要素に対する操作を実行する場合を理解しているため、ロケーターが複数の要素に解決される場合でも、次の呼び出しは完全に機能します。
複数の要素でも問題なく動作する
await page.getByRole('button').count();
複数の要素が一致する場合にどの要素を使用するかを locator.first()、locator.last()、locator.nth() を介してPlaywrightに伝えることで、厳密性チェックを明示的に無効にすることができます。これらのメソッドは**推奨されません**。なぜなら、ページが変更された場合、Playwrightが意図しない要素をクリックする可能性があるためです。代わりに、上記のベストプラクティスに従って、ターゲット要素を一意に識別するロケーターを作成してください。
その他のロケーター
あまり一般的ではないロケーターについては、その他のロケーターガイドを参照してください。