修复泥点余额明细刷新
钱包明细每次展开并在实时余额变化时读取最新拆分。 图片画板生成扣费或退款后同步刷新总额与充值中心。 充值中心读取增加 latest-wins 门禁并阻止旧响应覆盖权威结果。 补充钱包刷新、画板入口和充值乱序回归测试。 同步更新泥点统一资产入口决策记录。
This commit is contained in:
@@ -184,7 +184,7 @@
|
||||
## 2026-07-12 泥点充值收敛为四档并统一资产入口
|
||||
|
||||
- 背景:主站与图片画板的泥点余额入口、余额明细和充值弹窗存在不同实现,旧充值口径仍展示六档泥点、首充双倍和会员购买 / 升级入口,容易让展示、商品资格与后端余额真相发生漂移。
|
||||
- 决策:主站与图片画板统一复用公共泥点资产入口,收起态展示总额与充值,展开态只展示不限时泥点、每日免费泥点和使用详情;充值中心 BFF 继续统一下发总额、三桶余额、限时到期时间、每日免费基础重置额及下次重置时间,前端不得自行相减推算,但会员周期限时泥点仅用于存量兼容和后端结算,当前版本不在前台展示。默认泥点商品收敛为 `60 / ¥6`、`180 + 90 / ¥18`、`300 + 150 / ¥30`、`680 + 340 / ¥68` 四档,`60` 档无赠送,后三档按现有 `user_id + product_id` 独立资格规则首次购买加赠 `50%`。当前版本关闭会员购买页签、会员商品和购买 / 升级入口。
|
||||
- 决策:主站与图片画板统一复用公共泥点资产入口,收起态展示总额与充值,展开态只展示不限时泥点、每日免费泥点和使用详情;充值中心 BFF 继续统一下发总额、三桶余额、限时到期时间、每日免费基础重置额及下次重置时间,前端不得自行相减推算,但会员周期限时泥点仅用于存量兼容和后端结算,当前版本不在前台展示。钱包明细每次展开都重新读取充值中心 BFF,打开期间实时总额变化时继续补读;图片画板的生成扣费或退款完成后同时刷新总额与充值中心拆分。充值中心读请求必须使用 revision 门禁,支付创建、到账确认等权威响应写入时使旧读失效,避免旧响应覆盖新的每日免费 / 不限时明细。默认泥点商品收敛为 `60 / ¥6`、`180 + 90 / ¥18`、`300 + 150 / ¥30`、`680 + 340 / ¥68` 四档,`60` 档无赠送,后三档按现有 `user_id + product_id` 独立资格规则首次购买加赠 `50%`。当前版本关闭会员购买页签、会员商品和购买 / 升级入口。
|
||||
- 影响范围:`profile_recharge_product_config` 默认商品、充值中心 read model、共享前后端契约、主站与图片画板泥点资产入口、充值弹窗、后台充值商品默认值。
|
||||
- 验证方式:充值与统一入口定向前端测试、`npm run typecheck`、充值商品定向 Rust 测试、`cargo check -p spacetime-module -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
@@ -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
|
||||
? () => {
|
||||
|
||||
@@ -522,6 +522,10 @@ export function ImageCanvasEditorView({
|
||||
requestLogin: () => authUiRef.current?.openLoginModal(),
|
||||
currentUser: authUi?.user ?? null,
|
||||
});
|
||||
const refreshEditorWalletState = useCallback(() => {
|
||||
refreshEditorWalletBalance();
|
||||
loadRechargeCenter();
|
||||
}, [loadRechargeCenter, refreshEditorWalletBalance]);
|
||||
const isAccountPaymentModalOpen =
|
||||
isRewardCodeOpen ||
|
||||
isRechargeOpen ||
|
||||
@@ -1239,7 +1243,7 @@ export function ImageCanvasEditorView({
|
||||
assetFolderId: activeUploadFolderId,
|
||||
upsertGeneratedAsset,
|
||||
applyProjectSnapshot: applyGeneratedProjectSnapshot,
|
||||
onWalletBalanceMayHaveChanged: refreshEditorWalletBalance,
|
||||
onWalletBalanceMayHaveChanged: refreshEditorWalletState,
|
||||
});
|
||||
const handleEditorAgentConfirmSent = useCallback(() => {
|
||||
generationSurface.refreshTaskList();
|
||||
@@ -1258,7 +1262,7 @@ export function ImageCanvasEditorView({
|
||||
if (warning) {
|
||||
showGenerationWarning(warning);
|
||||
}
|
||||
refreshEditorWalletBalance();
|
||||
refreshEditorWalletState();
|
||||
void loadEditorProject(projectId)
|
||||
.then(applyGeneratedProjectSnapshot)
|
||||
.catch(() => undefined);
|
||||
@@ -1266,7 +1270,7 @@ export function ImageCanvasEditorView({
|
||||
[
|
||||
applyGeneratedProjectSnapshot,
|
||||
projectId,
|
||||
refreshEditorWalletBalance,
|
||||
refreshEditorWalletState,
|
||||
showGenerationWarning,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('ImageCanvasTopbarView', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '充值' }));
|
||||
expect(props.onRecharge).toHaveBeenCalledTimes(1);
|
||||
expect(props.onRequestWalletDetails).not.toHaveBeenCalled();
|
||||
expect(props.onRequestWalletDetails).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows the current user avatar beside the mud point balance', () => {
|
||||
|
||||
@@ -385,6 +385,7 @@ export function usePlatformProfileCenterController({
|
||||
useCopyFeedback();
|
||||
const pendingWechatRechargeOrderIdRef = useRef<string | null>(null);
|
||||
const confirmingWechatRechargeOrderIdRef = useRef<string | null>(null);
|
||||
const rechargeCenterReadRevisionRef = useRef(0);
|
||||
|
||||
// 中文注释:支持带邀请码 query 的直达场景,登录成功后自动打开兑换面板并复用同一套输入状态。
|
||||
useEffect(() => {
|
||||
@@ -426,18 +427,40 @@ export function usePlatformProfileCenterController({
|
||||
loadWalletLedger();
|
||||
}, [loadWalletLedger]);
|
||||
|
||||
const applyRechargeCenter = useCallback(
|
||||
(center: ProfileRechargeCenterResponse) => {
|
||||
rechargeCenterReadRevisionRef.current += 1;
|
||||
setIsLoadingRechargeCenter(false);
|
||||
setRechargeError(null);
|
||||
setRechargeCenter(center);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const loadRechargeCenter = useCallback(() => {
|
||||
const revision = ++rechargeCenterReadRevisionRef.current;
|
||||
setRechargeError(null);
|
||||
setIsLoadingRechargeCenter(true);
|
||||
void getRpgProfileRechargeCenter()
|
||||
.then(setRechargeCenter)
|
||||
.then((center) => {
|
||||
if (revision === rechargeCenterReadRevisionRef.current) {
|
||||
setRechargeCenter(center);
|
||||
}
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (revision !== rechargeCenterReadRevisionRef.current) {
|
||||
return;
|
||||
}
|
||||
setRechargeCenter(null);
|
||||
setRechargeError(
|
||||
error instanceof Error ? error.message : '读取泥点购买信息失败',
|
||||
);
|
||||
})
|
||||
.finally(() => setIsLoadingRechargeCenter(false));
|
||||
.finally(() => {
|
||||
if (revision === rechargeCenterReadRevisionRef.current) {
|
||||
setIsLoadingRechargeCenter(false);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const refreshRechargeState = useCallback(() => {
|
||||
@@ -482,7 +505,7 @@ export function usePlatformProfileCenterController({
|
||||
.then((response) => {
|
||||
const result = buildRechargePaymentResultForOrder(response.order);
|
||||
const isPaid = result.kind === 'success';
|
||||
setRechargeCenter(response.center);
|
||||
applyRechargeCenter(response.center);
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
confirmingWechatRechargeOrderIdRef.current = null;
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
@@ -525,7 +548,7 @@ export function usePlatformProfileCenterController({
|
||||
|
||||
clearWechatPayResultHash();
|
||||
return true;
|
||||
}, [onRechargeSuccess, refreshRechargeState]);
|
||||
}, [applyRechargeCenter, onRechargeSuccess, refreshRechargeState]);
|
||||
|
||||
const pollWechatPayResultFromHash = useCallback(
|
||||
() => handleWechatPayResult(),
|
||||
@@ -549,7 +572,7 @@ export function usePlatformProfileCenterController({
|
||||
.then((response) => {
|
||||
const result = buildRechargePaymentResultForOrder(response.order);
|
||||
const isPaid = result.kind === 'success';
|
||||
setRechargeCenter(response.center);
|
||||
applyRechargeCenter(response.center);
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
confirmingWechatRechargeOrderIdRef.current = null;
|
||||
setWechatRechargeOrderConfirmationState(null);
|
||||
@@ -569,7 +592,7 @@ export function usePlatformProfileCenterController({
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}, [nativeWechatPayment, onRechargeSuccess]);
|
||||
}, [applyRechargeCenter, nativeWechatPayment, onRechargeSuccess]);
|
||||
|
||||
const openRechargeModal = useCallback(() => {
|
||||
if (!currentUser) {
|
||||
@@ -625,7 +648,7 @@ export function usePlatformProfileCenterController({
|
||||
.then(async (response) => {
|
||||
if (paymentChannel === WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_CHANNEL) {
|
||||
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
|
||||
setRechargeCenter(response.center);
|
||||
applyRechargeCenter(response.center);
|
||||
const paymentHandled = await requestHostPayment({
|
||||
payload: response.wechatMiniProgramPayParams,
|
||||
orderId: response.order.orderId,
|
||||
@@ -637,7 +660,7 @@ export function usePlatformProfileCenterController({
|
||||
}
|
||||
if (paymentChannel === WECHAT_JSAPI_PAYMENT_CHANNEL) {
|
||||
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
|
||||
setRechargeCenter(response.center);
|
||||
applyRechargeCenter(response.center);
|
||||
setRechargePaymentResult({
|
||||
kind: 'pending',
|
||||
title: '正在打开微信支付',
|
||||
@@ -657,7 +680,7 @@ export function usePlatformProfileCenterController({
|
||||
confirmResponse.order,
|
||||
);
|
||||
const isPaid = result.kind === 'success';
|
||||
setRechargeCenter(confirmResponse.center);
|
||||
applyRechargeCenter(confirmResponse.center);
|
||||
setRechargePaymentResult(result);
|
||||
if (result.kind !== 'pending') {
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
@@ -681,7 +704,7 @@ export function usePlatformProfileCenterController({
|
||||
throw new Error('微信 H5 支付链接生成失败');
|
||||
}
|
||||
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
|
||||
setRechargeCenter(response.center);
|
||||
applyRechargeCenter(response.center);
|
||||
setRechargePaymentResult({
|
||||
kind: 'pending',
|
||||
title: '正在打开微信支付',
|
||||
@@ -698,7 +721,7 @@ export function usePlatformProfileCenterController({
|
||||
throw new Error('微信 Native 支付链接生成失败');
|
||||
}
|
||||
pendingWechatRechargeOrderIdRef.current = response.order.orderId;
|
||||
setRechargeCenter(response.center);
|
||||
applyRechargeCenter(response.center);
|
||||
setNativeWechatPayment({
|
||||
...wechatNativePayment,
|
||||
codeUrl,
|
||||
@@ -764,7 +787,7 @@ export function usePlatformProfileCenterController({
|
||||
setSubmittingRechargeProductId(null);
|
||||
});
|
||||
},
|
||||
[onRechargeSuccess, submittingRechargeProductId],
|
||||
[applyRechargeCenter, onRechargeSuccess, submittingRechargeProductId],
|
||||
);
|
||||
|
||||
const confirmNativeWechatPayment = useCallback(() => {
|
||||
@@ -787,7 +810,7 @@ export function usePlatformProfileCenterController({
|
||||
}
|
||||
const result = buildRechargePaymentResultForOrder(response.order);
|
||||
const isPaid = result.kind === 'success';
|
||||
setRechargeCenter(response.center);
|
||||
applyRechargeCenter(response.center);
|
||||
if (result.kind !== 'pending') {
|
||||
setNativeWechatPayment(null);
|
||||
pendingWechatRechargeOrderIdRef.current = null;
|
||||
@@ -819,7 +842,7 @@ export function usePlatformProfileCenterController({
|
||||
);
|
||||
})
|
||||
.finally(() => setSubmittingRechargeProductId(null));
|
||||
}, [nativeWechatPayment, onRechargeSuccess]);
|
||||
}, [applyRechargeCenter, nativeWechatPayment, onRechargeSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
const orderId = nativeWechatPayment?.orderId;
|
||||
@@ -845,7 +868,7 @@ export function usePlatformProfileCenterController({
|
||||
}
|
||||
|
||||
const result = buildRechargePaymentResultForOrder(response.order);
|
||||
setRechargeCenter(response.center);
|
||||
applyRechargeCenter(response.center);
|
||||
if (result.kind === 'pending') {
|
||||
await waitWechatPayConfirmDelay(WECHAT_NATIVE_WATCH_RETRY_DELAY_MS);
|
||||
continue;
|
||||
@@ -882,6 +905,7 @@ export function usePlatformProfileCenterController({
|
||||
}, [
|
||||
nativeWechatPayment?.expiresAt,
|
||||
nativeWechatPayment?.orderId,
|
||||
applyRechargeCenter,
|
||||
onRechargeSuccess,
|
||||
]);
|
||||
|
||||
|
||||
@@ -4465,6 +4465,98 @@ test('logged in mobile recommend page exposes the shared wallet details and rech
|
||||
expect(within(rechargeDialog).queryByText('会员月卡')).toBeNull();
|
||||
});
|
||||
|
||||
test('the latest recharge center read wins when wallet and recharge overlap', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockNarrowMobileLayout();
|
||||
let resolveWalletRead!: (
|
||||
center: ProfileRechargeCenterResponse,
|
||||
) => void;
|
||||
let resolveRechargeRead!: (
|
||||
center: ProfileRechargeCenterResponse,
|
||||
) => void;
|
||||
const walletReadPromise = new Promise<ProfileRechargeCenterResponse>(
|
||||
(resolve) => {
|
||||
resolveWalletRead = resolve;
|
||||
},
|
||||
);
|
||||
const rechargeReadPromise = new Promise<ProfileRechargeCenterResponse>(
|
||||
(resolve) => {
|
||||
resolveRechargeRead = resolve;
|
||||
},
|
||||
);
|
||||
mockGetRpgProfileRechargeCenter
|
||||
.mockReturnValueOnce(walletReadPromise)
|
||||
.mockReturnValueOnce(rechargeReadPromise);
|
||||
|
||||
const { container } = render(
|
||||
<ProfileHomeViewHarness
|
||||
activeTab="home"
|
||||
profileDashboardOverrides={{ walletBalance: 207 }}
|
||||
/>,
|
||||
);
|
||||
const walletLayer = container.querySelector(
|
||||
'.platform-mobile-recommend-wallet-entry',
|
||||
);
|
||||
expect(walletLayer).toBeTruthy();
|
||||
|
||||
await user.click(
|
||||
within(walletLayer as HTMLElement).getByRole('button', {
|
||||
name: '泥点 207',
|
||||
}),
|
||||
);
|
||||
const rechargeButtons = within(walletLayer as HTMLElement).getAllByRole(
|
||||
'button',
|
||||
{ name: '充值' },
|
||||
);
|
||||
await user.click(rechargeButtons[0]!);
|
||||
|
||||
expect(mockGetRpgProfileRechargeCenter).toHaveBeenCalledTimes(2);
|
||||
|
||||
resolveRechargeRead({
|
||||
walletBalance: 207,
|
||||
mudPointBalance: {
|
||||
totalPoints: 207,
|
||||
permanentPoints: 187,
|
||||
limitedPoints: 0,
|
||||
limitedExpiresAt: null,
|
||||
dailyFreePoints: 20,
|
||||
dailyFreeResetPoints: 20,
|
||||
dailyFreeResetsAt: '2026-07-20T16:00:00Z',
|
||||
},
|
||||
membership: buildNormalMembership(),
|
||||
pointProducts: [buildPointProduct()],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
});
|
||||
|
||||
expect(await screen.findByText('购买更多泥点')).toBeTruthy();
|
||||
expect(screen.getByText('当前余额 207 泥点')).toBeTruthy();
|
||||
|
||||
resolveWalletRead({
|
||||
walletBalance: 0,
|
||||
mudPointBalance: {
|
||||
totalPoints: 0,
|
||||
permanentPoints: 0,
|
||||
limitedPoints: 0,
|
||||
limitedExpiresAt: null,
|
||||
dailyFreePoints: 0,
|
||||
dailyFreeResetPoints: 20,
|
||||
dailyFreeResetsAt: '2026-07-20T16:00:00Z',
|
||||
},
|
||||
membership: buildNormalMembership(),
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
});
|
||||
|
||||
await act(async () => undefined);
|
||||
expect(screen.getByText('当前余额 207 泥点')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('mobile discover search submits public work code', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSearchPublicCode = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user