This commit is contained in:
2026-05-14 14:21:17 +08:00
parent 7a75f5d612
commit d33c937ebc
191 changed files with 1916 additions and 1549 deletions

View File

@@ -149,14 +149,14 @@ const {
pointProducts: [
{
productId: 'points_60',
title: '60点',
title: '60点',
priceCents: 600,
kind: 'points',
pointsAmount: 60,
bonusPoints: 60,
durationDays: 0,
badgeLabel: '首充双倍',
description: '首充送60点',
description: '首充送60点',
tier: 'normal',
},
],
@@ -176,7 +176,7 @@ const {
],
benefits: [
{
benefitName: '免点回合数',
benefitName: '免点回合数',
normalValue: '30',
monthValue: '100',
seasonValue: '100',
@@ -191,7 +191,7 @@ const {
order: {
orderId: 'order-1',
productId: 'points_60',
productTitle: '60点',
productTitle: '60点',
kind: 'points',
amountCents: 600,
status: 'paid',
@@ -335,6 +335,38 @@ function dispatchPointerEvent(
target.dispatchEvent(event);
}
function stubImage(width = 800, height = 600) {
class MockImage {
onload: null | (() => void) = null;
onerror: null | (() => void) = null;
naturalWidth = width;
naturalHeight = height;
width = width;
height = height;
set src(_value: string) {
this.onload?.();
}
}
vi.stubGlobal('Image', MockImage as unknown as typeof Image);
}
function stubFileReader(dataUrl: string) {
class MockFileReader {
result: string | null = null;
onload: null | (() => void) = null;
onerror: null | (() => void) = null;
readAsDataURL() {
this.result = dataUrl;
this.onload?.();
}
}
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
}
const puzzlePublicEntry = {
sourceType: 'puzzle',
workId: 'puzzle-work-public-1',
@@ -826,6 +858,7 @@ afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
mockGetRpgProfileReferralInviteCenter.mockResolvedValue(
mockBuildReferralCenter(),
);
@@ -901,9 +934,9 @@ test('opens wallet ledger modal from narrative coin card', async () => {
const user = userEvent.setup();
renderProfileView();
await user.click(screen.getByRole('button', { name: /\s*0/u }));
await user.click(screen.getByRole('button', { name: /\s*0/u }));
expect(await screen.findByText('点账单')).toBeTruthy();
expect(await screen.findByText('点账单')).toBeTruthy();
expect(mockGetRpgProfileWalletLedger).toHaveBeenCalledTimes(1);
expect(screen.getByText('资产操作消耗')).toBeTruthy();
expect(screen.getByText('-1')).toBeTruthy();
@@ -923,7 +956,7 @@ test('profile recharge modal buys points through mock channel outside mini progr
expect(await screen.findByText('账户充值')).toBeTruthy();
expect(mockGetRpgProfileRechargeCenter).toHaveBeenCalledTimes(1);
await user.click(screen.getByRole('button', { name: /60/u }));
await user.click(screen.getByRole('button', { name: /60/u }));
await waitFor(() => {
expect(mockCreateRpgProfileRechargeOrder).toHaveBeenCalledWith(
@@ -953,7 +986,7 @@ test('profile recharge modal posts requestPayment params in mini program web-vie
order: {
orderId: 'order-wechat-1',
productId: 'points_60',
productTitle: '60点',
productTitle: '60点',
kind: 'points',
amountCents: 600,
status: 'pending' as const,
@@ -993,7 +1026,7 @@ test('profile recharge modal posts requestPayment params in mini program web-vie
await user.click(
within(shortcutRegion).getByRole('button', { name: //u }),
);
await user.click(await screen.findByRole('button', { name: /60/u }));
await user.click(await screen.findByRole('button', { name: /60/u }));
await waitFor(() => {
expect(mockCreateRpgProfileRechargeOrder).toHaveBeenCalledWith(
@@ -1029,7 +1062,7 @@ test('profile daily task shortcut opens task center and claims reward', async ()
expect(mockClaimRpgProfileTaskReward).toHaveBeenCalledWith('daily_login');
});
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
expect(await screen.findByText('已领取 10 点')).toBeTruthy();
expect(await screen.findByText('已领取 10 点')).toBeTruthy();
expect(
(screen.getByRole('button', { name: '已领取' }) as HTMLButtonElement)
.disabled,
@@ -1073,17 +1106,42 @@ test('desktop account entry uses saved avatar image when available', () => {
expect(within(accountEntry).queryByText('测')).toBeNull();
});
test('profile avatar upload uses the shared square crop tool', async () => {
stubFileReader('data:image/png;base64,avatar-source');
stubImage(800, 600);
renderProfileView();
fireEvent.click(screen.getByRole('button', { name: '上传头像' }));
fireEvent.change(screen.getByLabelText('上传头像', { selector: 'input' }), {
target: {
files: [new File(['x'], 'avatar.png', { type: 'image/png' })],
},
});
await waitFor(() => {
expect(screen.getByRole('dialog', { name: '裁剪头像' })).toBeTruthy();
});
expect(screen.getByLabelText('头像裁剪操作区')).toBeTruthy();
expect(
screen.getByRole('button', { name: '拖拽右下角裁剪边界' }),
).toBeTruthy();
expect(screen.queryByText('缩放')).toBeNull();
expect(screen.queryByText('横向')).toBeNull();
expect(screen.queryByText('纵向')).toBeNull();
});
test('wallet ledger modal shows empty and error states', async () => {
const user = userEvent.setup();
mockGetRpgProfileWalletLedger.mockResolvedValueOnce({ entries: [] });
renderProfileView();
await user.click(screen.getByRole('button', { name: /\s*0/u }));
await user.click(screen.getByRole('button', { name: /\s*0/u }));
expect(await screen.findByText('暂无账单记录')).toBeTruthy();
await user.click(screen.getByLabelText('关闭点账单'));
await user.click(screen.getByLabelText('关闭点账单'));
mockGetRpgProfileWalletLedger.mockRejectedValueOnce(new Error('加载失败'));
await user.click(screen.getByRole('button', { name: /\s*0/u }));
await user.click(screen.getByRole('button', { name: /\s*0/u }));
expect(await screen.findByText('加载失败')).toBeTruthy();
expect(screen.getByText('重新加载')).toBeTruthy();
@@ -1104,7 +1162,7 @@ test('profile invite shortcut shows reward subtitle and invited users', async ()
expect(mockGetRpgProfileReferralInviteCenter).toHaveBeenCalledTimes(1);
expect(
await screen.findByText('邀请一个用户注册双方都可以获得30点。'),
await screen.findByText('邀请一个用户注册双方都可以获得30点。'),
).toBeTruthy();
expect(screen.getByText('每日最多获得十次邀请奖励。')).toBeTruthy();
expect(screen.getByText('成功邀请')).toBeTruthy();