核心原則

視覺完成是「畫面看起來對」、功能完成是「使用者意圖真的被滿足」。 兩者在簡單情境下重合、在邊界情境下分裂。視覺完成出現得早(手動 happy path 一試就過)、功能完成需要刻意對照「使用者意圖完整集合」才看得出來。

寫程式時把「畫面對了」當成完工訊號 = 把驗收標準降到視覺層、漏掉「功能在邊界情境下是否還對」這層。


為什麼視覺驗收會早於功能驗收成立

驗收訊號的成本梯度

驗收方式觸發成本覆蓋的失敗類型
手動視覺驗收低 — 開頁、輸入一個 caseHappy path 的視覺正確
多 case 視覺驗收中 — 想出邊界 case視覺面的邊界
功能對照(語意驗收)高 — 列使用者意圖完整集功能跟意圖之間的縫
跨資料規模驗收高 — 製造稀疏 / 大量資料資料規模相依的功能失敗

成本低的訊號出現早 → 容易誤判完工。

視覺驗收的盲區

視覺驗收只看「螢幕上呈現的」、不看「應該呈現但沒呈現的」。後者沒有視覺訊號 — 不會閃紅、不會報錯、只是「該有的東西沒出現」。

這個盲區包括:

  • Filter 把該顯示的藏掉了(見 #55 Filter 與 Source 的層錯位
  • Pagination 漏抓了某幾頁
  • Sort 漏了某類元素
  • Async race condition 把舊資料留在畫面

共通點:錯誤的形式是「不該不在的不在」、不是「畫面壞了」。


多面向:四類「畫面對但功能漏」

面向 1:Filter / Sort / Count 跟 source 不同層

見 #55。視覺層 filter 套在分批 source 上、稀疏 case 顯露語意縫。

面向 2:Async race / 競態

1input.addEventListener('input', async () => {
2  const r = await search(input.value);
3  render(r);  // 慢的 query 後到、畫面是舊 query 的結果
4});

畫面有結果、看起來對、但對應的不是當前 query。

面向 3:Empty state / loading state 不分

1<div class="results">
2  {{ if results }}{{ for r }}{{ render r }}{{ end }}{{ end }}
3</div>

「還在 loading」跟「真的沒結果」共用同一個畫面 — 都是空。視覺對、功能上「使用者不知道狀態」。

面向 4:Form submit 後狀態回饋失真

1button.onclick = () => { saveData(); button.textContent = "Saved"; };

按了顯示 saved、但 saveData 是 async 還沒完成 / 失敗 — 畫面對、實際資料沒進 DB。

四個面向共用結構:動作有視覺回饋、但回饋的「時機」或「對象」跟「實際語意」對不上


「畫面對」屬於哪個 checkpoint

驗收要分散在四個時點(寫之前 / 開發中 / ship 前 / ship 後)— 詳見 #68 驗收的時間軸:四個 checkpoint

「畫面對」是 開發中 的視覺驗收訊號 — 用來判斷「邏輯有跑、UI 沒崩」。它不能取代:

  • 寫之前的「意圖完整集列舉」
  • Ship 前的「邊界 / 規模 case」
  • Ship 後的「真實使用者紀錄」

把「畫面對」當完工 = 把開發中的中介訊號當終點訊號 = 跳過後三個 checkpoint。


跟 #42「2 次門檻」的關係

#42 2 次門檻 講「第 1 次成功是低資訊量訊號、第 2 次(同方向 / 同類)才是真訊號」。

「畫面對」就是 #42 在「驗收訊號」面向的應用:「畫面對了一次」是低資訊量訊號、跟「程式跑通一次」「測試過一次」是同類。它告訴你「至少不是完全壞的」、不告訴你「對了」。

低資訊量訊號真訊號
畫面對了一次跨多個 case、多個規模、跨時間後仍對
程式跑通一次跨多次執行、不同輸入仍跑通
測試過一次涵蓋邊界 / 失敗 / 規模、CI 持續通過
使用者用過一次沒反映多週多使用者沒累積反映

把低資訊量訊號當完工 = 跨情境就是「同方向加碼到第 3 次」 — 都是「太早信任早期成功」的同個錯誤。


識別「視覺完成但功能未完成」的訊號

訊號 1:驗收靠「再點一下試試」

如果發現 bug 的方式是「我再操作一次就看出來了」 — 表示 happy path 過了、邊界 case 沒過。看到這個訊號要主動列邊界 case。

訊號 2:使用者描述的 bug 含「有時候」「偶爾」「我以為」

「有時候 load more 沒動」「我以為都篩過了」 — 這類語言反映的是「畫面跟意圖之間有縫、使用者用視覺驗收結果跟意圖對不上」。

訊號 3:實作時下意識覺得「先這樣、晚點補」

1// TODO: 處理 cache 跟 fresh 的合併
2const data = cached || fresh;

「晚點補」的部分通常就是視覺看不見的功能缺口。如果視覺驗收會過、TODO 會被忘記到 production。

訊號 4:測試只有 happy path 截圖

PR / commit 附的截圖只有「最常見的 case」 — 沒有「沒結果」「載入中」「失敗」「資料規模特別大 / 特別小」的截圖 → 驗收層級停在視覺。


設計取捨:怎麼把驗收從視覺升到功能

四種做法、不同情境合理。

A:寫之前列「使用者意圖的完整 case 集合」、實作後逐一對照

  • 機制:開工前列 happy path / 邊界 case / 失敗 case 三類、實作完逐一檢查
  • 選 A 的理由:把驗收標準從「能用」升到「對齊意圖」
  • 代價:需要主動想 case、寫之前花時間

B:靠自動化測試(unit / e2e)覆蓋邊界

  • 機制:每個 case 寫一個測試、CI 跑
  • 跟 A 的取捨:B 持續性更好、但成本高、且測試是寫的人決定的、漏想 case 一樣會漏
  • B 才合理的情境:大專案、團隊協作、回歸風險高

C:靠使用者回報

  • 機制:先 ship、使用者反映再修
  • 跟 A 的取捨:C 工程量最低、但 trust 損失高、bug 進 production 才被發現
  • C 才合理的情境:原型期、使用者願意幫忙找 bug、易回滾

D:只做視覺驗收(反模式)

  • 為什麼是反模式:把驗收標準降到視覺層、漏掉「功能跟意圖之間的縫」這層 — 而那層的失敗最常見也最貴
  • 看起來吸引人的原因:成本最低、happy path 過了就 OK、不需要列邊界 case
  • 實際發生的代價:silent 缺口累積、系統性使用者不信任、ship 後發現修起來比早期貴 N 倍(見 #68 瀑布原則

判讀徵兆

訊號該做的行動
驗收只看了 happy path 截圖補:邊界 case + 失敗 case + 規模 case
內心 OS:「畫面對了應該就 OK」停 — 列「使用者意圖完整集合」對照
Bug report 含「有時候」「偶爾」「我以為」是「畫面跟意圖之間有縫」的訊號
實作時寫了 TODO 但視覺驗收會過TODO 會在 production 被遺忘、必須補完
Filter / sort / async / cache 等「狀態相依」的功能完成主動跑「規模 / 稀疏 / 競態」三類 case

核心原則:視覺驗收是必要、不是充分。功能驗收要對照「使用者意圖完整集合」、不只是「畫面對」。視覺對 + 意圖縫 = 比畫面壞更危險、因為它不會觸發任何訊號。

延伸到測試驗收:「測試 PASS」也是視覺訊號的同類 — 沒看過該測試 RED 過、不知道它有沒有 catch 能力。詳見 #69 Test-First:先看到 RED 才相信 GREEN