Skip to main content

定位器

簡介

定位器 是 Playwright 自動等待與可重試能力的核心。簡而言之,定位器代表能在任意時刻於頁面上找到元素的方法。

快速指南

以下是建議優先使用的內建定位器:

await Page.GetByLabel("User Name").FillAsync("John");

await Page.GetByLabel("Password").FillAsync("secret-password");

await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();

await Expect(Page.GetByText("Welcome, John!")).ToBeVisibleAsync();

定位元素

Playwright 提供多種內建定位器。為了讓測試更具韌性,建議優先使用面向使用者的屬性與明確契約,例如 Page.GetByRole()

例如,考慮以下 DOM 結構:

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

透過名稱為「Sign in」的 button 角色來定位該元素:

await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
note

使用程式碼產生器來產生定位器,然後依需求編輯。

每次使用定位器執行動作時,都會在頁面上定位到最新的 DOM 元素。以下範例中,底層 DOM 會定位兩次:每個動作前各一次。這代表如果在兩次呼叫之間因重新渲染導致 DOM 發生變化,定位器會對應到新的元素並予以使用。

var locator = Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" });

await locator.HoverAsync();
await locator.ClickAsync();

請注意,所有建立定位器的方法,例如 Page.GetByLabel(),同樣也能在 LocatorFrameLocator 類別上使用,因此你可以串接它們以逐步縮小定位範圍。

var locator = Page
.FrameLocator("#my-frame")
.GetByRole(AriaRole.Button, new() { Name = "Sign in" });

await locator.ClickAsync();

透過角色定位

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(AriaRole.Heading, new() { Name = "Sign up" }))
.ToBeVisibleAsync();

await Page
.GetByRole(AriaRole.Checkbox, new() { Name = "Subscribe" })
.CheckAsync();

await Page
.GetByRole(AriaRole.Button, new() {
NameRegex = new Regex("submit", RegexOptions.IgnoreCase)
})
.ClickAsync();

角色定位器涵蓋按鈕、核取方塊、標題、連結、清單、表格等,並遵循 W3C 針對 ARIA 角色ARIA 屬性可存取名稱的規範。請注意,許多 HTML 元素如 <button> 具有隱含角色定義,可被角色定位器辨識。

同時要注意,角色定位器並非用來取代無障礙稽核與相容性測試,而是能提供遵循 ARIA 準則的早期回饋。

何時使用角色定位器

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

透過標籤定位

多數表單控制項通常有對應的標籤,可方便地與表單互動。此時可使用 Page.GetByLabel() 透過關聯標籤文字來定位控制項。

例如,考慮以下 DOM 結構:

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

你可以先透過標籤文字定位輸入框,然後填值:

await Page.GetByLabel("Password").FillAsync("secret");
何時使用標籤定位器

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

透過 placeholder 定位

輸入框可能帶有 placeholder 屬性,提示使用者應輸入的值。你可以使用 Page.GetByPlaceholder() 來定位此類輸入框。

例如,考慮以下 DOM 結構:

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

你可以先透過 placeholder 文字定位輸入框,然後填值:

await Page
.GetByPlaceholder("name@example.com")
.FillAsync("playwright@microsoft.com");
何時使用 placeholder 定位器

當定位沒有標籤但有 placeholder 文字的表單元素時使用本定位器。

透過文字定位

依元素所含文字來尋找元素。使用 Page.GetByText() 時,可以以子字串、完全相符或正規表示式進行比對。

例如,考慮以下 DOM 結構:

http://localhost:3000
Welcome, John
<span>Welcome, John</span>

你可以以其包含的文字來定位元素:

await Expect(Page.GetByText("Welcome, John")).ToBeVisibleAsync();

設定完全相符:

await Expect(Page
.GetByText("Welcome, John", new() { Exact = true }))
.ToBeVisibleAsync();

使用正規表示式比對:

await Expect(Page
.GetByText(new Regex("welcome, john", RegexOptions.IgnoreCase)))
.ToBeVisibleAsync();
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").ClickAsync();
何時使用替代文字定位器

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

透過標題定位

使用 Page.GetByTitle() 透過 title 屬性定位元素。

例如,考慮以下 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").ClickAsync();
何時使用測試 ID 定位器

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

設定自訂測試 ID 屬性

預設情況下,Page.GetByTestId() 會以 data-testid 屬性來定位元素,但你可以在測試設定中或透過呼叫 Selectors.SetTestIdAttribute() 來自訂。

