|
@@ -0,0 +1,418 @@
|
|
|
|
|
+import React from 'react'
|
|
|
|
|
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
|
|
|
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
|
|
+import AddressEditPage from '@/pages/address-edit/index'
|
|
|
|
|
+
|
|
|
|
|
+// 导入Taro mock函数
|
|
|
|
|
+import { mockUseRouter, mockNavigateBack, mockShowToast } from '~/__mocks__/taroMock'
|
|
|
|
|
+
|
|
|
|
|
+// Mock API client
|
|
|
|
|
+jest.mock('@/api', () => ({
|
|
|
|
|
+ deliveryAddressClient: {
|
|
|
|
|
+ $post: jest.fn(),
|
|
|
|
|
+ ':id': {
|
|
|
|
|
+ $get: jest.fn(),
|
|
|
|
|
+ $put: jest.fn(),
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+// Mock auth hook
|
|
|
|
|
+jest.mock('@/utils/auth', () => ({
|
|
|
|
|
+ useAuth: () => ({
|
|
|
|
|
+ user: { id: 1 },
|
|
|
|
|
+ }),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+// Mock react-hook-form
|
|
|
|
|
+let mockFormData = {
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ phone: '',
|
|
|
|
|
+ province: undefined as number | undefined,
|
|
|
|
|
+ city: undefined as number | undefined,
|
|
|
|
|
+ district: undefined as number | undefined,
|
|
|
|
|
+ town: undefined as number | undefined,
|
|
|
|
|
+ address: '',
|
|
|
|
|
+ isDefault: false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const mockHandleSubmit = jest.fn((onSubmit, onError) => (e: any) => {
|
|
|
|
|
+ e?.preventDefault?.()
|
|
|
|
|
+ // 模拟验证通过,调用onSubmit
|
|
|
|
|
+ onSubmit(mockFormData)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const mockReset = jest.fn((data: any) => {
|
|
|
|
|
+ if (data) {
|
|
|
|
|
+ mockFormData = { ...mockFormData, ...data }
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const mockSetValue = jest.fn((name: string, value: any) => {
|
|
|
|
|
+ mockFormData = { ...mockFormData, [name]: value }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const mockWatch = jest.fn()
|
|
|
|
|
+
|
|
|
|
|
+jest.mock('react-hook-form', () => ({
|
|
|
|
|
+ useForm: () => ({
|
|
|
|
|
+ formState: {
|
|
|
|
|
+ isValid: true,
|
|
|
|
|
+ errors: {},
|
|
|
|
|
+ },
|
|
|
|
|
+ handleSubmit: mockHandleSubmit,
|
|
|
|
|
+ reset: mockReset,
|
|
|
|
|
+ setValue: mockSetValue,
|
|
|
|
|
+ watch: mockWatch,
|
|
|
|
|
+ getValues: () => mockFormData,
|
|
|
|
|
+ }),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+// Mock components
|
|
|
|
|
+jest.mock('@/components/ui/navbar', () => ({
|
|
|
|
|
+ Navbar: ({ title, onClickLeft }: { title: string; onClickLeft: () => void }) => (
|
|
|
|
|
+ <div data-testid="navbar">
|
|
|
|
|
+ <span>{title}</span>
|
|
|
|
|
+ <button onClick={onClickLeft}>返回</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+jest.mock('@/components/ui/button', () => ({
|
|
|
|
|
+ Button: ({ children, onClick, disabled, className }: any) => (
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={onClick}
|
|
|
|
|
+ disabled={disabled}
|
|
|
|
|
+ className={className}
|
|
|
|
|
+ data-testid="button"
|
|
|
|
|
+ >
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ ),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+jest.mock('@/components/ui/card', () => ({
|
|
|
|
|
+ Card: ({ children, className }: any) => (
|
|
|
|
|
+ <div className={className} data-testid="card">
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+jest.mock('@/components/ui/form', () => ({
|
|
|
|
|
+ Form: ({ children, ...props }: any) => (
|
|
|
|
|
+ <form {...props} data-testid="form">
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </form>
|
|
|
|
|
+ ),
|
|
|
|
|
+ FormField: ({ name, render }: any) => (
|
|
|
|
|
+ <div data-testid={`form-field-${name}`}>
|
|
|
|
|
+ {render({ field: { value: '', onChange: jest.fn() } })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ),
|
|
|
|
|
+ FormItem: ({ children }: any) => <div data-testid="form-item">{children}</div>,
|
|
|
|
|
+ FormLabel: ({ children }: any) => <label data-testid="form-label">{children}</label>,
|
|
|
|
|
+ FormControl: ({ children }: any) => <div data-testid="form-control">{children}</div>,
|
|
|
|
|
+ FormMessage: () => <div data-testid="form-message">错误消息</div>,
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+jest.mock('@/components/ui/input', () => ({
|
|
|
|
|
+ Input: ({ placeholder, ...props }: any) => (
|
|
|
|
|
+ <input
|
|
|
|
|
+ placeholder={placeholder}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ data-testid="input"
|
|
|
|
|
+ />
|
|
|
|
|
+ ),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+jest.mock('@/components/ui/switch', () => ({
|
|
|
|
|
+ Switch: ({ checked, onChange, color }: any) => (
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="checkbox"
|
|
|
|
|
+ checked={checked}
|
|
|
|
|
+ onChange={onChange}
|
|
|
|
|
+ data-testid="switch"
|
|
|
|
|
+ style={{ color }}
|
|
|
|
|
+ />
|
|
|
|
|
+ ),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+jest.mock('@/components/ui/city-selector', () => ({
|
|
|
|
|
+ CitySelector: ({
|
|
|
|
|
+ provinceValue,
|
|
|
|
|
+ cityValue,
|
|
|
|
|
+ districtValue,
|
|
|
|
|
+ townValue,
|
|
|
|
|
+ onProvinceChange,
|
|
|
|
|
+ onCityChange,
|
|
|
|
|
+ onDistrictChange,
|
|
|
|
|
+ onTownChange,
|
|
|
|
|
+ showLabels
|
|
|
|
|
+ }: any) => (
|
|
|
|
|
+ <div data-testid="city-selector">
|
|
|
|
|
+ <div>省份: {provinceValue}</div>
|
|
|
|
|
+ <div>城市: {cityValue}</div>
|
|
|
|
|
+ <div>区县: {districtValue}</div>
|
|
|
|
|
+ <div>乡镇: {townValue}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ),
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+describe('AddressEditPage', () => {
|
|
|
|
|
+ let queryClient: QueryClient
|
|
|
|
|
+
|
|
|
|
|
+ beforeEach(() => {
|
|
|
|
|
+ queryClient = new QueryClient({
|
|
|
|
|
+ defaultOptions: {
|
|
|
|
|
+ queries: { retry: false },
|
|
|
|
|
+ mutations: { retry: false },
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // Reset all mocks
|
|
|
|
|
+ jest.clearAllMocks()
|
|
|
|
|
+
|
|
|
|
|
+ // 重置表单数据
|
|
|
|
|
+ mockFormData = {
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ phone: '',
|
|
|
|
|
+ province: undefined,
|
|
|
|
|
+ city: undefined,
|
|
|
|
|
+ district: undefined,
|
|
|
|
|
+ town: undefined,
|
|
|
|
|
+ address: '',
|
|
|
|
|
+ isDefault: false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 默认设置无地址ID(添加模式)
|
|
|
|
|
+ mockUseRouter.mockReturnValue({
|
|
|
|
|
+ params: {},
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const renderWithProviders = (component: React.ReactElement) => {
|
|
|
|
|
+ return render(
|
|
|
|
|
+ <QueryClientProvider client={queryClient}>
|
|
|
|
|
+ {component}
|
|
|
|
|
+ </QueryClientProvider>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const mockAddress = {
|
|
|
|
|
+ id: 1,
|
|
|
|
|
+ name: '张三',
|
|
|
|
|
+ phone: '13812345678',
|
|
|
|
|
+ receiverProvince: 440000,
|
|
|
|
|
+ receiverCity: 440300,
|
|
|
|
|
+ receiverDistrict: 440305,
|
|
|
|
|
+ receiverTown: 440305001,
|
|
|
|
|
+ address: '科技大厦A座',
|
|
|
|
|
+ isDefault: 1,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ it('渲染添加地址页面标题和布局', async () => {
|
|
|
|
|
+ renderWithProviders(<AddressEditPage />)
|
|
|
|
|
+
|
|
|
|
|
+ expect(screen.getByTestId('navbar')).toBeInTheDocument()
|
|
|
|
|
+ expect(screen.getByText('添加地址')).toBeInTheDocument()
|
|
|
|
|
+ expect(screen.getByTestId('form')).toBeInTheDocument()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('渲染编辑地址页面标题和布局', async () => {
|
|
|
|
|
+ const { deliveryAddressClient } = await import('@/api')
|
|
|
|
|
+ ;(deliveryAddressClient[':id'].$get as jest.Mock).mockResolvedValue({
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ json: async () => mockAddress,
|
|
|
|
|
+ } as any)
|
|
|
|
|
+
|
|
|
|
|
+ mockUseRouter.mockReturnValue({
|
|
|
|
|
+ params: { id: '1' },
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ renderWithProviders(<AddressEditPage />)
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(screen.getByText('编辑地址')).toBeInTheDocument()
|
|
|
|
|
+ expect(deliveryAddressClient[':id'].$get).toHaveBeenCalledWith({
|
|
|
|
|
+ param: { id: 1 }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('填充编辑模式下的表单数据', async () => {
|
|
|
|
|
+ const { deliveryAddressClient } = await import('@/api')
|
|
|
|
|
+ ;(deliveryAddressClient[':id'].$get as jest.Mock).mockResolvedValue({
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ json: async () => mockAddress,
|
|
|
|
|
+ } as any)
|
|
|
|
|
+
|
|
|
|
|
+ mockUseRouter.mockReturnValue({
|
|
|
|
|
+ params: { id: '1' },
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ renderWithProviders(<AddressEditPage />)
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(deliveryAddressClient[':id'].$get).toHaveBeenCalledWith({
|
|
|
|
|
+ param: { id: 1 }
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('提交新建地址表单', async () => {
|
|
|
|
|
+ const { deliveryAddressClient } = await import('@/api')
|
|
|
|
|
+ const mockResponse = { id: 2, ...mockAddress, isDefault: 0 }
|
|
|
|
|
+ ;(deliveryAddressClient.$post as jest.Mock).mockResolvedValue({
|
|
|
|
|
+ status: 201,
|
|
|
|
|
+ json: async () => mockResponse,
|
|
|
|
|
+ } as any)
|
|
|
|
|
+
|
|
|
|
|
+ renderWithProviders(<AddressEditPage />)
|
|
|
|
|
+
|
|
|
|
|
+ // 找到保存按钮并点击
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ const saveButton = screen.getByText('保存地址')
|
|
|
|
|
+ expect(saveButton).toBeInTheDocument()
|
|
|
|
|
+
|
|
|
|
|
+ fireEvent.click(saveButton)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(deliveryAddressClient.$post).toHaveBeenCalled()
|
|
|
|
|
+ expect(mockShowToast).toHaveBeenCalledWith({
|
|
|
|
|
+ title: '添加成功',
|
|
|
|
|
+ icon: 'success'
|
|
|
|
|
+ })
|
|
|
|
|
+ expect(mockNavigateBack).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('提交编辑地址表单', async () => {
|
|
|
|
|
+ const { deliveryAddressClient } = await import('@/api')
|
|
|
|
|
+ ;(deliveryAddressClient[':id'].$get as jest.Mock).mockResolvedValue({
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ json: async () => mockAddress,
|
|
|
|
|
+ } as any)
|
|
|
|
|
+
|
|
|
|
|
+ const updatedAddress = { ...mockAddress, name: '李四' }
|
|
|
|
|
+ ;(deliveryAddressClient[':id'].$put as jest.Mock).mockResolvedValue({
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ json: async () => updatedAddress,
|
|
|
|
|
+ } as any)
|
|
|
|
|
+
|
|
|
|
|
+ mockUseRouter.mockReturnValue({
|
|
|
|
|
+ params: { id: '1' },
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ renderWithProviders(<AddressEditPage />)
|
|
|
|
|
+
|
|
|
|
|
+ // 等待数据加载
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(deliveryAddressClient[':id'].$get).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 确保表单已重置
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(mockReset).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 找到保存按钮并点击
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ const saveButton = screen.getByText('保存地址')
|
|
|
|
|
+ expect(saveButton).toBeInTheDocument()
|
|
|
|
|
+
|
|
|
|
|
+ fireEvent.click(saveButton)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(deliveryAddressClient[':id'].$put).toHaveBeenCalled()
|
|
|
|
|
+ // 检查调用参数
|
|
|
|
|
+ const call = deliveryAddressClient[':id'].$put.mock.calls[0]
|
|
|
|
|
+ expect(call[0]).toEqual(expect.objectContaining({
|
|
|
|
|
+ param: { id: 1 }
|
|
|
|
|
+ }))
|
|
|
|
|
+ expect(mockShowToast).toHaveBeenCalledWith({
|
|
|
|
|
+ title: '更新成功',
|
|
|
|
|
+ icon: 'success'
|
|
|
|
|
+ })
|
|
|
|
|
+ expect(mockNavigateBack).toHaveBeenCalled()
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('显示API错误提示', async () => {
|
|
|
|
|
+ const { deliveryAddressClient } = await import('@/api')
|
|
|
|
|
+ ;(deliveryAddressClient.$post as jest.Mock).mockResolvedValue({
|
|
|
|
|
+ status: 400,
|
|
|
|
|
+ json: async () => ({ message: '保存失败' }),
|
|
|
|
|
+ } as any)
|
|
|
|
|
+
|
|
|
|
|
+ renderWithProviders(<AddressEditPage />)
|
|
|
|
|
+
|
|
|
|
|
+ // 找到保存按钮并点击
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ const saveButton = screen.getByText('保存地址')
|
|
|
|
|
+ fireEvent.click(saveButton)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(mockShowToast).toHaveBeenCalledWith({
|
|
|
|
|
+ title: '保存地址失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('验证表单字段渲染', async () => {
|
|
|
|
|
+ renderWithProviders(<AddressEditPage />)
|
|
|
|
|
+
|
|
|
|
|
+ expect(screen.getByText('收货人姓名')).toBeInTheDocument()
|
|
|
|
|
+ expect(screen.getByText('手机号码')).toBeInTheDocument()
|
|
|
|
|
+ expect(screen.getByText('详细地址')).toBeInTheDocument()
|
|
|
|
|
+ expect(screen.getByText('设为默认地址')).toBeInTheDocument()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('点击返回按钮', async () => {
|
|
|
|
|
+ renderWithProviders(<AddressEditPage />)
|
|
|
|
|
+
|
|
|
|
|
+ const navbar = screen.getByTestId('navbar')
|
|
|
|
|
+ const backButton = navbar.querySelector('button')
|
|
|
|
|
+
|
|
|
|
|
+ if (backButton) {
|
|
|
|
|
+ fireEvent.click(backButton)
|
|
|
|
|
+ expect(mockNavigateBack).toHaveBeenCalled()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('显示保存中的加载状态', async () => {
|
|
|
|
|
+ const { deliveryAddressClient } = await import('@/api')
|
|
|
|
|
+ let resolvePromise: any
|
|
|
|
|
+ const promise = new Promise(resolve => {
|
|
|
|
|
+ resolvePromise = resolve
|
|
|
|
|
+ })
|
|
|
|
|
+ ;(deliveryAddressClient.$post as jest.Mock).mockReturnValue(promise)
|
|
|
|
|
+
|
|
|
|
|
+ renderWithProviders(<AddressEditPage />)
|
|
|
|
|
+
|
|
|
|
|
+ // 点击保存按钮
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ const saveButton = screen.getByText('保存地址')
|
|
|
|
|
+ fireEvent.click(saveButton)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 应该显示加载状态
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(screen.getByText('保存中...')).toBeInTheDocument()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 解析promise
|
|
|
|
|
+ resolvePromise({
|
|
|
|
|
+ status: 201,
|
|
|
|
|
+ json: async () => ({ id: 1 }),
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(screen.getByText('保存地址')).toBeInTheDocument()
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+})
|