Playwright Locators: 動的な要素への対応とベストプラクティス【日本語】

2025-05-31

Playwrightにおける Locator (ロケーター) とは、ウェブページ上の特定の要素(ボタン、テキスト、入力フィールド、画像など)を一意に特定し、操作するための強力な仕組みのことです。

簡単に言うと、「どの要素に対して操作を行いたいのか」をPlaywrightに伝えるための目印 のようなものです。

従来のSeleniumなどの自動化ツールでは、要素を特定するために IDclassXPathCSSセレクター などを用いることが一般的でしたが、PlaywrightのLocatorはこれらの概念をさらに発展させ、より柔軟で信頼性の高い要素特定を可能にします。

Locatorの重要な特徴

  • 推奨される要素特定戦略
    Playwrightのドキュメントでは、IDname属性、テキストコンテンツ、ARIAロールなどを優先的に使用することが推奨されており、XPathやCSSセレクターに頼りすぎないように促しています。これにより、より安定したテストを記述できます。
  • 強力なフィルタリングとチェイニング
    Locatorに対して、さらに条件を追加して絞り込んだり、複数のLocatorを組み合わせてより複雑な要素特定を行うことができます。例えば、「特定のテキストを含むボタン」や「ある要素の直下にある入力フィールド」などを簡潔に記述できます。
  • 自動的な待機 (Auto-waiting)
    Locatorを使って要素を操作しようとした際、Playwrightは自動的に要素が利用可能になるまで(例えば、ページが完全にロードされるまで、要素がDOMに現れるまで)一定時間待機します。これにより、明示的な待機処理を減らし、コードをより簡潔に保つことができます。
  • 遅延評価 (Lazy Evaluation)
    Locatorを作成した時点では、まだ実際の要素を探しに行きません。要素に対する操作(クリック、テキスト入力など)が実際に行われるまで、要素の検索は遅延されます。これにより、ページの動的な変化に対応しやすくなります。

Locatorの作成方法 (JavaScriptの例)

Playwrightでは、page.locator() メソッドを使用してLocatorを作成します。このメソッドには、要素を特定するための様々な引数を渡すことができます。

// IDで要素を特定
const myButton = page.locator('#submit-button');

// テキストコンテンツで要素を特定
const linkWithText = page.locator('a', { hasText: '詳細はこちら' });

// CSSセレクターで要素を特定
const specialInput = page.locator('.form-control[type="email"]');

// ARIAロールと名前で要素を特定
const dialog = page.locator('dialog[role="alertdialog"][name="エラー"]');

// 複数のLocatorを組み合わせて特定 (例: 特定のリスト内の最初の項目)
const firstItem = page.locator('#my-list > li').first();

Locatorを使用して要素を操作する例

作成したLocatorオブジェクトを使って、要素に対して様々な操作を行うことができます。

// ボタンをクリック
await myButton.click();

// テキストを入力
await page.locator('#username').type('testuser');

// 要素のテキストを取得
const pageTitle = await page.locator('h1').textContent();

// 要素が存在するかどうかを確認
const isVisible = await page.locator('#notification').isVisible();


