合并 master 并修复架构分支回归
合入 master 最新的认证、玩法契约与推荐页改动。 修复拼图草稿生成、推荐页下一关和公开详情访客试玩回归。 修复抓大鹅草稿试玩鉴权与首屏推荐详情测试入口。 补齐相关测试夹具、文档与团队记忆更新。
This commit is contained in:
@@ -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';
|
||||
@@ -42,7 +42,10 @@ import type {
|
||||
CustomWorldGalleryCard,
|
||||
CustomWorldLibraryEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import type {
|
||||
WoodenFishGalleryCardResponse,
|
||||
WoodenFishWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import {
|
||||
@@ -71,7 +74,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 +93,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 {
|
||||
@@ -155,6 +158,7 @@ import {
|
||||
deleteRpgEntryWorldProfile,
|
||||
getRpgEntryWorldGalleryDetail as getRpgEntryWorldGalleryDetailFromClient,
|
||||
getRpgEntryWorldGalleryDetailByCode,
|
||||
likeRpgEntryWorldGallery,
|
||||
recordRpgEntryWorldGalleryPlay,
|
||||
remixRpgEntryWorldGallery,
|
||||
} from '../../services/rpg-entry/rpgEntryLibraryClient';
|
||||
@@ -334,10 +338,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) {
|
||||
@@ -542,6 +542,7 @@ const rpgEntryLibraryServiceMocks = vi.hoisted(() => ({
|
||||
getRpgEntryWorldGalleryDetail: vi.fn(),
|
||||
getRpgEntryWorldGalleryDetailByCode: vi.fn(),
|
||||
getRpgEntryWorldLibraryDetail: vi.fn(),
|
||||
likeRpgEntryWorldGallery: vi.fn(),
|
||||
listRpgEntryWorldGallery: vi.fn(),
|
||||
listRpgEntryWorldLibrary: vi.fn(),
|
||||
publishRpgEntryWorldProfile: vi.fn(),
|
||||
@@ -2005,6 +2006,18 @@ function buildReadyPuzzleDraft(
|
||||
'/generated-puzzle-assets/puzzle-session-recovered/ui/background.png',
|
||||
uiBackgroundImageObjectKey:
|
||||
'generated-puzzle-assets/puzzle-session-recovered/ui/background.png',
|
||||
levelSceneImageSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-recovered/level-scene.png',
|
||||
levelSceneImageObjectKey:
|
||||
'generated-puzzle-assets/puzzle-session-recovered/level-scene.png',
|
||||
uiSpritesheetImageSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-recovered/ui-spritesheet.png',
|
||||
uiSpritesheetImageObjectKey:
|
||||
'generated-puzzle-assets/puzzle-session-recovered/ui-spritesheet.png',
|
||||
levelBackgroundImageSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-recovered/level-background.png',
|
||||
levelBackgroundImageObjectKey:
|
||||
'generated-puzzle-assets/puzzle-session-recovered/level-background.png',
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
],
|
||||
@@ -5216,6 +5229,101 @@ test('running puzzle draft opens generation progress from draft tab', async () =
|
||||
});
|
||||
});
|
||||
|
||||
test('puzzle text-only form stays generating when compile starts background image without cover', async () => {
|
||||
const user = userEvent.setup();
|
||||
const initialSession = buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-session-text-only',
|
||||
stage: 'collecting_anchors',
|
||||
progressPercent: 0,
|
||||
draft: null,
|
||||
});
|
||||
const generatingDraft = buildReadyPuzzleDraft({
|
||||
workTitle: '文字直创拼图',
|
||||
workDescription: '只输入文字后后台继续生成图片。',
|
||||
candidates: [],
|
||||
selectedCandidateId: null,
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
generationStatus: 'generating',
|
||||
levels: [
|
||||
{
|
||||
...buildReadyPuzzleDraft().levels![0]!,
|
||||
candidates: [],
|
||||
selectedCandidateId: null,
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
levelSceneImageSrc: null,
|
||||
levelSceneImageObjectKey: null,
|
||||
uiSpritesheetImageSrc: null,
|
||||
uiSpritesheetImageObjectKey: null,
|
||||
levelBackgroundImageSrc: null,
|
||||
levelBackgroundImageObjectKey: null,
|
||||
generationStatus: 'generating',
|
||||
},
|
||||
],
|
||||
});
|
||||
const generatingSession = buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-session-text-only',
|
||||
stage: 'image_refining',
|
||||
progressPercent: 88,
|
||||
draft: generatingDraft,
|
||||
lastAssistantReply: '已编译首关草稿,并启动首关画面和 UI 资产后台生成。',
|
||||
resultPreview: {
|
||||
draft: generatingDraft,
|
||||
blockers: [
|
||||
{
|
||||
id: 'missing-cover-image-puzzle-level-1',
|
||||
code: 'MISSING_COVER_IMAGE',
|
||||
message: '正式拼图图片尚未确定',
|
||||
},
|
||||
],
|
||||
qualityFindings: [],
|
||||
publishReady: false,
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
|
||||
session: initialSession,
|
||||
});
|
||||
vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
|
||||
operation: {
|
||||
operationId: 'compile-puzzle-text-only',
|
||||
type: 'compile_puzzle_draft',
|
||||
status: 'completed',
|
||||
phaseLabel: '首关拼图草稿',
|
||||
phaseDetail: '已编译首关草稿,并启动首关画面和 UI 资产后台生成。',
|
||||
progress: 0.88,
|
||||
},
|
||||
session: generatingSession,
|
||||
});
|
||||
vi.mocked(getPuzzleAgentSession).mockResolvedValue({
|
||||
session: generatingSession,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await findCreationTypeButton('拼图'));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '拼图图片生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(executePuzzleAgentAction).toHaveBeenCalledWith(
|
||||
'puzzle-session-text-only',
|
||||
expect.objectContaining({ action: 'compile_puzzle_draft' }),
|
||||
);
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: '生成完成' })).toBeNull();
|
||||
expect(screen.queryByText('请先选择一张正式拼图图片。')).toBeNull();
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
expect(updatePuzzleWork).not.toHaveBeenCalled();
|
||||
expect(startLocalPuzzleRun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('puzzle form checks mud points before creating a draft', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getProfileDashboard).mockResolvedValue({
|
||||
@@ -6523,6 +6631,7 @@ test('clicking a public work while logged out opens public detail without starti
|
||||
/>,
|
||||
);
|
||||
|
||||
await openDiscoverHub(user);
|
||||
const workCards = await screen.findAllByRole('button', {
|
||||
name: /潮雾列岛/u,
|
||||
});
|
||||
@@ -6576,20 +6685,29 @@ test('logged out public detail gates puzzle start and remix before real actions'
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('星桥机关').length).toBeGreaterThan(0);
|
||||
});
|
||||
await openDiscoverHub(user);
|
||||
|
||||
const workCards = screen.getAllByRole('button', { name: /星桥机关/u });
|
||||
await user.click(workCards[0]!);
|
||||
expect(await screen.findByText('详情')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '启动' }));
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
expect.objectContaining({
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(requireAuth).toHaveBeenCalledTimes(1);
|
||||
expect(startPuzzleRun).not.toHaveBeenCalled();
|
||||
requireAuth.mockClear();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '作品改造' }));
|
||||
expect(requireAuth).toHaveBeenCalledTimes(2);
|
||||
expect(requireAuth).toHaveBeenCalledTimes(1);
|
||||
expect(remixPuzzleGalleryWork).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -7369,6 +7487,75 @@ test('home recommendation share opens publish share modal', async () => {
|
||||
.toBeTruthy();
|
||||
});
|
||||
|
||||
test('home recommendation wooden fish like does not call RPG gallery like', async () => {
|
||||
const user = userEvent.setup();
|
||||
const publishedWoodenFishWork: WoodenFishGalleryCardResponse = {
|
||||
publicWorkCode: 'WF-3A9EC89B',
|
||||
workId: 'wooden-fish-work-like-1',
|
||||
profileId: 'wooden-fish-profile-like-1',
|
||||
ownerUserId: 'wooden-fish-user-1',
|
||||
authorDisplayName: '木鱼作者',
|
||||
workTitle: '莲台木鱼',
|
||||
workDescription: '推荐页里的敲木鱼作品。',
|
||||
coverImageSrc: null,
|
||||
themeTags: ['敲木鱼'],
|
||||
publicationStatus: 'published',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-04-25T09:00:00.000Z',
|
||||
publishedAt: '2026-04-25T09:00:00.000Z',
|
||||
generationStatus: 'ready',
|
||||
};
|
||||
|
||||
vi.mocked(woodenFishClient.listGallery).mockResolvedValue({
|
||||
items: [publishedWoodenFishWork],
|
||||
hasMore: false,
|
||||
nextCursor: null,
|
||||
});
|
||||
vi.mocked(woodenFishClient.startRun).mockResolvedValue({
|
||||
run: {
|
||||
runId: 'wooden-fish-run-like-1',
|
||||
profileId: publishedWoodenFishWork.profileId,
|
||||
ownerUserId: publishedWoodenFishWork.ownerUserId,
|
||||
status: 'playing',
|
||||
totalTapCount: 0,
|
||||
wordCounters: [],
|
||||
startedAtMs: 1,
|
||||
updatedAtMs: 1,
|
||||
finishedAtMs: null,
|
||||
},
|
||||
});
|
||||
vi.mocked(likeRpgEntryWorldGallery).mockResolvedValue(
|
||||
buildMockRpgGalleryDetail({
|
||||
ownerUserId: 'custom-world-user-1',
|
||||
profileId: 'custom-world-profile-1',
|
||||
publicWorkCode: 'CW-00000001',
|
||||
authorPublicUserCode: 'SY-00000001',
|
||||
visibility: 'published',
|
||||
publishedAt: '2026-04-25T09:00:00.000Z',
|
||||
updatedAt: '2026-04-25T09:00:00.000Z',
|
||||
authorDisplayName: 'RPG 作者',
|
||||
worldName: '不应被点赞的 RPG',
|
||||
subtitle: '错误分流',
|
||||
summaryText: 'WF 点赞不应进入这里。',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'mythic',
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
likeCount: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
const meta = await screen.findByLabelText('莲台木鱼 作品信息');
|
||||
await user.click(within(meta).getByRole('button', { name: '点赞 0' }));
|
||||
|
||||
expect(likeRpgEntryWorldGallery).not.toHaveBeenCalled();
|
||||
expect(
|
||||
await screen.findByText('作品类型 wooden-fish 暂不支持点赞。'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('home recommendation keeps logged-in puzzle start on default auth instead of guest token', async () => {
|
||||
const publishedPuzzleWork = {
|
||||
workId: 'puzzle-work-public-2',
|
||||
@@ -7471,12 +7658,6 @@ test('logged out home recommendation next starts the next puzzle work', async ()
|
||||
/>,
|
||||
);
|
||||
|
||||
const recommendNavButton = document.querySelector<HTMLButtonElement>(
|
||||
'.platform-bottom-nav [aria-label="推荐"]',
|
||||
);
|
||||
expect(recommendNavButton).toBeTruthy();
|
||||
await user.click(recommendNavButton!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -7505,7 +7686,114 @@ 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 keeps cover while switching during a pending puzzle start', async () => {
|
||||
const user = userEvent.setup();
|
||||
const firstWork = {
|
||||
workId: 'puzzle-work-pending-next-1',
|
||||
profileId: 'puzzle-profile-pending-next-1',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'puzzle-session-pending-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-pending-next-2',
|
||||
profileId: 'puzzle-profile-pending-next-2',
|
||||
ownerUserId: 'user-3',
|
||||
sourceSessionId: 'puzzle-session-pending-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;
|
||||
let resolveFirstRun!: (value: { run: PuzzleRunSnapshot }) => void;
|
||||
|
||||
vi.mocked(listPuzzleGallery).mockResolvedValue({
|
||||
items: [firstWork, secondWork],
|
||||
});
|
||||
vi.mocked(getPuzzleGalleryDetail).mockImplementation(async (profileId) => ({
|
||||
item: profileId === secondWork.profileId ? secondWork : firstWork,
|
||||
}));
|
||||
vi.mocked(startPuzzleRun).mockImplementationOnce(
|
||||
(async () =>
|
||||
new Promise((resolve) => {
|
||||
resolveFirstRun = resolve;
|
||||
})) as typeof startPuzzleRun,
|
||||
);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
authValue={createAuthValue({
|
||||
user: null,
|
||||
canAccessProtectedData: false,
|
||||
openLoginModal: () => {},
|
||||
requireAuth: (action) => action(),
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: firstWork.profileId,
|
||||
levelId: null,
|
||||
},
|
||||
expect.objectContaining({
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: '下一个' }));
|
||||
|
||||
expect(
|
||||
screen.queryByText('作品暂时无法进入,请稍后再试。'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
await screen.findByLabelText('贝壳潮汐 作品信息', undefined, {
|
||||
timeout: 3000,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(startPuzzleRun).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
resolveFirstRun({
|
||||
run: buildMockPuzzleRun(firstWork.profileId, '后端拼图关卡'),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: secondWork.profileId,
|
||||
levelId: null,
|
||||
},
|
||||
expect.objectContaining({
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(
|
||||
screen.queryByText('作品暂时无法进入,请稍后再试。'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
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 +7835,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 +7874,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 +7931,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 () => {
|
||||
@@ -8331,10 +8606,84 @@ test('direct jump hop result route restores work detail by profile id', async ()
|
||||
expect(screen.queryByText('跳一跳草稿未恢复')).toBeNull();
|
||||
expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith(
|
||||
'jump-hop-profile-restore-1',
|
||||
{ audience: 'creation' },
|
||||
);
|
||||
expect(jumpHopClient.getSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('completed unpublished jump hop draft opens result page without starting runtime', async () => {
|
||||
const user = userEvent.setup();
|
||||
const work = buildMockJumpHopWork({
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-work-draft-ready-1',
|
||||
profileId: 'jump-hop-profile-draft-ready-1',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'jump-hop-session-draft-ready-1',
|
||||
themeText: '未发布跳一跳草稿',
|
||||
workTitle: '未发布跳一跳草稿',
|
||||
workDescription: '已经生成完成,但还没有发布。',
|
||||
themeTags: ['草稿'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-30T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
});
|
||||
vi.mocked(jumpHopClient.listWorks).mockResolvedValue({
|
||||
items: [work.summary],
|
||||
});
|
||||
vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValueOnce({
|
||||
item: work,
|
||||
} satisfies JumpHopWorkDetailResponse);
|
||||
vi.mocked(fetchCreationEntryConfig).mockResolvedValueOnce({
|
||||
...testCreationEntryConfig,
|
||||
creationTypes: [
|
||||
...testCreationEntryConfig.creationTypes,
|
||||
{
|
||||
id: 'jump-hop',
|
||||
title: '跳一跳',
|
||||
subtitle: '主题驱动平台跳跃',
|
||||
badge: '可创建',
|
||||
imageSrc: '/creation-type-references/jump-hop.webp',
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 55,
|
||||
categoryId: 'recommended',
|
||||
categoryLabel: '热门推荐',
|
||||
categorySortOrder: 20,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openDraftHub(user);
|
||||
const draftPanel = getPlatformTabPanel('saves');
|
||||
await user.click(
|
||||
await within(draftPanel).findByRole('button', {
|
||||
name: /继续创作《未发布跳一跳草稿》/u,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('未发布跳一跳草稿')).toBeTruthy();
|
||||
expect(jumpHopClient.getWorkDetail).toHaveBeenCalledWith(
|
||||
'jump-hop-profile-draft-ready-1',
|
||||
{ audience: 'creation' },
|
||||
);
|
||||
expect(jumpHopClient.startRun).not.toHaveBeenCalled();
|
||||
expect(window.location.pathname).toBe('/creation/jump-hop/result');
|
||||
expect(window.location.search).toContain(
|
||||
'profileId=jump-hop-profile-draft-ready-1',
|
||||
);
|
||||
});
|
||||
|
||||
test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user