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>
355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
/** @vitest-environment jsdom */
|
|
|
|
import { act, renderHook } from '@testing-library/react';
|
|
import { createElement } from 'react';
|
|
import { renderToString } from 'react-dom/server';
|
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
const clientApi = vi.hoisted(() => ({
|
|
confirmClientWechatProfileRechargeOrder: vi.fn(),
|
|
createClientProfileRechargeOrder: vi.fn(),
|
|
getClientProfileRechargeCenter: vi.fn(),
|
|
getClientProfileWalletLedger: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../src/services/clientApi', () => clientApi);
|
|
|
|
import { useAccountWallet } from '../src/features/app-shell/useAccountWallet';
|
|
import { useWalletStore } from '../src/stores/useWalletStore';
|
|
|
|
function detailedBalance(totalPoints: number) {
|
|
return {
|
|
totalPoints,
|
|
permanentPoints: Math.max(0, totalPoints - 20),
|
|
limitedPoints: 20,
|
|
limitedExpiresAt: '2026-08-01T00:00:00Z',
|
|
dailyFreePoints: 0,
|
|
dailyFreeResetPoints: 20,
|
|
dailyFreeResetsAt: '2026-07-18T00:00:00Z',
|
|
};
|
|
}
|
|
|
|
describe('useWalletStore', () => {
|
|
beforeEach(() => {
|
|
useWalletStore.getState().resetWalletBalance();
|
|
useWalletStore.getState().setWalletOwner('user-a');
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
test('keeps mudPointBalance as the authoritative detailed balance state', async () => {
|
|
const mudPointBalance = detailedBalance(120);
|
|
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
|
walletBalance: 120,
|
|
mudPointBalance,
|
|
});
|
|
const { result } = renderHook(() => useWalletStore());
|
|
|
|
await act(async () => {
|
|
await result.current.onWalletBalanceMayHaveChanged();
|
|
});
|
|
|
|
expect(result.current.mudPointBalance).toEqual(mudPointBalance);
|
|
expect(result.current.legacyWalletBalance).toBe(120);
|
|
expect(result.current.mudPointBalanceStatus).toBe('ready');
|
|
expect(result.current.mudPointBalanceError).toBe('');
|
|
expect(Object.keys(result.current).sort()).toEqual([
|
|
'applyLegacyWalletBalanceSnapshot',
|
|
'applyWalletBalanceSnapshot',
|
|
'captureWalletBalanceSnapshot',
|
|
'legacyWalletBalance',
|
|
'mudPointBalance',
|
|
'mudPointBalanceError',
|
|
'mudPointBalanceStatus',
|
|
'onWalletBalanceMayHaveChanged',
|
|
'ownerUserId',
|
|
'resetWalletBalance',
|
|
'setWalletOwner',
|
|
]);
|
|
});
|
|
|
|
test('recovers from a failed refresh with a recharge-center snapshot', async () => {
|
|
clientApi.getClientProfileRechargeCenter.mockRejectedValueOnce(
|
|
new Error('首次读取失败'),
|
|
);
|
|
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
|
|
|
const mudPointBalance = detailedBalance(120);
|
|
const snapshot = useWalletStore
|
|
.getState()
|
|
.captureWalletBalanceSnapshot('user-a');
|
|
useWalletStore
|
|
.getState()
|
|
.applyWalletBalanceSnapshot(snapshot!, mudPointBalance);
|
|
|
|
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
|
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('ready');
|
|
expect(useWalletStore.getState().mudPointBalanceError).toBe('');
|
|
});
|
|
|
|
test('does not let an older refresh overwrite an applied snapshot', async () => {
|
|
let rejectRequest: ((reason: Error) => void) | undefined;
|
|
clientApi.getClientProfileRechargeCenter.mockImplementationOnce(
|
|
() =>
|
|
new Promise((_, reject) => {
|
|
rejectRequest = reject;
|
|
}),
|
|
);
|
|
|
|
const refresh = useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
|
const mudPointBalance = detailedBalance(160);
|
|
const snapshot = useWalletStore
|
|
.getState()
|
|
.captureWalletBalanceSnapshot('user-a');
|
|
useWalletStore
|
|
.getState()
|
|
.applyWalletBalanceSnapshot(snapshot!, mudPointBalance);
|
|
rejectRequest?.(new Error('过期请求失败'));
|
|
await refresh;
|
|
|
|
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
|
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('ready');
|
|
expect(useWalletStore.getState().mudPointBalanceError).toBe('');
|
|
});
|
|
|
|
test('rejects a response that omits mudPointBalance instead of inventing a breakdown', async () => {
|
|
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
|
walletBalance: 120,
|
|
});
|
|
|
|
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
|
|
|
expect(useWalletStore.getState().mudPointBalance).toBeNull();
|
|
expect(useWalletStore.getState().legacyWalletBalance).toBe(120);
|
|
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('error');
|
|
expect(useWalletStore.getState().mudPointBalanceError).toBe(
|
|
'泥点明细读取失败',
|
|
);
|
|
});
|
|
|
|
test('uses mudPointBalance from recharge-center as the authoritative snapshot', async () => {
|
|
const mudPointBalance = detailedBalance(180);
|
|
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
|
walletBalance: 999,
|
|
mudPointBalance,
|
|
});
|
|
|
|
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
|
|
|
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
|
});
|
|
|
|
test('discards an outdated response and performs one trailing refresh', async () => {
|
|
let resolveFirst: ((value: unknown) => void) | undefined;
|
|
clientApi.getClientProfileRechargeCenter
|
|
.mockImplementationOnce(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveFirst = resolve;
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce({
|
|
walletBalance: 80,
|
|
mudPointBalance: detailedBalance(80),
|
|
});
|
|
|
|
const firstRefresh = useWalletStore
|
|
.getState()
|
|
.onWalletBalanceMayHaveChanged();
|
|
const trailingRefresh = useWalletStore
|
|
.getState()
|
|
.onWalletBalanceMayHaveChanged();
|
|
resolveFirst?.({
|
|
walletBalance: 120,
|
|
mudPointBalance: detailedBalance(120),
|
|
});
|
|
await Promise.all([firstRefresh, trailingRefresh]);
|
|
|
|
expect(clientApi.getClientProfileRechargeCenter).toHaveBeenCalledTimes(2);
|
|
expect(useWalletStore.getState().mudPointBalance?.totalPoints).toBe(80);
|
|
});
|
|
|
|
test('retains the last balance when a refresh fails', async () => {
|
|
clientApi.getClientProfileRechargeCenter.mockResolvedValueOnce({
|
|
walletBalance: 90,
|
|
mudPointBalance: detailedBalance(90),
|
|
});
|
|
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
|
clientApi.getClientProfileRechargeCenter.mockRejectedValueOnce(
|
|
new Error('刷新失败'),
|
|
);
|
|
|
|
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
|
|
|
expect(useWalletStore.getState().mudPointBalance?.totalPoints).toBe(90);
|
|
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('error');
|
|
expect(useWalletStore.getState().mudPointBalanceError).toBe(
|
|
'泥点明细读取失败',
|
|
);
|
|
});
|
|
|
|
test('does not restore a late response after reset', async () => {
|
|
let resolveRequest: ((value: unknown) => void) | undefined;
|
|
clientApi.getClientProfileRechargeCenter.mockImplementationOnce(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveRequest = resolve;
|
|
}),
|
|
);
|
|
|
|
const refresh = useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
|
useWalletStore.getState().resetWalletBalance();
|
|
resolveRequest?.({
|
|
walletBalance: 75,
|
|
mudPointBalance: detailedBalance(75),
|
|
});
|
|
await refresh;
|
|
|
|
expect(useWalletStore.getState().mudPointBalance).toBeNull();
|
|
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('idle');
|
|
});
|
|
|
|
test('clears immediately when the adapter switches wallet owners', () => {
|
|
const snapshot = useWalletStore
|
|
.getState()
|
|
.captureWalletBalanceSnapshot('user-a');
|
|
useWalletStore
|
|
.getState()
|
|
.applyWalletBalanceSnapshot(snapshot!, detailedBalance(88));
|
|
|
|
useWalletStore.getState().setWalletOwner('user-b');
|
|
|
|
expect(useWalletStore.getState()).toMatchObject({
|
|
ownerUserId: 'user-b',
|
|
mudPointBalance: null,
|
|
mudPointBalanceStatus: 'idle',
|
|
});
|
|
});
|
|
|
|
test('masks another owner snapshot before the owner-binding effect runs', () => {
|
|
const snapshot = useWalletStore
|
|
.getState()
|
|
.captureWalletBalanceSnapshot('user-a');
|
|
useWalletStore
|
|
.getState()
|
|
.applyWalletBalanceSnapshot(snapshot!, detailedBalance(88));
|
|
|
|
function WalletProbe() {
|
|
const wallet = useAccountWallet('user-b');
|
|
return createElement(
|
|
'span',
|
|
null,
|
|
wallet.mudPointBalance?.totalPoints ?? 'empty',
|
|
);
|
|
}
|
|
|
|
expect(renderToString(createElement(WalletProbe))).toContain('empty');
|
|
expect(useWalletStore.getState().ownerUserId).toBe('user-a');
|
|
});
|
|
|
|
test('binds the account owner and refreshes again when the window regains focus', async () => {
|
|
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
|
walletBalance: 18,
|
|
mudPointBalance: detailedBalance(18),
|
|
});
|
|
|
|
renderHook(() => useAccountWallet('user-a'));
|
|
await act(async () => undefined);
|
|
expect(clientApi.getClientProfileRechargeCenter).toHaveBeenCalledTimes(1);
|
|
|
|
await act(async () => {
|
|
window.dispatchEvent(new Event('focus'));
|
|
});
|
|
|
|
expect(clientApi.getClientProfileRechargeCenter).toHaveBeenCalledTimes(2);
|
|
expect(useWalletStore.getState()).toMatchObject({
|
|
ownerUserId: 'user-a',
|
|
mudPointBalance: detailedBalance(18),
|
|
});
|
|
});
|
|
|
|
test('keeps a successful legacy recharge total when the breakdown refresh fails', async () => {
|
|
clientApi.getClientProfileRechargeCenter.mockResolvedValueOnce({
|
|
walletBalance: 90,
|
|
mudPointBalance: detailedBalance(90),
|
|
});
|
|
const { result } = renderHook(() => useAccountWallet('user-a'));
|
|
await act(async () => undefined);
|
|
clientApi.getClientProfileRechargeCenter
|
|
.mockResolvedValueOnce({
|
|
walletBalance: 37,
|
|
membership: null,
|
|
pointProducts: [],
|
|
membershipProducts: [],
|
|
benefits: [],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
})
|
|
.mockRejectedValueOnce(new Error('明细刷新失败'));
|
|
|
|
await act(async () => {
|
|
await result.current.loadRechargeCenter();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.legacyWalletBalance).toBe(37);
|
|
expect(result.current.mudPointBalance).toBeNull();
|
|
});
|
|
|
|
test('keeps ledger loading independent from the recharge lifecycle', async () => {
|
|
let resolveLedger!: (value: { entries: [] }) => void;
|
|
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
|
walletBalance: 18,
|
|
mudPointBalance: detailedBalance(18),
|
|
});
|
|
clientApi.getClientProfileWalletLedger.mockImplementation(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveLedger = resolve;
|
|
}),
|
|
);
|
|
const { result } = renderHook(() => useAccountWallet('user-a'));
|
|
await act(async () => undefined);
|
|
|
|
act(() => {
|
|
result.current.openWalletLedger();
|
|
result.current.openRecharge();
|
|
});
|
|
expect(result.current.walletLedgerLoading).toBe(true);
|
|
|
|
await act(async () => {
|
|
resolveLedger({ entries: [] });
|
|
});
|
|
|
|
expect(result.current.walletLedger).toEqual({ entries: [] });
|
|
expect(result.current.walletLedgerLoading).toBe(false);
|
|
});
|
|
|
|
test('hides account-owned dialogs and data when the account changes', async () => {
|
|
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
|
walletBalance: 18,
|
|
mudPointBalance: detailedBalance(18),
|
|
});
|
|
clientApi.getClientProfileWalletLedger.mockResolvedValue({ entries: [] });
|
|
const { result, rerender } = renderHook(
|
|
({ userId }) => useAccountWallet(userId),
|
|
{ initialProps: { userId: 'user-a' } },
|
|
);
|
|
await act(async () => undefined);
|
|
await act(async () => {
|
|
result.current.openWalletLedger();
|
|
result.current.openRecharge();
|
|
});
|
|
expect(result.current.walletLedgerOpen).toBe(true);
|
|
expect(result.current.rechargeOpen).toBe(true);
|
|
|
|
await act(async () => {
|
|
rerender({ userId: 'user-b' });
|
|
});
|
|
|
|
expect(result.current.walletLedgerOpen).toBe(false);
|
|
expect(result.current.walletLedger).toBeNull();
|
|
expect(result.current.rechargeOpen).toBe(false);
|
|
expect(result.current.nativeRechargePayment).toBeNull();
|
|
});
|
|
});
|