> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/zh-tw/development/testing.md).

# 測試

Node.js 測試執行器設定，以及測試涵蓋與未涵蓋的內容。

MyIP 使用 **Node.js 內建測試執行器**。完全沒有 Jest、沒有 Vitest，也沒有任何測試框架相依性。

```bash
pnpm test     # node --test tests/*.test.js
```

每個規格都位於 `tests/`，採平面式命名為 `<subject>.test.js`。可組合的規格以前綴： `tests/composable-status-tone.test.js`, `tests/composable-refresh-orchestrator.test.js`.

在反覆調整時，要執行單一檔案：

```bash
node --test tests/guards.test.js
```

## 規格結構

`node:test` 用於結構， `node:assert/strict` 用於斷言。除了檔案所需之外，不使用自訂輔助函式：

{% code title="tests/composable-status-tone.test.js" %}

```js
// ipFieldTone 的測試——統一的「狀態字串 → 語氣」對應，
// WebRtcTest / DnsLeaksTest / RuleTest / ConnectivityTest 都使用。

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { ipFieldTone } from '../frontend/composables/use-status-tone.js';

describe('ipFieldTone()', () => {
  it('當值等於等待標籤時回傳 "wait"', () => {
    assert.equal(ipFieldTone('等待中', { waitLabels: '等待中', errorLabels: '錯誤' }), 'wait');
  });
});
```

{% endcode %}

和專案中的其他檔案一樣，每個規格都會以標頭註解開頭，說明其涵蓋內容。

## 哪些內容需要規格

{% hint style="success" %}
**任何可在不發出網路呼叫的情況下執行的非視覺邏輯，都會在同一次變更中附上規格。**
{% endhint %}

實務上：

* **純函式** ——驗證器、格式化器、剖析器。 `tests/valid-ip.test.js`, `tests/bgp-prefix.test.js`, `tests/mtr-parse.test.js`，以及兩個以表格驅動的 IP 計算器規格， `tests/ip-math.test.js` 以及 `tests/ip-calc.test.js`.
* **轉換** ——任何會重塑上游資料的東西。 `tests/transform-ip-data.test.js`, `tests/service-status-transform.test.js`.
* **具有可模擬輸入的 composables** ——可透過傳入值來驅動的邏輯。 `tests/composable-achievement-engine.test.js`, `tests/composable-info-mask.test.js`.
* **中介軟體** —— `tests/guards.test.js` 位於 `common/guards.js` 搭配 `(req, res, next)` 的 stub。
* **靜態資料檔案** ——結構與完整性。 `tests/changelog.test.js`, `tests/achievements.test.js`, `tests/sections.test.js`, `tests/ip-databases.test.js`.
* **API 處理器** ——僅做冒煙覆蓋，見下方。

當行為變動時，請更新受影響的規格 **並在同一次變更中**。不要延後。

### 橋接規格

當某個 helper 位於 `common/` 並透過 `frontend/utils/`重新匯出時，規格會匯入 **兩個路徑** 並斷言它們一致。 `tests/valid-ip.test.js` 會這麼做，這也正是阻止橋接層悄悄重新長出重複實作的方法。

對於會重新匯出整個模組的橋接層（`export * from '../../common/ip-math.js'`), `tests/ip-math.test.js` 還更進一步：它將兩者都以命名空間匯入，並迭代 `common`的每個匯出，斷言橋接層回傳的是相同的函式——如此一來，新增匯出就不會在前端端悄悄缺失。

## 哪些內容不需要規格

| 超出範圍    | 原因                                |
| ------- | --------------------------------- |
| Vue 渲染  | Node 執行器不會掛載任何東西——沒有 DOM，也沒有元件。   |
| 真實網路呼叫  | 任何規格都不得打到線上的上游服務。                 |
| 瀏覽器 API | WebRTC、 `navigator`、canvas 指紋識別等。 |

{% hint style="warning" %}
**視覺變更無法自行測試。** 如果你的變更屬於視覺層面，交付時請明確說明，並讓人用 `pnpm dev`。通過的 `pnpm check` 並不能證明 UI 的外觀如何。
{% endhint %}