よくあるエラー

    • 原因
      指定したLocatorに一致する要素が、指定されたタイムアウト時間内に期待される状態(表示、非表示、有効、無効、安定、DOMへのアタッチ、デタッチ)にならなかった場合に発生します。

    • await page.locator('#non-existent-button').click(); // 存在しないIDを指定
      await page.locator('.loading-spinner').waitFor({ state: 'hidden', timeout: 500 }); // 短すぎるタイムアウト
      
  1. Error: locator.click: Element "[セレクター]" is not attached to the DOM (要素がDOMにアタッチされていないエラー)

    • 原因
      Locatorが見つけた要素が、操作を実行しようとした時点でDOMから削除されている場合に発生します。SPA (Single Page Application) などで、要素が動的にDOMに追加・削除される場合に起こりやすいです。

    • const element = page.locator('#temporary-element');
      await element.waitFor(); // 要素が現れるのを待つ
      // 何らかの操作で要素がDOMから削除される
      await element.click(); // エラー発生
      
  2. Error: locator.click: Element "[セレクター]" is not visible (要素が非表示のエラー)

    • 原因
      Locatorが見つけた要素がCSSなどで非表示になっている(display: none;visibility: hidden; など)場合に、クリックなどの操作を実行しようとすると発生します。

    • await page.locator('.hidden-button').click(); // CSSで非表示になっているボタン
      
  3. Error: locator.click: Element "[セレクター]" is disabled (要素が無効のエラー)

    • 原因
      Locatorが見つけた要素が disabled 属性を持っているなど、操作が許可されていない状態の場合に発生します。

    • <button id="disabled-button" disabled>送信</button>
      
      await page.locator('#disabled-button').click(); // disabled属性を持つボタン
      
  4. Error: locator.textContent: Error: No element is visible for locator "[セレクター]" (要素が見つからないエラー)

    • 原因
      textContent() などのメソッドを呼び出した際に、指定したLocatorに一致する要素がページ内に見つからない場合に発生します。

    • const text = await page.locator('.non-existent-class').textContent(); // 存在しないクラスを指定
      
  5. 誤ったセレクター

    • 原因
      CSSセレクターやXPathの記述が間違っており、意図した要素を特定できていない。

    • await page.locator('div p.wrong-class').click(); // 実際には <div class="container"><span class="correct-class"></span></div> のような構造
      

トラブルシューティング

  1. Playwright Inspectorの活用

    • Playwrightには、実行中のブラウザで要素をインタラクティブに選択し、対応するLocatorを生成してくれる非常に強力なツール「Playwright Inspector」があります。これを利用して、正しいLocatorが記述できているか確認しましょう。
    • テスト実行時に --debug フラグを付けるか、コード内で await page.pause() を呼び出すことでInspectorを起動できます。
  2. セレクターの検証

    • ブラウザの開発者ツール (Elementsタブ) を開き、Consoleタブで $() (CSSセレクターの場合) や $x() (XPathの場合) を使用して、記述したセレクターが意図した要素を正しく一つだけ選択しているか確認します。
    • 複数の要素が一致する場合は、より具体的なセレクターになるように修正します。
  3. waitFor() メソッドの活用

    • 要素が動的に現れたり、状態が変化するのを待つ必要がある場合は、locator.waitFor({ state: 'visible' })locator.waitFor({ state: 'hidden' }) などを適切に使用します。
    • タイムアウト時間を調整する必要がある場合もありますが、安易に長くしすぎるとテスト全体の実行時間が長くなるため注意が必要です。
  4. isVisible()、isHidden()、isEnabled() などの状態確認

    • 要素が存在するかどうかだけでなく、表示状態や有効/無効の状態を確認してから操作を行うようにすると、エラーを回避できる場合があります。
    •   const buttonLocator = page.locator('#my-button');
        if (await buttonLocator.isVisible()) {
            await buttonLocator.click();
        }
      
  5. より堅牢なLocator戦略

    • IDname 属性は一意であることが多いため、可能な限りこれらを優先的に使用します。
    • テキストコンテンツ (hasText) や ARIAロール (rolename) を利用することも、変更に強く安定したLocatorを作成する上で有効です。
    • XPathや複雑なCSSセレクターは、DOM構造が変更された場合に影響を受けやすいため、できるだけ避けるようにします。
  6. 親要素からの絞り込み

    • 特定の要素のコンテキスト内で目的の要素を探すことで、より正確な特定が可能になる場合があります。
    •   const container = page.locator('.my-container');
        const specificInput = container.locator('input[type="text"]');
      
  7. エラーメッセージの確認

    • 発生したエラーメッセージを注意深く読み、何が問題なのかを理解することが重要です。メッセージには、どのセレクターで問題が発生したか、どのような状態を期待していたかなどの情報が含まれています。
  8. テストの再実行とデバッグ

    • 一時的なネットワークの問題などでエラーが発生することもあるため、何度かテストを再実行してみるのも有効です。
    • await page.pause() を利用してテストの実行を一時停止し、ブラウザの状態を直接確認しながらデバッグを行うことも非常に有効です。


