核心原則

Playwright 不是最後手段、是縮短診斷迴圈的工具。 當靜態 CSS 推理 + 視覺截圖溝通的循環失敗 ≥ 2 次、就應該停止推理、改用 playwright browser_evaluate 直接讀 live DOM 與 computed style。早一點用 = 試錯次數更少、心智負擔更輕。


為什麼推理迴圈有極限

商業邏輯

CSS 行為由「規則 + DOM tree + 樣式繼承 + 框架渲染」四個變數共同決定。靜態推理只能基於假設的 DOM tree — 假設錯了、推理就錯。視覺截圖溝通只能傳達「結果是什麼」、無法傳達「為什麼是這個結果」。

Playwright 的 browser_evaluate 直接執行 JS 在 live page、返回真實的 DOM tree、computed style、bounding rect — 把「四個變數」全部變成已知

推理 vs 量測的成本曲線

方法第 1 次嘗試第 2 次第 3 次以上
靜態推理 + 截圖快 — 假設正確時一次到位慢 — 假設錯了得重來越來越慢 — 假設錯誤累積
Playwright 量測中 — 起 server、寫 evaluate快 — server 已在跑快 — 重用 setup

第 1 次推理快、後續成本爆炸;playwright 起步慢、後續穩定。門檻在第 2 次


這次任務的實際情境

觀察

要把 search scope UI 放在「搜尋輸入框與結果之間」。

第一輪:基於 class name 推測 DOM tree、用 grid + display:contents 設 grid-row 排序。第二輪:發現 scope 跑到頁尾、嘗試調 grid-template-rows。第三輪:嘗試 absolute 定位但時機不對。第四輪:使用者說「思路錯了」、要我換方向。

判讀

四輪推理都基於同一個假設:drawer.pagefind-ui 的直接子節點、跟 form 並列。實際用 playwright 一查:

1const drawer = document.querySelector('.pagefind-ui__drawer');
2let parents = []; let el = drawer;
3while (el && el !== document.body) {
4  parents.push(el.tagName + '.' + el.className);
5  el = el.parentElement;
6}

返回:

1DIV.pagefind-ui__drawer
2FORM.pagefind-ui__form    ← drawer 在 form 內!
3DIV.pagefind-ui

假設錯了 — drawer 是 form 的 child、不是 sibling。grid 規則無論怎麼寫都不會生效,因為 drawer 跟 form 共用同一個 grid cell。

四輪推理 ≈ 30 分鐘。Playwright 一次查清楚 ≈ 2 分鐘。

執行

確認 DOM 結構後:grid 不適合這個場景、改用 absolute + drawer margin-top spacer。一次到位。


Playwright 在開發循環的三個位置

1. 假設驗證

寫 CSS 規則前先量 DOM、確認結構符合假設。

1async () => ({
2  parents: [].slice.call(document.querySelectorAll('.target')).map(el => {
3    let chain = []; let n = el;
4    while (n) { chain.push(n.tagName + '.' + n.className); n = n.parentElement; }
5    return chain;
6  })
7})

2. 行為驗證

Layout 規則寫完後驗證實際結果。

1async () => ({
2  rect: document.querySelector('.target').getBoundingClientRect(),
3  computed: getComputedStyle(document.querySelector('.target')).gridRow,
4})

3. 互動驗證

驗證使用者互動後的狀態。

1async () => {
2  const input = document.querySelector('.search-input');
3  input.value = 'pre';
4  input.dispatchEvent(new Event('input', { bubbles: true }));
5  await new Promise(r => setTimeout(r, 1000));
6  return Array.from(document.querySelectorAll('.result'))
7    .filter(el => getComputedStyle(el).display !== 'none')
8    .map(el => el.textContent.slice(0, 50));
9}

內在屬性比較:四種 debug 方法

方法取得資訊量重複成本可寫成測試
靜態 CSS 推理低 — 全是假設高 — 每次重思考
視覺截圖溝通中 — 只有結果中 — 截圖 / 描述慢
瀏覽器 DevTools高 — DOM + computed中 — 每次手點
Playwright browser_evaluate最高 — 程式化任意查詢低 — 改 query 重跑是 — 同樣 query 可寫測試

選擇順序:簡單 layout 用 DevTools;複雜 / 反覆 debug 用 playwright;推理只在第 1 次試錯前


引入 playwright 的最低門檻

1# 啟動本地 server(任何方式)
2python3 -m http.server 8000 --directory public
3
4# 或專案有 hugo
5hugo server

Playwright MCP 提供:

  • browser_navigate(url) — 開頁
  • browser_evaluate(fn) — 執行 JS 拿結果
  • browser_take_screenshot() — 截圖
  • browser_snapshot() — accessibility tree

寫一個 evaluate fn ≈ 30 行 JS,比反覆推理快得多。


設計取捨:CSS / DOM debug 工具選擇

四種做法、各自機會成本不同。這個專案在推理 ≥ 2 次失敗後選 A(playwright browser_evaluate)當預設、其他做法在特定情境合理。

本篇是 #42 2 次門檻 抽象原則在「debug 工具切換」這個面向的應用。

A:Playwright browser_evaluate 程式化讀 live DOM(這個專案的預設)

  • 機制:起 server、用 browser_evaluate 寫 JS query 讀 DOM tree / computed style / bounding rect
  • 選 A 的理由:取得資訊量最大、可重跑、可寫成測試
  • 適合:推理失敗 ≥ 2 次、複雜或反覆 debug 的情境
  • 代價:起步成本中(需要 server + 寫 evaluate)

B:靜態 CSS 推理 + 視覺截圖溝通

  • 機制:純看 CSS 與假設的 DOM 推測、用截圖跟使用者溝通
  • 跟 A 的取捨:B 起步成本 0、A 起步成本中;但 B 第 2 次以後成本爆炸(每輪都基於前輪錯誤假設)
  • B 比 A 好的情境:第 1 次嘗試、預估假設正確機率高(簡單修改)

C:瀏覽器 DevTools 手動查

  • 機制:開 DevTools 切 Elements / Computed / Layout 面板手動探索
  • 跟 A 的取捨:C 不需 server / playwright setup、但每次手點切面板慢、不能寫成測試
  • C 比 A 好的情境:一次性確認、不需要重複 query 同樣資訊

D:寫成 playwright 測試固化

  • 機制:把 debug 過程寫成 playwright 測試、未來自動跑
  • 跟 A 的取捨:D 是 A 的延伸 — 第 2 次 debug 同個版型時、值得固化(#15 layout tests
  • D 比 A 好的情境:版型 bug 出現第 2 次以上、值得寫測試防止回歸

判讀徵兆

訊號工具切換時機第一個該寫的 evaluate
推理 ≥ 2 次失敗切到 playwright量目標元素的 ancestor chain
Layout 在某些狀態下錯、其他狀態下對切到 playwright量該元素在不同狀態下的 bounding rect
改 CSS 不生效、specificity 看起來對切到 playwright量 computed style 看真正套到的值
動態 DOM 結構不確定切到 playwright列出目標 container 的子節點

核心原則:縮短診斷迴圈的工具該早一點用、不該等到推理徹底失敗。第 2 次推理失敗就切換、別等第 5 次。

延伸應用:playwright 也用來查「資料層 vs 視覺層的層錯位」 — 見 #55 Filter 與 Source 的抽象層錯位browser_evaluate 量 source 真實 cardinality 與分批機制。