安安~我是ChiYu~
昨天我在搜尋按鈕外面多包一層元素,getByRole() 仍然順利找到它。這讓我留下另一個問題:
外層結構改了沒事,那按鈕名稱改了呢?
今天我真的只改四個字,把「搜尋活動」換成「探索場次」。活動資料沒變,submit handler
沒變,人類也照樣能按下按鈕,找到「WebMCP 入門工作坊」。同一支 Playwright 測試重跑後,
卻在 500ms 後丟出 timeout。
第一眼看到 locator.click: Timeout 500ms exceeded,很容易先怪網站太慢,或乾脆把等待時間
加長。實際上,Playwright 連按鈕都還沒找到。這個 timeout 發生在 click 以前,和搜尋結果
正不正確還沒有關係。
先把這次改動固定下來。程式差異里程碑是:
branch day-05-copy-change-failure
tag v3-day-05
commit 078fe7b
v3-day-05 保存按鈕改名與人類仍可操作的最小差異。讀者版測試放在tests/browser/reader-facing-labs.spec.ts,它會真的執行舊 Locator、保存TimeoutError,再用目前名稱完成搜尋。
改名版 Lab 的網址是:
http://127.0.0.1:5173/labs/day-04-playwright-locator/index.html?variant=renamed
實驗仍使用關鍵字 WebMCP、同一份活動資料與相同 submit handler。DOM 差異只有按鈕文字:
- <button type="submit" data-action="search-events">搜尋活動</button>
+ <button type="submit" data-action="search-events">探索場次</button>
type、data-action 和表單結構都沒動。我刻意把變因壓到只剩 visible name,這樣 timeout
出現時,才不會同時混進事件沒綁定、API 掛掉或活動資料被改壞等其他可能。
昨天的測試仍依舊名稱找按鈕:
page.getByRole("button", { name: "搜尋活動" })
頁面上現在只剩 accessible name 為「探索場次」的按鈕。人類看得懂兩句話在這裡要做同一件
事,Playwright 不會自己做這層推論;它只知道測試指定的名稱已經不存在。
為了不用每次都站在螢幕前陪它等,我把舊 Locator 的等待時間縮成 500ms:
await page
.getByRole("button", { name: "搜尋活動" })
.click({ timeout: 500 });
實際錯誤是:
locator.click: Timeout 500ms exceeded.
訊息裡雖然出現 click,Playwright 實際上還在找 role 是 button、accessible name 是
「搜尋活動」的元素。500ms 內找不到,它就停了。按鈕沒有被點擊,submit handler 當然也
還沒機會執行。
下面這張實測畫面把兩個結果放在一起:舊名稱產生 timeout,換成「探索場次」後,同一份搜尋
仍完成一場結果。

圖 1:目前 DOM 已是「探索場次」,舊 Locator 等待逾時;換成新名稱後,同一搜尋仍完成一場結果。
畫面最上方的「人類操作已完成:1 場」也很重要。它把責任範圍切開了:網站搜尋功能仍然
正常,失效的是測試用來辨認按鈕的線索。
大前天也出現過 timeout,但那次 Playwright 根本等不到正確網站啟動。兩個錯誤都寫 timeout,
發生位置完全不同:
| 類型 | 發生時機 | 先檢查什麼 |
|---|---|---|
Timed out waiting ... webServer |
測試案例開始前 | port、health URL、是否跑到別的專案 |
locator.click: Timeout 500ms exceeded |
案例已開啟頁面 | Locator 與目前 DOM/accessible name |
| 結果斷言失敗 | click 已完成之後 | submit handler、資料、API 與畫面更新 |
所以看到 timeout 時,我不會先調高秒數。我會先確認測試到底停在環境啟動、元素定位,還是
操作完成後的結果驗收。錯誤名字一樣,不代表修法可以共用。
這個 spec 沒有讓 timeout 直接炸掉整套測試,而是把它當成今天要觀察的結果:
const errorLine = await captureExpectedTimeout(
page.getByRole("button", { name: "搜尋活動" }),
"old-accessible-name-timeout.txt",
testInfo
);
await page.getByRole("button", { name: "探索場次" }).click();
await expect(page.getByRole("status"))
.toContainText("人類操作已完成:1 場");
最後輸出如下:
[預期失敗:accessible name] locator.click: Timeout 500ms exceeded.
[預期失敗:CSS selector] locator.click: Timeout 500ms exceeded.
4 passed
4 passed 不是「四題從頭到尾都沒出現錯誤」。它代表四個實驗都得到預期結果,其中兩個
實驗本來就要求舊 Locator 失敗,接著再用正確 Locator 完成復原。
如果文章只留下最後一行,讀者會以為整段一路綠燈;如果只截 timeout,又會看起來像測試
整組壞掉。兩段輸出要一起保留,才說得清楚這是一個受控失敗實驗。
把 timeout 從 500ms 調成 5 秒不會解決問題。頁面上沒有舊 accessible name,多等 4.5 秒
只是讓錯誤晚一點到。
我會照下面順序檢查:
若產品需求明訂按鈕必須叫「搜尋活動」,原測試失敗就是有用的 regression signal,應該修回
文案。若文案本來就可以調整,而測試要保護的是搜尋行為,則可以更新 accessible name,或
改用團隊約定的 [data-action="search-events"]。
我沒有因為一次 timeout 就宣布 CSS selector 或 data-testid 比較穩。Locator 怎麼選,取決於
我們要守住哪份契約。修完 Locator 後,「找到一場活動」的結果斷言也得留下;否則測試只會
證明按鈕按得下去,沒有證明搜尋真的完成。
今天的失敗沒有讓 Playwright 失去價值。搜尋表單能不能填、鍵盤能不能操作、報名送出與取消
dialog 有沒有守住人類確認,這些仍然適合交給 Browser Automation 做回歸測試。
這次實驗只說明一件事:工程師寫 UI 劇本時,會把 accessible name 或 DOM 結構一起寫進
測試契約。畫面線索改變,舊劇本就可能先停下來。
WebMCP 今天也還不能來領功。我們尚未定義任何 Tool contract,更沒有觀察瀏覽器 capability
或 Agent invocation,不能拿 Playwright 的 timeout 反過來宣稱 WebMCP 已經解決問題。
明天我會把 Browser Automation、REST API、MCP 與 WebMCP 放到同一張圖上。先看清楚它們
各自在呼叫誰、能讀到什麼狀態,再決定任務能力應該放在哪裡。否則新名詞學了一輪,最後只是
替原本的 API 和測試換了名牌。