前端開發 別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站 · 第 4 / 30 篇

Day 04|按鈕只多包一層,哪種 Playwright Locator 撐得住?

2026 iThome 鐵人賽 WebMCP Day 4:安安~我是ChiYu~ 昨天我把活動網站從搜尋一路走到取消報名,最後挑了風險最低的搜尋流程,準備交給 Playwright 重播。今天它正式接班:開啟頁面、填入 WebMCP、按下搜尋,再確認畫面出現 一場活動。 第一輪很順,四行程式碼就完成了。接著…

快速導覽 查看本文目錄

本文同步自 2026 iThome 鐵人賽原文 。完整系列可見 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 場");
});

它依序完成四件事:

  1. 開啟固定的 Lab。
  2. 依 <label> 找到關鍵字欄位,填入 WebMCP。
  3. 依 button role 與 accessible name 找到「搜尋活動」。
  4. 等待 status 顯示一場搜尋結果。

這四行也把腳本依賴的契約寫了出來:網址要正確、欄位要有可辨認的 label、按鈕要有符合的 role 與名稱,最後還要有可驗收的畫面結果。少了任何一項,Playwright 都不會像人類一樣 「看起來差不多就按下去」。

Playwright 官方 Locator 文件 建議優先使用面向使用者的 屬性與明確契約。getByLabel() 適合表單欄位;getByRole() 則依 ARIA role、相關屬性與 accessible name 尋找元素。這比記住畫面座標,更接近使用者與輔助技術理解介面的方式。

accessible name 不一定只來自肉眼看到的文字。瀏覽器可能依元素內容、aria-label、 aria-labelledby 或關聯的 <label> 計算名稱。所以 DOM 裡有一顆 button 還不夠, 它的 role 和計算後名稱都要符合 Locator。

第一次執行後,測試找到「搜尋活動」按鈕,也等到一場結果出現在 status:

Playwright 依 accessible name 成功完成搜尋

圖 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 過期」拆成兩件事來看。

讀完了!你覺得這篇如何?

你的回饋會決定下一篇要寫什麼。

還有疑問

Learning path

別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站

這篇是系列中的第 4 / 30 篇。可以順著列表補完整脈絡,也可以直接跳到需要的段落。

  1. 01 Day 01|網站真的會被 Agent 使用嗎?我先丟了一句自然語言
  2. 02 Day 02|換到全新資料夾後,網站還能不能跑?
  3. 03 Day 03|先別急著做 Tool,我把活動網站自己走了一遍
  4. 04 Day 04|按鈕只多包一層,哪種 Playwright Locator 撐得住? 目前閱讀
  5. 05 Day 05|只改按鈕四個字,Playwright 為什麼停在 click 以前?
  6. 06 Day 06|Playwright、REST API、MCP 與 WebMCP,各自負責哪一段?
  7. 07 Day 07|Agent 偶爾答對還不夠,我先出二十道測試題
  8. 08 Day 08|WebMCP Tool 該怎麼設計?把「點按鈕」改成「搜尋活動」
  9. 09 Day 09|網站該公開哪些 WebMCP Tool?從操作盤點到五支正式能力
  10. 10 Day 10|Inspector 裝好了,為什麼 Tool 面板還是一片空白?
  11. 11 Day 11|什麼是 Declarative API?讓 HTML 表單成為 WebMCP Tool
  12. 12 Day 12|Tool 註冊完還不算完成:離開 route 後誰把它收回來?
  13. 13 Day 13|第一次按下 Inspector 的 Send,先被 Gemini 429 擋在門外
  14. 14 Day 14|五支 Tool 不再往外長:把產品範圍寫成會亮紅燈的契約
  15. 15 Day 15|讓人類與 Agent 共用同一套活動搜尋規則
  16. 16 Day 16|Agent 怎麼知道「我現在看的活動」是哪一場?
  17. 17 Day 17|Tool 回太少做不完,回太多又把內部資料倒給 Agent
  18. 18 Day 18|活動服務暫時失敗時,Agent 應該重試還是停下來?
  19. 19 Day 19|同一場活動收藏兩次,網站為什麼只能留下同一筆?
  20. 20 Day 20|表單填好了,為什麼這次報名仍然不能算通過?
  21. 21 Day 21|一句「這個我不要了」,Agent 到底敢不敢直接取消?
  22. 22 Day 22|五支 Tool 如何共用同一套規則,串成三條完整流程?
  23. 23 Day 23|在問 Agent 聰不聰明前,先證明網站沒有壞
  24. 24 Day 24|活動摘要叫 Agent 洩漏祕密,它會把這句話當資料還是命令?
  25. 25 Day 25|localhost 成功不算交付:怎麼證明公開網址跑的是同一份程式?
  26. 26 Day 26|把自然語言送進公開站,Agent 真的會自己選對 Tool 嗎?
  27. 27 Day 27|寫入型 Tool 怎麼驗收?把 Trace、Network、UI 與 Server 狀態放在一起看
  28. 28 Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層?
  29. 29 Day 29|把網站交給 Agent 前,我最後會檢查哪十件事?
  30. 30 Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼?

