# 快照測試慣例 / Snapshot Testing Convention

---

## 0. 與傳統斷言的比較

| 面向 | 傳統斷言 | 文件化快照 |
|------|---------|-----------|
| 主要目標 | 驗證正確性 | 行為展示 + 驗證 |
| 可讀性 | 取決於測試名稱 | 快照本身即文件 |
| 維護成本 | 低 | 中（需更新說明） |
| 適用場景 | 單元測試 | API 範例、教學 |

---

## 1. 快照物件結構

```typescript
expect({
    explain: '...',                   // 測試說明（必填）
    tags?: 'tag1, tag2, ...',          // 分類標籤（可選，有特別意義或需檢索時使用）
    result: { ... },                   // 輸出結果（必填）
    input?: {                          // 輸入資料（可選，簡單時保留）
        target: { ... },
        source: { ... },
    },
}).toMatchSnapshot()
```

### 必填欄位

| 欄位 | 類型 | 說明 |
|------|------|------|
| `explain` | `string` | 測試說明（必填，作為標題/描述/邏輯解釋，單行或多行皆可） |
| `result` | `object` | 函式輸出結果（必填，始終保留） |

### 可選欄位

| 欄位 | 類型 | 說明 |
|------|------|------|
| `tags` | `string` | 分類標籤（以 `, ` 分隔），只有當測試項目有特別意義或需要被檢索時才需要加上 |
| `input` | `object` | 輸入資料（來自函式參數，簡單時保留） |

---

## 2. 欄位規範

### 2.1 explain - 測試說明

- **必填**：每個快照至少需要 `explain`
- **用途**：幫助快速理解快照的內容與邏輯
- **類型**：
  - **標題式**：簡短說明測試目的
  - **描述式**：描述預期行為
  - **邏輯式**：解釋合併邏輯
  - **複合式**：結合以上，以快速理解為主
- **格式**：雙語（繁中 + 英文）或純英文
- **長度**：可以是**單行**或**多行**文字，視說明需求而定
- **擴展用途**：可以補充說明 `result` 中各欄位的來源或意義，幫助理解輸出結構

```typescript
// 短句範例
explain: '✅ 新增鍵 / Add keys'

// 複雜範例：說明 result 中各欄位的來源
explain: '✅ 多個物件合併（不同類型）：所有屬性被合併

result 中
- first: 來自第一個物件
- second: 來自第二個物件
- third: 來自第三個物件
- fourth: 來自第四個物件'
```

### 2.2 tags - 分類標籤

- **格式**：以 `, ` （逗號 + 空格）分隔的多重標籤
- **語法**：字串形式 `tag1, tag2, tag3`（非陣列）
- **用途**：人類可讀的分類識別
- **使用時機**：只有當測試項目有特別意義或需要被檢索時，才需要加上 `tags`

```typescript
// ✅ 正確
tags: 'basic, root-level, keys'

// ❌ 錯誤（陣列形式）
tags: ['basic', 'root-level']
```

### 2.3 input - 輸入資料

- **來源**：來自函式參數，不一定是 `target` / `source`
- **結構**：根據函式簽章決定（如 `{ array: [...] }`、`{ options: {} }`）
- **判定邏輯**：

```
input 複雜度判定
    │
    ▼
屬性數量 ≤ 3 且 無深度嵌套？
    │
    ├─ 是 → 保留 input
    │
    └─ 否 → 移除 input（依賴 result 推導）
```

```typescript
// ✅ 簡單案例：保留 input（來自函式參數）
input: { array: [1, 2, 3] }
input: { target: { a: 1 }, source: { b: 2 } }

// ✅ 複雜案例：移除 input
// （依賴快照差異顯示輸入輸出變化）
```

### 2.4 result - 輸出結果

- **必填**：始終保留
- **用途**：斷言驗證 + 行為展示

---

## 3. tags 命名慣例

### 3.1 常用標籤分類

