Files
Genarrative/src/components/platform-entry/usePlatformProfileCenterController.recharge.test.tsx
T
k88936 fc0dcb0782
Project CI / Repository checks (push) Successful in 50s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
修复泥点消耗刷新不及时 (#138)
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>
2026-08-08 10:25:02 +08:00

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),
});
});
});