فهرست منبع

✨ feat(test): 添加 Jest 测试配置和组件测试

- 新增 Jest 配置文件,配置 TypeScript 支持和 DOM 环境
- 添加测试相关的 npm 脚本命令
- 创建 Taro API mock 文件用于组件测试
- 添加 AreaPicker 组件完整测试用例
- 添加 Button 组件基础测试用例
- 添加测试示例文件
- 配置测试环境设置,包括 Taro 组件 mock 和浏览器 API mock
yourname 3 هفته پیش
والد
کامیت
b279f47e07

+ 38 - 0
mini/jest.config.js

@@ -0,0 +1,38 @@
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'jsdom',
+  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1',
+    '^~/(.*)$': '<rootDir>/tests/$1',
+    '^@tarojs/taro$': '<rootDir>/tests/__mocks__/taroMock.ts',
+    '\.(css|less|scss|sass)$': '<rootDir>/tests/__mocks__/styleMock.js',
+    '\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
+      '<rootDir>/tests/__mocks__/fileMock.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)$': 'babel-jest',
+    '^+\.(js|jsx)$': 'babel-jest'
+  },
+  transformIgnorePatterns: [
+    '/node_modules/(?!(swiper|@tarojs)/)'
+  ],
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
+}

+ 13 - 1
mini/package.json

@@ -31,6 +31,11 @@
     "dev:qq": "npm run build:qq -- --watch",
     "dev:jd": "npm run build:jd -- --watch",
     "dev:harmony-hybrid": "npm run build:harmony-hybrid -- --watch",
+    "test": "jest",
+    "test:watch": "jest --watch",
+    "test:coverage": "jest --coverage",
+    "test:components": "jest tests/components",
+    "test:pages": "jest tests/pages",
     "typecheck": "tsc --noEmit --project ."
   },
   "browserslist": {
@@ -111,6 +116,13 @@
     "typescript": "^5.4.5",
     "weapp-tailwindcss": "^4.2.5",
     "webpack": "5.91.0",
-    "webpack-plugin-iframe-communicator": "^0.0.10"
+    "webpack-plugin-iframe-communicator": "^0.0.10",
+    "@testing-library/jest-dom": "^6.8.0",
+    "@testing-library/react": "^16.3.0",
+    "@testing-library/user-event": "^14.6.1",
+    "@types/jest": "^29.5.14",
+    "jest": "^30.2.0",
+    "jest-environment-jsdom": "^29.7.0",
+    "ts-jest": "^29.4.5"
   }
 }

+ 1 - 0
mini/tests/__mocks__/fileMock.js

@@ -0,0 +1 @@
+module.exports = 'test-file-stub'

+ 1 - 0
mini/tests/__mocks__/styleMock.js

@@ -0,0 +1 @@
+module.exports = {}

+ 100 - 0
mini/tests/__mocks__/taroMock.ts

