This commit is contained in:
2026-05-09 19:56:03 +08:00
parent 052dbc248b
commit 7c8aa1e124
12 changed files with 483 additions and 59 deletions

View File

@@ -2810,6 +2810,7 @@ test('owned public puzzle detail edits original draft instead of remixing', asyn
render(<TestWrapper withAuth />);
await openDiscoverHub(user);
await waitFor(() => {
expect(screen.getAllByText('星桥机关').length).toBeGreaterThan(0);
});
@@ -3066,6 +3067,44 @@ test('home recommendation starts embedded puzzle without global auth reset on lo
});
});
test('home recommendation surfaces start failure instead of staying in loading state', async () => {
const publishedPuzzleWork = {
workId: 'puzzle-work-public-1',
profileId: 'puzzle-profile-public-1',
ownerUserId: 'user-2',
sourceSessionId: 'puzzle-session-public-1',
authorDisplayName: '拼图作者',
levelName: '星桥机关',
summary: '旋转碎片并接通星桥机关。',
themeTags: ['机关', '星桥'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'published',
updatedAt: '2026-04-25T09:00:00.000Z',
publishedAt: '2026-04-25T09:00:00.000Z',
playCount: 3,
likeCount: 0,
publishReady: true,
} satisfies PuzzleWorkSummary;
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [publishedPuzzleWork],
});
vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
item: publishedPuzzleWork,
});
vi.mocked(startPuzzleRun).mockRejectedValueOnce(
new Error('启动拼图玩法失败'),
);
render(<TestWrapper withAuth />);
expect(
await screen.findByText('作品暂时无法进入,请稍后再试。'),
).toBeTruthy();
expect(screen.queryByText('加载中...')).toBeNull();
});
test('published big fish works stay hidden from platform home and game category channel', async () => {
const user = userEvent.setup();
const publishedBigFishWork: BigFishWorkSummary = {

View File

@@ -515,6 +515,7 @@ function renderLoggedOutHomeView(
| 'featuredEntries'
| 'latestEntries'
| 'onOpenGalleryDetail'
| 'onOpenRecommendGalleryDetail'
| 'onSearchPublicCode'
| 'recommendRuntimeContent'
| 'activeRecommendEntryKey'
@@ -568,6 +569,7 @@ function renderLoggedOutHomeView(
onOpenCreateWorld={vi.fn()}
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()}
onOpenRecommendGalleryDetail={overrides.onOpenRecommendGalleryDetail}
recommendRuntimeContent={
overrides.recommendRuntimeContent ?? (
<div data-testid="recommend-runtime"></div>
@@ -592,6 +594,7 @@ function renderStatefulLoggedOutHomeView(
| 'featuredEntries'
| 'latestEntries'
| 'onOpenGalleryDetail'
| 'onOpenRecommendGalleryDetail'
| 'onSearchPublicCode'
| 'recommendRuntimeContent'
| 'activeRecommendEntryKey'
@@ -650,6 +653,7 @@ function renderStatefulLoggedOutHomeView(
onOpenCreateWorld={vi.fn()}
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()}
onOpenRecommendGalleryDetail={overrides.onOpenRecommendGalleryDetail}
recommendRuntimeContent={
overrides.recommendRuntimeContent ?? (
<div data-testid="recommend-runtime" />
@@ -1171,7 +1175,93 @@ test('logged out recommend cover opens login modal again', async () => {
await user.click(screen.getByRole('button', { name: / /u }));
expect(openLoginModal).toHaveBeenCalledTimes(2);
expect(openLoginModal).toHaveBeenLastCalledWith(expect.any(Function));
expect(openLoginModal).toHaveBeenLastCalledWith();
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
test('logged out desktop recommend page renders cover only', () => {
mockDesktopLayout();
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
});
expect(document.querySelector('.platform-recommend-cover-only')).toBeTruthy();
expect(screen.queryByText('今日游戏')).toBeNull();
expect(screen.queryByText('作品分类')).toBeNull();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
});
test('logged in recommend page uses gated recommend detail callback', async () => {
const user = userEvent.setup();
const onOpenGalleryDetail = vi.fn();
const onOpenRecommendGalleryDetail = vi.fn();
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="home"
onTabChange={vi.fn()}
hasSavedGame={false}
savedSnapshot={null}
saveEntries={[]}
saveError={null}
featuredEntries={[]}
latestEntries={[puzzlePublicEntry]}
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={onOpenGalleryDetail}
onOpenRecommendGalleryDetail={onOpenRecommendGalleryDetail}
recommendRuntimeError="作品暂时无法进入,请稍后再试。"
activeRecommendEntryKey="puzzle:user-2:puzzle-profile-public-1"
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={vi.fn()}
/>
</AuthUiContext.Provider>,
);
await user.click(screen.getByText('作品暂时无法进入,请稍后再试。'));
expect(onOpenRecommendGalleryDetail).toHaveBeenCalledWith(puzzlePublicEntry);
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
@@ -1435,7 +1525,7 @@ test('mobile today channel only shows newly published works from today', async (
expect(within(discoverPanel).queryByText('今日更新旧作')).toBeNull();
});
test('desktop home syncs mobile home modules without square or latest labels', () => {
test('desktop logged in home syncs mobile home modules without square or latest labels', () => {
mockDesktopLayout();
const todayPublishedAt = new Date().toISOString();
const todayEntry = {
@@ -1448,9 +1538,64 @@ test('desktop home syncs mobile home modules without square or latest labels', (
updatedAt: todayPublishedAt,
} satisfies PlatformPublicGalleryCard;
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry, todayEntry],
});
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="home"
onTabChange={vi.fn()}
hasSavedGame={false}
savedSnapshot={null}
saveEntries={[]}
saveError={null}
featuredEntries={[]}
latestEntries={[puzzlePublicEntry, todayEntry]}
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()}
/>
</AuthUiContext.Provider>,
);
expect(screen.getByText('今日游戏')).toBeTruthy();
expect(screen.getAllByText('推荐').length).toBeGreaterThan(0);

View File

@@ -118,6 +118,7 @@ export interface RpgEntryHomeViewProps {
onOpenCreateWorld: () => void;
onOpenCreateTypePicker: () => void;
onOpenGalleryDetail: (entry: PlatformPublicGalleryCard) => void;
onOpenRecommendGalleryDetail?: (entry: PlatformPublicGalleryCard) => void;
recommendRuntimeContent?: ReactNode;
activeRecommendEntryKey?: string | null;
isStartingRecommendEntry?: boolean;
@@ -2898,6 +2899,7 @@ export function RpgEntryHomeView({
onResumeSave,
onOpenCreateTypePicker,
onOpenGalleryDetail,
onOpenRecommendGalleryDetail,
recommendRuntimeContent,
activeRecommendEntryKey = null,
isStartingRecommendEntry = false,
@@ -3007,6 +3009,8 @@ export function RpgEntryHomeView({
const [isSavingAvatar, setIsSavingAvatar] = useState(false);
const isAuthenticated = Boolean(authUi?.user);
const isDesktopLayout = usePlatformDesktopLayout();
const openRecommendGalleryDetail =
onOpenRecommendGalleryDetail ?? onOpenGalleryDetail;
const featuredShelf = useMemo(
() => featuredEntries.slice(0, 6),
[featuredEntries],
@@ -3771,12 +3775,17 @@ export function RpgEntryHomeView({
}
if (!isAuthenticated) {
authUi?.openLoginModal(() => onOpenGalleryDetail(activeRecommendEntry));
authUi?.openLoginModal();
return;
}
onOpenGalleryDetail(activeRecommendEntry);
}, [activeRecommendEntry, authUi, isAuthenticated, onOpenGalleryDetail]);
openRecommendGalleryDetail(activeRecommendEntry);
}, [
activeRecommendEntry,
authUi,
isAuthenticated,
openRecommendGalleryDetail,
]);
const selectNextRecommendEntry = useCallback(() => {
onSelectNextRecommendEntry?.();
}, [onSelectNextRecommendEntry]);
@@ -3786,7 +3795,7 @@ export function RpgEntryHomeView({
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
const openLeadPublicEntry = () => {
if (leadPublicEntry) {
onOpenGalleryDetail(leadPublicEntry);
openRecommendGalleryDetail(leadPublicEntry);
return;
}
@@ -3870,7 +3879,7 @@ export function RpgEntryHomeView({
type="button"
onClick={() =>
activeRecommendEntry
? onOpenGalleryDetail(activeRecommendEntry)
? openRecommendGalleryDetail(activeRecommendEntry)
: undefined
}
className="platform-recommend-runtime-state platform-recommend-runtime-state--button"
@@ -4463,7 +4472,7 @@ export function RpgEntryHomeView({
key={`${buildPublicGalleryCardKey(entry)}:desktop-today`}
entry={entry}
rank={index + 1}
onClick={() => onOpenGalleryDetail(entry)}
onClick={() => openRecommendGalleryDetail(entry)}
/>
))}
</div>
@@ -4486,7 +4495,7 @@ export function RpgEntryHomeView({
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-featured`}
entry={entry}
onClick={() => onOpenGalleryDetail(entry)}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
@@ -4552,7 +4561,7 @@ export function RpgEntryHomeView({
key={`${entry.ownerUserId}:${entry.profileId}:desktop-history`}
type="button"
onClick={() =>
onOpenGalleryDetail({
openRecommendGalleryDetail({
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
publicWorkCode: null,
@@ -4621,7 +4630,7 @@ export function RpgEntryHomeView({
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-category:${activeCategoryGroup.tag}`}
entry={entry}
onClick={() => onOpenGalleryDetail(entry)}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
@@ -4638,7 +4647,10 @@ export function RpgEntryHomeView({
);
const tabContentById = {
home: isDesktopLayout ? desktopHomeContent : mobileRecommendContent,
home:
!isAuthenticated || !isDesktopLayout
? mobileRecommendContent
: desktopHomeContent,
category: categoryContent,
create: createContent,
saves: savesContent,