Skip to main content

定位器

簡介

定位器是 Playwright 自動等待和重試能力的核心部分。簡而言之,定位器代表在頁面上任何時刻尋找元素的方法。

快速指南

這些是建議的內建定位器。

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 結構。

http://localhost:3000
<button>Sign in</button>

透過名稱為 "Sign in" 的 button 角色定位元素。

await page.getByRole('button', { name: 'Sign in' }).click();
note

使用程式碼產生器產生定位器,然後依需要進行編輯。

每次使用定位器執行動作時,都會在頁面中定位到最新的 DOM 元素。在下面的程式碼片段中,底層的 DOM 元素會被定位兩次,每個動作之前各一次。這意味著如果 DOM 因為重新渲染而在呼叫之間發生改變,將會使用對應定位器的新元素。

const locator = page.getByRole('button', { name: 'Sign in' });

await locator.hover();
await locator.click();

請注意,所有建立定位器的方法,如 page.getByLabel(),也可在 LocatorFrameLocator 類別上使用,因此您可以鏈接它們並反覆縮小定位器範圍。

const locator = page
.frameLocator('#my-frame')
.getByRole('button', { name: 'Sign in' });

await locator.click();

透過角色定位

page.getByRole() 定位器反映使用者和輔助技術如何認知頁面,例如某個元素是按鈕還是核取方塊。透過角色定位時,通常也應該傳遞可存取名稱,以便定位器精確指向特定元素。

例如,考慮以下 DOM 結構。

http://localhost:3000

Sign up

<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 規範。請注意,許多 HTML 元素如 <button> 都有隱含定義的角色,可被角色定位器識別。

請注意,角色定位器不能取代無障礙稽核和符合性測試,而是提供關於 ARIA 指引的早期回饋。

何時使用角色定位器

我們建議優先使用角色定位器來定位元素,因為這是最接近使用者和輔助技術感知頁面的方式。

透過標籤定位

大多數表單控制項通常都有專用標籤,可以方便地用來與表單互動。在這種情況下,您可以使用 page.getByLabel() 透過其關聯標籤定位控制項。

例如,考慮以下 DOM 結構。

http://localhost:3000
<label>Password <input type="password" /></label>

您可以透過標籤文字定位輸入欄位後填入:

await page.getByLabel('Password').fill('secret');
何時使用標籤定位器

在定位表單欄位時使用此定位器。

透過預留位置定位

輸入欄位可能有預留位置屬性來提示使用者應該輸入什麼值。您可以使用 page.getByPlaceholder() 定位這類輸入欄位。

例如,考慮以下 DOM 結構。

http://localhost:3000
<input type="email" placeholder="name@example.com" />

您可以透過預留位置文字定位輸入欄位後填入:

await page
.getByPlaceholder('name@example.com')
.fill('playwright@microsoft.com');
何時使用預留位置定位器

在定位沒有標籤但有預留位置文字的表單元素時使用此定位器。

透過文字定位

透過元素包含的文字尋找元素。使用 page.getByText() 時,您可以透過子字串、完全字串或正規表示式進行配對。

例如,考慮以下 DOM 結構。

http://localhost:3000
Welcome, John
<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();
note

透過文字配對始終會正規化空白字元,即使在完全配對的情況下也是如此。例如,它會將多個空格轉換為一個,將換行符轉換為空格,並忽略前導和後置空白字元。

何時使用文字定位器

我們建議使用文字定位器來尋找非互動元素,如 divspanp 等。對於互動元素如 buttonainput 等,請使用角色定位器

您也可以透過文字篩選,這在嘗試在清單中尋找特定項目時很有用。

透過替代文字定位

所有圖片都應該有描述圖片的 alt 屬性。您可以使用 page.getByAltText() 根據文字替代內容定位圖片。

例如,考慮以下 DOM 結構。

http://localhost:3000
playwright logo
<img alt="playwright logo" src="/img/playwright-logo.svg" width="100" />

您可以透過文字替代內容定位圖片後點擊:

await page.getByAltText('playwright logo').click();
何時使用替代文字定位器

當您的元素支援替代文字(如 imgarea 元素)時使用此定位器。

透過標題定位

使用 page.getByTitle() 定位具有配對標題屬性的元素。

例如,考慮以下 DOM 結構。

http://localhost:3000
25 issues
<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 結構。

http://localhost:3000
<button data-testid="directions">Itinéraire</button>

您可以透過測試 ID 定位元素:

await page.getByTestId('directions').click();
何時使用測試 ID 定位器

當您選擇使用測試 ID 方法論或無法透過角色文字定位時,您也可以使用測試 ID。

設定自訂測試 ID 屬性

預設情況下,page.getByTestId() 將根據 data-testid 屬性定位元素,但您可以在測試組態中設定或透過呼叫 selectors.setTestIdAttribute() 進行設定。

設定測試 ID 為您的測試使用自訂資料屬性。

playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
use: {
testIdAttribute: 'data-pw'
}
});

在您的 HTML 中,您現在可以使用 data-pw 作為測試 ID,而不是預設的 data-testid

http://localhost:3000
<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 中的元素一起使用。例外情況如下:

考慮以下自訂網頁元件的範例:

