Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 87 additions & 69 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,57 +18,72 @@

测试分为三层,全部使用 **vitest** 运行:

```
```text
tests/
├── e2e/ # E2E 集成测试(子进程运行真实 CLI)
│ ├── helpers.ts # runCli() 共享工具
│ ├── public-commands.test.ts # 公开 API 命令(无需浏览器)
│ ├── helpers.ts # runCli() / parseJsonOutput() 共享工具
│ ├── public-commands.test.ts # 公开 API 命令
│ ├── browser-public.test.ts # 浏览器命令(公开数据)
│ ├── browser-auth.test.ts # 需登录命令(graceful failure 测试
│ ├── management.test.ts # 管理命令(list, validate, verify, help)
│ └── output-formats.test.ts # 输出格式(json/yaml/csv/md)
├── smoke/ # 烟雾测试(仅定时 / 手动触发)
│ └── api-health.test.ts # 外部 API 可用性检测
│ ├── browser-auth.test.ts # 需登录命令(graceful failure)
│ ├── management.test.ts # 管理命令(list / validate / verify / help)
│ └── output-formats.test.ts # 输出格式校验
├── smoke/
│ └── api-health.test.ts # 外部 API、adapter 定义、命令注册健康检查
src/
── *.test.ts # 单元测试(已有 8 个
── **/*.test.ts # 单元测试(当前 31 个文件
```

| 层 | 位置 | 运行方式 | 用途 |
|---|---|---|---|
| 单元测试 | `src/**/*.test.ts` | `npx vitest run src/` | 内部模块逻辑 |
| E2E 测试 | `tests/e2e/*.test.ts` | `npx vitest run tests/e2e/` | 真实 CLI 命令执行 |
| 烟雾测试 | `tests/smoke/*.test.ts` | `npx vitest run tests/smoke/` | 外部 API 健康 |
| 层 | 位置 | 当前文件数 | 运行方式 | 用途 |
|---|---|---:|---|---|
| 单元测试 | `src/**/*.test.ts` | 31 | `npx vitest run src/` | 内部模块、pipeline、adapter 工具函数 |
| E2E 测试 | `tests/e2e/*.test.ts` | 5 | `npx vitest run tests/e2e/` | 真实 CLI 命令执行 |
| 烟雾测试 | `tests/smoke/*.test.ts` | 1 | `npx vitest run tests/smoke/` | 外部 API 与注册完整性 |

---

## 当前覆盖范围

### 单元测试(8 个文件)
### 单元测试(31 个文件)

| 文件 | 覆盖内容 |
| 领域 | 文件 |
|---|---|
| `browser.test.ts` | JSON-RPC、tab 管理、extension/standalone 模式切换 |
| `engine.test.ts` | 命令发现与执行 |
| `registry.test.ts` | 命令注册与策略分配 |
| `output.test.ts` | 输出格式渲染 |
| `doctor.test.ts` | Token 诊断 |
| `coupang.test.ts` | 数据归一化 |
| `pipeline/template.test.ts` | 模板表达式求值 |
| `pipeline/transform.test.ts` | 数据变换步骤 |

### E2E 测试(~52 个用例)

| 文件 | 覆盖站点/功能 | 测试数 |
|---|---|---|
| `public-commands.test.ts` | hackernews/top, v2ex/hot, v2ex/latest, v2ex/topic | 5 |
| `browser-public.test.ts` | bbc, bilibili×3, weibo, zhihu×2, reddit×2, twitter, xueqiu×2, reuters, youtube, smzdm, boss, ctrip, coupang, xiaohongshu, yahoo-finance, v2ex/daily | 21 |
| `browser-auth.test.ts` | bilibili/me,dynamic,favorite,history,following + twitter/bookmarks,timeline,notifications + v2ex/me,notifications + xueqiu/feed,watchlist + xiaohongshu/feed,notifications | 14 |
| `management.test.ts` | list×5 格式, validate×3 级别, verify, --version, --help, unknown cmd | 12 |
| `output-formats.test.ts` | json, yaml, csv, md 格式验证 | 5 |
| 核心运行时与输出 | `src/browser.test.ts`, `src/browser/dom-snapshot.test.ts`, `src/build-manifest.test.ts`, `src/capabilityRouting.test.ts`, `src/doctor.test.ts`, `src/engine.test.ts`, `src/interceptor.test.ts`, `src/output.test.ts`, `src/plugin.test.ts`, `src/registry.test.ts`, `src/snapshotFormatter.test.ts` |
| pipeline 与下载 | `src/download/index.test.ts`, `src/pipeline/executor.test.ts`, `src/pipeline/template.test.ts`, `src/pipeline/transform.test.ts` |
| 站点 / adapter 逻辑 | `src/clis/apple-podcasts/commands.test.ts`, `src/clis/apple-podcasts/utils.test.ts`, `src/clis/bloomberg/utils.test.ts`, `src/clis/chaoxing/utils.test.ts`, `src/clis/coupang/utils.test.ts`, `src/clis/google/utils.test.ts`, `src/clis/grok/ask.test.ts`, `src/clis/twitter/timeline.test.ts`, `src/clis/weread/utils.test.ts`, `src/clis/xiaohongshu/creator-note-detail.test.ts`, `src/clis/xiaohongshu/creator-notes-summary.test.ts`, `src/clis/xiaohongshu/creator-notes.test.ts`, `src/clis/xiaohongshu/user-helpers.test.ts`, `src/clis/xiaoyuzhou/utils.test.ts`, `src/clis/youtube/transcript-group.test.ts`, `src/clis/zhihu/download.test.ts` |

这些测试覆盖的重点包括:

- Browser Bridge、DOM snapshot、interceptor、capability routing
- manifest 生成、命令发现、插件安装与注册表
- 输出格式渲染与 snapshot formatting
- pipeline 模板求值、执行器与变换步骤
- 各站点 adapter 的数据归一化、参数处理与容错逻辑

### E2E 测试(5 个文件)

| 文件 | 当前覆盖范围 |
|---|---|
| `tests/e2e/public-commands.test.ts` | `bloomberg`、`apple-podcasts`、`hackernews`、`v2ex`、`xiaoyuzhou`、`google suggest` 等公开命令 |
| `tests/e2e/browser-public.test.ts` | `bbc`、`bloomberg`、`bilibili`、`weibo`、`zhihu`、`reddit`、`twitter`、`xueqiu`、`reuters`、`youtube`、`smzdm`、`boss`、`ctrip`、`coupang`、`xiaohongshu`、`google`、`yahoo-finance`、`v2ex daily` |
| `tests/e2e/browser-auth.test.ts` | `bilibili`、`twitter`、`v2ex`、`xueqiu`、`linux-do`、`xiaohongshu` 的需登录命令 graceful failure |
| `tests/e2e/management.test.ts` | `list`、`validate`、`verify`、`--version`、`--help`、unknown command |
| `tests/e2e/output-formats.test.ts` | `json` / `yaml` / `csv` / `md` 输出格式校验 |

### 烟雾测试(1 个文件)

| 文件 | 当前覆盖范围 |
|---|---|
| `tests/smoke/api-health.test.ts` | `hackernews`、`v2ex` 公开 API 可用性,`validate` 全量 adapter 校验,以及命令注册表基础完整性 |

### 烟雾测试
### 快速核对命令

公开 API 可用性(hackernews, v2ex×2, v2ex/topic)+ 全站点注册完整性检查。
需要刷新测试清单时,直接以仓库文件为准:

```bash
find src -name '*.test.ts' | sort
find tests/e2e -name '*.test.ts' | sort
find tests/smoke -name '*.test.ts' | sort
```

---

Expand All @@ -78,7 +93,7 @@ src/

```bash
npm ci # 安装依赖
npm run build # 编译(E2E 测试需要 dist/main.js)
npm run build # 编译(E2E / smoke 测试需要 dist/main.js)
```

### 运行命令
Expand All @@ -87,37 +102,39 @@ npm run build # 编译(E2E 测试需要 dist/main.js)
# 全部单元测试
npx vitest run src/

# 全部 E2E 测试(会真实调用外部 API)
# 全部 E2E 测试(会真实调用外部 API / 浏览器
npx vitest run tests/e2e/

# 全部 smoke 测试
npx vitest run tests/smoke/

# 单个测试文件
npx vitest run src/clis/apple-podcasts/commands.test.ts
npx vitest run tests/e2e/management.test.ts

# 全部测试(单元 + E2E)
# 全部测试
npx vitest run

# 烟雾测试
npx vitest run tests/smoke/

# watch 模式(开发时推荐)
npx vitest src/
```

### 浏览器命令本地测试须知

- opencli 通过 Browser Bridge 扩展连接已运行的 Chrome 浏览器
- `browser-public.test.ts` 使用 `tryBrowserCommand()`,站点反爬导致空数据时 warn + pass
- `browser-auth.test.ts` 验证 **graceful failure**(不 crash 不 hang 即通过)
- 如需测试完整登录态,保持 Chrome 登录态并安装 Browser Bridge 扩展,手动跑对应测试
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/main.js`
- `browser-public.test.ts` 使用 `tryBrowserCommand()`,站点反爬或地域限制导致空数据时会 warn + pass
- `browser-auth.test.ts` 验证 **graceful failure**,重点是不 crash、不 hang、错误信息可控
- 如需测试完整登录态,保持 Chrome 登录态并安装 Browser Bridge 扩展,再手动运行对应测试

---

## 如何添加新测试

### 新增 YAML Adapter(如 `src/clis/producthunt/trending.yaml`)

1. **无需额外操作**:`validate` 测试会自动覆盖 YAML 结构验证
2. 根据 adapter 类型,在对应文件加一个 `it()` block
1. `opencli validate` 的 E2E / smoke 测试会覆盖 adapter 结构校验
2. 根据 adapter 类型,在对应测试文件补一个 `it()` block

```typescript
// 如果 browser: false(公开 API)→ tests/e2e/public-commands.test.ts
Expand Down Expand Up @@ -148,15 +165,15 @@ it('producthunt me fails gracefully without login', async () => {

### 新增管理命令(如 `opencli export`)

在 `tests/e2e/management.test.ts` 添加测试。
在 `tests/e2e/management.test.ts` 添加测试;如果新命令会影响输出格式,也同步补 `tests/e2e/output-formats.test.ts`

### 新增内部模块

在 `src/` 下对应位置创建 `*.test.ts`。
在对应源码旁创建 `*.test.ts`,优先和被测模块放在同一目录下,便于发现与维护

### 决策流程图

```
```text
新增功能 → 是内部模块? → 是 → src/ 下加 *.test.ts
↓ 否
是 CLI 命令? → browser: false? → tests/e2e/public-commands.test.ts
Expand All @@ -170,32 +187,33 @@ it('producthunt me fails gracefully without login', async () => {

## CI/CD 流水线

### ci.yml(主流水线)
### `ci.yml`

| Job | 触发条件 | 内容 |
|---|---|---|
| **build** | push/PR to main,dev | typecheck + build |
| **unit-test** | push/PR to main,dev | 单元测试,2 shard 并行 |
| **smoke-test** | 每周一 08:00 UTC / 手动 | xvfb + real Chrome,外部 API 健康检查 |
| `build` | push/PR 到 `main`,`dev` | `tsc --noEmit` + `npm run build` |
| `unit-test` | push/PR 到 `main`,`dev` | Node `20` 与 `22` 双版本运行 `src/` 单元测试,按 `2` shard 并行 |
| `smoke-test` | `schedule` 或 `workflow_dispatch` | 安装真实 Chrome,`xvfb-run` 执行 `tests/smoke/` |

### e2e-headed.yml(E2E 测试)
### `e2e-headed.yml`

| Job | 触发条件 | 内容 |
|---|---|---|
| **e2e-headed** | push/PR to main,dev | xvfb + real Chrome,全部 E2E 测试 |
| `e2e-headed` | push/PR 到 `main`,`dev`,或手动触发 | 安装真实 Chrome,`xvfb-run` 执行 `tests/e2e/` |

E2E 使用 `browser-actions/setup-chrome` 安装真实 Chrome,配合 `xvfb-run` 提供虚拟显示器,以 headed 模式运行浏览器
E2E 与 smoke 都使用 `./.github/actions/setup-chrome` 准备真实 Chrome,并通过 `OPENCLI_BROWSER_EXECUTABLE_PATH` 注入浏览器路径

### Sharding

单元测试使用 vitest 内置 shard:
单元测试使用 vitest 内置 shard,并在 Node `20` / `22` 两个版本上运行

```yaml
strategy:
matrix:
node-version: ['20', '22']
shard: [1, 2]
steps:
- run: npx vitest run src/ --shard=${{ matrix.shard }}/2
- run: npx vitest run src/ --reporter=verbose --shard=${{ matrix.shard }}/2
```

---
Expand All @@ -206,8 +224,8 @@ opencli 通过 Browser Bridge 扩展连接浏览器:

| 条件 | 模式 | 使用场景 |
|---|---|---|
| 扩展已安装 | Extension 模式 | 本地用户,连接已登录的 Chrome |
| 扩展未安装 | CLI 报错提示安装 | 需要安装 Browser Bridge 扩展 |
| 扩展已安装 / 已连接 | Extension 模式 | 本地用户,连接已登录的 Chrome |
| 无扩展 token | CLI 自行拉起浏览器 | CI、无登录态或纯自动化场景 |

CI 中使用 `OPENCLI_BROWSER_EXECUTABLE_PATH` 指定真实 Chrome 路径:

Expand All @@ -220,14 +238,14 @@ env:

## 站点兼容性

GitHub Actions 美国 runner 上,部分站点因地域限制或登录要求返回空数据。E2E 测试对这些站点使用 warn + pass 策略,不影响 CI 绿灯
GitHub Actions 的美国 runner 上,部分站点会因为地域限制、登录要求或反爬而返回空数据。当前 E2E 对这些场景采用 warn + pass 策略,避免偶发站点限制把整条 CI 打红

| 站点 | CI 状态 | 限制原因 |
| 站点 | CI 表现 | 常见原因 |
|---|---|---|
| hackernews, bbc, v2ex | ✅ 返回数据 | 无限制 |
| yahoo-finance | ✅ 返回数据 | 无限制 |
| bilibili, zhihu, weibo, xiaohongshu | ⚠️ 空数据 | 地域限制(中国站点) |
| reddit, twitter, youtube | ⚠️ 空数据 | 需登录或 cookie |
| smzdm, boss, ctrip, coupang, xueqiu | ⚠️ 空数据 | 地域限制 / 需登录 |
| `hackernews`、`bbc`、`v2ex`、`bloomberg` | 通常返回数据 | 公开接口或公开页面 |
| `yahoo-finance`、`google` | 通常返回数据 | 页面公开,但仍可能受限流影响 |
| `bilibili`、`zhihu`、`weibo`、`xiaohongshu`、`xueqiu` | 容易空数据 | 地域限制、反爬、登录要求 |
| `reddit`、`twitter`、`youtube` | 容易空数据 | 登录态、cookie、机器人检测 |
| `smzdm`、`boss`、`ctrip`、`coupang`、`linux-do` | 结果波动较大 | 地域限制、风控或页面结构变动 |

> 使用 self-hosted runner(国内服务器)可解决地域限制问题
> 如果需要更稳定的浏览器 E2E 结果,优先使用具备目标站点网络可达性的 self-hosted runner。
Loading
Loading