@@ -0,0 +1,100 @@
+/**
+ * Taro API Mock 文件
+ * 通过 jest.config.js 的 moduleNameMapper 重定向 @tarojs/taro 到这里
+ */
+
+// 创建所有 Taro API 的 mock 函数
+export const mockShowToast = jest.fn()
+export const mockShowLoading = jest.fn()
+export const mockHideLoading = jest.fn()
+export const mockNavigateTo = jest.fn()
+export const mockNavigateBack = jest.fn()
+export const mockSwitchTab = jest.fn()
+export const mockShowModal = jest.fn()
+export const mockReLaunch = jest.fn()
+export const mockOpenCustomerServiceChat = jest.fn()
+export const mockUseRouter = jest.fn()
+export const mockRequestPayment = jest.fn()
+export const mockGetEnv = jest.fn()
+export const mockUseLoad = jest.fn()
+export const mockUseShareAppMessage = jest.fn()
+export const mockUseShareTimeline = jest.fn()
+export const mockGetCurrentInstance = jest.fn()
+
+// 环境类型常量
+export const ENV_TYPE = {
+  WEAPP: 'WEAPP',
+  WEB: 'WEB',
+  RN: 'RN',
+  SWAN: 'SWAN',
+  ALIPAY: 'ALIPAY',
+  TT: 'TT',
+  QQ: 'QQ',
+  JD: 'JD',
+  HARMONY: 'HARMONY'
+}
+
+// 导出所有 mock 函数,便于在测试中访问
+export default {
+  // UI 相关
+  showToast: mockShowToast,
+  showLoading: mockShowLoading,
+  hideLoading: mockHideLoading,
+  showModal: mockShowModal,
+
+  // 导航相关
+  navigateTo: mockNavigateTo,
+  navigateBack: mockNavigateBack,
+  switchTab: mockSwitchTab,
+  reLaunch: mockReLaunch,
+  useRouter: () => mockUseRouter(),
+  useLoad: (callback: any) => mockUseLoad(callback),
+
+  // 微信相关
+  openCustomerServiceChat: mockOpenCustomerServiceChat,
+  requestPayment: mockRequestPayment,
+
+  // 系统信息
+  getSystemInfoSync: () => ({
+    statusBarHeight: 20
+  }),
+  getMenuButtonBoundingClientRect: () => ({
+    width: 87,
+    height: 32,
+    top: 48,
+    right: 314,
+    bottom: 80,
+    left: 227
+  }),
+  getEnv: mockGetEnv,
+
+  // 分享相关
+  useShareAppMessage: mockUseShareAppMessage,
+  useShareTimeline: mockUseShareTimeline,
+
+  // 实例相关
+  getCurrentInstance: mockGetCurrentInstance,
+
+  // 环境类型常量
+  ENV_TYPE
+}
+
+// 为命名导入导出所有函数
+export {
+  mockShowToast as showToast,
+  mockShowLoading as showLoading,
+  mockHideLoading as hideLoading,
+  mockShowModal as showModal,
+  mockNavigateTo as navigateTo,
+  mockNavigateBack as navigateBack,
+  mockSwitchTab as switchTab,
+  mockReLaunch as reLaunch,
+  mockUseRouter as useRouter,
+  mockUseLoad as useLoad,
+  mockOpenCustomerServiceChat as openCustomerServiceChat,
+  mockRequestPayment as requestPayment,
+  mockGetEnv as getEnv,
+  mockUseShareAppMessage as useShareAppMessage,
+  mockUseShareTimeline as useShareTimeline,
+  mockGetCurrentInstance as getCurrentInstance
+}

+ 313 - 0
mini/tests/components/AreaPicker.test.tsx

