This commit is contained in:
2026-05-11 20:27:41 +08:00
parent e30b733b17
commit 481a27fc53
60 changed files with 6357 additions and 1100 deletions

View File

@@ -609,7 +609,9 @@ function renderLoggedOutHomeView(
isStartingRecommendEntry={overrides.isStartingRecommendEntry}
recommendRuntimeError={overrides.recommendRuntimeError}
onSelectNextRecommendEntry={overrides.onSelectNextRecommendEntry}
onSelectPreviousRecommendEntry={overrides.onSelectPreviousRecommendEntry}
onSelectPreviousRecommendEntry={
overrides.onSelectPreviousRecommendEntry
}
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={overrides.onSearchPublicCode ?? vi.fn()}
/>
@@ -617,6 +619,76 @@ function renderLoggedOutHomeView(
);
}
function renderLoggedInHomeView(
overrides: Partial<
Pick<
RpgEntryHomeViewProps,
'activeTab' | 'hasUnreadDraftUpdate' | 'draftTabContent'
>
> = {},
) {
return render(
<AuthUiContext.Provider
value={{
user: {
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: new Date().toISOString(),
},
canAccessProtectedData: true,
openLoginModal: vi.fn(),
requireAuth: (action) => action(),
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={overrides.activeTab ?? 'saves'}
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={vi.fn()}
hasUnreadDraftUpdate={overrides.hasUnreadDraftUpdate ?? false}
draftTabContent={overrides.draftTabContent}
/>
</AuthUiContext.Provider>,
);
}
function renderStatefulLoggedOutHomeView(
overrides: Partial<
Pick<
@@ -691,7 +763,9 @@ function renderStatefulLoggedOutHomeView(
}
activeRecommendEntryKey={overrides.activeRecommendEntryKey}
onSelectNextRecommendEntry={overrides.onSelectNextRecommendEntry}
onSelectPreviousRecommendEntry={overrides.onSelectPreviousRecommendEntry}
onSelectPreviousRecommendEntry={
overrides.onSelectPreviousRecommendEntry
}
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={overrides.onSearchPublicCode ?? vi.fn()}
/>
@@ -937,7 +1011,9 @@ test('profile redeem invite shortcut hides after redeemed or one day old', async
unmount();
renderProfileView(vi.fn(), {}, { createdAt: '2026-04-01T00:00:00.000Z' });
const expiredShortcutRegion = screen.getByRole('region', { name: '常用功能' });
const expiredShortcutRegion = screen.getByRole('region', {
name: '常用功能',
});
expect(
within(expiredShortcutRegion).queryByRole('button', {
name: //u,
@@ -945,7 +1021,6 @@ test('profile redeem invite shortcut hides after redeemed or one day old', async
).toBeNull();
});
test('invite query opens login modal for logged out users', async () => {
const openLoginModal = vi.fn();
window.history.replaceState(null, '', '/?inviteCode=spring-2026');
@@ -1041,6 +1116,21 @@ test('logged out bottom nav turns active recommend tab into next action', () =>
expect(buttons[2]?.querySelector('.lucide-compass')).toBeTruthy();
});
test('logged in draft bottom tab shows unread marker', () => {
const { container } = renderLoggedInHomeView({
hasUnreadDraftUpdate: true,
draftTabContent: <div>稿</div>,
});
const nav = container.querySelector('.platform-bottom-nav');
expect(nav).toBeTruthy();
const draftButton = within(nav as HTMLElement).getByRole('button', {
name: '草稿,有新草稿',
});
expect(draftButton.querySelector('.platform-nav-unread-dot')).toBeTruthy();
});
test('mobile discover search submits public work code', async () => {
const user = userEvent.setup();
const onSearchPublicCode = vi.fn();
@@ -1048,9 +1138,8 @@ test('mobile discover search submits public work code', async () => {
renderStatefulLoggedOutHomeView({ onSearchPublicCode });
await user.click(screen.getByRole('button', { name: '发现' }));
const searchInput = screen.getByPlaceholderText(
'搜索作品号、名称、作者、描述',
);
const searchInput =
screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
await user.type(searchInput, 'PZ-PROFILE1{enter}');
expect(onSearchPublicCode).toHaveBeenCalledWith('PZ-PROFILE1');
@@ -1092,7 +1181,8 @@ test('discover search fuzzy matches public work id, name, author and description
throw new Error('缺少发现面板');
}
const searchInput = screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
const searchInput =
screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
await user.type(searchInput, 'MOON01{enter}');
expect(await within(discoverPanel).findByText('搜索结果')).toBeTruthy();
expect(within(discoverPanel).getByText('月井机关')).toBeTruthy();
@@ -1175,7 +1265,8 @@ test('mobile discover keeps edutainment works in the last dedicated channel only
).toBeTruthy();
expect(within(discoverPanel).queryByText('普通拼图作品')).toBeNull();
const searchInput = screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
const searchInput =
screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
await user.type(searchInput, '儿童动作热身{enter}');
expect(await within(discoverPanel).findByText('搜索结果')).toBeTruthy();
expect(within(discoverPanel).queryByText('儿童动作热身 Demo')).toBeNull();
@@ -1213,7 +1304,8 @@ test('mobile discover hides edutainment channel and work when switch is disabled
expect(channels).toEqual(['推荐', '今日', '分类', '排行']);
expect(within(discoverPanel).queryByText('关闭后隐藏的热身 Demo')).toBeNull();
const searchInput = screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
const searchInput =
screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
await user.type(searchInput, 'PZ-EDUOFF1{enter}');
expect(await within(discoverPanel).findByText('搜索结果')).toBeTruthy();
expect(within(discoverPanel).queryByText('关闭后隐藏的热身 Demo')).toBeNull();
@@ -1230,7 +1322,8 @@ test('discover search keeps public code fallback when local works do not match',
});
await user.click(screen.getByRole('button', { name: '发现' }));
const searchInput = screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
const searchInput =
screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
await user.type(searchInput, 'CW-REMOTE-ONLY{enter}');
expect(onSearchPublicCode).toHaveBeenCalledWith('CW-REMOTE-ONLY');
@@ -1264,7 +1357,9 @@ test('logged out mobile shell defaults to discover tab', () => {
const activePanel = container.querySelector('.platform-tab-panel--active');
expect(activePanel?.id).toBe('platform-tab-panel-category');
expect(screen.getByPlaceholderText('搜索作品号、名称、作者、描述')).toBeTruthy();
expect(
screen.getByPlaceholderText('搜索作品号、名称、作者、描述'),
).toBeTruthy();
});
test('logged out recommend tab opens login modal and shows cover only', async () => {
@@ -1283,7 +1378,9 @@ 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();
expect(
container.querySelector('.platform-recommend-cover-only'),
).toBeTruthy();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
expect(screen.queryByLabelText('奇幻拼图 作品信息')).toBeNull();
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
@@ -1305,7 +1402,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.getByRole('button', { name: / /u }),
);
expect(openLoginModal).toHaveBeenCalledTimes(2);
expect(openLoginModal).toHaveBeenLastCalledWith();
@@ -1648,7 +1747,11 @@ test('mobile discover recommend feed only rotates the card closest to screen cen
value: (handle: number) => window.clearTimeout(handle),
});
const firstEntry = buildCarouselPuzzleEntry('center1', '中心拼图一', 'center-one');
const firstEntry = buildCarouselPuzzleEntry(
'center1',
'中心拼图一',
'center-one',
);
const secondEntry = buildCarouselPuzzleEntry(
'center2',
'中心拼图二',