Merge remote-tracking branch 'origin/master' into codex/fix-draft-result-back-target

This commit is contained in:
kdletters
2026-05-26 16:45:57 +08:00
47 changed files with 2678 additions and 79 deletions

View File

@@ -6367,6 +6367,260 @@ test('home recommendation keeps logged-in puzzle start on default auth instead o
);
});
test('logged out home recommendation next starts the next puzzle work', async () => {
const user = userEvent.setup();
const firstWork = {
workId: 'puzzle-work-public-next-1',
profileId: 'puzzle-profile-public-next-1',
ownerUserId: 'user-2',
sourceSessionId: 'puzzle-session-public-next-1',
authorDisplayName: '拼图作者',
levelName: '家常菜',
summary: '酱猪蹄不是酱肘子。',
themeTags: ['家常菜', '拼图'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'published',
updatedAt: '2026-04-25T10:00:00.000Z',
publishedAt: '2026-04-25T10:00:00.000Z',
playCount: 47,
likeCount: 1,
publishReady: true,
} satisfies PuzzleWorkSummary;
const secondWork = {
...firstWork,
workId: 'puzzle-work-public-next-2',
profileId: 'puzzle-profile-public-next-2',
ownerUserId: 'user-3',
sourceSessionId: 'puzzle-session-public-next-2',
authorDisplayName: '贝壳作者',
levelName: '贝壳',
summary: '第二个公开拼图。',
themeTags: ['贝壳', '拼图'],
playCount: 1,
likeCount: 0,
updatedAt: '2026-04-25T09:00:00.000Z',
publishedAt: '2026-04-25T09:00:00.000Z',
} satisfies PuzzleWorkSummary;
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [firstWork, secondWork],
});
vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
item: profileId === secondWork.profileId ? secondWork : firstWork,
}));
render(
<TestWrapper
authValue={createAuthValue({
user: null,
canAccessProtectedData: false,
openLoginModal: () => {},
requireAuth: (action) => action(),
})}
/>,
);
const recommendNavButton = document.querySelector<HTMLButtonElement>(
'.platform-bottom-nav [aria-label="推荐"]',
);
expect(recommendNavButton).toBeTruthy();
await user.click(recommendNavButton!);
await waitFor(() => {
expect(startPuzzleRun).toHaveBeenCalledWith(
{
profileId: firstWork.profileId,
levelId: null,
},
expect.objectContaining({
runtimeGuestToken: 'runtime-guest-token',
}),
);
});
expect(startPuzzleRun).toHaveBeenCalledTimes(1);
await user.click(await screen.findByRole('button', { name: '下一个' }));
await waitFor(() => {
expect(startPuzzleRun).toHaveBeenCalledWith(
{
profileId: secondWork.profileId,
levelId: null,
},
expect.objectContaining({
runtimeGuestToken: 'runtime-guest-token',
}),
);
});
});
test('home recommendation puzzle next level switches to similar work detail', async () => {
const user = userEvent.setup();
const entryWork = {
workId: 'puzzle-work-public-guest-1',
profileId: 'puzzle-profile-public-guest-1',
ownerUserId: 'user-2',
sourceSessionId: 'puzzle-session-public-guest-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,
levels: [
{
levelId: 'puzzle-level-1',
levelName: '雨夜猫塔',
pictureDescription: '首关。',
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'ready',
},
{
levelId: 'puzzle-level-2',
levelName: '星桥机关',
pictureDescription: '同作品第二关。',
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'ready',
},
],
} satisfies PuzzleWorkSummary;
const similarWork = {
...entryWork,
workId: 'puzzle-work-similar-guest-1',
profileId: 'puzzle-profile-similar-guest-1',
levelName: '风塔试炼',
summary: '另一套奇幻机关拼图。',
levels: [
{
levelId: 'similar-level-1',
levelName: '风塔试炼',
pictureDescription: '相似作品首关。',
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'ready',
},
],
} satisfies PuzzleWorkSummary;
const clearedRun = buildClearedPuzzleRun({
runId: 'run-puzzle-profile-public-guest-1',
entryProfileId: entryWork.profileId,
profileId: entryWork.profileId,
levelName: entryWork.levelName,
levelIndex: 1,
elapsedMs: 18_000,
});
const clearedRunWithSameWorkNext: PuzzleRunSnapshot = {
...clearedRun,
recommendedNextProfileId: entryWork.profileId,
nextLevelMode: 'sameWork',
nextLevelProfileId: entryWork.profileId,
nextLevelId: 'puzzle-level-2',
recommendedNextWorks: [],
};
const startedRun = buildMockPuzzleRun(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(),
},
};
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [entryWork],
});
vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
item: profileId === similarWork.profileId ? similarWork : entryWork,
}));
vi.mocked(startPuzzleRun).mockResolvedValue({
run: {
...startedRun,
currentLevel: {
...startedRun.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 />);
await waitFor(() => {
expect(startPuzzleRun).toHaveBeenCalledWith(
{
profileId: entryWork.profileId,
levelId: null,
},
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
);
});
await waitFor(() => {
expect(screen.getByTestId('puzzle-board')).toBeTruthy();
});
await user.click(document.querySelector('[data-piece-id="piece-0"]')!);
await user.click(document.querySelector('[data-piece-id="piece-1"]')!);
const dialog = await screen.findByRole(
'dialog',
{ name: '通关完成' },
{ timeout: 3000 },
);
await user.click(within(dialog).getByRole('button', { name: '下一关' }));
await waitFor(() => {
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
clearedRun.runId,
{ preferSimilarWork: true },
);
});
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);
});
test('home recommendation Match3D runtime keeps profile generated models when card summary is stale', async () => {
const match3dCard: Match3DWorkSummary = {
workId: 'match3d-work-card-1',

View File

@@ -195,8 +195,8 @@ export interface RpgEntryHomeViewProps {
activeRecommendEntryKey?: string | null;
isStartingRecommendEntry?: boolean;
recommendRuntimeError?: string | null;
onSelectNextRecommendEntry?: () => void;
onSelectPreviousRecommendEntry?: () => void;
onSelectNextRecommendEntry?: (activeEntryKey?: string | null) => void;
onSelectPreviousRecommendEntry?: (activeEntryKey?: string | null) => void;
onLikeRecommendEntry?: (entry: PlatformPublicGalleryCard) => void;
onRemixRecommendEntry?: (entry: PlatformPublicGalleryCard) => void;
onOpenLibraryDetail: (
@@ -5214,6 +5214,9 @@ export function RpgEntryHomeView({
const [recommendShareState, setRecommendShareState] = useState<
'idle' | 'copied' | 'failed'
>('idle');
const activeRecommendEntryKeyForSelection = activeRecommendEntry
? buildPublicGalleryCardKey(activeRecommendEntry)
: null;
const recommendShareResetTimerRef = useRef<number | null>(null);
const recommendCardStageRef = useRef<HTMLDivElement | null>(null);
const recommendDragStartRef = useRef<{
@@ -5236,15 +5239,16 @@ export function RpgEntryHomeView({
);
window.setTimeout(() => {
if (direction === 1) {
onSelectNextRecommendEntry?.();
onSelectNextRecommendEntry?.(activeRecommendEntryKeyForSelection);
} else {
onSelectPreviousRecommendEntry?.();
onSelectPreviousRecommendEntry?.(activeRecommendEntryKeyForSelection);
}
setRecommendDragOffsetY(0);
setRecommendDragCommitDirection(null);
}, RECOMMEND_ENTRY_COMMIT_ANIMATION_MS);
},
[
activeRecommendEntryKeyForSelection,
onSelectNextRecommendEntry,
onSelectPreviousRecommendEntry,
recommendDragCommitDirection,
@@ -5344,9 +5348,10 @@ export function RpgEntryHomeView({
return;
}
onSelectNextRecommendEntry?.();
onSelectNextRecommendEntry?.(activeRecommendEntryKeyForSelection);
}, [
activeRecommendEntry,
activeRecommendEntryKeyForSelection,
commitRecommendDrag,
isAuthenticated,
onSelectNextRecommendEntry,