/* @vitest-environment jsdom */ import {render, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {beforeEach, expect, test, vi} from 'vitest'; import { getAdminUserDetail, reconcileAdminUserConsumption, updateAdminWalletRestriction, } from '../api/adminApiClient'; import type { AdminProfileWalletPayload, AdminUserDetailResponse, } from '../api/adminApiTypes'; import {AdminUserReferenceButton} from './AdminUserReferenceButton'; vi.mock('../api/adminApiClient', () => ({ formatAdminApiError: vi.fn((error: unknown) => error instanceof Error ? error.message : '请求失败', ), getAdminUserDetail: vi.fn(), isAdminApiError: vi.fn(() => false), reconcileAdminUserConsumption: vi.fn(), updateAdminWalletRestriction: vi.fn(), })); const wallet: AdminProfileWalletPayload = { userId: 'user-1', totalBalance: 96, spendableBalance: 40, dailyFreePoints: 6, membershipLimitedPoints: 20, permanentPoints: 70, heldPoints: 5, refundDebtPoints: 25, manualFrozen: false, refundDebtFrozen: true, walletFrozen: true, manualRestriction: null, }; const detail: AdminUserDetailResponse = { userId: 'user-1', publicUserCode: 'TN1001', displayName: '陶泥用户', avatarUrl: 'https://example.com/avatar.png', phoneNumberMasked: '138****5678', loginMethod: 'phone', bindingStatus: 'bound', phoneBound: true, wechatBound: true, historicalConsumedPoints: 1234, canReconcileConsumption: true, wallet, rechargeOrders: [ { orderId: 'order-1', userId: 'user-1', user: null, productId: 'points_60', productTitle: '60泥点', productKind: 'points', amountCents: 600, status: 'paid', paymentChannel: 'wechat_native', paidAtMicros: 1_720_000_000_000_000, providerTransactionId: 'wx-1', createdAtMicros: 1_720_000_000_000_000, pointsDelta: 60, cumulativeSuccessRefundCents: 300, targetRecoveryPoints: 30, recoveredPoints: 5, unrecoveredPoints: 25, recoveryStatus: 'shortfall', wallet, refunds: [], activeHold: null, remainingRefundableCents: 300, refundEligible: false, refundBlockReasonCode: 'refund_reconciliation_pending', }, ], }; beforeEach(() => { vi.clearAllMocks(); vi.mocked(getAdminUserDetail).mockResolvedValue(detail); vi.mocked(reconcileAdminUserConsumption).mockResolvedValue({ userId: 'user-1', previousHistoricalConsumedPoints: 1234, historicalConsumedPoints: 1300, changed: true, reconciledAtMicros: 1_720_000_000_000_000, }); vi.mocked(updateAdminWalletRestriction).mockResolvedValue({wallet}); }); test('用户查看按钮按内部 ID 查询并展示脱敏资料、余额与退款限制', async () => { const user = userEvent.setup(); const parentClick = vi.fn(); render(
, ); const trigger = screen.getByRole('button', {name: '查看用户信息'}); await user.click(trigger); expect(parentClick).not.toHaveBeenCalled(); expect(await screen.findByRole('dialog', {name: '用户详情'})).toBeTruthy(); expect(getAdminUserDetail).toHaveBeenCalledWith('admin-token', { userId: 'user-1', publicUserCode: undefined, }); expect(screen.getByText('陶泥用户')).toBeTruthy(); expect(screen.getAllByText('TN1001').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('138****5678')).toBeTruthy(); expect(screen.getByText('退款欠账限制')).toBeTruthy(); expect(screen.getByText('25', {selector: 'strong'})).toBeTruthy(); expect(screen.getByText('历史花费')).toBeTruthy(); expect(screen.getByText('1234', {selector: 'strong'})).toBeTruthy(); expect(screen.getByText('order-1')).toBeTruthy(); await user.keyboard('{Escape}'); await waitFor(() => expect(screen.queryByRole('dialog', {name: '用户详情'})).toBeNull()); await waitFor(() => expect(document.activeElement).toBe(trigger)); }); test('只有陶泥号时按 publicUserCode 查询用户', async () => { const user = userEvent.setup(); render( , ); await user.click(screen.getByRole('button', {name: '查看用户信息'})); await screen.findByText('陶泥用户'); expect(getAdminUserDetail).toHaveBeenCalledWith('admin-token', { userId: undefined, publicUserCode: 'TN1001', }); }); test('历史花费支持手动对账并用权威结果校准展示', async () => { const user = userEvent.setup(); render( , ); await user.click(screen.getByRole('button', {name: '查看用户信息'})); await screen.findByText('陶泥用户'); await user.click(screen.getByRole('button', {name: '手动对账历史花费'})); await user.click(screen.getByRole('button', {name: '确认'})); await waitFor(() => { expect(reconcileAdminUserConsumption).toHaveBeenCalledWith('admin-token', { userId: 'user-1', }); }); expect(await screen.findByText('对账完成,历史花费已校准')).toBeTruthy(); expect(screen.getByText('1300', {selector: 'strong'})).toBeTruthy(); }); test('没有独立操作权限时不显示历史花费对账按钮', async () => { vi.mocked(getAdminUserDetail).mockResolvedValue({ ...detail, canReconcileConsumption: false, }); const user = userEvent.setup(); render( , ); await user.click(screen.getByRole('button', {name: '查看用户信息'})); await screen.findByText('陶泥用户'); expect(screen.queryByRole('button', {name: '手动对账历史花费'})).toBeNull(); }); test('人工冻结和解除人工冻结分别提交原因且不解除退款欠账限制', async () => { const user = userEvent.setup(); const manuallyFrozenWallet: AdminProfileWalletPayload = { ...wallet, manualFrozen: true, walletFrozen: true, manualRestriction: { frozen: true, reason: '风险核查', createdByAdminUserId: 'admin:root', createdByAdminDisplayName: '后台负责人', createdAtMicros: 1_720_000_000_000_000, updatedByAdminUserId: 'admin:root', updatedByAdminDisplayName: '后台负责人', updatedAtMicros: 1_720_000_000_000_000, }, }; vi.mocked(updateAdminWalletRestriction) .mockResolvedValueOnce({wallet: manuallyFrozenWallet}) .mockResolvedValueOnce({wallet: {...wallet, manualFrozen: false}}); render( , ); await user.click(screen.getByRole('button', {name: '查看用户信息'})); await screen.findByText('陶泥用户'); await user.type(screen.getByRole('textbox', {name: '人工冻结操作原因'}), '异常登录'); await user.click(screen.getByRole('button', {name: '人工冻结钱包'})); await user.click(screen.getByRole('button', {name: '确认'})); await waitFor(() => { expect(updateAdminWalletRestriction).toHaveBeenNthCalledWith(1, 'admin-token', { userId: 'user-1', frozen: true, reason: '异常登录', }); }); expect(await screen.findByText(/后台负责人/)).toBeTruthy(); expect(screen.queryByText(/admin:root/)).toBeNull(); expect(await screen.findByText('解除人工冻结后,退款欠账限制仍会保留。')).toBeTruthy(); await user.type(screen.getByRole('textbox', {name: '人工冻结操作原因'}), '核查完成'); await user.click(screen.getByRole('button', {name: '解除人工冻结'})); await user.click(screen.getByRole('button', {name: '确认'})); await waitFor(() => { expect(updateAdminWalletRestriction).toHaveBeenNthCalledWith(2, 'admin-token', { userId: 'user-1', frozen: false, reason: '核查完成', }); }); expect(screen.getByText('退款欠账限制')).toBeTruthy(); }); test('用户详情读取失败后可以重试', async () => { const user = userEvent.setup(); vi.mocked(getAdminUserDetail) .mockRejectedValueOnce(new Error('读取失败')) .mockResolvedValueOnce(detail); render( , ); await user.click(screen.getByRole('button', {name: '查看用户信息'})); expect(await screen.findByText('读取失败')).toBeTruthy(); await user.click(screen.getByRole('button', {name: '重试'})); expect(await screen.findByText('陶泥用户')).toBeTruthy(); expect(getAdminUserDetail).toHaveBeenCalledTimes(2); });