@@ -0,0 +1,313 @@
+import React from 'react'
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { AreaPicker } from '../../src/components/AreaPicker'
+
+// Mock API 客户端
+const mockAreaClient = {
+  provinces: {
+    $get: jest.fn()
+  },
+  cities: {
+    $get: jest.fn()
+  },
+  districts: {
+    $get: jest.fn()
+  }
+}
+
+jest.mock('../../src/api', () => ({
+  areaClient: mockAreaClient
+}))
+
+// 创建测试用的 QueryClient
+const createTestQueryClient = () => new QueryClient({
+  defaultOptions: {
+    queries: {
+      retry: false,
+    },
+  },
+})
+
+// 包装组件
+const Wrapper = ({ children }: { children: React.ReactNode }) => {
+  const queryClient = createTestQueryClient()
+  return (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  )
+}
+
+// 模拟数据
+const mockProvinces = {
+  success: true,
+  data: {
+    provinces: [
+      { id: 1, name: '北京市' },
+      { id: 2, name: '上海市' },
+      { id: 3, name: '广东省' }
+    ]
+  },
+  message: ''
+}
+
+const mockCities = {
+  success: true,
+  data: {
+    cities: [
+      { id: 11, name: '北京市' },
+      { id: 12, name: '朝阳区' },
+      { id: 13, name: '海淀区' }
+    ]
+  },
+  message: ''
+}
+
+const mockDistricts = {
+  success: true,
+  data: {
+    districts: [
+      { id: 101, name: '朝阳区' },
+      { id: 102, name: '海淀区' },
+      { id: 103, name: '西城区' }
+    ]
+  },
+  message: ''
+}
+
+describe('AreaPicker 组件', () => {
+  beforeEach(() => {
+    // 重置所有 mock
+    jest.clearAllMocks()
+
+    // 设置默认的 mock 返回值
+    mockAreaClient.provinces.$get.mockResolvedValue({
+      status: 200,
+      json: async () => mockProvinces
+    })
+
+    mockAreaClient.cities.$get.mockResolvedValue({
+      status: 200,
+      json: async () => mockCities
+    })
+
+    mockAreaClient.districts.$get.mockResolvedValue({
+      status: 200,
+      json: async () => mockDistricts
+    })
+  })
+
+  test('应该正确渲染组件', async () => {
+    const onClose = jest.fn()
+    const onConfirm = jest.fn()
+
+    render(
+      <Wrapper>
+        <AreaPicker
+          visible={true}
+          onClose={onClose}
+          onConfirm={onConfirm}
+        />
+      </Wrapper>
+    )
+
+    // 检查标题
+    expect(screen.getByText('选择地区')).toBeInTheDocument()
+
+    // 检查选择器标签
+    expect(screen.getByText('省份')).toBeInTheDocument()
+    expect(screen.getByText('城市')).toBeInTheDocument()
+    expect(screen.getByText('区县')).toBeInTheDocument()
+
+    // 检查按钮
+    expect(screen.getByText('取消')).toBeInTheDocument()
+    expect(screen.getByText('确定')).toBeInTheDocument()
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(mockAreaClient.provinces.$get).toHaveBeenCalled()
+    })
+  })
+
+  test('应该显示自定义标题', () => {
+    const onClose = jest.fn()
+    const onConfirm = jest.fn()
+
+    render(
+      <Wrapper>
+        <AreaPicker
+          visible={true}
+          onClose={onClose}
+          onConfirm={onConfirm}
+          title="选择出发地"
+        />
+      </Wrapper>
+    )
+
+    expect(screen.getByText('选择出发地')).toBeInTheDocument()
+  })
+
+  test('应该初始化选择值', async () => {
+    const onClose = jest.fn()
+    const onConfirm = jest.fn()
+
+    render(
+      <Wrapper>
+        <AreaPicker
+          visible={true}
+          onClose={onClose}
+          onConfirm={onConfirm}
+          value={[1, 11, 101]}
+        />
+      </Wrapper>
+    )
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(mockAreaClient.provinces.$get).toHaveBeenCalled()
+    })
+
+    // 检查是否调用了城市和区县查询
+    await waitFor(() => {
+      expect(mockAreaClient.cities.$get).toHaveBeenCalledWith({
+        query: { provinceId: 1, page: 1, pageSize: 50 }
+      })
+    })
+
+    await waitFor(() => {
+      expect(mockAreaClient.districts.$get).toHaveBeenCalledWith({
+        query: { cityId: 11, page: 1, pageSize: 50 }
+      })
+    })
+  })
+
+  test('应该处理取消操作', () => {
+    const onClose = jest.fn()
+    const onConfirm = jest.fn()
+
+    render(
+      <Wrapper>
+        <AreaPicker
+          visible={true}
+          onClose={onClose}
+          onConfirm={onConfirm}
+        />
+      </Wrapper>
+    )
+
+    const cancelButton = screen.getByText('取消')
+    fireEvent.click(cancelButton)
+
+    expect(onClose).toHaveBeenCalledTimes(1)
+    expect(onConfirm).not.toHaveBeenCalled()
+  })
+
+  test('应该处理确认操作', async () => {
+    const onClose = jest.fn()
+    const onConfirm = jest.fn()
+
+    render(
+      <Wrapper>
+        <AreaPicker
+          visible={true}
+          onClose={onClose}
+          onConfirm={onConfirm}
+        />
+      </Wrapper>
+    )
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(mockAreaClient.provinces.$get).toHaveBeenCalled()
+    })
+
+    const confirmButton = screen.getByText('确定')
+    fireEvent.click(confirmButton)
+
+    // 由于没有选择任何值,应该传递空数组
+    expect(onConfirm).toHaveBeenCalledWith([], [])
+    expect(onClose).toHaveBeenCalledTimes(1)
+  })
+
+  test('当不可见时应该返回 null', () => {
+    const onClose = jest.fn()
+    const onConfirm = jest.fn()
+
+    const { container } = render(
+      <Wrapper>
+        <AreaPicker
+          visible={false}
+          onClose={onClose}
+          onConfirm={onConfirm}
+        />
+      </Wrapper>
+    )
+
+    // 检查容器是否为空
+    expect(container.firstChild).toBeNull()
+  })
+
+  test('应该处理 API 错误', async () => {
+    // 模拟 API 错误
+    mockAreaClient.provinces.$get.mockResolvedValue({
+      status: 500,
+      json: async () => ({ success: false, message: '服务器错误' })
+    })
+
+    const onClose = jest.fn()
+    const onConfirm = jest.fn()
+
+    render(
+      <Wrapper>
+        <AreaPicker
+          visible={true}
+          onClose={onClose}
+          onConfirm={onConfirm}
+        />
+      </Wrapper>
+    )
+
+    // 组件应该正常渲染,即使 API 调用失败
+    expect(screen.getByText('选择地区')).toBeInTheDocument()
+
+    // 等待 API 调用
+    await waitFor(() => {
+      expect(mockAreaClient.provinces.$get).toHaveBeenCalled()
+    })
+  })
+
+  test('应该正确显示已选择的省市区文本', async () => {
+    const onClose = jest.fn()
+    const onConfirm = jest.fn()
+
+    render(
+      <Wrapper>
+        <AreaPicker
+          visible={true}
+          onClose={onClose}
+          onConfirm={onConfirm}
+          value={[1, 11, 101]}
+        />
+      </Wrapper>
+    )
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(mockAreaClient.provinces.$get).toHaveBeenCalled()
+    })
+
+    await waitFor(() => {
+      expect(mockAreaClient.cities.$get).toHaveBeenCalled()
+    })
+
+    await waitFor(() => {
+      expect(mockAreaClient.districts.$get).toHaveBeenCalled()
+    })
+
+    // 检查显示文本
+    await waitFor(() => {
+      expect(screen.getByText('北京市 北京市 朝阳区')).toBeInTheDocument()
+    })
+  })
+})

