This commit is contained in:
2026-05-25 22:57:14 +08:00
parent 30cf8abbf7
commit 5a6e68b6dc
8 changed files with 627 additions and 202 deletions

View File

@@ -1035,6 +1035,15 @@ afterEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
Object.defineProperty(HTMLMediaElement.prototype, 'play', {
configurable: true,
value: vi.fn(async () => undefined),
});
Object.defineProperty(navigator, 'mediaDevices', {
configurable: true,
value: undefined,
});
Reflect.deleteProperty(globalThis as Record<string, unknown>, 'BarcodeDetector');
window.wx = undefined;
document
.querySelectorAll(
@@ -1832,10 +1841,68 @@ test('profile daily task shortcut opens task center and claims reward', async ()
});
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
expect(await screen.findByText('已领取 10 泥点')).toBeTruthy();
expect(
(screen.getByRole('button', { name: '已领取' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(screen.queryByRole('button', { name: '已领取' })).toBeNull();
expect(screen.getByText('暂无任务')).toBeTruthy();
});
test('profile task center keeps only the highest priority actionable task', async () => {
const user = userEvent.setup();
mockGetRpgProfileTasks.mockResolvedValueOnce(
mockBuildTaskCenter({
tasks: [
{
taskId: 'claimed_low',
title: '低优先级已完成',
description: '',
eventKey: 'profile.task.claimed_low',
cycle: 'daily',
threshold: 1,
progressCount: 1,
rewardPoints: 5,
status: 'claimed',
dayKey: 20260503,
claimedAt: '2026-05-03T08:01:00Z',
updatedAt: '2026-05-03T08:01:00Z',
},
{
taskId: 'claimable_mid',
title: '中优先级可领取',
description: '',
eventKey: 'profile.task.claimable_mid',
cycle: 'daily',
threshold: 2,
progressCount: 2,
rewardPoints: 10,
status: 'claimable',
dayKey: 20260503,
claimedAt: null,
updatedAt: '2026-05-03T08:01:00Z',
},
{
taskId: 'incomplete_high',
title: '高优先级未完成',
description: '',
eventKey: 'profile.task.incomplete_high',
cycle: 'daily',
threshold: 3,
progressCount: 1,
rewardPoints: 20,
status: 'incomplete',
dayKey: 20260503,
claimedAt: null,
updatedAt: '2026-05-03T08:01:00Z',
},
],
}),
);
renderProfileView();
await user.click(screen.getByRole('button', { name: //u }));
expect(await screen.findByText('中优先级可领取')).toBeTruthy();
expect(screen.queryByText('高优先级未完成')).toBeNull();
expect(screen.queryByText('低优先级已完成')).toBeNull();
});
test('profile total play time card always uses hours', () => {
@@ -1882,21 +1949,35 @@ test('profile stats cards are centered without update timestamp', () => {
});
test('mobile profile page matches the reference layout sections', async () => {
mockWechatMobileLayout();
mockNarrowMobileLayout();
const { container } = renderProfileView(vi.fn(), {
walletBalance: 70,
totalPlayTimeMs: 0,
playedWorldCount: 0,
}, { createdAt: buildFreshProfileCreatedAt() });
});
const profilePage = container.querySelector('.platform-profile-page');
expect(profilePage).toBeTruthy();
expect(profilePage?.classList.contains('platform-page-stage')).toBe(true);
expect(profilePage?.classList.contains('platform-page-stage')).toBe(false);
expect(profilePage?.querySelector('.platform-profile-scene-decor')).toBeTruthy();
expect(profilePage?.classList.contains('platform-profile-page')).toBe(true);
expect(profilePage?.getAttribute('style') ?? '').not.toContain('overflow: hidden');
const topbar = container.querySelector('.platform-mobile-topbar');
expect(topbar).toBeTruthy();
expect(
within(topbar as HTMLElement).getByRole('button', { name: '扫码' }),
).toBeTruthy();
expect(
within(topbar as HTMLElement).getByRole('button', { name: '打开设置' }),
).toBeTruthy();
expect(
within(topbar as HTMLElement).queryByRole('button', {
name: //u,
}),
).toBeNull();
const membershipCard = screen.getByRole('button', { name: '查看权益' });
expect(membershipCard.className).toContain('platform-profile-membership-card');
expect(
@@ -1914,6 +1995,7 @@ test('mobile profile page matches the reference layout sections', async () => {
expect(
within(statPanel).getByRole('button', { name: /\s*70/u }).className,
).toContain('platform-profile-stat-card');
expect(statPanel.querySelectorAll('.platform-profile-stat-card__icon')).toHaveLength(3);
const dailyTask = screen.getByRole('button', { name: //u });
expect(dailyTask.className).toContain('platform-profile-daily-task-card');
@@ -1953,18 +2035,11 @@ test('mobile profile page matches the reference layout sections', async () => {
within(settingsRegion).getByRole('button', { name: new RegExp(label, 'u') }),
).toBeTruthy();
}
expect(
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
const secondaryShortcuts = screen.getByRole('region', {
name: '次级入口',
});
expect(
within(secondaryShortcuts).getByRole('button', { name: //u }),
).toBeTruthy();
expect(
await within(secondaryShortcuts).findByRole('button', {
name: //u,
}),
).toBeTruthy();
expect(screen.queryByRole('region', { name: '次级入口' })).toBeNull();
const profileHeader = profilePage?.querySelector('.platform-profile-header');
expect(profileHeader).toBeTruthy();
@@ -1982,6 +2057,46 @@ test('mobile profile page matches the reference layout sections', async () => {
expect(legalRegion.querySelector('.platform-profile-legal-strip__link')).toBeTruthy();
});
test('profile scan action opens camera scanner instead of recharge panel', async () => {
const user = userEvent.setup();
const stopTrack = vi.fn();
const stream = {
getTracks: () => [{ stop: stopTrack }],
} as unknown as MediaStream;
const getUserMedia = vi.fn(async () => stream);
mockNarrowMobileLayout();
Object.defineProperty(globalThis, 'BarcodeDetector', {
configurable: true,
value: class {
async detect() {
return [];
}
},
});
Object.defineProperty(navigator, 'mediaDevices', {
configurable: true,
value: { getUserMedia },
});
renderProfileView();
const topbar = document.querySelector('.platform-mobile-topbar');
expect(topbar).toBeTruthy();
await user.click(
within(topbar as HTMLElement).getByRole('button', { name: '扫码' }),
);
expect(await screen.findByRole('dialog', { name: '扫码' })).toBeTruthy();
await waitFor(() => {
expect(getUserMedia).toHaveBeenCalledWith({
audio: false,
video: { facingMode: { ideal: 'environment' } },
});
});
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
});
test('desktop account entry uses saved avatar image when available', () => {
mockDesktopLayout();
const avatarUrl = 'data:image/png;base64,AAAA';
@@ -2195,7 +2310,7 @@ test('opens reward code modal from profile action on mobile', async () => {
expect(screen.getByLabelText('关闭兑换码')).toBeTruthy();
});
test('profile page shows legal entries and ICP record link', async () => {
test('profile page shows legal entries and hides archive shortcuts', async () => {
const user = userEvent.setup();
renderProfileView();
@@ -2221,18 +2336,9 @@ test('profile page shows legal entries and ICP record link', async () => {
const settingsRegion = screen.getByRole('region', { name: '设置入口' });
expect(
within(settingsRegion).getByRole('button', { name: //u }),
).toBeTruthy();
const secondaryShortcuts = screen.getByRole('region', {
name: '次级入口',
});
expect(
within(secondaryShortcuts).getByRole('button', { name: //u }),
).toBeTruthy();
expect(
within(secondaryShortcuts).queryByRole('button', { name: //u }),
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
expect(screen.queryByRole('region', { name: '次级入口' })).toBeNull();
const legalRegion = screen.getByRole('region', { name: '法律信息' });
expect(
@@ -2697,7 +2803,7 @@ test('logged out mobile shell defaults to discover tab', () => {
).toBeNull();
});
test('logged out recommend tab opens login modal and shows cover only', async () => {
test('logged out recommend tab opens recommend runtime directly', async () => {
const user = userEvent.setup();
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
@@ -2715,17 +2821,17 @@ test('logged out recommend tab opens login modal and shows cover only', async ()
expect(openLoginModal).toHaveBeenCalledTimes(1);
expect(
container.querySelector('.platform-recommend-cover-only'),
).toBeTruthy();
).toBeNull();
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
expect(
container.querySelector('.platform-mobile-entry-shell--recommend'),
).toBeTruthy();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
expect(screen.queryByLabelText('奇幻拼图 作品信息')).toBeNull();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(screen.getByLabelText('奇幻拼图 作品信息')).toBeTruthy();
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
});
test('logged out recommend cover opens login modal again', async () => {
test('logged out recommend meta keeps gallery detail gated', async () => {
const user = userEvent.setup();
const onOpenGalleryDetail = vi.fn();
const { openLoginModal } = renderStatefulLoggedOutHomeView({
@@ -2741,12 +2847,9 @@ test('logged out recommend cover opens login modal again', async () => {
await user.click(
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
);
await user.click(
screen.getByRole('button', { name: / /u }),
);
await user.click(screen.getByLabelText('奇幻拼图 作品信息'));
expect(openLoginModal).toHaveBeenCalledTimes(2);
expect(openLoginModal).toHaveBeenLastCalledWith();
expect(openLoginModal).toHaveBeenCalledTimes(1);
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
@@ -3082,7 +3185,7 @@ test('mobile recommend meta loads real author avatar from public user summary',
await waitFor(() => {
expect(
document
.querySelector('.platform-recommend-cover-only__author img')
.querySelector('.platform-recommend-work-meta__avatar img')
?.getAttribute('src'),
).toBe('data:image/png;base64,AUTHOR');
});