# Day 52-53:verifyPassword 的第一個 Vitest 測試(續);HTML/CSS 基本架構 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2026-02-04 **URL:** https://paragraph.com/@gcake/day-52-53 ## Content 今日閱讀:《單元測試的藝術》3/e 2.6 p.56 ~ 2.8 p.64Vitest 生命週期 API書中示範用 Jest beforeEach API 把不同測試條件共同需要初始化的程式碼抽出來,Vitest 也有相對應完全相容的 APIAPI執行時機適用情境潛在問題beforeEach每個測試前• 初始化測試資料
• 建立新的物件實例
• 重置狀態⚠ 如果初始化成本高,會拖慢測試速度afterEach每個測試後• 清理資源
• 關閉連線
• 刪除測試檔案⚠ 如果忘記清理,可能影響其他測試beforeAll所有測試前一次• 建立昂貴的資源(DB連線)
• 載入大型設定檔⚠ 測試間可能共享狀態,導致互相影響afterAll所有測試後一次• 關閉資料庫連線
• 清理共享資源⚠ 如果測試失敗可能不會執行需要在測試前做初始化? ↓ 能用 beforeEach 嗎? ↓ 是 ✅ 用 beforeEach(預設選擇) ↓ 否(速度真的太慢) ↓ 能重構程式碼避免依賴嗎? ↓ 是 ✅ 重構(最佳解法) ↓ 否(物理限制) ↓ ⚠️ 用 beforeAll(最後手段) + 寫清楚的註解說明為什麼 + 確保狀態真的不會被修改 + 加上 afterAll 清理 // password-verifier1.test.js import { describe, it, expect, beforeEach } from "vitest"; import { PasswordVerifier1 } from "./password-verifier1"; describe("Password Verifier", () => { // 以 USE 原則為測試命名 describe("with a failing rule", () => { let verifier; let fakeRule; beforeEach(() => { // 設定測試的輸入 verifier = new PasswordVerifier1(); fakeRule = (input) => ({ passed: false, reason: "fake reason" }); verifier.addRule(fakeRule); }); it("has an error message based on the rule.reason", () => { // 用輸入來呼叫進入點 const errors = verifier.verify("any value"); // 檢查退出點 expect(errors[0]).toMatch("fake reason"); }); // 在同一個退出點檢查額外的最終結果,解決斷言輪盤問題 it("has exactly one error message", () => { // 用輸入來呼叫進入點 const errors = verifier.verify("any value"); // 檢查退出點 expect(errors.length).toBe(1); }); }); }); 執行順序describe("with a failing rule") → let verifier; let fakeRule; (變數宣告) → beforeEach 執行 (第1次) - 賦值給 verifier, fakeRule → it("has an error message...") 執行 → beforeEach 執行 (第2次) - **重新賦值** → it("has exactly one error") 執行 按照範例寫完覺得上下捲動閱讀似乎降低了可讀性,聯想到之前看過的工廠函式概念,嘗試重構改寫:// password-verifier1.test.js import { describe, it, expect, beforeEach } from "vitest"; import { PasswordVerifier1 } from "./password-verifier1"; describe("Password Verifier", () => { // 以 USE 原則為測試命名 describe("with a failing rule", () => { // 用工廠函式取代 beforeEach 初始化每個測試條件需要的輸入參數 function createVerifierWithFakeRule() { const verifier = new PasswordVerifier1(); const fakeRule = (input) => ({ passed: false, reason: "fake reason" }); verifier.addRule(fakeRule); return verifier; } it("has an error message based on the rule.reason", () => { // 用輸入來呼叫進入點 const verifier = createVerifierWithFakeRule(); const errors = verifier.verify("any value"); // 檢查退出點 expect(errors[0]).toMatch("fake reason"); }); // 在同一個退出點檢查額外的最終結果,解決斷言輪盤問題 it("has exactly one error message", () => { // 用輸入來呼叫進入點 const verifier = createVerifierWithFakeRule(); const errors = verifier.verify("any value"); // 檢查退出點 expect(errors.length).toBe(1); }); }); }); 比較三種方案:方案可讀性彈性複雜度重複代碼⭐⭐⭐ 完整獨立⭐⭐⭐ 每個測試都能客製化⭐ 最簡單beforeEach⭐ 需要跳轉閱讀⭐ 難以客製化⭐⭐ 中等工廠函式⭐⭐ 語意清楚⭐⭐⭐ 可以傳參數客製化⭐⭐ 中等註:自己做完工廠函式版本才發現這就是作者緊接著在 2.7 安排的內容,專有名詞是避免「捲動疲勞」,2.7 略讀帶過(可選)拆除嵌套 describe如果確認測試程式已封裝完整且不再需要 describe 結構,可以考慮移除,作者似乎是比較喜歡最後保留 test...expect 簡潔風格,跟 describe...it...expect 的結構化風格各有好處,實作時要拿捏可維護性和可讀性的平衡點今日閱讀: MDN Getting Started Modules 文件 目前進度到 Learn > Getting started modules > Web standards > The web standards modelHTML 基礎HTML 是標記語言,告訴瀏覽器該如何呈現畫面元素 element由起始標籤、內容、結束標籤組成,可以增加一或多個屬性以利設定元素色彩、對齊方式、格線等視覺特性屬性 attribute包在起始標籤內,與元素名稱或其他屬性間有空格,屬性名稱後接 =空元素img 是一種沒有內容的空元素,因為圖片元素是直接把圖檔嵌在 HTML 上,以下列範例程式碼來說,它有開始標籤、兩個屬性、沒有內容、沒有結束標籤My test image HTML 基本架構 My test page My test image 用元素的概念階層式閱讀,可以找到的元素: 文件類型 根元素 元素,包含 metadata,預設樣式不會顯示 指定字元編碼為 UTF-8 瀏覽器標題列會顯示的標題<body> 元素,包含所有網頁瀏覽器會顯示的內容img 圖片元素,是一種沒有內容和結尾標籤的空元素圖片元素屬性src :source,圖片來源,可以是本地或遠端路徑alt :alternative,替代說明文字,可以讓報讀軟體說明圖片內容給視覺障礙者文字標記有 <h1> - <h6> 六個階層標題可以使用,<p> 用來表示文字段落清單無排序清單 unordered list:<ul>...<li>排序清單 ordered list:<ol>...<li>連結<a> anchor,用 href 屬性加上網址CSS 基本架構CSS 是一種風格頁面語言,讓 HTML 元素呈現不同樣式p { color: red; width: 500px; border: 1px solid black; } 選擇器1, 選擇器2, 選擇器3 { 屬性1: 屬性值1; 屬性2: 屬性值2; 屬性3: 屬性值3; } 選擇器有非常多不同類型,分別選取不同元素、屬性、類別 class、id …… 等 HTML 元素box 模型CSS 佈局主要基於「box 模型」。在頁面空間的每個 box 都有下列屬性:padding:內容周圍的空格border: 位於矩形內容外部的實線margin: 元素外部的空間 用 display 控制元素是 box 或 inline 顯示JavaScript 定義網頁互動行為CSS 預設樣式核心概念User Agent Stylesheet(使用者代理樣式表)每個瀏覽器都有一套內建的預設樣式表,當 HTML 沒有套用任何 CSS 時,瀏覽器會自動套用這套樣式讓網頁有基本排版 。 geeksforgeeks 本質:<head> 只是普通 HTML 元素,透過 display: none; 隱藏 stackoverflow所有元素的「預設長相」都來自 CSS,不是寫死在瀏覽器中瀏覽器廠商可以自行決定預設樣式細節 reddit瀏覽器預設樣式差異常見差異案例元素可能的差異<p>Chrome/Firefox/Safari 的 margin 值略有不同 blog.csdn<h1>font-size 通常一致,但 margin 可能有差異<button>圓角、內距、邊框樣式在 Safari 和 Chrome 有差異 stackoverflow表單元素輸入框、下拉選單外觀差異最明顯 geeksforgeeks實際範例<!-- 在不同瀏覽器看起來會有細微差異 --> <h1>歡迎來到我的網站</h1> <p>這是一段文字</p> Chrome DevTools 檢查方式:打開開發者工具(F12)選取元素找到標註「user agent stylesheet」的樣式 stackoverflowCSS Reset 的必要性2026 年的結論不像以前那麼必要,但仍有實務價值 youtube為什麼不像以前必要現代瀏覽器(Chrome、Firefox、Safari、Edge)預設樣式已非常接近不像 IE6 時代需要大量調整 dev為什麼仍有開發者會使用減少重複工作:避免每個專案都寫一次相同的覆寫團隊一致性:確保所有開發者從相同基準線開始 mui細微差異仍存在:表單元素在不同瀏覽器還是有差異 geeksforgeeksCSS Cascade 與 Reset 的關係關鍵理解誤解:「CSS 後面覆蓋前面,所以不需要 Reset」 正確觀念:覆蓋只能蓋住「你有寫的屬性」沒寫的屬性仍會使用瀏覽器預設值不同瀏覽器的預設值可能不同實例說明/* 情境 1:沒有 Reset */ h1 { font-size: 24px; /* 只覆蓋 font-size */ } /* 問題: margin 沒寫,各瀏覽器可能不同 */ /* 情境 2:有 Reset */ * { margin: 0; /* 先清空所有 margin */ } h1 { font-size: 24px; margin-bottom: 16px; /* 明確定義需要的間距 */ } /* 結果: 完全掌控,各瀏覽器一致 */ 現代 Reset 實務選擇選項 1:輕量級自訂 Reset(推薦)/* 現代最小化 Reset */ *, *::before, *::after { box-sizing: border-box; } body { margin: 0; line-height: 1.5; } img, picture, video { max-width: 100%; display: block; } button, input, select, textarea { font: inherit; } 選項 2:Normalize.css<!-- 保留有用的預設樣式,只修正不一致的部分 --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"> 適用場景: youtube不想完全清空預設樣式希望保留瀏覽器有意義的預設值(如 <b> 的粗體)選項 3:框架內建Material UI、Bootstrap 等框架已內建 Reset,使用框架時不需額外引入 。 muiReset 對 <head> 的影響核心觀念誤解:「Reset 只影響 <body> 元素」 正確觀念:CSS Reset 的萬用選擇器 * 會選中所有元素,包括 <html>、<head>、<meta> youtube<head> 隱藏是因為瀏覽器預設 display: none;,不是因為它特殊Reset 通常不會改變 display 屬性,所以 <head> 仍然不顯示實驗驗證/* 實驗 1:證明 <head> 是普通元素 */ head { display: block !important; background: yellow; padding: 10px; } /* 結果: <head> 會顯示在頁面上 */ /* 實驗 2:Reset 不會影響 display */ * { margin: 0; padding: 0; } /* 結果: <head> 仍然隱藏,因為 display: none 沒被覆蓋 */ CSS Cascade 規則當多個樣式來源同時存在時:瀏覽器預設: head { display: none; margin: 8px; } 你的 Reset: * { margin: 0; } ───────────────────────────────────────────── 合併結果: head { display: none; margin: 0; } 關鍵:屬性會合併,不是完全取代 。 stackoverflow易錯提醒1. 瀏覽器差異❌ 只在 Chrome 測試就上線✅ 至少在 Chrome、Firefox、Safari 測試2. Reset 策略❌ 盲目複製 2010 年的完全清空式 Reset✅ 使用現代輕量級 Reset,只重置必要項目 news.ycombinator3. CSS Cascade 理解❌ 以為「自己的 CSS」會完全取代「瀏覽器預設」✅ 理解「只有寫到的屬性會覆蓋」4. <head> 元素❌ 以為 <head> 有特殊的「永遠不顯示」機制✅ 理解它只是透過 display: none; 隱藏的普通元素 stackoverflow檢驗練習練習 1:檢查預設樣式建立空白 HTML 檔案,只放 <h1> 和 <p>在 Chrome DevTools 檢查它們的 margin 值加上 * { margin: 0; },觀察變化練習 2:理解 Cascade/* 執行這段 CSS,觀察結果 */ * { border: 1px solid red; } head { display: block; } 思考:為什麼 <head> 會顯示並有紅色邊框?練習 3:比較瀏覽器在 Chrome 和 Firefox 分別測試同一個按鈕的外觀差異。延伸閱讀MDN - CSS Cascade - 理解層疊原理 developer.mozillaA Modern CSS Reset (2024) - 業界最新思維 news.ycombinatorW3C HTML 預設樣式參考 - 官方建議的預設樣式 blog.csdn總結問題答案不同瀏覽器預設樣式有差異嗎?有,但現代瀏覽器差異已很小 dev2026 年還需要 CSS Reset 嗎?不是必須,但有討論仍建議使用輕量版 news.ycombinatorCSS 能覆蓋預設樣式,為何要 Reset?只能覆蓋「有寫的屬性」,沒寫的仍有差異Reset 會影響 <head> 嗎?會選中,但不會改變其 display: none stackoverflow核心原則:從統一的起點開始,明確定義所有需要的樣式,不依賴瀏覽器預設值。 ## Publication Information - [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/): Publication homepage - [All Posts](https://paragraph.com/@gcake/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@gcake): Subscribe to updates