Day 52-53:verifyPassword 的第一個 Vitest 測試(續);HTML/CSS 基本架構

Share Dialog

今日閱讀:《單元測試的藝術》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

本質:

  • <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 檢查方式:

  1. 打開開發者工具(F12)

  2. 選取元素

  3. 找到標註「user agent stylesheet」的樣式 stackoverflow


CSS Reset 的必要性

2026 年的結論

不像以前那麼必要,但仍有實務價值 youtube

為什麼不像以前必要

  • 現代瀏覽器(Chrome、Firefox、Safari、Edge)預設樣式已非常接近

  • 不像 IE6 時代需要大量調整 dev

為什麼仍有開發者會使用

  1. 減少重複工作:避免每個專案都寫一次相同的覆寫

  2. 團隊一致性:確保所有開發者從相同基準線開始 mui

  3. 細微差異仍存在:表單元素在不同瀏覽器還是有差異 geeksforgeeks


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

  • 不想完全清空預設樣式

  • 希望保留瀏覽器有意義的預設值(如 <b> 的粗體)

選項 3:框架內建

Material UI、Bootstrap 等框架已內建 Reset,使用框架時不需額外引入 。 mui


Reset 對 <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.ycombinator

3. CSS Cascade 理解

  • 以為「自己的 CSS」會完全取代「瀏覽器預設」

  • 理解「只有寫到的屬性會覆蓋」

4. <head> 元素

  • 以為 <head> 有特殊的「永遠不顯示」機制

  • 理解它只是透過 display: none; 隱藏的普通元素 stackoverflow


檢驗練習

練習 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 分別測試同一個按鈕的外觀差異。


延伸閱讀


總結

問題

答案

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

有,但現代瀏覽器差異已很小 dev

2026 年還需要 CSS Reset 嗎?

不是必須,但有討論仍建議使用輕量版 news.ycombinator

CSS 能覆蓋預設樣式,為何要 Reset?

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

Reset 會影響 <head> 嗎?

會選中,但不會改變其 display: none stackoverflow

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