# Mini UI 包测试规范

**版本**: 1.0
**创建日期**: 2025-12-26
**适用范围**: 所有 Taro 小程序 UI 包测试

## 概述

本文档规定了 Mini UI 包的测试编写规范，基于故事 017.003 的实施经验总结。遵循这些规范可以确保测试的一致性、可维护性和正确性。

## 核心原则

### 1. 使用 Jest，不是 Vitest

**重要**: Mini UI 包使用 **Jest** 测试框架，不是 Vitest。

```javascript
// jest.config.cjs
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  // ...
}
```

### 2. 使用共享的 mini-testing-utils

**关键原则**: 不要在每个 UI 包中重写 Taro mock，直接使用 `@d8d/mini-testing-utils` 提供的共享 mock。

```javascript
// jest.config.cjs
module.exports = {
  setupFilesAfterEnv: ['@d8d/mini-testing-utils/testing/setup'],
  moduleNameMapper: {
    '^@tarojs/taro$': '@d8d/mini-testing-utils/testing/taro-api-mock.ts',
  }
}
```

### 3. 使用真实的 React Query

**关键原则**: 使用真实的 React Query（不要 mock），以便验证 RPC 类型推断。

```typescript
// ✅ 正确: 使用真实的 React Query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const createTestWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false, staleTime: Infinity }
    }
  })
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}
```

```typescript
// ❌ 错误: 不要 mock React Query
jest.mock('@tanstack/react-query', () => ({
  useQuery: jest.fn()
}))
```

## Jest 配置规范

### 标准 jest.config.cjs 模板

```javascript
// jest.config.cjs
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',

  // 使用 mini-testing-utils 提供的共享 setup
  setupFilesAfterEnv: ['@d8d/mini-testing-utils/testing/setup'],

  moduleNameMapper: {
    // 测试文件中的别名映射（仅用于测试文件）
    '^@/(.*)$': '<rootDir>/src/$1',
    '^~/(.*)$': '<rootDir>/tests/$1',

    // Taro API 重定向到共享 mock
    '^@tarojs/taro$': '@d8d/mini-testing-utils/testing/taro-api-mock.ts',

    // 样式和文件映射
    '\\.(css|less|scss|sass)$': '@d8d/mini-testing-utils/testing/style-mock.js',
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '@d8d/mini-testing-utils/testing/file-mock.js'
  },

  testMatch: [
    '<rootDir>/tests/**/*.spec.{ts,tsx}',
    '<rootDir>/tests/**/*.test.{ts,tsx}'
  ],

  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/index.{ts,tsx}',
    '!src/**/*.stories.{ts,tsx}'
  ],

  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],

  testPathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/coverage/'
  ],

  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
    '^.+\\.(js|jsx)$': 'babel-jest'
  },

  transformIgnorePatterns: [
    '/node_modules/(?!(swiper|@tarojs)/)'
  ],

  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
}
```

## 测试文件结构规范

```
mini-ui-packages/<package-name>/
└── tests/
    ├── unit/                      # 单元测试
    │   └── components/
    │       ├── ComponentName.test.tsx
    │       └── ...
    └── pages/                     # 页面组件测试
        └── PageName/
            └── PageName.test.tsx
```

## 测试编写规范

### 1. 组件测试模板

```typescript
/**
 * ComponentName 组件测试
 */
import React from 'react'
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import ComponentName, { PropsInterface } from '../../../src/components/ComponentName'
import Taro from '@tarojs/taro'

describe('ComponentName', () => {
  const mockProps: PropsInterface = {
    // mock props
  }

  beforeEach(() => {
    // 清理 Taro API mock
    ;(Taro.someApi as jest.Mock).mockClear()
  })

  it('应该正确渲染', () => {
    render(<ComponentName {...mockProps} />)
    expect(screen.getByText('expected text')).toBeInTheDocument()
  })
})
```

### 2. 页面组件测试模板

```typescript
/**
 * PageName 页面测试
 * 使用真实的React Query和RPC类型验证
 */
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import PageName from '../../../src/pages/PageName/PageName'
import { apiClient } from '../../../src/api'
import { useRequireAuth } from '@d8d/xxx-auth-ui/hooks'
import Taro from '@tarojs/taro'

// Mock auth hooks
jest.mock('@d8d/xxx-auth-ui/hooks', () => ({
  useRequireAuth: jest.fn()
}))

// Mock API client - 使用真实的RPC类型
jest.mock('../../../src/api', () => ({
  apiClient: {
    route: {
      $get: jest.fn()
    }
  }
}))

const createMockResponse = <T,>(status: number, data: T) => ({
  status,
  ok: status >= 200 && status < 300,
  json: async () => data
})

const createTestWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        staleTime: Infinity,
        refetchOnWindowFocus: false
      }
    }
  })

  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

describe('PageName', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    // Mock useRequireAuth to do nothing (user is authenticated)
    ;(useRequireAuth as jest.Mock).mockImplementation(() => {})
    // Reset Taro API mocks
    ;(Taro.setNavigationBarTitle as jest.Mock).mockClear()
    ;(Taro.showToast as jest.Mock).mockClear()
  })

  it('应该渲染带有正确数据的页面', async () => {
    // Mock API calls - 使用符合RPC类型的响应
    ;(apiClient.route.$get as jest.Mock).mockResolvedValue(
      createMockResponse(200, mockData)
    )

    const wrapper = createTestWrapper()
    render(<PageName />, { wrapper })

    await waitFor(() => {
      expect(screen.getByText('expected text')).toBeInTheDocument()
    })
  })
})
```

