Browse Source

test(dealer/withdraw): 添加提现功能的单元测试

- 新增了多个测试用例,覆盖了提现功能的主要场景
- 添加了对最低提现金额、可用余额、支付宝信息完整性的验证
-测试了微信提现和支付宝提现的提交逻辑
- 新增了快捷金额按钮和全部金额按钮的测试
- 添加了调试组件,用于测试 Tabs 组件的点击和切换功能
- 优化了提现记录的渲染逻辑,增加了日志输出
demo
科技小王子 1 month ago
parent
commit
d3eb65bc09
  1. 184
      src/dealer/withdraw/__tests__/withdraw.test.tsx
  2. 80
      src/dealer/withdraw/debug.tsx
  3. 230
      src/dealer/withdraw/index.tsx

184
src/dealer/withdraw/__tests__/withdraw.test.tsx

@ -0,0 +1,184 @@
import React from 'react'
import { render, fireEvent, waitFor } from '@testing-library/react'
import DealerWithdraw from '../index'
import { useDealerUser } from '@/hooks/useDealerUser'
import * as withdrawAPI from '@/api/shop/shopDealerWithdraw'
// Mock dependencies
jest.mock('@/hooks/useDealerUser')
jest.mock('@/api/shop/shopDealerWithdraw')
jest.mock('@tarojs/taro', () => ({
showToast: jest.fn(),
getStorageSync: jest.fn(() => 123),
}))
const mockUseDealerUser = useDealerUser as jest.MockedFunction<typeof useDealerUser>
const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.addShopDealerWithdraw>
const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.pageShopDealerWithdraw>
describe('DealerWithdraw', () => {
const mockDealerUser = {
userId: 123,
money: '10000.00',
realName: '测试用户',
mobile: '13800138000'
}
beforeEach(() => {
mockUseDealerUser.mockReturnValue({
dealerUser: mockDealerUser,
loading: false,
error: null,
refresh: jest.fn()
})
mockPageShopDealerWithdraw.mockResolvedValue({
list: [],
count: 0
})
})
afterEach(() => {
jest.clearAllMocks()
})
test('应该正确显示可提现余额', () => {
const { getByText } = render(<DealerWithdraw />)
expect(getByText('10000.00')).toBeInTheDocument()
expect(getByText('可提现余额')).toBeInTheDocument()
})
test('应该验证最低提现金额', async () => {
mockAddShopDealerWithdraw.mockResolvedValue('success')
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入低于最低金额的数值
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '50' } })
// 选择提现方式
const wechatRadio = getByText('微信钱包')
fireEvent.click(wechatRadio)
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '最低提现金额为100元',
icon: 'error'
})
})
})
test('应该验证提现金额不超过可用余额', async () => {
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入超过可用余额的金额
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '20000' } })
// 选择提现方式
const wechatRadio = getByText('微信钱包')
fireEvent.click(wechatRadio)
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '提现金额超过可用余额',
icon: 'error'
})
})
})
test('应该验证支付宝账户信息完整性', async () => {
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入有效金额
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '1000' } })
// 选择支付宝提现
const alipayRadio = getByText('支付宝')
fireEvent.click(alipayRadio)
// 只填写账号,不填写姓名
const accountInput = getByPlaceholderText('请输入支付宝账号')
fireEvent.change(accountInput, { target: { value: 'test@alipay.com' } })
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '请填写完整的支付宝信息',
icon: 'error'
})
})
})
test('应该成功提交微信提现申请', async () => {
mockAddShopDealerWithdraw.mockResolvedValue('success')
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入有效金额
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '1000' } })
// 选择微信提现
const wechatRadio = getByText('微信钱包')
fireEvent.click(wechatRadio)
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(mockAddShopDealerWithdraw).toHaveBeenCalledWith({
userId: 123,
money: '1000',
payType: 10,
applyStatus: 10,
platform: 'MiniProgram'
})
})
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '提现申请已提交',
icon: 'success'
})
})
})
test('快捷金额按钮应该正常工作', () => {
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
// 点击快捷金额按钮
const quickAmountButton = getByText('500')
fireEvent.click(quickAmountButton)
// 验证金额输入框的值
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
expect(amountInput.value).toBe('500')
})
test('全部按钮应该设置为可用余额', () => {
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
// 点击全部按钮
const allButton = getByText('全部')
fireEvent.click(allButton)
// 验证金额输入框的值
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
expect(amountInput.value).toBe('10000.00')
})
})

80
src/dealer/withdraw/debug.tsx