## API 處理器的冒煙測試

位於 `api/` 的處理器，其冒煙覆蓋在 `tests/api-handlers.test.js`。目前大多數都已涵蓋；你新增或修改的處理器，會在同一次變更中帶上對應區塊。原則很窄也很嚴格：

{% hint style="danger" %}
**絕不打到真實上游。** 只針對會在 **之前** 第一個 `fetchUpstream` 呼叫前就回傳的分支做斷言。
{% endhint %}

因此只剩三種斷言：

* **方法門檻** —— `POST` 送到僅限 GET 的處理器會回傳 `405`.
* **參數分支** ——缺少或格式錯誤的輸入會回傳 `400`.
* **「API 金鑰缺失」的提早回傳** ——處理器會在呼叫外部之前直接中止。

這個檔案提供兩個所有處理器測試都會重用的 stub：

{% code title="tests/api-handlers.test.js" %}

```js
function createRequest(options = {}) {
    const method = options.method || 'GET';
    const query = options.query || {};
    const referer = Object.hasOwn(options, 'referer') ? options.referer : 'http://localhost/';
    const headers = {};
    if (referer !== undefined) headers.referer = referer;
    return { method, headers, query, body: options.body };
}

function createResponse() {
    return {
        statusCode: 200,
        body: undefined,
        status(code) { this.statusCode = code; return this; },
        json(payload) { this.body = payload; return this; },
        send(payload) { this.body = payload; return this; },
    };
}
```

{% endcode %}

完整的處理器區塊如下：

{% code title="tests/api-handlers.test.js" %}

```js
describe('mac-checker handler', () => {
    it('拒絕缺少 ?mac', async () => {
        const res = createResponse();
        await macCheckerHandler(createRequest(), res);
        assert.equal(res.statusCode, 400);
        assert.deepEqual(res.body, { error: '未提供 MAC 位址' });
    });

    it('拒絕無效的 MAC 格式', async () => {
        const res = createResponse();
        await macCheckerHandler(createRequest({ query: { mac: 'not-a-mac' } }), res);
        assert.equal(res.statusCode, 400);
        assert.deepEqual(res.body, { error: '無效的 MAC 位址' });
    });
});
```

{% endcode %}

### 環境變數

會切換環境變數的測試，會將其名稱註冊到 `ENV_KEYS` 檔案頂端的陣列中。 `beforeEach` 會備份這些鍵，並在 `afterEach` 復原它們，因此沒有規格會把狀態洩漏到下一個。

### 不要重複中介軟體

Referer 檢查與參數驗證是由中介軟體而不是處理器強制執行的。它們只會在 `tests/guards.test.js`。處理器規格不應再次斷言「拒絕錯誤網域」——因為處理器根本不會看到它。

慣例是把這點寫在上方註解中，位於 `describe` 區塊上方，就像 OONI 處理器的區塊那樣：

```js
// 網域是否存在／格式是否正確由 requireValidDomain 中介軟體強制執行
//（tests/guards.test.js）；處理器唯一的 pre-fetch 分支是
// 防禦性的 method 門檻。
```

### 防禦性的 method 門檻維持

有些處理器保留一個 `req.method !== 'GET'` 檢查，即使路由本身已經限制方法。這些門檻之所以存在，是因為冒煙測試會直接對它們斷言。請保留它們。

## 值得知道的相關規格

