修复泥点余额明细刷新
钱包明细每次展开并在实时余额变化时读取最新拆分。 图片画板生成扣费或退款后同步刷新总额与充值中心。 充值中心读取增加 latest-wins 门禁并阻止旧响应覆盖权威结果。 补充钱包刷新、画板入口和充值乱序回归测试。 同步更新泥点统一资产入口决策记录。
This commit is contained in:
@@ -57,7 +57,7 @@ test('shows only permanent and daily free points in the shared wallet panel', as
|
||||
expect(within(details).getByText('每日免费泥点')).toBeTruthy();
|
||||
expect(within(details).getByText('27')).toBeTruthy();
|
||||
expect(within(details).getByText('每天重置为 20 泥点')).toBeTruthy();
|
||||
expect(onRequestDetails).not.toHaveBeenCalled();
|
||||
expect(onRequestDetails).toHaveBeenCalledTimes(1);
|
||||
|
||||
await user.click(within(details).getByRole('button', { name: '使用详情' }));
|
||||
expect(onOpenLedger).toHaveBeenCalledTimes(1);
|
||||
@@ -85,6 +85,61 @@ test('keeps the chip on the live balance when cached details are stale', () => {
|
||||
expect(screen.queryByRole('button', { name: '泥点 207' })).toBeNull();
|
||||
});
|
||||
|
||||
test('refreshes cached balance details while open and after reopening', () => {
|
||||
const onRequestDetails = vi.fn();
|
||||
const { rerender } = render(
|
||||
<PlatformMudPointWalletEntry
|
||||
balance={207}
|
||||
breakdown={breakdown}
|
||||
onRequestDetails={onRequestDetails}
|
||||
onRecharge={vi.fn()}
|
||||
onOpenLedger={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const balanceButton = screen.getByRole('button', { name: '泥点 207' });
|
||||
fireEvent.mouseEnter(balanceButton);
|
||||
expect(onRequestDetails).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender(
|
||||
<PlatformMudPointWalletEntry
|
||||
balance={195}
|
||||
breakdown={breakdown}
|
||||
onRequestDetails={onRequestDetails}
|
||||
onRecharge={vi.fn()}
|
||||
onOpenLedger={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '泥点账户详情' })).toBeTruthy();
|
||||
expect(onRequestDetails).toHaveBeenCalledTimes(2);
|
||||
|
||||
fireEvent.pointerDown(document.body);
|
||||
fireEvent.mouseEnter(screen.getByRole('button', { name: '泥点 195' }));
|
||||
expect(onRequestDetails).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('requests missing details after the opening balance load completes', () => {
|
||||
const onRequestDetails = vi.fn();
|
||||
const props = {
|
||||
balance: 20,
|
||||
breakdown: null,
|
||||
variant: 'mobile' as const,
|
||||
onRequestDetails,
|
||||
onRecharge: vi.fn(),
|
||||
onOpenLedger: vi.fn(),
|
||||
};
|
||||
const { rerender } = render(
|
||||
<PlatformMudPointWalletEntry {...props} isLoading />,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '泥点 20' }));
|
||||
expect(onRequestDetails).not.toHaveBeenCalled();
|
||||
|
||||
rerender(<PlatformMudPointWalletEntry {...props} isLoading={false} />);
|
||||
expect(onRequestDetails).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('keeps the desktop panel open while moving across the gap without click pinning', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ export function PlatformMudPointWalletEntry({
|
||||
}: PlatformMudPointWalletEntryProps) {
|
||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
const isOpenRef = useRef(false);
|
||||
const requestedDetailsKeyForOpenRef = useRef<string | null>(null);
|
||||
const closeTimerRef = useRef<number | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const isCompact = variant === 'mobile';
|
||||
@@ -74,6 +75,7 @@ export function PlatformMudPointWalletEntry({
|
||||
: formatMudPointCount(displayedBalance, true);
|
||||
const exactBalanceLabel =
|
||||
displayedBalance === null ? '--' : formatMudPointCount(displayedBalance);
|
||||
const detailsRequestKey = `balance:${balance ?? breakdown?.totalPoints ?? 'unknown'}`;
|
||||
|
||||
const cancelPendingClose = useCallback(() => {
|
||||
if (closeTimerRef.current !== null) {
|
||||
@@ -85,9 +87,21 @@ export function PlatformMudPointWalletEntry({
|
||||
const closeDetails = useCallback(() => {
|
||||
cancelPendingClose();
|
||||
isOpenRef.current = false;
|
||||
requestedDetailsKeyForOpenRef.current = null;
|
||||
setIsOpen(false);
|
||||
}, [cancelPendingClose]);
|
||||
|
||||
const requestDetailsIfNeeded = useCallback(() => {
|
||||
if (
|
||||
isLoading ||
|
||||
requestedDetailsKeyForOpenRef.current === detailsRequestKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
requestedDetailsKeyForOpenRef.current = detailsRequestKey;
|
||||
onRequestDetails();
|
||||
}, [detailsRequestKey, isLoading, onRequestDetails]);
|
||||
|
||||
const requestAndOpen = useCallback(() => {
|
||||
cancelPendingClose();
|
||||
if (isOpenRef.current) {
|
||||
@@ -95,13 +109,17 @@ export function PlatformMudPointWalletEntry({
|
||||
}
|
||||
isOpenRef.current = true;
|
||||
setIsOpen(true);
|
||||
if (!breakdown && !isLoading) {
|
||||
onRequestDetails();
|
||||
}
|
||||
}, [breakdown, cancelPendingClose, isLoading, onRequestDetails]);
|
||||
requestDetailsIfNeeded();
|
||||
}, [cancelPendingClose, requestDetailsIfNeeded]);
|
||||
|
||||
useEffect(() => cancelPendingClose, [cancelPendingClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
requestDetailsIfNeeded();
|
||||
}
|
||||
}, [isOpen, requestDetailsIfNeeded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
@@ -151,9 +169,7 @@ export function PlatformMudPointWalletEntry({
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={`platform-mud-point-wallet-entry relative shrink-0 ${className ?? ''}`}
|
||||
onMouseEnter={isCompact ? undefined : requestAndOpen}
|
||||
onMouseLeave={isCompact ? undefined : closeAfterPointerLeaves}
|
||||
onFocusCapture={isCompact ? undefined : requestAndOpen}
|
||||
onBlurCapture={closeAfterFocusLeaves}
|
||||
>
|
||||
<div
|
||||
@@ -168,6 +184,8 @@ export function PlatformMudPointWalletEntry({
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="dialog"
|
||||
aria-busy={isLoading}
|
||||
onMouseEnter={isCompact ? undefined : requestAndOpen}
|
||||
onFocus={isCompact ? undefined : requestAndOpen}
|
||||
onClick={
|
||||
isCompact
|
||||
? () => {
|
||||
|
||||
Reference in New Issue
Block a user