8ed716c6e2
- 会员结算 canSubmit 由 !submitting 改为 !submittingProductId,与充点分支的 ctaDisabled 口径一致 - 避免充点在途时仍能提交会员结算、被 buyRechargeProduct 静默丢弃后直接关闭弹层 - 补充反向用例:泥点订单提交中时会员结算按钮禁用
809 lines
26 KiB
TypeScript
809 lines
26 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';
|
|
|
|
import { PlatformProfileRechargeModal } from './index';
|
|
|
|
function buildNormalMembership() {
|
|
return {
|
|
status: 'normal' as const,
|
|
plan: 'normal' as const,
|
|
startedAt: null,
|
|
expiresAt: null,
|
|
updatedAt: null,
|
|
cycleStartedAt: null,
|
|
cycleResetsAt: null,
|
|
cycleGrantedPoints: 0,
|
|
cycleRemainingPoints: 0,
|
|
cycleIndex: 1,
|
|
cycleCount: 1,
|
|
cycleKind: 'monthly' as const,
|
|
};
|
|
}
|
|
|
|
function buildActiveMembership(
|
|
plan: 'plus' | 'max',
|
|
cycleKind: 'monthly' | 'yearly',
|
|
) {
|
|
return {
|
|
status: 'active' as const,
|
|
plan,
|
|
startedAt: '2026-01-01T00:00:00+08:00',
|
|
expiresAt: '2027-01-01T00:00:00+08:00',
|
|
updatedAt: '2026-04-01T00:00:00+08:00',
|
|
cycleStartedAt: '2026-03-20T09:00:00+08:00',
|
|
cycleResetsAt: '2026-04-20T09:00:00+08:00',
|
|
cycleGrantedPoints: 1_150,
|
|
cycleRemainingPoints: 615,
|
|
cycleIndex: 4,
|
|
cycleCount: 12,
|
|
cycleKind,
|
|
};
|
|
}
|
|
|
|
/** 三池余额 fixture:每日免费 / 月度 / 永久。 */
|
|
function buildMudPointBalance(totalPoints: number) {
|
|
return {
|
|
dailyFreePoints: 0,
|
|
dailyFreeResetPoints: 20,
|
|
dailyFreeResetsAt: '2026-08-04T00:00:00+08:00',
|
|
monthlyPoints: 0,
|
|
monthlyResetsAt: null,
|
|
permanentPoints: totalPoints,
|
|
};
|
|
}
|
|
|
|
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泥点',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// 六档充点商品:没有首充赠送。
|
|
function buildPointProducts(): ProfileRechargeProduct[] {
|
|
return [60, 180, 300, 680, 1_280, 3_280].map((pointsAmount) =>
|
|
buildPointProduct({
|
|
productId: `points_${pointsAmount}`,
|
|
title: `${pointsAmount}泥点`,
|
|
priceCents: pointsAmount * 10,
|
|
pointsAmount,
|
|
description: `${pointsAmount}泥点`,
|
|
}),
|
|
);
|
|
}
|
|
|
|
const STARTER_PLAN = {
|
|
plan: 'starter' as const,
|
|
title: 'Starter',
|
|
rank: 1,
|
|
monthPriceCents: 3_900,
|
|
yearPriceCents: 39_000,
|
|
periodPoints: 400,
|
|
modelAccess: 'basic' as const,
|
|
concurrentJobLimit: 1,
|
|
recommended: false,
|
|
unlimitedConcurrency: false,
|
|
enabled: true,
|
|
};
|
|
|
|
const PLUS_PLAN = {
|
|
...STARTER_PLAN,
|
|
plan: 'plus' as const,
|
|
title: 'Plus',
|
|
rank: 2,
|
|
monthPriceCents: 9_900,
|
|
yearPriceCents: 99_000,
|
|
periodPoints: 1_150,
|
|
modelAccess: 'full' as const,
|
|
concurrentJobLimit: 3,
|
|
recommended: true,
|
|
};
|
|
|
|
const MAX_PLAN = {
|
|
...STARTER_PLAN,
|
|
plan: 'max' as const,
|
|
title: 'Max',
|
|
rank: 4,
|
|
monthPriceCents: 69_900,
|
|
yearPriceCents: 699_000,
|
|
periodPoints: 8_650,
|
|
modelAccess: 'full' as const,
|
|
concurrentJobLimit: 128,
|
|
unlimitedConcurrency: true,
|
|
};
|
|
|
|
vi.mock('qrcode', () => ({
|
|
default: {
|
|
toDataURL: vi.fn(async () => 'data:image/png;base64,wechat-native-qr'),
|
|
},
|
|
}));
|
|
|
|
describe('PlatformProfileRechargeModal', () => {
|
|
test('renders the membership plans and forwards a first-time purchase', async () => {
|
|
const user = userEvent.setup();
|
|
const onSubmitMembershipCheckout = vi.fn();
|
|
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(29),
|
|
membership: buildNormalMembership(),
|
|
pointProducts: buildPointProducts(),
|
|
membershipPlans: [STARTER_PLAN, PLUS_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
nativePayment={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={onSubmitMembershipCheckout}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByRole('dialog', { name: '会员与泥点' })).toBeTruthy();
|
|
expect(screen.getByText('可用总额 29 泥点')).toBeTruthy();
|
|
const tablist = screen.getByRole('tablist', { name: '会员与泥点' });
|
|
expect(
|
|
within(tablist)
|
|
.getByRole('tab', { name: '会员订阅' })
|
|
.getAttribute('aria-selected'),
|
|
).toBe('true');
|
|
expect(screen.getAllByText('Starter').length).toBeGreaterThan(0);
|
|
expect(screen.getAllByText('Plus').length).toBeGreaterThan(0);
|
|
|
|
await user.click(
|
|
screen.getByRole('button', { name: 'Starter 月付 开通会员' }),
|
|
);
|
|
|
|
const checkout = screen.getByRole('dialog', { name: '确认订单' });
|
|
expect(within(checkout).getByText('¥39')).toBeTruthy();
|
|
expect(within(checkout).getByText('400(首期)')).toBeTruthy();
|
|
expect(within(checkout).getByText('微信支付')).toBeTruthy();
|
|
expect(within(checkout).queryByText(/报价已失效/)).toBeNull();
|
|
expect(within(checkout).queryByText(/倒计时/)).toBeNull();
|
|
|
|
await user.click(
|
|
within(checkout).getByRole('button', { name: '支付 ¥39' }),
|
|
);
|
|
|
|
expect(onSubmitMembershipCheckout).toHaveBeenCalledWith(
|
|
'starter',
|
|
'monthly',
|
|
);
|
|
});
|
|
|
|
test('周期切换按钮支持方向键与 roving tabindex', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(29),
|
|
membership: buildNormalMembership(),
|
|
pointProducts: buildPointProducts(),
|
|
membershipPlans: [STARTER_PLAN, PLUS_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
nativePayment={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const group = screen.getByRole('radiogroup', { name: '付费周期' });
|
|
const monthly = within(group).getByRole('radio', { name: '月付' });
|
|
const yearly = within(group).getByRole('radio', { name: '年付' });
|
|
expect(monthly.getAttribute('tabindex')).toBe('0');
|
|
expect(yearly.getAttribute('tabindex')).toBe('-1');
|
|
|
|
monthly.focus();
|
|
await user.keyboard('{ArrowRight}');
|
|
|
|
expect(yearly.getAttribute('aria-checked')).toBe('true');
|
|
expect(monthly.getAttribute('aria-checked')).toBe('false');
|
|
expect(yearly.getAttribute('tabindex')).toBe('0');
|
|
expect(monthly.getAttribute('tabindex')).toBe('-1');
|
|
expect(document.activeElement).toBe(yearly);
|
|
});
|
|
|
|
test('requests the server quote and shows the upgrade amount', async () => {
|
|
const user = userEvent.setup();
|
|
const onRequestMembershipQuote = vi.fn();
|
|
const onSubmitMembershipCheckout = vi.fn();
|
|
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(615),
|
|
membership: buildActiveMembership('plus', 'yearly'),
|
|
pointProducts: buildPointProducts(),
|
|
membershipPlans: [STARTER_PLAN, PLUS_PLAN, MAX_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: true,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
nativePayment={null}
|
|
membershipQuote={{
|
|
currentPlan: 'plus',
|
|
targetPlan: 'max',
|
|
cycleKind: 'yearly',
|
|
cycleIndex: 4,
|
|
cycleCount: 12,
|
|
amountCents: 10_000,
|
|
grantedPointsDelta: 1_250,
|
|
monthlyBalanceAfter: 1_865,
|
|
expiresAt: '2027-01-01T09:00:00+08:00',
|
|
cycleResetsAt: '2026-04-20T09:00:00+08:00',
|
|
remainingFullMonths: 8,
|
|
}}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={onRequestMembershipQuote}
|
|
onSubmitMembershipCheckout={onSubmitMembershipCheckout}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(
|
|
screen.getByRole('button', { name: 'Plus 年付 当前套餐' }),
|
|
).toHaveProperty('disabled', true);
|
|
expect(screen.getByText('当前 Plus · 年付')).toBeTruthy();
|
|
expect(screen.getAllByText(/可同周期补差升档/).length).toBeGreaterThan(0);
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Max 年付 补差升级' }));
|
|
|
|
expect(onRequestMembershipQuote).toHaveBeenCalledWith('max');
|
|
|
|
const checkout = screen.getByRole('dialog', { name: '确认套餐变更' });
|
|
expect(within(checkout).getByText('¥100')).toBeTruthy();
|
|
expect(within(checkout).getByText('本期补发月度泥点')).toBeTruthy();
|
|
|
|
await user.click(
|
|
within(checkout).getByRole('button', { name: '补差升级 ¥100' }),
|
|
);
|
|
|
|
expect(onSubmitMembershipCheckout).toHaveBeenCalledWith('max', 'yearly');
|
|
});
|
|
|
|
test('已是最高的 max 档不再提示可补差升档', () => {
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(8_650),
|
|
membership: buildActiveMembership('max', 'yearly'),
|
|
pointProducts: buildPointProducts(),
|
|
membershipPlans: [STARTER_PLAN, PLUS_PLAN, MAX_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: true,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
nativePayment={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByText('当前 Max · 年付')).toBeTruthy();
|
|
expect(screen.queryByText(/可同周期补差升档/)).toBeNull();
|
|
expect(screen.getByText('周期切换需等到期后')).toBeTruthy();
|
|
});
|
|
|
|
test('keeps an active member as member when their current plan is disabled', async () => {
|
|
const user = userEvent.setup();
|
|
const onRequestMembershipQuote = vi.fn();
|
|
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(615),
|
|
membership: buildActiveMembership('plus', 'yearly'),
|
|
pointProducts: buildPointProducts(),
|
|
// plus 在后台被临时禁用:目录里仍有该档,但过滤后的可购买列表只剩 max。
|
|
membershipPlans: [{ ...PLUS_PLAN, enabled: false }, MAX_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: true,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
nativePayment={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={onRequestMembershipQuote}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// 状态卡仍认得当前档位,而不是退化成「会员」。
|
|
expect(screen.getByText('当前 Plus · 年付')).toBeTruthy();
|
|
// 有效会员看到的是补差升级,而不是可在有效会员上重复开通的「开通会员」。
|
|
await user.click(screen.getByRole('button', { name: 'Max 年付 补差升级' }));
|
|
expect(onRequestMembershipQuote).toHaveBeenCalledWith('max');
|
|
|
|
const checkout = screen.getByRole('dialog', { name: '确认套餐变更' });
|
|
// 即使原档位已禁用,升级摘要也要给出「原套餐标准年价」。
|
|
expect(within(checkout).getByText('原套餐标准年价')).toBeTruthy();
|
|
expect(within(checkout).getByText('¥990')).toBeTruthy();
|
|
});
|
|
|
|
test('shows a placeholder instead of the list price while the upgrade quote is loading', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(615),
|
|
membership: buildActiveMembership('plus', 'yearly'),
|
|
pointProducts: buildPointProducts(),
|
|
membershipPlans: [STARTER_PLAN, PLUS_PLAN, MAX_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: true,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
nativePayment={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={true}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Max 年付 补差升级' }));
|
|
|
|
const checkout = screen.getByRole('dialog', { name: '确认套餐变更' });
|
|
// 报价未到:金额占位,不能把 Max 标准年价(¥6990)当成「本次补付」。
|
|
expect(within(checkout).getByText('—')).toBeTruthy();
|
|
expect(
|
|
within(checkout).queryByRole('button', { name: /^补差升级 ¥/ }),
|
|
).toBeNull();
|
|
expect(
|
|
within(checkout).getByRole('button', { name: '补差升级' }),
|
|
).toHaveProperty('disabled', true);
|
|
});
|
|
|
|
test('recharge tab picks a point product and forwards the buy action', async () => {
|
|
const user = userEvent.setup();
|
|
const onBuy = vi.fn();
|
|
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(29),
|
|
membership: buildNormalMembership(),
|
|
pointProducts: buildPointProducts(),
|
|
membershipPlans: [STARTER_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
nativePayment={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={onBuy}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('tab', { name: '泥点充值' }));
|
|
|
|
const products = screen.getByRole('radiogroup', { name: '泥点充值档位' });
|
|
await user.click(
|
|
within(products).getByRole('radio', { name: '60泥点 ¥6' }),
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: '充值 ¥6' }));
|
|
|
|
const checkout = screen.getByRole('dialog', { name: '确认订单' });
|
|
expect(within(checkout).getByText('60 永久泥点')).toBeTruthy();
|
|
|
|
await user.click(within(checkout).getByRole('button', { name: '充值 ¥6' }));
|
|
|
|
expect(onBuy).toHaveBeenCalledTimes(1);
|
|
expect(onBuy.mock.calls[0][0]).toMatchObject({
|
|
productId: 'points_60',
|
|
priceCents: 600,
|
|
});
|
|
});
|
|
|
|
test('会员订单提交中时泥点档位仍可选,但下单入口禁用', async () => {
|
|
const user = userEvent.setup();
|
|
const renderModal = (submittingProductId: string | null) => (
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(29),
|
|
membership: buildNormalMembership(),
|
|
pointProducts: buildPointProducts(),
|
|
membershipPlans: [STARTER_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={submittingProductId}
|
|
nativePayment={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
const { rerender } = render(renderModal(null));
|
|
await user.click(screen.getByRole('tab', { name: '泥点充值' }));
|
|
const product = within(
|
|
screen.getByRole('radiogroup', { name: '泥点充值档位' }),
|
|
).getByRole('radio', { name: '60泥点 ¥6' });
|
|
// 既有行为:会员订单提交不波及泥点档位的可选状态。
|
|
expect(product).not.toHaveProperty('disabled', true);
|
|
|
|
await user.click(product);
|
|
await user.click(screen.getByRole('button', { name: '充值 ¥6' }));
|
|
|
|
// 会员订单提交中:泥点结算 CTA 禁用,避免被宿主守卫静默丢弃。
|
|
rerender(renderModal('membership-pro-monthly'));
|
|
expect(
|
|
within(screen.getByRole('dialog', { name: '确认订单' })).getByRole(
|
|
'button',
|
|
{ name: '充值 ¥6' },
|
|
),
|
|
).toHaveProperty('disabled', true);
|
|
});
|
|
|
|
test('泥点订单提交中时会员结算入口禁用', async () => {
|
|
const user = userEvent.setup();
|
|
const renderModal = (submittingProductId: string | null) => (
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(29),
|
|
membership: buildNormalMembership(),
|
|
pointProducts: buildPointProducts(),
|
|
membershipPlans: [STARTER_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={submittingProductId}
|
|
nativePayment={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>
|
|
);
|
|
|
|
const { rerender } = render(renderModal(null));
|
|
await user.click(
|
|
screen.getByRole('button', { name: 'Starter 月付 开通会员' }),
|
|
);
|
|
|
|
// 泥点订单提交中:会员结算 CTA 同样禁用,避免宿主守卫静默丢弃提交后直接关弹层。
|
|
rerender(renderModal('points_60'));
|
|
expect(
|
|
within(screen.getByRole('dialog', { name: '确认订单' })).getByRole(
|
|
'button',
|
|
{ name: '支付 ¥39' },
|
|
),
|
|
).toHaveProperty('disabled', true);
|
|
});
|
|
|
|
test('shows the empty state when the recharge tab has no point product', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(29),
|
|
membership: buildNormalMembership(),
|
|
pointProducts: [],
|
|
membershipPlans: [STARTER_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
nativePayment={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await user.click(screen.getByRole('tab', { name: '泥点充值' }));
|
|
|
|
expect(screen.getAllByText('Starter').length).toBeGreaterThan(0);
|
|
expect(screen.getByText('暂无可购买套餐')).toBeTruthy();
|
|
});
|
|
|
|
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={{
|
|
mudPointBalance: buildMudPointBalance(12),
|
|
membership: buildNormalMembership(),
|
|
pointProducts: [],
|
|
membershipPlans: [],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={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()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={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={{
|
|
mudPointBalance: buildMudPointBalance(12),
|
|
membership: buildNormalMembership(),
|
|
pointProducts: [],
|
|
membershipPlans: [],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={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()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={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();
|
|
});
|
|
|
|
test('hides the daily free pool when the daily grant is zero', () => {
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: {
|
|
...buildMudPointBalance(100),
|
|
dailyFreePoints: 0,
|
|
dailyFreeResetPoints: 0,
|
|
},
|
|
membership: buildNormalMembership(),
|
|
pointProducts: [],
|
|
membershipPlans: [STARTER_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
nativePayment={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const dialog = screen.getByRole('dialog', { name: '会员与泥点' });
|
|
expect(within(dialog).queryByText('每日免费')).toBeNull();
|
|
expect(within(dialog).getByText('月度泥点')).toBeTruthy();
|
|
expect(within(dialog).getByText('永久泥点')).toBeTruthy();
|
|
});
|
|
|
|
test('shows the daily free pool while the daily grant is positive', () => {
|
|
render(
|
|
<PlatformProfileRechargeModal
|
|
center={{
|
|
mudPointBalance: buildMudPointBalance(100),
|
|
membership: buildNormalMembership(),
|
|
pointProducts: [],
|
|
membershipPlans: [STARTER_PLAN],
|
|
latestOrder: null,
|
|
hasPointsRecharged: false,
|
|
}}
|
|
isLoading={false}
|
|
error={null}
|
|
submittingProductId={null}
|
|
membershipQuote={null}
|
|
isLoadingMembershipQuote={false}
|
|
membershipQuoteError={null}
|
|
nativePayment={null}
|
|
onClose={vi.fn()}
|
|
onRetry={vi.fn()}
|
|
onBuy={vi.fn()}
|
|
onRequestMembershipQuote={vi.fn()}
|
|
onSubmitMembershipCheckout={vi.fn()}
|
|
onConfirmNativePayment={vi.fn()}
|
|
onCloseNativePayment={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const dialog = screen.getByRole('dialog', { name: '会员与泥点' });
|
|
expect(within(dialog).getByText('每日免费')).toBeTruthy();
|
|
});
|
|
});
|