fc0dcb0782
refactor: - 主站复用game agent的zustand store fix: - 修复旧请求覆盖问题 - 修复账号切换问题 - 主站原本已经每 4 秒轮询 external generation 任务状态, 在这里补充触发余额刷新的时机 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/138 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
371 lines
13 KiB
TypeScript
371 lines
13 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { act, waitFor } from '@testing-library/react';
|
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
import type { ProfileRechargeProduct } from '../../../packages/shared/src/contracts/runtime';
|
|
import { usePlatformWalletStore } from '../../stores/usePlatformWalletStore';
|
|
import {
|
|
apiClientMocks,
|
|
hostBridgeMocks,
|
|
paymentPlatformMocks,
|
|
profileClientMocks,
|
|
renderController,
|
|
userA,
|
|
userB,
|
|
} from './usePlatformProfileCenterController.testSupport';
|
|
|
|
const rechargeProduct = {
|
|
productId: 'points-60',
|
|
kind: 'points',
|
|
} as ProfileRechargeProduct;
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void;
|
|
let reject!: (reason?: unknown) => void;
|
|
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
resolve = resolvePromise;
|
|
reject = rejectPromise;
|
|
});
|
|
return { promise, reject, resolve };
|
|
}
|
|
|
|
function mudPointBalance(totalPoints: number) {
|
|
return {
|
|
totalPoints,
|
|
permanentPoints: totalPoints,
|
|
limitedPoints: 0,
|
|
limitedExpiresAt: null,
|
|
dailyFreePoints: 0,
|
|
dailyFreeResetPoints: 20,
|
|
dailyFreeResetsAt: '2026-08-07T00:00:00+08:00',
|
|
};
|
|
}
|
|
|
|
function nativeRechargeResponse(totalPoints = 80, orderId = 'order-a') {
|
|
return {
|
|
order: {
|
|
orderId,
|
|
productTitle: '60 泥点',
|
|
amountCents: 600,
|
|
status: 'pending',
|
|
expirationCheckedAt: null,
|
|
},
|
|
center: {
|
|
walletBalance: totalPoints,
|
|
mudPointBalance: mudPointBalance(totalPoints),
|
|
},
|
|
wechatNativePayment: {
|
|
codeUrl: `weixin://wxpay/${orderId}`,
|
|
expiresAt: '2099-08-07T00:00:00+08:00',
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('usePlatformProfileCenterController recharge fallback', () => {
|
|
beforeEach(() => {
|
|
window.history.replaceState(null, '', '/profile');
|
|
usePlatformWalletStore.getState().resetWalletBalance();
|
|
vi.clearAllMocks();
|
|
paymentPlatformMocks.resolveProfileRechargeProductPaymentChannel.mockReturnValue(
|
|
'wechat_native',
|
|
);
|
|
hostBridgeMocks.getHostRuntime.mockReturnValue({ kind: 'browser' });
|
|
profileClientMocks.watchWechatPlatformProfileRechargeOrder.mockReturnValue(
|
|
new Promise(() => undefined),
|
|
);
|
|
});
|
|
|
|
test('keeps the response legacy balance when no shared snapshot exists', async () => {
|
|
usePlatformWalletStore.getState().setWalletOwner('user-a');
|
|
profileClientMocks.getPlatformProfileRechargeCenter.mockResolvedValue({
|
|
walletBalance: 37,
|
|
membership: null,
|
|
pointProducts: [],
|
|
membershipProducts: [],
|
|
benefits: [],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
});
|
|
const { result } = renderController(userA);
|
|
|
|
act(() => result.current.loadRechargeCenter());
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.rechargeCenter?.walletBalance).toBe(37);
|
|
});
|
|
});
|
|
|
|
test('synchronously rejects a duplicate recharge submission before React commits state', async () => {
|
|
const pendingOrder = deferred<ReturnType<typeof nativeRechargeResponse>>();
|
|
profileClientMocks.createPlatformProfileRechargeOrder.mockReturnValue(
|
|
pendingOrder.promise,
|
|
);
|
|
usePlatformWalletStore.getState().setWalletOwner('user-a');
|
|
const { result } = renderController(userA);
|
|
|
|
act(() => {
|
|
result.current.buyRechargeProduct(rechargeProduct);
|
|
result.current.buyRechargeProduct(rechargeProduct);
|
|
});
|
|
|
|
expect(
|
|
profileClientMocks.createPlatformProfileRechargeOrder,
|
|
).toHaveBeenCalledTimes(1);
|
|
await act(async () => {
|
|
pendingOrder.resolve(nativeRechargeResponse());
|
|
await pendingOrder.promise;
|
|
});
|
|
});
|
|
|
|
test('does not let a late payment confirmation overwrite a newer wallet operation', async () => {
|
|
const pendingConfirmation = deferred<{
|
|
order: ReturnType<typeof nativeRechargeResponse>['order'];
|
|
center: ReturnType<typeof nativeRechargeResponse>['center'];
|
|
}>();
|
|
profileClientMocks.createPlatformProfileRechargeOrder.mockResolvedValue(
|
|
nativeRechargeResponse(80),
|
|
);
|
|
profileClientMocks.confirmWechatPlatformProfileRechargeOrder.mockReturnValue(
|
|
pendingConfirmation.promise,
|
|
);
|
|
profileClientMocks.getPlatformProfileRechargeCenter.mockRejectedValue(
|
|
new Error('refresh unavailable'),
|
|
);
|
|
usePlatformWalletStore.getState().setWalletOwner('user-a');
|
|
const { result } = renderController(userA);
|
|
act(() => result.current.buyRechargeProduct(rechargeProduct));
|
|
await waitFor(() => {
|
|
expect(result.current.nativeWechatPayment?.orderId).toBe('order-a');
|
|
});
|
|
act(() => result.current.confirmNativeWechatPayment());
|
|
|
|
act(() => {
|
|
const newerSnapshot = usePlatformWalletStore
|
|
.getState()
|
|
.captureWalletBalanceSnapshot('user-a');
|
|
usePlatformWalletStore
|
|
.getState()
|
|
.applyWalletBalanceSnapshot(newerSnapshot!, mudPointBalance(150));
|
|
});
|
|
await act(async () => {
|
|
pendingConfirmation.resolve({
|
|
order: {
|
|
...nativeRechargeResponse().order,
|
|
status: 'paid',
|
|
},
|
|
center: nativeRechargeResponse(100).center,
|
|
});
|
|
await pendingConfirmation.promise;
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(usePlatformWalletStore.getState().mudPointBalance?.totalPoints).toBe(
|
|
150,
|
|
);
|
|
});
|
|
|
|
test('ignores an old owner order response after the account changes', async () => {
|
|
const pendingOrder = deferred<ReturnType<typeof nativeRechargeResponse>>();
|
|
profileClientMocks.createPlatformProfileRechargeOrder
|
|
.mockReturnValueOnce(pendingOrder.promise)
|
|
.mockResolvedValueOnce(nativeRechargeResponse(90, 'order-b'));
|
|
usePlatformWalletStore.getState().setWalletOwner('user-a');
|
|
const { result, rerender } = renderController(userA);
|
|
|
|
act(() => result.current.buyRechargeProduct(rechargeProduct));
|
|
const oldAccountSignal = profileClientMocks
|
|
.createPlatformProfileRechargeOrder.mock.calls[0]?.[2]
|
|
?.signal as AbortSignal;
|
|
expect(oldAccountSignal.aborted).toBe(false);
|
|
expect(result.current.submittingRechargeProductId).toBe('points-60');
|
|
|
|
act(() => {
|
|
rerender({ user: userB });
|
|
usePlatformWalletStore.getState().setWalletOwner('user-b');
|
|
});
|
|
expect(oldAccountSignal.aborted).toBe(true);
|
|
act(() => result.current.buyRechargeProduct(rechargeProduct));
|
|
await waitFor(() => {
|
|
expect(result.current.nativeWechatPayment?.orderId).toBe('order-b');
|
|
});
|
|
await act(async () => {
|
|
pendingOrder.resolve(nativeRechargeResponse());
|
|
await pendingOrder.promise;
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.submittingRechargeProductId).toBeNull();
|
|
expect(result.current.nativeWechatPayment?.orderId).toBe('order-b');
|
|
expect(result.current.rechargePaymentResult).toBeNull();
|
|
expect(result.current.rechargeCenter?.walletBalance).toBe(90);
|
|
expect(usePlatformWalletStore.getState()).toMatchObject({
|
|
ownerUserId: 'user-b',
|
|
mudPointBalance: mudPointBalance(90),
|
|
});
|
|
});
|
|
|
|
test('aborts an in-flight recharge write when the controller unmounts', () => {
|
|
profileClientMocks.createPlatformProfileRechargeOrder.mockReturnValue(
|
|
new Promise(() => undefined),
|
|
);
|
|
usePlatformWalletStore.getState().setWalletOwner('user-a');
|
|
const { result, unmount } = renderController(userA);
|
|
|
|
act(() => result.current.buyRechargeProduct(rechargeProduct));
|
|
const signal = profileClientMocks.createPlatformProfileRechargeOrder.mock
|
|
.calls[0]?.[2]?.signal as AbortSignal;
|
|
expect(signal.aborted).toBe(false);
|
|
|
|
unmount();
|
|
|
|
expect(signal.aborted).toBe(true);
|
|
});
|
|
|
|
test('ignores an old owner native confirmation after the account changes', async () => {
|
|
const pendingConfirmation = deferred<{
|
|
order: ReturnType<typeof nativeRechargeResponse>['order'];
|
|
center: ReturnType<typeof nativeRechargeResponse>['center'];
|
|
}>();
|
|
const onRechargeSuccess = vi.fn();
|
|
profileClientMocks.createPlatformProfileRechargeOrder.mockResolvedValue(
|
|
nativeRechargeResponse(),
|
|
);
|
|
profileClientMocks.confirmWechatPlatformProfileRechargeOrder.mockReturnValue(
|
|
pendingConfirmation.promise,
|
|
);
|
|
usePlatformWalletStore.getState().setWalletOwner('user-a');
|
|
const { result, rerender } = renderController(userA, onRechargeSuccess);
|
|
|
|
act(() => result.current.buyRechargeProduct(rechargeProduct));
|
|
await waitFor(() => {
|
|
expect(result.current.nativeWechatPayment?.orderId).toBe('order-a');
|
|
});
|
|
act(() => result.current.confirmNativeWechatPayment());
|
|
|
|
act(() => {
|
|
rerender({ user: userB });
|
|
usePlatformWalletStore.getState().setWalletOwner('user-b');
|
|
});
|
|
await act(async () => {
|
|
pendingConfirmation.resolve({
|
|
order: {
|
|
...nativeRechargeResponse().order,
|
|
status: 'paid',
|
|
},
|
|
center: nativeRechargeResponse(100).center,
|
|
});
|
|
await pendingConfirmation.promise;
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.nativeWechatPayment).toBeNull();
|
|
expect(result.current.rechargePaymentResult).toBeNull();
|
|
expect(result.current.wechatRechargeOrderConfirmationState).toBeNull();
|
|
expect(onRechargeSuccess).not.toHaveBeenCalled();
|
|
expect(usePlatformWalletStore.getState()).toMatchObject({
|
|
ownerUserId: 'user-b',
|
|
mudPointBalance: null,
|
|
});
|
|
});
|
|
|
|
test('aborts delayed confirmation retries when the account changes', async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
profileClientMocks.createPlatformProfileRechargeOrder.mockResolvedValue(
|
|
nativeRechargeResponse(),
|
|
);
|
|
profileClientMocks.confirmWechatPlatformProfileRechargeOrder.mockResolvedValue(
|
|
{
|
|
order: nativeRechargeResponse().order,
|
|
center: nativeRechargeResponse().center,
|
|
},
|
|
);
|
|
usePlatformWalletStore.getState().setWalletOwner('user-a');
|
|
const { result, rerender } = renderController(userA);
|
|
|
|
await act(async () => {
|
|
result.current.buyRechargeProduct(rechargeProduct);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
expect(result.current.nativeWechatPayment?.orderId).toBe('order-a');
|
|
|
|
await act(async () => {
|
|
result.current.confirmNativeWechatPayment();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
expect(
|
|
profileClientMocks.confirmWechatPlatformProfileRechargeOrder,
|
|
).toHaveBeenCalledTimes(1);
|
|
const confirmationSignal = profileClientMocks
|
|
.confirmWechatPlatformProfileRechargeOrder.mock.calls[0]?.[1]
|
|
?.signal as AbortSignal;
|
|
expect(confirmationSignal.aborted).toBe(false);
|
|
expect(
|
|
profileClientMocks.watchWechatPlatformProfileRechargeOrder,
|
|
).toHaveBeenCalledWith('order-a', { signal: confirmationSignal });
|
|
|
|
act(() => {
|
|
rerender({ user: userB });
|
|
usePlatformWalletStore.getState().setWalletOwner('user-b');
|
|
});
|
|
expect(confirmationSignal.aborted).toBe(true);
|
|
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(5000);
|
|
});
|
|
expect(
|
|
profileClientMocks.confirmWechatPlatformProfileRechargeOrder,
|
|
).toHaveBeenCalledTimes(1);
|
|
expect(apiClientMocks.clearStoredAccessToken).not.toHaveBeenCalled();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
test('does not clear the new owner token from an old owner payment error', async () => {
|
|
const pendingOrder = deferred<never>();
|
|
paymentPlatformMocks.resolveProfileRechargeProductPaymentChannel.mockReturnValue(
|
|
'wechat_mp_virtual',
|
|
);
|
|
hostBridgeMocks.getHostRuntime.mockReturnValue({
|
|
kind: 'wechat_mini_program',
|
|
});
|
|
profileClientMocks.createPlatformProfileRechargeOrder
|
|
.mockReturnValueOnce(pendingOrder.promise)
|
|
.mockResolvedValueOnce(nativeRechargeResponse(70, 'order-b'));
|
|
usePlatformWalletStore.getState().setWalletOwner('user-a');
|
|
const { result, rerender } = renderController(userA);
|
|
|
|
act(() => result.current.buyRechargeProduct(rechargeProduct));
|
|
act(() => {
|
|
rerender({ user: userB });
|
|
usePlatformWalletStore.getState().setWalletOwner('user-b');
|
|
});
|
|
paymentPlatformMocks.resolveProfileRechargeProductPaymentChannel.mockReturnValue(
|
|
'wechat_native',
|
|
);
|
|
act(() => result.current.buyRechargeProduct(rechargeProduct));
|
|
await waitFor(() => {
|
|
expect(result.current.nativeWechatPayment?.orderId).toBe('order-b');
|
|
});
|
|
await act(async () => {
|
|
pendingOrder.reject(new Error('当前登录设备不支持充值'));
|
|
await pendingOrder.promise.catch(() => undefined);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(apiClientMocks.clearStoredAccessToken).not.toHaveBeenCalled();
|
|
expect(hostBridgeMocks.requestHostLogin).not.toHaveBeenCalled();
|
|
expect(result.current.rechargeError).toBeNull();
|
|
expect(result.current.rechargePaymentResult).toBeNull();
|
|
expect(result.current.nativeWechatPayment?.orderId).toBe('order-b');
|
|
expect(usePlatformWalletStore.getState()).toMatchObject({
|
|
ownerUserId: 'user-b',
|
|
mudPointBalance: mudPointBalance(70),
|
|
});
|
|
});
|
|
});
|