# Day 52-53：verifyPassword 的第一個 Vitest 測試（續）；HTML/CSS 基本架構

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2026-02-04

---

今日閱讀：《單元測試的藝術》3/e 2.6 p.56 ~ 2.8 p.64

Vitest 生命週期 API
---------------

書中示範用 Jest `beforeEach` API 把不同測試條件共同需要初始化的程式碼抽出來，Vitest 也有相對應完全相容的 API

API

執行時機

適用情境

潛在問題

`beforeEach`

每個測試前

• 初始化測試資料<br>• 建立新的物件實例<br>• 重置狀態

⚠ 如果初始化成本高，會拖慢測試速度

`afterEach`

每個測試後

• 清理資源<br>• 關閉連線<br>• 刪除測試檔案

⚠ 如果忘記清理，可能影響其他測試

`beforeAll`

所有測試前一次

• 建立昂貴的資源（DB連線）<br>• 載入大型設定檔

⚠ 測試間可能共享狀態，導致互相影響

`afterAll`

所有測試後一次

• 關閉資料庫連線<br>• 清理共享資源

⚠ 如果測試失敗可能不會執行

    需要在測試前做初始化？
        ↓
    能用 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 model

HTML 基礎
-------

HTML 是標記語言，告訴瀏覽器該如何呈現畫面

### 元素 element

由起始標籤、內容、結束標籤組成，可以增加一或多個屬性以利設定元素色彩、對齊方式、格線等視覺特性

### 屬性 attribute

包在起始標籤內，與元素名稱或其他屬性間有空格，屬性名稱後接 `=`

### 空元素

img 是一種沒有內容的空元素，因為圖片元素是直接把圖檔嵌在 HTML 上，以下列範例程式碼來說，它有開始標籤、兩個屬性、沒有內容、沒有結束標籤

    <img src="images/firefox-icon.png" alt="My test image" />
    

HTML 基本架構
---------

    <!doctype html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>My test page</title>
      </head>
      <body>
        <img src="images/firefox-icon.png" alt="My test image" />
      </body>
    </html>
    

用元素的概念階層式閱讀，可以找到的元素：

*   `<!doctype html>` 文件類型
    