| 規格                                                              | 涵蓋內容                                                                                                                                                                                              |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tests/guards.test.js`                                          | 涵蓋 `common/guards.js`                                                                                                                                                                             |
| `tests/fetch-with-timeout.test.js`                              | 上游逾時與中止行為                                                                                                                                                                                         |
| `tests/locale-packs.test.js`                                    | 翻譯門檻：註冊表與套件檔案一致，每個套件都恰好包含 `en`的鍵，佔位符是英文鍵的子集，而且 `完整的` 語系不會留下任何未翻譯內容——見 [i18n](/developer/zh-tw/development/i18n.md)                                                                                |
| `tests/locale-registry.test.js` / `tests/locale-pack.test.js`   | 註冊表的項目結構與衍生對應； `""`「-」代表未翻譯的慣例及其建置時剝除                                                                                                                                                             |
| `tests/index-html-i18n.test.js`                                 | `index.html`手動維護的內嵌文案——開機畫面俏皮語、JSON-LD、語言選擇器——與註冊表一致                                                                                                                                              |
| `tests/i18n-scaffold.test.js`                                   | 以下 `pnpm i18n-new` / `pnpm i18n-sync` 腳手架腳本                                                                                                                                                       |
| `tests/changelog.test.js`                                       | 變更記錄的結構，以及每個 `完整的` 語系的翻譯——見 [i18n](/developer/zh-tw/development/i18n.md)                                                                                                                          |
| `tests/report-schema.test.js` / `tests/report-builders.test.js` | 可分享的診斷報告管線                                                                                                                                                                                        |
| `tests/app-commands.test.js`                                    | 命令匯流排：註冊、派送、逾時，以及 reject-code 合約                                                                                                                                                                  |
| `tests/ip-math.test.js`                                         | `common/ip-math.js`：嚴格的 IPv4 / IPv6 剖析、RFC 5952 格式化、遮罩與計數（包括 RFC 3021 的 `/31` 情況）、包含關係、拆分／彙整／range-to-CIDR——以及上方的橋接同一性檢查                                                                          |
| `tests/ip-calc.test.js`                                         | `frontend/utils/ip-calc.js`：分類器的規則順序與 `原因` 代碼、IANA 特殊用途表（每一列都能剖析、對齊、引用 RFC，且具有唯一 id）、IPv6 解碼器（內嵌 IPv4、Teredo、EUI-64、multicast、ULA）、PTR 名稱、混淆與內嵌格式、計數格式化，以及 `calculate()` 絕不拋出例外。MAC 輸入會在 *缺失時被斷言* |
| `tests/connectivity-lists.test.js`                              | Connectivity 區段的多清單模型（`frontend/utils/connectivity-lists.js`）：開機時的清理與從舊式 flat-target 鍵的遷移、清單 CRUD 防護，以及精選匯入規劃                                                                                     |
| `tests/connectivity-import-lists.test.js`                       | 精選的 Connectivity 匯入清單資料完整性：項目結構、僅限 HTTPS 的 URL、清單名稱的語系覆蓋，以及每個成員都有提交的 favicon PNG。缺少的圖示會在本機自動透過 `scripts/fetch-favicons.js`；CI 會保持離線，並只會告訴你執行 `pnpm fetch-favicons` 並提交這些 PNG                      |
| `tests/banners.test.js`                                         | 區段橫幅 helpers，以及對位於下列路徑中的任何部署時資料檔案進行合約驗證： `frontend/data/banners/` ——當被 gitignore 的目錄為空時，會因空泛成立而呈綠色。參見 [區段橫幅](/developer/zh-tw/configuration/section-banners.md)                                   |
| `tests/persona-i18n.test.js`                                    | 在下列檔案中宣告的每一個 In-depth Persona Check id、reason 與 detail 鍵 `frontend/utils/persona/check-ids.js` 都在每個 `完整的` 套件                                                                                      |

## `pnpm check` 中有對應文案，且必須呈綠燈

```bash
pnpm check     # pnpm test && pnpm build
```

測試加上真正的正式版建置。每次交付前都要執行——不論是 PR、commit，還是審查請求。

CI 會執行相同的兩個步驟（`pnpm test`，接著 `pnpm run build`）在對 `main` 以及 `dev`，在 Node 24 上，並使用 `pnpm install --frozen-lockfile`。本機若呈綠燈 `check` 通常代表 CI 也會呈綠燈。

## 接下來

* [編碼慣例](/developer/zh-tw/development/coding-conventions.md) ——規格據以檢查的規則。
* [新增工具](/developer/zh-tw/development/adding-a-new-tool.md) ——測試在完整功能中的位置。
* [後端](/developer/zh-tw/architecture/backend.md) ——處理器與中介軟體設計。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/zh-tw/development/testing.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