+ 37 - 0
mini/tests/components/Button.test.tsx

@@ -0,0 +1,37 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { Button } from '@tarojs/components'
+
+describe('Button 组件测试', () => {
+  test('应该正确渲染按钮', () => {
+    render(<Button>测试按钮</Button>)
+
+    const button = screen.getByRole('button')
+    expect(button).toBeInTheDocument()
+    expect(button).toHaveTextContent('测试按钮')
+  })
+
+  test('应该响应点击事件', () => {
+    const handleClick = jest.fn()
+
+    render(<Button onClick={handleClick}>可点击按钮</Button>)
+
+    const button = screen.getByRole('button')
+    fireEvent.click(button)
+
+    expect(handleClick).toHaveBeenCalledTimes(1)
+  })
+
+  test('应该禁用按钮', () => {
+    render(<Button disabled>禁用按钮</Button>)
+
+    const button = screen.getByRole('button')
+    expect(button).toBeDisabled()
+  })
+
+  test('应该应用自定义类名', () => {
+    render(<Button className="custom-class">自定义按钮</Button>)
+
+    const button = screen.getByRole('button')
+    expect(button).toHaveClass('custom-class')
+  })
+})

+ 43 - 0
mini/tests/example.test.tsx

@@ -0,0 +1,43 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { Text, View } from '@tarojs/components'
+
+// 简单的测试组件
+const TestComponent = () => {
+  return (
+    <View className="test-component">
+      <Text className="btn">点击我</Text>
+    </View>
+  )
+}
+
+describe('Taro 组件测试示例', () => {
+  test('应该正确渲染组件', () => {
+    render(<TestComponent />)
+
+    const button = screen.getByText('点击我')
+    expect(button).toBeInTheDocument()
+    expect(button).toHaveClass('btn')
+  })
+
+  test('应该响应点击事件', () => {
+    const handleClick = jest.fn()
+
+    const InteractiveComponent = () => (
+      <View className="test-component">
+        <Text className="btn" onClick={handleClick}>点击我</Text>
+      </View>
+    )
+
+    render(<InteractiveComponent />)
+
+    const button = screen.getByText('点击我')
+    fireEvent.click(button)
+
+    expect(handleClick).toHaveBeenCalledTimes(1)
+  })
+
+  test('应该匹配快照', () => {
+    const { container } = render(<TestComponent />)
+    expect(container.firstChild).toMatchSnapshot()
+  })
+})

+ 432 - 0
mini/tests/setup.ts

