basic.test.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. import React from 'react'
  2. import { render, screen, fireEvent, waitFor } from '@testing-library/react'
  3. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  4. import AddressEditPage from '@/pages/address-edit/index'
  5. // 导入Taro mock函数
  6. import { mockUseRouter, mockNavigateBack, mockShowToast } from '~/__mocks__/taroMock'
  7. // Mock API client
  8. jest.mock('@/api', () => ({
  9. deliveryAddressClient: {
  10. $post: jest.fn(),
  11. ':id': {
  12. $get: jest.fn(),
  13. $put: jest.fn(),
  14. },
  15. },
  16. }))
  17. // Mock auth hook
  18. jest.mock('@/utils/auth', () => ({
  19. useAuth: () => ({
  20. user: { id: 1 },
  21. }),
  22. }))
  23. // Mock react-hook-form
  24. let mockFormData = {
  25. name: '',
  26. phone: '',
  27. province: undefined as number | undefined,
  28. city: undefined as number | undefined,
  29. district: undefined as number | undefined,
  30. town: undefined as number | undefined,
  31. address: '',
  32. isDefault: false
  33. }
  34. const mockHandleSubmit = jest.fn((onSubmit, onError) => (e: any) => {
  35. e?.preventDefault?.()
  36. // 模拟验证通过,调用onSubmit
  37. onSubmit(mockFormData)
  38. })
  39. const mockReset = jest.fn((data: any) => {
  40. if (data) {
  41. mockFormData = { ...mockFormData, ...data }
  42. }
  43. })
  44. const mockSetValue = jest.fn((name: string, value: any) => {
  45. mockFormData = { ...mockFormData, [name]: value }
  46. })
  47. const mockWatch = jest.fn()
  48. jest.mock('react-hook-form', () => ({
  49. useForm: () => ({
  50. formState: {
  51. isValid: true,
  52. errors: {},
  53. },
  54. handleSubmit: mockHandleSubmit,
  55. reset: mockReset,
  56. setValue: mockSetValue,
  57. watch: mockWatch,
  58. getValues: () => mockFormData,
  59. }),
  60. }))
  61. // Mock components
  62. jest.mock('@/components/ui/navbar', () => ({
  63. Navbar: ({ title, onClickLeft }: { title: string; onClickLeft: () => void }) => (
  64. <div data-testid="navbar">
  65. <span>{title}</span>
  66. <button onClick={onClickLeft}>返回</button>
  67. </div>
  68. ),
  69. }))
  70. jest.mock('@/components/ui/button', () => ({
  71. Button: ({ children, onClick, disabled, className }: any) => (
  72. <button
  73. onClick={onClick}
  74. disabled={disabled}
  75. className={className}
  76. data-testid="button"
  77. >
  78. {children}
  79. </button>
  80. ),
  81. }))
  82. jest.mock('@/components/ui/card', () => ({
  83. Card: ({ children, className }: any) => (
  84. <div className={className} data-testid="card">
  85. {children}
  86. </div>
  87. ),
  88. }))
  89. jest.mock('@/components/ui/form', () => ({
  90. Form: ({ children, ...props }: any) => (
  91. <form {...props} data-testid="form">
  92. {children}
  93. </form>
  94. ),
  95. FormField: ({ name, render }: any) => (
  96. <div data-testid={`form-field-${name}`}>
  97. {render({ field: { value: '', onChange: jest.fn() } })}
  98. </div>
  99. ),
  100. FormItem: ({ children }: any) => <div data-testid="form-item">{children}</div>,
  101. FormLabel: ({ children }: any) => <label data-testid="form-label">{children}</label>,
  102. FormControl: ({ children }: any) => <div data-testid="form-control">{children}</div>,
  103. FormMessage: () => <div data-testid="form-message">错误消息</div>,
  104. }))
  105. jest.mock('@/components/ui/input', () => ({
  106. Input: ({ placeholder, ...props }: any) => (
  107. <input
  108. placeholder={placeholder}
  109. {...props}
  110. data-testid="input"
  111. />
  112. ),
  113. }))
  114. jest.mock('@/components/ui/switch', () => ({
  115. Switch: ({ checked, onChange, color }: any) => (
  116. <input
  117. type="checkbox"
  118. checked={checked}
  119. onChange={onChange}
  120. data-testid="switch"
  121. style={{ color }}
  122. />
  123. ),
  124. }))
  125. jest.mock('@/components/ui/city-selector', () => ({
  126. CitySelector: ({
  127. provinceValue,
  128. cityValue,
  129. districtValue,
  130. townValue,
  131. onProvinceChange,
  132. onCityChange,
  133. onDistrictChange,
  134. onTownChange,
  135. showLabels
  136. }: any) => (
  137. <div data-testid="city-selector">
  138. <div>省份: {provinceValue}</div>
  139. <div>城市: {cityValue}</div>
  140. <div>区县: {districtValue}</div>
  141. <div>乡镇: {townValue}</div>
  142. </div>
  143. ),
  144. }))
  145. describe('AddressEditPage', () => {
  146. let queryClient: QueryClient
  147. beforeEach(() => {
  148. queryClient = new QueryClient({
  149. defaultOptions: {
  150. queries: { retry: false },
  151. mutations: { retry: false },
  152. },
  153. })
  154. // Reset all mocks
  155. jest.clearAllMocks()
  156. // 重置表单数据
  157. mockFormData = {
  158. name: '',
  159. phone: '',
  160. province: undefined,
  161. city: undefined,
  162. district: undefined,
  163. town: undefined,
  164. address: '',
  165. isDefault: false
  166. }
  167. // 默认设置无地址ID(添加模式)
  168. mockUseRouter.mockReturnValue({
  169. params: {},
  170. })
  171. })
  172. const renderWithProviders = (component: React.ReactElement) => {
  173. return render(
  174. <QueryClientProvider client={queryClient}>
  175. {component}
  176. </QueryClientProvider>
  177. )
  178. }
  179. const mockAddress = {
  180. id: 1,
  181. name: '张三',
  182. phone: '13812345678',
  183. receiverProvince: 440000,
  184. receiverCity: 440300,
  185. receiverDistrict: 440305,
  186. receiverTown: 440305001,
  187. address: '科技大厦A座',
  188. isDefault: 1,
  189. }
  190. it('渲染添加地址页面标题和布局', async () => {
  191. renderWithProviders(<AddressEditPage />)
  192. expect(screen.getByTestId('navbar')).toBeInTheDocument()
  193. expect(screen.getByText('添加地址')).toBeInTheDocument()
  194. expect(screen.getByTestId('form')).toBeInTheDocument()
  195. })
  196. it('渲染编辑地址页面标题和布局', async () => {
  197. const { deliveryAddressClient } = await import('@/api')
  198. ;(deliveryAddressClient[':id'].$get as jest.Mock).mockResolvedValue({
  199. status: 200,
  200. json: async () => mockAddress,
  201. } as any)
  202. mockUseRouter.mockReturnValue({
  203. params: { id: '1' },
  204. })
  205. renderWithProviders(<AddressEditPage />)
  206. await waitFor(() => {
  207. expect(screen.getByText('编辑地址')).toBeInTheDocument()
  208. expect(deliveryAddressClient[':id'].$get).toHaveBeenCalledWith({
  209. param: { id: 1 }
  210. })
  211. })
  212. })
  213. it('填充编辑模式下的表单数据', async () => {
  214. const { deliveryAddressClient } = await import('@/api')
  215. ;(deliveryAddressClient[':id'].$get as jest.Mock).mockResolvedValue({
  216. status: 200,
  217. json: async () => mockAddress,
  218. } as any)
  219. mockUseRouter.mockReturnValue({
  220. params: { id: '1' },
  221. })
  222. renderWithProviders(<AddressEditPage />)
  223. await waitFor(() => {
  224. expect(deliveryAddressClient[':id'].$get).toHaveBeenCalledWith({
  225. param: { id: 1 }
  226. })
  227. })
  228. })
  229. it('提交新建地址表单', async () => {
  230. const { deliveryAddressClient } = await import('@/api')
  231. const mockResponse = { id: 2, ...mockAddress, isDefault: 0 }
  232. ;(deliveryAddressClient.$post as jest.Mock).mockResolvedValue({
  233. status: 201,
  234. json: async () => mockResponse,
  235. } as any)
  236. renderWithProviders(<AddressEditPage />)
  237. // 找到保存按钮并点击
  238. await waitFor(() => {
  239. const saveButton = screen.getByText('保存地址')
  240. expect(saveButton).toBeInTheDocument()
  241. fireEvent.click(saveButton)
  242. })
  243. await waitFor(() => {
  244. expect(deliveryAddressClient.$post).toHaveBeenCalled()
  245. expect(mockShowToast).toHaveBeenCalledWith({
  246. title: '添加成功',
  247. icon: 'success'
  248. })
  249. expect(mockNavigateBack).toHaveBeenCalled()
  250. })
  251. })
  252. it('提交编辑地址表单', async () => {
  253. const { deliveryAddressClient } = await import('@/api')
  254. ;(deliveryAddressClient[':id'].$get as jest.Mock).mockResolvedValue({
  255. status: 200,
  256. json: async () => mockAddress,
  257. } as any)
  258. const updatedAddress = { ...mockAddress, name: '李四' }
  259. ;(deliveryAddressClient[':id'].$put as jest.Mock).mockResolvedValue({
  260. status: 200,
  261. json: async () => updatedAddress,
  262. } as any)
  263. mockUseRouter.mockReturnValue({
  264. params: { id: '1' },
  265. })
  266. renderWithProviders(<AddressEditPage />)
  267. // 等待数据加载
  268. await waitFor(() => {
  269. expect(deliveryAddressClient[':id'].$get).toHaveBeenCalled()
  270. })
  271. // 确保表单已重置
  272. await waitFor(() => {
  273. expect(mockReset).toHaveBeenCalled()
  274. })
  275. // 找到保存按钮并点击
  276. await waitFor(() => {
  277. const saveButton = screen.getByText('保存地址')
  278. expect(saveButton).toBeInTheDocument()
  279. fireEvent.click(saveButton)
  280. })
  281. await waitFor(() => {
  282. expect(deliveryAddressClient[':id'].$put).toHaveBeenCalled()
  283. // 检查调用参数
  284. const call = deliveryAddressClient[':id'].$put.mock.calls[0]
  285. expect(call[0]).toEqual(expect.objectContaining({
  286. param: { id: 1 }
  287. }))
  288. expect(mockShowToast).toHaveBeenCalledWith({
  289. title: '更新成功',
  290. icon: 'success'
  291. })
  292. expect(mockNavigateBack).toHaveBeenCalled()
  293. })
  294. })
  295. it('显示API错误提示', async () => {
  296. const { deliveryAddressClient } = await import('@/api')
  297. ;(deliveryAddressClient.$post as jest.Mock).mockResolvedValue({
  298. status: 400,
  299. json: async () => ({ message: '保存失败' }),
  300. } as any)
  301. renderWithProviders(<AddressEditPage />)
  302. // 找到保存按钮并点击
  303. await waitFor(() => {
  304. const saveButton = screen.getByText('保存地址')
  305. fireEvent.click(saveButton)
  306. })
  307. await waitFor(() => {
  308. expect(mockShowToast).toHaveBeenCalledWith({
  309. title: '保存地址失败',
  310. icon: 'none'
  311. })
  312. })
  313. })
  314. it('验证表单字段渲染', async () => {
  315. renderWithProviders(<AddressEditPage />)
  316. expect(screen.getByText('收货人姓名')).toBeInTheDocument()
  317. expect(screen.getByText('手机号码')).toBeInTheDocument()
  318. expect(screen.getByText('详细地址')).toBeInTheDocument()
  319. expect(screen.getByText('设为默认地址')).toBeInTheDocument()
  320. })
  321. it('点击返回按钮', async () => {
  322. renderWithProviders(<AddressEditPage />)
  323. const navbar = screen.getByTestId('navbar')
  324. const backButton = navbar.querySelector('button')
  325. if (backButton) {
  326. fireEvent.click(backButton)
  327. expect(mockNavigateBack).toHaveBeenCalled()
  328. }
  329. })
  330. it('显示保存中的加载状态', async () => {
  331. const { deliveryAddressClient } = await import('@/api')
  332. let resolvePromise: any
  333. const promise = new Promise(resolve => {
  334. resolvePromise = resolve
  335. })
  336. ;(deliveryAddressClient.$post as jest.Mock).mockReturnValue(promise)
  337. renderWithProviders(<AddressEditPage />)
  338. // 点击保存按钮
  339. await waitFor(() => {
  340. const saveButton = screen.getByText('保存地址')
  341. fireEvent.click(saveButton)
  342. })
  343. // 应该显示加载状态
  344. await waitFor(() => {
  345. expect(screen.getByText('保存中...')).toBeInTheDocument()
  346. })
  347. // 解析promise
  348. resolvePromise({
  349. status: 201,
  350. json: async () => ({ id: 1 }),
  351. })
  352. await waitFor(() => {
  353. expect(screen.getByText('保存地址')).toBeInTheDocument()
  354. })
  355. })
  356. })