|
|
@@ -0,0 +1,334 @@
|
|
|
+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()
|
|
|
+}))
|
|
|
+
|
|
|
+// Mock API client
|
|
|
+jest.mock('@/api', () => ({
|
|
|
+ orderClient: {
|
|
|
+ ':id': {
|
|
|
+ $get: jest.fn()
|
|
|
+ },
|
|
|
+ cancelOrder: {
|
|
|
+ $post: jest.fn()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}))
|
|
|
+
|
|
|
+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('OrderDetailPage', () => {
|
|
|
+ beforeEach(() => {
|
|
|
+ jest.clearAllMocks()
|
|
|
+ ;(Taro.getCurrentInstance as jest.Mock).mockReturnValue({
|
|
|
+ router: { params: { id: '1' } }
|
|
|
+ })
|
|
|
+ ;(Taro.getNetworkType as jest.Mock).mockResolvedValue({ networkType: 'wifi' })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should render order details when data is loaded', async () => {
|
|
|
+ const mockGetOrder = require('@/api').orderClient[':id'].$get as jest.Mock
|
|
|
+ mockGetOrder.mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(mockOrder)
|
|
|
+ })
|
|
|
+
|
|
|
+ const { findByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderDetailPage />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(await findByText('订单详情')).toBeTruthy()
|
|
|
+ expect(await findByText('待付款')).toBeTruthy()
|
|
|
+ expect(await findByText('请尽快完成支付')).toBeTruthy()
|
|
|
+ expect(await findByText('商品1')).toBeTruthy()
|
|
|
+ expect(await findByText('ORDER001')).toBeTruthy()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should show loading state when fetching order', () => {
|
|
|
+ const mockGetOrder = require('@/api').orderClient[':id'].$get as jest.Mock
|
|
|
+ mockGetOrder.mockImplementation(() => new Promise(() => {})) // 永不resolve
|
|
|
+
|
|
|
+ const { getByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderDetailPage />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 检查加载状态
|
|
|
+ expect(getByText('订单详情')).toBeTruthy()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should show error when order not found', async () => {
|
|
|
+ const mockGetOrder = require('@/api').orderClient[':id'].$get as jest.Mock
|
|
|
+ mockGetOrder.mockRejectedValue(new Error('订单不存在'))
|
|
|
+
|
|
|
+ const { findByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderDetailPage />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(await findByText('订单不存在')).toBeTruthy()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should show cancel dialog when cancel button is clicked', async () => {
|
|
|
+ const mockGetOrder = require('@/api').orderClient[':id'].$get as jest.Mock
|
|
|
+ mockGetOrder.mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(mockOrder)
|
|
|
+ })
|
|
|
+
|
|
|
+ const { findByText, getByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderDetailPage />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ await findByText('订单详情')
|
|
|
+
|
|
|
+ fireEvent.click(getByText('取消订单'))
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(getByText('取消订单')).toBeTruthy() // 对话框标题
|
|
|
+ expect(getByText('请选择或填写取消原因:')).toBeTruthy()
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should call cancel API when cancel is confirmed', async () => {
|
|
|
+ const mockGetOrder = require('@/api').orderClient[':id'].$get as jest.Mock
|
|
|
+ const mockCancelOrder = require('@/api').orderClient.cancelOrder.$post as jest.Mock
|
|
|
+ const mockShowModal = Taro.showModal as jest.Mock
|
|
|
+
|
|
|
+ mockGetOrder.mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(mockOrder)
|
|
|
+ })
|
|
|
+
|
|
|
+ mockCancelOrder.mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve({ success: true, message: '取消成功' })
|
|
|
+ })
|
|
|
+
|
|
|
+ mockShowModal.mockResolvedValue({ confirm: true })
|
|
|
+
|
|
|
+ const { findByText, getByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderDetailPage />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ await findByText('订单详情')
|
|
|
+
|
|
|
+ // 打开取消对话框
|
|
|
+ fireEvent.click(getByText('取消订单'))
|
|
|
+
|
|
|
+ // 选择取消原因并确认
|
|
|
+ await waitFor(() => {
|
|
|
+ const reasonOption = getByText('我不想买了')
|
|
|
+ fireEvent.click(reasonOption)
|
|
|
+
|
|
|
+ const confirmButton = getByText('确认取消')
|
|
|
+ fireEvent.click(confirmButton)
|
|
|
+ })
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(mockShowModal).toHaveBeenCalledWith({
|
|
|
+ title: '确认取消',
|
|
|
+ content: expect.stringContaining('我不想买了'),
|
|
|
+ success: expect.any(Function)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should show success message when cancel succeeds', async () => {
|
|
|
+ const mockGetOrder = require('@/api').orderClient[':id'].$get as jest.Mock
|
|
|
+ const mockCancelOrder = require('@/api').orderClient.cancelOrder.$post as jest.Mock
|
|
|
+ const mockShowToast = Taro.showToast as jest.Mock
|
|
|
+ const mockShowModal = Taro.showModal as jest.Mock
|
|
|
+
|
|
|
+ mockGetOrder.mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(mockOrder)
|
|
|
+ })
|
|
|
+
|
|
|
+ mockCancelOrder.mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve({ success: true, message: '取消成功' })
|
|
|
+ })
|
|
|
+
|
|
|
+ mockShowModal.mockResolvedValue({ confirm: true })
|
|
|
+
|
|
|
+ const { findByText, getByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderDetailPage />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ await findByText('订单详情')
|
|
|
+
|
|
|
+ fireEvent.click(getByText('取消订单'))
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ const reasonOption = getByText('我不想买了')
|
|
|
+ fireEvent.click(reasonOption)
|
|
|
+
|
|
|
+ const confirmButton = getByText('确认取消')
|
|
|
+ fireEvent.click(confirmButton)
|
|
|
+ })
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(mockShowToast).toHaveBeenCalledWith({
|
|
|
+ title: '订单取消成功',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 2000
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should show error message when cancel fails', async () => {
|
|
|
+ const mockGetOrder = require('@/api').orderClient[':id'].$get as jest.Mock
|
|
|
+ const mockCancelOrder = require('@/api').orderClient.cancelOrder.$post as jest.Mock
|
|
|
+ const mockShowToast = Taro.showToast as jest.Mock
|
|
|
+ const mockShowModal = Taro.showModal as jest.Mock
|
|
|
+
|
|
|
+ mockGetOrder.mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(mockOrder)
|
|
|
+ })
|
|
|
+
|
|
|
+ mockCancelOrder.mockRejectedValue(new Error('订单状态不允许取消'))
|
|
|
+ mockShowModal.mockResolvedValue({ confirm: true })
|
|
|
+
|
|
|
+ const { findByText, getByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderDetailPage />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ await findByText('订单详情')
|
|
|
+
|
|
|
+ fireEvent.click(getByText('取消订单'))
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ const reasonOption = getByText('我不想买了')
|
|
|
+ fireEvent.click(reasonOption)
|
|
|
+
|
|
|
+ const confirmButton = getByText('确认取消')
|
|
|
+ fireEvent.click(confirmButton)
|
|
|
+ })
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(mockShowToast).toHaveBeenCalledWith({
|
|
|
+ title: '当前订单状态不允许取消',
|
|
|
+ icon: 'error',
|
|
|
+ duration: 3000
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should copy order number when copy button is clicked', async () => {
|
|
|
+ const mockGetOrder = require('@/api').orderClient[':id'].$get as jest.Mock
|
|
|
+ const mockSetClipboardData = Taro.setClipboardData as jest.Mock
|
|
|
+ const mockShowToast = Taro.showToast as jest.Mock
|
|
|
+
|
|
|
+ mockGetOrder.mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(mockOrder)
|
|
|
+ })
|
|
|
+
|
|
|
+ mockSetClipboardData.mockResolvedValue({ success: true })
|
|
|
+
|
|
|
+ const { findByText, getByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderDetailPage />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ await findByText('订单详情')
|
|
|
+
|
|
|
+ const copyButton = getByText('复制')
|
|
|
+ fireEvent.click(copyButton)
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(mockSetClipboardData).toHaveBeenCalledWith({
|
|
|
+ data: 'ORDER001'
|
|
|
+ })
|
|
|
+ expect(mockShowToast).toHaveBeenCalledWith({
|
|
|
+ title: '订单号已复制',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('should check network before showing cancel dialog', async () => {
|
|
|
+ const mockGetOrder = require('@/api').orderClient[':id'].$get as jest.Mock
|
|
|
+ const mockShowToast = Taro.showToast as jest.Mock
|
|
|
+
|
|
|
+ mockGetOrder.mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(mockOrder)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 模拟无网络
|
|
|
+ ;(Taro.getNetworkType as jest.Mock).mockResolvedValue({ networkType: 'none' })
|
|
|
+
|
|
|
+ const { findByText, getByText } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <OrderDetailPage />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ await findByText('订单详情')
|
|
|
+
|
|
|
+ fireEvent.click(getByText('取消订单'))
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(mockShowToast).toHaveBeenCalledWith({
|
|
|
+ title: '网络连接失败,请检查网络后重试',
|
|
|
+ icon: 'error',
|
|
|
+ duration: 3000
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|