feat: surface platform errors in copyable dialogs

This commit is contained in:
kdletters
2026-05-26 14:27:18 +08:00
parent 44c65df5c9
commit fbda614156
16 changed files with 715 additions and 191 deletions

View File

@@ -1826,11 +1826,18 @@ test('non-wechat profile opens reward code from recharge-shaped entry', async ()
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
});
test('profile daily task shortcut opens task center and claims reward', async () => {
test('profile daily task shortcut reflects task progress and claim updates', async () => {
const user = userEvent.setup();
const onRechargeSuccess = vi.fn();
renderProfileView(onRechargeSuccess);
const dailyTask = screen.getByRole('button', { name: //u });
await waitFor(() => {
expect(within(dailyTask).getByText('1 / 1')).toBeTruthy();
});
expect(within(dailyTask).getByText('领取')).toBeTruthy();
await user.click(screen.getByRole('button', { name: //u }));
expect(await screen.findByText('每日登录')).toBeTruthy();
@@ -1847,6 +1854,7 @@ test('profile daily task shortcut opens task center and claims reward', async ()
expect(await screen.findByText('已领取 10 泥点')).toBeTruthy();
expect(screen.queryByRole('button', { name: '已领取' })).toBeNull();
expect(screen.getByText('暂无任务')).toBeTruthy();
expect(within(dailyTask).getByText('已完成')).toBeTruthy();
});
test('profile task center keeps only the highest priority actionable task', async () => {
@@ -1909,7 +1917,7 @@ test('profile task center keeps only the highest priority actionable task', asyn
expect(screen.queryByText('低优先级已完成')).toBeNull();
});
test('profile total play time card always uses hours', () => {
test('profile total play time card always uses hours', async () => {
renderProfileView(vi.fn(), {
totalPlayTimeMs: 90 * 60 * 1000,
});
@@ -1920,9 +1928,10 @@ test('profile total play time card always uses hours', () => {
expect(within(playTimeCard).getByText('1.5小时')).toBeTruthy();
expect(within(playTimeCard).queryByText('90分')).toBeNull();
await screen.findByText('1 / 1');
});
test('profile played works card shows count unit', () => {
test('profile played works card shows count unit', async () => {
renderProfileView(vi.fn(), {
playedWorldCount: 1,
});
@@ -1932,9 +1941,10 @@ test('profile played works card shows count unit', () => {
});
expect(within(playedCard).getByText('1个')).toBeTruthy();
await screen.findByText('1 / 1');
});
test('profile stats cards are centered without update timestamp', () => {
test('profile stats cards are centered without update timestamp', async () => {
renderProfileView(vi.fn(), {
updatedAt: '2026-05-03T08:01:00Z',
});
@@ -1950,6 +1960,7 @@ test('profile stats cards are centered without update timestamp', () => {
expect(card.className).toContain('text-center');
}
expect(screen.queryByText(//u)).toBeNull();
await screen.findByText('1 / 1');
});
test('mobile profile page matches the reference layout sections', async () => {
@@ -2007,7 +2018,7 @@ test('mobile profile page matches the reference layout sections', async () => {
expect(dailyTask.querySelector('.platform-profile-daily-task-card__desc')).toBeTruthy();
expect(dailyTask.querySelector('.platform-profile-daily-task-card__progress')).toBeTruthy();
expect(dailyTask.textContent).toContain('完成任务可领取 10 泥点');
expect(within(dailyTask).getByText('0 / 1')).toBeTruthy();
expect(await within(dailyTask).findByText('1 / 1')).toBeTruthy();
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
expect(
@@ -2101,7 +2112,7 @@ test('profile scan action opens camera scanner instead of recharge panel', async
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
});
test('desktop account entry uses saved avatar image when available', () => {
test('desktop account entry uses saved avatar image when available', async () => {
mockDesktopLayout();
const avatarUrl = 'data:image/png;base64,AAAA';
@@ -2111,6 +2122,7 @@ test('desktop account entry uses saved avatar image when available', () => {
const avatarImage = accountEntry.querySelector('img');
expect(avatarImage?.getAttribute('src')).toBe(avatarUrl);
expect(within(accountEntry).queryByText('测')).toBeNull();
await screen.findByText('1 / 1');
});
test('profile avatar upload uses the shared square crop tool', async () => {
@@ -2184,7 +2196,7 @@ test('profile invite shortcut shows reward subtitle and invited users', async ()
expect(screen.queryByText('今日')).toBeNull();
});
test('profile redeem invite shortcut sits between invite and community for fresh accounts', async () => {
test('profile page hides legacy redeem invite secondary shortcut for fresh accounts', async () => {
renderProfileView(
vi.fn(),
{},
@@ -2192,20 +2204,16 @@ test('profile redeem invite shortcut sits between invite and community for fresh
);
const inviteButton = screen.getByRole('button', { name: //u });
const redeemButton = await screen.findByRole('button', {
name: //u,
});
const communityButton = screen.getByRole('button', { name: //u });
const secondaryShortcuts = screen.getByRole('region', {
name: '次级入口',
await waitFor(() => {
expect(mockGetRpgProfileReferralInviteCenter).toHaveBeenCalledTimes(1);
});
expect(inviteButton).toBeTruthy();
expect(communityButton).toBeTruthy();
expect(
within(secondaryShortcuts).getByRole('button', { name: //u }),
).toBeTruthy();
expect(within(redeemButton).getByText('新用户奖励')).toBeTruthy();
expect(screen.queryByRole('region', { name: '次级入口' })).toBeNull();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
});
test('profile redeem invite shortcut hides after redeemed or one day old', async () => {
@@ -2226,6 +2234,7 @@ test('profile redeem invite shortcut hides after redeemed or one day old', async
expect(
within(firstShortcutRegion).queryByRole('button', { name: //u }),
).toBeNull();
await screen.findByText('1 / 1');
unmount();
renderProfileView(vi.fn(), {}, { createdAt: '2026-04-01T00:00:00.000Z' });
@@ -2237,6 +2246,7 @@ test('profile redeem invite shortcut hides after redeemed or one day old', async
name: //u,
}),
).toBeNull();
await screen.findByText('1 / 1');
});
test('invite query opens login modal for logged out users', async () => {
@@ -2269,9 +2279,10 @@ test('profile redeem invite modal reads query invite code after login', async ()
expect((input as HTMLInputElement).value).toBe('SPRING2026');
});
test('profile redeem invite modal submits code and hides shortcut after success', async () => {
test('profile redeem invite query modal submits code after login', async () => {
const user = userEvent.setup();
const onRechargeSuccess = vi.fn();
window.history.replaceState(null, '', '/?inviteCode=spring-2026');
renderProfileView(
onRechargeSuccess,
@@ -2279,9 +2290,7 @@ test('profile redeem invite modal submits code and hides shortcut after success'
{ createdAt: buildFreshProfileCreatedAt() },
);
await user.click(await screen.findByRole('button', { name: //u }));
const input = await screen.findByLabelText('邀请码');
await user.type(input, 'spring-2026');
expect(await screen.findByLabelText('邀请码')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '提交' }));
await waitFor(() => {
@@ -2291,12 +2300,7 @@ test('profile redeem invite modal submits code and hides shortcut after success'
});
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
expect(await screen.findByText('已填写')).toBeTruthy();
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
expect(
within(shortcutRegion).queryByRole('button', {
name: //u,
}),
).toBeNull();
expect(screen.queryByRole('region', { name: '次级入口' })).toBeNull();
});
test('opens reward code modal from profile action on mobile', async () => {