@ -0,0 +1,80 @@
import React, { useState } from 'react'
import { View, Text } from '@tarojs/components'
import { Tabs, Button } from '@nutui/nutui-react-taro'
/**
*
* Tabs
*/
const WithdrawDebug: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>('0')
const [clickCount, setClickCount] = useState(0)
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换:', { from: activeTab, to: value, type: typeof value })
setActiveTab(value)
setClickCount(prev => prev + 1)
}
// 手动切换测试
const manualSwitch = (tab: string | number) => {
console.log('手动切换到:', tab)
setActiveTab(tab)
setClickCount(prev => prev + 1)
}
return (
<View className="bg-gray-50 min-h-screen p-4">
<View className="bg-white rounded-lg p-4 mb-4">
<Text className="text-lg font-bold mb-2"></Text>
<Text className="block mb-1">Tab: {String(activeTab)}</Text>
<Text className="block mb-1">: {clickCount}</Text>
<Text className="block mb-1">Tab类型: {typeof activeTab}</Text>
</View>
<View className="bg-white rounded-lg p-4 mb-4">
<Text className="text-lg font-bold mb-2"></Text>
<View className="flex gap-2">
<Button size="small" onClick={() => manualSwitch('0')}>
</Button>
<Button size="small" onClick={() => manualSwitch('1')}>
</Button>
</View>
</View>
<View className="bg-white rounded-lg">
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="申请提现" value="0">
<View className="p-4">
<Text className="text-center text-gray-600"></Text>
<Text className="text-center text-sm text-gray-400 mt-2">
Tab值: {String(activeTab)}
</Text>
</View>
</Tabs.TabPane>
<Tabs.TabPane title="提现记录" value="1">
<View className="p-4">
<Text className="text-center text-gray-600"></Text>
<Text className="text-center text-sm text-gray-400 mt-2">
Tab值: {String(activeTab)}
</Text>
</View>
</Tabs.TabPane>
</Tabs>
</View>
<View className="bg-white rounded-lg p-4 mt-4">
<Text className="text-lg font-bold mb-2"></Text>
<Text className="text-sm text-gray-500">
</Text>
</View>
</View>
)
}
export default WithdrawDebug

