Skip to content

E2E 测试最佳实践

国际化项目的 E2E 测试规范


核心原则:禁止使用明文选择器

由于本项目是国际化项目,支持多语言切换,严禁使用硬编码的文本内容作为元素选择器

错误示例 ❌

typescript
// 严禁:使用硬编码的中文文本
await page.click('text=转账')
await page.click('text=确认')
await page.waitForSelector('text=发送成功')
await page.click('button:has-text("下一步")')

正确示例 ✅

typescript
// 正确:使用 data-testid 属性
await page.click('[data-testid="send-button"]')
await page.click('[data-testid="confirm-button"]')
await page.waitForSelector('[data-testid="success-message"]')
await page.click('[data-testid="next-step-button"]')

data-testid 命名规范

命名格式

采用 kebab-case(短横线分隔),格式为:{组件/功能}-{元素类型}

chain-selector          // 链选择器
send-button            // 发送按钮
receive-button         // 收款按钮
confirm-dialog         // 确认对话框
pattern-lock           // 图案锁
mnemonic-textarea      // 助记词文本框
wallet-card            // 钱包卡片
token-list             // 代币列表

页面级别命名

页面级别的元素添加页面前缀:

home-balance           // 首页余额显示
send-amount-input      // 发送页金额输入
receive-qrcode         // 收款页二维码
settings-language      // 设置页语言选项

动态元素命名

对于列表项等动态元素,使用 data-testid 配合索引或唯一标识:

tsx
// 列表项
{tokens.map((token, index) => (
  <div data-testid={`token-item-${index}`}>
    {/* 或使用唯一ID */}
  </div>
))}

// 链选项
{chains.map(chain => (
  <div data-testid={`chain-option-${chain.id}`}>
    {chain.name}
  </div>
))}

必须添加 data-testid 的元素

以下类型的元素必须添加 data-testid

1. 交互元素

元素类型命名示例
按钮send-button, confirm-button, cancel-button
输入框pattern-lock, amount-input, address-input
选择器chain-selector, token-selector, language-selector
开关dark-mode-toggle, notification-toggle
链接settings-link, help-link

2. 内容展示

元素类型命名示例
余额显示balance-display, fiat-value
地址显示address-display, qrcode-address
状态指示loading-spinner, success-message, error-message
列表容器token-list, transaction-list, wallet-list

3. 容器/区域

元素类型命名示例
页面标题page-title, section-title
Sheet/Modalchain-sheet, confirm-dialog, wallet-lock-modal
导航bottom-tabs, back-button, nav-header

组件实现示例

PageHeader 组件

tsx
export function PageHeader({ title, onBack }: PageHeaderProps) {
  return (
    <header data-testid="page-header">
      {onBack && (
        <button data-testid="back-button" onClick={onBack}>
          <IconArrowLeft />
        </button>
      )}
      <h1 data-testid="page-title">{title}</h1>
    </header>
  )
}

GradientButton 组件

tsx
export function GradientButton({ 
  children, 
  'data-testid': testId,
  ...props 
}: GradientButtonProps) {
  return (
    <button data-testid={testId} {...props}>
      {children}
    </button>
  )
}

列表组件

tsx
export function TokenList({ tokens }: TokenListProps) {
  return (
    <div data-testid="token-list">
      {tokens.map((token, index) => (
        <TokenItem
          key={token.symbol}
          data-testid={`token-item-${index}`}
          token={token}
        />
      ))}
    </div>
  )
}

E2E 测试编写规范

测试文件结构

typescript
import { test, expect } from '@playwright/test'

// 使用辅助函数设置测试环境
async function setupTestWallet(page: Page) {
  await page.addInitScript((data) => {
    localStorage.setItem('bfm_wallets', JSON.stringify(data))
  }, TEST_WALLET_DATA)
  await page.goto('/')
  await page.waitForLoadState('networkidle')
}

test.describe('功能模块名称', () => {
  test.beforeEach(async ({ page }) => {
    await page.addInitScript(() => localStorage.clear())
  })

  test('测试用例描述', async ({ page }) => {
    await setupTestWallet(page)
    
    // 使用 data-testid 进行元素定位
    await page.click('[data-testid="send-button"]')
    await page.waitForSelector('[data-testid="page-title"]')
    
    // 使用 expect 进行断言
    await expect(page.locator('[data-testid="balance-display"]')).toBeVisible()
  })
})

等待策略

typescript
// 推荐:等待特定 testid 元素
await page.waitForSelector('[data-testid="success-message"]')

// 推荐:等待元素状态变化
await page.waitForSelector('[data-testid="modal"]', { state: 'hidden' })

// 推荐:等待网络空闲(用于异步数据加载)
await page.waitForLoadState('networkidle')

// 避免:使用固定延时
// await page.waitForTimeout(1000) // ❌

表单交互

typescript
// 输入框使用 testid
await page.fill('[data-testid="pattern-lock-input"]', '0,1,2,5,8')
await page.fill('[data-testid="pattern-lock-confirm"]', '0,1,2,5,8')

// 按钮点击使用 testid
await page.click('[data-testid="submit-button"]')

已定义的 data-testid 列表

全局组件

testid组件/元素所在文件
chain-selector链选择器按钮HomeTab.tsx
chain-sheet链选择 SheetChainSelectorSheet.tsx
send-button发送/转账按钮HomeTab.tsx
receive-button收款按钮HomeTab.tsx
page-title页面标题PageHeader.tsx
back-button返回按钮PageHeader.tsx

钱包相关

testid组件/元素所在文件
wallet-card钱包卡片WalletCard.tsx
address-display地址显示AddressDisplay.tsx
balance-display余额显示BalanceDisplay.tsx
token-list代币列表TokenList.tsx

导入/创建流程

testid组件/元素所在文件
create-wallet-button创建钱包按钮WelcomePage.tsx
import-wallet-button导入钱包按钮WelcomePage.tsx
key-type-selector密钥类型选择OnboardingRecover.tsx
mnemonic-textarea助记词输入框MnemonicInput.tsx
pattern-lock图案锁组件PatternLock.tsx
pattern-lock-confirm确认图案锁PatternLockSetup.tsx
continue-button继续按钮多个页面
success-message成功提示ImportSuccess.tsx
enter-wallet-button进入钱包按钮ImportSuccess.tsx

审查清单

在提交 E2E 测试代码前,请确认:

  • [ ] 所有元素定位都使用 data-testid
  • [ ] 没有使用 text=xxx:has-text("xxx") 选择器
  • [ ] 相关组件已添加必要的 data-testid 属性
  • [ ] testid 命名遵循 kebab-case 规范
  • [ ] 新增的 testid 已添加到上方列表

迁移指南

对于现有使用明文选择器的测试:

  1. 找到所有 text=:has-text() 选择器
  2. 确定对应的组件文件
  3. 在组件中添加 data-testid 属性
  4. 更新测试代码使用新的 testid
  5. 本地运行测试确认通过
  6. 更新 testid 文档列表

Released under the MIT License.