This commit is contained in:
2026-05-08 20:48:29 +08:00
parent abf1f1ebea
commit 94975e4735
82 changed files with 7786 additions and 1012 deletions

View File

@@ -508,6 +508,11 @@ function renderLoggedOutHomeView(
| 'latestEntries'
| 'onOpenGalleryDetail'
| 'onSearchPublicCode'
| 'recommendRuntimeContent'
| 'activeRecommendEntryKey'
| 'isStartingRecommendEntry'
| 'recommendRuntimeError'
| 'onSelectRecommendEntry'
>
> = {},
) {
@@ -553,6 +558,15 @@ function renderLoggedOutHomeView(
onOpenCreateWorld={vi.fn()}
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()}
recommendRuntimeContent={
overrides.recommendRuntimeContent ?? (
<div data-testid="recommend-runtime"></div>
)
}
activeRecommendEntryKey={overrides.activeRecommendEntryKey}
isStartingRecommendEntry={overrides.isStartingRecommendEntry}
recommendRuntimeError={overrides.recommendRuntimeError}
onSelectRecommendEntry={overrides.onSelectRecommendEntry}
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={overrides.onSearchPublicCode ?? vi.fn()}
/>
@@ -562,7 +576,13 @@ function renderLoggedOutHomeView(
function renderStatefulLoggedOutHomeView(
overrides: Partial<
Pick<RpgEntryHomeViewProps, 'featuredEntries' | 'latestEntries'>
Pick<
RpgEntryHomeViewProps,
| 'featuredEntries'
| 'latestEntries'
| 'onOpenGalleryDetail'
| 'onSearchPublicCode'
>
> = {},
) {
function StatefulLoggedOutHomeView() {
@@ -610,9 +630,10 @@ function renderStatefulLoggedOutHomeView(
onResumeSave={vi.fn()}
onOpenCreateWorld={vi.fn()}
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={vi.fn()}
onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()}
recommendRuntimeContent={<div data-testid="recommend-runtime" />}
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={vi.fn()}
onSearchPublicCode={overrides.onSearchPublicCode ?? vi.fn()}
/>
</AuthUiContext.Provider>
);
@@ -956,57 +977,12 @@ test('logged out bottom nav keeps creation centered with recommend icon', () =>
expect(buttons[2]?.querySelector('.lucide-compass')).toBeTruthy();
});
test('mobile home search submits public work code', async () => {
test('mobile discover search submits public work code', async () => {
const user = userEvent.setup();
const onSearchPublicCode = vi.fn();
render(
<AuthUiContext.Provider
value={{
user: null,
canAccessProtectedData: false,
openLoginModal: vi.fn(),
requireAuth: vi.fn(),
openSettingsModal: vi.fn(),
openAccountModal: vi.fn(),
setCurrentUser: vi.fn(),
logout: vi.fn(async () => undefined),
musicVolume: 0.42,
setMusicVolume: vi.fn(),
platformTheme: 'light',
setPlatformTheme: vi.fn(),
isHydratingSettings: false,
isPersistingSettings: false,
settingsError: null,
}}
>
<RpgEntryHomeView
activeTab="home"
onTabChange={vi.fn()}
hasSavedGame={false}
savedSnapshot={null}
saveEntries={[]}
saveError={null}
featuredEntries={[]}
latestEntries={[]}
myEntries={[]}
historyEntries={[]}
profileDashboard={null}
isLoadingPlatform={false}
isLoadingDashboard={false}
isResumingSaveWorldKey={null}
platformError={null}
dashboardError={null}
onContinueGame={vi.fn()}
onResumeSave={vi.fn()}
onOpenCreateWorld={vi.fn()}
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={vi.fn()}
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={onSearchPublicCode}
/>
</AuthUiContext.Provider>,
);
renderStatefulLoggedOutHomeView({ onSearchPublicCode });
await user.click(screen.getByRole('button', { name: '发现' }));
const searchInput = screen.getByPlaceholderText(
'搜索作品号、名称、作者、描述',
@@ -1016,7 +992,7 @@ test('mobile home search submits public work code', async () => {
expect(onSearchPublicCode).toHaveBeenCalledWith('PZ-PROFILE1');
});
test('home search fuzzy matches public work id, name, author and description', async () => {
test('discover search fuzzy matches public work id, name, author and description', async () => {
const user = userEvent.setup();
const onOpenGalleryDetail = vi.fn();
const onSearchPublicCode = vi.fn();
@@ -1041,46 +1017,52 @@ test('home search fuzzy matches public work id, name, author and description', a
},
] satisfies PlatformPublicGalleryCard[];
renderLoggedOutHomeView(vi.fn(), {
renderStatefulLoggedOutHomeView({
latestEntries: entries,
onOpenGalleryDetail,
onSearchPublicCode,
});
await user.click(screen.getByRole('button', { name: '发现' }));
const discoverPanel = document.getElementById('platform-tab-panel-category');
if (!discoverPanel) {
throw new Error('缺少发现面板');
}
const searchInput = screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
await user.type(searchInput, 'MOON01{enter}');
expect(await screen.findByText('搜索结果')).toBeTruthy();
expect(screen.getByText('月井机关')).toBeTruthy();
expect(screen.queryByText('火桥谜图')).toBeNull();
expect(await within(discoverPanel).findByText('搜索结果')).toBeTruthy();
expect(within(discoverPanel).getByText('月井机关')).toBeTruthy();
expect(within(discoverPanel).queryByText('火桥谜图')).toBeNull();
expect(onSearchPublicCode).not.toHaveBeenCalled();
await user.clear(searchInput);
await user.type(searchInput, '火桥{enter}');
expect(await screen.findByText('火桥谜图')).toBeTruthy();
expect(screen.queryByText('月井机关')).toBeNull();
expect(await within(discoverPanel).findByText('火桥谜图')).toBeTruthy();
expect(within(discoverPanel).queryByText('月井机关')).toBeNull();
await user.clear(searchInput);
await user.type(searchInput, '月井守望{enter}');
expect(await screen.findByText('月井机关')).toBeTruthy();
expect(screen.queryByText('火桥谜图')).toBeNull();
expect(await within(discoverPanel).findByText('月井机关')).toBeTruthy();
expect(within(discoverPanel).queryByText('火桥谜图')).toBeNull();
await user.clear(searchInput);
await user.type(searchInput, '熔岩断桥{enter}');
expect(await screen.findByText('火桥谜图')).toBeTruthy();
expect(screen.queryByText('月井机关')).toBeNull();
expect(await within(discoverPanel).findByText('火桥谜图')).toBeTruthy();
expect(within(discoverPanel).queryByText('月井机关')).toBeNull();
await user.click(screen.getByRole('button', { name: //u }));
expect(onOpenGalleryDetail).toHaveBeenCalledWith(entries[1]);
});
test('home search keeps public code fallback when local works do not match', async () => {
test('discover search keeps public code fallback when local works do not match', async () => {
const user = userEvent.setup();
const onSearchPublicCode = vi.fn();
renderLoggedOutHomeView(vi.fn(), {
renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
onSearchPublicCode,
});
await user.click(screen.getByRole('button', { name: '发现' }));
const searchInput = screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
await user.type(searchInput, 'CW-REMOTE-ONLY{enter}');
@@ -1093,10 +1075,11 @@ test('public gallery cards hide work code until detail is opened', async () => {
const user = userEvent.setup();
const onOpenGalleryDetail = vi.fn();
renderLoggedOutHomeView(vi.fn(), {
renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
onOpenGalleryDetail,
});
await user.click(screen.getByRole('button', { name: '发现' }));
expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull();
expect(
@@ -1108,47 +1091,54 @@ test('public gallery cards hide work code until detail is opened', async () => {
expect(onOpenGalleryDetail).toHaveBeenCalledWith(puzzlePublicEntry);
});
test('mobile public work cards render cover, author, kind and cover stats', () => {
const { container } = renderLoggedOutHomeView(vi.fn(), {
test('mobile recommend page renders runtime viewport and bottom switcher', () => {
const onSelectRecommendEntry = vi.fn();
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
onSelectRecommendEntry,
});
const card = screen.getByRole('button', {
name: /20512/u,
});
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(screen.queryByText('一张用于公开分享的拼图作品。')).toBeNull();
expect(
card.querySelector('.platform-public-work-card__cover.aspect-video'),
).toBeTruthy();
expect(
card.querySelector('.platform-public-work-card__cover-stats'),
).toBeTruthy();
expect(
card.querySelectorAll('.platform-public-work-card__cover-stat'),
).toHaveLength(3);
expect(
card.querySelector('.platform-public-work-card__kind')?.textContent,
).toBe('拼图');
expect(
card.querySelector('.platform-public-work-card__author-avatar')
?.textContent,
).toBe('拼');
expect(screen.getByText('奇幻拼图')).toBeTruthy();
document.querySelector('.platform-public-work-card__cover'),
).toBeNull();
expect(screen.getByText('拼图玩家')).toBeTruthy();
expect(screen.getByText('一张用于公开分享的拼图作品。')).toBeTruthy();
expect(screen.getByText('奇幻')).toBeTruthy();
expect(screen.getByText('20')).toBeTruthy();
expect(screen.getByText('5')).toBeTruthy();
expect(screen.getByText('12')).toBeTruthy();
expect(card.querySelector('.platform-pill--warm')?.textContent).not.toBe(
'推荐',
);
expect(
container.querySelector('.platform-mobile-home-channel--active')
?.textContent,
).toBe('推荐');
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
expect(screen.getAllByText('20').length).toBeGreaterThan(0);
expect(screen.getAllByText('12').length).toBeGreaterThan(0);
const switchButton = screen.getByRole('button', {
name: '切换到 奇幻拼图',
});
expect(switchButton.getAttribute('aria-pressed')).toBe('true');
});
test('public work cards load real author avatar from public user summary', async () => {
test('mobile recommend switcher selects a different public work', async () => {
const user = userEvent.setup();
const onSelectRecommendEntry = vi.fn();
const secondEntry = {
...puzzlePublicEntry,
workId: 'puzzle-work-second',
profileId: 'puzzle-profile-second',
publicWorkCode: 'PZ-SECOND',
worldName: '第二拼图',
} satisfies PlatformPublicGalleryCard;
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry, secondEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
onSelectRecommendEntry,
});
await user.click(screen.getByRole('button', { name: '切换到 第二拼图' }));
expect(onSelectRecommendEntry).toHaveBeenCalledWith(secondEntry);
});
test('mobile recommend meta loads real author avatar from public user summary', async () => {
mockGetPublicAuthUserById.mockResolvedValueOnce({
id: 'user-2',
publicUserCode: 'SY-00000002',
@@ -1159,16 +1149,13 @@ test('public work cards load real author avatar from public user summary', async
renderLoggedOutHomeView(vi.fn(), {
featuredEntries: [puzzlePublicEntry],
latestEntries: [puzzlePublicEntry],
});
const card = screen.getByRole('button', {
name: /20512/u,
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
});
await waitFor(() => {
expect(
card
.querySelector('.platform-public-work-card__author-avatar-image')
document
.querySelector('.platform-recommend-work-meta__avatar img')
?.getAttribute('src'),
).toBe('data:image/png;base64,AUTHOR');
});
@@ -1177,7 +1164,7 @@ test('public work cards load real author avatar from public user summary', async
expect(mockGetPublicAuthUserByCode).not.toHaveBeenCalled();
});
test('mobile home feed only rotates the card closest to screen center', () => {
test('mobile discover recommend feed only rotates the card closest to screen center', async () => {
vi.useFakeTimers();
Object.defineProperty(window, 'requestAnimationFrame', {
configurable: true,
@@ -1199,9 +1186,12 @@ test('mobile home feed only rotates the card closest to screen center', () => {
);
const cardRects = new Map<string, DOMRect>();
renderLoggedOutHomeView(vi.fn(), {
renderStatefulLoggedOutHomeView({
latestEntries: [firstEntry, secondEntry],
});
act(() => {
screen.getByRole('button', { name: '发现' }).click();
});
const tabPanel = document.querySelector('.platform-tab-panel--active');
const firstCard = screen.getByRole('button', { name: //u });
@@ -1340,15 +1330,22 @@ test('mobile today channel only shows newly published works from today', async (
updatedAt: todayPublishedAt,
} satisfies PlatformPublicGalleryCard;
renderLoggedOutHomeView(vi.fn(), {
renderStatefulLoggedOutHomeView({
latestEntries: [yesterdayEntry, updatedTodayEntry, todayEntry],
});
await user.click(screen.getByRole('button', { name: '今日游戏' }));
await user.click(screen.getByRole('button', { name: '发现' }));
await user.click(screen.getByRole('button', { name: '今日' }));
const discoverPanel = document.getElementById('platform-tab-panel-category');
if (!discoverPanel) {
throw new Error('缺少发现面板');
}
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
expect(screen.queryByText('昨日旧作')).toBeNull();
expect(screen.queryByText('今日更新旧作')).toBeNull();
expect(
within(discoverPanel).getByRole('button', { name: //u }),
).toBeTruthy();
expect(within(discoverPanel).queryByText('昨日旧作')).toBeNull();
expect(within(discoverPanel).queryByText('今日更新旧作')).toBeNull();
});
test('desktop home syncs mobile home modules without square or latest labels', () => {
@@ -1369,7 +1366,7 @@ test('desktop home syncs mobile home modules without square or latest labels', (
});
expect(screen.getByText('今日游戏')).toBeTruthy();
expect(screen.getByText('推荐')).toBeTruthy();
expect(screen.getAllByText('推荐').length).toBeGreaterThan(0);
expect(screen.getByText('作品分类')).toBeTruthy();
expect(screen.getAllByText('桌面今日新游').length).toBeGreaterThan(0);
expect(screen.queryByText('趋势关注')).toBeNull();
@@ -1383,16 +1380,17 @@ test('desktop home syncs mobile home modules without square or latest labels', (
test('mobile home moves category shelf into game category channel', async () => {
const user = userEvent.setup();
const { container } = renderLoggedOutHomeView(vi.fn(), {
const { container } = renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
});
expect(screen.queryByRole('button', { name: 'PC游戏' })).toBeNull();
expect(screen.queryByRole('button', { name: '即点即玩' })).toBeNull();
await user.click(screen.getByRole('button', { name: '游戏分类' }));
await user.click(screen.getByRole('button', { name: '发现' }));
await user.click(screen.getByRole('button', { name: '分类' }));
expect(screen.getAllByText('游戏分类').length).toBeGreaterThan(0);
expect(screen.getAllByText('分类').length).toBeGreaterThan(0);
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
expect(screen.getByRole('button', { name: '奇幻' })).toBeTruthy();
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
@@ -1407,11 +1405,12 @@ test('mobile home moves category shelf into game category channel', async () =>
test('mobile game category list orders works by composite public metric', async () => {
const user = userEvent.setup();
renderLoggedOutHomeView(vi.fn(), {
renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry, hotRankEntry],
});
await user.click(screen.getByRole('button', { name: '游戏分类' }));
await user.click(screen.getByRole('button', { name: '发现' }));
await user.click(screen.getByRole('button', { name: '分类' }));
await user.click(screen.getByRole('button', { name: '奇幻' }));
const gameItems = Array.from(
@@ -1427,8 +1426,7 @@ test('bottom category tab becomes ranking and switches ranking metrics', async (
latestEntries: [remixRankEntry, hotRankEntry, newRankEntry],
});
expect(screen.queryByRole('button', { name: '分类' })).toBeNull();
await user.click(screen.getByRole('button', { name: '发现' }));
await user.click(screen.getByRole('button', { name: '排行' }));
expect(await screen.findByRole('tab', { name: '热门榜' })).toBeTruthy();
@@ -1462,6 +1460,7 @@ test('ranking rows limit displayed work name and show two short tags on the thir
latestEntries: [longTextRankEntry],
});
await user.click(screen.getByRole('button', { name: '发现' }));
await user.click(screen.getByRole('button', { name: '排行' }));
const rankingPanel = document.getElementById('platform-tab-panel-category');