2
0
Эх сурвалжийг харах

♻️ refactor(test): 修复CancelReasonDialog测试和Input组件事件兼容性

- 修复Input组件事件处理,兼容Taro小程序和React事件格式
- 添加CancelReasonDialog状态重置逻辑,确保对话框重新打开时状态正确
- 移除Button、Input、Label组件的mock,使用真实组件进行测试
- 创建Input组件的完整单元测试(13个测试用例)
- 修复CancelReasonDialog测试中的查询问题,所有11个测试现在通过
- 修复order-detail测试中的Taro mock导入问题

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 1 сар өмнө
parent
commit
089730bb13

+ 10 - 2
mini/src/components/common/CancelReasonDialog/index.tsx

@@ -1,4 +1,4 @@
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
 import {
   Dialog,
   DialogContent,
@@ -37,6 +37,14 @@ export default function CancelReasonDialog({
   const [selectedReason, setSelectedReason] = useState('')
   const [error, setError] = useState('')
 
+  // 当对话框打开时重置状态
+  useEffect(() => {
+    if (open) {
+      console.debug('CancelReasonDialog: 对话框打开,重置状态')
+      handleReset()
+    }
+  }, [open])
+
   // 处理原因选择
   const handleReasonSelect = (reasonText: string) => {
     setSelectedReason(reasonText)
@@ -142,7 +150,7 @@ export default function CancelReasonDialog({
                 id="reason"
                 placeholder="请输入其他取消原因..."
                 value={reason}
-                onChange={(e) => handleCustomReasonChange(e.target.value)}
+                onChange={handleCustomReasonChange}
                 className={error ? 'border-destructive' : ''}
               />
               {error && (

+ 3 - 2
mini/src/components/ui/input.tsx

@@ -40,9 +40,10 @@ export interface InputProps extends Omit<TaroInputProps, 'className' | 'onChange
 const Input = forwardRef<any, InputProps>(
   ({ className, variant, size, leftIcon, rightIcon, error, errorMessage, onLeftIconClick, onRightIconClick, onChange, ...props }, ref) => {
     const handleInput = (event: any) => {
-      const value = event.detail.value
+      // 兼容Taro小程序事件(event.detail.value)和React事件(event.target.value)
+      const value = event.detail?.value || event.target?.value
       onChange?.(value, event)
-      
+
       // 同时调用原始的onInput(如果提供了)
       if (props.onInput) {
         props.onInput(event)

+ 42 - 42
mini/tests/setup.ts

@@ -433,51 +433,51 @@ console.error = (...args: any[]) => {
 // })
 
 // Mock Button 组件
-jest.mock('@/components/ui/button', () => {
-  const React = require('react')
-  const MockButton = React.forwardRef(({ children, onClick, disabled, className, ...props }: any, ref: any) => {
-    return React.createElement('button', {
-      onClick,
-      disabled,
-      className,
-      ref,
-      ...props
-    }, children)
-  })
-  MockButton.displayName = 'MockButton'
-  return MockButton
-})
+// jest.mock('@/components/ui/button', () => {
+//   const React = require('react')
+//   const MockButton = React.forwardRef(({ children, onClick, disabled, className, ...props }: any, ref: any) => {
+//     return React.createElement('button', {
+//       onClick,
+//       disabled,
+//       className,
+//       ref,
+//       ...props
+//     }, children)
+//   })
+//   MockButton.displayName = 'MockButton'
+//   return MockButton
+// })
 
 // Mock Input 组件
-jest.mock('@/components/ui/input', () => {
-  const React = require('react')
-  const MockInput = React.forwardRef(({ value, onChange, placeholder, className, ...props }: any, ref: any) => {
-    return React.createElement('input', {
-      value,
-      onChange: (e: any) => onChange?.(e.target.value, e),
-      placeholder,
-      className,
-      ref,
-      ...props
-    })
-  })
-  MockInput.displayName = 'MockInput'
-  return MockInput
-})
+// jest.mock('@/components/ui/input', () => {
+//   const React = require('react')
+//   const MockInput = React.forwardRef(({ value, onChange, placeholder, className, ...props }: any, ref: any) => {
+//     return React.createElement('input', {
+//       value,
+//       onChange: (e: any) => onChange?.(e.target.value, e),
+//       placeholder,
+//       className,
+//       ref,
+//       ...props
+//     })
+//   })
+//   MockInput.displayName = 'MockInput'
+//   return MockInput
+// })
 
 // Mock Label 组件
-jest.mock('@/components/ui/label', () => {
-  const React = require('react')
-  const MockLabel = React.forwardRef(({ children, htmlFor, className, ...props }: any, ref: any) => {
-    return React.createElement('label', {
-      htmlFor,
-      className,
-      ref,
-      ...props
-    }, children)
-  })
-  MockLabel.displayName = 'MockLabel'
-  return MockLabel
-})
+// jest.mock('@/components/ui/label', () => {
+//   const React = require('react')
+//   const MockLabel = React.forwardRef(({ children, htmlFor, className, ...props }: any, ref: any) => {
+//     return React.createElement('label', {
+//       htmlFor,
+//       className,
+//       ref,
+//       ...props
+//     }, children)
+//   })
+//   MockLabel.displayName = 'MockLabel'
+//   return MockLabel
+// })
 
 

+ 14 - 7
mini/tests/unit/components/common/CancelReasonDialog.test.tsx

@@ -15,7 +15,7 @@ describe('CancelReasonDialog', () => {
   })
 
   it('应该渲染对话框当可见时为true', () => {
-    const { getByText } = render(<CancelReasonDialog {...defaultProps} />)
+    const { getByText, getAllByText } = render(<CancelReasonDialog {...defaultProps} />)
 
     expect(getByText('取消订单')).toBeTruthy()
     expect(getByText('请选择或填写取消原因,这将帮助我们改进服务')).toBeTruthy()
@@ -23,7 +23,8 @@ describe('CancelReasonDialog', () => {
     expect(getByText('信息填写错误,重新下单')).toBeTruthy()
     expect(getByText('商家缺货')).toBeTruthy()
     expect(getByText('价格不合适')).toBeTruthy()
-    expect(getByText('其他原因')).toBeTruthy()
+    // 有多个"其他原因"文本,使用getAllByText
+    expect(getAllByText('其他原因').length).toBeGreaterThan(0)
   })
 
   it('不应该渲染对话框当open为false时', () => {
@@ -64,12 +65,14 @@ describe('CancelReasonDialog', () => {
   })
 
   it('应该显示错误当确认空原因时', () => {
-    const { getByText } = render(<CancelReasonDialog {...defaultProps} />)
+    const { getByText, getByRole } = render(<CancelReasonDialog {...defaultProps} />)
 
     const confirmButton = getByText('确认取消')
     fireEvent.click(confirmButton)
 
-    expect(getByText('请输入取消原因')).toBeTruthy()
+    // 使用更精确的查询方式查找错误消息
+    const errorMessage = getByText('请输入取消原因')
+    expect(errorMessage).toBeTruthy()
     expect(defaultProps.onConfirm).not.toHaveBeenCalled()
   })
 
@@ -84,7 +87,8 @@ describe('CancelReasonDialog', () => {
     const confirmButton = getByText('确认取消')
     fireEvent.click(confirmButton)
 
-    expect(getByText('取消原因不能超过200个字符')).toBeTruthy()
+    const errorMessage = getByText('取消原因不能超过200个字符')
+    expect(errorMessage).toBeTruthy()
     expect(defaultProps.onConfirm).not.toHaveBeenCalled()
   })
 
@@ -133,12 +137,15 @@ describe('CancelReasonDialog', () => {
 
     // 重新渲染关闭的对话框
     rerender(<CancelReasonDialog {...defaultProps} open={false} />)
+
+    // 重新渲染打开的对话框
     rerender(<CancelReasonDialog {...defaultProps} open={true} />)
 
-    // 检查状态是否重置
+    // 检查状态是否重置 - 直接点击确认按钮应该显示错误
     const confirmButton = getByText('确认取消')
     fireEvent.click(confirmButton)
 
-    expect(getByText('请输入取消原因')).toBeTruthy()
+    const errorMessage = getByText('请输入取消原因')
+    expect(errorMessage).toBeTruthy()
   })
 })

+ 132 - 0
mini/tests/unit/components/ui/input.test.tsx

@@ -0,0 +1,132 @@
+import { render, fireEvent } from '@testing-library/react'
+import { Input } from '@/components/ui/input'
+
+describe('Input', () => {
+  it('应该渲染输入框', () => {
+    const { container } = render(<Input />)
+
+    const input = container.querySelector('input')
+    expect(input).toBeTruthy()
+  })
+
+  it('应该显示占位符文本', () => {
+    const { getByPlaceholderText } = render(
+      <Input placeholder="请输入内容" />
+    )
+
+    expect(getByPlaceholderText('请输入内容')).toBeTruthy()
+  })
+
+  it('应该处理输入事件', () => {
+    const handleChange = jest.fn()
+    const { container } = render(
+      <Input onChange={handleChange} />
+    )
+
+    const input = container.querySelector('input') as HTMLInputElement
+    fireEvent.input(input, { target: { value: '测试输入' } })
+
+    expect(handleChange).toHaveBeenCalledWith('测试输入', expect.any(Object))
+  })
+
+  it('应该显示错误状态', () => {
+    const { container, getByText } = render(
+      <Input error={true} errorMessage="输入错误" />
+    )
+
+    const input = container.querySelector('input')
+    expect(input?.className).toContain('border-red-500')
+    expect(getByText('输入错误')).toBeTruthy()
+  })
+
+  it('应该显示左侧图标', () => {
+    const { container } = render(
+      <Input leftIcon="search" />
+    )
+
+    const leftIcon = container.querySelector('.absolute.left-3')
+    expect(leftIcon).toBeTruthy()
+  })
+
+  it('应该显示右侧图标', () => {
+    const { container } = render(
+      <Input rightIcon="close" />
+    )
+
+    const rightIcon = container.querySelector('.absolute.right-3')
+    expect(rightIcon).toBeTruthy()
+  })
+
+  it('应该处理左侧图标点击事件', () => {
+    const handleLeftIconClick = jest.fn()
+    const { container } = render(
+      <Input leftIcon="search" onLeftIconClick={handleLeftIconClick} />
+    )
+
+    const leftIcon = container.querySelector('.absolute.left-3')
+    fireEvent.click(leftIcon!)
+
+    expect(handleLeftIconClick).toHaveBeenCalled()
+  })
+
+  it('应该处理右侧图标点击事件', () => {
+    const handleRightIconClick = jest.fn()
+    const { container } = render(
+      <Input rightIcon="close" onRightIconClick={handleRightIconClick} />
+    )
+
+    const rightIcon = container.querySelector('.absolute.right-3')
+    fireEvent.click(rightIcon!)
+
+    expect(handleRightIconClick).toHaveBeenCalled()
+  })
+
+  it('应该应用不同的变体样式', () => {
+    const { container: defaultContainer } = render(<Input variant="default" />)
+    const { container: outlineContainer } = render(<Input variant="outline" />)
+    const { container: filledContainer } = render(<Input variant="filled" />)
+
+    const defaultInput = defaultContainer.querySelector('input')
+    const outlineInput = outlineContainer.querySelector('input')
+    const filledInput = filledContainer.querySelector('input')
+
+    expect(defaultInput?.className).toContain('border-gray-300')
+    expect(outlineInput?.className).toContain('border-input')
+    expect(filledInput?.className).toContain('border-none')
+  })
+
+  it('应该应用不同的大小样式', () => {
+    const { container: defaultContainer } = render(<Input size="default" />)
+    const { container: smContainer } = render(<Input size="sm" />)
+    const { container: lgContainer } = render(<Input size="lg" />)
+
+    const defaultInput = defaultContainer.querySelector('input')
+    const smInput = smContainer.querySelector('input')
+    const lgInput = lgContainer.querySelector('input')
+
+    expect(defaultInput?.className).toContain('h-10')
+    expect(smInput?.className).toContain('h-9')
+    expect(lgInput?.className).toContain('h-11')
+  })
+
+  it('应该禁用输入框', () => {
+    const { container } = render(<Input disabled />)
+
+    const input = container.querySelector('input')
+    expect(input?.disabled).toBe(true)
+  })
+
+  it('应该设置输入框类型', () => {
+    const { container } = render(<Input type="password" />)
+
+    const input = container.querySelector('input')
+    expect(input?.type).toBe('password')
+  })
+
+  it('应该设置输入框值', () => {
+    const { container } = render(<Input value="预设值" />)
+
+    const input = container.querySelector('input') as HTMLInputElement
+    expect(input.value).toBe('预设值')
+  })
+})

+ 1 - 10
mini/tests/unit/pages/order-detail/order-detail.test.tsx

@@ -2,16 +2,7 @@ import { render, fireEvent, waitFor } from '@testing-library/react'
 import Taro from '@tarojs/taro'
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
 import OrderDetailPage from '@/pages/order-detail/index'
-
-// Mock Taro API
-jest.mock('@tarojs/taro', () => ({
-  getCurrentInstance: jest.fn(),
-  showModal: jest.fn(),
-  showToast: jest.fn(),
-  getNetworkType: jest.fn(),
-  navigateBack: jest.fn(),
-  setClipboardData: jest.fn()
-}))
+import '~/__mocks__/taroMock'
 
 // Mock API client
 jest.mock('@/api', () => ({