修复泥点消耗刷新不及时 (#138)
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

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>
This commit was merged in pull request #138.
This commit is contained in:
2026-08-08 10:25:02 +08:00
committed by 段舒康
parent 44de26d204
commit fc0dcb0782
36 changed files with 4160 additions and 491 deletions
@@ -11,7 +11,10 @@ export function AccountWalletBar({
return (
<div className="launcher-account-bar" aria-label="账户资产">
<PlatformMudPointWalletEntry
balance={controller.mudPointBalance?.totalPoints ?? null}
balance={
controller.mudPointBalance?.totalPoints ??
controller.legacyWalletBalance
}
breakdown={controller.mudPointBalance}
isLoading={controller.mudPointBalanceStatus === 'loading'}
error={controller.mudPointBalanceError || null}
@@ -49,7 +52,11 @@ export function AccountWalletDialogs({
{controller.walletLedgerOpen ? (
<PlatformProfileWalletLedgerModal
ledger={controller.walletLedger}
fallbackBalance={controller.mudPointBalance?.totalPoints ?? 0}
fallbackBalance={
controller.mudPointBalance?.totalPoints ??
controller.legacyWalletBalance ??
0
}
isLoading={controller.walletLedgerLoading}
error={controller.walletLedgerError}
onClose={() => controller.setWalletLedgerOpen(false)}
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import type { PlatformProfileRechargeNativePaymentState } from '../../../../../packages/shared/src/components/PlatformProfileRechargeModal';
import type {
@@ -17,10 +17,15 @@ import { useWalletStore } from '../../stores/useWalletStore';
export function useAccountWallet(currentUserId: string) {
const {
ownerUserId,
mudPointBalance,
legacyWalletBalance,
mudPointBalanceStatus,
mudPointBalanceError,
setWalletOwner,
captureWalletBalanceSnapshot,
applyWalletBalanceSnapshot,
applyLegacyWalletBalanceSnapshot,
onWalletBalanceMayHaveChanged,
resetWalletBalance,
} = useWalletStore();
@@ -41,32 +46,98 @@ export function useAccountWallet(currentUserId: string) {
const [nativeRechargePayment, setNativeRechargePayment] =
useState<PlatformProfileRechargeNativePaymentState | null>(null);
const rechargeLifecycleRef = useRef(0);
const walletLedgerLifecycleRef = useRef(0);
const [walletUiOwnerUserId, setWalletUiOwnerUserId] = useState(currentUserId);
const currentUserIdRef = useRef(currentUserId);
const walletOwnerMatchesCurrentUser =
Boolean(currentUserId) && ownerUserId === currentUserId;
const walletUiOwnerMatchesCurrentUser =
Boolean(currentUserId) && walletUiOwnerUserId === currentUserId;
const walletUiIsVisible =
walletOwnerMatchesCurrentUser && walletUiOwnerMatchesCurrentUser;
const visibleMudPointBalance = walletOwnerMatchesCurrentUser
? mudPointBalance
: null;
const visibleLegacyWalletBalance = walletOwnerMatchesCurrentUser
? legacyWalletBalance
: null;
const visibleMudPointBalanceStatus = walletOwnerMatchesCurrentUser
? mudPointBalanceStatus
: 'idle';
const visibleMudPointBalanceError = walletOwnerMatchesCurrentUser
? mudPointBalanceError
: '';
const visibleWalletBalance =
visibleMudPointBalance?.totalPoints ?? visibleLegacyWalletBalance;
useLayoutEffect(() => {
currentUserIdRef.current = currentUserId;
rechargeLifecycleRef.current += 1;
walletLedgerLifecycleRef.current += 1;
}, [currentUserId]);
useEffect(() => {
resetWalletBalance();
void onWalletBalanceMayHaveChanged();
setWalletOwner(currentUserId || null);
if (currentUserId) {
void onWalletBalanceMayHaveChanged();
}
const refreshWalletBalance = () => {
void onWalletBalanceMayHaveChanged();
if (currentUserId) {
void onWalletBalanceMayHaveChanged();
}
};
window.addEventListener('focus', refreshWalletBalance);
return () => {
window.removeEventListener('focus', refreshWalletBalance);
};
}, [currentUserId, onWalletBalanceMayHaveChanged, resetWalletBalance]);
}, [currentUserId, onWalletBalanceMayHaveChanged, setWalletOwner]);
useEffect(() => {
setWalletUiOwnerUserId(currentUserId);
setWalletLedgerOpen(false);
setWalletLedger(null);
setWalletLedgerLoading(false);
setWalletLedgerError(null);
setRechargeOpen(false);
setRechargeContent(null);
setRechargeLoading(false);
setRechargeError(null);
setSubmittingRechargeProductId(null);
setNativeRechargePayment(null);
}, [currentUserId]);
async function loadWalletLedger() {
const walletLedgerLifecycle = ++walletLedgerLifecycleRef.current;
const snapshotOwnerUserId = currentUserId;
setWalletLedgerLoading(true);
setWalletLedgerError(null);
try {
setWalletLedger(await getClientProfileWalletLedger());
const ledger = await getClientProfileWalletLedger();
if (
walletLedgerLifecycleRef.current === walletLedgerLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setWalletLedger(ledger);
}
} catch (error) {
if (
walletLedgerLifecycleRef.current !== walletLedgerLifecycle ||
currentUserIdRef.current !== snapshotOwnerUserId
) {
return;
}
setWalletLedger(null);
setWalletLedgerError(
error instanceof Error ? error.message : '读取泥点账单失败',
);
} finally {
setWalletLedgerLoading(false);
if (
walletLedgerLifecycleRef.current === walletLedgerLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setWalletLedgerLoading(false);
}
}
}
@@ -75,37 +146,55 @@ export function useAccountWallet(currentUserId: string) {
void loadWalletLedger();
}
function applyRechargeContent(center: ProfileRechargeCenterResponse) {
function applyRechargeContent(
walletSnapshot: ReturnType<typeof captureWalletBalanceSnapshot>,
center: ProfileRechargeCenterResponse,
) {
const {
walletBalance,
mudPointBalance: nextMudPointBalance,
...content
} = center;
void walletBalance;
if (nextMudPointBalance) {
applyWalletBalanceSnapshot(nextMudPointBalance);
if (nextMudPointBalance && walletSnapshot) {
applyWalletBalanceSnapshot(walletSnapshot, nextMudPointBalance);
} else if (walletSnapshot && Number.isFinite(walletBalance)) {
applyLegacyWalletBalanceSnapshot(walletSnapshot, walletBalance);
void onWalletBalanceMayHaveChanged();
} else {
void onWalletBalanceMayHaveChanged();
}
setRechargeContent(content);
}
async function loadRechargeCenter() {
const snapshotOwnerUserId = currentUserId;
const walletSnapshot = captureWalletBalanceSnapshot(snapshotOwnerUserId);
const rechargeLifecycle = rechargeLifecycleRef.current;
setRechargeLoading(true);
setRechargeError(null);
try {
const center = await getClientProfileRechargeCenter();
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
if (
rechargeLifecycleRef.current !== rechargeLifecycle ||
currentUserIdRef.current !== snapshotOwnerUserId
) {
return;
}
applyRechargeContent(center);
applyRechargeContent(walletSnapshot, center);
} catch (error) {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setRechargeError(
error instanceof Error ? error.message : '读取泥点购买信息失败',
);
}
} finally {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setRechargeLoading(false);
}
}
@@ -130,16 +219,21 @@ export function useAccountWallet(currentUserId: string) {
return;
}
const rechargeLifecycle = rechargeLifecycleRef.current;
const snapshotOwnerUserId = currentUserId;
const walletSnapshot = captureWalletBalanceSnapshot(snapshotOwnerUserId);
setSubmittingRechargeProductId(product.productId);
setRechargeError(null);
try {
const response = await createClientProfileRechargeOrder(
product.productId,
);
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
if (
rechargeLifecycleRef.current !== rechargeLifecycle ||
currentUserIdRef.current !== snapshotOwnerUserId
) {
return;
}
applyRechargeContent(response.center);
applyRechargeContent(walletSnapshot, response.center);
const nativePayment = response.wechatNativePayment;
const codeUrl = nativePayment?.codeUrl?.trim();
const expiresAt = nativePayment?.expiresAt?.trim();
@@ -155,11 +249,17 @@ export function useAccountWallet(currentUserId: string) {
isConfirming: false,
});
} catch (error) {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setRechargeError(error instanceof Error ? error.message : '充值失败');
}
} finally {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setSubmittingRechargeProductId(null);
}
}
@@ -170,6 +270,8 @@ export function useAccountWallet(currentUserId: string) {
return;
}
const rechargeLifecycle = rechargeLifecycleRef.current;
const snapshotOwnerUserId = currentUserId;
const walletSnapshot = captureWalletBalanceSnapshot(snapshotOwnerUserId);
const orderId = nativeRechargePayment.orderId;
setNativeRechargePayment((current) =>
current?.orderId === orderId
@@ -178,10 +280,13 @@ export function useAccountWallet(currentUserId: string) {
);
try {
const response = await confirmClientWechatProfileRechargeOrder(orderId);
if (rechargeLifecycleRef.current !== rechargeLifecycle) {
if (
rechargeLifecycleRef.current !== rechargeLifecycle ||
currentUserIdRef.current !== snapshotOwnerUserId
) {
return;
}
applyRechargeContent(response.center);
applyRechargeContent(walletSnapshot, response.center);
if (response.order.status === 'paid') {
setNativeRechargePayment(null);
void onWalletBalanceMayHaveChanged();
@@ -199,7 +304,10 @@ export function useAccountWallet(currentUserId: string) {
: current,
);
} catch {
if (rechargeLifecycleRef.current === rechargeLifecycle) {
if (
rechargeLifecycleRef.current === rechargeLifecycle &&
currentUserIdRef.current === snapshotOwnerUserId
) {
setNativeRechargePayment((current) =>
current?.orderId === orderId
? {
@@ -214,29 +322,35 @@ export function useAccountWallet(currentUserId: string) {
}
return {
mudPointBalance,
mudPointBalanceStatus,
mudPointBalanceError,
ownerUserId,
mudPointBalance: visibleMudPointBalance,
legacyWalletBalance: visibleLegacyWalletBalance,
mudPointBalanceStatus: visibleMudPointBalanceStatus,
mudPointBalanceError: visibleMudPointBalanceError,
onWalletBalanceMayHaveChanged,
resetWalletBalance,
walletLedgerOpen,
walletLedgerOpen: walletUiIsVisible && walletLedgerOpen,
setWalletLedgerOpen,
walletLedger,
walletLedgerLoading,
walletLedgerError,
rechargeOpen,
walletLedger: walletUiIsVisible ? walletLedger : null,
walletLedgerLoading: walletUiIsVisible && walletLedgerLoading,
walletLedgerError: walletUiIsVisible ? walletLedgerError : null,
rechargeOpen: walletUiIsVisible && rechargeOpen,
rechargeModalCenter:
rechargeContent && mudPointBalance
rechargeContent && visibleWalletBalance !== null
? {
...rechargeContent,
walletBalance: mudPointBalance.totalPoints,
mudPointBalance,
walletBalance: visibleWalletBalance,
...(visibleMudPointBalance
? { mudPointBalance: visibleMudPointBalance }
: {}),
}
: null,
rechargeLoading,
rechargeError,
submittingRechargeProductId,
nativeRechargePayment,
rechargeLoading: walletUiIsVisible && rechargeLoading,
rechargeError: walletUiIsVisible ? rechargeError : null,
submittingRechargeProductId: walletUiIsVisible
? submittingRechargeProductId
: null,
nativeRechargePayment: walletUiIsVisible ? nativeRechargePayment : null,
setNativeRechargePayment,
loadWalletLedger,
openWalletLedger,
@@ -137,10 +137,10 @@ export function getClientProfileDashboard() {
);
}
export function getClientProfileRechargeCenter() {
export function getClientProfileRechargeCenter(signal?: AbortSignal) {
return requestClientApi<ProfileRechargeCenterResponse>(
'/api/profile/recharge-center',
{ method: 'GET' },
{ method: 'GET', signal },
'读取泥点明细失败',
);
}
@@ -1,100 +1,6 @@
import { create } from 'zustand';
import type { ProfileMudPointBalance } from '../../../../packages/shared/src';
import { createProfileWalletStore } from '../../../../packages/shared/src';
import { getClientProfileRechargeCenter } from '../services/clientApi';
export type MudPointBalanceStatus = 'idle' | 'loading' | 'ready' | 'error';
type WalletStore = {
mudPointBalance: ProfileMudPointBalance | null;
mudPointBalanceStatus: MudPointBalanceStatus;
mudPointBalanceError: string;
applyWalletBalanceSnapshot: (balance: ProfileMudPointBalance) => void;
onWalletBalanceMayHaveChanged: () => Promise<void>;
resetWalletBalance: () => void;
};
const initialWalletState: Pick<
WalletStore,
'mudPointBalance' | 'mudPointBalanceStatus' | 'mudPointBalanceError'
> = {
mudPointBalance: null,
mudPointBalanceStatus: 'idle',
mudPointBalanceError: '',
};
let refreshVersion = 0;
let requestGeneration = 0;
let activeRefresh: Promise<void> | null = null;
export const useWalletStore = create<WalletStore>((set) => ({
...initialWalletState,
applyWalletBalanceSnapshot: (balance) => {
requestGeneration += 1;
refreshVersion = 0;
activeRefresh = null;
set({
mudPointBalance: balance,
mudPointBalanceStatus: 'ready',
mudPointBalanceError: '',
});
},
onWalletBalanceMayHaveChanged: () => {
refreshVersion += 1;
if (activeRefresh) {
return activeRefresh;
}
const refreshRequestGeneration = requestGeneration;
const refresh = (async () => {
while (refreshRequestGeneration === requestGeneration) {
const requestedVersion = refreshVersion;
set({ mudPointBalanceStatus: 'loading', mudPointBalanceError: '' });
try {
const center = await getClientProfileRechargeCenter();
if (!center.mudPointBalance) {
throw new Error('充值中心响应缺少泥点余额');
}
if (refreshRequestGeneration !== requestGeneration) {
return;
}
if (requestedVersion !== refreshVersion) {
continue;
}
set({
mudPointBalance: center.mudPointBalance,
mudPointBalanceStatus: 'ready',
mudPointBalanceError: '',
});
return;
} catch (error) {
if (refreshRequestGeneration !== requestGeneration) {
return;
}
if (requestedVersion !== refreshVersion) {
continue;
}
set({
mudPointBalanceStatus: 'error',
mudPointBalanceError:
error instanceof Error ? error.message : '泥点明细读取失败',
});
return;
}
}
})();
activeRefresh = refresh;
void refresh.finally(() => {
if (activeRefresh === refresh) {
activeRefresh = null;
}
});
return refresh;
},
resetWalletBalance: () => {
requestGeneration += 1;
refreshVersion = 0;
activeRefresh = null;
set(initialWalletState);
},
}));
export const useWalletStore = createProfileWalletStore({
getRechargeCenter: getClientProfileRechargeCenter,
});
@@ -303,13 +303,23 @@ export function registerClientHomeTests() {
([command]) => command === 'inspect_local_project_directory',
).length;
await waitFor(() => {
expect(runtimeHarness.listen).toHaveBeenCalledWith(
'game-creator-manifest-invalidated',
expect.any(Function),
);
});
manifestChanged = true;
act(() => {
runtimeHarness.emitManifestInvalidated('art-asset-plan');
});
expect(
await screen.findByRole('button', { name: /runtime-live-hero\.png/ }),
await screen.findByRole(
'button',
{ name: /runtime-live-hero\.png/ },
{ timeout: 5_000 },
),
).not.toBeNull();
expect(screen.getByRole('button', { name: /版本 1/ })).not.toBeNull();
expect(runButton.getAttribute('data-unavailable')).toBeNull();
@@ -359,9 +369,7 @@ export function registerClientHomeTests() {
source: { kind: 'generated' },
},
];
let resolveStaleRefresh!: (
manifest: typeof staleFirstManifest,
) => void;
let resolveStaleRefresh!: (manifest: typeof staleFirstManifest) => void;
const staleRefresh = new Promise<typeof staleFirstManifest>((resolve) => {
resolveStaleRefresh = resolve;
});
@@ -1,14 +1,20 @@
/** @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) {
@@ -26,10 +32,11 @@ function detailedBalance(totalPoints: number) {
describe('useWalletStore', () => {
beforeEach(() => {
useWalletStore.getState().resetWalletBalance();
useWalletStore.getState().setWalletOwner('user-a');
vi.clearAllMocks();
});
test('keeps mudPointBalance as the only balance state', async () => {
test('keeps mudPointBalance as the authoritative detailed balance state', async () => {
const mudPointBalance = detailedBalance(120);
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
walletBalance: 120,
@@ -42,15 +49,21 @@ describe('useWalletStore', () => {
});
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',
]);
});
@@ -61,7 +74,12 @@ describe('useWalletStore', () => {
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
const mudPointBalance = detailedBalance(120);
useWalletStore.getState().applyWalletBalanceSnapshot(mudPointBalance);
const snapshot = useWalletStore
.getState()
.captureWalletBalanceSnapshot('user-a');
useWalletStore
.getState()
.applyWalletBalanceSnapshot(snapshot!, mudPointBalance);
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('ready');
@@ -79,7 +97,12 @@ describe('useWalletStore', () => {
const refresh = useWalletStore.getState().onWalletBalanceMayHaveChanged();
const mudPointBalance = detailedBalance(160);
useWalletStore.getState().applyWalletBalanceSnapshot(mudPointBalance);
const snapshot = useWalletStore
.getState()
.captureWalletBalanceSnapshot('user-a');
useWalletStore
.getState()
.applyWalletBalanceSnapshot(snapshot!, mudPointBalance);
rejectRequest?.(new Error('过期请求失败'));
await refresh;
@@ -96,9 +119,10 @@ describe('useWalletStore', () => {
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(
'充值中心响应缺少泥点余额',
'泥点明细读取失败',
);
});
@@ -128,10 +152,12 @@ describe('useWalletStore', () => {
mudPointBalance: detailedBalance(80),
});
const firstRefresh =
useWalletStore.getState().onWalletBalanceMayHaveChanged();
const trailingRefresh =
useWalletStore.getState().onWalletBalanceMayHaveChanged();
const firstRefresh = useWalletStore
.getState()
.onWalletBalanceMayHaveChanged();
const trailingRefresh = useWalletStore
.getState()
.onWalletBalanceMayHaveChanged();
resolveFirst?.({
walletBalance: 120,
mudPointBalance: detailedBalance(120),
@@ -156,7 +182,9 @@ describe('useWalletStore', () => {
expect(useWalletStore.getState().mudPointBalance?.totalPoints).toBe(90);
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('error');
expect(useWalletStore.getState().mudPointBalanceError).toBe('刷新失败');
expect(useWalletStore.getState().mudPointBalanceError).toBe(
'泥点明细读取失败',
);
});
test('does not restore a late response after reset', async () => {
@@ -179,4 +207,148 @@ describe('useWalletStore', () => {
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();
});
});