設定以自訂的資料屬性作為測試 ID:

playwright.Selectors.SetTestIdAttribute("data-pw");

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

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

接著便可像平常一樣定位該元素:

await Page.GetByTestId("directions").ClickAsync();

使用 CSS 或 XPath 定位

若你確實必須使用 CSS 或 XPath 定位器,可以使用 Page.Locator() 建立接受選擇器的定位器,描述如何在頁面中尋找元素。Playwright 支援 CSS 與 XPath 選擇器;若省略 css=xpath= 前綴,會自動偵測選擇器類型。

await Page.Locator("css=button").ClickAsync();
await Page.Locator("xpath=//button").ClickAsync();

await Page.Locator("button").ClickAsync();
await Page.Locator("//button").ClickAsync();

XPath 與 CSS 選擇器常與 DOM 結構或實作緊密耦合,當 DOM 結構改變時,這些選擇器可能失效。下面這類冗長的 CSS 或 XPath 鏈結就是導致測試不穩定的「不良實務」:

await Page.Locator("#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input").ClickAsync();

await Page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync();
何時使用此作法

不建議使用 CSS 與 XPath,因為 DOM 經常變更會導致測試不具韌性。請嘗試設計更貼近使用者感知的定位器,例如角色定位器,或使用測試 ID 定義明確的測試契約

在 Shadow DOM 中定位

Playwright 中的定位器預設即可作用於 Shadow DOM 中的元素。以下為例外情況:

考慮下列自訂 Web Component 範例:

<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").ClickAsync();
<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", new() { HasText = "Details" })
.ClickAsync();
<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")).ToContainTextAsync("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(AriaRole.Listitem)
.Filter(new() { HasText = "Product 2" })
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();

使用正規表示式:

await page
.GetByRole(AriaRole.Listitem)
.Filter(new() { HasTextRegex = new Regex("Product 2") })
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();

依「沒有某段文字」篩選

或是依「不包含」某段文字來篩選:

// 5 in-stock items
await Expect(Page.GetByRole(AriaRole.Listitem).Filter(new() { HasNotText = "Out of stock" }))
.ToHaveCountAsync(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(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.Heading, new() {
Name = "Product 2"
})
})
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();

我們也能針對產品卡做斷言以確保只有一個:

await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
}))
.ToHaveCountAsync(1);

篩選用的定位器「必須」相對於原始定位器,並從原始定位器的配對開始查詢,而非從文件根節點開始。因此以下範例不會生效,因為篩選定位器從 <ul> 清單元素開始配對,該元素在原先 <li> 清單項目之外:

// ✖ WRONG
await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
Has = page.GetByRole(AriaRole.List).GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
}))
.ToHaveCountAsync(1);

依「沒有子/後代元素」篩選

也可以依「沒有」內部匹配元素來篩選:

await Expect(Page
.GetByRole(AriaRole.Listitem)
.Filter(new() {
HasNot = page.GetByRole(AriaRole.Heading, new() { Name = "Product 2" })
}))
.ToHaveCountAsync(1);

請注意,內部定位器是從外部定位器開始配對,而非從文件根節點。

定位器運算元

在定位器內部比對

你可以串接會建立定位器的方法,例如 Page.GetByText()Locator.GetByRole(),以將搜尋範圍縮小到頁面的特定部分。

以下範例中,我們先以 listitem 角色建立名為 product 的定位器,然後依文字篩選。之後再用這個 product 定位器取得按鈕角色並點擊,最後以斷言確保只剩一個包含文字「Product 2」的產品:

var product = page
.GetByRole(AriaRole.Listitem)
.Filter(new() { HasText = "Product 2" });

await product
.GetByRole(AriaRole.Button, new() { Name = "Add to cart" })
.ClickAsync();

你也可以將兩個定位器串接,例如在特定對話框中尋找「Save」按鈕:

var saveButton = page.GetByRole(AriaRole.Button, new() { Name = "Save" });
// ...
var dialog = page.GetByTestId("settings-dialog");
await dialog.Locator(saveButton).ClickAsync();

同時比對兩個定位器

Locator.And() 會藉由再匹配一個定位器來縮小既有定位器的範圍。例如可以結合 Page.GetByRole()Page.GetByTitle(),同時依角色與標題比對。

var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe"));

在多個候選定位器中擇一比對

若你要鎖定兩個或以上元素其中之一,但不確定會是哪一個,可用 Locator.Or() 建立可比對任一(或兩者)替代方案的定位器。

