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()
})
})
})