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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,24 @@ jobs:
run: npm ci

- name: Run unit tests (Node ${{ matrix.node-version }}, shard ${{ matrix.shard }}/2)
run: npx vitest run src/ --reporter=verbose --shard=${{ matrix.shard }}/2
run: npm test -- --reporter=verbose --shard=${{ matrix.shard }}/2

adapter-test:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run focused adapter tests
run: npm run test:adapter -- --reporter=verbose

# ── Smoke tests (scheduled / manual only) ──
smoke-test:
Expand Down
9 changes: 6 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ npm run build

# 4. Run a few checks
npx tsc --noEmit
npx vitest run src/
npm test
npm run test:adapter

# 5. Link globally (optional, for testing `opencli` command)
npm link
Expand Down Expand Up @@ -161,7 +162,8 @@ args: [
See [TESTING.md](./TESTING.md) for the full guide and exact test locations.

```bash
npx vitest run src/ # Unit tests
npm test # Core unit tests (non-adapter)
npm run test:adapter # Focused adapter tests: zhihu/twitter/reddit/bilibili
npx vitest run tests/e2e/ # E2E tests
npx vitest run # All tests
```
Expand Down Expand Up @@ -194,7 +196,8 @@ Common scopes: site name (`twitter`, `reddit`) or module name (`browser`, `pipel
3. Run the checks that apply:
```bash
npx tsc --noEmit # Type check
npx vitest run src/ # Unit tests
npm test # Core unit tests
npm run test:adapter # Focused adapter tests (if you touched adapter logic)
opencli validate # YAML validation (if applicable)
```
4. Commit using conventional commit format
Expand Down
6 changes: 4 additions & 2 deletions docs/developer/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ npm run build

# 4. Run a few checks
npx tsc --noEmit
npx vitest run src/
npm test
npm run test:adapter

# 5. Link globally (optional, for testing `opencli` command)
npm link
Expand Down Expand Up @@ -129,7 +130,8 @@ chore: bump vitest to v4
3. Run the checks:
```bash
npx tsc --noEmit # Type check
npx vitest run src/ # Unit tests
npm test # Core unit tests
npm run test:adapter # Focused adapter tests (if adapter logic changed)
opencli validate # YAML validation (if applicable)
```
4. Commit using conventional commit format
Expand Down
22 changes: 14 additions & 8 deletions docs/developer/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,28 @@ tests/
├── smoke/
│ └── api-health.test.ts # 外部 API、adapter 定义、命令注册健康检查
src/
└── **/*.test.ts # 单元测试(当前 31 个文件)
├── **/*.test.ts # 核心单元测试(默认 `unit` project)
└── clis/{zhihu,twitter,reddit,bilibili}/**/*.test.ts # 聚焦 adapter tests
```

| 层 | 位置 | 当前文件数 | 运行方式 | 用途 |
|---|---|---:|---|---|
| 单元测试 | `src/**/*.test.ts` | 31 | `npx vitest run src/` | 内部模块、pipeline、adapter 工具函数 |
| 单元测试 | `src/**/*.test.ts`(排除 `src/clis/**`) | - | `npm test` | 内部模块、pipeline、runtime |
| Adapter 测试 | `src/clis/{zhihu,twitter,reddit,bilibili}/**/*.test.ts` | - | `npm run test:adapter` | 保留 4 个重点站点的 adapter 覆盖 |
| E2E 测试 | `tests/e2e/*.test.ts` | 5 | `npx vitest run tests/e2e/` | 真实 CLI 命令执行 |
| 烟雾测试 | `tests/smoke/*.test.ts` | 1 | `npx vitest run tests/smoke/` | 外部 API 与注册完整性 |

---

## 当前覆盖范围

### 单元测试(31 个文件)
### 单元测试与 Adapter 测试

| 领域 | 文件 |
|---|---|
| 核心运行时与输出 | `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` |
| 聚焦 adapter 逻辑 | `src/clis/zhihu/download.test.ts`, `src/clis/twitter/timeline.test.ts`, `src/clis/reddit/read.test.ts`, `src/clis/bilibili/dynamic.test.ts` |

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

Expand Down Expand Up @@ -99,8 +101,11 @@ npm run build # 编译(E2E / smoke 测试需要 dist/main.js)
### 运行命令

```bash
# 全部单元测试
npx vitest run src/
# 默认核心单元测试(不含大多数 adapter tests)
npm test

# 聚焦 adapter tests(只保留 4 个重点站点)
npm run test:adapter

# 全部 E2E 测试(会真实调用外部 API / 浏览器)
npx vitest run tests/e2e/
Expand Down Expand Up @@ -192,7 +197,8 @@ it('producthunt me fails gracefully without login', async () => {
| Job | 触发条件 | 内容 |
|---|---|---|
| `build` | push/PR 到 `main`,`dev` | `tsc --noEmit` + `npm run build` |
| `unit-test` | push/PR 到 `main`,`dev` | Node `20` 与 `22` 双版本运行 `src/` 单元测试,按 `2` shard 并行 |
| `unit-test` | push/PR 到 `main`,`dev` | Node `20` 与 `22` 双版本运行核心 `unit` tests,按 `2` shard 并行 |
| `adapter-test` | push/PR 到 `main`,`dev` | Node `22` 运行聚焦的 `zhihu/twitter/reddit/bilibili` adapter tests |
| `smoke-test` | `schedule` 或 `workflow_dispatch` | 安装真实 Chrome,`xvfb-run` 执行 `tests/smoke/` |

### `e2e-headed.yml`
Expand All @@ -214,7 +220,7 @@ strategy:
node-version: ['20', '22']
shard: [1, 2]
steps:
- run: npx vitest run src/ --reporter=verbose --shard=${{ matrix.shard }}/2
- run: npm test -- --reporter=verbose --shard=${{ matrix.shard }}/2
```
:::

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"lint": "tsc --noEmit",
"prepublishOnly": "npm run build",
"test": "vitest run --project unit",
"test:adapter": "vitest run --project adapter",
"test:all": "vitest run",
"test:e2e": "vitest run --project e2e",
"docs:dev": "vitepress dev docs",
Expand Down
79 changes: 79 additions & 0 deletions src/clis/bilibili/dynamic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

const { mockApiGet } = vi.hoisted(() => ({
mockApiGet: vi.fn(),
}));

vi.mock('./utils.js', () => ({
apiGet: mockApiGet,
}));

import { getRegistry } from '../../registry.js';
import './dynamic.js';

describe('bilibili dynamic adapter', () => {
const command = getRegistry().get('bilibili/dynamic');

beforeEach(() => {
mockApiGet.mockReset();
});

it('maps desc text rows from the dynamic feed payload', async () => {
mockApiGet.mockResolvedValue({
data: {
items: [
{
id_str: '123',
modules: {
module_author: { name: 'Alice' },
module_dynamic: { desc: { text: 'hello world' } },
module_stat: { like: { count: 9 } },
},
},
],
},
});

const result = await command!.func!({} as any, { limit: 5 });

expect(mockApiGet).toHaveBeenCalledWith({}, '/x/polymer/web-dynamic/v1/feed/all', { params: {}, signed: false });
expect(result).toEqual([
{
id: '123',
author: 'Alice',
text: 'hello world',
likes: 9,
url: 'https://t.bilibili.com/123',
},
]);
});

it('falls back to archive title when desc text is absent', async () => {
mockApiGet.mockResolvedValue({
data: {
items: [
{
id_str: '456',
modules: {
module_author: { name: 'Bob' },
module_dynamic: { major: { archive: { title: 'Video title' } } },
module_stat: { like: { count: 3 } },
},
},
],
},
});

const result = await command!.func!({} as any, { limit: 5 });

expect(result).toEqual([
{
id: '456',
author: 'Bob',
text: 'Video title',
likes: 3,
url: 'https://t.bilibili.com/456',
},
]);
});
});
34 changes: 34 additions & 0 deletions src/clis/reddit/read.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it, vi } from 'vitest';
import { getRegistry } from '../../registry.js';
import './read.js';

describe('reddit read adapter', () => {
const command = getRegistry().get('reddit/read');

it('returns threaded rows from the browser-evaluated payload', async () => {
const page = {
goto: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn().mockResolvedValue([
{ type: 'POST', author: 'alice', score: 10, text: 'Title' },
{ type: 'L0', author: 'bob', score: 5, text: 'Comment' },
]),
} as any;

const result = await command!.func!(page, { 'post-id': 'abc123', limit: 5 });

expect(page.goto).toHaveBeenCalledWith('https://www.reddit.com');
expect(result).toEqual([
{ type: 'POST', author: 'alice', score: 10, text: 'Title' },
{ type: 'L0', author: 'bob', score: 5, text: 'Comment' },
]);
});

it('surfaces adapter-level API errors clearly', async () => {
const page = {
goto: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn().mockResolvedValue({ error: 'Reddit API returned HTTP 403' }),
} as any;

await expect(command!.func!(page, { 'post-id': 'abc123' })).rejects.toThrow('Reddit API returned HTTP 403');
});
});
17 changes: 16 additions & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,34 @@ export default defineConfig({
test: {
name: 'unit',
include: ['src/**/*.test.ts'],
exclude: ['src/clis/**/*.test.ts'],
// Run unit tests before e2e tests to avoid project-level contention in CI.
sequence: {
groupOrder: 0,
},
},
},
{
test: {
name: 'adapter',
include: [
'src/clis/zhihu/**/*.test.ts',
'src/clis/twitter/**/*.test.ts',
'src/clis/reddit/**/*.test.ts',
'src/clis/bilibili/**/*.test.ts',
],
sequence: {
groupOrder: 1,
},
},
},
{
test: {
name: 'e2e',
include: ['tests/**/*.test.ts'],
maxWorkers: 2,
sequence: {
groupOrder: 1,
groupOrder: 2,
},
},
},
Expand Down
Loading