iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0

Day 17 我故意沒做一件事:匯入前確認。

那時候,匯入一份合法的 draft,會直接取代目前的 draft。

而 ClarifyBuild 每次變更都會寫進 localStorage。被取代的舊 draft,沒有版本可以回頭找。

Day 18 我也還沒補 browser test。

我先把按鈕背後的 app flow 抽出來測:

generate spec
↓
generate prompt
↓
export draft
↓
import draft
↓
regenerate spec and prompt

所以 Day 19 接著做的,就是 Day 17 留下的那道安全確認。

產品上的規則是:

合法,不等於可以立刻取代使用者目前的工作。

程式上的對應,是讓「合法」和「取代」變成兩個不同步驟:

先驗證
↓
再確認
↓
最後才套用

browser test 一樣先放著,文末再說。


匯入為什麼要先問

ClarifyBuild 的 draft 不是一段普通文字。

它保存的是使用者目前整理到一半的來源資料和操作進度。

例如:

原始想法
需求釐清結果
功能範圍
Specification Setup
專案限制
技術設定

這些資料加起來,就是目前這份專案規格的來源。

如果使用者已經花了一段時間,把一個模糊想法整理成 draft,然後選到另一個 JSON 檔,App 不應該只因為檔案格式合法,就直接把目前工作蓋掉。

今天加的確認文字是:

匯入會取代目前 draft,且需要重新產生輸出

這句話提醒兩件事。

第一,匯入會取代目前 draft。

第二,匯入後不能沿用舊的 generated output。

這道確認擋的是「使用者沒意識到匯入會覆蓋目前 draft」。

它還擋不了「使用者選錯檔」,因為 native window.confirm() 裡沒有顯示即將匯入的 draft 摘要。這也是之後如果改成 App 內 modal,可以再補上的細節:例如顯示即將匯入的 idea。


先驗證,但還不生效

加確認其實不難。

Day 18 的 importDraftState(raw) 本來就只是解析、驗證,然後回傳一份新的 state。它不會碰目前的 state。

所以如果只看今天的 native window.confirm(),在 state = imported 前面插一行確認,也能做到取消時保留 draft。

我還是把它拆成兩段,原因有三個。

第一,「檔案合法,但還沒生效」有了名字,也有固定的回傳形狀,可以被測。

第二,匯入的 state 要進 App,一定要經過 applyPreparedDraftImport()。

第三,今天用的是同步的 window.confirm(),跳出來的時候 App 整個停住。但之後如果換成 App 內 modal,畫面是靠 state render 出來的。打開 modal 本身就可能是一次 commit(),而 commit() 每次都會寫 localStorage。

那時候,如果 imported state 已經先換進 state,舊 draft 在確認視窗打開的那一刻就被蓋掉了。

第三點今天還沒發生。

今天先把順序切開,是為了之後換 modal 時不用重做這段流程。

Day 19 新增的第一段叫:

prepareDraftImport(raw)

它只做:

raw draft file
↓
parse JSON
↓
validate draft export shape
↓
restore imported builder state
↓
return prepared import result

如果檔案不合法,它回傳:

ok: false
state: null
message: "Draft 匯入失敗:檔案格式不符合 ClarifyBuild draft。"

如果檔案合法,它回傳:

ok: true
message: "匯入會取代目前 draft,且需要重新產生輸出"
state: importedState

重點是:prepareDraftImport() 不知道目前 App 的 current state 是什麼,也不會修改它。

它只是說:

這份檔案是合法 draft
如果使用者確認,可以套用這個 imported state

確認後才 apply

第二段叫:

applyPreparedDraftImport(preparedImport)

匯入後,Spec 和 Prompt 一定是空的。這件事有兩層在守。

第一層在 storage。

Day 17 開始,匯出檔本來就不寫 generatedSpec 和 generatedPrompt。匯入時,就算檔案裡多了這兩個欄位,也不會被還原。

第二層在 applyPreparedDraftImport()。

state 進 App 前,它再清一次。

為什麼這麼在意?

因為 Spec 和 Prompt 是從 draft 算出來的結果,來源資料只有 draft。

如果有哪個畫面還留著舊 draft 的 Spec,它很可能又被拿去產生 Prompt。這樣 Coding Agent 拿到的,就不是這份 draft 的決策。

所以 draft 一換,Spec 和 Prompt 就要從新的 draft 重新產生。


main.js 現在怎麼走

改完之後,browser action 裡的匯入流程是:

選檔案
↓
prepareDraftImport()
↓
不合法:跳錯誤 toast,目前 draft 不動,也不會問使用者
↓
合法:window.confirm()
↓
取消:目前 draft 和 Spec / Prompt 都在,跳「已取消」toast
↓
確定:applyPreparedDraftImport() → render + save → 跳「請重新產生輸出」toast

確定之後,如果 render 失敗,Day 17 留下來的 rollback 還在。

使用者反悔和程式出錯是兩回事。

使用者反悔時,imported state 不應該進 App。

程式出錯時,才需要把 state 放回 previousState。

今天先用 native window.confirm(),不是因為它是最好的互動。

