cherry-studio/tests/e2e
George·Dong 2a31fa2ad5
refactor: switch yarn to pnpm (#12260)
* refactor: switch workflows from yarn to pnpm

Replace Yarn usage with pnpm in CI workflows to standardize package
management and leverage pnpm's store/cache behavior.

- Use pnpm/action-setup to install pnpm (v) instead of enabling corepack
  and preparing Yarn.
- Retrieve pnpm store path and update cache actions to cache the pnpm
  store and use pnpm-lock.yaml for cache keys and restores.
- Replace yarn commands with pnpm equivalents across workflows:
  install, i18n:sync/translate, format, build:* and tsx invocation.
- Avoid committing lockfile changes by resetting pnpm-lock.yaml instead
  of yarn.lock when checking for changes.
- Update install flags: use pnpm install --frozen-lockfile / --install
  semantics where appropriate.

These changes unify dependency tooling, improve caching correctness,
and ensure CI uses pnpm-specific lockfile and cache paths.

* build: switch pre-commit hook to pnpm lint-staged

Update .husky/pre-commit to run pnpm lint-staged instead of yarn.
This aligns the pre-commit hook with the project's package manager
and ensures lint-staged runs using pnpm's environment and caching.

* chore(ci): remove pinned pnpm version from GH Action steps

Remove the explicit `with: version: 9` lines from multiple GitHub Actions workflows
(auto-i18n.yml, nightly-build.yml, pr-ci.yml, update-app-upgrade-config.yml,
sync-to-gitcode.yml, release.yml). The workflows still call `pnpm/action-setup@v4`
but no longer hardcode a pnpm version.

This simplifies maintenance and allows the action to resolve an appropriate pnpm
version (or use its default) without needing updates whenever the pinned
version becomes outdated. It reduces churn when bumping pnpm across CI configs
and prevents accidental pin drift between workflow files.

* build: Update pnpm to 10.27.0 and add onlyBuiltDependencies config

* Update @cherrystudio/openai to 6.15.0 and consolidate overrides

* Add @langchain/core to overrides

* Add override for openai-compatible 1.0.27

* build: optimize pnpm config and add missing dependencies

- Comment out shamefully-hoist in .npmrc for better pnpm compatibility
- Add React-related packages to optimizeDeps in electron.vite.config.ts
- Add missing peer dependencies and packages that were previously hoisted

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* build: refine pnpm configuration and dependency management

- Simplify .npmrc to only essential electron mirror config
- Move platform-specific dependencies to devDependencies
- Pin sharp version to 0.34.3 for consistency
- Update sharp-libvips versions to 1.2.4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* reduce app size

* format

* build: remove unnecessary disableOxcRecommendation option from react plugin configuration

* docs: Replace yarn commands with pnpm in documentation and scripts

* Revert "build: optimize pnpm config and add missing dependencies"

This reverts commit acffad31f8.

* build: import dependencies from yarn.lock

* build: Add some phantom dependencies and reorganize dependencies

* build: Keep consistent by removing types of semver

It's not in the previous package.json

* build: Add some phantom dependencies

Keep same version with yarn.lock

* build: Add form-data dependency version 4.0.4

* Add chalk dependency

* build: downgrade some dependencies

Reference: .yarn-state-copy.yml. These phantom dependencies should use top-level package of that version in node_modules

* build: Add phantom dependencies

* build: pin tiptap dependencies to exact versions

Ensure consistent dependency resolution by removing caret ranges and pinning all @tiptap packages to exact version 3.2.0

* chore: pin embedjs dependencies to exact versions

* build: pin @modelcontextprotocol/sdk to exact version 1.23.0

Remove caret from version specifier to prevent automatic upgrades and ensure consistent dependencies

* chore: update @types/node dependency to 22.17.2

Update package.json and pnpm-lock.yaml to use @types/node version 22.17.2 instead of 22.19.3 to maintain consistency across dependencies

* build: move some dependencies to dev deps and pin dependency versions to exact numbers

Remove caret (^) from version ranges to ensure consistent dependency resolution across environments

* chore: move dependencies from prod to dev and update lockfile

Move @ant-design/icons, chalk, form-data, and open from dependencies to devDependencies
Update pnpm-lock.yaml to reflect dependency changes

* build: update package dependencies

- Add new dependencies: md5, @libsql/win32-x64-msvc, @strongtz/win32-arm64-msvc, bonjour-service, emoji-picker-element-data, gray-matter, js-yaml
- Remove redundant dependencies from devDependencies

* build: add cors, katex and pako dependencies

add new dependencies to support cross-origin requests, mathematical notation rendering and data compression

* move some js deps to dev deps

* test: update snapshot tests for Spinner and InputEmbeddingDimension

* chore: exclude .zed directory from biome formatting

* Update @ai-sdk/openai-compatible patch hash

* chore: update @kangfenmao/keyv-storage to version 0.1.3 in package.json and pnpm-lock.yaml

---------

Co-authored-by: icarus <eurfelux@gmail.com>
Co-authored-by: beyondkmp <beyondkmp@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
2026-01-05 22:16:34 +08:00
..
fixtures feat(test): e2e framework (#11494) 2025-11-27 19:52:31 +08:00
pages feat(test): e2e framework (#11494) 2025-11-27 19:52:31 +08:00
specs feat(test): e2e framework (#11494) 2025-11-27 19:52:31 +08:00
utils feat(test): e2e framework (#11494) 2025-11-27 19:52:31 +08:00
global-setup.ts feat(test): e2e framework (#11494) 2025-11-27 19:52:31 +08:00
global-teardown.ts feat(test): e2e framework (#11494) 2025-11-27 19:52:31 +08:00
README.md refactor: switch yarn to pnpm (#12260) 2026-01-05 22:16:34 +08:00

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

运行命令

# 运行所有 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 应用启动较慢,可在测试中增加超时时间:

test.setTimeout(60000) // 60秒

AI 助手指南:创建新测试用例

以下内容供 AI 助手(如 Claude、GPT在创建新测试用例时参考。

基本原则

  1. 使用 Page Object Model (POM):所有页面交互应通过 pages/ 目录下的页面对象进行
  2. 使用自定义 fixture:从 ../fixtures/electron.fixture 导入 testexpect
  3. 等待策略:使用 utils/wait-helpers.ts 中的等待函数,避免硬编码 waitForTimeout
  4. 测试独立性:每个测试应该独立运行,不依赖其他测试的状态

创建新测试文件

// 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 }) => {
    // 测试逻辑
  })
})

创建新页面对象

// 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()
  }
}

选择器最佳实践

// 优先级从高到低:

// 1. data-testid最稳定但需要在源码中添加
page.locator('[data-testid="submit-button"]')

// 2. 语义化角色
page.locator('button[role="submit"]')
page.locator('[aria-label="Send message"]')

// 3. 类名模糊匹配(适应 CSS Modules / styled-components
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除非必要

等待策略

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' })

断言模式

// 使用 Playwright 的自动重试断言
await expect(page.locator('.element')).toBeVisible()
await expect(page.locator('.element')).toHaveText('expected text')
await expect(page.locator('.element')).toHaveCount(3)

// 检查 URLHashRouter
await expect(page).toHaveURL(/.*#\/settings.*/)

// 软断言(不会立即失败)
await expect.soft(page.locator('.element')).toBeVisible()

// 自定义超时
await expect(page.locator('.slow-element')).toBeVisible({ timeout: 30000 })

处理 Electron 特性

// 访问 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 进行页面交互
  • 使用描述性的测试名称
  • 添加适当的断言
  • 处理可能的异步操作
  • 考虑测试失败时的清理

调试技巧

// 截图调试
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: 并发数 (1Electron 需要串行)
  • retries: 重试次数 (CI 环境下为 2)

相关文档