| 類別 | 標籤範例 | 說明 |
|------|---------|------|
| **測試類型** | `basic`, `advanced`, `edge-case` | 測試複雜度 |
| **資料結構** | `object`, `array`, `nested`, `deep` | 輸入資料類型 |
| **功能特性** | `clone`, `merge`, `replace` | 合併行為 |
| **應用場景** | `config`, `preferences`, `state` | 實際應用 |
| **對照組** | `good-example`, `bad-example` | 展示正確/錯誤用法 |

### 3.2 命名格式

- 使用 **小寫字母**
- 使用 `-` 或 `_` 分隔詞彙：`nested-object`, `deep_merge`
- 避免中文標籤

```typescript
// ✅ 正確
tags: 'basic, nested-object, merge'

// ❌ 錯誤
tags: '基礎, 巢狀, 合併'
```

---

## 4. 範例

### 4.1 簡單案例

```typescript
it('should add keys to empty target', () => {
    const target = {};
    const source = { key1: 'value1', key2: 'value2' };

    const result = merge(target, source);

    expect({
        explain: '✅ 新增至空物件 / Add to empty object',
        tags: 'basic, root-level, keys',
        input: { target, source },  // 來自函式參數
        result,
    }).toMatchSnapshot();
});
```

### 4.2 deepmergeAll 案例

```typescript
it('should merge array of objects', () => {
    const input = [{ a: 1 }, { b: 2 }];
    const result = deepmergeAll(input);

    expect({
        explain: '合併物件陣列',
        input: { input },  // 參數名稱為 input
        result,
    }).toMatchSnapshot();
});
```

### 4.2 複雜案例

```typescript
it('should deeply merge nested objects', () => {
    const target = {
        user: {
            name: 'Alice',
            settings: { theme: 'dark' }
        }
    };
    const source = {
        user: {
            age: 30,
            settings: { language: 'en' }
        }
    };

    const result = merge(target, source);

    expect({
        explain: 'Deep nested merge preserves nested structure',
        tags: 'nested, deep, object-merge',
        result,
    }).toMatchSnapshot();
});
```

### 4.3 比較測試（對照組）

當需要比較 deepmerge 與其他函式庫（如 lodash）的行為差異時使用。

```typescript
it('should match lodash _.assign behavior', () => {
    const target = { a: 1, b: 2 };
    const source = { b: 3, c: 4 };

    // deepmerge 結果
    const resultDeepmerge = deepmerge(target, source);

    // lodash 結果
    const resultLodash = _.assign({}, target, source);

    expect({
        explain: `deepmerge 模擬 _.assign：
        - target: { a: 1, b: 2 }
        - source: { b: 3, c: 4 }
        - 結果：b 被 source 覆蓋`,
        result: {
            resultDeepmerge,
            resultLodash,
            isEqual: resultDeepmerge.a === resultLodash.a &&
                     resultDeepmerge.b === resultLodash.b &&
                     resultDeepmerge.c === resultLodash.c
        },
    }).toMatchSnapshot();
});
```

**快照結構**:

```json
{
    "explain": "deepmerge 模擬 _.assign：...",
    "result": {
        "resultDeepmerge": { "a": 1, "b": 3, "c": 4 },
        "resultLodash": { "a": 1, "b": 3, "c": 4 },
        "isEqual": true
    }
}
```

**說明**:
- 使用 `resultDeepmerge` 和 `resultLodash` 兩個鍵名（而非 `deepmergeResult` / `lodashResult`）
- 最後一個鍵為 `isEqual` 用於快速識別結果是否相同
- explain 使用模板字串（反引號）進行多行說明

---

## 5. 相關資源

- [Jest Snapshot Testing](https://jestjs.io/docs/snapshot-testing)
- [deepmerge 測試檔案](./test/)

---

## 6. 核心規範速查

### 最小結構

```typescript
expect({
    explain: '說明 / 描述 / 邏輯解釋',  // 必填（標題/描述/邏輯/複合式）
    result: { /* ... */ },             // 必填（始終保留）
}).toMatchSnapshot()
```

### 完整結構

```typescript
expect({
    explain: '說明 / Description',     // 必填
    tags?: 'tag1, tag2, tag3',         // 可選
    result: { /* ... */ },             // 必填
    input?: { target, source },       // 可選
}).toMatchSnapshot()
```
