import { render, fireEvent, waitFor } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { mockShowModal, mockShowToast, mockGetNetworkType, mockGetEnv } from '~/__mocks__/taroMock' import OrderButtonBar from '@/components/order/OrderButtonBar' // Mock API client jest.mock('@/api', () => ({ orderClient: { cancelOrder: { $post: jest.fn() } } })) // Mock CancelReasonDialog 组件 jest.mock('@/components/common/CancelReasonDialog', () => { const React = require('react') const MockCancelReasonDialog = ({ open, onOpenChange, onConfirm, loading }: any) => { const [reason, setReason] = React.useState('') const [error, setError] = React.useState('') if (!open) return null const handleConfirm = () => { const trimmedReason = reason.trim() if (!trimmedReason) { setError('请输入取消原因') return } if (trimmedReason.length < 5) { setError('取消原因至少需要5个字符') return } if (trimmedReason.length > 200) { setError('取消原因不能超过200个字符') return } setError('') onConfirm(trimmedReason) } // 预定义原因选项 const CANCEL_REASONS = [ '我不想买了', '信息填写错误,重新下单', '商家缺货', '价格不合适', '其他原因' ] return React.createElement('div', { 'data-testid': 'cancel-reason-dialog' }, [ React.createElement('div', { key: 'title' }, '取消订单'), React.createElement('div', { key: 'description' }, '请选择或填写取消原因,这将帮助我们改进服务'), // 预定义原因选项 React.createElement('div', { key: 'reasons' }, CANCEL_REASONS.map((reasonText, index) => React.createElement('div', { key: index, onClick: () => { setReason(reasonText) if (error) setError('') } }, reasonText) ) ), React.createElement('input', { key: 'reason-input', placeholder: '请输入其他取消原因...', value: reason, onChange: (e: any) => { setReason(e.target.value) if (error) setError('') } }), error && React.createElement('div', { key: 'error', 'data-testid': 'error-message' }, error), React.createElement('button', { key: 'confirm', onClick: handleConfirm, disabled: loading }, loading ? '提交中...' : '确认取消'), React.createElement('button', { key: 'cancel', onClick: () => onOpenChange(false) }, '取消') ]) } MockCancelReasonDialog.displayName = 'MockCancelReasonDialog' return MockCancelReasonDialog }) const mockOrder = { id: 1, orderNo: 'ORDER001', payState: 0, // 未支付 state: 0, // 未发货 amount: 100, payAmount: 100, freightAmount: 0, discountAmount: 0, goodsDetail: JSON.stringify([ { id: 1, name: '商品1', price: 50, num: 2, image: '', spec: '默认规格' } ]), recevierName: '张三', receiverMobile: '13800138000', address: '北京市朝阳区', createdAt: '2025-01-01T00:00:00Z' } const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }) const TestWrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) describe('OrderButtonBar', () => { beforeEach(() => { jest.clearAllMocks() // 模拟网络检查成功回调 mockGetNetworkType.mockImplementation((options) => { if (options?.success) { options.success({ networkType: 'wifi' }) } return Promise.resolve() }) // 模拟环境检查 mockGetEnv.mockReturnValue('WEB') }) it('should render cancel button for unpaid order', () => { const { getByText } = render( ) expect(getByText('取消订单')).toBeTruthy() expect(getByText('去支付')).toBeTruthy() expect(getByText('查看详情')).toBeTruthy() }) it('should show cancel reason dialog when cancel button is clicked', async () => { const { getByText, getByTestId } = render( ) fireEvent.click(getByText('取消订单')) await waitFor(() => { expect(getByTestId('cancel-reason-dialog')).toBeTruthy() // 检查对话框内容 expect(getByText('请选择或填写取消原因,这将帮助我们改进服务')).toBeTruthy() }) }) it('should call API when cancel order is confirmed', async () => { const mockApiCall = require('@/api').orderClient.cancelOrder.$post as jest.Mock mockShowModal.mockResolvedValueOnce({ confirm: true }) // 确认取消 mockApiCall.mockResolvedValue({ status: 200, json: () => Promise.resolve({ success: true, message: '取消成功' }) }) const { getByText, getByPlaceholderText, getByTestId } = render( ) // 打开取消对话框 fireEvent.click(getByText('取消订单')) await waitFor(() => { expect(getByTestId('cancel-reason-dialog')).toBeTruthy() }) // 输入取消原因 const reasonInput = getByPlaceholderText('请输入其他取消原因...') fireEvent.change(reasonInput, { target: { value: '测试取消原因' } }) // 点击确认取消按钮 fireEvent.click(getByText('确认取消')) await waitFor(() => { expect(mockShowModal).toHaveBeenCalledWith({ title: '确认取消', content: '确定要取消订单吗?\n取消原因:测试取消原因', success: expect.any(Function) }) }) // 模拟确认对话框确认 const modalCall = mockShowModal.mock.calls[0][0] if (modalCall.success) { modalCall.success({ confirm: true }) } await waitFor(() => { expect(mockApiCall).toHaveBeenCalledWith({ json: { orderId: 1, reason: '测试取消原因' } }) }) }) it('should show error when cancel reason is empty', async () => { const { getByText, getByTestId } = render( ) // 打开取消对话框 fireEvent.click(getByText('取消订单')) await waitFor(() => { expect(getByTestId('cancel-reason-dialog')).toBeTruthy() }) // 直接点击确认取消按钮(不输入原因) fireEvent.click(getByText('确认取消')) await waitFor(() => { expect(getByTestId('error-message')).toBeTruthy() expect(getByText('请输入取消原因')).toBeTruthy() }) }) it('should handle network error gracefully', async () => { const mockApiCall = require('@/api').orderClient.cancelOrder.$post as jest.Mock mockShowModal.mockResolvedValueOnce({ confirm: true }) mockApiCall.mockRejectedValue(new Error('网络连接失败')) const { getByText, getByPlaceholderText, getByTestId } = render( ) // 打开取消对话框 fireEvent.click(getByText('取消订单')) await waitFor(() => { expect(getByTestId('cancel-reason-dialog')).toBeTruthy() }) // 输入取消原因 const reasonInput = getByPlaceholderText('请输入其他取消原因...') fireEvent.change(reasonInput, { target: { value: '测试取消原因' } }) // 点击确认取消按钮 fireEvent.click(getByText('确认取消')) // 模拟确认对话框确认 await waitFor(() => { expect(mockShowModal).toHaveBeenCalledWith({ title: '确认取消', content: '确定要取消订单吗?\n取消原因:测试取消原因', success: expect.any(Function) }) }) const modalCall = mockShowModal.mock.calls[0][0] if (modalCall.success) { modalCall.success({ confirm: true }) } await waitFor(() => { expect(mockShowToast).toHaveBeenCalledWith({ title: '网络连接失败,请检查网络后重试', icon: 'error', duration: 3000 }) }) }) it('should disable cancel button during mutation', async () => { // 模拟mutation正在进行中 const mockApiCall = require('@/api').orderClient.cancelOrder.$post as jest.Mock mockApiCall.mockImplementation(() => new Promise(() => {})) // 永不resolve的promise const { getByText, getByPlaceholderText, getByTestId } = render( ) // 打开取消对话框 fireEvent.click(getByText('取消订单')) await waitFor(() => { expect(getByTestId('cancel-reason-dialog')).toBeTruthy() }) // 输入取消原因 const reasonInput = getByPlaceholderText('请输入其他取消原因...') fireEvent.change(reasonInput, { target: { value: '测试取消原因' } }) // 点击确认取消按钮 fireEvent.click(getByText('确认取消')) // 模拟确认对话框确认 await waitFor(() => { expect(mockShowModal).toHaveBeenCalledWith({ title: '确认取消', content: '确定要取消订单吗?\n取消原因:测试取消原因', success: expect.any(Function) }) }) const modalCall = mockShowModal.mock.calls[0][0] if (modalCall.success) { modalCall.success({ confirm: true }) } // 检查按钮状态 await waitFor(() => { expect(getByText('取消中...')).toBeTruthy() }) }) it('should not show cancel button for shipped order', () => { const shippedOrder = { ...mockOrder, payState: 2, // 已支付 state: 1 // 已发货 } const { queryByText } = render( ) expect(queryByText('取消订单')).toBeNull() expect(queryByText('确认收货')).toBeTruthy() }) it('should use external cancel handler when provided', async () => { const mockOnCancelOrder = jest.fn() const { getByText } = render( ) fireEvent.click(getByText('取消订单')) await waitFor(() => { expect(mockOnCancelOrder).toHaveBeenCalled() }) }) })