1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
@@ -118,10 +118,24 @@ vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../ResolvedAssetImage', () => ({
|
||||
ResolvedAssetImage: () => null,
|
||||
ResolvedAssetImage: ({
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
...rest
|
||||
}: {
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
}) =>
|
||||
src ? (
|
||||
<img src={src} alt={alt ?? ''} className={className} {...rest} />
|
||||
) : null,
|
||||
}));
|
||||
|
||||
const originalMatchMedia = window.matchMedia;
|
||||
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||
const originalCancelAnimationFrame = window.cancelAnimationFrame;
|
||||
const puzzlePublicEntry = {
|
||||
sourceType: 'puzzle',
|
||||
workId: 'puzzle-work-public-1',
|
||||
@@ -156,6 +170,33 @@ const remixRankEntry = {
|
||||
updatedAt: '2026-04-25T11:00:00.000Z',
|
||||
} satisfies PlatformPublicGalleryCard;
|
||||
|
||||
function buildCarouselPuzzleEntry(
|
||||
id: string,
|
||||
worldName: string,
|
||||
coverPrefix: string,
|
||||
) {
|
||||
return {
|
||||
...puzzlePublicEntry,
|
||||
workId: `puzzle-work-${id}`,
|
||||
profileId: `puzzle-profile-${id}`,
|
||||
publicWorkCode: `PZ-${id.toUpperCase()}`,
|
||||
worldName,
|
||||
coverImageSrc: `${coverPrefix}-fallback.png`,
|
||||
coverSlides: [
|
||||
{
|
||||
id: `${id}-cover-1`,
|
||||
imageSrc: `${coverPrefix}-1.png`,
|
||||
label: `${worldName} 1`,
|
||||
},
|
||||
{
|
||||
id: `${id}-cover-2`,
|
||||
imageSrc: `${coverPrefix}-2.png`,
|
||||
label: `${worldName} 2`,
|
||||
},
|
||||
],
|
||||
} satisfies PlatformPublicGalleryCard;
|
||||
}
|
||||
|
||||
const hotRankEntry = {
|
||||
...puzzlePublicEntry,
|
||||
workId: 'puzzle-work-hot-rank',
|
||||
@@ -414,12 +455,23 @@ function renderStatefulLoggedOutHomeView(
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: originalMatchMedia,
|
||||
});
|
||||
Object.defineProperty(window, 'requestAnimationFrame', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: originalRequestAnimationFrame,
|
||||
});
|
||||
Object.defineProperty(window, 'cancelAnimationFrame', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: originalCancelAnimationFrame,
|
||||
});
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: undefined,
|
||||
@@ -430,7 +482,7 @@ test('opens wallet ledger modal from narrative coin card', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderProfileView();
|
||||
await user.click(screen.getByText('剩余陶泥币'));
|
||||
await user.click(screen.getByRole('button', { name: /陶泥币\s*0/u }));
|
||||
|
||||
expect(await screen.findByText('陶泥币账单')).toBeTruthy();
|
||||
expect(mockGetRpgProfileWalletLedger).toHaveBeenCalledTimes(1);
|
||||
@@ -446,7 +498,7 @@ test('profile total play time card always uses hours', () => {
|
||||
});
|
||||
|
||||
const playTimeCard = screen.getByRole('button', {
|
||||
name: /总游戏时长/u,
|
||||
name: /游戏时长/u,
|
||||
});
|
||||
|
||||
expect(within(playTimeCard).getByText('1.5小时')).toBeTruthy();
|
||||
@@ -470,12 +522,12 @@ test('wallet ledger modal shows empty and error states', async () => {
|
||||
mockGetRpgProfileWalletLedger.mockResolvedValueOnce({ entries: [] });
|
||||
|
||||
renderProfileView();
|
||||
await user.click(screen.getByText('剩余陶泥币'));
|
||||
await user.click(screen.getByRole('button', { name: /陶泥币\s*0/u }));
|
||||
expect(await screen.findByText('暂无账单记录')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByLabelText('关闭陶泥币账单'));
|
||||
mockGetRpgProfileWalletLedger.mockRejectedValueOnce(new Error('加载失败'));
|
||||
await user.click(screen.getByText('剩余陶泥币'));
|
||||
await user.click(screen.getByRole('button', { name: /陶泥币\s*0/u }));
|
||||
|
||||
expect(await screen.findByText('加载失败')).toBeTruthy();
|
||||
expect(screen.getByText('重新加载')).toBeTruthy();
|
||||
@@ -622,6 +674,126 @@ test('mobile public work cards render cover, author, kind and cover stats', () =
|
||||
).toBe('推荐');
|
||||
});
|
||||
|
||||
test('mobile home feed only rotates the card closest to screen center', () => {
|
||||
vi.useFakeTimers();
|
||||
Object.defineProperty(window, 'requestAnimationFrame', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (callback: FrameRequestCallback) =>
|
||||
window.setTimeout(() => callback(0), 0),
|
||||
});
|
||||
Object.defineProperty(window, 'cancelAnimationFrame', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (handle: number) => window.clearTimeout(handle),
|
||||
});
|
||||
|
||||
const firstEntry = buildCarouselPuzzleEntry('center1', '中心拼图一', 'center-one');
|
||||
const secondEntry = buildCarouselPuzzleEntry(
|
||||
'center2',
|
||||
'中心拼图二',
|
||||
'center-two',
|
||||
);
|
||||
const cardRects = new Map<string, DOMRect>();
|
||||
|
||||
renderLoggedOutHomeView(vi.fn(), {
|
||||
latestEntries: [firstEntry, secondEntry],
|
||||
});
|
||||
|
||||
const tabPanel = document.querySelector('.platform-tab-panel--active');
|
||||
const firstCard = screen.getByRole('button', { name: /中心拼图一/u });
|
||||
const secondCard = screen.getByRole('button', { name: /中心拼图二/u });
|
||||
if (!tabPanel) {
|
||||
throw new Error('缺少移动端首页滚动面板');
|
||||
}
|
||||
|
||||
tabPanel.getBoundingClientRect = vi.fn(
|
||||
() =>
|
||||
({
|
||||
top: 0,
|
||||
bottom: 600,
|
||||
height: 600,
|
||||
left: 0,
|
||||
right: 360,
|
||||
width: 360,
|
||||
}) as DOMRect,
|
||||
);
|
||||
firstCard.getBoundingClientRect = vi.fn(() => cardRects.get('first')!);
|
||||
secondCard.getBoundingClientRect = vi.fn(() => cardRects.get('second')!);
|
||||
cardRects.set('first', {
|
||||
top: 170,
|
||||
bottom: 370,
|
||||
height: 200,
|
||||
left: 0,
|
||||
right: 320,
|
||||
width: 320,
|
||||
} as DOMRect);
|
||||
cardRects.set('second', {
|
||||
top: 420,
|
||||
bottom: 620,
|
||||
height: 200,
|
||||
left: 0,
|
||||
right: 320,
|
||||
width: 320,
|
||||
} as DOMRect);
|
||||
|
||||
act(() => {
|
||||
vi.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
expect(within(firstCard).getByRole('img').getAttribute('src')).toBe(
|
||||
'center-one-1.png',
|
||||
);
|
||||
expect(within(secondCard).getByRole('img').getAttribute('src')).toBe(
|
||||
'center-two-1.png',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(4200);
|
||||
});
|
||||
|
||||
expect(within(firstCard).getByRole('img').getAttribute('src')).toBe(
|
||||
'center-one-2.png',
|
||||
);
|
||||
expect(within(secondCard).getByRole('img').getAttribute('src')).toBe(
|
||||
'center-two-1.png',
|
||||
);
|
||||
|
||||
cardRects.set('first', {
|
||||
top: -120,
|
||||
bottom: 80,
|
||||
height: 200,
|
||||
left: 0,
|
||||
right: 320,
|
||||
width: 320,
|
||||
} as DOMRect);
|
||||
cardRects.set('second', {
|
||||
top: 200,
|
||||
bottom: 400,
|
||||
height: 200,
|
||||
left: 0,
|
||||
right: 320,
|
||||
width: 320,
|
||||
} as DOMRect);
|
||||
|
||||
act(() => {
|
||||
tabPanel.dispatchEvent(new Event('scroll'));
|
||||
vi.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
expect(within(firstCard).getByRole('img').getAttribute('src')).toBe(
|
||||
'center-one-1.png',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(4200);
|
||||
});
|
||||
|
||||
expect(within(secondCard).getByRole('img').getAttribute('src')).toBe(
|
||||
'center-two-2.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('mobile today channel only shows newly published works from today', async () => {
|
||||
const user = userEvent.setup();
|
||||
const now = new Date();
|
||||
@@ -676,13 +848,31 @@ test('mobile today channel only shows newly published works from today', async (
|
||||
expect(screen.queryByText('今日更新旧作')).toBeNull();
|
||||
});
|
||||
|
||||
test('desktop trending list shows kind instead of work code or timestamp text', () => {
|
||||
test('desktop home syncs mobile home modules without square or latest labels', () => {
|
||||
mockDesktopLayout();
|
||||
const todayPublishedAt = new Date().toISOString();
|
||||
const todayEntry = {
|
||||
...puzzlePublicEntry,
|
||||
workId: 'puzzle-work-desktop-today',
|
||||
profileId: 'puzzle-profile-desktop-today',
|
||||
publicWorkCode: 'PZ-DTODAY',
|
||||
worldName: '桌面今日新游',
|
||||
publishedAt: todayPublishedAt,
|
||||
updatedAt: todayPublishedAt,
|
||||
} satisfies PlatformPublicGalleryCard;
|
||||
|
||||
renderLoggedOutHomeView(vi.fn(), {
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
latestEntries: [puzzlePublicEntry, todayEntry],
|
||||
});
|
||||
|
||||
expect(screen.getByText('今日游戏')).toBeTruthy();
|
||||
expect(screen.getByText('推荐')).toBeTruthy();
|
||||
expect(screen.getByText('作品分类')).toBeTruthy();
|
||||
expect(screen.getAllByText('桌面今日新游').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('趋势关注')).toBeNull();
|
||||
expect(screen.queryByText('最新发布')).toBeNull();
|
||||
expect(screen.queryByText('作品广场')).toBeNull();
|
||||
expect(screen.queryByText('公开作品')).toBeNull();
|
||||
expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull();
|
||||
expect(screen.getAllByText('拼图').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('1777110165.990127Z')).toBeNull();
|
||||
|
||||
Reference in New Issue
Block a user