mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 14:01:30 +08:00
Co-authored-by: gujiaming <52187003+AtomsH4@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: kangfenmao <kangfenmao@qq.com> Signed-off-by: gujiaming <52187003+AtomsH4@users.noreply.github.com>
311 lines
8.2 KiB
Markdown
311 lines
8.2 KiB
Markdown
# E2E Testing Guide
|
||
|
||
本目录包含 Cherry Studio 的端到端 (E2E) 测试,使用 Playwright 测试 Electron 应用。
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
tests/e2e/
|
||
├── README.md # 本文档
|
||
├── global-setup.ts # 全局测试初始化
|
||
├── global-teardown.ts # 全局测试清理
|
||
├── fixtures/
|
||
│ └── electron.fixture.ts # Electron 应用启动 fixture
|
||
├── utils/
|
||
│ ├── wait-helpers.ts # 等待辅助函数
|
||
│ └── index.ts # 工具导出
|
||
├── pages/ # Page Object Model
|
||
│ ├── base.page.ts # 基础页面对象类
|
||
│ ├── sidebar.page.ts # 侧边栏导航
|
||
│ ├── home.page.ts # 首页/聊天页
|
||
│ ├── settings.page.ts # 设置页
|
||
│ ├── chat.page.ts # 聊天交互
|
||
│ └── index.ts # 页面对象导出
|
||
└── specs/ # 测试用例
|
||
├── app-launch.spec.ts # 应用启动测试
|
||
├── navigation.spec.ts # 页面导航测试
|
||
├── settings/ # 设置相关测试
|
||
│ └── general.spec.ts
|
||
└── conversation/ # 对话相关测试
|
||
└── basic-chat.spec.ts
|
||
```
|
||
|
||
---
|
||
|
||
## 运行测试
|
||
|
||
### 前置条件
|
||
|
||
1. 安装依赖:`pnpm install`
|
||
2. 构建应用:`pnpm build`
|
||
|
||
### 运行命令
|
||
|
||
```bash
|
||
# 运行所有 e2e 测试
|
||
pnpm test:e2e
|
||
|
||
# 带可视化窗口运行(可以看到测试过程)
|
||
pnpm test:e2e --headed
|
||
|
||
# 运行特定测试文件
|
||
pnpm playwright test tests/e2e/specs/app-launch.spec.ts
|
||
|
||
# 运行匹配名称的测试
|
||
pnpm playwright test -g "should launch"
|
||
|
||
# 调试模式(会暂停并打开调试器)
|
||
pnpm playwright test --debug
|
||
|
||
# 使用 Playwright UI 模式
|
||
pnpm playwright test --ui
|
||
|
||
# 查看测试报告
|
||
pnpm playwright show-report
|
||
```
|
||
|
||
### 常见问题
|
||
|
||
**Q: 测试时看不到窗口?**
|
||
A: 默认是 headless 模式,使用 `--headed` 参数可看到窗口。
|
||
|
||
**Q: 测试失败,提示找不到元素?**
|
||
A:
|
||
1. 确保已运行 `pnpm build` 构建最新代码
|
||
2. 检查选择器是否正确,UI 可能已更新
|
||
|
||
**Q: 测试超时?**
|
||
A: Electron 应用启动较慢,可在测试中增加超时时间:
|
||
```typescript
|
||
test.setTimeout(60000) // 60秒
|
||
```
|
||
|
||
---
|
||
|
||
## AI 助手指南:创建新测试用例
|
||
|
||
以下内容供 AI 助手(如 Claude、GPT)在创建新测试用例时参考。
|
||
|
||
### 基本原则
|
||
|
||
1. **使用 Page Object Model (POM)**:所有页面交互应通过 `pages/` 目录下的页面对象进行
|
||
2. **使用自定义 fixture**:从 `../fixtures/electron.fixture` 导入 `test` 和 `expect`
|
||
3. **等待策略**:使用 `utils/wait-helpers.ts` 中的等待函数,避免硬编码 `waitForTimeout`
|
||
4. **测试独立性**:每个测试应该独立运行,不依赖其他测试的状态
|
||
|
||
### 创建新测试文件
|
||
|
||
```typescript
|
||
// tests/e2e/specs/[feature]/[feature].spec.ts
|
||
|
||
import { test, expect } from '../../fixtures/electron.fixture'
|
||
import { SomePageObject } from '../../pages/some.page'
|
||
import { waitForAppReady } from '../../utils/wait-helpers'
|
||
|
||
test.describe('Feature Name', () => {
|
||
let pageObject: SomePageObject
|
||
|
||
test.beforeEach(async ({ mainWindow }) => {
|
||
await waitForAppReady(mainWindow)
|
||
pageObject = new SomePageObject(mainWindow)
|
||
})
|
||
|
||
test('should do something', async ({ mainWindow }) => {
|
||
// 测试逻辑
|
||
})
|
||
})
|
||
```
|
||
|
||
### 创建新页面对象
|
||
|
||
```typescript
|
||
// tests/e2e/pages/[feature].page.ts
|
||
|
||
import { Page, Locator } from '@playwright/test'
|
||
import { BasePage } from './base.page'
|
||
|
||
export class FeaturePage extends BasePage {
|
||
// 定义页面元素定位器
|
||
readonly someButton: Locator
|
||
readonly someInput: Locator
|
||
|
||
constructor(page: Page) {
|
||
super(page)
|
||
// 使用多种选择器策略,提高稳定性
|
||
this.someButton = page.locator('[class*="SomeButton"], button:has-text("Some Text")')
|
||
this.someInput = page.locator('input[placeholder*="placeholder"]')
|
||
}
|
||
|
||
// 页面操作方法
|
||
async doSomething(): Promise<void> {
|
||
await this.someButton.click()
|
||
}
|
||
|
||
// 状态检查方法
|
||
async isSomethingVisible(): Promise<boolean> {
|
||
return this.someButton.isVisible()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 选择器最佳实践
|
||
|
||
```typescript
|
||
// 优先级从高到低:
|
||
|
||
// 1. data-testid(最稳定,但需要在源码中添加)
|
||
page.locator('[data-testid="submit-button"]')
|
||
|
||
// 2. 语义化角色
|
||
page.locator('button[role="submit"]')
|
||
page.locator('[aria-label="Send message"]')
|
||
|
||
// 3. 类名模糊匹配(适应 CSS Modules / generated class names)
|
||
page.locator('[class*="SendButton"]')
|
||
page.locator('[class*="send-button"]')
|
||
|
||
// 4. 文本内容
|
||
page.locator('button:has-text("发送")')
|
||
page.locator('text=Submit')
|
||
|
||
// 5. 组合选择器(提高稳定性)
|
||
page.locator('[class*="ChatInput"] textarea, [class*="InputBar"] textarea')
|
||
|
||
// 避免使用:
|
||
// - 精确类名(容易因构建变化而失效)
|
||
// - 层级过深的选择器
|
||
// - 索引选择器(如 nth-child)除非必要
|
||
```
|
||
|
||
### 等待策略
|
||
|
||
```typescript
|
||
import { waitForAppReady, waitForNavigation, waitForModal } from '../../utils/wait-helpers'
|
||
|
||
// 等待应用就绪
|
||
await waitForAppReady(mainWindow)
|
||
|
||
// 等待导航完成(HashRouter)
|
||
await waitForNavigation(mainWindow, '/settings')
|
||
|
||
// 等待模态框出现
|
||
await waitForModal(mainWindow)
|
||
|
||
// 等待元素可见
|
||
await page.locator('.some-element').waitFor({ state: 'visible', timeout: 10000 })
|
||
|
||
// 等待元素消失
|
||
await page.locator('.loading').waitFor({ state: 'hidden' })
|
||
|
||
// 避免使用固定等待时间
|
||
// BAD: await page.waitForTimeout(3000)
|
||
// GOOD: await page.waitForSelector('.element', { state: 'visible' })
|
||
```
|
||
|
||
### 断言模式
|
||
|
||
```typescript
|
||
// 使用 Playwright 的自动重试断言
|
||
await expect(page.locator('.element')).toBeVisible()
|
||
await expect(page.locator('.element')).toHaveText('expected text')
|
||
await expect(page.locator('.element')).toHaveCount(3)
|
||
|
||
// 检查 URL(HashRouter)
|
||
await expect(page).toHaveURL(/.*#\/settings.*/)
|
||
|
||
// 软断言(不会立即失败)
|
||
await expect.soft(page.locator('.element')).toBeVisible()
|
||
|
||
// 自定义超时
|
||
await expect(page.locator('.slow-element')).toBeVisible({ timeout: 30000 })
|
||
```
|
||
|
||
### 处理 Electron 特性
|
||
|
||
```typescript
|
||
// 访问 Electron 主进程
|
||
const bounds = await electronApp.evaluate(({ BrowserWindow }) => {
|
||
const win = BrowserWindow.getAllWindows()[0]
|
||
return win?.getBounds()
|
||
})
|
||
|
||
// 检查窗口状态
|
||
const isMaximized = await electronApp.evaluate(({ BrowserWindow }) => {
|
||
const win = BrowserWindow.getAllWindows()[0]
|
||
return win?.isMaximized()
|
||
})
|
||
|
||
// 调用 IPC(通过 preload 暴露的 API)
|
||
const result = await mainWindow.evaluate(() => {
|
||
return (window as any).api.someMethod()
|
||
})
|
||
```
|
||
|
||
### 测试文件命名规范
|
||
|
||
```
|
||
specs/
|
||
├── [feature].spec.ts # 单文件测试
|
||
├── [feature]/
|
||
│ ├── [sub-feature].spec.ts # 子功能测试
|
||
│ └── [another].spec.ts
|
||
```
|
||
|
||
示例:
|
||
- `app-launch.spec.ts` - 应用启动
|
||
- `navigation.spec.ts` - 页面导航
|
||
- `settings/general.spec.ts` - 通用设置
|
||
- `conversation/basic-chat.spec.ts` - 基础聊天
|
||
|
||
### 添加新页面对象后的清单
|
||
|
||
1. 在 `pages/` 目录创建 `[feature].page.ts`
|
||
2. 继承 `BasePage` 类
|
||
3. 在 `pages/index.ts` 中导出
|
||
4. 在对应的 spec 文件中导入使用
|
||
|
||
### 测试用例编写清单
|
||
|
||
- [ ] 使用自定义 fixture (`test`, `expect`)
|
||
- [ ] 在 `beforeEach` 中调用 `waitForAppReady`
|
||
- [ ] 使用 Page Object 进行页面交互
|
||
- [ ] 使用描述性的测试名称
|
||
- [ ] 添加适当的断言
|
||
- [ ] 处理可能的异步操作
|
||
- [ ] 考虑测试失败时的清理
|
||
|
||
### 调试技巧
|
||
|
||
```typescript
|
||
// 截图调试
|
||
await mainWindow.screenshot({ path: 'debug.png' })
|
||
|
||
// 打印页面 HTML
|
||
console.log(await mainWindow.content())
|
||
|
||
// 暂停测试进行调试
|
||
await mainWindow.pause()
|
||
|
||
// 打印元素数量
|
||
console.log(await page.locator('.element').count())
|
||
```
|
||
|
||
---
|
||
|
||
## 配置文件
|
||
|
||
主要配置在项目根目录的 `playwright.config.ts`:
|
||
|
||
- `testDir`: 测试目录 (`./tests/e2e/specs`)
|
||
- `timeout`: 测试超时 (60秒)
|
||
- `workers`: 并发数 (1,Electron 需要串行)
|
||
- `retries`: 重试次数 (CI 环境下为 2)
|
||
|
||
---
|
||
|
||
## 相关文档
|
||
|
||
- [Playwright 官方文档](https://playwright.dev/docs/intro)
|
||
- [Playwright Electron 测试](https://playwright.dev/docs/api/class-electron)
|
||
- [Page Object Model](https://playwright.dev/docs/pom)
|