基本的なLocatorの作成と操作

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');

  // ID属性で要素を特定
  const headingLocator = page.locator('#heading');
  const headingText = await headingLocator.textContent();
  console.log(`見出しのテキスト: ${headingText}`);

  // CSSクラスで要素を特定
  const linkLocator = page.locator('.link');
  const linkCount = await linkLocator.count();
  console.log(`リンクの数: ${linkCount}`);

  // テキストコンテンツで要素を特定
  const specificLinkLocator = page.locator('a', { hasText: 'More information...' });
  await specificLinkLocator.click();
  await page.waitForLoadState('domcontentloaded');
  console.log('「More information...」リンクをクリックしました。');

  await browser.close();
})();

説明

  • locator.click(): Locatorに一致する要素をクリックします。
  • locator.count(): Locatorに一致する要素の数を取得します。
  • locator.textContent(): Locatorに一致する要素のテキストコンテンツを取得します。複数の要素に一致する場合は、最初に見つかった要素のテキストを取得します。
  • page.locator('.link'): CSSクラスが link の要素すべてを特定するLocatorを作成します。
  • page.locator('#heading'): IDが heading の要素を特定するLocatorを作成します。

要素の状態に基づいた操作

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');

  // ボタンが有効になるまで待機してからクリック
  const enabledButtonLocator = page.locator('#enabled-button');
  await enabledButtonLocator.isEnabled();
  await enabledButtonLocator.click();
  console.log('有効なボタンをクリックしました。');

  // 特定のテキストを含む要素が表示されるまで待機
  const dynamicTextLocator = page.locator('.dynamic-text', { hasText: '読み込み完了' });
  await dynamicTextLocator.waitFor({ state: 'visible', timeout: 5000 });
  console.log('動的なテキストが表示されました。');

  await browser.close();
})();

説明

  • locator.waitFor({ state: 'visible', timeout: 5000 }): Locatorに一致する要素がタイムアウト(5000ミリ秒)以内に表示されるまで待機します。他の状態 (hidden, attached, detached, stable) も指定できます。
  • locator.isEnabled(): Locatorに一致する要素が有効かどうかをチェックします。これはPromiseを返すため、await を使用する必要があります。

要素の絞り込み (フィルタリングとチェイニング)

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com/users'); // 架空のURL

  // 特定のクラスを持つリスト内の最初の要素を特定
  const firstSpecialItemLocator = page.locator('.user-list > li.special').first();
  const firstSpecialItemText = await firstSpecialItemLocator.textContent();
  console.log(`最初の特別なアイテム: ${firstSpecialItemText}`);

  // 特定のテキストを含むボタンの親要素にある入力フィールドを特定
  const inputNearButtonLocator = page.locator('button', { hasText: '送信' }).locator('../input[type="text"]');
  await inputNearButtonLocator.type('入力されたテキスト');
  console.log('ボタンの近くの入力フィールドにテキストを入力しました。');

  await browser.close();
})();

説明

  • locator('button', { hasText: '送信' }).locator('../input[type="text"]'): まずテキスト「送信」を持つボタンを特定し、その親要素 (..) の中にある type 属性が text<input> 要素を特定します (Locatorのチェイニング)。
  • locator('.user-list > li.special').first(): CSSセレクターで .user-list 内の .special クラスを持つ <li> 要素すべてを特定し、その最初の要素を取得します。

getByRole() を使用したアクセシビリティに基づいた特定

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');

  // ARIAロールが "button" で、名前が "Submit" の要素を特定
  const submitButtonLocator = page.getByRole('button', { name: 'Submit' });
  await submitButtonLocator.click();
  console.log('「Submit」ボタンをクリックしました。');

  // ARIAロールが "textbox" で、プレースホルダーが "Email" の要素を特定
  const emailInputLocator = page.getByRole('textbox', { placeholder: 'Email' });
  await emailInputLocator.type('[email protected]');
  console.log('メールアドレスを入力しました。');

  await browser.close();
})();
  • page.getByRole('textbox', { placeholder: 'Email' }): ARIAロールが textbox であり、プレースホルダーが Email である要素を特定します。
  • page.getByRole('button', { name: 'Submit' }): ARIA (Accessible Rich Internet Applications) のロールが button であり、アクセス可能な名前 (通常はボタンのテキスト) が Submit である要素を特定します。アクセシビリティを考慮した、より堅牢な特定方法です。


page.$() および page.$$() (CSS/XPathセレクターの直接利用)

