{Module} 模块 E2E 测试覆盖
模块概述
路由: /module, /module/[id]
功能清单与测试覆盖
1. 功能分组名称
| 功能点 | 描述 | 优先级 | 状态 | 测试文件 |
|---|---|---|---|---|
| 功能A | xxx | P0 | ✅ | xxx.feature |
| 功能B | xxx | P1 | ⏳ |
测试文件结构
测试执行
已知问题
更新记录
其中状态图例约定:✅ 已完成、⏳ 待实现、🚧 进行中。
### 阶段 C:功能探索与场景设计(Step 4–5)
**Step 4:探索模块功能**。手册建议用 Task/Explore 子代理深入模块代码,覆盖四个探查面:
- 路由结构:`src/app/[variants]/(main)/{module}/`
- 功能组件:`src/features/`
- Store actions:`src/store/{module}/`
- 所有用户交互(按钮、菜单、表单)
然后按用户旅程区域(如 Sidebar、Editor Header、Editor Content 等)把全部功能点登记进 README。
**Step 5:按功能区域编写 Feature 文件**。文件位于 `e2e/src/features/{module}/{area}.feature`,命名约定示例:`crud.feature`(基础 CRUD)、`editor-meta.feature`(标题/图标等元数据)、`editor-content.feature`(富文本)、`copilot.feature`(AI Copilot 交互)。
Feature 文件模板(BDD 三段式,中文描述贴近产品需求):
```gherkin
@journey @P0 @{module-tag}
Feature: {Feature Name in Chinese}
作为用户,我希望能够 {user goal},
以便 {business value}
Background:
Given 用户已登录系统
# ============================================
# 功能分组注释
# ============================================
@{MODULE-AREA-001}
Scenario: {Scenario description in Chinese}
Given {precondition}
When {user action}
Then {expected outcome}
And {additional verification}
仓库内的真实实现可对照 e2e/src/features/page/editor-meta.feature:它声明了 @journey @P0 @page 模块标签,用 6 个场景覆盖标题编辑(含回车保存、点击外部保存、清空显示占位符)与 Emoji 图标的添加/更换/删除,每个场景都带 @PAGE-TITLE-xxx / @PAGE-EMOJI-xxx 的用例级唯一标签,便于单独回放调试。
标签约定是全套体系的核心约定,分为四类:
# 测试类型
@journey # 用户旅程测试(体验基准线)
@smoke # 冒烟测试(快速验证)
@regression # 回归测试
@skip # 跳过此测试(已知问题)
# 优先级
@P0 # 最高优先级(CI 必跑)
@P1 # 高优先级(Nightly)
@P2 # 中优先级(发版前)
# 模块
@agent # Agent 模块
@agent-group # Agent Group 模块
@page # Page/Docs 模块
@knowledge # 知识库模块
@memory # Memory 模块
@settings # Settings 模块
@home # Home sidebar 模块
阶段 D:Step 定义与 hooks 扩展(Step 6)
Step 6:实现 Step Definitions,文件位于 e2e/src/steps/{module}/{area}.steps.ts。模板示意如下:
import { Given, When, Then } from '@cucumber/cucumber';
import { CustomWorld } from '../../support/world';
// ============================================
// Given / When / Then Steps
// ============================================
Given('用户打开一个文稿编辑器', async function (this: CustomWorld) {
console.log(' 📍 Step: 创建并打开一个文稿...');
// Implementation
});
When('用户点击标题输入框', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击标题输入框...');
// Implementation
});
Then('文稿标题应该更新为 {string}', async function (this: CustomWorld, title: string) {
console.log(` 📍 Step: 验证标题为 "${title}"...`);
// Assertions
});
注意几个实现细节:
- 步骤方法通过
async function (this: CustomWorld)拿到共享的page、browserContext与断言上下文。CustomWorld在 e2e/src/support/world.ts 中定义:默认 viewport 为 1280×720,浏览器按HEADLESS !== 'false'决定是否显示,expect断言超时与页面默认超时统一为 30 秒,还封装了modKey(macOS 返回Meta,其余平台返回Control)用于跨平台快捷键测试,以及把全页截图写入screenshots/的takeScreenshot(name)。 - 每个关键步骤写
console.log便于定位(手册明确要求 DO add console logs in step definitions for debugging)。
如果新增了用例级标签前缀,需要同步更新 e2e/src/steps/hooks.ts 中识别测试 ID 的列表。该文件目前识别的前缀为 @COMMUNITY-、@AGENT-、@HOME-、@OIDC-、@PAGE-、@ROUTES-:
const testId = pickle.tags.find(
(tag) =>
tag.name.startsWith('@COMMUNITY-') ||
tag.name.startsWith('@AGENT-') ||
tag.name.startsWith('@HOME-') ||
tag.name.startsWith('@PAGE-') || // 新模块在此追加
tag.name.startsWith('@ROUTES-'),
);
hooks 文件还承担了三个关键职责,值得作为样板复用:
- 登录会话缓存:在
BeforeAll中通过/api/auth/sign-in/email完成一次 API 登录,把 session cookies 缓存后写入每个新 browser context,从而让所有Given 用户已登录系统免去重复走登录 UI; - Community 模块 API Mock:带
@community标签的场景在导航前通过mockManager.setup(page)挂载列表/搜索/详情 fixtures,原因是线上市场会限流匿名的 CI 流量,而 E2E 目标是「Community UI 流程的用户体验基准线」而非真实市场可用性探测; - 失败即取证:在
After中对FAILED场景自动保存全页截图(screenshots/)并 attach HTML 页面与收集到的 JS 错误,方便快速定位。
阶段 E:Mock 与运行验证(Step 7–8)
Step 7:为 LLM 相关测试配置 Mock。所有依赖真实 LLM API 的用例都必须使用 Mock 框架,禁止走真实请求。基础用法:
import { llmMockManager, presetResponses } from '../../mocks/llm';
// 在页面导航之前设置
llmMockManager.setResponse('user message', 'Expected AI response');
await llmMockManager.setup(this.page);
LLM Mock 的源码细节与 SSE 格式见本文第六节。
Step 8:运行与验证测试。先启动本地环境,再做 dry-run 校验,最后正式运行。
启动本地环境(从项目根目录执行):
# 仅设置数据库(启动 PostgreSQL + 运行迁移)
bun e2e/scripts/setup.ts
# 设置数据库并启动服务器
bun e2e/scripts/setup.ts --start
# 完整设置(数据库 + 构建 + 启动服务器)
bun e2e/scripts/setup.ts --build --start
# 清理环境
bun e2e/scripts/setup.ts --clean
setup.ts 支持的选项如下(对照 e2e/docs/local-setup.md):
| 选项 | 说明 |
|---|---|
--clean |
清理现有容器和进程 |
--skip-db |
跳过数据库设置(使用已有的) |
--skip-migrate |
跳过数据库迁移 |
--build |
启动前构建应用 |
--start |
设置完成后启动服务器 |
--port <port> |
服务器端口(默认 3006) |
--help |
显示帮助信息 |
运行测试的完整命令序列:
# Step 8.1:启动本地环境(见上)
# Step 8.2:先跑 dry-run 验证 step definitions 齐全
cd e2e
BASE_URL=http://localhost:3006 \
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres \
pnpm exec cucumber-js --config cucumber.config.js --tags "@{module-tag}" --dry-run
# Step 8.3:按用例 ID 运行单个新测试(非 headless,便于观察)
HEADLESS=false BASE_URL=http://localhost:3006 \
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres \
pnpm exec cucumber-js --config cucumber.config.js --tags "@{TEST-ID}"
# 运行整模块测试(排除已知不稳定的 @skip 用例)
HEADLESS=true BASE_URL=http://localhost:3006 \
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres \
pnpm exec cucumber-js --config cucumber.config.js --tags "@{module-tag} and not @skip"
Step 8.4:修复失败。出现失败时按顺序排查:查看 e2e/screenshots/ 中的失败截图 → 调整选择器与等待 → 对于不稳定(flaky)的用例加 @skip 标签并在 README 已知问题中记录 → 确保测试稳定通过。运行前的重要前提(来自 e2e/docs/local-setup.md):
- Docker Desktop 正在运行,Node.js 18+,pnpm 已安装且项目已
pnpm install; - 数据库必须使用
paradedb/paradedb:latest镜像(支持 pgvector 扩展),映射到宿主机 5433 端口; - 应用服务器必须在项目根目录启动,绝不能
cd e2e后执行next start,否则会报Cannot find module './src/libs/next/config/define-config'; - S3 环境变量是必需的(即使不测试文件上传),脚本已自动处理。
阶段 F:文档同步与 PR 提交(Step 9–10)
Step 9:更新文档。一是更新模块 README:已完成功能标 ✅、更新统计、补充已知问题;二是回写 .claude/prompts/e2e-coverage.md:把目标模块表中的状态刷新为最新覆盖情况,并把新学到的最佳实践沉淀回手册,形成团队知识的正向循环。
Step 10:创建 Pull Request:
- 分支名:
test/e2e-{module-name} - Commit 格式:
✅ test: add E2E tests for {module-name} - PR 标题:
✅ test: add E2E tests for {module-name} - PR body 使用给定模板,包含 Summary、Test Coverage 勾选清单与可直接复制的运行命令:
## Summary
- Added E2E BDD tests for `{module-name}`
- Feature files added: [number]
- Scenarios covered: [number]
## Test Coverage
- [x] Feature area 1: {description}
- [ ] Feature area 2: {pending}
## Test Execution
# Run these tests
cd e2e && pnpm exec cucumber-js --config cucumber.config.js --tags "@{module-tag} and not @skip"
四、标签系统与 CI 执行策略
标签与优先级、执行时机一一对应,这是把「体验基准线」落到 CI 的关键。执行策略命令如下(源自 e2e/CLAUDE.md):
# CI - P0 冒烟测试(每次 PR 必跑)
pnpm exec cucumber-js --config cucumber.config.js --tags "@smoke and @P0"
# Nightly - 所有用户旅程
pnpm exec cucumber-js --config cucumber.config.js --tags "@journey"
# 发版前 - 完整回归
pnpm exec cucumber-js --config cucumber.config.js --tags "@P0 or @P1"
# 完整测试
pnpm exec cucumber-js --config cucumber.config.js
测试设计原则(与执行策略配合的四条铁律):
- 按 CRUD + 核心交互覆盖:每个模块必须覆盖创建、读取、更新、删除及核心交互流程;
- LLM 响应必须 Mock:保证测试稳定性和可重复性;
- 中文描述场景:Feature 文件用中文,贴近产品需求、便于产品与测试对齐;
- 优先级分层:合理分配 P0/P1/P2,控制 CI 执行时间。
五、Hard Rules:编写时的 DO / DO NOT 清单
手册中的「Important Rules」是每个模块落地前必须逐条核对的红线:
| 必须做(DO) | 禁止做(DO NOT) |
|---|---|
| Feature 文件用中文编写 | 依赖真实 LLM API 调用 |
| 添加合适的标签(@journey、@P0/@P1/@P2、模块名) | 制造 flaky 测试(PR 前必须保证稳定) |
| 对 LLM 响应做 Mock 保证稳定 | 修改生产代码(除非仅添加 data-testid 属性) |
| Step definitions 中加 console.log 便于调试 | 本地未跑通就提交 PR |
| 处理 desktop/mobile 双组件下的元素可见性问题 | |
用 page.waitForTimeout() 等待动画/过渡 |
|
| 同时兼容中英文文案(如 `/^(无标题 | Untitled)$/`) |
| 用时间戳生成唯一测试数据避免用例间冲突 |
六、LLM Mock 框架源码解析
LLM Mock 是保证 E2E 稳定性的核心设施,实现位于 e2e/src/mocks/llm/index.ts。
6.1 拦截原理
Mock 目标是对所有形如 /webapi/chat/* 的请求(OpenAI 等各 provider 通用)返回预设的 SSE 流式响应。实现没有用 page.route(),原因是 Playwright 的 route.fulfill() 会一次性缓冲整个响应体,无法模拟逐 token 的 SSE 流式下发。因此框架采用两层配合:
- 通过
page.exposeFunction('__lobehubE2ELLMMock', ...)暴露一个构造「流计划」的回调:LLMMockManager.createStreamPlan()解析请求体中的messages,匹配预设响应并切分成 SSE chunk,返回{ chunks, responseDelay, streamDelay }; - 通过
page.addInitScript注入一段原生 JavaScript 覆写window.fetch:仅当请求路径以/webapi/chat/开头时接管,用真实的ReadableStream按responseDelay与streamDelay逐 chunk 下发,并正确响应AbortSignal(支持测试中断言流)、返回Content-Type: text/event-stream。
注入脚本刻意使用原始 JS 字符串而非函数参数,注释中说明了原因:tsx 会给命名函数装饰一个模块级 helper,在浏览器页面中并不存在。
6.2 SSE 响应格式(必须严格匹配)
LobeHub 使用特定 SSE 协议,buildSSEChunks(content, chunkSize) 按如下序列组装(默认 streamChunkSize=10,即每 10 字符一个 text 事件):
# 1. 初始 data 事件
id: msg_xxx
event: data
data: {"id":"msg_xxx","model":"gpt-4o-mini","role":"assistant","type":"message",...}
# 2. 文本内容分块(text 事件,每 chunk 一条)
id: msg_xxx
event: text
data: "Hello"
# 3. 停止事件
id: msg_xxx
event: stop
data: "end_turn"
# 4. 使用量统计
id: msg_xxx
event: usage
data: {"totalTokens":100,...}
# 5. 最终停止
id: msg_xxx
event: stop
data: "message_stop"
6.3 Manager API
LLMMockManager(单例导出为 llmMockManager)提供如下方法:
setResponse(userMessage, response):为某条完整匹配的用户消息设置响应(内部做小写 + trim 归一化);setResponseContaining(fragment, response):为包含片段的消息设置响应,适用于系统包装了用户内容的 prompt;setConfig(partial)/resetConfig():临时覆盖流式参数(如需要更慢的流来模拟流中交互),resetConfig()应放在Afterhook 中防止参数泄漏到下一场景;clearResponses():清空全部自定义响应;enable()/disable():全局开关;setup(page):在页面导航之前调用,注册 fetch 拦截(常见问题之一就是「路由拦截设置在导航之后导致 Mock 未生效」)。
配置项 LLMMockConfig 含默认值:defaultResponse(无匹配时的兜底文案)、enabled(默认 true)、responseDelay(默认 100ms)、streamChunkSize(默认 10)、streamDelay(默认 20ms)。
6.4 预设响应
presetResponses 提供了多种开箱即用的预设:greeting、codeHelp、error、nameIntro / nameRecall(多轮对话记忆验证)、regenerated(重新生成场景)、以及两个长文预设 longArticle 与 longScrollArticle——后者由 60 段拼接而成,注释说明它用于 @AGENT-SCROLL-* 场景,保证回复必然超出视口以便观察滚动行为。
七、元素定位与常见测试模式
7.1 富文本编辑器(contenteditable)
LobeHub 的输入类编辑器是 contenteditable 富文本,不能用 locator.fill()(对 contenteditable 不生效)。正确姿势是先点击容器获得焦点,再逐字键入:
const editor = this.page.locator('[contenteditable="true"]').first();
await editor.click();
await this.page.waitForTimeout(500); // 等待焦点
await this.page.keyboard.type(message, { delay: 30 });
await this.page.keyboard.press('Enter'); // 发送
7.2 斜杠命令(Slash Command)
斜杠菜单出现需要时间,输入后必须等待:
await this.page.keyboard.type('/', { delay: 100 });
await this.page.waitForTimeout(800); // Wait for slash menu
await this.page.keyboard.type('h1', { delay: 80 });
await this.page.keyboard.press('Enter');
7.3 i18n(中英文)兼容
默认文案要同时兼容两种语言,例如无标题文稿的正则 /^(无标题|Untitled)$/;按钮可用 getByRole('button', { name: /choose.*icon|选择图标/i })。
7.4 唯一测试数据
用时间戳生成标题,避免测试运行间相互冲突:
const uniqueTitle = `E2E Page ${Date.now()}`;
7.5 多元素匹配
同一页面 desktop/mobile 双组件并存时,选择器常命中多个元素,触发 strict mode violation。处理手法:用 .first() / .nth(n) 定位,或遍历所有匹配并按可见性过滤后点击。必要时应给生产组件补 data-testid:
<Component data-testid="unique-identifier" />
7.6 高频场景模板
手册沉淀了五类可直接套用的场景模板:导航测试(点菜单 → 断言 URL 与标题)、CRUD 测试(创建 / 编辑 / 删除三连)、编辑器标题测试(点击标题框 → 输入 → 回车 → 断言更新)、富文本测试(/h1 斜杠插入一级标题)、LLM 交互测试(Given LLM Mock 已配置 → 发送消息 → 断言 AI 回复与历史记录)。以 CRUD 为例:
Scenario: 创建新项目
Given 用户已登录系统
When 用户点击创建按钮
And 用户输入名称 "{name}"
And 用户点击保存
Then 应该看到新创建的项目 "{name}"
Scenario: 编辑项目
Given 用户已创建项目 "{name}"
When 用户打开项目编辑
And 用户修改名称为 "{new-name}"
And 用户保存更改
Then 项目名称应更新为 "{new-name}"
Scenario: 删除项目
Given 用户已创建项目 "{name}"
When 用户删除该项目
And 用户确认删除
Then 项目列表中不应包含 "{name}"
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00