昨天講完 Chrome DevTools MCP(以下簡稱 CDP)能拿到 Network 和 Console,測試的驗證力跟我想像的比較靠近
所以想嘗試能不能順便幫我把 pytest 腳本寫出來呢?
CDP 跑測試的過程中,其實已經掌握了「哪個元素、什麼操作、什麼預期結果」。這些資訊丟掉太可惜。
所以我在 Prompt 裡加了兩段:
- 最後執行完成後,更新至 md_result/result.md
1. 幫我將有成功的測試步驟轉換成正確元素的 Xpath,以便於之後生產測試腳本使用
- Xpath 策略邏輯為
1. 有明確 id 先找 id(是 DOM 屬性上的 id,並非 Chrome MCP 生成的 id)
2. 使用完整文字組合搭配相對路徑
3. 完整 Xpath 絕對路徑
2. 讀取測試結果檔案,生成一個 pytest 腳本並且自動執行,
如果有錯誤要能自行修復後更新回腳本中,最多 3 次
整條鏈路變成這樣:

CDP 跑測試
↓
生成 md 測試結果(含 XPath)
↓
AI 讀 md,生成 pytest 腳本
↓
自動執行腳本
↓
失敗 → 自我修復 → 更新回腳本(最多 3 次)
注意那段 XPath 策略。
這是我覺得整個實驗裡最有價值的設計
我沒有讓 AI 隨便挑定位器,而是給了它一個優先序:
(這實際建議參考前端團隊設計的思路為主)
| 優先序 | 策略 | 為什麼 |
|---|---|---|
| 1 | 有明確 id 就用 id |
最穩定,前端改版最不容易動到 |
| 2 | 完整文字組合 + 相對路徑 | 語意明確,看得懂在測什麼 |
| 3 | 完整 XPath 絕對路徑 | 最後手段,改版必死 |
再執行結果中 XPath 策略有確實照著我給的優先序跑,不是隨便抓一條絕對路徑了事。
這個原理就跟之前這篇 Browser-use 底層原理有很大的關係了,也是借鏡了這原理進行這的設計
當下我以為:既然簡單的案例能生成,複雜的應該也只是多幾行而已。
實際發生的:完全不是這樣。
我一樣拿 Day14 兩個測試案例做對比:
案例 A(2 步):
案例 B:
MCP 執行 UI 的能力,跟「把執行過程翻譯成可重複的程式碼」的能力,是兩件事。
前者它做得很好,後者步驟一多就容易開始失真。
👉(這就像有人可以很流暢地開車載你到目的地,但你叫他把路線寫成一份別人看得懂的行車指南,可能就會寫得很零散。)
它是一個很好的「操作層」,但完整的測試流程需要的還是「框架層」。
| CDP 有沒有 | 測試流程需不需要 | |
|---|---|---|
| 開瀏覽器、點按鈕、輸入 | ✅ 有,而且很穩 | ✅ 需要 |
| 拿到 Console / Network | ✅ 有,這是它最強的地方 | ✅ 需要 |
| 明確的成功/失敗回饋 | ✅ 有 | ✅ 需要 |
| 自動登入 / 環境準備 | ❌ 沒有 | ✅ 必要 |
| 測試資料的建立與還原 | ❌ 沒有 | ✅ 必要 |
| 批次執行、排程 | ❌ 沒有 | ✅ 必要 |
| 結果收斂成報告 | ⚠️ 有輸出,但格式要自己定 | ✅ 需要 |
| 失敗了要通知誰 | ❌ 沒有 | ✅ 需要 |
| 這次學到的東西存下來 | ❌ 完全沒有 | ✅ 這才是關鍵 |
附上案例二的腳本:
"""
Test Case: 購物車移除商品後結帳
來源: md_template/testcase.md
Xpath 由 Chrome DevTools MCP 實際執行時擷取的 DOM 屬性產生,
優先序: DOM id > 文字+相對路徑(data-test) > 絕對路徑
"""
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
BASE_URL = "https://www.saucedemo.com"
@pytest.fixture
def driver():
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1280,900")
drv = webdriver.Chrome(options=options)
drv.implicitly_wait(5)
yield drv
drv.quit()
def test_cart_remove_item_then_checkout(driver):
wait = WebDriverWait(driver, 10)
# Step 1: 前往網站
driver.get(BASE_URL)
# Step 2: 登入
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="user-name"]'))).send_keys("standard_user")
driver.find_element(By.XPATH, '//*[@id="password"]').send_keys("secret_sauce")
driver.find_element(By.XPATH, '//*[@id="login-button"]').click()
# 用實際畫面元素(排序下拉選單)出現作為登入完成的判斷依據,
# 比只等 URL 變化更可靠:SPA 可能先更新網址列,畫面才渲染完成
wait.until(EC.presence_of_element_located(
(By.XPATH, '//select[@data-test="product-sort-container"]')
))
# Step 3: 排序 Price (low to high)
sort_select = Select(wait.until(
EC.presence_of_element_located((By.XPATH, '//select[@data-test="product-sort-container"]'))
))
sort_select.select_by_value("lohi")
# Expect Result 1: 排序後第一個商品為 Sauce Labs Onesie($7.99)
first_item_name = wait.until(
EC.presence_of_element_located((By.XPATH, '(//div[@data-test="inventory-item-name"])[1]'))
).text
first_item_price = driver.find_element(By.XPATH, '(//div[@data-test="inventory-item-price"])[1]').text
assert first_item_name == "Sauce Labs Onesie", f"排序後第一項應為 Sauce Labs Onesie,實際為 {first_item_name}"
assert first_item_price == "$7.99", f"排序後第一項價格應為 $7.99,實際為 {first_item_price}"
# Step 4: 加入三項商品到購物車(含重試,因實際執行時觀察到偶發點擊未生效)
def add_to_cart_with_retry(xpath, retries=3):
for attempt in range(retries):
driver.find_element(By.XPATH, xpath).click()
try:
wait.until(EC.presence_of_element_located(
(By.XPATH, xpath.replace('add-to-cart-', 'remove-'))
))
return
except Exception:
if attempt == retries - 1:
raise
raise AssertionError(f"加入購物車失敗,已重試 {retries} 次: {xpath}")
add_to_cart_with_retry('//*[@id="add-to-cart-sauce-labs-onesie"]')
add_to_cart_with_retry('//*[@id="add-to-cart-sauce-labs-bolt-t-shirt"]')
add_to_cart_with_retry('//*[@id="add-to-cart-sauce-labs-backpack"]')
cart_badge = driver.find_element(By.XPATH, '//*[@data-test="shopping-cart-badge"]').text
assert cart_badge == "3", f"購物車應顯示 3 件,實際為 {cart_badge}"
# Step 5: 進入購物車頁
driver.find_element(By.XPATH, '//a[@data-test="shopping-cart-link"]').click()
wait.until(EC.url_contains("cart.html"))
# Step 6: 移除 Sauce Labs Bolt T-Shirt
driver.find_element(By.XPATH, '//*[@id="remove-sauce-labs-bolt-t-shirt"]').click()
wait.until(EC.invisibility_of_element_located((By.XPATH, '//*[@id="remove-sauce-labs-bolt-t-shirt"]')))
# Step 7: Checkout,填資料,Continue
driver.find_element(By.XPATH, '//*[@id="checkout"]').click()
wait.until(EC.url_contains("checkout-step-one.html"))
driver.find_element(By.XPATH, '//*[@id="first-name"]').send_keys("QA")
driver.find_element(By.XPATH, '//*[@id="last-name"]').send_keys("Tester")
driver.find_element(By.XPATH, '//*[@id="postal-code"]').send_keys("100")
driver.find_element(By.XPATH, '//*[@id="continue"]').click()
wait.until(EC.url_contains("checkout-step-two.html"))
# Expect Result 2: 結帳確認頁只有 Onesie、Backpack 兩項
item_names = [el.text for el in driver.find_elements(By.XPATH, '//div[@data-test="inventory-item-name"]')]
assert set(item_names) == {"Sauce Labs Onesie", "Sauce Labs Backpack"}, f"結帳項目不符,實際為 {item_names}"
assert len(item_names) == 2, f"結帳項目應為 2 項,實際為 {len(item_names)} 項"
# Expect Result 3: 金額
item_total = driver.find_element(By.XPATH, '//div[@class="summary_subtotal_label"]').text
tax = driver.find_element(By.XPATH, '//div[@class="summary_tax_label"]').text
total = driver.find_element(By.XPATH, '//div[@class="summary_total_label"]').text
assert item_total == "Item total: $37.98", f"Item total 不符: {item_total}"
assert tax == "Tax: $3.04", f"Tax 不符: {tax}"
assert total == "Total: $41.02", f"Total 不符: {total}"
# Step 8: Finish
driver.find_element(By.XPATH, '//*[@id="finish"]').click()
wait.until(EC.url_contains("checkout-complete.html"))
# Expect Result 4: 出現 Thank you for your order!
complete_header = driver.find_element(By.XPATH, '//h2[@data-test="complete-header"]').text
assert complete_header == "Thank you for your order!", f"完成頁文案不符: {complete_header}"
# Expect Result 5: 購物車圖示不再顯示數字
badges = driver.find_elements(By.XPATH, '//*[@data-test="shopping-cart-badge"]')
assert len(badges) == 0, "購物車圖示應不再顯示數字(無 badge 元素)"