66178edc7c
- 新增每日免费泥点独立余额桶,按北京时间零点固定重置20泥点 - 按每日免费、会员限时、永久顺序扣费并支持跨日退款按原分桶回补 - 统一主站与图片画板泥点入口,隐藏限时泥点、任务入口和会员购买入口 - 收敛四档充值商品与泥点购买弹窗,并接入后台动态商品配置 - 同步余额read model、SpacetimeDB迁移绑定、回归测试和工程文档 - 将泥点明细浮层圆角匹配首页卡片视觉规范
310 lines
8.8 KiB
TypeScript
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();
|
|
});
|
|
});
|