自動化測試最常壞掉的地方,不是流程, 而是「找不到元素」。這篇談 Locator,也就是定位:程式要怎麼描述「畫面上的那個東西」。你不需要會寫定位,但要會判斷 AI 選的定位方式好不好,因為它決定了測試三個月後還活不活著。
定位是什麼?先用指路來想
想像你請同事幫忙:「幫我把那份文件拿給穿紅色外套的那位」。你用的是對方的特徵。另一種指法:「拿給從門口數來第三排、左邊數來第二個位子的人」。你用的是位置。
兩種都指得到人。但如果那個人換了位子呢?第一種照樣找得到,第二種就拿錯人了。
程式定位畫面元素,也是這兩派。一派用元素的「意義」來找:一顆叫「登入」的按鈕、標籤寫著「Email」的輸入框。另一派用元素在網頁內部結構裡的「位置」來找:某層某格的第幾個。網頁改版就像有人換位子,用意義找的活下來,用位置找的常常陣亡。

圖 1:定位方式的優先順序,越上面越穩定
一個一個看:
• getByRole:用「角色 + 名稱」找。角色是指按鈕、連結、輸入框這類身分,例如「叫『登入』的按鈕」。這最接近使用者看畫面的方式,所以放第一。
• getByLabel、getByPlaceholder:用欄位的標籤文字或框內的提示文字找輸入框。填表單的場景首選。
• getByText:用畫面上的文字找。常用,但要小心同一段文字出現在多處的情況。
• getByTestId:用開發者特別埋的「測試專用記號」找。很穩,但需要開發者配合,等下細講。
• CSS / XPath 選擇器:用網頁內部結構找,就是「第三排左二」那種指法。最後手段。
判斷原則一句話:這個定位方式,像不像使用者描述畫面的方式?像,就會穩定;不像,改版就危險。
光看文字不好想像,放一個畫面對照。同一個會員登入頁,每種元素各有最順手的定位法:
圖 2:同一個畫面,不同元素對應的定位方法
照著圖,一個一個講:
• 搜尋框:框裡那行灰灰的「搜尋商品…」叫提示文字(placeholder),還沒輸入前它就是這個框最明顯的身分。所以用 getByPlaceholder('搜尋商品') 找它。
• 「會員登入」標題:標題在網頁裡有「標題」這個正式角色,加上名稱就唯一了:getByRole('heading', { name: '會員登入' })。
• Email 輸入框:輸入框自己沒有字,但它上面的標籤「Email」就是它的名字。getByLabel('Email') 找的就是「標籤叫 Email 的那個輸入框」,填表單的首選。
• 登入按鈕:角色是按鈕、名稱是登入,getByRole('button', { name: '登入' })。這是全部方法裡最穩的一招,因為「一顆叫登入的按鈕」這個描述,改版幾乎動不了它。
• 「忘記密碼?」連結:它就是一段看得到的字,用 getByText('忘記密碼') 直接找。要更嚴謹,也可以用連結角色:getByRole('link', { name: '忘記密碼' })。
• 訂單摘要卡片:麻煩的來了——內容是動態產生的(這個月 3 筆、下個月 5 筆),沒有一段穩定的字可以認,也沒有明確的角色。這種元素前五招都使不上力,要用下一節的辦法。
下次要請 AI 操作某個元素,先在心裡問:它在這張圖裡像哪一個?答案就是描述它的方式。
為什麼「用意義找」活得久
圖 3:UI 改版時,語意化定位存活,結構式定位陣亡
開發者改版時,改的多半是外觀和內部結構:顏色、位置、包了幾層、內部的 class 名稱。但「登入按鈕」這個身分幾乎不會變,除非登入功能整個被拿掉。
所以用「叫『登入』的按鈕」寫的定位,改版後大機率照常運作;綁在內部結構上的定位,一次改版就找不到東西。
實際的程式長相對照一下,有感一點。同一顆登入按鈕,兩種寫法:
// 用意義找:改版後還活著
page.getByRole('button', { name: '登入' })
// 用結構找:class 名稱一改就陣亡
page.locator('.btn.btn-primary > span.txt-login')
讀 AI 產出的測試時,掃一眼定位那幾行:大多是 getByRole、getByLabel、getByText 開頭,心可以放一半;滿眼都是 .class > div 這種符號,就該追問了。
這對維護成本的影響很大。一套用結構式定位寫的測試,每次改版都要大修,很多自動化專案就是這樣被拖垮的;語意化定位的測試,只有功能真的變了才要動,而功能變了本來就該改測試。
常見狀況:畫面上有兩個同名的東西
實務上很快會遇到這個:畫面上有兩顆「刪除」按鈕,或同一個字出現在標題和按鈕上。這時 Playwright 會直接報錯,告訴你「找到不只一個符合的元素」,而不是隨便挑一個。
這個設計是好意:隨便挑一個,測試可能點錯東西還照樣通過,那比報錯危險多了。
遇到時的處理方式,還是描述:把「哪一個」講清楚。「訂單列表裡第一筆的刪除按鈕」「彈窗裡的那顆確定,不是頁面底下那顆」。AI 聽得懂這種描述,會用範圍限定的方式寫定位:先找到那筆訂單,再在裡面找刪除鈕。跟你指路時說「靠窗那桌的服務鈴」是同一個邏輯。
跟 AI 怎麼配合
好消息:Claude Code 產出的定位,多半已經是語意化的。你的工作是抽查。產出測試後可以問:
你可以這樣對 Claude Code 說:
列出這個測試用到的所有 locator,說明每一個為什麼這樣選。如果有用到 CSS 選擇器或 XPath,解釋為什麼不能用 getByRole,並評估改版時壞掉的風險。
如果它回答「這個元素沒有明確的角色也沒有穩定文字,只能用 CSS」,先別失望,這個回答本身有價值:它代表你們產品的這個畫面,連螢幕報讀軟體都找不到路,無障礙也有問題。拿去跟開發者聊,通常補一個角色標記或測試記號就解決,順手改善了兩件事。
getByTestId:跟開發者的約定
剛才的訂單卡片示範了一種困境:元素的內容一直在變、又沒有明確角色,前五招都認不了它。這種情況的正解,是請開發者在網頁原始碼裡,替這個元素加一個專門給測試用的記號:data-testid 屬性。
打個比方:一間房子重新油漆、改裝、連招牌都換了,郵差還是找得到它,因為門牌號碼沒變。data-testid 就是這個門牌——它寫在原始碼上、對畫面沒有任何影響、跟外觀和內部結構完全脫鉤。開發者掛上去,測試認著它找:
圖 4:data-testid 的運作方式——開發者掛門牌,測試認門牌
圖裡有三個重點。原始碼那行黃色的 data-testid="order-summary" 就是門牌本體,使用者完全看不到它。測試端用 getByTestId('order-summary'),跟門牌一字不差對上。最關鍵的是下方那格:之後不管 class 全換、卡片搬家、文案重寫,只要門牌那行沒動,測試照常通過。
它為什麼叫「約定」?因為要兩邊都守。開發者這邊,不隨意改門牌,要改先知會測試;測試這邊,拿到門牌就別再用脆弱的方式定位那個元素。談一次,長期省下的維護時間非常可觀。
哪些元素值得掛門牌?幾個常見例子:
• 動態內容的卡片或區塊:訂單摘要、儀表板上的統計格,內容天天變、沒有穩定文字。
• 圖表:整塊是畫出來的圖,沒有可認的角色或文字。
• 清單裡重複的每一列:你練習用的 TodoMVC,每筆待辦就掛著 data-testid="todo-item"——官方示範站自己就在用這招,打開它的原始碼可以親眼看到。
跟開發者開口,話術可以很簡單:「訂單摘要那塊我要自動化,但它沒有穩定的文字可以定位,能幫我加個 data-testid 嗎?名字我建議叫 order-summary。」多數開發者一分鐘就改好,因為這對他們是零成本的小事。
最後劃一條線:門牌不是萬靈丹,別拿到鎚子就到處釘。有角色、有標籤、有穩定文字的元素,照前五招走——那些描述本來就存在,不用勞煩任何人。門牌留給真正「沒得認」的元素。
今天的練習
• 到官方示範站 demo.playwright.dev/todomvc,請 Claude Code 列出它會怎麼定位:輸入框、清單裡的某一筆、底下的 Active 篩選鈕。對照圖 1 的優先順序,看它各用了哪一級。
• 打開你們產品任何一頁,挑三個你常測的元素,問自己:用「角色 + 名稱」描述得出來嗎?描述不出來的那個記下來,它就是未來自動化的地雷,之後拿去跟開發者討論 data-testid。
• 順便觀察:TodoMVC 的清單項目用的是 getByTestId('todo-item'),這就是「測試專用門牌」的實例——官方示範站自己就埋了。