/* @vitest-environment jsdom */ import { act } from '@testing-library/react'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { usePlatformWalletStore } from '../../stores/usePlatformWalletStore'; import { profileClientMocks, renderController, userA, userB, } from './usePlatformProfileCenterController.testSupport'; const clipboardMock = vi.hoisted(() => ({ copyTextToClipboard: vi.fn(async () => true), })); vi.mock('../../services/clipboard', () => clipboardMock); describe('usePlatformProfileCenterController redemption lifecycle', () => { beforeEach(() => { window.history.replaceState(null, '', '/profile'); usePlatformWalletStore.getState().resetWalletBalance(); vi.clearAllMocks(); clipboardMock.copyTextToClipboard.mockResolvedValue(true); }); test('copies the invite link and the invite code as separate actions', async () => { profileClientMocks.getPlatformProfileReferralInviteCenter.mockResolvedValue( { inviteCode: 'USER-A', inviteLinkPath: '/?inviteCode=USER-A', invitedCount: 0, rewardedInviteCount: 0, todayInviterRewardCount: 0, todayInviterRewardRemaining: 10, rewardPoints: 30, invitedUsers: [], hasRedeemedCode: false, boundInviterUserId: null, boundAt: null, updatedAt: '2026-10-01T00:00:00.000Z', }, ); const expectedInviteLink = `${window.location.origin}/?inviteCode=USER-A`; const { result } = renderController(userA); await act(async () => { result.current.openProfilePopupPanel('invite'); }); expect(result.current.profilePopupPanel).toBe('invite'); expect(result.current.inviteLinkUrl).toBe(expectedInviteLink); await act(async () => { result.current.copyInviteLink(); }); expect(clipboardMock.copyTextToClipboard).toHaveBeenLastCalledWith( expectedInviteLink, ); expect(result.current.referralSuccess).toBe('邀请链接已复制'); await act(async () => { result.current.copyInviteCode(); }); expect(clipboardMock.copyTextToClipboard).toHaveBeenLastCalledWith( 'USER-A', ); expect(result.current.referralSuccess).toBe('邀请码已复制'); }); test('ignores a reward-code response after the account changes', async () => { let resolveReward!: (value: { walletBalance: number; amountGranted: number; ledgerEntry: never; }) => void; profileClientMocks.redeemPlatformProfileRewardCode.mockImplementation( () => new Promise((resolve) => { resolveReward = resolve; }), ); const onRechargeSuccess = vi.fn(); const { result, rerender } = renderController(userA, onRechargeSuccess); act(() => result.current.setRewardCodeInput('reward-code')); act(() => result.current.submitRewardCode()); const oldAccountSignal = profileClientMocks.redeemPlatformProfileRewardCode .mock.calls[0]?.[1]?.signal as AbortSignal; expect(oldAccountSignal.aborted).toBe(false); rerender({ user: userB }); expect(oldAccountSignal.aborted).toBe(true); await act(async () => { resolveReward({ walletBalance: 100, amountGranted: 20, ledgerEntry: undefined as never, }); }); expect(result.current.rewardCodeSuccess).toBeNull(); expect(result.current.isSubmittingRewardCode).toBe(false); expect(onRechargeSuccess).not.toHaveBeenCalled(); }); test('ignores an invite-code response after the account changes', async () => { let resolveReferral!: (value: { center: never; inviteeRewardGranted: boolean; inviterRewardGranted: boolean; inviteeBalanceAfter: number; inviterBalanceAfter: number; }) => void; profileClientMocks.redeemPlatformProfileReferralInviteCode.mockImplementation( () => new Promise((resolve) => { resolveReferral = resolve; }), ); const onRechargeSuccess = vi.fn(); const { result, rerender } = renderController(userA, onRechargeSuccess); act(() => result.current.setReferralRedeemCode('invite-code')); act(() => result.current.submitReferralRedeemCode()); const oldAccountSignal = profileClientMocks .redeemPlatformProfileReferralInviteCode.mock.calls[0]?.[1] ?.signal as AbortSignal; expect(oldAccountSignal.aborted).toBe(false); rerender({ user: userB }); expect(oldAccountSignal.aborted).toBe(true); await act(async () => { resolveReferral({ center: undefined as never, inviteeRewardGranted: true, inviterRewardGranted: false, inviteeBalanceAfter: 100, inviterBalanceAfter: 0, }); }); expect(result.current.referralSuccess).toBeNull(); expect(result.current.isSubmittingReferralRedeem).toBe(false); expect(onRechargeSuccess).not.toHaveBeenCalled(); }); test('clears referral UI and ignores a late referral-center read after account change', async () => { let resolveReferralCenter!: (value: never) => void; profileClientMocks.getPlatformProfileReferralInviteCenter.mockImplementation( () => new Promise((resolve) => { resolveReferralCenter = resolve; }), ); const { result, rerender } = renderController(userA); act(() => { result.current.openProfilePopupPanel('redeem'); result.current.setReferralRedeemCode('USER-A-CODE'); result.current.loadReferralCenter(); }); expect(result.current.profilePopupPanel).toBe('redeem'); expect(result.current.isLoadingReferral).toBe(true); rerender({ user: userB }); expect(result.current.profilePopupPanel).toBeNull(); expect(result.current.referralRedeemCode).toBe(''); expect(result.current.referralCenter).toBeNull(); expect(result.current.isLoadingReferral).toBe(false); await act(async () => { resolveReferralCenter({ inviteCode: 'USER-A', inviteLinkPath: '/?inviteCode=USER-A', } as never); }); expect(result.current.referralCenter).toBeNull(); expect(result.current.isLoadingReferral).toBe(false); }); });