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
- 主站「我的」页签常用功能宫格补回「邀请好友」,邀请码与邀请链接拆成两个复制动作 - 邀请弹层下沉到 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 与客户端专用弹层样式,原地删除旧邀请弹层实现 - 同步决策记录与产品基线文档中邀请奖励、跨端复用与文案口径
180 lines
6.0 KiB
TypeScript
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);
|
|
});
|
|
});
|