*   `<html>` 根元素
    
    *   `<head>` 元素，包含 metadata，預設樣式不會顯示
        
        *   `<meta charset="utf-8" />` 指定字元編碼為 UTF-8
            
        *   `<title>` 瀏覽器標題列會顯示的標題
            
    *   `<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](https://www.geeksforgeeks.org/css/what-is-a-user-agent-stylesheet/)

**本質**:

*   `<head>` 只是普通 HTML 元素，透過 `display: none;` 隱藏 [stackoverflow](https://stackoverflow.com/questions/16130690/is-the-head-tag-a-regular-tag-that-is-not-displayed-by-default/16130724)
    
*   所有元素的「預設長相」都來自 CSS，不是寫死在瀏覽器中
    
*   瀏覽器廠商可以自行決定預設樣式細節 [reddit](https://www.reddit.com/r/webdev/comments/kvipvn/does_each_browser_have_their_own_default_css_for/)
    

* * *

### 瀏覽器預設樣式差異

#### 常見差異案例

元素

可能的差異

`<p>`

Chrome/Firefox/Safari 的 `margin` 值略有不同 [blog.csdn](https://blog.csdn.net/liaozhongping/article/details/47058243)

`<h1>`

`font-size` 通常一致，但 `margin` 可能有差異

`<button>`

圓角、內距、邊框樣式在 Safari 和 Chrome 有差異 [stackoverflow](https://stackoverflow.com/questions/9598624/css-padding-variations-between-chrome-firefox-safari)

表單元素

輸入框、下拉選單外觀差異最明顯 [geeksforgeeks](https://www.geeksforgeeks.org/css/difference-between-reset-vs-normalize-css/)

#### 實際範例

    <!-- 在不同瀏覽器看起來會有細微差異 -->
    <h1>歡迎來到我的網站</h1>
    <p>這是一段文字</p>
    

**Chrome DevTools 檢查方式**:

1.  打開開發者工具（F12）
    
2.  選取元素
    
3.  找到標註「user agent stylesheet」的樣式 [stackoverflow](https://stackoverflow.com/questions/12582624/what-is-a-user-agent-stylesheet)
    

* * *

### CSS Reset 的必要性

#### 2026 年的結論

**不像以前那麼必要，但仍有實務價值** [youtube](https://www.youtube.com/watch?v=msBOZEwWaNE)

#### 為什麼不像以前必要

*   現代瀏覽器（Chrome、Firefox、Safari、Edge）預設樣式已非常接近
    
*   不像 IE6 時代需要大量調整 [dev](https://dev.to/hankchizljaw/a-modern-css-reset-6p3)
    

#### 為什麼仍有開發者會使用

1.  **減少重複工作**:避免每個專案都寫一次相同的覆寫
    
2.  **團隊一致性**:確保所有開發者從相同基準線開始 [mui](https://mui.com/material-ui/react-css-baseline/)
    
3.  **細微差異仍存在**:表單元素在不同瀏覽器還是有差異 [geeksforgeeks](https://www.geeksforgeeks.org/css/difference-between-reset-vs-normalize-css/)
    

* * *

CSS 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](https://www.youtube.com/watch?v=LrTR-iJDFEo)

*   不想完全清空預設樣式
    
*   希望保留瀏覽器有意義的預設值（如 `<b>` 的粗體）
    

### 選項 3:框架內建

Material UI、Bootstrap 等框架已內建 Reset，使用框架時不需額外引入 。 [mui](https://mui.com/material-ui/react-css-baseline/)

* * *

Reset 對 `<head>` 的影響
--------------------

### 核心觀念

**誤解**:「Reset 只影響 `<body>` 元素」

**正確觀念**:

*   CSS Reset 的萬用選擇器 `*` 會選中**所有元素**，包括 `<html>`、`<head>`、`<meta>` [youtube](https://www.youtube.com/watch?v=5e0wuvhxLk4)
    
*   `<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](https://stackoverflow.com/questions/7466404/chrome-user-agent-stylesheet-overwriting-my-site-style)

* * *

易錯提醒
----

### 1\. 瀏覽器差異

*   ❌ 只在 Chrome 測試就上線
    
*   ✅ 至少在 Chrome、Firefox、Safari 測試
    

### 2\. Reset 策略

*   ❌ 盲目複製 2010 年的完全清空式 Reset
    
*   ✅ 使用現代輕量級 Reset，只重置必要項目 [news.ycombinator](https://news.ycombinator.com/item?id=41943063)
    

### 3\. CSS Cascade 理解

*   ❌ 以為「自己的 CSS」會完全取代「瀏覽器預設」
    
*   ✅ 理解「只有寫到的屬性會覆蓋」
    

### 4\. `<head>` 元素

*   ❌ 以為 `<head>` 有特殊的「永遠不顯示」機制
    
*   ✅ 理解它只是透過 `display: none;` 隱藏的普通元素 [stackoverflow](https://stackoverflow.com/questions/16130690/is-the-head-tag-a-regular-tag-that-is-not-displayed-by-default/16130724)
    

* * *

檢驗練習
----

### 練習 1:檢查預設樣式

1.  建立空白 HTML 檔案，只放 `<h1>` 和 `<p>`
    
2.  在 Chrome DevTools 檢查它們的 `margin` 值
    
3.  加上 `* { margin: 0; }`，觀察變化
    

### 練習 2:理解 Cascade

    /* 執行這段 CSS，觀察結果 */
    * {
      border: 1px solid red;
    }
    
    head {
      display: block;
    }
    

**思考**:為什麼 `<head>` 會顯示並有紅色邊框？

### 練習 3:比較瀏覽器

在 Chrome 和 Firefox 分別測試同一個按鈕的外觀差異。

* * *

延伸閱讀
----

*   [MDN - CSS Cascade](https://developer.mozilla.org/zh-TW/docs/Glossary/CSS) - 理解層疊原理 [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Glossary/CSS)
    
*   [A Modern CSS Reset (2024)](https://news.ycombinator.com/item?id=41943063) - 業界最新思維 [news.ycombinator](https://news.ycombinator.com/item?id=41943063)
    
*   [W3C HTML 預設樣式參考](http://www.w3.org/TR/CSS21/sample.html) - 官方建議的預設樣式 [blog.csdn](https://blog.csdn.net/WuLex/article/details/100153573)
    

* * *

總結
--

問題

答案

不同瀏覽器預設樣式有差異嗎？

有，但現代瀏覽器差異已很小 [dev](https://dev.to/hankchizljaw/a-modern-css-reset-6p3)

2026 年還需要 CSS Reset 嗎？

不是必須，但有討論仍建議使用輕量版 [news.ycombinator](https://news.ycombinator.com/item?id=41943063)

CSS 能覆蓋預設樣式，為何要 Reset？

只能覆蓋「有寫的屬性」，沒寫的仍有差異

Reset 會影響 `<head>` 嗎？

會選中，但不會改變其 `display: none` [stackoverflow](https://stackoverflow.com/questions/16130690/is-the-head-tag-a-regular-tag-that-is-not-displayed-by-default/16130724)

**核心原則**:從統一的起點開始，明確定義所有需要的樣式，不依賴瀏覽器預設值。

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day-52-53)*