例如,想點擊「New email」按鈕,但有時會出現安全性設定對話框。此時可以等待「New email」按鈕或對話框,並依情況採取行動。

note

若「New email」按鈕與安全性對話框同時出現,「or」定位器會同時比對兩者,可能會拋出嚴格模式違規錯誤。此時可改用 Locator.First 只比對其中一個。

var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" });
var dialog = page.GetByText("Confirm security settings");
await Expect(newEmail.Or(dialog).First).ToBeVisibleAsync();
if (await dialog.IsVisibleAsync())
await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).ClickAsync();
await newEmail.ClickAsync();

只比對可見元素

note

通常更好的做法是找出更可靠且能唯一識別該元素的方式,而不是檢查可見性。

考慮一個具有兩個按鈕的頁面:第一個不可見,第二個為可見

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

    await page.Locator("button").ClickAsync();
  • 以下只會找到第二個(因為它是可見的),然後點擊:

    await page.Locator("button").Filter(new() { Visible = true }).ClickAsync();

清單(Lists)

計算清單中的項目數

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

例如,考慮以下 DOM 結構:

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

使用數量斷言確認清單有 3 個項目:

await Expect(Page.GetByRole(AriaRole.Listitem)).ToHaveCountAsync(3);

斷言清單中所有文字

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

例如,考慮以下 DOM 結構:

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

使用 Expect(Locator).ToHaveTextAsync() 確保清單包含「apple」、「banana」與「orange」:

await Expect(Page
.GetByRole(AriaRole.Listitem))
.ToHaveTextAsync(new string[] {"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").ClickAsync();

依文字篩選

使用 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(AriaRole.Listitem)
.Filter(new() { HasText = "orange" })
.ClickAsync();

透過測試 ID 取得

使用 Page.GetByTestId() 來定位清單中的元素。若你尚未有測試 ID,可能需要修改 HTML 來加入。

例如,考慮以下 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").ClickAsync();

透過序號取得第 N 個

若你有一份相同元素的清單,而且唯一可區分的方式是順序,可以用 Locator.FirstLocator.LastLocator.Nth() 選取特定元素。

var banana = page.GetByRole(AriaRole.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」的那一列螢幕截圖:

var rowLocator = page.GetByRole(AriaRole.Listitem);

await rowLocator
.Filter(new() { HasText = "Mary" })
.Filter(new() {
Has = page.GetByRole(AriaRole.Button, new() { Name = "Say goodbye" })
})
.ScreenshotAsync(new() { Path = "screenshot.png" });

你應該會在專案根目錄中看到一個 "screenshot.png" 檔案。

罕見使用情境

對清單中每個元素做些事情

遍歷元素:

foreach (var row in await page.GetByRole(AriaRole.Listitem).AllAsync())
Console.WriteLine(await row.TextContentAsync());

使用一般 for 迴圈遍歷:

var rows = page.GetByRole(AriaRole.Listitem);
var count = await rows.CountAsync();
for (int i = 0; i < count; ++i)
Console.WriteLine(await rows.Nth(i).TextContentAsync());

在頁面中執行評估

Locator.EvaluateAllAsync() 內的程式碼會在頁面中執行,你可以在那裡呼叫任何 DOM API。

var rows = page.GetByRole(AriaRole.Listitem);
var texts = await rows.EvaluateAllAsync(
"list => list.map(element => element.textContent)");

嚴格性(Strictness)

定位器具備嚴格性。也就是說,所有需要對應到目標 DOM 元素的定位器操作,若有多個元素符合就會拋出例外。例如,若 DOM 中有數個按鈕,下列呼叫會拋出例外:

當符合多於一個元素時會拋出錯誤

await page.GetByRole(AriaRole.Button).ClickAsync();

另一方面,Playwright 能理解你執行的是多元素操作,因此當定位器解析為多個元素時,以下呼叫完全沒有問題:

可在多元素情況下正常運作

await page.GetByRole(AriaRole.Button).CountAsync();

你可以明確地跳過嚴格性檢查,透過 Locator.FirstLocator.LastLocator.Nth() 指示 Playwright 在多個元素符合時要使用哪一個。這些方法「不建議」使用,因為當頁面變更時,Playwright 可能會點擊非預期元素。建議遵循上方最佳實務,建立能唯一識別目標元素的定位器。

更多定位器

較少使用的定位器請參考其它定位器指南。