fix: route recommend puzzle next through feed
This commit is contained in:
@@ -5808,10 +5808,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleRun(null);
|
||||
setPuzzleRuntimeAuthMode('default');
|
||||
setSelectedPuzzleDetail(null);
|
||||
platformBootstrap.setPlatformTab(authUi?.user ? 'home' : 'category');
|
||||
platformBootstrap.setPlatformTab('home');
|
||||
setSelectionStage('platform');
|
||||
clearPuzzleRuntimeUrlState();
|
||||
}, [authUi?.user, platformBootstrap, setSelectionStage]);
|
||||
}, [platformBootstrap, setSelectionStage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -12566,10 +12566,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const targetProfileId = _target?.profileId?.trim() ?? '';
|
||||
const preferSimilarWork =
|
||||
activeRecommendRuntimeKind === 'puzzle' &&
|
||||
puzzleRuntimeReturnStage === 'platform' &&
|
||||
puzzleRun.nextLevelMode === 'sameWork';
|
||||
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
|
||||
const itemPromise =
|
||||
selectedPuzzleDetail?.profileId === targetProfileId
|
||||
@@ -12609,13 +12605,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
preferSimilarWork ? { preferSimilarWork: true } : {},
|
||||
{},
|
||||
runtimeGuestOptions,
|
||||
)
|
||||
: await advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
preferSimilarWork ? { preferSimilarWork: true } : {},
|
||||
);
|
||||
: await advancePuzzleNextLevel(puzzleRun.runId, {});
|
||||
const nextProfileId = run.currentLevel?.profileId?.trim() ?? '';
|
||||
if (
|
||||
nextProfileId &&
|
||||
@@ -16011,8 +16004,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
onDragPiece={(payload) => {
|
||||
void dragPuzzlePiece(payload);
|
||||
}}
|
||||
onAdvanceNextLevel={(target) => {
|
||||
void advancePuzzleLevel(target);
|
||||
onAdvanceNextLevel={() => {
|
||||
selectAdjacentRecommendRuntimeEntry(1, activeRecommendEntryKey);
|
||||
}}
|
||||
onRestartLevel={() => {
|
||||
void restartPuzzleCurrentLevel();
|
||||
@@ -16266,9 +16259,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
squareHoleRun,
|
||||
submitBigFishInput,
|
||||
submitVisualNovelRuntimeAction,
|
||||
advancePuzzleLevel,
|
||||
dragPuzzlePiece,
|
||||
restartPuzzleCurrentLevel,
|
||||
selectAdjacentRecommendRuntimeEntry,
|
||||
setSquareHoleError,
|
||||
swapPuzzlePiecesInRun,
|
||||
syncPuzzleRuntimeTimeout,
|
||||
|
||||
@@ -13,16 +13,16 @@ import type {
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
CustomWorldWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type {
|
||||
JumpHopRuntimeRunSnapshotResponse,
|
||||
JumpHopWorkDetailResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
JumpHopWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
@@ -71,7 +71,6 @@ import {
|
||||
submitBigFishInput,
|
||||
} from '../../services/big-fish-runtime';
|
||||
import { listBigFishWorks } from '../../services/big-fish-works';
|
||||
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
|
||||
import {
|
||||
type CreationEntryConfig,
|
||||
fetchCreationEntryConfig,
|
||||
@@ -91,6 +90,7 @@ import {
|
||||
regenerateBabyObjectMatchDraftAssets,
|
||||
saveBabyObjectMatchDraft,
|
||||
} from '../../services/edutainment-baby-object';
|
||||
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
|
||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
|
||||
import {
|
||||
@@ -334,10 +334,6 @@ const ISOLATED_RUNTIME_AUTH_OPTIONS = {
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
};
|
||||
const RECOMMEND_RUNTIME_AUTH_OPTIONS = {
|
||||
...ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
};
|
||||
const LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS = ISOLATED_RUNTIME_AUTH_OPTIONS;
|
||||
|
||||
function getPlatformTabPanel(tab: string) {
|
||||
@@ -7505,7 +7501,7 @@ test('logged out home recommendation next starts the next puzzle work', async ()
|
||||
});
|
||||
});
|
||||
|
||||
test('home recommendation puzzle next level switches to similar work detail', async () => {
|
||||
test('home recommendation puzzle next level uses unified recommend switching', async () => {
|
||||
const user = userEvent.setup();
|
||||
const entryWork = {
|
||||
workId: 'puzzle-work-public-guest-1',
|
||||
@@ -7547,17 +7543,17 @@ test('home recommendation puzzle next level switches to similar work detail', as
|
||||
},
|
||||
],
|
||||
} satisfies PuzzleWorkSummary;
|
||||
const similarWork = {
|
||||
const nextRecommendWork = {
|
||||
...entryWork,
|
||||
workId: 'puzzle-work-similar-guest-1',
|
||||
profileId: 'puzzle-profile-similar-guest-1',
|
||||
workId: 'puzzle-work-public-guest-2',
|
||||
profileId: 'puzzle-profile-public-guest-2',
|
||||
levelName: '风塔试炼',
|
||||
summary: '另一套奇幻机关拼图。',
|
||||
summary: '另一套推荐拼图。',
|
||||
levels: [
|
||||
{
|
||||
levelId: 'similar-level-1',
|
||||
levelId: 'next-recommend-level-1',
|
||||
levelName: '风塔试炼',
|
||||
pictureDescription: '相似作品首关。',
|
||||
pictureDescription: '推荐队列下一张拼图。',
|
||||
candidates: [],
|
||||
selectedCandidateId: null,
|
||||
coverImageSrc: null,
|
||||
@@ -7586,47 +7582,35 @@ test('home recommendation puzzle next level switches to similar work detail', as
|
||||
entryWork.profileId,
|
||||
entryWork.levelName,
|
||||
);
|
||||
const similarRun = {
|
||||
...buildMockPuzzleRun(similarWork.profileId, similarWork.levelName),
|
||||
runId: clearedRun.runId,
|
||||
entryProfileId: entryWork.profileId,
|
||||
currentLevelIndex: 2,
|
||||
currentLevel: {
|
||||
...buildMockPuzzleRun(similarWork.profileId, similarWork.levelName)
|
||||
.currentLevel!,
|
||||
runId: clearedRun.runId,
|
||||
levelIndex: 2,
|
||||
levelId: 'similar-level-1',
|
||||
startedAtMs: Date.now(),
|
||||
},
|
||||
};
|
||||
const nextRecommendRun = buildMockPuzzleRun(
|
||||
nextRecommendWork.profileId,
|
||||
nextRecommendWork.levelName,
|
||||
);
|
||||
|
||||
vi.mocked(listPuzzleGallery).mockResolvedValue({
|
||||
items: [entryWork],
|
||||
items: [entryWork, nextRecommendWork],
|
||||
});
|
||||
vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
|
||||
item: profileId === similarWork.profileId ? similarWork : entryWork,
|
||||
item: profileId === nextRecommendWork.profileId ? nextRecommendWork : entryWork,
|
||||
}));
|
||||
vi.mocked(startPuzzleRun).mockResolvedValue({
|
||||
run: {
|
||||
...startedRun,
|
||||
currentLevel: {
|
||||
...startedRun.currentLevel!,
|
||||
startedAtMs: Date.now(),
|
||||
vi.mocked(startPuzzleRun).mockImplementation(async (payload) => {
|
||||
const run =
|
||||
payload.profileId === nextRecommendWork.profileId
|
||||
? nextRecommendRun
|
||||
: startedRun;
|
||||
return {
|
||||
run: {
|
||||
...run,
|
||||
currentLevel: {
|
||||
...run.currentLevel!,
|
||||
startedAtMs: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mocked(submitPuzzleLeaderboard).mockResolvedValue({
|
||||
run: clearedRunWithSameWorkNext,
|
||||
});
|
||||
let resolveAdvancePuzzleNextLevel!: (value: {
|
||||
run: PuzzleRunSnapshot;
|
||||
}) => void;
|
||||
vi.mocked(advancePuzzleNextLevel).mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveAdvancePuzzleNextLevel = resolve;
|
||||
}),
|
||||
);
|
||||
vi.mocked(swapLocalPuzzlePieces).mockReturnValue(clearedRun);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
@@ -7655,24 +7639,23 @@ test('home recommendation puzzle next level switches to similar work detail', as
|
||||
await user.click(within(dialog).getByRole('button', { name: '下一关' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(clearedRun.runId, {
|
||||
preferSimilarWork: true,
|
||||
});
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: nextRecommendWork.profileId,
|
||||
levelId: null,
|
||||
},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(advancePuzzleNextLevel).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('puzzle-board')).toBeTruthy();
|
||||
expect(screen.queryByText('加载中...')).toBeNull();
|
||||
|
||||
resolveAdvancePuzzleNextLevel({ run: similarRun });
|
||||
await waitFor(() => {
|
||||
expect(getPuzzleGalleryDetail).toHaveBeenCalledWith(similarWork.profileId);
|
||||
});
|
||||
expect(
|
||||
await screen.findByLabelText('风塔试炼 作品信息', undefined, {
|
||||
timeout: 3000,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.getAllByText('风塔试炼').length).toBeGreaterThan(0);
|
||||
expect(startPuzzleRun).toHaveBeenCalledTimes(1);
|
||||
expect(startPuzzleRun).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('home recommendation Match3D runtime keeps profile generated models when card summary is stale', async () => {
|
||||
|
||||
@@ -1039,7 +1039,7 @@ function renderStatefulLoggedOutHomeView(
|
||||
|
||||
function StatefulLoggedOutHomeView() {
|
||||
const [activeTab, setActiveTab] =
|
||||
useState<RpgEntryHomeViewProps['activeTab']>('category');
|
||||
useState<RpgEntryHomeViewProps['activeTab']>('home');
|
||||
|
||||
return (
|
||||
<AuthUiContext.Provider
|
||||
@@ -3640,24 +3640,19 @@ test('public gallery cards hide phone masked author and public user code', async
|
||||
expect(within(card).queryByText('SY-00000003')).toBeNull();
|
||||
});
|
||||
|
||||
test('logged out mobile shell defaults to discover tab', () => {
|
||||
test('logged out mobile shell defaults to recommend tab', () => {
|
||||
const { container } = renderStatefulLoggedOutHomeView({
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
});
|
||||
|
||||
const activePanel = container.querySelector('.platform-tab-panel--active');
|
||||
expect(activePanel?.id).toBe('platform-tab-panel-category');
|
||||
expect(
|
||||
screen.getByPlaceholderText('搜索作品号、名称、作者、描述'),
|
||||
).toBeTruthy();
|
||||
expect(container.querySelector('.platform-mobile-topbar')).toBeTruthy();
|
||||
expect(activePanel?.id).toBe('platform-tab-panel-home');
|
||||
expect(
|
||||
container.querySelector('.platform-mobile-entry-shell--recommend'),
|
||||
).toBeNull();
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('logged out recommend tab opens embedded runtime without login modal', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
|
||||
@@ -3667,10 +3662,6 @@ test('logged out recommend tab opens embedded runtime without login modal', asyn
|
||||
throw new Error('缺少底部导航');
|
||||
}
|
||||
|
||||
await user.click(
|
||||
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||
);
|
||||
|
||||
expect(openLoginModal).not.toHaveBeenCalled();
|
||||
expect(container.querySelector('.platform-recommend-cover-only')).toBeNull();
|
||||
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
|
||||
@@ -3683,7 +3674,6 @@ test('logged out recommend tab opens embedded runtime without login modal', asyn
|
||||
});
|
||||
|
||||
test('logged out recommend runtime keeps detail callback idle', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenGalleryDetail = vi.fn();
|
||||
const { openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
@@ -3695,10 +3685,6 @@ test('logged out recommend runtime keeps detail callback idle', async () => {
|
||||
throw new Error('缺少底部导航');
|
||||
}
|
||||
|
||||
await user.click(
|
||||
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||
);
|
||||
|
||||
expect(openLoginModal).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
||||
@@ -3920,7 +3906,7 @@ test('mobile recommend startup keeps cover visible without loading copy', () =>
|
||||
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('mobile recommend next level keeps runtime visual stable when active work changes', async () => {
|
||||
test('mobile recommend keeps runtime visual stable when active entry changes', async () => {
|
||||
const animationCallbacks: FrameRequestCallback[] = [];
|
||||
Object.defineProperty(window, 'requestAnimationFrame', {
|
||||
configurable: true,
|
||||
@@ -3944,18 +3930,18 @@ test('mobile recommend next level keeps runtime visual stable when active work c
|
||||
worldName: '当前拼图',
|
||||
coverImageSrc: 'current-cover.png',
|
||||
} satisfies PlatformPublicGalleryCard;
|
||||
const similarEntry = {
|
||||
const nextEntry = {
|
||||
...puzzlePublicEntry,
|
||||
workId: 'puzzle-work-similar-1',
|
||||
profileId: 'puzzle-profile-similar-1',
|
||||
workId: 'puzzle-work-next-1',
|
||||
profileId: 'puzzle-profile-next-1',
|
||||
ownerUserId: 'user-feed-2',
|
||||
publicWorkCode: 'PZ-SIMILAR1',
|
||||
worldName: '相似拼图',
|
||||
coverImageSrc: 'similar-cover.png',
|
||||
publicWorkCode: 'PZ-NEXT1',
|
||||
worldName: '下一张拼图',
|
||||
coverImageSrc: 'next-cover.png',
|
||||
} satisfies PlatformPublicGalleryCard;
|
||||
|
||||
const { rerender } = renderLoggedOutHomeView(vi.fn(), {
|
||||
latestEntries: [firstEntry, similarEntry],
|
||||
latestEntries: [firstEntry, nextEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-feed-1:puzzle-profile-feed-1',
|
||||
isRecommendRuntimeReady: true,
|
||||
});
|
||||
@@ -3998,7 +3984,7 @@ test('mobile recommend next level keeps runtime visual stable when active work c
|
||||
saveEntries={[]}
|
||||
saveError={null}
|
||||
featuredEntries={[]}
|
||||
latestEntries={[firstEntry, similarEntry]}
|
||||
latestEntries={[firstEntry, nextEntry]}
|
||||
myEntries={[]}
|
||||
historyEntries={[]}
|
||||
profileDashboard={null}
|
||||
@@ -4013,7 +3999,7 @@ test('mobile recommend next level keeps runtime visual stable when active work c
|
||||
onOpenCreateTypePicker={vi.fn()}
|
||||
onOpenGalleryDetail={vi.fn()}
|
||||
recommendRuntimeContent={<div data-testid="recommend-runtime" />}
|
||||
activeRecommendEntryKey="puzzle:user-feed-2:puzzle-profile-similar-1"
|
||||
activeRecommendEntryKey="puzzle:user-feed-2:puzzle-profile-next-1"
|
||||
isRecommendRuntimeReady
|
||||
onOpenLibraryDetail={vi.fn()}
|
||||
onSearchPublicCode={vi.fn()}
|
||||
@@ -4026,7 +4012,7 @@ test('mobile recommend next level keeps runtime visual stable when active work c
|
||||
) as HTMLElement | null;
|
||||
expect(rail?.className).toContain('platform-recommend-swipe-rail--settled');
|
||||
expect(rail?.style.transform).toBe('translate3d(0, 0px, 0)');
|
||||
expect(screen.getByLabelText('相似拼图 作品信息')).toBeTruthy();
|
||||
expect(screen.getByLabelText('下一张拼图 作品信息')).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector('.platform-recommend-runtime-cover')?.className,
|
||||
).toContain('platform-recommend-runtime-cover--hidden');
|
||||
@@ -4394,6 +4380,7 @@ test('mobile discover recommend feed only rotates the card closest to screen cen
|
||||
});
|
||||
|
||||
test('mobile discover recommend feed renders cover fallback for legacy browsers', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderStatefulLoggedOutHomeView({
|
||||
latestEntries: [
|
||||
{
|
||||
@@ -4403,6 +4390,7 @@ test('mobile discover recommend feed renders cover fallback for legacy browsers'
|
||||
},
|
||||
],
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: '发现' }));
|
||||
|
||||
const discoverPanel = document.getElementById('platform-tab-panel-category');
|
||||
if (!discoverPanel) {
|
||||
|
||||
@@ -1019,8 +1019,8 @@ function RecommendRuntimeVisual({
|
||||
}
|
||||
previousEntryKeyRef.current = activeEntryKey;
|
||||
setIsRuntimeMounted((currentValue) => {
|
||||
// 中文注释:拼图推荐流“下一关”会在同一个 run 内切到相似作品;
|
||||
// 此时只更新作品信息和分享基准,不应重显封面造成运行态闪跳。
|
||||
// 中文注释:推荐运行态已挂载后,用户切换推荐作品只更新作品信息;
|
||||
// 不重显封面,避免已 ready 的运行态视觉闪跳。
|
||||
if (currentValue && !isStarting && isRuntimeReady) {
|
||||
return currentValue;
|
||||
}
|
||||
@@ -4425,9 +4425,9 @@ export function RpgEntryHomeView({
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibleTabs.includes(activeTab)) {
|
||||
onTabChange(isAuthenticated ? 'home' : 'category');
|
||||
onTabChange('home');
|
||||
}
|
||||
}, [activeTab, isAuthenticated, onTabChange, visibleTabs]);
|
||||
}, [activeTab, onTabChange, visibleTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
||||
@@ -74,8 +74,7 @@ export function useRpgEntryBootstrap(
|
||||
PlatformBrowseHistoryEntry[]
|
||||
>([]);
|
||||
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>([]);
|
||||
const [platformTab, setPlatformTabState] =
|
||||
useState<PlatformHomeTab>('category');
|
||||
const [platformTab, setPlatformTabState] = useState<PlatformHomeTab>('home');
|
||||
const [platformError, setPlatformError] = useState<string | null>(null);
|
||||
const [dashboardError, setDashboardError] = useState<string | null>(null);
|
||||
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||
@@ -351,8 +350,8 @@ export function useRpgEntryBootstrap(
|
||||
!hasInitialAgentSession &&
|
||||
!hasExplicitPlatformTabSelectionRef.current
|
||||
) {
|
||||
// 中文注释:新用户先进入发现页;推荐页可直接进入,真正受保护的动作再单独做登录门禁。
|
||||
setPlatformTabState(isAuthenticated ? 'home' : 'category');
|
||||
// 中文注释:新用户也先进入推荐页;真正受保护的动作再单独做登录门禁。
|
||||
setPlatformTabState('home');
|
||||
}
|
||||
} finally {
|
||||
if (isActive) {
|
||||
@@ -369,7 +368,6 @@ export function useRpgEntryBootstrap(
|
||||
canReadProtectedData,
|
||||
getProfileDashboard,
|
||||
hasInitialAgentSession,
|
||||
isAuthenticated,
|
||||
user,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user