本文同步自 2026 iThome 鐵人賽原文 ,發布日期為 2026-08-06。完整系列可見 iThome 系列頁 。
安安~我是ChiYu~
昨天我把活動網站從搜尋一路走到取消報名,最後挑了風險最低的搜尋流程,準備交給
Playwright 重播。今天它正式接班:開啟頁面、填入 WebMCP、按下搜尋,再確認畫面出現
一場活動。
第一輪很順,四行程式碼就完成了。接著我只在搜尋按鈕外面多包一層元素,畫面看起來一樣, 人類也照樣能按。這時兩種 Locator 卻走向不同結果:依 role 與 accessible name 找按鈕的 測試繼續通過,寫死父子結構的 CSS selector 則直接找不到目標。
這個改動小到很容易被 code review 略過,卻剛好適合回答今天的問題:Playwright 到底是 怎麼「看見」頁面上的按鈕?
本系列使用的 Locator Lab 保留在:
/labs/day-04-playwright-locator/index.html
用四行 Playwright 重播昨天的活動搜尋
我先把昨天的手動操作改寫成下面這支測試:
test("the original accessible-name locator performs the search", async ({ page }) => {
await page.goto("/labs/day-04-playwright-locator/index.html");
await page.getByLabel("關鍵字").fill("WebMCP");
await page.getByRole("button", { name: "搜尋活動" }).click();
await expect(page.getByRole("status"))
.toContainText("人類操作已完成:1 場");
});
它的操作順序很單純:
- 開啟固定的 Lab。
- 依
<label>找到關鍵字欄位,填入WebMCP。 - 依
buttonrole 與 accessible name 找到「搜尋活動」。 - 等待 status 顯示一場搜尋結果。
這四行也把腳本依賴的條件寫了出來。網址要正確、欄位要有可辨認的 label、按鈕要有符合的 role 與名稱,最後還要有一個可驗收的畫面結果。少了任何一項,Playwright 都不會像人類一樣 「看起來差不多就按下去」。
Playwright 官方 Locator 文件
建議優先使用面向
使用者的屬性與明確契約。getByLabel() 適合表單欄位;getByRole() 則會依 ARIA role、
相關屬性與 accessible name 尋找元素。這比記住畫面座標,更接近使用者與輔助技術理解
介面的方式。
accessible name 不一定只來自肉眼看到的文字。瀏覽器可能依元素內容、aria-label、
aria-labelledby 或關聯的 <label> 計算名稱。所以 DOM 裡有一顆 button 還不夠,
它的 role 和計算後名稱都要符合 Locator。
第一次執行後,測試找到「搜尋活動」按鈕,也等到一場結果出現在 status:

圖 1:Locator、目前 DOM 與可見結果放在同一張實測畫面中。這是 E2 browser test,不是 Agent trace。
按鈕多包一層後,role Locator 通過、CSS selector 失敗
第一輪綠燈後,我開啟另一個變體:
/labs/day-04-playwright-locator/index.html?variant=wrapped
這個版本只在按鈕外新增 .action-shell。按鈕還是按鈕,accessible name 也仍然是「搜尋活動」,
所以這行照常找得到:
page.getByRole("button", { name: "搜尋活動" })
但直接指定父子關係的 #search-form > button 會失敗。原本按鈕是 form 的直接子元素,多包
一層後,這個 DOM 關係已經不存在了。
人類從頭到尾沒感覺到差異,測試卻因為「怎麼找按鈕」而得到不同答案。這不是哪種寫法永遠 比較高級,而是兩支 Locator 在保護不同契約:role Locator 保護使用者能感知的語意,CSS selector 保護指定的 DOM 結構。
| Locator | 它依賴的線索 | 介面改動後可能發生什麼 |
|---|---|---|
getByRole('button', { name }) | role 與 accessible name | 改文案會失敗,外層包裝通常不影響 |
getByLabel('關鍵字') | label 與表單控制項的關聯 | 移除 label 或改名稱會失敗 |
#search-form > button | 固定的父子 DOM 結構 | 多包一層元素就失敗 |
[data-action='search-events'] | 團隊定義的穩定屬性 | UI 可以改,但團隊要維護這份契約 |
這次我保留 getByRole(),因為讀者版測試真正要守住的是「使用者仍能找到並操作搜尋按鈕」。
如果某支測試的目的就是限制 DOM 階層,那麼 CSS selector 才是合理選擇。Locator 不能只挑
最短的,要先問這支測試究竟想保護哪件事。
Playwright 的 Locator 也不是在測試開始時抓住同一個節點不放。每次操作時,它都會重新查詢 當下 DOM,並搭配 auto-wait 與 retry。這能處理正常的非同步 render,卻不會替工程師猜測 「探索場次」是不是等同原本的「搜尋活動」。那是明天要故意留下的另一個坑。
重跑 Locator Lab,同時保存成功與預期失敗
要重播這個實驗,先啟動專案:
npm run dev
接著開啟原始 Lab,手動搜尋一次:
http://127.0.0.1:5173/labs/day-04-playwright-locator/index.html
最後執行 focused spec:
npx playwright test tests/browser/reader-facing-labs.spec.ts --grep "Day 4" --reporter=line
這一輪得到 4 passed。前兩題驗證原始 accessible-name Locator 與新增 DOM wrapper 後的結果; 後兩題會刻意使用錯誤 Locator,捕捉 timeout,再換回正確 Locator 完成復原。換句話說,整份 spec 是綠燈,不代表過程中從未失敗,而是預期中的失敗也有被測試接住。
我也沒有把驗收停在「click() 沒丟例外」。最後一行會等待 role="status" 顯示
「人類操作已完成:1 場」。如果按鈕存在,submit handler 卻沒有接上,點擊動作可能照樣完成,
結果斷言仍會把問題攔下來。
想逐步看瀏覽器怎麼操作,可以加上 debug 模式:
npx playwright test tests/browser/reader-facing-labs.spec.ts --grep "Day 4" --debug --workers=1
Playwright 也能保存 trace,讓我們在失敗後回頭看 action、DOM snapshot 與錯誤位置。 Trace Viewer 官方文件 提供以下開啟方式:
npx playwright show-trace <trace.zip>
一張畫面只能告訴我「當時長這樣」,trace 則能繼續回答:用了哪個 Locator、頁面上有哪些 節點、等待花了多久,以及錯誤發生在操作前還是操作後。明天按鈕改名後,這些資訊會用來判斷 搜尋功能真的壞了,還是舊名稱讓腳本找不到入口。
Playwright 能重播 UI 劇本,還不會自己理解任務
回頭看今天的四行測試,Playwright 很忠實地完成了工程師指定的步驟。DOM 多包一層時,
getByRole() 仍能依使用者可感知的語意找到按鈕,這也確認了我選擇這支 Locator 的理由。
但它沒有自己理解「幫我找活動」。網址、欄位、按鈕名稱與成功條件,全部是我事先寫進劇本的。 今天證明的是 UI 流程可以在瀏覽器裡重播,也就是 E2 browser test;它還不是 Agent 看見任務後 自行選擇 Tool 的證據。
明天我會把按鈕的「搜尋活動」改成「探索場次」,資料與 submit handler 都不碰。人類仍然 知道該按哪裡,今天這支依舊名稱找按鈕的腳本卻會停在 click 以前。到時候我們就能把「網站 壞掉」和「Locator 過期」拆成兩件事來看。