|
|
@@ -0,0 +1,370 @@
|
|
|
+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 }) => (
|
|
|
+ <QueryClientProvider client={createTestQueryClient()}>
|
|
|
+ {children}
|
|
|
+ </QueryClientProvider>
|
|
|
+)
|
|
|
+
|
|
|
+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(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ 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(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ 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(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 打开取消对话框
|
|
|
+ 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(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 打开取消对话框
|
|
|
+ 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(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 打开取消对话框
|
|
|
+ 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(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 打开取消对话框
|
|
|
+ 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(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderButtonBar order={shippedOrder} onViewDetail={jest.fn()} />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(queryByText('取消订单')).toBeNull()
|
|
|
+ expect(queryByText('确认收货')).toBeTruthy()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should use external cancel handler when provided', async () => {
|
|
|
+ const mockOnCancelOrder = jest.fn()
|
|
|
+
|
|
|
+ const { getByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderButtonBar
|
|
|
+ order={mockOrder}
|
|
|
+ onViewDetail={jest.fn()}
|
|
|
+ onCancelOrder={mockOnCancelOrder}
|
|
|
+ />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ fireEvent.click(getByText('取消订单'))
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(mockOnCancelOrder).toHaveBeenCalled()
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|