### 3. Mock RPC 响应规范

```typescript
// ✅ 正确: 使用 Mock 响应工具函数
const createMockResponse = <T,>(status: number, data: T) => ({
  status,
  ok: status >= 200 && status < 300,
  json: async () => data
})

// 使用示例
; (apiClient.route.$get as jest.Mock).mockResolvedValue(
  createMockResponse(200, { name: '张三', age: 35 })
)
```

### 4. Taro API 使用规范

```typescript
// ✅ 正确: 直接使用 Taro API，不需要自定义 mock
import Taro from '@tarojs/taro'

describe('Component', () => {
  beforeEach(() => {
    // 清理 mock
    ;(Taro.showToast as jest.Mock).mockClear()
  })

  it('应该调用Taro API', () => {
    render(<Component />)
    expect(Taro.showToast).toHaveBeenCalledWith({ title: '成功' })
  })
})

// ❌ 错误: 不要在测试文件中自定义 Taro mock
jest.mock('@tarojs/taro', () => ({
  default: {
    showToast: jest.fn()  // 错误！应该使用 mini-testing-utils 的共享 mock
  }
}))
```

## 测试最佳实践

### 1. RPC 类型推断验证

使用真实的 React Query 来验证 RPC 类型推断：

```typescript
// ✅ 正确: 测试中使用真实的 RPC 类型
const mockData = {
  name: '张三',
  age: 35,
  // 类型错误会在编译时被捕获
  wrongField: 'type error'  // TypeScript 会报错
}

; (apiClient.route.$get as jest.Mock).mockResolvedValue(
  createMockResponse(200, mockData)
)
```

### 2. 异步测试

使用 `waitFor` 处理异步状态更新：

```typescript
it('应该在加载完成后渲染数据', async () => {
  ; (apiClient.route.$get as jest.Mock).mockResolvedValue(
    createMockResponse(200, mockData)
  )

  render(<Page />, { wrapper })

  await waitFor(() => {
    expect(screen.getByText('data')).toBeInTheDocument()
  })
})
```

### 3. 用户交互测试

```typescript
it('应该处理点击事件', () => {
  const { fireEvent } = require('@testing-library/react')

  render(<Component />)

  const button = screen.getByText('Click me')
  fireEvent.click(button)

  expect(Taro.someApi).toHaveBeenCalled()
})
```

## 常见错误

### 错误 1: 重写 Taro mock

```typescript
// ❌ 错误: 在测试文件中重写 Taro mock
jest.mock('@tarojs/taro', () => ({
  default: {
    showToast: jest.fn()
  }
}))

// ✅ 正确: 直接导入使用
import Taro from '@tarojs/taro'
// mock 在 mini-testing-utils/testing/setup.ts 中已配置
```

### 错误 2: Mock React Query

```typescript
// ❌ 错误: Mock React Query
jest.mock('@tanstack/react-query', () => ({
  useQuery: jest.fn(() => ({ data: mockData }))
}))

// ✅ 正确: 使用真实的 React Query
const wrapper = createTestWrapper()
render(<Page />, { wrapper })
```

### 错误 3: 在 UI 包源代码中使用别名

```typescript
// ❌ 错误: 源代码中使用别名
import { apiClient } from '@/api'
import { Component } from '@/components'

// ✅ 正确: 源代码中使用相对路径
import { apiClient } from '../api'
import { Component } from '../components'
```

### 错误 4: 忘记清理 mock

```typescript
// ❌ 错误: 忘记清理 mock
describe('Component', () => {
  it('test 1', () => {
    expect(Taro.showToast).toHaveBeenCalledTimes(1)
  })

  it('test 2', () => {
    // test 1 的 mock 调用会影响 test 2
    expect(Taro.showToast).toHaveBeenCalledTimes(1)  // 可能失败
  })
})

// ✅ 正确: 在 beforeEach 中清理 mock
describe('Component', () => {
  beforeEach(() => {
    ; (Taro.showToast as jest.Mock).mockClear()
  })

  it('test 1', () => {
    expect(Taro.showToast).toHaveBeenCalledTimes(1)
  })

  it('test 2', () => {
    // mock 已清理，计数重新开始
    expect(Taro.showToast).toHaveBeenCalledTimes(1)
  })
})
```

## 测试执行

```bash
# 运行所有测试
cd mini-ui-packages/<package-name> && pnpm test

# 运行特定测试
pnpm test --testNamePattern="ComponentName"

# 生成覆盖率报告
pnpm test:coverage

# 监听模式
pnpm test:watch
```

## mini-testing-utils 扩展

如果需要添加新的 Taro API mock，在 `mini-testing-utils/testing/taro-api-mock.ts` 中添加：

```typescript
// 1. 添加 mock 函数声明
export const mockNewApi = jest.fn()

// 2. 在默认导出中添加
export default {
  // ...
  newApi: mockNewApi
}

// 3. 在命名导出中添加
export {
  // ...
  mockNewApi as newApi
}
```

## 参考资料

- [Jest 官方文档](https://jestjs.io/)
- [Testing Library](https://testing-library.com/)
- [Taro 官方文档](https://taro-docs.jd.com/)
- [mini-ui-package-standards.md](./mini-ui-package-standards.md) - Mini UI 包开发规范
- [testing-strategy.md](./testing-strategy.md) - 通用测试策略

## 版本历史

| 版本 | 日期 | 描述 | 作者 |
|------|------|------|------|
| 1.0 | 2025-12-26 | 初始版本，基于故事 017.003 实施经验 | Dev Agent |
