前兩篇分別讓 AI 生成測試,然後說明測試失敗時怎麼用 trace 找出失敗原因。今天是第三個緩衝日,讓我們練習實際用AI生成測試然後用 trace 找出失敗原因吧!
實際上在產出測試時,因為AI產出具有隨機性,如果生出的測試程式跟這篇的內容有不一樣是很正常的,所以這篇的重點會是在利用AI生成測試跟練習使用trace找出失敗原因這件事,只要知道你要怎麼針對你的AI產出的程式去做驗證跟修正就好。
這次挑的是 react-admin demo 原本就有的商品編輯頁(/#/products/1):
99.9
Updated by Day23
流程只有三步,但它在三個不同的層次都可能出錯:
| 可能出錯的地方 | 原因 |
|---|---|
| 分頁 | TabbedForm 沒切到分頁,DOM 結構中有這個欄位,只是是在別的分頁中 |
| 富文本編輯器 | Description 是 contenteditable 的編輯器,不是一般的 <input>,而且 label 是空字串 |
| 儲存時機 | react-admin 的 Edit 預設是 undoable 模式:按下 Save 後會先顯示通知並提供 Undo,請求要晚一點才真正送出 |
對 AI 來說,這三件事都沒辦法只靠「看需求」就猜對,所以很適合拿來練習。
直接讓 AI去讀取整個codebase,了解react-admin的運作模式,寫出一支完整的測試程式,以下是這次的prompt:
請用 Playwright + TypeScript 寫一支測試:
登入後進入 /#/products/1,在 Details 分頁把 Price 改成 99.9,
在 Description 分頁把描述改成「Updated by Day23」,按 Save 並確認資料真的存到後端。
規範:
- 從 '../fixtures/fixtures' 匯入 test / expect,使用 loggedInPage fixture
- 每個階段用 test.step 包起來
這次特別要求AI產出要每個階段用 test.step 包起來,這邊補充說明一下。
test.step 是什麼?test.step 主要是用來將一個測試程式碼結構依步驟拆分,每個步驟都可以有自己的小標題讓人識別這個步驟在做什麼,也讓你看 HTML 報告和追蹤記錄能更容易理解,測試失敗時可以很明確知道是在哪個步驟出問題。
await test.step('Details 分頁:修改價格', async () => {
await page.getByRole('tab', { name: 'Details' }).click();
await page.getByLabel('Price').fill('99.9');
});
// playwright-tests/tests/day23-gemini-ai.spec.ts
import { test, expect } from '../fixtures/fixtures';
test('編輯產品 (ID: 1) 之價格與描述並確認儲存至後端', async ({ loggedInPage: page }) => {
// 1. 前往產品編輯頁面
await test.step('前往產品 1 編輯頁面', async () => {
await page.goto('/#/products/1');
await page.waitForLoadState('networkidle');
});
// 2. 切換至 Details 分頁並修改 Price
await test.step('在 Details 分頁修改 Price 為 99.9', async () => {
await page.getByRole('tab', { name: 'Details' }).click();
const priceInput = page.getByLabel('Price');
await priceInput.fill('99.9');
await expect(priceInput).toHaveValue('99.9');
});
// 3. 切換至 Description 分頁並修改描述
await test.step('在 Description 分頁修改描述為「Updated by Day23」', async () => {
await page.getByRole('tab', { name: 'Description' }).click();
const descriptionEditor = page.locator('.ProseMirror');
await descriptionEditor.fill('Updated by Day23');
await expect(descriptionEditor).toContainText('Updated by Day23');
});
// 4. 按下 Save 並驗證後端 API 回應與 UI 提示
await test.step('點擊 Save 並確認資料成功儲存至後端', async () => {
// 設定監聽器:等待產品更新 (PUT / PATCH) 的後端 API 回應
const saveResponsePromise = page.waitForResponse(
(response) =>
response.url().includes('/products/1') &&
['PUT', 'PATCH'].includes(response.request().method())
);
// 點擊 Save 按鈕
await page.getByRole('button', { name: 'Save' }).click();
// 驗證後端 API 回應狀態碼與 Payload
const response = await saveResponsePromise;
expect(response.status()).toBe(200);
const responseData = await response.json();
expect(responseData.price).toBe(99.9);
expect(responseData.description).toContain('Updated by Day23');
// 驗證畫面出現成功儲存訊息提示 (Toast)
await expect(page.getByText(/Element updated|Poster updated/i)).toBeVisible();
});
});
執行之前,我們先快速看一下他產出的內容:
| 檢查項目 | 觀察 |
|---|---|
| 架構合規性 | 有用 loggedInPage fixture,沒有自己重寫登入 ✅ |
| Locator | 分頁、欄位、按鈕都用 role 或 label 定位;富文本編輯器用 .ProseMirror 這個 class 定位,可以確認一下是否有更好的定位做法 ⚠️ |
| 等待機制 | 沒有 waitForTimeout ✅;但用了 waitForLoadState('networkidle'),這是官方不建議的等待方式 ⚠️ |
| 斷言品質 | 填完欄位後有馬上確認值;儲存後不只看狀態碼,還檢查了回應裡的 price 和 description ✅ |
整體來說,這支測試寫得比預期好:它知道要先切分頁、知道編輯器不是一般的 input,儲存後也確實去驗證後端收到的資料,而不是只看畫面。不過最後一行:/Element updated|Poster updated/i看起來 AI 不確定通知會顯示哪段文字,所以寫了一個兩種都接受的正規表示式,這部分可能要人工確認一下。
接下來我們會跑測試,如果有錯誤就試看看使用 trace viewer 來協助除錯。另外元素定位部分的調整,因為之前的篇章已經介紹過了,這篇就不再重述,大家可以自己試試看。
因為我們沒有要求AI要開啟trace viewer功能,所以我們直接在執行時補上--trace on,可以看到以下的錯誤訊息:
npx playwright test day23-gemini-ai --project=chromium --trace on
Error: expect(locator).toBeVisible() failed
Locator: getByText(/Element updated|Poster updated/i)
Expected: visible
Timeout: 5000ms
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for getByText(/Element updated|Poster updated/i)
第一步:確認是哪個階段壞的。
因為有 test.step,動作列表會照四個步驟分組。前三個步驟都通過了,失敗的是最後一個「點擊 Save 並確認資料成功儲存至後端」裡的 toBeVisible。初步排除其他測試步驟有問題的可能。

第二步:看失敗那一步的快照。
錯誤訊息是 element(s) not found。第一個直覺通常是「文字寫錯了」,而且 AI 自己也不確定通知文字,寫了 /Element updated|Poster updated/i,看起來更像是文字的問題。
我們先點開失敗的那一步,看它的快照發現畫面已經回到商品列表頁,且沒看到任何寫著Element updated或是Poster updated的通知。

通知不在,可能是它根本沒出現過,或者出現過但已經消失了。只看這一張快照還分不出來,我們再往前找。
第三步:往前找,通知到底有沒有出現過?
往上點選 Click Save 那一步,看它的 After 快照,發現通知就在畫面下方:
Poster updated UNDO

這表示:
Poster updated,AI 寫的正規表示式其中一個有對應到,所以應該不是文字的問題。問題從「找不到文字」變成為什麼斷言執行的時候,通知已經消失了?
第四步:用 Network 對時間。
打開 Network 分頁,找 PUT https://demo.api.marmelab.com/products/1,發現這個請求是在8.8秒才發送出去。
再點選左側動作後查看下方Call顯示該動作的起始時間:
| 時間(測試開始後) | 發生的事 |
|---|---|
| 約 4.4 秒 | 按下 Save,通知出現 |
| 約 9.1 秒 | 開始執行 toBeVisible,通知早已消失,等了 5 秒後失敗 |

這樣我們可以推論,按下 Save 後可能是先顯示通知,讓使用者有幾秒鐘可以按 Undo,之後通知關閉,PUT 請求才真正送出,等到斷言執行的時候,通知早就消失了。
回頭看 AI 的程式碼順序:
await page.getByRole('button', { name: 'Save' }).click();
const response = await saveResponsePromise; // ← 在這裡等了 4 秒多,等通知關閉、PUT 送出
// ...檢查 response...
await expect(page.getByText(/Element updated|Poster updated/i)).toBeVisible(); // ← 通知早就消失了
AI 先等 PUT 回來,最後才檢查通知。但實際上的順序應該是先出現通知,然後等待通知消失,PUT 請求才會被送出,所以目前測試的這個寫法註定會失敗。
另外,Network 也顯示 PUT 確實有送出、回應 200,response 裡的 price 和 description 也都通過了斷言,所以 waitForResponse 這段是正確的。
既然知道問題是在斷言的順序,那我們調換順序試看看,順便把正規表示式改成'Poster updated',然後重跑一次,看新的 trace。
await page.getByRole('button', { name: 'Save' }).click();
+ // 通知在按下 Save 後立刻出現,要在它消失前檢查
+ await expect(page.getByText('Poster updated')).toBeVisible();
+
+ // 通知關閉後才會送出 PUT
const response = await saveResponsePromise;
expect(response.status()).toBe(200);
const responseData = await response.json();
expect(responseData.price).toBe(99.9);
expect(responseData.description).toContain('Updated by Day23');
-
- // 驗證畫面出現成功儲存訊息提示 (Toast)
- await expect(page.getByText(/Element updated|Poster updated/i)).toBeVisible();
重跑之後,測試全部通過!
可以,但最好可以提供一些你目前排查的狀況,而不是只說「測試壞了」。比較兩種說法:
Poster updated 立刻出現;約 4 秒後通知關閉,PUT 才送出;而斷言是在 PUT 回來之後才執行,那時通知已經消失。」第一種說法,AI 看到 element(s) not found,很可能會以為是文字寫錯,再換一組文字押寶,或者把 timeout 調大。第二種說法已經把時間順序講清楚了,AI 要做的只是調換程式碼的順序。
AI 給的修正依然只是假設,要再跑一次、看新的 trace,確認它是對的。
原本的錯誤訊息是 element(s) not found 加上 AI 本來寫的正規表示式,可能會很自然地以為是文字寫錯才找不到元素,但實際看trace發現文字其實是對的。
而真正的原因是斷言的時機不對,如果沒有研究每個動作及請求的開始時間,會很難發現問題是在驗證時間點的問題。
今天完整走了一次 AI 產生測試 → 執行失敗 → trace 找證據 → 修正 → 再跑 的過程。請 AI 生成測試時要求用 test.step 分段,失敗時可以直接看出是哪個階段壞的。針對錯誤訊息有時候還是需要透過trace提供的詳細資訊判斷測試失敗的原因是什麼,才能正確修正測試。
下一篇開始進入資料與登入優化的階段,第一個主題是參數化測試。