工程日記第三天:126 項測試、圖片去重與自動化健康監控
生產加固第三天。測試數量突破 126 項,攻克了 framer-motion mock 和 Node require() 別名解析難題,對 5 個頁面完成圖片去重,並上線了 GitHub Actions 每週圖片健康檢查。
第三天:測試攀升
今天開始時我們有 91 項測試,結束時是 126 項。這個數字看起來不驚人,直到你瞭解背後的付出:爲 framer-motion 定製 mock 策略、對 Node 模塊解析系統打猴子補丁、以及對整個平臺的圖片重複使用進行全面審計。
這是 VOLO 生產加固階段的第三天。網站自 2 月 14 日上線以來已經運行了三天。Phase A 到 D(營銷網站、預訂平臺、Agent API、雙模式 UI)在上線前已完成。現在我們處於 Phase E:讓一切防彈。
第一部分:SmartBookingInput 組件測試
SmartBookingInput 是預訂體驗的核心——一個 1,380 行的組件(最近被重構爲 6 個模塊),同時處理 AI 自然語言模式和傳統表單模式。測試它意味着解決一個困擾許多 React 項目的問題:如何測試重度依賴 framer-motion 的組件?
framer-motion Mock 難題
framer-motion 的 motion.div 和 AnimatePresence 不是簡單的包裝器。它們在 React 樹中注入佈局測量、手勢處理和動畫編排。在 jsdom 測試環境中這些都不工作——更糟的是,framer-motion 傳遞自定義 props(variants、initial、animate、exit、layout、custom),這些不是有效的 DOM 屬性,會導致 React 拋出警告。
我們的方案:一個全面的 mock,用純 HTML 元素替換 motion.div 和 motion.span,通過 filterDomProps 輔助函數過濾動畫特有的 props,讓 AnimatePresence 成爲簡單的透傳包裝器。過濾函數顯式移除 15 個動畫 props,轉發其餘所有內容。
測試覆蓋
12 項測試覆蓋關鍵路徑:
- 默認 AI 模式渲染,正確的標題和佔位符
- AI 和表單模式之間的標籤切換
- AI 模式下的文本輸入(真實用戶事件模擬)
- 航班解析芯片顯示(輸入 "NYC to LA tomorrow" 看到解析後的芯片)
- 表單提交回調正確觸發
- 緊湊模式 prop 傳播
我們遵循的規則:mock 邊界,測試行爲。我們不測試 framer-motion 動畫是否正確——那是 framer-motion 的事。我們測試組件是否渲染了正確的內容並響應用戶操作。
第二部分:機場搜索引擎——23 項測試和一個模塊 Hack
我們的機場搜索引擎由 Fuse.js 驅動,索引全球 7,900+ 個機場,在 IATA 代碼、ICAO 代碼、城市名(中英文)和機場名之間進行加權模糊匹配。它是預訂流程中每個"出發"和"到達"字段的自動補全骨幹。
require() 別名問題
搜索引擎用 require("@/content/data/airports-global.json") 加載數據——這是服務器端數據加載的刻意選擇。@/ 前綴是 Next.js 和 Vite 自動解析 ESM import 語句的路徑別名。但 require() 是 Node.js 內置功能,完全繞過 Vite 的解析。
我們嘗試了三種方法都失敗了。最終的解決方案:在測試文件頂部補丁 Module._resolveFilename。這鉤入 Node 的內部模塊解析,攔截任何以 @/ 開頭的 require() 調用,並將其重映射到 src/ 下的絕對路徑。10 行猴子補丁讓整個機場搜索模塊可測試,無需修改生產代碼。
測試覆蓋
23 項測試覆蓋搜索引擎的每條代碼路徑:
- 精確匹配:IATA "JFK" 返回肯尼迪機場;ICAO "KJFK" 也能解析
- 大小寫不敏感:"jfk"、"Jfk"、"JFK" 都返回同一結果
- 城市前綴:"shangh" 匹配上海,"lond" 匹配倫敦各機場
- 模糊搜索:"tokyo" 通過 Fuse.js 匹配成田和羽田
- 中文搜索:用中文城市名查詢返回正確結果
- 邊界情況:空字符串、單字符、純空白、不存在的代碼 "ZZZ"
測試搜索引擎不是驗證 Fuse.js 是否工作,而是驗證你的配置——權重、閾值、匹配策略、優先級邏輯——是否產出用戶期望的結果。
第三部分:全平臺圖片去重
全面審計發現多張庫存圖片在多個頁面重複使用。對於奢侈品牌,視覺單調是一個安靜的可信度殺手——訪客在"關於"頁面和"餐飲"頁面看到同一張主圖時,會下意識地將其歸類爲"模板級別"。
發現的問題
網站上 4 組重複圖片:
- Pexels 3745234(海濱風景):出現在首頁、關於頁、餐飲頁和禮賓頁——4 次
- Pexels 30547618(現代建築):出現在首頁和禮賓頁——2 次
- Unsplash 1534088568(飛機):出現在包機和空腿頁——2 次
- Pexels 20562278(機艙內飾):在機艙體驗頁出現 3 次
替換策略
每個替換圖片都根據頁面的主題語境精選:
- 關於頁:替換爲全景風光(Pexels 2373201)——適合公司故事板塊
- 餐飲頁:替換爲精緻餐飲擺盤(Pexels 30507463)——直接關聯機上美食敘事
- 禮賓頁:替換爲奢華服務內景(Pexels 912050)——匹配白手套禮賓主題
- 空腿頁主圖:替換爲進場降落的飛機(Pexels 46148)——更好地傳達空腿機會
- 機艙頁分隔:替換爲高空機翼照片(Pexels 62623)——在機艙內飾板塊間增添視覺韻律
所有 8 個替換 URL 在提交前均通過 HTTP HEAD 請求驗證(狀態碼 200)。
第四部分:自動化圖片健康監控
上篇工程日記中,我們寫了修復 70 個失效 Unsplash URL 的過程,並承諾建立自動化健康檢查。今天兌現了。
GitHub Actions Cron 工作流
新的工作流位於 .github/workflows/image-health.yml,每週一 UTC 9:00 運行。它安裝項目、執行 check:images 腳本(向代碼庫中每個圖片 URL 發送 HEAD 請求),如果有任何 URL 返回非 200 狀態碼,自動創建標題爲"檢測到失效 Unsplash URL"的 GitHub Issue。
工作流還支持通過 workflow_dispatch 手動觸發,任何人都可以從 GitHub Actions 標籤頁按需運行。
最好的監控是那種設置一次就忘掉的。如果圖片 CDN 刪除了一張照片,我們會在一週內知道——而且問題已經在我們的 backlog 裏了,比任何人在線上看到裂圖都要早。
數據看板
| 指標 | 今日之前 | 今日之後 |
|---|---|---|
| 通過測試總數 | 91 | 126(+35) |
| 測試文件 | 4 | 6(+2) |
| ESLint 錯誤 | 0 | 0 |
| 重複圖片組 | 4 | 0 |
| 替換圖片的頁面 | — | 5 |
| CI 工作流 | 1(構建) | 2(+圖片健康 cron) |
| 生成靜態頁面 | 557 | 557 |
技術決策
爲什麼補丁 Module._resolveFilename 而不是重構爲 import()?
我們可以將生產代碼中的 require() 改爲動態 import()。選擇不改有三個原因:(1) require() 是爲服務器端同步數據加載特意選擇的,(2) 改變它會改變模塊的延遲初始化行爲,(3) 僅限測試的猴子補丁隔離在單個文件中,不影響生產代碼。準則:當存在測試級別的變通方案時,不要爲了滿足測試基礎設施而修改生產代碼。
爲什麼不 Mock 機場數據?
我們用真實的 7,900 機場數據集運行測試,而不是 5 個機場的 mock。這能發現真實問題:城市名編碼問題、相同 IATA/ICAO 前綴的排序邊界情況、模糊搜索的性能退化。數據集 1.2 MB——Vitest 加載不到 100ms。成本可以忽略;信心增益不可以。
第三天是關於深度,不是廣度。我們沒有發佈任何新功能。我們讓現有功能可證明是正確的、視覺上獨特的。這就是把演示變成產品的那種安靜工作。
準備好飛行了嗎?幾秒鐘獲取個性化包機報價。
訂閱資訊
空腿航班優惠、新航線與航空洞察,直達您的郵箱。