230
src/dealer/withdraw/index.tsx

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import React, {useState, useRef, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {
Cell,
Space,
@ -14,19 +14,19 @@ import {
Loading,
PullToRefresh
} from '@nutui/nutui-react-taro'
import { Wallet } from '@nutui/icons-react-taro'
import { businessGradients } from '@/styles/gradients'
import {Wallet} from '@nutui/icons-react-taro'
import {businessGradients} from '@/styles/gradients'
import Taro from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
import {useDealerUser} from '@/hooks/useDealerUser'
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
}
const DealerWithdraw: React.FC = () => {
const [activeTab, setActiveTab] = useState('0')
const [activeTab, setActiveTab] = useState<string | number>('0')
const [selectedAccount, setSelectedAccount] = useState('')
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
@ -35,17 +35,28 @@ const DealerWithdraw: React.FC = () => {
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
const formRef = useRef<any>(null)
const { dealerUser } = useDealerUser()
const {dealerUser} = useDealerUser()
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
setActiveTab(value)
// 如果切换到提现记录页面,刷新数据
if (String(value) === '1') {
fetchWithdrawRecords()
}
}
// 获取可提现余额
const fetchBalance = useCallback(async () => {
console.log(dealerUser,'dealerUser...')
console.log(dealerUser, 'dealerUser...')
try {
setAvailableAmount(dealerUser?.money || '0.00')
setAvailableAmount(dealerUser?.money || '0.00')
} catch (error) {
console.error('获取余额失败:', error)
}
}, [])
}, [dealerUser])
// 获取提现记录
const fetchWithdrawRecords = useCallback(async () => {
@ -106,21 +117,31 @@ const DealerWithdraw: React.FC = () => {
const getStatusText = (status?: number) => {
switch (status) {
case 40: return '已到账'
case 20: return '审核通过'
case 10: return '待审核'
case 30: return '已驳回'
default: return '未知'
case 40:
return '已到账'
case 20:
return '审核通过'
case 10:
return '待审核'
case 30:
return '已驳回'
default:
return '未知'
}
}
const getStatusColor = (status?: number) => {
switch (status) {
case 40: return 'success'
case 20: return 'success'
case 10: return 'warning'
case 30: return 'danger'
default: return 'default'
case 40:
return 'success'
case 20:
return 'success'
case 10:
return 'warning'
case 30:
return 'danger'
default:
return 'default'
}
}
@ -133,9 +154,25 @@ const DealerWithdraw: React.FC = () => {
return
}
if (!values.accountType) {
Taro.showToast({
title: '请选择提现方式',
icon: 'error'
})
return
}
// 验证提现金额
const amount = parseFloat(values.amount)
const available = parseFloat(availableAmount.replace(',', ''))
const available = parseFloat(availableAmount.replace(/,/g, ''))
if (isNaN(amount) || amount <= 0) {
Taro.showToast({
title: '请输入有效的提现金额',
icon: 'error'
})
return
}
if (amount < 100) {
Taro.showToast({
@ -153,6 +190,25 @@ const DealerWithdraw: React.FC = () => {
return
}
// 验证账户信息
if (values.accountType === 'alipay') {
if (!values.account || !values.accountName) {
Taro.showToast({
title: '请填写完整的支付宝信息',
icon: 'error'
})
return
}
} else if (values.accountType === 'bank') {
if (!values.account || !values.accountName || !values.bankName) {
Taro.showToast({
title: '请填写完整的银行卡信息',
icon: 'error'
})
return
}
}
try {
setSubmitting(true)
@ -160,7 +216,7 @@ const DealerWithdraw: React.FC = () => {
userId: dealerUser.userId,
money: values.amount,
payType: values.accountType === 'wechat' ? 10 :
values.accountType === 'alipay' ? 20 : 30,
values.accountType === 'alipay' ? 20 : 30,
applyStatus: 10, // 待审核
platform: 'MiniProgram'
}
@ -206,11 +262,11 @@ const DealerWithdraw: React.FC = () => {
const quickAmounts = ['100', '300', '500', '1000']
const setQuickAmount = (amount: string) => {
formRef.current?.setFieldsValue({ amount })
formRef.current?.setFieldsValue({amount})
}
const setAllAmount = () => {
formRef.current?.setFieldsValue({ amount: availableAmount.replace(',', '') })
formRef.current?.setFieldsValue({amount: availableAmount.replace(/,/g, '')})
}
// 格式化金额
@ -240,7 +296,7 @@ const DealerWithdraw: React.FC = () => {
<View className="p-3 rounded-full" style={{
background: 'rgba(255, 255, 255, 0.2)'
}}>
<Wallet color="white" size="32" />
<Wallet color="white" size="32"/>
</View>
</View>
<View className="mt-4 pt-4 relative z-10" style={{
@ -262,6 +318,14 @@ const DealerWithdraw: React.FC = () => {
<Input
placeholder="请输入提现金额"
type="number"
onChange={(value) => {
// 实时验证提现金额
const amount = parseFloat(value)
const available = parseFloat(availableAmount.replace(/,/g, ''))
if (!isNaN(amount) && amount > available) {
// 可以在这里添加实时提示,但不阻止输入
}
}}
/>
</Form.Item>
@ -308,10 +372,10 @@ const DealerWithdraw: React.FC = () => {
{selectedAccount === 'alipay' && (
<>
<Form.Item name="account" label="支付宝账号" required>
<Input placeholder="请输入支付宝账号" />
<Input placeholder="请输入支付宝账号"/>
</Form.Item>
<Form.Item name="accountName" label="支付宝姓名" required>
<Input placeholder="请输入支付宝实名姓名" />
<Input placeholder="请输入支付宝实名姓名"/>
</Form.Item>
</>
)}
@ -319,13 +383,13 @@ const DealerWithdraw: React.FC = () => {
{selectedAccount === 'bank' && (
<>
<Form.Item name="bankName" label="开户银行" required>
<Input placeholder="请输入开户银行名称" />
<Input placeholder="请输入开户银行名称"/>
</Form.Item>
<Form.Item name="account" label="银行卡号" required>
<Input placeholder="请输入银行卡号" />
<Input placeholder="请输入银行卡号"/>
</Form.Item>
<Form.Item name="accountName" label="开户姓名" required>
<Input placeholder="请输入银行卡开户姓名" />
<Input placeholder="请输入银行卡开户姓名"/>
</Form.Item>
</>
)}
@ -354,60 +418,64 @@ const DealerWithdraw: React.FC = () => {
</View>
)
const renderWithdrawRecords = () => (
<PullToRefresh
disabled={refreshing}
onRefresh={handleRefresh}
>
<View className="p-4">
{loading ? (
<View className="text-center py-8">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : withdrawRecords.length > 0 ? (
withdrawRecords.map(record => (
<View key={record.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<View>
<Text className="font-semibold text-gray-800 mb-1">
¥{record.money}
</Text>
<Text className="text-sm text-gray-500">
{record.accountDisplay}
</Text>
const renderWithdrawRecords = () => {
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
return (
<PullToRefresh
disabled={refreshing}
onRefresh={handleRefresh}
>
<View>
{loading ? (
<View className="text-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : withdrawRecords.length > 0 ? (
withdrawRecords.map(record => (
<View key={record.id} className="rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<View>
<Text className="font-semibold text-gray-800 mb-1">
¥{record.money}
</Text>
<Text className="text-sm text-gray-500">
{record.accountDisplay}
</Text>
</View>
<Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)}
</Tag>
</View>
<Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)}
</Tag>
</View>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{new Date(record.auditTime).toLocaleString()}
</Text>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{new Date(record.auditTime).toLocaleString()}
</Text>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
</View>
</View>
</View>
))
) : (
<Empty description="暂无提现记录" />
)}
</View>
</PullToRefresh>
)
))
) : (
<Empty description="暂无提现记录"/>
)}
</View>
</PullToRefresh>
)
}
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
@ -415,12 +483,12 @@ const DealerWithdraw: React.FC = () => {
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={() => setActiveTab}>
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="申请提现" value="0">
{renderWithdrawForm()}
</Tabs.TabPane>
<Tabs.TabPane title="提现记录" value="1">
<Tabs.TabPane title="提现记录" value="1" className={'bg-none'} style={{backgroundColor: 'transparent'}}>
{renderWithdrawRecords()}
</Tabs.TabPane>
</Tabs>

Loading…
Cancel
Save