これらのメソッドは、従来のSeleniumなどと同様に、CSSセレクターまたはXPathを直接使用して要素を特定します。

  • page.$$(selector)
    指定されたCSSまたはXPathセレクターに一致するすべての要素の ElementHandle の配列を返します。一致する要素がない場合は空の配列を返します。
  • page.$(selector)
    指定されたCSSまたはXPathセレクターに一致する最初の要素の ElementHandle を返します。要素が見つからない場合は null を返します。

例 (CSSセレクター)

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');

  const headingElement = await page.$('#heading');
  if (headingElement) {
    const headingText = await headingElement.textContent();
    console.log(`見出しのテキスト (page.$): ${headingText}`);
  }

  const linkElements = await page.$$('.link');
  console.log(`リンクの数 (page.$$): ${linkElements.length}`);

  await browser.close();
})();

例 (XPathセレクター)

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');

  const paragraphElement = await page.$('xpath=//p');
  if (paragraphElement) {
    const paragraphText = await paragraphElement.textContent();
    console.log(`最初の段落のテキスト (XPath): ${paragraphText}`);
  }

  const allLinks = await page.$$('xpath=//a');
  console.log(`リンクの数 (XPath): ${allLinks.length}`);

  await browser.close();
})();

注意点

  • Playwrightの推奨する要素特定戦略からはやや外れるため、getByRole() やテキストベースのLocatorなど、よりセマンティックで安定した方法を優先することが推奨されます。
  • 要素の状態 (表示、有効など) を確認したり、動的な要素を扱う場合は、明示的に waitForSelector() などのメソッドを使用する必要があります。
  • page.$() および page.$$() は、Locatorのような自動的な待機や遅延評価の機能を提供しません。要素が存在しない場合、すぐに null または空の配列を返します。

page.locator(selector, options) の柔軟な利用

page.locator() は、基本的なセレクター文字列だけでなく、より複雑なオプションオブジェクトを受け付けることで、多様な要素特定を可能にします。

  • hasNot オプション
    特定のLocatorに一致する子孫要素を持たない要素を特定します。
  • has オプション
    特定のLocatorに一致する子孫要素を持つ要素を特定します。
  • hasText オプション
    特定のテキストコンテンツを持つ要素を特定します (既に説明済み)。

例 (has オプション)

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com/products'); // 架空のURL

  // 特定の画像を持つ商品カードを特定
  const productCardWithImage = page.locator('.product-card', {
    has: page.locator('img[src="/images/special.png"]')
  });
  const productName = await productCardWithImage.locator('.product-name').textContent();
  console.log(`特別な画像を持つ商品: ${productName}`);

  await browser.close();
})();

例 (hasText との組み合わせ)

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com/forms'); // 架空のURL

  // 「送信」ボタンの近くにあるエラーメッセージを特定
  const errorMessageNearSubmit = page.locator('button', { hasText: '送信' }).locator('../.error-message');
  if (await errorMessageNearSubmit.isVisible()) {
    const messageText = await errorMessageNearSubmit.textContent();
    console.log(`送信ボタン近くのエラー: ${messageText}`);
  }

  await browser.close();
})();

ElementHandle を直接操作する (高度なケース)

特定の状況下では、page.$()page.$$() で取得した ElementHandle を直接操作することがあります。例えば、複雑なDOM構造をプログラム的に解析したり、特定の要素の子要素を反復処理したりする場合などです。

例 (子要素の反復処理)

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com/list'); // 架空のURL

  const listItemHandles = await page.$$('.item');
  for (const itemHandle of listItemHandles) {
    const itemText = await itemHandle.textContent();
    console.log(`リストアイテム: ${itemText}`);
  }

  await browser.close();
})();

注意点

  • DOM構造が変更されると、ElementHandle が指す要素が無効になる可能性があります。
  • ElementHandle は特定のDOM要素への直接的な参照であり、Locatorのような柔軟性や自動的な待機機能はありません。

Playwrightでは、page.locator() を中心とした強力な要素特定メカニズムが推奨されますが、従来のCSS/XPathセレクターを直接利用する page.$() および page.$$()、より高度なフィルタリングオプションを持つ page.locator() の利用、そして ElementHandle の直接操作といった代替方法も存在します。