Files
Genarrative/src/components/platform-entry/usePlatformProfileCenterController.redemption.test.tsx
T
kdletters 41b882e1fe
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m50s
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m26s
Project CI / Native shell tests (pull_request) Failing after 36s
Project CI / Repository checks (pull_request) Failing after 26s
Project CI / Frontend tests (pull_request) Successful in 2m35s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m25s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 11m17s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 12m41s
我的页签恢复邀请好友入口,AGC 客户端账号菜单接入邀请码与玩家社区
- 主站「我的」页签常用功能宫格补回「邀请好友」,邀请码与邀请链接拆成两个复制动作
- 邀请弹层下沉到 packages/shared/src/components/PlatformProfileReferralModal,主站与客户端共用一份实现
- 邀请奖励说明不再写死次数与金额,每日上限由 todayInviterRewardCount 与 todayInviterRewardRemaining 推导
- 奖励说明从面板正文移入右上角帮助按钮,鼠标悬浮展开、触屏与键盘点击开合
- 帮助按钮复用平台 .platform-icon-button 配色,说明气泡复用 PlatformStatusMessage 的 warning token
- AGC 客户端账号菜单新增「邀请码」与「玩家社区」两项,弹层标题与菜单入口文案保持一致
- 新增 Tauri 命令 read_profile_referral_invite_center,用登录 origin 补全 inviteLinkPath 后交给渲染层
- 客户端邀请中心按登录账号隔离,切号重置并作废在飞响应,不本地另存邀请码
- 新命令登记进 apps/ai-game-creator-shell/scripts/check-config.mjs 的 native-only 白名单门禁
- 微信群与 QQ 群二维码从 media/social-media-group 迁入共享组件 assets
- 移除已下线文件的 Tailwind @source 与客户端专用弹层样式,原地删除旧邀请弹层实现
- 同步决策记录与产品基线文档中邀请奖励、跨端复用与文案口径
2026-10-01 19:46:18 +08:00

180 lines
6.0 KiB
TypeScript

/* @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);
});
});