OrderButtonBar.test.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import { render, fireEvent, waitFor } from '@testing-library/react'
  2. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  3. import { mockShowModal, mockShowToast, mockGetNetworkType, mockGetEnv } from '~/__mocks__/taroMock'
  4. import OrderButtonBar from '@/components/order/OrderButtonBar'
  5. // Mock API client
  6. jest.mock('@/api', () => ({
  7. orderClient: {
  8. 'cancel-order': {
  9. $post: jest.fn()
  10. }
  11. }
  12. }))
  13. // Mock CancelReasonDialog 组件
  14. jest.mock('@/components/common/CancelReasonDialog', () => {
  15. const React = require('react')
  16. const MockCancelReasonDialog = ({ open, onOpenChange, onConfirm, loading }: any) => {
  17. const [reason, setReason] = React.useState('')
  18. const [error, setError] = React.useState('')
  19. if (!open) return null
  20. const handleConfirm = () => {
  21. const trimmedReason = reason.trim()
  22. if (!trimmedReason) {
  23. setError('请输入取消原因')
  24. return
  25. }
  26. if (trimmedReason.length < 5) {
  27. setError('取消原因至少需要5个字符')
  28. return
  29. }
  30. if (trimmedReason.length > 200) {
  31. setError('取消原因不能超过200个字符')
  32. return
  33. }
  34. setError('')
  35. onConfirm(trimmedReason)
  36. }
  37. // 预定义原因选项
  38. const CANCEL_REASONS = [
  39. '我不想买了',
  40. '信息填写错误,重新下单',
  41. '商家缺货',
  42. '价格不合适',
  43. '其他原因'
  44. ]
  45. return React.createElement('div', { 'data-testid': 'cancel-reason-dialog' }, [
  46. React.createElement('div', { key: 'title' }, '取消订单'),
  47. React.createElement('div', { key: 'description' }, '请选择或填写取消原因,这将帮助我们改进服务'),
  48. // 预定义原因选项
  49. React.createElement('div', { key: 'reasons' },
  50. CANCEL_REASONS.map((reasonText, index) =>
  51. React.createElement('div', {
  52. key: index,
  53. onClick: () => {
  54. setReason(reasonText)
  55. if (error) setError('')
  56. }
  57. }, reasonText)
  58. )
  59. ),
  60. React.createElement('input', {
  61. key: 'reason-input',
  62. placeholder: '请输入其他取消原因...',
  63. value: reason,
  64. onChange: (e: any) => {
  65. setReason(e.target.value)
  66. if (error) setError('')
  67. }
  68. }),
  69. error && React.createElement('div', { key: 'error', 'data-testid': 'error-message' }, error),
  70. React.createElement('button', {
  71. key: 'confirm',
  72. onClick: handleConfirm,
  73. disabled: loading
  74. }, loading ? '提交中...' : '确认取消'),
  75. React.createElement('button', {
  76. key: 'cancel',
  77. onClick: () => onOpenChange(false)
  78. }, '取消')
  79. ])
  80. }
  81. MockCancelReasonDialog.displayName = 'MockCancelReasonDialog'
  82. return MockCancelReasonDialog
  83. })
  84. const mockOrder = {
  85. id: 1,
  86. tenantId: 1,
  87. orderNo: 'ORDER001',
  88. userId: 1,
  89. authCode: null,
  90. cardNo: null,
  91. sjtCardNo: null,
  92. amount: 100,
  93. costAmount: 80,
  94. freightAmount: 0,
  95. discountAmount: 0,
  96. payAmount: 100,
  97. orderType: 1,
  98. payType: 1,
  99. payState: 0, // 未支付
  100. state: 0, // 未发货
  101. deliveryType: 1,
  102. deliveryCompany: null,
  103. deliveryNo: null,
  104. deliveryTime: null,
  105. confirmTime: null,
  106. cancelTime: null,
  107. cancelReason: null,
  108. remark: null,
  109. recevierName: '张三',
  110. receiverMobile: '13800138000',
  111. address: '北京市朝阳区',
  112. recevierProvince: 110000,
  113. recevierCity: 110100,
  114. recevierDistrict: 110105,
  115. recevierTown: 0,
  116. addressId: 1,
  117. merchantId: 1,
  118. supplierId: 1,
  119. createdBy: 1,
  120. updatedBy: 1,
  121. createdAt: '2025-01-01T00:00:00Z',
  122. updatedAt: '2025-01-01T00:00:00Z',
  123. deletedAt: null,
  124. goodsDetail: JSON.stringify([
  125. { id: 1, name: '商品1', price: 50, num: 2, image: '', spec: '默认规格' }
  126. ]),
  127. merchant: null,
  128. supplier: null,
  129. deliveryAddress: null,
  130. orderGoods: []
  131. }
  132. const createTestQueryClient = () => new QueryClient({
  133. defaultOptions: {
  134. queries: { retry: false },
  135. mutations: { retry: false }
  136. }
  137. })
  138. const TestWrapper = ({ children }: { children: React.ReactNode }) => (
  139. <QueryClientProvider client={createTestQueryClient()}>
  140. {children}
  141. </QueryClientProvider>
  142. )
  143. describe('OrderButtonBar', () => {
  144. beforeEach(() => {
  145. jest.clearAllMocks()
  146. // 模拟网络检查成功回调
  147. mockGetNetworkType.mockImplementation((options) => {
  148. if (options?.success) {
  149. options.success({ networkType: 'wifi' })
  150. }
  151. return Promise.resolve()
  152. })
  153. // 模拟环境检查
  154. mockGetEnv.mockReturnValue('WEB')
  155. })
  156. it('should render cancel button for unpaid order', () => {
  157. const { getByText } = render(
  158. <TestWrapper>
  159. <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
  160. </TestWrapper>
  161. )
  162. expect(getByText('取消订单')).toBeTruthy()
  163. expect(getByText('去支付')).toBeTruthy()
  164. expect(getByText('查看详情')).toBeTruthy()
  165. })
  166. it('should show cancel reason dialog when cancel button is clicked', async () => {
  167. const { getByText, getByTestId } = render(
  168. <TestWrapper>
  169. <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
  170. </TestWrapper>
  171. )
  172. fireEvent.click(getByText('取消订单'))
  173. await waitFor(() => {
  174. expect(getByTestId('cancel-reason-dialog')).toBeTruthy()
  175. // 检查对话框内容
  176. expect(getByText('请选择或填写取消原因,这将帮助我们改进服务')).toBeTruthy()
  177. })
  178. })
  179. it('should call API when cancel order is confirmed', async () => {
  180. const mockApiCall = require('@/api').orderClient['cancel-order'].$post as jest.Mock
  181. mockShowModal.mockResolvedValueOnce({ confirm: true }) // 确认取消
  182. mockApiCall.mockResolvedValue({ status: 200, json: () => Promise.resolve({ success: true, message: '取消成功' }) })
  183. const { getByText, getByPlaceholderText, getByTestId } = render(
  184. <TestWrapper>
  185. <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
  186. </TestWrapper>
  187. )
  188. // 打开取消对话框
  189. fireEvent.click(getByText('取消订单'))
  190. await waitFor(() => {
  191. expect(getByTestId('cancel-reason-dialog')).toBeTruthy()
  192. })
  193. // 输入取消原因
  194. const reasonInput = getByPlaceholderText('请输入其他取消原因...')
  195. fireEvent.change(reasonInput, { target: { value: '测试取消原因' } })
  196. // 点击确认取消按钮
  197. fireEvent.click(getByText('确认取消'))
  198. await waitFor(() => {
  199. expect(mockShowModal).toHaveBeenCalledWith({
  200. title: '确认取消',
  201. content: '确定要取消订单吗?\n取消原因:测试取消原因',
  202. success: expect.any(Function)
  203. })
  204. })
  205. // 模拟确认对话框确认
  206. const modalCall = mockShowModal.mock.calls[0][0]
  207. if (modalCall.success) {
  208. modalCall.success({ confirm: true })
  209. }
  210. await waitFor(() => {
  211. expect(mockApiCall).toHaveBeenCalledWith({
  212. json: {
  213. orderId: 1,
  214. reason: '测试取消原因'
  215. }
  216. })
  217. })
  218. })
  219. it('should show error when cancel reason is empty', async () => {
  220. const { getByText, getByTestId } = render(
  221. <TestWrapper>
  222. <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
  223. </TestWrapper>
  224. )
  225. // 打开取消对话框
  226. fireEvent.click(getByText('取消订单'))
  227. await waitFor(() => {
  228. expect(getByTestId('cancel-reason-dialog')).toBeTruthy()
  229. })
  230. // 直接点击确认取消按钮(不输入原因)
  231. fireEvent.click(getByText('确认取消'))
  232. await waitFor(() => {
  233. expect(getByTestId('error-message')).toBeTruthy()
  234. expect(getByText('请输入取消原因')).toBeTruthy()
  235. })
  236. })
  237. it('should handle network error gracefully', async () => {
  238. const mockApiCall = require('@/api').orderClient['cancel-order'].$post as jest.Mock
  239. mockShowModal.mockResolvedValueOnce({ confirm: true })
  240. mockApiCall.mockRejectedValue(new Error('网络连接失败'))
  241. const { getByText, getByPlaceholderText, getByTestId } = render(
  242. <TestWrapper>
  243. <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
  244. </TestWrapper>
  245. )
  246. // 打开取消对话框
  247. fireEvent.click(getByText('取消订单'))
  248. await waitFor(() => {
  249. expect(getByTestId('cancel-reason-dialog')).toBeTruthy()
  250. })
  251. // 输入取消原因
  252. const reasonInput = getByPlaceholderText('请输入其他取消原因...')
  253. fireEvent.change(reasonInput, { target: { value: '测试取消原因' } })
  254. // 点击确认取消按钮
  255. fireEvent.click(getByText('确认取消'))
  256. // 模拟确认对话框确认
  257. await waitFor(() => {
  258. expect(mockShowModal).toHaveBeenCalledWith({
  259. title: '确认取消',
  260. content: '确定要取消订单吗?\n取消原因:测试取消原因',
  261. success: expect.any(Function)
  262. })
  263. })
  264. const modalCall = mockShowModal.mock.calls[0][0]
  265. if (modalCall.success) {
  266. modalCall.success({ confirm: true })
  267. }
  268. await waitFor(() => {
  269. expect(mockShowToast).toHaveBeenCalledWith({
  270. title: '网络连接失败,请检查网络后重试',
  271. icon: 'error',
  272. duration: 3000
  273. })
  274. })
  275. })
  276. it('should disable cancel button during mutation', async () => {
  277. // 模拟mutation正在进行中
  278. const mockApiCall = require('@/api').orderClient['cancel-order'].$post as jest.Mock
  279. mockApiCall.mockImplementation(() => new Promise(() => {})) // 永不resolve的promise
  280. const { getByText, getByPlaceholderText, getByTestId } = render(
  281. <TestWrapper>
  282. <OrderButtonBar order={mockOrder} onViewDetail={jest.fn()} />
  283. </TestWrapper>
  284. )
  285. // 打开取消对话框
  286. fireEvent.click(getByText('取消订单'))
  287. await waitFor(() => {
  288. expect(getByTestId('cancel-reason-dialog')).toBeTruthy()
  289. })
  290. // 输入取消原因
  291. const reasonInput = getByPlaceholderText('请输入其他取消原因...')
  292. fireEvent.change(reasonInput, { target: { value: '测试取消原因' } })
  293. // 点击确认取消按钮
  294. fireEvent.click(getByText('确认取消'))
  295. // 模拟确认对话框确认
  296. await waitFor(() => {
  297. expect(mockShowModal).toHaveBeenCalledWith({
  298. title: '确认取消',
  299. content: '确定要取消订单吗?\n取消原因:测试取消原因',
  300. success: expect.any(Function)
  301. })
  302. })
  303. const modalCall = mockShowModal.mock.calls[0][0]
  304. if (modalCall.success) {
  305. modalCall.success({ confirm: true })
  306. }
  307. // 检查按钮状态
  308. await waitFor(() => {
  309. expect(getByText('取消中...')).toBeTruthy()
  310. })
  311. })
  312. it('should not show cancel button for shipped order', () => {
  313. const shippedOrder = {
  314. ...mockOrder,
  315. payState: 2, // 已支付
  316. state: 1 // 已发货
  317. }
  318. const { queryByText } = render(
  319. <TestWrapper>
  320. <OrderButtonBar order={shippedOrder} onViewDetail={jest.fn()} />
  321. </TestWrapper>
  322. )
  323. expect(queryByText('取消订单')).toBeNull()
  324. expect(queryByText('确认收货')).toBeTruthy()
  325. })
  326. it('should use external cancel handler when provided', async () => {
  327. const mockOnCancelOrder = jest.fn()
  328. const { getByText } = render(
  329. <TestWrapper>
  330. <OrderButtonBar
  331. order={mockOrder}
  332. onViewDetail={jest.fn()}
  333. onCancelOrder={mockOnCancelOrder}
  334. />
  335. </TestWrapper>
  336. )
  337. fireEvent.click(getByText('取消订单'))
  338. await waitFor(() => {
  339. expect(mockOnCancelOrder).toHaveBeenCalled()
  340. })
  341. })
  342. })