昨天我用表單找出「台北、免費、入門」的活動。今天我把相同流程改寫成
Playwright,原本幾秒鐘完成的動作,立刻被拆成網址、欄位、按鈕與成功條件。
人類看到表單就知道該填哪裡;Playwright 不會自己理解「幫我找活動」這句話。
工程師必須先替它寫好每一步。它擅長的是忠實重播劇本,而這正好讓我看清楚 Browser
Automation 到底依賴哪些畫面線索。
本系列使用的 Locator Lab 保留在:
/labs/day-04-playwright-locator/index.html
目前的成功案例是:
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 場");
});
它做了四件很具體的事:
<label> 找到輸入欄,填入 WebMCP。button role 與 accessible name 找到「搜尋活動」。Playwright 官方 Locator 文件建議優先使用
面向使用者的屬性與明確契約。getByRole() 會依 ARIA role、屬性與 accessible
name 找元素;getByLabel() 適合表單欄位。這比用畫面座標點擊更接近使用者與
輔助技術理解頁面的方式。
accessible name 不一定等於肉眼看到的文字。瀏覽器會依元素內容、aria-label、aria-labelledby 與關聯 <label> 計算名稱。這也是為什麼「DOM 裡確實有一顆
button」仍不足以讓 getByRole() 成功:role 要符合,計算後的名稱也要符合。

圖 1:Locator、目前 DOM 與可見結果放在同一張實測畫面中。這是 E2 browser test,不是 Agent trace。
getByRole("button", { name: "搜尋活動" }) 同時寫下兩個契約:元素仍是按鈕,
而且使用者感知到的名稱仍是「搜尋活動」。產品若要求這段文案不可改,測試對名稱
敏感很合理。
Lab 還有另一個變體:
/labs/day-04-playwright-locator/index.html?variant=wrapped
它只在按鈕外面新增 .action-shell。role 與 accessible name 沒變,原本的getByRole() 仍然成功;直接子元素 selector #search-form > button 卻會找不到。
Locator 的選擇,決定測試要保護哪一層。CSS selector 保護 DOM 關係,role locator
保護使用者可感知的語意。該選哪一個,要看這支測試想保護什麼,不能只比長短。
| Locator | 依賴的線索 | 介面改動後可能發生什麼 |
|---|---|---|
getByRole('button', { name }) |
role 與 accessible name | 改文案會失敗,外層包裝通常不影響 |
getByLabel('關鍵字') |
label 與表單控制項關聯 | 移除 label 或改名稱會失敗 |
#search-form > button |
固定父子 DOM 結構 | 多包一層元素就失敗 |
[data-action='search-events'] |
團隊定義的穩定屬性 | UI 可改,但團隊必須維護這份契約 |
Playwright 不是在測試開始時找到元素後就永遠抓著同一個節點。Locator 每次操作會
重新查詢當下 DOM,也有 auto-wait 與 retry。這能處理正常的非同步 render,卻不能
替工程師猜出「探索場次」是否等同原本的「搜尋活動」。
啟動專案:
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;
後兩題會在案例內刻意捕捉 timeout,再以正確 Locator 完成復原,因此整份 spec
仍是綠燈。
成功斷言也值得看。測試沒有停在「click 沒丟例外」,而是等待 role="status" 顯示
「人類操作已完成:1 場」。若按鈕存在卻沒有接上 submit handler,click 仍可能
成功,結果斷言會把這個問題攔下來。操作與可見結果必須一起驗收。
若要逐步看瀏覽器操作,可加上:
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、
頁面上有哪些節點、等待花了多久,以及錯誤發生在操作前還是操作後。Day 5 會用同一
種資訊判斷「網站壞掉」和「舊名稱找不到」的差別。
Browser Automation 沒有過時,也不會被 WebMCP 趕走。它負責驗證真實 UI 是否
仍能被人使用;WebMCP 則讓網站另外描述任務能力。前者知道工程師寫好的操作順序,
後者仍需要 Agent 判斷何時選擇哪個 Tool。
接著我會故意只改一個地方:把按鈕從「搜尋活動」換成「探索場次」。功能、資料與
submit handler 全部不動,再看今天這支成功腳本會在哪裡停下來。