先講一個比喻:你請助理去辦公室拿東西
自動化測試就是一份寫給助理的指令:到哪個畫面、找到哪個東西、對它做什麼。在 Playwright 裡,「怎麼描述要找的東西」叫做 locator。
大部分測試會壞,功能其實沒變,只是畫面搬了桌子。問題幾乎都出在你怎麼跟助理描述那個東西。
有兩種描述方式:
• 用位置描述:「進門左轉第二間,靠窗第三個抽屜,從上面數下來第四張紙。」
• 用東西本身描述:「拿那張標題寫著『請假單』的表格。」
第一種只要有人搬了桌子就找不到。第二種不管搬到哪都找得到。這篇文章要講的就是:為什麼大多數人寫的是第一種,以及怎麼改成第二種。
官方只有一條規則:用使用者看得到的東西找,不要用位置
Playwright 官方文件的建議很簡單:優先用「角色加名稱」找元素,因為那最接近使用者和螢幕閱讀器看到的畫面。退而求其次用標籤、文字;真的沒辦法才貼測試專用的暗記;CSS 和 XPath 是最後手段。
翻成白話,就是這張對照表:
| 優先 | 寫法 | 白話意思 | 什麼時候用 |
| 1 | getByRole(‘button’, { name: ‘送出’ }) | 「那個寫著『送出』的按鈕」 | 按鈕、連結、輸入框、勾選框、標題,幾乎所有可以互動的東西 |
| 2 | getByLabel(‘Email’) | 「標題是『Email』的欄位」 | 表單欄位 |
| 3 | getByPlaceholder(‘請輸入帳號’) | 「裡面有灰字提示『請輸入帳號』的欄位」 | 沒有標題只有提示字的欄位 |
| 4 | getByText(‘訂單已送出’) | 「寫著『訂單已送出』的那段字」 | 純文字,不是按鈕 |
| 5 | getByTestId(‘checkout-btn’) | 「底部貼了『checkout-btn』條碼的東西」 | 文字會變、或東西本身沒有名字 |
| 6 | locator(‘#root > div > button’) | 「進門左轉第二間……」 | 上面都不行才用,而且要說明原因 |
記住一個判斷方法:先問自己「使用者會怎麼跟同事描述這個東西?」他會說「按那個『送出』」,不會說「按第二個 div 裡的 button」。照使用者的說法寫,就是對的 locator。
九個最常見的問題,以及當下怎麼處理
每一個問題都分成三段:症狀長什麼樣、大家通常怎麼做(通常是錯的)、建議怎麼做。
1. 用位置找東西,畫面一改就全壞
症狀:測試裡滿是 #root > div:nth-child(2) > form > button 或從瀏覽器複製來的 XPath。前端改個外框、換個 UI 套件,一整批測試紅掉,功能卻沒壞。
常見做法:壞了就再去瀏覽器複製一次新路徑,下次改版再複製一次,無限循環。
建議做法:讓工具幫你選寫法。在終端機執行:
npx playwright codegen https://你的網站
會開一個瀏覽器,你用滑鼠點畫面,右側自動產生 locator,而且是照官方優先順序挑的。另一個方法是用 npx playwright test –ui 打開介面,按「Pick locator」滑到元素上看建議。這兩個動作比翻文件快,邊用邊學。
2. 描述太模糊,找到好幾個
症狀:錯誤訊息出現 strict mode violation。你說「按『刪除』」,但畫面上十列資料每列都有刪除鈕,Playwright 不敢亂按,直接停下來。
常見做法:補一句「按第三個」(.nth(2) 或 .first())。今天能跑,明天資料順序一換就刪錯人。
建議做法:三步驟。
- 看錯誤訊息。Playwright 會列出它找到的每一個元素,先看是哪些。
- 問自己「使用者會怎麼區分它們?」通常是靠同一列上的其他文字。
- 先框住那一列,再往下找:
page.getByRole(‘row’, { name: ‘張三’ }).getByRole(‘button’, { name: ‘刪除’ })
白話就是「找到張三那一列,按那一列的刪除」。只有「第一筆」本身就是需求的時候,才用 .first()。
3. 東西本身沒有名字
症狀:想用 getByRole 找按鈕,結果那個按鈕是一塊會點的色塊(<div onClick>),不是真的按鈕;輸入框沒有標題;純圖示的按鈕沒有文字。
常見做法:退回用位置找,問題回到第 1 點。
建議做法:這不是測試問題,是前端沒把 HTML 寫對。開一張票給前端,請他們做三件事:可點的用 <button>,輸入框加 <label>,純圖示按鈕加 aria-label。補完之後不只測試好寫,視障使用者的螢幕閱讀器也才唸得出來。等不及的話先貼 data-testid 當暫時方案,並在程式碼註明「待前端補語意後移除」。
4. 明明有名字,卻找不到
症狀:按鈕明明顯示「送出」,getByRole(‘button’, { name: ‘送出’ }) 卻找不到。原因通常是按鈕真正的名稱是別的(例如 aria-label=”submit-form”),或文字前後夾著空白和圖示。
常見做法:改用 getByText 碰運氣。
建議做法:把整頁的「無障礙樹」印出來看,每個元素叫什麼名字一目了然:
console.log(await page.locator(‘body’).ariaSnapshot())
名稱有空白或圖示混在一起,就改用「包含」比對:{ name: /送出/ }。
5. 文字會變
症狀:按鈕今天叫「送出」、明天改成「確認送出」;英文版叫「Submit」;數字會變(「共 3 筆」)。
常見做法:測試只跑一種語言,或在測試裡寫判斷式。
建議做法:這正是官方說適合用測試暗記的情境。互動元素改用 getByTestId(‘submit-btn’),數字用「包含」比對:getByText(/共 \d+ 筆/)。
6. 字對一半也算
症狀:找「登入」,結果「登入」和「重新登入」都被找到,又回到問題 2。
建議做法:加上「字要完全一樣」:getByText(‘登入’, { exact: true })。很多人不知道預設是部分比對,因為其他工具的預設相反。
7. 自動產生的編號
症狀:元素的 id 長得像 mui-123、class 長得像 css-1x2y3z,每次重新部署都變。
常見做法:用「開頭是 mui」之類的方式比對,更脆弱。
建議做法:這類編號像訪客證號碼,明天就失效。一律不用,改回角色、標籤,或請前端加 testid。
8. 東西在另一個房間
症狀:付款表單嵌在另一個網頁(iframe)裡,直接找找不到。
建議做法:先進房間再找:
page.frameLocator(‘iframe[title=”付款”]’).getByRole(‘button’, { name: ‘付款’ })
順帶一提,Shadow DOM 不用特別處理,Playwright 預設會穿透(只有 XPath 不行)。
9. 找不到就「等三秒再找」
症狀:測試時好時壞,有人加了 waitForTimeout(3000)。像助理找不到東西就站著發呆三秒再找一次,有時剛好成功。
常見做法:失敗就把三秒改成五秒。
建議做法:Playwright 本來就會自動等待,還是失敗通常是 locator 對到錯的元素,或元素被別的東西擋住。打開錄影紀錄看:
npx playwright test –trace on
npx playwright show-report
點進失敗的測試,每一步在等什麼、找到什麼、為什麼沒按下去都有記錄。九成情況會發現是描述錯了,不是時間不夠。
發表迴響