@@ -0,0 +1,432 @@
+import '@testing-library/jest-dom'
+
+/* eslint-disable react/display-name */
+
+// 设置环境变量
+process.env.TARO_ENV = 'h5'
+process.env.TARO_PLATFORM = 'web'
+process.env.SUPPORT_TARO_POLYFILL = 'disabled'
+
+// Mock Taro 组件
+// eslint-disable-next-line react/display-name
+jest.mock('@tarojs/components', () => {
+  const React = require('react')
+  const MockView = React.forwardRef((props: any, ref: any) => {
+    const { children, ...restProps } = props
+    return React.createElement('div', { ...restProps, ref }, children)
+  })
+  MockView.displayName = 'MockView'
+
+  const MockScrollView = React.forwardRef((props: any, ref: any) => {
+    const {
+      children,
+      onScroll,
+      onTouchStart,
+      onScrollEnd,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      scrollY,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      showScrollbar,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      scrollTop,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      scrollWithAnimation,
+      ...restProps
+    } = props
+    return React.createElement('div', {
+      ...restProps,
+      ref,
+      onScroll: (e: any) => {
+        if (onScroll) onScroll(e)
+      },
+      onTouchStart: (e: any) => {
+        if (onTouchStart) onTouchStart(e)
+      },
+      onTouchEnd: () => {
+        if (onScrollEnd) onScrollEnd()
+      },
+      style: {
+        overflow: 'auto',
+        height: '200px',
+        ...restProps.style
+      }
+    }, children)
+  })
+  MockScrollView.displayName = 'MockScrollView'
+
+  return {
+    View: MockView,
+    ScrollView: MockScrollView,
+    Text: (() => {
+      const MockText = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('span', { ...restProps, ref }, children)
+      })
+      MockText.displayName = 'MockText'
+      return MockText
+    })(),
+    Button: (() => {
+      const MockButton = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('button', { ...restProps, ref }, children)
+      })
+      MockButton.displayName = 'MockButton'
+      return MockButton
+    })(),
+    Input: (() => {
+      const MockInput = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('input', { ...restProps, ref })
+      })
+      MockInput.displayName = 'MockInput'
+      return MockInput
+    })(),
+    Textarea: (() => {
+      const MockTextarea = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('textarea', { ...restProps, ref }, children)
+      })
+      MockTextarea.displayName = 'MockTextarea'
+      return MockTextarea
+    })(),
+    Image: (() => {
+      const MockImage = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('img', { ...restProps, ref })
+      })
+      MockImage.displayName = 'MockImage'
+      return MockImage
+    })(),
+    Form: (() => {
+      const MockForm = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('form', { ...restProps, ref }, children)
+      })
+      MockForm.displayName = 'MockForm'
+      return MockForm
+    })(),
+    Label: (() => {
+      const MockLabel = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('label', { ...restProps, ref }, children)
+      })
+      MockLabel.displayName = 'MockLabel'
+      return MockLabel
+    })(),
+    Picker: (() => {
+      const MockPicker = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('div', { ...restProps, ref }, children)
+      })
+      MockPicker.displayName = 'MockPicker'
+      return MockPicker
+    })(),
+    Switch: (() => {
+      const MockSwitch = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('input', { type: 'checkbox', ...restProps, ref })
+      })
+      MockSwitch.displayName = 'MockSwitch'
+      return MockSwitch
+    })(),
+    Slider: (() => {
+      const MockSlider = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('input', { type: 'range', ...restProps, ref })
+      })
+      MockSlider.displayName = 'MockSlider'
+      return MockSlider
+    })(),
+    Radio: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('input', { type: 'radio', ...restProps, ref }, children)
+    }),
+    RadioGroup: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Checkbox: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('input', { type: 'checkbox', ...restProps, ref }, children)
+    }),
+    CheckboxGroup: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Progress: React.forwardRef((props: any, ref: any) => {
+      const { ...restProps } = props
+      return React.createElement('progress', { ...restProps, ref })
+    }),
+    RichText: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    MovableArea: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    MovableView: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Swiper: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    SwiperItem: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Navigator: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('a', { ...restProps, ref }, children)
+    }),
+    Audio: React.forwardRef((props: any, ref: any) => {
+      const { ...restProps } = props
+      return React.createElement('audio', { ...restProps, ref })
+    }),
+    Video: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('video', { ...restProps, ref }, children)
+    }),
+    Camera: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    LivePlayer: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    LivePusher: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Map: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Canvas: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('canvas', { ...restProps, ref }, children)
+    }),
+    OpenData: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    WebView: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('iframe', { ...restProps, ref }, children)
+    }),
+    Ad: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    OfficialAccount: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    CoverView: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    CoverImage: React.forwardRef((props: any, ref: any) => {
+      const { ...restProps } = props
+      return React.createElement('img', { ...restProps, ref })
+    }),
+    FunctionalPageNavigator: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    AdContent: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    MatchMedia: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    PageContainer: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    ShareElement: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    KeyboardAccessory: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    RootPortal: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    PageMeta: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    NavigationBar: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Block: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Import: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Include: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Template: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Slot: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    NativeSlot: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    CustomWrapper: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Editor: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    VoipRoom: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    AdCustom: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    })
+  }
+})
+
+// 模拟 MutationObserver
+// @ts-ignore
+global.MutationObserver = class {
+  disconnect() {}
+  observe(_element: any, _initObject: any) {}
+  takeRecords() { return [] }
+}
+
+// 模拟 IntersectionObserver
+// @ts-ignore
+global.IntersectionObserver = class {
+  constructor(fn: (args: any[]) => void) {
+    setTimeout(() => {
+      fn([{ isIntersecting: true }])
+    }, 1000)
+  }
+
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+  takeRecords() { return [] }
+  root: null = null
+  rootMargin: string = ''
+  thresholds: number[] = []
+}
+
+// 模拟 ResizeObserver
+// @ts-ignore
+global.ResizeObserver = class {
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+}
+
+// 模拟 matchMedia
+Object.defineProperty(window, 'matchMedia', {
+  writable: true,
+  value: jest.fn().mockImplementation(query => ({
+    matches: false,
+    media: query,
+    onchange: null,
+    addListener: jest.fn(), // deprecated
+    removeListener: jest.fn(), // deprecated
+    addEventListener: jest.fn(),
+    removeEventListener: jest.fn(),
+    dispatchEvent: jest.fn(),
+  })),
+})
+
+// 模拟 getComputedStyle
+Object.defineProperty(window, 'getComputedStyle', {
+  value: () => ({
+    getPropertyValue: (prop: string) => {
+      return {
+        'font-size': '16px',
+        'font-family': 'Arial',
+        color: 'rgb(0, 0, 0)',
+        'background-color': 'rgb(255, 255, 255)',
+        width: '100px',
+        height: '100px',
+        top: '0px',
+        left: '0px',
+        right: '0px',
+        bottom: '0px',
+        x: '0px',
+        y: '0px'
+      }[prop] || ''
+    }
+  })
+})
+
+// 模拟 Element.prototype.getBoundingClientRect
+Element.prototype.getBoundingClientRect = jest.fn(() => ({
+  width: 100,
+  height: 100,
+  top: 0,
+  left: 0,
+  bottom: 100,
+  right: 100,
+  x: 0,
+  y: 0,
+  toJSON: () => ({
+    width: 100,
+    height: 100,
+    top: 0,
+    left: 0,
+    bottom: 100,
+    right: 100,
+    x: 0,
+    y: 0
+  })
+}))
+
+// 静默 console.error 在测试中
+const originalConsoleError = console.error
+console.error = (...args: any[]) => {
+  // 检查是否在测试环境中(通过 Jest 环境变量判断)
+  const isTestEnv = process.env.JEST_WORKER_ID !== undefined ||
+                    typeof jest !== 'undefined'
+
+  // 在测试环境中静默错误输出,除非是重要错误
+  if (isTestEnv && !args[0]?.includes?.('重要错误')) {
+    return
+  }
+  originalConsoleError(...args)
+}
+
+// Mock 常用 UI 组件
+jest.mock('@/components/ui/dialog', () => {
+  const React = require('react')
+  return {
+    Dialog: ({ open, children }: any) => open ? React.createElement('div', { 'data-testid': 'dialog' }, children) : null,
+    DialogContent: ({ children, className }: any) => React.createElement('div', { className }, children),
+    DialogHeader: ({ children, className }: any) => React.createElement('div', { className }, children),
+    DialogTitle: ({ children, className }: any) => React.createElement('div', { className }, children),
+    DialogFooter: ({ children, className }: any) => React.createElement('div', { className }, children)
+  }
+})