Further reading

Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼? 封面圖 2026-09-30 Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼? Day 29|把網站交給 Agent 前,我最後會檢查哪十件事? 封面圖 2026-09-29 Day 29|把網站交給 Agent 前,我最後會檢查哪十件事? Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層? 封面圖 2026-09-28 Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層?

留言討論

用 GitHub 帳號留言,留言會同步到這個 Blog 的 GitHub Discussions。

閱讀導覽

約 5 分鐘

本文目錄

本篇所在系列 · 第 4 / 30 篇

別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站

  1. Day 01|網站真的會被 Agent 使用嗎?我先丟了一句自然語言
  2. Day 02|換到全新資料夾後,網站還能不能跑?
  3. Day 03|先別急著做 Tool,我把活動網站自己走了一遍
  4. Day 04|按鈕只多包一層,哪種 Playwright Locator 撐得住?
  5. Day 05|只改按鈕四個字,Playwright 為什麼停在 click 以前?
  6. Day 06|Playwright、REST API、MCP 與 WebMCP,各自負責哪一段?
  7. Day 07|Agent 偶爾答對還不夠,我先出二十道測試題
  8. Day 08|WebMCP Tool 該怎麼設計?把「點按鈕」改成「搜尋活動」
  9. Day 09|網站該公開哪些 WebMCP Tool?從操作盤點到五支正式能力
  10. Day 10|Inspector 裝好了,為什麼 Tool 面板還是一片空白?
  11. Day 11|什麼是 Declarative API?讓 HTML 表單成為 WebMCP Tool
  12. Day 12|Tool 註冊完還不算完成:離開 route 後誰把它收回來?
  13. Day 13|第一次按下 Inspector 的 Send,先被 Gemini 429 擋在門外
  14. Day 14|五支 Tool 不再往外長:把產品範圍寫成會亮紅燈的契約
  15. Day 15|讓人類與 Agent 共用同一套活動搜尋規則
  16. Day 16|Agent 怎麼知道「我現在看的活動」是哪一場?
  17. Day 17|Tool 回太少做不完,回太多又把內部資料倒給 Agent
  18. Day 18|活動服務暫時失敗時,Agent 應該重試還是停下來?
  19. Day 19|同一場活動收藏兩次,網站為什麼只能留下同一筆?
  20. Day 20|表單填好了,為什麼這次報名仍然不能算通過?
  21. Day 21|一句「這個我不要了」,Agent 到底敢不敢直接取消?
  22. Day 22|五支 Tool 如何共用同一套規則,串成三條完整流程?
  23. Day 23|在問 Agent 聰不聰明前,先證明網站沒有壞
  24. Day 24|活動摘要叫 Agent 洩漏祕密,它會把這句話當資料還是命令?
  25. Day 25|localhost 成功不算交付:怎麼證明公開網址跑的是同一份程式?
  26. Day 26|把自然語言送進公開站,Agent 真的會自己選對 Tool 嗎?
  27. Day 27|寫入型 Tool 怎麼驗收?把 Trace、Network、UI 與 Server 狀態放在一起看
  28. Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層?
  29. Day 29|把網站交給 Agent 前,我最後會檢查哪十件事?
  30. Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼?
看完整系列 30 篇