Files
Genarrative/src/components/platform-entry/PlatformProfileRechargeModal.test.tsx
T
kdletters 66178edc7c 统一泥点账户与每日免费额度
- 新增每日免费泥点独立余额桶,按北京时间零点固定重置20泥点
- 按每日免费、会员限时、永久顺序扣费并支持跨日退款按原分桶回补
- 统一主站与图片画板泥点入口,隐藏限时泥点、任务入口和会员购买入口
- 收敛四档充值商品与泥点购买弹窗,并接入后台动态商品配置
- 同步余额read model、SpacetimeDB迁移绑定、回归测试和工程文档
- 将泥点明细浮层圆角匹配首页卡片视觉规范
2026-07-12 19:35:22 +08:00

310 lines
8.8 KiB
TypeScript

/* @vitest-environment jsdom */
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, test, vi } from 'vitest';
import type { ProfileRechargeProduct } from '../../../packages/shared/src/contracts/runtime';
import { PlatformProfileRechargeModal } from './PlatformProfileRechargeModal';
function buildNormalMembership() {
return {
status: 'normal' as const,
tier: 'normal' as const,
startedAt: null,
expiresAt: null,
updatedAt: null,
cycleStartedAt: null,
cycleResetsAt: null,
cycleGrantedPoints: 0,
cycleRemainingPoints: 0,
cyclePeriodDays: 30,
};
}
function buildPointProduct(
overrides: Partial<ProfileRechargeProduct> = {},
): ProfileRechargeProduct {
return {
productId: 'points_60',
title: '60泥点',
priceCents: 600,
kind: 'points' as const,
pointsAmount: 60,
bonusPoints: 0,
durationDays: 0,
badgeLabel: '',
description: '60泥点',
tier: 'normal' as const,
membershipPeriodPoints: 0,
membershipPeriodDays: 0,
membershipQueueLimit: 0,
membershipDiscountBps: 0,
...overrides,
};
}
function buildPointProducts(): ProfileRechargeProduct[] {
return [
buildPointProduct(),
buildPointProduct({
productId: 'points_180',
title: '180泥点',
priceCents: 1800,
pointsAmount: 180,
bonusPoints: 90,
badgeLabel: '首充加赠',
description: '首充加赠90泥点',
}),
buildPointProduct({
productId: 'points_300',
title: '300泥点',
priceCents: 3000,
pointsAmount: 300,
bonusPoints: 150,
badgeLabel: '首充加赠',
description: '首充加赠150泥点',
}),
buildPointProduct({
productId: 'points_680',
title: '680泥点',
priceCents: 6800,
pointsAmount: 680,
bonusPoints: 340,
badgeLabel: '首充加赠',
description: '首充加赠340泥点',
}),
];
}
function buildMembershipProduct(): ProfileRechargeProduct {
return {
...buildPointProduct(),
productId: 'member_month',
title: '会员月卡',
priceCents: 2800,
kind: 'membership',
pointsAmount: 0,
durationDays: 30,
description: '会员月卡',
tier: 'month',
membershipPeriodDays: 30,
};
}
vi.mock('qrcode', () => ({
default: {
toDataURL: vi.fn(async () => 'data:image/png;base64,wechat-native-qr'),
},
}));
describe('PlatformProfileRechargeModal', () => {
test('renders the four point products and forwards the selected buy action', async () => {
const user = userEvent.setup();
const onBuy = vi.fn();
render(
<PlatformProfileRechargeModal
center={{
walletBalance: 29,
membership: buildNormalMembership(),
pointProducts: buildPointProducts(),
membershipProducts: [buildMembershipProduct()],
benefits: [],
latestOrder: null,
hasPointsRecharged: false,
}}
isLoading={false}
error={null}
submittingProductId={null}
nativePayment={null}
onClose={vi.fn()}
onRetry={vi.fn()}
onBuy={onBuy}
onConfirmNativePayment={vi.fn()}
onCloseNativePayment={vi.fn()}
/>,
);
const dialog = screen.getByRole('dialog', { name: '购买更多泥点' });
expect(within(dialog).getByText('当前余额 29 泥点')).toBeTruthy();
expect(
within(dialog).getByRole('button', { name: '60泥点 ¥6 购买' }),
).toBeTruthy();
expect(
within(dialog).getByRole('button', {
name: '180泥点 +90泥点 ¥18 购买',
}),
).toBeTruthy();
expect(
within(dialog).getByRole('button', {
name: '300泥点 +150泥点 ¥30 购买',
}),
).toBeTruthy();
expect(
within(dialog).getByRole('button', {
name: '680泥点 +340泥点 ¥68 购买',
}),
).toBeTruthy();
expect(within(dialog).getAllByText('首充加赠')).toHaveLength(3);
expect(within(dialog).queryByRole('tablist')).toBeNull();
expect(within(dialog).queryByText('会员月卡')).toBeNull();
await user.click(
within(dialog).getByRole('button', {
name: '180泥点 +90泥点 ¥18 购买',
}),
);
expect(onBuy).toHaveBeenCalledWith(
expect.objectContaining({ productId: 'points_180' }),
);
});
test('shows the point-product empty state without exposing membership purchase', () => {
render(
<PlatformProfileRechargeModal
center={{
walletBalance: 0,
membership: buildNormalMembership(),
pointProducts: [],
membershipProducts: [buildMembershipProduct()],
benefits: [],
latestOrder: null,
hasPointsRecharged: false,
}}
isLoading={false}
error={null}
submittingProductId={null}
nativePayment={null}
onClose={vi.fn()}
onRetry={vi.fn()}
onBuy={vi.fn()}
onConfirmNativePayment={vi.fn()}
onCloseNativePayment={vi.fn()}
/>,
);
expect(screen.getByText('暂无可购买套餐')).toBeTruthy();
expect(screen.queryByText('会员月卡')).toBeNull();
expect(screen.queryByRole('tablist')).toBeNull();
});
test('opens native payment QR code in a separate dialog', async () => {
const user = userEvent.setup();
const onConfirmNativePayment = vi.fn();
const onCloseNativePayment = vi.fn();
render(
<PlatformProfileRechargeModal
center={{
walletBalance: 12,
membership: buildNormalMembership(),
pointProducts: [],
membershipProducts: [],
benefits: [],
latestOrder: null,
hasPointsRecharged: false,
}}
isLoading={false}
error={null}
submittingProductId={null}
nativePayment={{
orderId: 'rcg_native_001',
productTitle: '100泥点',
amountCents: 1000,
codeUrl: 'weixin://wxpay/bizpayurl?pr=test',
expiresAt: '2099-01-01T00:05:00Z',
isConfirming: false,
confirmMessage: '暂未确认到账,请确认付款完成后再点一次。',
}}
onClose={vi.fn()}
onRetry={vi.fn()}
onBuy={vi.fn()}
onConfirmNativePayment={onConfirmNativePayment}
onCloseNativePayment={onCloseNativePayment}
/>,
);
const rechargeDialog = screen.getByRole('dialog', {
name: '购买更多泥点',
});
const paymentDialog = screen.getByRole('dialog', {
name: '微信扫码支付',
});
expect(
within(rechargeDialog).queryByAltText('微信 Native 支付二维码'),
).toBeNull();
expect(
await within(paymentDialog).findByAltText('微信 Native 支付二维码'),
).toBeTruthy();
expect(within(paymentDialog).getByText('100泥点')).toBeTruthy();
expect(within(paymentDialog).getByText('¥10')).toBeTruthy();
expect(
within(paymentDialog).getByText(
'暂未确认到账,请确认付款完成后再点一次。',
),
).toBeTruthy();
await user.click(
within(paymentDialog).getByRole('button', { name: '我已支付' }),
);
expect(onConfirmNativePayment).toHaveBeenCalledTimes(1);
await user.click(
within(paymentDialog).getByRole('button', {
name: '关闭微信扫码支付',
}),
);
expect(onCloseNativePayment).toHaveBeenCalledTimes(1);
});
test('shows expired native payment state when qr countdown ends', async () => {
const onConfirmNativePayment = vi.fn();
render(
<PlatformProfileRechargeModal
center={{
walletBalance: 12,
membership: buildNormalMembership(),
pointProducts: [],
membershipProducts: [],
benefits: [],
latestOrder: null,
hasPointsRecharged: false,
}}
isLoading={false}
error={null}
submittingProductId={null}
nativePayment={{
orderId: 'rcg_native_expired',
productTitle: '100泥点',
amountCents: 1000,
codeUrl: 'weixin://wxpay/bizpayurl?pr=expired',
expiresAt: '2000-01-01T00:00:00Z',
isConfirming: false,
}}
onClose={vi.fn()}
onRetry={vi.fn()}
onBuy={vi.fn()}
onConfirmNativePayment={onConfirmNativePayment}
onCloseNativePayment={vi.fn()}
/>,
);
const paymentDialog = screen.getByRole('dialog', {
name: '微信扫码支付',
});
const confirmButton = within(paymentDialog).getByRole('button', {
name: '已过期',
});
expect(
await within(paymentDialog).findByAltText('微信 Native 支付二维码'),
).toBeTruthy();
expect(within(paymentDialog).getAllByText('已过期')).toHaveLength(2);
expect(confirmButton).toHaveProperty('disabled', true);
expect(onConfirmNativePayment).not.toHaveBeenCalled();
});
});