Major changes: - Shift from "OpenFang desktop client" to "independent AI Agent desktop app" - Add decision principle: "Is this useful for ZCLAW? Does it affect ZCLAW?" - Simplify project structure and tech stack sections - Replace OpenClaw vs OpenFang comparison with unified backend approach - Consolidate troubleshooting from scattered sections into organized FAQ - Update Hands system documentation with 8 capabilities and status - Stream
574 lines
22 KiB
Markdown
574 lines
22 KiB
Markdown
# ZCLAW 前端完整验证方案
|
|
|
|
## 背景
|
|
|
|
当前 E2E 测试(`functional-scenarios.spec.ts`)主要验证 UI 表面:
|
|
- 验证元素是否存在
|
|
- 验证按钮能否点击
|
|
- 验证对话框能否打开
|
|
|
|
**问题**:这些测试无法发现真正的功能问题,例如:
|
|
- 点击按钮后 API 是否被正确调用
|
|
- Store 状态是否正确更新
|
|
- 数据是否正确渲染
|
|
- 边界情况是否处理
|
|
|
|
## 目标
|
|
|
|
设计一套**深度模拟用户操作**的验证方案,确保:
|
|
1. **完整操作流程** - 从用户视角完成端到端操作
|
|
2. **数据流验证** - 检查 UI → Store → API → 后端的完整链路
|
|
3. **状态变化验证** - 验证操作前后状态正确转换
|
|
4. **结果展示验证** - 验证返回数据正确渲染到 UI
|
|
5. **边界情况验证** - 验证错误处理、空状态、加载状态
|
|
|
|
---
|
|
|
|
## 验证架构
|
|
|
|
### 测试分层
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ E2E 用户流程测试 │
|
|
│ (Playwright - 完整用户操作 + 数据流验证) │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ Store 状态测试 │
|
|
│ (Vitest - Store 逻辑 + 状态转换验证) │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ API 集成测试 │
|
|
│ (MSW Mock - 请求/响应验证) │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 新增测试文件
|
|
|
|
```
|
|
desktop/tests/
|
|
├── e2e/
|
|
│ ├── specs/
|
|
│ │ ├── functional-scenarios.spec.ts # 现有 - 保留
|
|
│ │ ├── data-flow.spec.ts # 新增 - 数据流深度验证
|
|
│ │ ├── store-state.spec.ts # 新增 - Store 状态验证
|
|
│ │ └── edge-cases.spec.ts # 新增 - 边界情况测试
|
|
│ ├── fixtures/
|
|
│ │ ├── mock-gateway.ts # 新增 - Gateway Mock
|
|
│ │ ├── test-data.ts # 新增 - 测试数据工厂
|
|
│ │ └── store-inspectors.ts # 新增 - Store 检查工具
|
|
│ └── utils/
|
|
│ ├── network-helpers.ts # 新增 - 网络拦截工具
|
|
│ ├── store-assertions.ts # 新增 - Store 断言
|
|
│ └── user-actions.ts # 新增 - 用户操作模拟
|
|
└── store/
|
|
└── *.test.ts # 现有 - 扩展 Store 单元测试
|
|
```
|
|
|
|
---
|
|
|
|
## 模块验证用例
|
|
|
|
### 1. 聊天模块 (ChatArea.tsx + chatStore.ts)
|
|
|
|
#### 数据流验证
|
|
```
|
|
用户输入 → sendMessage() → gateway-client.chatStream()
|
|
→ WebSocket 事件 → initStreamListener() → Store 更新 → UI 渲染
|
|
```
|
|
|
|
#### 深度验证用例
|
|
|
|
| 用例 ID | 验证场景 | 验证步骤 | 断言点 |
|
|
|---------|----------|----------|--------|
|
|
| CHAT-DF-01 | 发送消息并接收流式响应 | 1. 输入消息 2. 点击发送 3. 拦截 WebSocket 4. 验证请求格式 5. 模拟流式响应 6. 验证 Store 状态 7. 验证 UI 渲染 | ① 请求包含正确 sessionKey ② isStreaming=true ③ 消息增量追加 ④ streaming=false ⑤ messages 数组更新 |
|
|
| CHAT-DF-02 | 模型切换 | 1. 点击模型选择器 2. 选择新模型 3. 发送消息 | ① Store.currentModel 更新 ② 请求使用新模型 ③ UI 显示新模型名 |
|
|
| CHAT-DF-03 | Agent 切换 | 1. 当前有消息 2. 切换 Agent 3. 验证状态保存 4. 切换回来 | ① 会话保存到 conversations ② 消息清空 ③ sessionKey 更新 ④ 切回后恢复原消息 |
|
|
| CHAT-DF-04 | 新建对话 | 1. 发送消息 2. 点击新建 3. 验证状态 | ① 消息清空 ② currentConversationId=null ③ sessionKey=null |
|
|
| CHAT-DF-05 | 网络错误处理 | 1. 模拟网络断开 2. 发送消息 | ① 错误消息显示 ② isStreaming=false ③ error 字段设置 ④ 可重试 |
|
|
| CHAT-DF-06 | 流式中断 | 1. 开始流式响应 2. 中途断开连接 | ① 部分消息保留 ② 错误指示器显示 ③ 可恢复 |
|
|
|
|
#### Store 断言示例
|
|
```typescript
|
|
// 验证 chatStore 状态
|
|
const chatState = await page.evaluate(() => {
|
|
const stored = localStorage.getItem('zclaw-chat-storage');
|
|
return stored ? JSON.parse(stored).state : null;
|
|
});
|
|
|
|
expect(chatState).toMatchObject({
|
|
isStreaming: false,
|
|
messages: expect.arrayContaining([
|
|
expect.objectContaining({ role: 'user', content: 'test' }),
|
|
expect.objectContaining({ role: 'assistant', streaming: false }),
|
|
]),
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### 2. 分身管理 (CloneManager.tsx + agentStore.ts)
|
|
|
|
#### 数据流验证
|
|
```
|
|
UI → createClone() → POST /api/clones → Response → clones 更新 → UI 重渲染
|
|
```
|
|
|
|
#### 深度验证用例
|
|
|
|
| 用例 ID | 验证场景 | 验证步骤 | 断言点 |
|
|
|---------|----------|----------|--------|
|
|
| CLONE-DF-01 | 创建分身 | 1. 点击创建 2. 填写表单 3. 提交 4. 拦截 API 请求 | ① POST 请求格式正确 ② Store.clones 增加 ③ UI 显示新分身 |
|
|
| CLONE-DF-02 | 切换分身 | 1. 当前在 Agent A 2. 切换到 Agent B | ① currentAgent 更新 ② 聊天消息切换 ③ sessionKey 更新 |
|
|
| CLONE-DF-03 | 删除分身 | 1. 选择分身 2. 点击删除 3. 确认 | ① DELETE API 调用 ② Store 移除 ③ 回退到默认 |
|
|
| CLONE-DF-04 | 修改配置 | 1. 打开设置 2. 修改字段 3. 保存 | ① PATCH 请求含 delta ② Store 更新 ③ UI 显示新值 |
|
|
|
|
---
|
|
|
|
### 3. Hands 系统 (HandsPanel.tsx + handStore.ts)
|
|
|
|
#### 数据流验证
|
|
```
|
|
组件挂载 → loadHands() → GET /api/hands → 数据映射 → hands 数组 → UI 渲染
|
|
触发执行 → triggerHand() → POST /api/hands/:name/trigger → 状态变更 → UI 更新
|
|
```
|
|
|
|
#### 深度验证用例
|
|
|
|
| 用例 ID | 验证场景 | 验证步骤 | 断言点 |
|
|
|---------|----------|----------|--------|
|
|
| HAND-DF-01 | 加载 Hands 列表 | 1. 导航到 Hands 2. 拦截 API 3. 验证映射 | ① GET /api/hands 调用 ② 状态徽章正确 ③ requirements_met 显示 |
|
|
| HAND-DF-02 | 触发 Hand 执行 | 1. 点击执行 2. 如有参数则填写 3. 提交 | ① POST 触发请求 ② status → running ③ currentRunId 设置 |
|
|
| HAND-DF-03 | 参数表单验证 | 1. 展开 Hand 2. 填写各类型参数 3. 验证失败 4. 修正后提交 | ① 类型验证生效 ② 错误提示显示 ③ 正确值传递 |
|
|
| HAND-DF-04 | 审批流程 | 1. 触发需审批 Hand 2. 状态 needs_approval 3. 批准/拒绝 | ① 审批对话框显示 ② approveHand API 调用 ③ 状态更新 |
|
|
| HAND-DF-05 | 执行历史 | 1. 查看 Hand 详情 2. 历史记录标签 | ① GET /api/hands/:name/runs ② 记录正确显示 |
|
|
|
|
#### 参数类型验证矩阵
|
|
|
|
| 参数类型 | 验证项 |
|
|
|----------|--------|
|
|
| text | 空值、最大长度、正则 pattern |
|
|
| number | 边界值、min/max、非数字 |
|
|
| boolean | 切换状态、默认值 |
|
|
| select | 选项列表、默认选中 |
|
|
| array | 添加/删除项、空数组 |
|
|
| object | JSON 解析、格式验证 |
|
|
| file | 文件选择、大小限制 |
|
|
|
|
---
|
|
|
|
### 4. 自动化面板 (AutomationPanel.tsx)
|
|
|
|
#### 深度验证用例
|
|
|
|
| 用例 ID | 验证场景 | 验证步骤 | 断言点 |
|
|
|---------|----------|----------|--------|
|
|
| AUTO-DF-01 | 分类过滤 | 1. 点击分类 2. 验证列表 | ① 请求带 category 参数 ② 列表过滤正确 ③ 空状态处理 |
|
|
| AUTO-DF-02 | 搜索功能 | 1. 输入关键词 2. 实时过滤 | ① 匹配名称/描述 ② 高亮显示 ③ 无结果提示 |
|
|
| AUTO-DF-03 | 批量执行 | 1. 多选 Hands 2. 批量触发 | ① 选中状态管理 ② 依次触发 API ③ 进度显示 |
|
|
| AUTO-DF-04 | 视图切换 | 1. 网格视图 2. 列表视图 | ① 布局变化 ② 数据一致 ③ 响应式适配 |
|
|
|
|
---
|
|
|
|
### 5. 工作流编辑 (WorkflowEditor.tsx + workflowStore.ts)
|
|
|
|
#### 深度验证用例
|
|
|
|
| 用例 ID | 验证场景 | 验证步骤 | 断言点 |
|
|
|---------|----------|----------|--------|
|
|
| WF-DF-01 | 创建工作流 | 1. 打开编辑器 2. 添加步骤 3. 配置参数 4. 保存 | ① POST 请求格式 ② steps 数组正确 ③ Store 更新 |
|
|
| WF-DF-02 | 步骤操作 | 1. 添加步骤 2. 上移/下移 3. 删除 | ① 顺序更新 ② Hand 选择器 ③ JSON 参数解析 |
|
|
| WF-DF-03 | 执行工作流 | 1. 选择工作流 2. 触发执行 | ① POST execute ② 状态 running ③ 步骤进度显示 |
|
|
| WF-DF-04 | 验证规则 | 1. 空名称 2. 无步骤 3. 保存 | ① 验证错误显示 ② 禁用保存按钮 |
|
|
|
|
---
|
|
|
|
### 6. 技能市场 (SkillMarket.tsx)
|
|
|
|
#### 深度验证用例
|
|
|
|
| 用例 ID | 验证场景 | 验证步骤 | 断言点 |
|
|
|---------|----------|----------|--------|
|
|
| SKILL-DF-01 | 浏览技能 | 1. 加载目录 2. 验证数据 | ① GET /api/skills ② 分类显示 ③ 安装状态 |
|
|
| SKILL-DF-02 | 搜索技能 | 1. 输入关键词 2. 验证匹配 | ① 名称/能力/触发词匹配 ② 结果排序 |
|
|
| SKILL-DF-03 | 安装技能 | 1. 点击安装 2. 等待完成 | ① 安装 API 调用 ② 状态变更 ③ UI 更新 |
|
|
| SKILL-DF-04 | 技能详情 | 1. 展开/点击 2. 查看详情 | ① 触发词列表 ② 工具依赖 ③ 使用示例 |
|
|
|
|
---
|
|
|
|
### 7. 团队协作 (TeamList.tsx + teamStore.ts)
|
|
|
|
#### 深度验证用例
|
|
|
|
| 用例 ID | 验证场景 | 验证步骤 | 断言点 |
|
|
|---------|----------|----------|--------|
|
|
| TEAM-DF-01 | 创建团队 | 1. 填写表单 2. 选择成员 3. 选择模式 4. 创建 | ① POST 格式正确 ② members 数组 ③ pattern 字段 |
|
|
| TEAM-DF-02 | 成员管理 | 1. 添加成员 2. 分配角色 3. 移除 | ① Store 更新 ② 角色字段 ③ UI 反映 |
|
|
| TEAM-DF-03 | 任务分配 | 1. 创建任务 2. 分配成员 3. 更新状态 | ① tasks 数组 ② assigneeId ③ status 变化 |
|
|
| TEAM-DF-04 | Dev↔QA 循环 | 1. 启动循环 2. 提交审查 3. 反馈 4. 迭代 | ① activeLoops 更新 ② iteration 递增 ③ state 转换 |
|
|
|
|
---
|
|
|
|
### 8. 设置页面 (Settings/*)
|
|
|
|
#### 深度验证用例
|
|
|
|
| 用例 ID | 验证场景 | 验证步骤 | 断言点 |
|
|
|---------|----------|----------|--------|
|
|
| SET-DF-01 | 通用设置 | 1. 修改字段 2. 保存 | ① PATCH 请求 ② Store 更新 ③ 成功提示 |
|
|
| SET-DF-02 | 模型配置 | 1. 添加 API Key 2. 测试连接 | ① 密钥掩码显示 ② 连接测试 API ③ 模型列表加载 |
|
|
| SET-DF-03 | MCP 服务 | 1. 查看列表 2. 启用/禁用 | ① GET /api/plugins ② PATCH 状态 ③ UI 更新 |
|
|
| SET-DF-04 | 审计日志 | 1. 打开日志 2. 筛选 3. 分页 | ① 日志加载 ② 过滤参数 ③ 分页正确 |
|
|
|
|
---
|
|
|
|
### 9. 审批系统 (ApprovalQueue.tsx)
|
|
|
|
#### 深度验证用例
|
|
|
|
| 用例 ID | 验证场景 | 验证步骤 | 断言点 |
|
|
|---------|----------|----------|--------|
|
|
| APPROVE-DF-01 | 查看队列 | 1. 加载审批列表 | ① GET /api/approvals?status=pending ② 详情显示 |
|
|
| APPROVE-DF-02 | 批准操作 | 1. 选择审批 2. 批准 | ① POST respond (approved=true) ② 队列移除 ③ 状态更新 |
|
|
| APPROVE-DF-03 | 拒绝操作 | 1. 选择审批 2. 拒绝 3. 填写原因 | ① 原因字段 ② POST respond (approved=false) ③ 队列更新 |
|
|
|
|
---
|
|
|
|
## 边界情况验证清单
|
|
|
|
### 网络边界
|
|
|
|
| 场景 | 预期行为 | 验证方法 |
|
|
|------|----------|----------|
|
|
| Gateway 断开 | 连接指示器红色,输入禁用,错误消息 | 模拟断开,检查 UI 状态 |
|
|
| WebSocket 重连 | 自动重连尝试,状态保留,指示器显示 | 断开后恢复,验证状态 |
|
|
| 请求超时 | 超时错误显示,重试选项可用 | 设置短超时,检查处理 |
|
|
| 无效响应 | 解析错误处理,优雅降级 | 返回畸形 JSON |
|
|
| 限流 (429) | 用户通知,自动退避重试 | 模拟 429 响应 |
|
|
|
|
### 数据边界
|
|
|
|
| 场景 | 预期行为 | 验证方法 |
|
|
|------|----------|----------|
|
|
| 空消息列表 | 欢迎状态,开始提示 | 清空数据,检查 UI |
|
|
| 超长消息 | 容器内滚动,布局不破 | 输入 10000+ 字符 |
|
|
| 空 Hands 列表 | "无可用 Hands" + 连接提示 | Mock 空响应 |
|
|
| 无效 JSON 参数 | 解析错误显示,原值保留 | 输入畸形 JSON |
|
|
| 大文件上传 | 进度指示,大小限制检查 | 选择超大文件 |
|
|
|
|
### 状态边界
|
|
|
|
| 场景 | 预期行为 | 验证方法 |
|
|
|------|----------|----------|
|
|
| 快速连续点击 | 防抖,单次操作,正确最终状态 | 连续点击 10 次 |
|
|
| 流式响应中切换 | 取消/完成当前,干净切换 | 发消息后立即切换 |
|
|
| 浏览器刷新 | 从 localStorage 恢复,会话保留 | 刷新后检查状态 |
|
|
| 标签页切换 | 操作继续,返回时状态同步 | 切换标签再返回 |
|
|
|
|
---
|
|
|
|
## 实现方案
|
|
|
|
### 1. 网络拦截工具
|
|
|
|
```typescript
|
|
// tests/e2e/utils/network-helpers.ts
|
|
export const networkHelpers = {
|
|
// 拦截并记录所有 API 请求
|
|
async interceptAllAPI(page: Page) {
|
|
const requests: Array<{ url: string; method: string; body?: any }> = [];
|
|
|
|
await page.route('**/api/**', async (route) => {
|
|
const request = route.request();
|
|
requests.push({
|
|
url: request.url(),
|
|
method: request.method(),
|
|
body: request.postDataJSON(),
|
|
});
|
|
await route.continue();
|
|
});
|
|
|
|
return requests;
|
|
},
|
|
|
|
// Mock API 响应
|
|
async mockAPI(page: Page, path: string, response: any, status = 200) {
|
|
await page.route(`**/api/${path}**`, async (route) => {
|
|
await route.fulfill({
|
|
status,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(response),
|
|
});
|
|
});
|
|
},
|
|
|
|
// 模拟网络错误
|
|
async simulateNetworkError(page: Page, path: string) {
|
|
await page.route(`**/api/${path}**`, async (route) => {
|
|
await route.abort('failed');
|
|
});
|
|
},
|
|
|
|
// 模拟延迟响应
|
|
async simulateDelay(page: Page, path: string, delayMs: number) {
|
|
await page.route(`**/api/${path}**`, async (route) => {
|
|
await new Promise(r => setTimeout(r, delayMs));
|
|
await route.continue();
|
|
});
|
|
},
|
|
};
|
|
```
|
|
|
|
### 2. Store 检查工具
|
|
|
|
```typescript
|
|
// tests/e2e/fixtures/store-inspectors.ts
|
|
export const storeInspectors = {
|
|
// 获取持久化的 Store 状态
|
|
async getPersistedState(page: Page, storeName: string) {
|
|
return page.evaluate((name) => {
|
|
const stored = localStorage.getItem(`zclaw-${name}-storage`);
|
|
return stored ? JSON.parse(stored).state : null;
|
|
}, storeName);
|
|
},
|
|
|
|
// 等待 Store 状态变化
|
|
async waitForStateChange(page: Page, storeName: string, path: string, expectedValue: any) {
|
|
await page.waitForFunction(
|
|
({ store, path, expected }) => {
|
|
const stored = localStorage.getItem(`zclaw-${store}-storage`);
|
|
if (!stored) return false;
|
|
const state = JSON.parse(stored).state;
|
|
const value = path.split('.').reduce((obj, key) => obj?.[key], state);
|
|
return JSON.stringify(value) === JSON.stringify(expected);
|
|
},
|
|
{ store: storeName, path, expected: expectedValue },
|
|
{ timeout: 5000 }
|
|
);
|
|
},
|
|
|
|
// 验证 Store 状态
|
|
async assertStoreState(page: Page, storeName: string, expected: Record<string, any>) {
|
|
const state = await this.getPersistedState(page, storeName);
|
|
for (const [key, value] of Object.entries(expected)) {
|
|
expect(state?.[key]).toEqual(value);
|
|
}
|
|
},
|
|
};
|
|
```
|
|
|
|
### 3. 用户操作模拟
|
|
|
|
```typescript
|
|
// tests/e2e/utils/user-actions.ts
|
|
export const userActions = {
|
|
// 完整聊天流程
|
|
async sendChatMessage(page: Page, message: string) {
|
|
const chatInput = page.locator('textarea').first();
|
|
await chatInput.waitFor({ state: 'visible' });
|
|
await chatInput.fill(message);
|
|
|
|
// 拦截请求
|
|
const [request] = await Promise.all([
|
|
page.waitForRequest('**/api/chat**'),
|
|
page.getByRole('button', { name: '发送消息' }).click(),
|
|
]);
|
|
|
|
return request;
|
|
},
|
|
|
|
// 创建分身完整流程
|
|
async createClone(page: Page, data: { name: string; role?: string }) {
|
|
await page.getByRole('button', { name: /创建|new/i }).click();
|
|
await page.waitForSelector('[role="dialog"]');
|
|
|
|
const dialog = page.locator('[role="dialog"]');
|
|
await dialog.locator('input').first().fill(data.name);
|
|
|
|
const [response] = await Promise.all([
|
|
page.waitForResponse('**/api/clones'),
|
|
dialog.getByRole('button', { name: /确认|创建|save/i }).click(),
|
|
]);
|
|
|
|
return response;
|
|
},
|
|
|
|
// 触发 Hand 完整流程
|
|
async triggerHand(page: Page, handName: string, params?: Record<string, any>) {
|
|
// 导航到 Hands
|
|
await page.getByRole('tab', { name: 'Hands' }).click();
|
|
|
|
// 找到并点击 Hand
|
|
const handCard = page.locator(`[data-hand-name="${handName}"]`).or(
|
|
page.getByRole('button', { name: new RegExp(handName, 'i') })
|
|
);
|
|
await handCard.click();
|
|
|
|
// 如果有参数表单,填写参数
|
|
if (params) {
|
|
for (const [key, value] of Object.entries(params)) {
|
|
const input = page.locator(`[name="${key}"]`).or(
|
|
page.locator(`label:has-text("${key}")`).locator('..').locator('input, textarea, select')
|
|
);
|
|
if (await input.isVisible()) {
|
|
await input.fill(String(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 触发执行
|
|
const [request] = await Promise.all([
|
|
page.waitForRequest(`**/api/hands/${handName}/trigger`),
|
|
page.getByRole('button', { name: /执行|触发|run/i }).click(),
|
|
]);
|
|
|
|
return request;
|
|
},
|
|
};
|
|
```
|
|
|
|
### 4. 数据流测试示例
|
|
|
|
```typescript
|
|
// tests/e2e/specs/data-flow.spec.ts
|
|
import { test, expect } from '@playwright/test';
|
|
import { networkHelpers } from '../utils/network-helpers';
|
|
import { storeInspectors } from '../fixtures/store-inspectors';
|
|
import { userActions } from '../utils/user-actions';
|
|
|
|
test.describe('聊天数据流验证', () => {
|
|
test('CHAT-DF-01: 发送消息完整数据流', async ({ page }) => {
|
|
// 1. 设置网络拦截
|
|
const requests = await networkHelpers.interceptAllAPI(page);
|
|
|
|
// 2. Mock 流式响应
|
|
await networkHelpers.mockAPI(page, 'chat', {
|
|
type: 'stream',
|
|
messages: [
|
|
{ delta: '你好', phase: 'start' },
|
|
{ delta: '!我是', phase: 'delta' },
|
|
{ delta: 'AI 助手', phase: 'delta' },
|
|
{ phase: 'end' },
|
|
],
|
|
});
|
|
|
|
// 3. 发送消息
|
|
await page.goto('http://localhost:1420');
|
|
const request = await userActions.sendChatMessage(page, '测试消息');
|
|
|
|
// 4. 验证请求格式
|
|
const requestBody = request.postDataJSON();
|
|
expect(requestBody).toMatchObject({
|
|
message: '测试消息',
|
|
sessionKey: expect.any(String),
|
|
});
|
|
|
|
// 5. 验证 Store 状态
|
|
const chatState = await storeInspectors.getPersistedState(page, 'chat');
|
|
expect(chatState.messages).toContainEqual(
|
|
expect.objectContaining({ role: 'user', content: '测试消息' })
|
|
);
|
|
|
|
// 6. 验证 UI 渲染
|
|
await expect(page.locator('[class*="message"]').filter({ hasText: '测试消息' })).toBeVisible();
|
|
await expect(page.locator('[class*="assistant"]')).toBeVisible();
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 验证执行
|
|
|
|
### 运行命令
|
|
|
|
```bash
|
|
# 运行所有 E2E 测试
|
|
pnpm --filter desktop test:e2e
|
|
|
|
# 运行数据流测试
|
|
pnpm --filter desktop playwright test -- --grep "数据流|data-flow"
|
|
|
|
# 运行边界情况测试
|
|
pnpm --filter desktop playwright test -- --grep "边界|edge-case"
|
|
|
|
# 调试模式
|
|
pnpm --filter desktop playwright test -- --headed --debug
|
|
|
|
# 生成报告
|
|
pnpm --filter desktop playwright show-report
|
|
```
|
|
|
|
### CI 集成
|
|
|
|
```yaml
|
|
# .github/workflows/frontend-verification.yml
|
|
name: Frontend Verification
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
e2e-tests:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: pnpm/action-setup@v2
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: 'pnpm'
|
|
- run: pnpm install
|
|
- run: pnpm --filter desktop exec playwright install --with-deps
|
|
- name: Run E2E tests
|
|
run: pnpm --filter desktop test:e2e
|
|
- uses: actions/upload-artifact@v4
|
|
if: always()
|
|
with:
|
|
name: playwright-report
|
|
path: desktop/test-results/
|
|
```
|
|
|
|
---
|
|
|
|
## 关键文件清单
|
|
|
|
### 需要创建的文件
|
|
|
|
| 文件路径 | 用途 |
|
|
|----------|------|
|
|
| `desktop/tests/e2e/specs/data-flow.spec.ts` | 数据流深度验证测试 |
|
|
| `desktop/tests/e2e/specs/store-state.spec.ts` | Store 状态验证测试 |
|
|
| `desktop/tests/e2e/specs/edge-cases.spec.ts` | 边界情况测试 |
|
|
| `desktop/tests/e2e/fixtures/mock-gateway.ts` | Gateway Mock 工具 |
|
|
| `desktop/tests/e2e/fixtures/test-data.ts` | 测试数据工厂 |
|
|
| `desktop/tests/e2e/fixtures/store-inspectors.ts` | Store 检查工具 |
|
|
| `desktop/tests/e2e/utils/network-helpers.ts` | 网络拦截工具 |
|
|
| `desktop/tests/e2e/utils/store-assertions.ts` | Store 断言工具 |
|
|
| `desktop/tests/e2e/utils/user-actions.ts` | 用户操作模拟 |
|
|
|
|
### 需要修改的文件
|
|
|
|
| 文件路径 | 修改内容 |
|
|
|----------|----------|
|
|
| `desktop/tests/e2e/playwright.config.ts` | 添加新测试配置 |
|
|
| `desktop/src/store/*.ts` | 添加测试辅助方法(可选) |
|
|
|
|
---
|
|
|
|
## 成功标准
|
|
|
|
### 覆盖率目标
|
|
|
|
| 层级 | 目标 | 度量方式 |
|
|
|------|------|----------|
|
|
| E2E 用户流程 | 100% | 10 个模块全覆盖 |
|
|
| 数据流路径 | 100% | UI → Store → API → UI 验证 |
|
|
| 边界情况 | 80% | 网络/数据/状态边界 |
|
|
| 错误处理 | 100% | 所有错误状态 |
|
|
|
|
### 质量门槛
|
|
|
|
- [ ] 所有测试在 CI 中通过
|
|
- [ ] 正常流程无控制台错误
|
|
- [ ] 网络故障优雅处理
|
|
- [ ] 刷新后状态持久化
|
|
- [ ] 所有用户输入有验证
|
|
- [ ] 加载状态正确显示
|