<x-details role=button aria-expanded=true aria-controls=inner-details>
<div>Title</div>
#shadow-root
<div id=inner-details>Details</div>
</x-details>

您可以像 shadow root 不存在一樣進行定位。

要點擊 <div>Details</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> 包含文字 "Details":

await expect(page.locator('x-details')).toContainText('Details');

篩選定位器

考慮以下 DOM 結構,我們想要點擊第二個產品卡片的購買按鈕。我們有幾個選項來篩選定位器以取得正確的目標。

http://localhost:3000
  • Product 1

  • Product 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() 等。

http://localhost:3000
  • Product 1

  • Product 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();

我們也可以對產品卡片進行斷言,確保只有一個:

await expect(page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) }))
.toHaveCount(1);

篩選定位器必須相對於原始定位器,並從原始定位器匹配開始查詢,而不是從文件根目錄開始。因此,以下方式不會生效,因為篩選定位器從 <ul> 清單元素開始匹配,該元素在原始定位器匹配的 <li> 清單項目之外:

// ✖ 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 定位器取得按鈕角色並點擊它,然後使用斷言確保只有一個包含文字「Product 2」的產品。

const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });

await product.getByRole('button', { name: 'Add to cart' }).click();

await expect(product).toHaveCount(1);

您也可以將兩個定位器串連在一起,例如在特定對話框中尋找「儲存」按鈕:

const saveButton = page.getByRole('button', { name: 'Save' });
// ...
const dialog = page.getByTestId('settings-dialog');
await dialog.locator(saveButton).click();

同時匹配兩個定位器

locator.and() 方法透過匹配額外的定位器來縮小現有定位器的範圍。例如,您可以結合 page.getByRole()page.getByTitle() 同時依角色和標題匹配。

const button = page.getByRole('button').and(page.getByTitle('Subscribe'));

匹配兩個替代定位器之一

如果您想要鎖定兩個或更多元素中的一個,且不知道會是哪一個,請使用 locator.or() 建立符合任一個或兩個替代方案的定位器。

例如,考慮一個想要點擊「新電子郵件」按鈕的情境,但有時會出現安全性設定對話框。在這種情況下,您可以等待「新電子郵件」按鈕或對話框並相應地採取行動。

note

如果「新電子郵件」按鈕和安全性對話框都出現在螢幕上,「or」定位器會匹配兩者,可能拋出「嚴格模式違規」錯誤。在這種情況下,您可以使用 locator.first() 只匹配其中一個。

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();

只匹配可見元素

note

通常最好找到更可靠的方式來唯一識別元素,而不是檢查可見性。

考慮一個包含兩個按鈕的頁面,第一個不可見,第二個可見

<button style='display: none'>Invisible</button>
<button>Visible</button>
  • 這會找到兩個按鈕並拋出嚴格性違規錯誤:

    await page.locator('button').click();
  • 這只會找到第二個按鈕,因為它是可見的,然後點擊它。

    await page.locator('button').filter({ visible: true }).click();

清單(Lists)

計算清單中的項目數

您可以對定位器進行斷言來計算清單中的項目數量。

例如,請考慮以下 DOM 結構:

http://localhost:3000
  • apple
  • banana
  • orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

使用計數斷言確保清單有 3 個項目。

await expect(page.getByRole('listitem')).toHaveCount(3);

斷言清單中所有文字

您可以對定位器進行斷言來尋找清單中的所有文字。

例如,請考慮以下 DOM 結構:

http://localhost:3000
  • apple
  • banana
  • orange
<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 結構:

http://localhost:3000
  • apple
  • banana
  • orange
<ul>
<li>apple</li>
<li>banana</li>
<li>orange</li>
</ul>

透過文字內容定位項目並點擊它。

await page.getByText('orange').click();

依文字篩選

使用 locator.filter() 定位清單中的特定項目。

例如,請考慮以下 DOM 結構:

http://localhost:3000
  • apple
  • banana
  • orange
<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 結構:

http://localhost:3000
  • apple
  • banana
  • orange
<ul>
<li data-testid='apple'>apple</li>
<li data-testid='banana'>banana</li>
<li data-testid='orange'>orange</li>
</ul>

透過測試 ID "orange" 定位項目並點擊。

await page.getByTestId('orange').click();

透過序號取得第 N 個

如果您有一個相同元素的清單,而唯一能區別它們的方式是順序,您可以使用 locator.first()locator.last()locator.nth() 從清單中選擇特定元素。

const banana = await page.getByRole('listitem').nth(1);

然而,使用此方法時要小心。通常頁面可能會變更,定位器會指向與您預期完全不同的元素。相反地,嘗試想出通過嚴格性條件的唯一定位器。

串接多重篩選

當您有具有各種相似性的元素時,可以使用 locator.filter() 方法選擇正確的元素。您也可以鏈接多個篩選器以縮小選擇範圍。

例如,請考慮以下 DOM 結構:

http://localhost:3000
  • John
  • Mary
  • John
  • Mary
<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 可能會點擊您無意的元素。相反地,請遵循上述最佳實踐建立唯一識別目標元素的定位器。

更多定位器

對於較少使用的定位器,請查看其他定位器指南。