merge: master into codex/bark-battle
This commit is contained in:
@@ -1561,6 +1561,65 @@ function buildMockPuzzleAgentSession(
|
||||
};
|
||||
}
|
||||
|
||||
function buildReadyPuzzleDraft(
|
||||
overrides: Partial<PuzzleResultDraft> = {},
|
||||
): PuzzleResultDraft {
|
||||
return {
|
||||
workTitle: '自动恢复拼图',
|
||||
workDescription: '前端断连后复读 session 恢复的拼图。',
|
||||
levelName: '雨夜猫街',
|
||||
summary: '屋檐下的猫与暖灯街角。',
|
||||
themeTags: ['猫咪', '雨夜', '拼图'],
|
||||
forbiddenDirectives: [],
|
||||
creatorIntent: null,
|
||||
anchorPack: buildPuzzleAnchorPack(),
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-1',
|
||||
imageSrc: '/puzzle/recovered-candidate.png',
|
||||
assetId: 'asset-1',
|
||||
prompt: '雨夜猫街',
|
||||
actualPrompt: null,
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-1',
|
||||
coverImageSrc: '/puzzle/recovered-candidate.png',
|
||||
coverAssetId: 'asset-1',
|
||||
generationStatus: 'ready',
|
||||
levels: [
|
||||
{
|
||||
levelId: 'puzzle-level-1',
|
||||
levelName: '雨夜猫街',
|
||||
pictureDescription: '屋檐下的猫与暖灯街角。',
|
||||
pictureReference: null,
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-1',
|
||||
imageSrc: '/puzzle/recovered-candidate.png',
|
||||
assetId: 'asset-1',
|
||||
prompt: '雨夜猫街',
|
||||
actualPrompt: null,
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-1',
|
||||
coverImageSrc: '/puzzle/recovered-candidate.png',
|
||||
coverAssetId: 'asset-1',
|
||||
uiBackgroundPrompt: '雨夜猫街竖屏纯背景',
|
||||
uiBackgroundImageSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-recovered/ui/background.png',
|
||||
uiBackgroundImageObjectKey:
|
||||
'generated-puzzle-assets/puzzle-session-recovered/ui/background.png',
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildClearedPuzzleRun(params: {
|
||||
runId: string;
|
||||
entryProfileId: string;
|
||||
@@ -1626,6 +1685,20 @@ function buildMockMatch3DRun(profileId: string): Match3DRunSnapshot {
|
||||
};
|
||||
}
|
||||
|
||||
const match3DGeneratedUiAsset = {
|
||||
prompt: '果园竖屏纯背景',
|
||||
imageSrc: '/generated-match3d-assets/session/profile/background/background.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/background/background.png',
|
||||
containerPrompt: '果园浅盘容器',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/container.png',
|
||||
containerImageObjectKey:
|
||||
'generated-match3d-assets/session/profile/ui-container/container.png',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
} satisfies NonNullable<Match3DWorkSummary['generatedBackgroundAsset']>;
|
||||
|
||||
function buildMockMatch3DAgentSession(
|
||||
overrides: Partial<Match3DAgentSessionSnapshot> = {},
|
||||
): Match3DAgentSessionSnapshot {
|
||||
@@ -3415,6 +3488,73 @@ test('running match3d persisted draft reopens progress instead of unfinished res
|
||||
);
|
||||
});
|
||||
|
||||
test('persisted generating match3d draft opens generation progress after refresh', async () => {
|
||||
const user = userEvent.setup();
|
||||
const persistedGeneratingWork: Match3DWorkSummary = {
|
||||
workId: 'match3d-work-generating',
|
||||
profileId: 'match3d-profile-generating',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'match3d-session-generating',
|
||||
gameName: '生成中抓鹅',
|
||||
themeText: '霓虹水果摊',
|
||||
summary: '刷新后仍应回到抓大鹅生成面板。',
|
||||
tags: ['水果', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-18T12:05:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: 'generating',
|
||||
generatedItemAssets: [],
|
||||
};
|
||||
|
||||
vi.mocked(listMatch3DWorks).mockResolvedValue({
|
||||
items: [persistedGeneratingWork],
|
||||
});
|
||||
vi.mocked(match3dCreationClient.getSession).mockResolvedValueOnce({
|
||||
session: buildMockMatch3DAgentSession({
|
||||
sessionId: 'match3d-session-generating',
|
||||
draft: {
|
||||
profileId: 'match3d-profile-generating',
|
||||
gameName: '生成中抓鹅',
|
||||
themeText: '霓虹水果摊',
|
||||
summary: '刷新后仍应回到抓大鹅生成面板。',
|
||||
tags: ['水果', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
generatedItemAssets: [],
|
||||
},
|
||||
stage: 'draft_ready',
|
||||
lastAssistantReply: '正在生成抓大鹅素材。',
|
||||
updatedAt: '2026-05-18T12:05:00.000Z',
|
||||
}),
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openDraftHub(user);
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: /继续创作《生成中抓鹅》/u }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(match3dCreationClient.getSession).toHaveBeenCalledWith(
|
||||
'match3d-session-generating',
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('抓大鹅草稿生成进度')).toBeTruthy();
|
||||
expect(screen.queryByText('抓大鹅结果页')).toBeNull();
|
||||
expect(getMatch3DWorkDetail).not.toHaveBeenCalledWith(
|
||||
'match3d-profile-generating',
|
||||
);
|
||||
});
|
||||
|
||||
test('running match3d form generation keeps other creation templates available', async () => {
|
||||
const user = userEvent.setup();
|
||||
const runningSession = buildMockMatch3DAgentSession({
|
||||
@@ -3885,6 +4025,7 @@ test('match3d result trial passes generated models into first runtime mount', as
|
||||
subscriptionKey: 'sub-strawberry',
|
||||
status: 'model_ready',
|
||||
error: null,
|
||||
backgroundAsset: match3DGeneratedUiAsset,
|
||||
},
|
||||
];
|
||||
const match3dDraftWork: Match3DWorkSummary = {
|
||||
@@ -4132,6 +4273,7 @@ test('match3d draft generation auto starts trial and runtime back opens draft re
|
||||
subscriptionKey: 'sub-strawberry',
|
||||
status: 'model_ready',
|
||||
error: null,
|
||||
backgroundAsset: match3DGeneratedUiAsset,
|
||||
},
|
||||
];
|
||||
const generatedSession = buildMockMatch3DAgentSession({
|
||||
@@ -4195,6 +4337,26 @@ test('match3d draft generation auto starts trial and runtime back opens draft re
|
||||
expect(
|
||||
await screen.findByTestId('match3d-runtime-generated-model-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-background-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
});
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
expect(
|
||||
match3dGeneratedModelCache.preloadMatch3DGeneratedRuntimeAssets,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.any(Array),
|
||||
expect.objectContaining({
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/background.png',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/container.png',
|
||||
}),
|
||||
{ expireSeconds: 300 },
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
@@ -4202,6 +4364,110 @@ test('match3d draft generation auto starts trial and runtime back opens draft re
|
||||
expect(screen.getByText('自动试玩抓大鹅')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('match3d result trial loads generated background and container assets', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
|
||||
{
|
||||
itemId: 'match3d-trial-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/items/match3d-trial-item-1-item/image.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/items/match3d-trial-item-1-item/image.png',
|
||||
imageViews: [],
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
modelFileName: null,
|
||||
taskUuid: null,
|
||||
subscriptionKey: null,
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
backgroundAsset: match3DGeneratedUiAsset,
|
||||
},
|
||||
];
|
||||
const match3dDraftWork: Match3DWorkSummary = {
|
||||
workId: 'match3d-work-trial-ui',
|
||||
profileId: 'match3d-profile-trial-ui',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'match3d-session-trial-ui',
|
||||
gameName: '手动试玩抓大鹅',
|
||||
themeText: '水果',
|
||||
summary: '',
|
||||
tags: ['水果', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-14T11:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generatedBackgroundAsset: null,
|
||||
generatedItemAssets,
|
||||
};
|
||||
|
||||
vi.mocked(listMatch3DWorks).mockResolvedValue({
|
||||
items: [match3dDraftWork],
|
||||
});
|
||||
vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
|
||||
session: buildMockMatch3DAgentSession({
|
||||
sessionId: 'match3d-session-trial-ui',
|
||||
stage: 'draft_ready',
|
||||
draft: {
|
||||
profileId: 'match3d-profile-trial-ui',
|
||||
gameName: '手动试玩抓大鹅',
|
||||
themeText: '水果',
|
||||
summary: '',
|
||||
tags: ['水果', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
generatedItemAssets,
|
||||
},
|
||||
}),
|
||||
});
|
||||
vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
|
||||
item: match3dDraftWork,
|
||||
});
|
||||
match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
|
||||
run: buildMockMatch3DRun(match3dDraftWork.profileId),
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openDraftHub(user);
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: /继续创作《手动试玩抓大鹅》/u }),
|
||||
);
|
||||
expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '试玩' }));
|
||||
|
||||
expect(await screen.findByText(/抓大鹅运行态/u)).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-background-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
});
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
expect(
|
||||
match3dGeneratedModelCache.preloadMatch3DGeneratedRuntimeAssets,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.any(Array),
|
||||
expect.objectContaining({
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/background/background.png',
|
||||
containerImageSrc:
|
||||
'/generated-match3d-assets/session/profile/ui-container/container.png',
|
||||
}),
|
||||
{ expireSeconds: 300 },
|
||||
);
|
||||
});
|
||||
|
||||
test('completed match3d draft notice first opens trial then reopens result', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
|
||||
@@ -4220,6 +4486,7 @@ test('completed match3d draft notice first opens trial then reopens result', asy
|
||||
subscriptionKey: 'sub-notice-strawberry',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
backgroundAsset: match3DGeneratedUiAsset,
|
||||
},
|
||||
];
|
||||
const runningSession = buildMockMatch3DAgentSession({
|
||||
@@ -4313,6 +4580,14 @@ test('completed match3d draft notice first opens trial then reopens result', asy
|
||||
expect(await screen.findByText(/抓大鹅运行态/u)).toBeTruthy();
|
||||
expect(screen.queryByText('抓大鹅草稿生成进度')).toBeNull();
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-background-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
});
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
@@ -4439,51 +4714,14 @@ test('completed baby object match draft shows unread marker after leaving genera
|
||||
|
||||
test('puzzle draft generation auto starts trial and runtime back opens draft result', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedDraft: PuzzleResultDraft = {
|
||||
const generatedDraft = buildReadyPuzzleDraft({
|
||||
workTitle: '自动试玩拼图',
|
||||
workDescription: '生成完成后直接试玩。',
|
||||
levelName: '雨夜猫街',
|
||||
summary: '屋檐下的猫与暖灯街角。',
|
||||
themeTags: ['猫咪', '雨夜', '拼图'],
|
||||
forbiddenDirectives: [],
|
||||
creatorIntent: null,
|
||||
anchorPack: buildPuzzleAnchorPack(),
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-1',
|
||||
imageSrc: '/puzzle/auto-candidate.png',
|
||||
assetId: 'asset-1',
|
||||
prompt: '雨夜猫街',
|
||||
actualPrompt: null,
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-1',
|
||||
coverImageSrc: '/puzzle/auto-candidate.png',
|
||||
coverAssetId: 'asset-1',
|
||||
generationStatus: 'ready',
|
||||
levels: [
|
||||
{
|
||||
levelId: 'puzzle-level-1',
|
||||
levelName: '雨夜猫街',
|
||||
pictureDescription: '屋檐下的猫与暖灯街角。',
|
||||
pictureReference: null,
|
||||
candidates: [
|
||||
{
|
||||
candidateId: 'candidate-1',
|
||||
imageSrc: '/puzzle/auto-candidate.png',
|
||||
assetId: 'asset-1',
|
||||
prompt: '雨夜猫街',
|
||||
actualPrompt: null,
|
||||
sourceType: 'generated',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
selectedCandidateId: 'candidate-1',
|
||||
...buildReadyPuzzleDraft().levels![0]!,
|
||||
coverImageSrc: '/puzzle/auto-candidate.png',
|
||||
coverAssetId: 'asset-1',
|
||||
uiBackgroundPrompt: '水果乐园竖屏纯背景',
|
||||
uiBackgroundImageSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
|
||||
uiBackgroundImageObjectKey:
|
||||
@@ -4499,10 +4737,9 @@ test('puzzle draft generation auto starts trial and runtime back opens draft res
|
||||
title: '水果乐园',
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
},
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
const generatedSession: PuzzleAgentSessionSnapshot = {
|
||||
sessionId: 'puzzle-session-auto-1',
|
||||
seedText: '屋檐下的猫与暖灯街角。',
|
||||
@@ -4586,6 +4823,63 @@ test('puzzle draft generation auto starts trial and runtime back opens draft res
|
||||
expect(screen.getByDisplayValue('雨夜猫街')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('embedded puzzle form recovers when compile request times out after backend completion', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedDraft = buildReadyPuzzleDraft();
|
||||
const generatedSession = buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-session-recovered',
|
||||
stage: 'ready_to_publish',
|
||||
progressPercent: 100,
|
||||
draft: generatedDraft,
|
||||
lastAssistantReply: '拼图草稿已经生成。',
|
||||
resultPreview: {
|
||||
draft: generatedDraft,
|
||||
publishReady: true,
|
||||
blockers: [],
|
||||
qualityFindings: [],
|
||||
},
|
||||
updatedAt: '2026-05-12T10:00:00.000Z',
|
||||
});
|
||||
|
||||
vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
|
||||
session: buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-session-recovered',
|
||||
}),
|
||||
});
|
||||
vi.mocked(executePuzzleAgentAction).mockRejectedValueOnce(
|
||||
Object.assign(new Error('请求超时:90000ms'), {
|
||||
name: 'TimeoutError',
|
||||
}),
|
||||
);
|
||||
vi.mocked(getPuzzleAgentSession).mockResolvedValueOnce({
|
||||
session: generatedSession,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('button', { name: '生成草稿' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getPuzzleAgentSession).toHaveBeenCalledWith(
|
||||
'puzzle-session-recovered',
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(updatePuzzleWork).toHaveBeenCalledWith(
|
||||
'puzzle-profile-recovered',
|
||||
expect.objectContaining({
|
||||
levelName: '雨夜猫街',
|
||||
coverImageSrc: '/puzzle/recovered-candidate.png',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(screen.queryByText('执行拼图操作失败。')).toBeNull();
|
||||
expect(screen.queryByText('请求超时:90000ms')).toBeNull();
|
||||
expect(screen.queryByText('拼图草稿生成进度')).toBeNull();
|
||||
expect(startLocalPuzzleRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('embedded puzzle form routes through requireAuth while logged out', async () => {
|
||||
const user = userEvent.setup();
|
||||
const requireAuth = vi.fn();
|
||||
@@ -5657,6 +5951,12 @@ test('home recommendation Match3D runtime keeps image, music and UI assets witho
|
||||
'textContent',
|
||||
'1',
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-background-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
});
|
||||
|
||||
test('home recommendation Match3D runtime passes top-level UI background assets', async () => {
|
||||
@@ -5830,6 +6130,12 @@ test('home recommendation Match3D runtime reloads detail when card only has UI a
|
||||
expect(
|
||||
await screen.findByTestId('match3d-runtime-generated-item-image-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-background-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
expect(
|
||||
screen.getByTestId('match3d-runtime-top-level-container-ui-count'),
|
||||
).toHaveProperty('textContent', '1');
|
||||
});
|
||||
|
||||
test('home recommendation surfaces start failure instead of staying in loading state', async () => {
|
||||
@@ -6227,6 +6533,59 @@ test('puzzle draft result back button returns to creation hub', async () => {
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
});
|
||||
|
||||
test('persisted generating puzzle draft opens generation progress after refresh', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listPuzzleWorks).mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
workId: 'puzzle-work-session-generating',
|
||||
profileId: 'puzzle-profile-session-generating',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'puzzle-session-generating',
|
||||
authorDisplayName: '测试玩家',
|
||||
workTitle: '生成中拼图',
|
||||
workDescription: '刷新后仍应回到生成面板。',
|
||||
levelName: '生成中拼图',
|
||||
summary: '刷新后仍应回到生成面板。',
|
||||
themeTags: ['雨夜'],
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
publicationStatus: 'draft',
|
||||
updatedAt: '2026-05-18T12:00:00.000Z',
|
||||
publishedAt: null,
|
||||
playCount: 0,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
publishReady: false,
|
||||
generationStatus: 'generating',
|
||||
},
|
||||
],
|
||||
});
|
||||
vi.mocked(getPuzzleAgentSession).mockResolvedValueOnce({
|
||||
session: buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-session-generating',
|
||||
stage: 'collecting_anchors',
|
||||
progressPercent: 42,
|
||||
lastAssistantReply: '正在生成拼图草稿。',
|
||||
updatedAt: '2026-05-18T12:00:00.000Z',
|
||||
}),
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openDraftHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: /继续创作/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getPuzzleAgentSession).toHaveBeenCalledWith(
|
||||
'puzzle-session-generating',
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('拼图草稿生成进度')).toBeTruthy();
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
});
|
||||
|
||||
test('published puzzle work card restores its source session for editing', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
@@ -387,16 +387,24 @@ vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
|
||||
vi.mock('../ResolvedAssetImage', () => ({
|
||||
ResolvedAssetImage: ({
|
||||
src,
|
||||
fallbackSrc,
|
||||
alt,
|
||||
className,
|
||||
...rest
|
||||
}: {
|
||||
src?: string | null;
|
||||
fallbackSrc?: string | null;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
}) =>
|
||||
src ? (
|
||||
<img src={src} alt={alt ?? ''} className={className} {...rest} />
|
||||
<img
|
||||
src={src}
|
||||
data-fallback-src={fallbackSrc ?? undefined}
|
||||
alt={alt ?? ''}
|
||||
className={className}
|
||||
{...rest}
|
||||
/>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
@@ -2901,6 +2909,36 @@ 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 () => {
|
||||
renderStatefulLoggedOutHomeView({
|
||||
latestEntries: [
|
||||
{
|
||||
...puzzlePublicEntry,
|
||||
coverImageSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-1/cover/image.png',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const discoverPanel = document.getElementById('platform-tab-panel-category');
|
||||
if (!discoverPanel) {
|
||||
throw new Error('缺少发现面板');
|
||||
}
|
||||
|
||||
const card = within(discoverPanel).getByRole('button', { name: /奇幻拼图/u });
|
||||
const cover = card.querySelector('.platform-public-work-card__cover');
|
||||
const image = within(card).getByRole('img');
|
||||
|
||||
expect(cover).toBeTruthy();
|
||||
expect(cover?.className).toContain('platform-public-work-card__cover');
|
||||
expect(image.getAttribute('src')).toBe(
|
||||
'/generated-puzzle-assets/puzzle-session-1/cover/image.png',
|
||||
);
|
||||
expect(image.getAttribute('data-fallback-src')).toBe(
|
||||
'/creation-type-references/puzzle.webp',
|
||||
);
|
||||
});
|
||||
|
||||
test('mobile today channel only shows newly published works from today', async () => {
|
||||
const user = userEvent.setup();
|
||||
const now = new Date();
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
UserRound,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import QRCode from 'qrcode';
|
||||
import {
|
||||
type ComponentType,
|
||||
type CSSProperties,
|
||||
@@ -42,7 +43,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
import communityQqQrImage from '../../../media/social-media-group/qq.png';
|
||||
import communityWechatQrImage from '../../../media/social-media-group/wechat.png';
|
||||
@@ -58,13 +58,13 @@ import type {
|
||||
ProfileRechargeCenterResponse,
|
||||
ProfileRechargeProduct,
|
||||
ProfileReferralInviteCenterResponse,
|
||||
WechatNativePayment,
|
||||
ProfileSaveArchiveSummary,
|
||||
ProfileTaskCenterResponse,
|
||||
ProfileTaskItem,
|
||||
ProfileWalletLedgerResponse,
|
||||
RedeemProfileRewardCodeResponse,
|
||||
WechatMiniProgramPayParams,
|
||||
WechatNativePayment,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
||||
@@ -137,6 +137,7 @@ import {
|
||||
resolvePlatformPublicWorkCode,
|
||||
resolvePlatformWorldCoverImage,
|
||||
resolvePlatformWorldCoverSlides,
|
||||
resolvePlatformWorldFallbackCoverImage,
|
||||
resolvePlatformWorldLeadPortrait,
|
||||
} from './rpgEntryWorldPresentation';
|
||||
|
||||
@@ -392,11 +393,13 @@ function usePlatformDesktopLayout() {
|
||||
|
||||
function ResolvedAssetBackdrop({
|
||||
src,
|
||||
fallbackSrc,
|
||||
alt,
|
||||
className,
|
||||
ariaHidden = false,
|
||||
}: {
|
||||
src?: string | null;
|
||||
fallbackSrc?: string | null;
|
||||
alt: string;
|
||||
className: string;
|
||||
ariaHidden?: boolean;
|
||||
@@ -404,6 +407,7 @@ function ResolvedAssetBackdrop({
|
||||
return (
|
||||
<ResolvedAssetImage
|
||||
src={src}
|
||||
fallbackSrc={fallbackSrc}
|
||||
alt={alt}
|
||||
aria-hidden={ariaHidden}
|
||||
className={className}
|
||||
@@ -522,6 +526,7 @@ function WorldCard({
|
||||
variant?: 'standard' | 'immersive';
|
||||
}) {
|
||||
const fallbackCoverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const fallbackAssetCoverImage = resolvePlatformWorldFallbackCoverImage(entry);
|
||||
const coverSlides = useMemo(() => {
|
||||
if (!enableCoverCarousel) {
|
||||
return fallbackCoverImage
|
||||
@@ -606,6 +611,7 @@ function WorldCard({
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
fallbackSrc={fallbackAssetCoverImage}
|
||||
alt={entry.worldName}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
@@ -692,6 +698,7 @@ function RecommendCoverOnlyCard({
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const fallbackCoverImage = resolvePlatformWorldFallbackCoverImage(entry);
|
||||
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
||||
const typeLabel = describePublicGalleryCardKind(entry);
|
||||
const authorName = entry.authorDisplayName.trim() || '玩家';
|
||||
@@ -708,6 +715,7 @@ function RecommendCoverOnlyCard({
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
fallbackSrc={fallbackCoverImage}
|
||||
alt={entry.worldName}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
mapBabyObjectMatchDraftToPlatformGalleryCard,
|
||||
mapVisualNovelWorkToPlatformGalleryCard,
|
||||
type PlatformEdutainmentGalleryCard,
|
||||
type PlatformPuzzleGalleryCard,
|
||||
resolvePlatformPublicWorkCode,
|
||||
resolvePlatformWorldFallbackCoverImage,
|
||||
} from './rpgEntryWorldPresentation';
|
||||
|
||||
test('formatPlatformWorldTime formats backend seconds timestamp text as date', () => {
|
||||
@@ -46,6 +48,32 @@ test('platform work display text limits names and tags by character count', () =
|
||||
).toEqual(['超长机关', '星桥']);
|
||||
});
|
||||
|
||||
test('platform public cards use play type reference images as cover fallback', () => {
|
||||
const puzzleCard: PlatformPuzzleGalleryCard = {
|
||||
sourceType: 'puzzle',
|
||||
workId: 'puzzle-work-1',
|
||||
profileId: 'puzzle-profile-1',
|
||||
publicWorkCode: 'PZ-PUZZLE1',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '玩家',
|
||||
worldName: '机关拼图',
|
||||
subtitle: '拼图关卡',
|
||||
summaryText: '公开作品',
|
||||
coverImageSrc: '/generated-puzzle-assets/session/cover/image.png',
|
||||
themeTags: ['拼图'],
|
||||
playCount: 1,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
visibility: 'published',
|
||||
publishedAt: '2026-05-18T00:00:00.000Z',
|
||||
updatedAt: '2026-05-18T00:00:00.000Z',
|
||||
};
|
||||
|
||||
expect(resolvePlatformWorldFallbackCoverImage(puzzleCard)).toBe(
|
||||
'/creation-type-references/puzzle.webp',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildPuzzleWorkCoverSlides prefers each level formal image', () => {
|
||||
const slides = buildPuzzleWorkCoverSlides({
|
||||
workId: 'work-1',
|
||||
|
||||
@@ -446,6 +446,36 @@ export function resolvePlatformWorldCoverImage(entry: PlatformWorldCardLike) {
|
||||
return '';
|
||||
}
|
||||
|
||||
export function resolvePlatformWorldFallbackCoverImage(
|
||||
entry: PlatformWorldCardLike,
|
||||
) {
|
||||
if (isPuzzleGalleryEntry(entry)) {
|
||||
return '/creation-type-references/puzzle.webp';
|
||||
}
|
||||
|
||||
if (isMatch3DGalleryEntry(entry)) {
|
||||
return '/creation-type-references/match3d.webp';
|
||||
}
|
||||
|
||||
if (isSquareHoleGalleryEntry(entry)) {
|
||||
return '/creation-type-references/square-hole.webp';
|
||||
}
|
||||
|
||||
if (isVisualNovelGalleryEntry(entry)) {
|
||||
return '/creation-type-references/visual-novel.webp';
|
||||
}
|
||||
|
||||
if (isBigFishGalleryEntry(entry)) {
|
||||
return '/creation-type-references/big-fish.webp';
|
||||
}
|
||||
|
||||
if (isEdutainmentGalleryEntry(entry)) {
|
||||
return '/creation-type-references/creative-agent.webp';
|
||||
}
|
||||
|
||||
return '/creation-type-references/rpg.webp';
|
||||
}
|
||||
|
||||
export function resolvePlatformWorldCoverSlides(
|
||||
entry: PlatformWorldCardLike,
|
||||
): PlatformPuzzleCoverSlide[] {
|
||||
|
||||
Reference in New Issue
Block a user