而是因為 Day 19 要先把產品行為切清楚:

合法 draft 不再立即覆蓋目前 draft

等 browser test 有了,再決定要不要把確認視窗換成 App 內 modal。


補一個 Day 18 留下的 regression

Day 18 還有一個缺口。

改了來源資料就清掉 generated output,那段寫在 main.js 的 action 裡,Day 18 沒有測到。

這和今天的 import confirmation 有關。

因為兩者都在處理 generated output lifecycle。

ClarifyBuild 的規則是:

只要來源資料變了,舊的 Spec 和 Prompt 就不能繼續被當成目前輸出。

所以 Day 19 也把清空 generated output 的 helper 放進 appFlow.js:

invalidateGeneratedOutput(state)

main.js 原本的 local helper 現在改成呼叫它。

這沒有新增產品邏輯,只是把既有行為移到可以測的位置。

不過這個測試守得很小。

它只確認 helper 會清掉 generatedSpec 和 generatedPrompt。

至於 main.js 的 action 有沒有真的呼叫到它,今天沒有測。

清掉舊輸出,守的是 state 的規則:輸出一定是從目前 draft 算出來的。

今天的畫面上,反而看不出差別。使用者改完來源資料,要回到預覽頁就得重新產生,舊的 Spec 和 Prompt 不會被看到。

所以 browser test 從畫面驗不到這件事。要測它,得讓測試讀得到 state,或是像 Day 18 那樣,把這段狀態流程抽出來測。


今天測了什麼

Day 19 新增四個 app-flow 測試。

第一個測 prepareDraftImport():

draft import preparation validates without replacing the current draft

這個測試建立一份 current state,又建立另一份要匯入的 draft。

它確認合法檔案會變成 prepared import,而且 current state 沒有被改到。

嚴格說,current state 沒被改到,是函式簽名本身保證的:prepareDraftImport(raw) 根本沒有收到 current state。

但這個測試仍然有價值,因為它把 prepared result 的形狀固定下來:ok、message、state,還有匯入後的 stage 和空的 generated output。

第二個測壞檔:

draft import preparation rejects invalid draft files without changing current output

壞掉的 JSON 會失敗,不會產生可套用的 state。

第三個測 apply:

applying a prepared import clears generated output and rejects unusable results

它確認 applyPreparedDraftImport() 自己會清掉 generated output,也會拒絕不可用的 prepared result。

第四個測 source edit invalidation:

source edit flow invalidates generated output before regeneration

它鎖住 shared invalidation helper。

今天的測試沒有執行 src/main.js。

也就是說,真正的「使用者選檔 → confirmation → cancel / confirm → toast → render」還沒被自動化跑過。


測試結果

Day 18 結束時:

tests 20
pass 20
fail 0

Day 19 結束後:

tests 24
pass 24
fail 0

另外也做了語法檢查:

node --check src/main.js
node --check src/state/appFlow.js
node --check test/appFlow.test.js

都通過。

靜態 server 也有確認可以啟動並回應首頁。


今天沒有做什麼

Day 19 沒有新增 browser UI automation。

也沒有引入 Playwright 或 jsdom。

src/main.js 仍然沒有被自動化測試真正執行。

確認 UI 也只是 native window.confirm(),不是 App 內 modal。

所以今天完成的是資料規則和 app-flow helper,還不是完整的瀏覽器互動驗證。


Day 19 小結

Day 19 表面上只是多了一句確認:

匯入會取代目前 draft,且需要重新產生輸出

但實際上,它是在修正一個產品邊界:

驗證合法,不等於可以立刻取代使用者目前的工作。

Day 17 讓匯入變安全:

不合法的檔案不能進 App
generated output 不能從 draft 復活

Day 18 讓匯入所在的 app flow 可以被測:

產生輸出
匯出
匯入
重新產生

Day 19 則補上使用者操作上的安全:

先驗證
再確認
最後才套用

今天最重要的承諾,是「取消就保留目前 draft」。

它寫在 main.js 的 if 裡,今天沒有任何測試會執行它。

Node tests 守住了 prepare 和 apply 的資料規則。

但使用者是不是真的選得了檔、看得到確認、按了取消之後什麼都沒少,還需要真的在瀏覽器裡跑一次。

明天 Day 20,我想補這條 browser-level test,先走三條路:

選壞檔 → 沒有確認視窗,跳錯誤 toast,目前 draft 還在
選好檔 → 取消 → 目前 draft 和 Spec / Prompt 都在
選好檔 → 確定 → 回到 Specification Setup,重新產生的 Spec 是新 draft 的內容

做到這裡,browser test 就繞不過 dependency 的決定了。

要不要把 window.confirm() 換成 App 內 modal,等這條測試有了再說。

Day 19 完成。

明天見。


上一篇
Day 18|還沒有 browser test,我先把按鈕背後的狀態流程抽出來測
下一篇
Day 20|終於補 browser test,但我只測使用者看得到的事
系列文
AI 寫不好,可能是我沒說清楚:30 天打造 ClarifyBuild,讓 Vibe Coding 從需求開始 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言