1
This commit is contained in:
@@ -257,3 +257,70 @@ test('creation hub published work spans full mobile row', () => {
|
||||
expect(html).toContain('col-span-2 sm:col-span-1');
|
||||
expect(html).not.toContain('grid-cols-1 gap-3 md:grid-cols-2');
|
||||
});
|
||||
|
||||
test('creation hub draft cards use cover background and hide updated time', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
mode="works-only"
|
||||
items={[]}
|
||||
puzzleItems={[
|
||||
{
|
||||
workId: 'puzzle:old-draft',
|
||||
profileId: 'puzzle-profile-old',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试作者',
|
||||
workTitle: '旧草稿',
|
||||
workDescription: '先前修改的拼图草稿。',
|
||||
levelName: '旧草稿',
|
||||
summary: '先前修改的拼图草稿。',
|
||||
themeTags: [],
|
||||
coverImageSrc: '/covers/old-draft.webp',
|
||||
publicationStatus: 'draft',
|
||||
updatedAt: '2026-05-07T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
},
|
||||
{
|
||||
workId: 'puzzle:new-draft',
|
||||
profileId: 'puzzle-profile-new',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试作者',
|
||||
workTitle: '新草稿',
|
||||
workDescription: '最近修改的拼图草稿。',
|
||||
levelName: '新草稿',
|
||||
summary: '最近修改的拼图草稿。',
|
||||
themeTags: [],
|
||||
coverImageSrc: '/covers/new-draft.webp',
|
||||
publicationStatus: 'draft',
|
||||
updatedAt: '1778457601.234567Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
},
|
||||
]}
|
||||
loading={false}
|
||||
error={null}
|
||||
onRetry={() => {}}
|
||||
onCreateType={noopCreateType}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
onOpenPuzzleDetail={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const newerIndex = html.indexOf('新草稿');
|
||||
const olderIndex = html.indexOf('旧草稿');
|
||||
|
||||
expect(newerIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(olderIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(newerIndex).toBeLessThan(olderIndex);
|
||||
expect(html).toContain(
|
||||
'class="absolute inset-0 h-full w-full object-cover" src="/covers/new-draft.webp"',
|
||||
);
|
||||
expect(html).toContain('src="/covers/new-draft.webp"');
|
||||
expect(html).not.toContain('1778457601.234567Z');
|
||||
expect(html).not.toContain('2026-05-07');
|
||||
expect(html).not.toContain('更新于');
|
||||
expect(html).not.toContain('最后修改');
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import { buildCreationWorkShelfItems } from './creationWorkShelf';
|
||||
import {
|
||||
buildCreationWorkShelfItems,
|
||||
getCreationWorkShelfItemTime,
|
||||
} from './creationWorkShelf';
|
||||
|
||||
test('buildCreationWorkShelfItems maps visual novel items with VN public code', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
@@ -141,3 +144,54 @@ test('buildCreationWorkShelfItems maps baby object match local drafts', () => {
|
||||
expect(items[1]?.status).toBe('draft');
|
||||
expect(items[1]?.publicWorkCode).toBeNull();
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems sorts works by latest updatedAt across timestamp formats', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [
|
||||
{
|
||||
workId: 'puzzle:older',
|
||||
profileId: 'puzzle-profile-older',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试作者',
|
||||
levelName: '旧草稿',
|
||||
summary: '较早修改。',
|
||||
themeTags: [],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
updatedAt: '2026-05-07T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
},
|
||||
{
|
||||
workId: 'puzzle:newer',
|
||||
profileId: 'puzzle-profile-newer',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试作者',
|
||||
levelName: '新草稿',
|
||||
summary: '较晚修改。',
|
||||
themeTags: [],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
updatedAt: '1778457601.234567Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(items.map((item) => item.id)).toEqual([
|
||||
'puzzle:newer',
|
||||
'puzzle:older',
|
||||
]);
|
||||
});
|
||||
|
||||
test('getCreationWorkShelfItemTime parses backend seconds.microsZ values', () => {
|
||||
expect(getCreationWorkShelfItemTime('1778457601.234567Z')).toBe(
|
||||
1778457601234.567,
|
||||
);
|
||||
expect(getCreationWorkShelfItemTime('2026-05-07T00:00:00.000Z')).toBe(
|
||||
new Date('2026-05-07T00:00:00.000Z').getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -240,7 +240,8 @@ export function buildCreationWorkShelfItems(params: {
|
||||
})
|
||||
.sort(
|
||||
(left, right) =>
|
||||
getShelfItemTime(right.updatedAt) - getShelfItemTime(left.updatedAt),
|
||||
getCreationWorkShelfItemTime(right.updatedAt) -
|
||||
getCreationWorkShelfItemTime(left.updatedAt),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -731,7 +732,25 @@ function buildStatusBadge(
|
||||
};
|
||||
}
|
||||
|
||||
function getShelfItemTime(value: string) {
|
||||
const timestamp = new Date(value).getTime();
|
||||
export function getCreationWorkShelfItemTime(value: string) {
|
||||
const normalized = value.trim();
|
||||
const numericTimestamp = normalized.match(/^(-?\d+(?:\.\d+)?)(?:Z)?$/u);
|
||||
if (numericTimestamp?.[1]) {
|
||||
const rawTimestamp = Number(numericTimestamp[1]);
|
||||
if (Number.isFinite(rawTimestamp)) {
|
||||
const absoluteTimestamp = Math.abs(rawTimestamp);
|
||||
if (absoluteTimestamp >= 1_000_000_000_000_000) {
|
||||
return rawTimestamp / 1000;
|
||||
}
|
||||
if (absoluteTimestamp >= 1_000_000_000_000) {
|
||||
return rawTimestamp;
|
||||
}
|
||||
if (absoluteTimestamp >= 1_000_000_000) {
|
||||
return rawTimestamp * 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = new Date(normalized).getTime();
|
||||
return Number.isNaN(timestamp) ? 0 : timestamp;
|
||||
}
|
||||
|
||||
@@ -52,42 +52,42 @@ const MATCH3D_ASSET_STYLE_OPTIONS = [
|
||||
label: '扁平图标',
|
||||
imageSrc: '/match3d-style-references/flat-icon.png',
|
||||
prompt:
|
||||
'干净扁平的 2D 游戏道具图标风格,正面视角,色块清楚,边缘硬朗,高可读性,适合移动端休闲游戏素材。',
|
||||
'干净扁平的2D游戏道具图标风格,正面视角,色块清楚,边缘硬朗。',
|
||||
},
|
||||
{
|
||||
id: 'cel-cartoon',
|
||||
label: '赛璐璐卡通',
|
||||
imageSrc: '/match3d-style-references/cel-cartoon.png',
|
||||
prompt:
|
||||
'明亮赛璐璐卡通 2D 游戏道具风格,清晰线稿,硬边阴影,饱和配色,轮廓醒目。',
|
||||
'明亮赛璐璐卡通2D游戏道具风格,清晰线稿,硬边阴影,饱和配色,轮廓醒目。',
|
||||
},
|
||||
{
|
||||
id: 'pixel-retro',
|
||||
label: '像素复古',
|
||||
label: '像素',
|
||||
imageSrc: '/match3d-style-references/pixel-retro.png',
|
||||
prompt:
|
||||
'真正复古像素 2D 游戏道具 sprite 风格,先以约 64x64 低分辨率像素块绘制再按整数倍放大,硬边方块像素清晰可见,有限色板 12-24 色,禁止抗锯齿、柔焦、平滑渐变、真实 3D 渲染、PBR 材质和摄影光照。',
|
||||
'像素2D游戏道具sprite风格',
|
||||
},
|
||||
{
|
||||
id: 'watercolor',
|
||||
label: '手绘水彩',
|
||||
imageSrc: '/match3d-style-references/watercolor.png',
|
||||
prompt:
|
||||
'手绘水彩 2D 道具素材风格,柔和纸张纹理,透明叠色,边缘轻微晕染,主体仍保持清楚可读。',
|
||||
'手绘水彩2D道具素材风格',
|
||||
},
|
||||
{
|
||||
id: 'sticker-outline',
|
||||
label: '贴纸描边',
|
||||
imageSrc: '/match3d-style-references/sticker-outline.png',
|
||||
prompt:
|
||||
'贴纸描边 2D 游戏道具素材风格,粗白边与深色外轮廓,柔和投影,色彩活泼,适合休闲消除游戏。',
|
||||
'贴纸描边2D游戏道具素材风格,粗白边与深色外轮廓',
|
||||
},
|
||||
{
|
||||
id: 'painterly-icon',
|
||||
label: '厚涂图标',
|
||||
imageSrc: '/match3d-style-references/painterly-icon.png',
|
||||
prompt:
|
||||
'厚涂 2D 游戏道具图标风格,笔触细腻,体积光影明确,中心构图,保持图标级清晰剪影。',
|
||||
'厚涂2D游戏道具图标风格,笔触细腻,体积光影明确,中心构图,保持图标级清晰剪影。',
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
|
||||
@@ -1414,4 +1414,73 @@ describe('Match3DResultView', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('背景音乐在非首个素材时仍显示并进入试玩 profile', async () => {
|
||||
const onStartTestRun = vi.fn();
|
||||
const generatedItemAssets = [
|
||||
createReadyGeneratedItemAsset(1),
|
||||
{
|
||||
...createReadyGeneratedItemAsset(2),
|
||||
backgroundMusicTitle: '漂浮船歌',
|
||||
backgroundMusicStyle: '轻快, 愉悦, 现代',
|
||||
backgroundMusicPrompt: '',
|
||||
backgroundMusic: {
|
||||
taskId: 'music-task-2',
|
||||
provider: 'vector-engine-suno',
|
||||
assetObjectId: 'asset-music-2',
|
||||
assetKind: 'match3d_background_music',
|
||||
audioSrc: '/generated-match3d-assets/audio/floating-song.mp3',
|
||||
prompt: '',
|
||||
title: '漂浮船歌',
|
||||
updatedAt: '2026-05-14T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
];
|
||||
const profile = createProfile({ generatedItemAssets });
|
||||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||||
item: createProfile({ generatedItemAssets: [] }),
|
||||
});
|
||||
vi.mocked(
|
||||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||||
).mockResolvedValue({
|
||||
item: createProfile({ generatedItemAssets }),
|
||||
});
|
||||
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={profile}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={onStartTestRun}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '背景音乐' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('抓大鹅背景音乐').getAttribute('src')).toBe(
|
||||
'https://signed.example.com/generated-match3d-assets/audio/floating-song.mp3',
|
||||
);
|
||||
});
|
||||
expect(screen.queryByText('暂无音乐')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onStartTestRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
generatedItemAssets: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
itemId: 'match3d-item-1',
|
||||
backgroundMusic: expect.objectContaining({
|
||||
audioSrc:
|
||||
'/generated-match3d-assets/audio/floating-song.mp3',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({ itemTypeCountOverride: 2 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
} from '../../services/match3d-works';
|
||||
import {
|
||||
getMatch3DGeneratedImageViewSources,
|
||||
mergeMatch3DGeneratedItemAssetsForRuntime,
|
||||
normalizeMatch3DGeneratedItemAssetsForRuntime,
|
||||
resolveMatch3DGeneratedImageAssetSource,
|
||||
resolveMatch3DGeneratedModelAssetSource,
|
||||
} from '../../services/match3dGeneratedModelCache';
|
||||
@@ -768,7 +770,7 @@ function createGeneratedAssetsFromDrafts(
|
||||
asset.backgroundMusicStyle ?? existing?.backgroundMusicStyle ?? null,
|
||||
backgroundMusicPrompt:
|
||||
asset.backgroundMusicPrompt ?? existing?.backgroundMusicPrompt ?? null,
|
||||
backgroundMusic: asset.backgroundMusic,
|
||||
backgroundMusic: asset.backgroundMusic ?? existing?.backgroundMusic ?? null,
|
||||
clickSound: asset.clickSound,
|
||||
backgroundAsset:
|
||||
asset.backgroundAsset ??
|
||||
@@ -1118,27 +1120,23 @@ function resolveMatch3DResultGeneratedItemAssets(
|
||||
const profileAssets = profile.generatedItemAssets ?? [];
|
||||
const draftAssets = draft?.generatedItemAssets ?? [];
|
||||
if (draftAssets.length <= 0) {
|
||||
return profileAssets;
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(profileAssets);
|
||||
}
|
||||
if (profileAssets.length <= 0) {
|
||||
return draftAssets;
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(draftAssets);
|
||||
}
|
||||
|
||||
const profileAssetsById = new Map(
|
||||
profileAssets.map((asset) => [asset.itemId, asset]),
|
||||
return mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
draftAssets.map((draftAsset) => {
|
||||
const profileAsset = profileAssets.find(
|
||||
(asset) => asset.itemId === draftAsset.itemId,
|
||||
);
|
||||
return profileAsset
|
||||
? mergeMatch3DGeneratedItemAsset(draftAsset, profileAsset)
|
||||
: draftAsset;
|
||||
}),
|
||||
profileAssets,
|
||||
);
|
||||
const mergedAssets = draftAssets.map((draftAsset) => {
|
||||
const profileAsset = profileAssetsById.get(draftAsset.itemId);
|
||||
return profileAsset
|
||||
? mergeMatch3DGeneratedItemAsset(draftAsset, profileAsset)
|
||||
: draftAsset;
|
||||
});
|
||||
for (const profileAsset of profileAssets) {
|
||||
if (!mergedAssets.some((asset) => asset.itemId === profileAsset.itemId)) {
|
||||
mergedAssets.push(profileAsset);
|
||||
}
|
||||
}
|
||||
return mergedAssets;
|
||||
}
|
||||
|
||||
function attachMatch3DGeneratedItemAssets(
|
||||
@@ -1152,7 +1150,8 @@ function attachMatch3DGeneratedItemAssets(
|
||||
// 中文注释:试玩入口依赖当前页面可见的生成素材;保存接口若返回旧快照,不能把素材从运行态入参里丢掉。
|
||||
return {
|
||||
...profile,
|
||||
generatedItemAssets: [...generatedItemAssets],
|
||||
generatedItemAssets:
|
||||
normalizeMatch3DGeneratedItemAssetsForRuntime(generatedItemAssets),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1177,10 +1176,12 @@ function buildPersistableGeneratedItemAssets(
|
||||
return [];
|
||||
}
|
||||
|
||||
return createGeneratedAssetsFromDrafts(
|
||||
assetDrafts,
|
||||
generatedItemAssets,
|
||||
).filter(hasPersistableMatch3DGeneratedItemAsset);
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
createGeneratedAssetsFromDrafts(
|
||||
assetDrafts,
|
||||
generatedItemAssets,
|
||||
).filter(hasPersistableMatch3DGeneratedItemAsset),
|
||||
);
|
||||
}
|
||||
|
||||
function Match3DResultHeader({
|
||||
|
||||
@@ -78,6 +78,7 @@ afterEach(() => {
|
||||
__MATCH3D_KEEP_3D_TEST_RENDER__?: boolean;
|
||||
}
|
||||
).__MATCH3D_KEEP_3D_TEST_RENDER__;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function renderRuntime(
|
||||
@@ -475,6 +476,74 @@ test('运行态会换签并渲染抓大鹅中心容器 UI 图', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('运行态从任意素材读取作品级背景音乐并换签播放', async () => {
|
||||
const run = startLocalMatch3DRun(3);
|
||||
const playSpy = vi
|
||||
.spyOn(HTMLMediaElement.prototype, 'play')
|
||||
.mockResolvedValue(undefined);
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation((input) => {
|
||||
const url = String(input);
|
||||
const signedUrl = url.includes('legacyPublicPath')
|
||||
? 'https://oss.example.com/match3d-music.mp3'
|
||||
: 'https://oss.example.com/match3d-view.png';
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
read: {
|
||||
signedUrl,
|
||||
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
const generatedItemAssets: Match3DGeneratedItemAsset[] = [
|
||||
{
|
||||
itemId: 'match3d-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc: '/match3d/strawberry.png',
|
||||
imageObjectKey: null,
|
||||
imageViews: [],
|
||||
status: 'image_ready',
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
},
|
||||
{
|
||||
itemId: 'match3d-item-2',
|
||||
itemName: '苹果',
|
||||
imageSrc: '/match3d/apple.png',
|
||||
imageObjectKey: null,
|
||||
imageViews: [],
|
||||
status: 'image_ready',
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
backgroundMusic: {
|
||||
taskId: 'music-task-1',
|
||||
provider: 'vector-engine-suno',
|
||||
assetObjectId: 'asset-music-1',
|
||||
assetKind: 'match3d_background_music',
|
||||
audioSrc: '/generated-match3d-assets/audio/music.mp3',
|
||||
prompt: '',
|
||||
title: '果园轻舞',
|
||||
updatedAt: '2026-05-14T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
renderRuntime(run, generatedItemAssets);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('抓大鹅背景音乐').getAttribute('src')).toBe(
|
||||
'https://oss.example.com/match3d-music.mp3',
|
||||
);
|
||||
});
|
||||
await waitFor(() => expect(playSpy).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
test('本地试玩按难度档位生成类型并兼容历史硬核消除数', () => {
|
||||
const smallRun = startLocalMatch3DRun(12);
|
||||
const hardRun = startLocalMatch3DRun(20);
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from '../../services/assetReadUrlService';
|
||||
import {
|
||||
getMatch3DGeneratedImageViewSources,
|
||||
normalizeMatch3DGeneratedItemAssetsForRuntime,
|
||||
} from '../../services/match3dGeneratedModelCache';
|
||||
import {
|
||||
DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG,
|
||||
@@ -481,6 +482,10 @@ export function Match3DRuntimeShell({
|
||||
useState('');
|
||||
const musicVolume = authUi?.musicVolume ?? DEFAULT_MATCH3D_MUSIC_VOLUME;
|
||||
const levelAudioConfig = DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG;
|
||||
const runtimeGeneratedItemAssets = useMemo(
|
||||
() => normalizeMatch3DGeneratedItemAssetsForRuntime(generatedItemAssets),
|
||||
[generatedItemAssets],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeLeftMs(run?.remainingMs ?? 0);
|
||||
@@ -559,7 +564,7 @@ export function Match3DRuntimeShell({
|
||||
|
||||
const backgroundAssetSrc =
|
||||
backgroundImageSrc?.trim() ||
|
||||
generatedItemAssets
|
||||
runtimeGeneratedItemAssets
|
||||
.map(
|
||||
(asset) =>
|
||||
asset.backgroundAsset?.imageSrc?.trim() ||
|
||||
@@ -569,7 +574,7 @@ export function Match3DRuntimeShell({
|
||||
.find(Boolean) ||
|
||||
'';
|
||||
const containerAssetSrc =
|
||||
generatedItemAssets
|
||||
runtimeGeneratedItemAssets
|
||||
.map(
|
||||
(asset) =>
|
||||
asset.backgroundAsset?.containerImageSrc?.trim() ||
|
||||
@@ -578,8 +583,8 @@ export function Match3DRuntimeShell({
|
||||
)
|
||||
.find(Boolean) || '';
|
||||
const imageSourcesByType = useMemo(
|
||||
() => buildMatch3DImageSourcesByType(run, generatedItemAssets),
|
||||
[generatedItemAssets, run],
|
||||
() => buildMatch3DImageSourcesByType(run, runtimeGeneratedItemAssets),
|
||||
[runtimeGeneratedItemAssets, run],
|
||||
);
|
||||
const imageSourceSignature = useMemo(
|
||||
() => buildMatch3DImageSourceSignature(imageSourcesByType),
|
||||
@@ -597,7 +602,7 @@ export function Match3DRuntimeShell({
|
||||
[imageSourcesByType, resolvedImageSources],
|
||||
);
|
||||
const backgroundMusicSrc =
|
||||
generatedItemAssets.find((asset) => asset.backgroundMusic?.audioSrc)
|
||||
runtimeGeneratedItemAssets.find((asset) => asset.backgroundMusic?.audioSrc)
|
||||
?.backgroundMusic?.audioSrc ?? null;
|
||||
const [resolvedBackgroundMusicSrc, setResolvedBackgroundMusicSrc] = useState('');
|
||||
const [resolvedContainerImageSrc, setResolvedContainerImageSrc] = useState('');
|
||||
@@ -605,7 +610,7 @@ export function Match3DRuntimeShell({
|
||||
if (!run) {
|
||||
return new Map<string, string>();
|
||||
}
|
||||
const readyAssets = generatedItemAssets.filter(
|
||||
const readyAssets = runtimeGeneratedItemAssets.filter(
|
||||
(asset) => asset.clickSound?.audioSrc,
|
||||
);
|
||||
const sortedTypes = [
|
||||
@@ -617,7 +622,7 @@ export function Match3DRuntimeShell({
|
||||
return src ? [[typeId, src] as const] : [];
|
||||
}),
|
||||
);
|
||||
}, [generatedItemAssets, run]);
|
||||
}, [runtimeGeneratedItemAssets, run]);
|
||||
|
||||
const tryPlayBackgroundMusic = useCallback(() => {
|
||||
const audio = backgroundAudioRef.current;
|
||||
@@ -879,6 +884,7 @@ export function Match3DRuntimeShell({
|
||||
src={resolvedBackgroundMusicSrc}
|
||||
loop
|
||||
preload="auto"
|
||||
aria-label="抓大鹅背景音乐"
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
|
||||
@@ -164,6 +164,8 @@ import {
|
||||
} from '../../services/match3d-works';
|
||||
import {
|
||||
hasMatch3DGeneratedImageAsset,
|
||||
mergeMatch3DGeneratedItemAssetsForRuntime,
|
||||
normalizeMatch3DGeneratedItemAssetsForRuntime,
|
||||
preloadMatch3DGeneratedRuntimeAssets,
|
||||
} from '../../services/match3dGeneratedModelCache';
|
||||
import {
|
||||
@@ -643,7 +645,9 @@ function mapPublicWorkDetailToMatch3DWork(
|
||||
entry.generatedItemAssets
|
||||
?.map((asset) => asset.backgroundAsset ?? null)
|
||||
.find(Boolean) ?? null,
|
||||
generatedItemAssets: entry.generatedItemAssets ?? [],
|
||||
generatedItemAssets: normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
entry.generatedItemAssets ?? [],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -678,7 +682,9 @@ function buildMatch3DProfileFromSession(
|
||||
backgroundImageSrc: draft.backgroundImageSrc ?? null,
|
||||
backgroundImageObjectKey: draft.backgroundImageObjectKey ?? null,
|
||||
generatedBackgroundAsset: draft.generatedBackgroundAsset ?? null,
|
||||
generatedItemAssets: draft.generatedItemAssets,
|
||||
generatedItemAssets: normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
draft.generatedItemAssets,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -702,7 +708,7 @@ function resolveMatch3DRuntimeGeneratedItemAssets(
|
||||
|
||||
if (runProfileId && profile?.profileId === runProfileId) {
|
||||
if (hasMatch3DRuntimeAsset(profileAssets)) {
|
||||
return profileAssets;
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(profileAssets);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -711,11 +717,14 @@ function resolveMatch3DRuntimeGeneratedItemAssets(
|
||||
publicWorkDetail.profileId === runProfileId
|
||||
) {
|
||||
return hasMatch3DRuntimeAsset(publicDetailAssets)
|
||||
? publicDetailAssets
|
||||
: profileAssets;
|
||||
? mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
publicDetailAssets,
|
||||
profileAssets,
|
||||
)
|
||||
: normalizeMatch3DGeneratedItemAssetsForRuntime(profileAssets);
|
||||
}
|
||||
|
||||
return profileAssets;
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(profileAssets);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -724,13 +733,15 @@ function resolveMatch3DRuntimeGeneratedItemAssets(
|
||||
isMatch3DGalleryEntry(publicWorkDetail) &&
|
||||
publicWorkDetail.profileId === runProfileId
|
||||
) {
|
||||
return publicDetailAssets;
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(publicDetailAssets);
|
||||
}
|
||||
|
||||
if (hasMatch3DRuntimeAsset(profileAssets)) {
|
||||
return profileAssets;
|
||||
return normalizeMatch3DGeneratedItemAssetsForRuntime(profileAssets);
|
||||
}
|
||||
return publicDetailAssets.length > 0 ? publicDetailAssets : profileAssets;
|
||||
return publicDetailAssets.length > 0
|
||||
? normalizeMatch3DGeneratedItemAssetsForRuntime(publicDetailAssets)
|
||||
: normalizeMatch3DGeneratedItemAssetsForRuntime(profileAssets);
|
||||
}
|
||||
|
||||
function resolveActiveMatch3DRuntimeProfile(
|
||||
@@ -2551,6 +2562,23 @@ export function PlatformEntryFlowShellImpl({
|
||||
activePuzzleGenerationSessionIdRef.current === sessionId
|
||||
);
|
||||
}, []);
|
||||
const isDraftNoticeGenerating = useCallback(
|
||||
(kind: CreationWorkShelfKind, ids: Array<string | null | undefined>) => {
|
||||
return collectDraftNoticeKeys(kind, ids).some(
|
||||
(key) => draftGenerationNotices[key]?.status === 'generating',
|
||||
);
|
||||
},
|
||||
[draftGenerationNotices],
|
||||
);
|
||||
const isDraftNoticeReadyUnread = useCallback(
|
||||
(kind: CreationWorkShelfKind, ids: Array<string | null | undefined>) => {
|
||||
return collectDraftNoticeKeys(kind, ids).some((key) => {
|
||||
const notice = draftGenerationNotices[key];
|
||||
return notice?.status === 'ready' && !notice.seen;
|
||||
});
|
||||
},
|
||||
[draftGenerationNotices],
|
||||
);
|
||||
|
||||
const resolveBigFishErrorMessage = useCallback(
|
||||
(error: unknown, fallback: string) =>
|
||||
@@ -3584,9 +3612,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
const { item } = await getMatch3DWorkDetail(profileId);
|
||||
runtimeProfile = {
|
||||
...item,
|
||||
generatedItemAssets:
|
||||
response.session.draft?.generatedItemAssets ??
|
||||
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
response.session.draft?.generatedItemAssets,
|
||||
item.generatedItemAssets,
|
||||
),
|
||||
};
|
||||
setMatch3DProfile(runtimeProfile);
|
||||
await refreshMatch3DShelf().catch(() => undefined);
|
||||
@@ -4201,8 +4230,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
const resetAutoSaveTrackingToIdle =
|
||||
autosaveCoordinator.resetAutoSaveTrackingToIdle;
|
||||
|
||||
const activeMatch3DBackgroundCompileTask =
|
||||
getMatch3DBackgroundCompileTask(match3dSession?.sessionId);
|
||||
const activeMatch3DGenerationSessionId =
|
||||
selectionStage === 'match3d-generating'
|
||||
? (activeMatch3DGenerationSessionIdRef.current ??
|
||||
match3dSession?.sessionId ??
|
||||
null)
|
||||
: null;
|
||||
const activeMatch3DBackgroundCompileTask = getMatch3DBackgroundCompileTask(
|
||||
activeMatch3DGenerationSessionId,
|
||||
);
|
||||
const match3dGenerationViewState =
|
||||
activeMatch3DBackgroundCompileTask?.generationState ??
|
||||
match3dGenerationState;
|
||||
@@ -4217,8 +4253,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
isMiniGameDraftGenerating(
|
||||
activeMatch3DBackgroundCompileTask?.generationState ?? null,
|
||||
);
|
||||
const activePuzzleGenerationSessionId =
|
||||
selectionStage === 'puzzle-generating'
|
||||
? (activePuzzleGenerationSessionIdRef.current ??
|
||||
puzzleSession?.sessionId ??
|
||||
null)
|
||||
: null;
|
||||
const activePuzzleBackgroundCompileTask = getPuzzleBackgroundCompileTask(
|
||||
puzzleSession?.sessionId,
|
||||
activePuzzleGenerationSessionId,
|
||||
);
|
||||
const puzzleGenerationViewState =
|
||||
activePuzzleBackgroundCompileTask?.generationState ?? puzzleGenerationState;
|
||||
@@ -4634,9 +4676,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
const { item } = await getMatch3DWorkDetail(profileId);
|
||||
runtimeProfile = {
|
||||
...item,
|
||||
generatedItemAssets:
|
||||
response.session.draft?.generatedItemAssets ??
|
||||
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
response.session.draft?.generatedItemAssets,
|
||||
item.generatedItemAssets,
|
||||
),
|
||||
};
|
||||
if (isViewingMatch3DGeneration(nextSession.sessionId)) {
|
||||
setMatch3DProfile(runtimeProfile);
|
||||
@@ -6211,11 +6254,23 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (!hasMatch3DRuntimeAsset(profile.generatedItemAssets)) {
|
||||
try {
|
||||
const { item } = await getMatch3DWorkDetail(profile.profileId);
|
||||
runtimeProfile = item;
|
||||
runtimeProfile = {
|
||||
...item,
|
||||
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
item.generatedItemAssets,
|
||||
profile.generatedItemAssets,
|
||||
),
|
||||
};
|
||||
} catch {
|
||||
// 中文注释:详情补读只为拿完整生成素材;失败时继续按摘要开局,避免推荐流卡死。
|
||||
}
|
||||
}
|
||||
runtimeProfile = {
|
||||
...runtimeProfile,
|
||||
generatedItemAssets: normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
runtimeProfile.generatedItemAssets,
|
||||
),
|
||||
};
|
||||
await preloadMatch3DGeneratedRuntimeAssets(
|
||||
runtimeProfile.generatedItemAssets,
|
||||
{ expireSeconds: 300 },
|
||||
@@ -8019,9 +8074,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const backgroundTask = getPuzzleBackgroundCompileTask(
|
||||
item.sourceSessionId,
|
||||
);
|
||||
const activeGenerationState =
|
||||
backgroundTask?.generationState ?? puzzleGenerationViewState;
|
||||
|
||||
if (
|
||||
item.sourceSessionId === puzzleSession?.sessionId &&
|
||||
isMiniGameDraftGenerating(puzzleGenerationViewState)
|
||||
isMiniGameDraftGenerating(activeGenerationState)
|
||||
) {
|
||||
enterCreateTab();
|
||||
selectionStageRef.current = 'puzzle-generating';
|
||||
@@ -8030,9 +8091,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const backgroundTask = getPuzzleBackgroundCompileTask(
|
||||
item.sourceSessionId,
|
||||
);
|
||||
if (
|
||||
backgroundTask &&
|
||||
isMiniGameDraftGenerating(backgroundTask.generationState)
|
||||
@@ -8086,31 +8144,78 @@ export function PlatformEntryFlowShellImpl({
|
||||
item: Match3DWorkSummary,
|
||||
options: { forceDraft?: boolean } = {},
|
||||
) => {
|
||||
const noticeKeys = collectDraftNoticeKeys('match3d', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]);
|
||||
const hasUnreadReadyNotice = isDraftNoticeReadyUnread('match3d', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]);
|
||||
setMatch3DRun(null);
|
||||
setMatch3DError(null);
|
||||
setMatch3DProfile(null);
|
||||
setMatch3DRuntimeProfile(null);
|
||||
markDraftNoticeSeen(
|
||||
collectDraftNoticeKeys('match3d', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]),
|
||||
);
|
||||
|
||||
if (item.publicationStatus === 'published' && !options.forceDraft) {
|
||||
markDraftNoticeSeen(noticeKeys);
|
||||
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(item));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.sourceSessionId?.trim()) {
|
||||
markDraftNoticeSeen(noticeKeys);
|
||||
setMatch3DError('这份抓大鹅草稿缺少会话信息,请重新开始创作。');
|
||||
return;
|
||||
}
|
||||
|
||||
const isMarkedGenerating = isDraftNoticeGenerating('match3d', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]);
|
||||
|
||||
const backgroundTask = getMatch3DBackgroundCompileTask(
|
||||
item.sourceSessionId,
|
||||
);
|
||||
const activeGenerationState =
|
||||
backgroundTask?.generationState ?? match3dGenerationViewState;
|
||||
|
||||
if (hasUnreadReadyNotice) {
|
||||
try {
|
||||
const { session: latestSession } =
|
||||
await match3dCreationClient.getSession(item.sourceSessionId);
|
||||
setMatch3DSession(latestSession);
|
||||
setMatch3DFormDraftPayload(null);
|
||||
const profileId = latestSession.draft?.profileId ?? item.profileId;
|
||||
const { item: profile } = await getMatch3DWorkDetail(profileId);
|
||||
match3dFlow.setIsBusy(false);
|
||||
const started = await startMatch3DRunFromProfile(
|
||||
profile,
|
||||
'match3d-result',
|
||||
);
|
||||
if (!started) {
|
||||
setMatch3DProfile(profile);
|
||||
enterCreateTab();
|
||||
setSelectionStage('match3d-result');
|
||||
}
|
||||
markDraftNoticeSeen(noticeKeys);
|
||||
return;
|
||||
} catch (error) {
|
||||
setMatch3DError(
|
||||
resolveMatch3DErrorMessage(error, '启动抓大鹅试玩失败。'),
|
||||
);
|
||||
markDraftNoticeSeen(noticeKeys);
|
||||
await refreshMatch3DShelf().catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
item.sourceSessionId === match3dSession?.sessionId &&
|
||||
isMiniGameDraftGenerating(match3dGenerationViewState)
|
||||
isMiniGameDraftGenerating(activeGenerationState)
|
||||
) {
|
||||
enterCreateTab();
|
||||
selectionStageRef.current = 'match3d-generating';
|
||||
@@ -8119,9 +8224,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const backgroundTask = getMatch3DBackgroundCompileTask(
|
||||
item.sourceSessionId,
|
||||
);
|
||||
if (
|
||||
backgroundTask &&
|
||||
isMiniGameDraftGenerating(backgroundTask.generationState)
|
||||
@@ -8139,6 +8241,32 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMarkedGenerating) {
|
||||
try {
|
||||
const { session: latestSession } =
|
||||
await match3dCreationClient.getSession(item.sourceSessionId);
|
||||
setMatch3DSession(latestSession);
|
||||
setMatch3DFormDraftPayload(null);
|
||||
setMatch3DProfile(null);
|
||||
setMatch3DGenerationState(
|
||||
createMiniGameDraftGenerationState('match3d'),
|
||||
);
|
||||
enterCreateTab();
|
||||
selectionStageRef.current = 'match3d-generating';
|
||||
activeMatch3DGenerationSessionIdRef.current = item.sourceSessionId;
|
||||
setSelectionStage('match3d-generating');
|
||||
return;
|
||||
} catch (error) {
|
||||
setMatch3DError(
|
||||
resolveMatch3DErrorMessage(error, '读取抓大鹅创作草稿失败。'),
|
||||
);
|
||||
await refreshMatch3DShelf().catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
markDraftNoticeSeen(noticeKeys);
|
||||
|
||||
const restoredSession = await match3dFlow.restoreDraft(
|
||||
item.sourceSessionId,
|
||||
);
|
||||
@@ -8163,6 +8291,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
[
|
||||
enterCreateTab,
|
||||
getMatch3DBackgroundCompileTask,
|
||||
isDraftNoticeGenerating,
|
||||
isDraftNoticeReadyUnread,
|
||||
markDraftNoticeSeen,
|
||||
match3dFlow,
|
||||
match3dGenerationViewState,
|
||||
@@ -8173,6 +8303,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setMatch3DFormDraftPayload,
|
||||
setMatch3DError,
|
||||
setSelectionStage,
|
||||
startMatch3DRunFromProfile,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -10190,7 +10321,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
fallback={<LazyPanelFallback label="正在加载拼图创作..." />}
|
||||
>
|
||||
<PuzzleAgentWorkspace
|
||||
session={puzzleSession}
|
||||
session={null}
|
||||
isBusy={isStreamingPuzzleReply}
|
||||
error={puzzleError}
|
||||
onBack={leavePuzzleFlow}
|
||||
|
||||
@@ -792,6 +792,93 @@ describe('PuzzleResultView', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('生成完成回包合并音乐和UI背景后试玩使用最新资源', () => {
|
||||
const onStartTestRun = vi.fn();
|
||||
const base = createSession();
|
||||
const localLevel = {
|
||||
...base.draft!.levels![0]!,
|
||||
generationStatus: 'generating' as const,
|
||||
uiBackgroundPrompt: '旧的UI背景提示词',
|
||||
uiBackgroundImageSrc: null,
|
||||
backgroundMusic: null,
|
||||
};
|
||||
const incomingLevel = {
|
||||
...localLevel,
|
||||
generationStatus: 'ready' as const,
|
||||
uiBackgroundPrompt: '水果乐园UI背景',
|
||||
uiBackgroundImageSrc:
|
||||
'/generated-puzzle-assets/session/ui/fruit-background.png',
|
||||
uiBackgroundImageObjectKey:
|
||||
'generated-puzzle-assets/session/ui/fruit-background.png',
|
||||
backgroundMusic: {
|
||||
taskId: 'music-task-fruit',
|
||||
provider: 'vector-engine-suno',
|
||||
assetObjectId: 'asset-music-fruit',
|
||||
assetKind: 'puzzle_background_music',
|
||||
audioSrc: '/generated-puzzle-assets/session/audio/fruit.mp3',
|
||||
prompt: '',
|
||||
title: '水果乐园',
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
},
|
||||
};
|
||||
const { rerender } = render(
|
||||
<PuzzleResultView
|
||||
session={createSession({
|
||||
draft: {
|
||||
...base.draft!,
|
||||
levels: [localLevel],
|
||||
},
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onStartTestRun={onStartTestRun}
|
||||
/>,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<PuzzleResultView
|
||||
session={createSession({
|
||||
draft: {
|
||||
...base.draft!,
|
||||
coverImageSrc: incomingLevel.coverImageSrc,
|
||||
coverAssetId: incomingLevel.coverAssetId,
|
||||
generationStatus: 'ready',
|
||||
levels: [incomingLevel],
|
||||
},
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onStartTestRun={onStartTestRun}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
expect(screen.getByAltText('拼图UI背景图').getAttribute('src')).toBe(
|
||||
'/generated-puzzle-assets/session/ui/fruit-background.png',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '背景音乐' }));
|
||||
expect(screen.getByLabelText('拼图背景音乐').getAttribute('src')).toBe(
|
||||
'https://signed.example.com/generated-puzzle-assets/session/audio/fruit.mp3',
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||||
|
||||
expect(onStartTestRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
levels: [
|
||||
expect.objectContaining({
|
||||
uiBackgroundImageSrc:
|
||||
'/generated-puzzle-assets/session/ui/fruit-background.png',
|
||||
backgroundMusic: expect.objectContaining({
|
||||
audioSrc: '/generated-puzzle-assets/session/audio/fruit.mp3',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('auto saves UI background prompt edits through levels', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(puzzleWorksService.updatePuzzleWork).mockResolvedValue({
|
||||
|
||||
@@ -302,6 +302,7 @@ function mergeDraftEditStateWithIncomingState(
|
||||
uiBackgroundImageObjectKey:
|
||||
incomingLevel.uiBackgroundImageObjectKey ??
|
||||
level.uiBackgroundImageObjectKey,
|
||||
backgroundMusic: incomingLevel.backgroundMusic ?? level.backgroundMusic,
|
||||
generationStatus: incomingLevel.generationStatus || 'ready',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -18,7 +18,11 @@ import type {
|
||||
PuzzleAnchorPack,
|
||||
PuzzleResultDraft,
|
||||
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
||||
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions';
|
||||
import type {
|
||||
CreatePuzzleAgentSessionRequest,
|
||||
PuzzleAgentSessionSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
|
||||
@@ -183,6 +187,15 @@ async function openDraftHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
async function expectDraftHubGeneratingBadgeCountAtLeast(count: number) {
|
||||
const panel = getPlatformTabPanel('saves');
|
||||
await waitFor(() => {
|
||||
expect(within(panel).getAllByText('生成中').length).toBeGreaterThanOrEqual(
|
||||
count,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function openDiscoverHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
await clickFirstButtonByName(user, '发现');
|
||||
const panel = getPlatformTabPanel('category');
|
||||
@@ -450,6 +463,16 @@ vi.mock('../../services/match3dGeneratedModelCache', () => ({
|
||||
),
|
||||
),
|
||||
),
|
||||
normalizeMatch3DGeneratedItemAssetsForRuntime: vi.fn(
|
||||
(assets: Match3DWorkSummary['generatedItemAssets']) =>
|
||||
assets ? [...assets] : [],
|
||||
),
|
||||
mergeMatch3DGeneratedItemAssetsForRuntime: vi.fn(
|
||||
(
|
||||
primaryAssets: Match3DWorkSummary['generatedItemAssets'],
|
||||
fallbackAssets: Match3DWorkSummary['generatedItemAssets'],
|
||||
) => (primaryAssets ? [...primaryAssets] : fallbackAssets ? [...fallbackAssets] : []),
|
||||
),
|
||||
preloadMatch3DGeneratedRuntimeAssets: vi.fn(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
@@ -541,22 +564,21 @@ vi.mock('../puzzle-agent/PuzzleAgentWorkspace', () => ({
|
||||
isBusy,
|
||||
error,
|
||||
onBack,
|
||||
onExecuteAction,
|
||||
onCreateFromForm,
|
||||
}: {
|
||||
session: { sessionId: string; messages: Array<{ text: string }> } | null;
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
onBack: () => void;
|
||||
onCreateFromForm?: (payload: {
|
||||
seedText: string;
|
||||
workTitle: string;
|
||||
workDescription: string;
|
||||
pictureDescription: string;
|
||||
referenceImageSrc: string | null;
|
||||
}) => void;
|
||||
onExecuteAction: (payload: PuzzleAgentActionRequest) => void;
|
||||
onCreateFromForm?: (payload: CreatePuzzleAgentSessionRequest) => void;
|
||||
}) => (
|
||||
<div className="puzzle-agent-workspace-mock">
|
||||
<div>拼图工作区:{session?.sessionId ?? 'missing-session'}</div>
|
||||
<div data-testid="puzzle-workspace-busy-state">
|
||||
{isBusy ? 'busy' : 'idle'}
|
||||
</div>
|
||||
{session?.messages.map((message) => (
|
||||
<div key={`${session.sessionId}-${message.text}`}>{message.text}</div>
|
||||
))}
|
||||
@@ -565,13 +587,23 @@ vi.mock('../puzzle-agent/PuzzleAgentWorkspace', () => ({
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onCreateFromForm?.({
|
||||
const payload = {
|
||||
seedText: '暖灯猫街',
|
||||
workTitle: '暖灯猫街',
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
});
|
||||
};
|
||||
if (session) {
|
||||
onExecuteAction({
|
||||
action: 'compile_puzzle_draft',
|
||||
promptText: payload.pictureDescription,
|
||||
...payload,
|
||||
candidateCount: 1,
|
||||
});
|
||||
return;
|
||||
}
|
||||
onCreateFromForm?.(payload);
|
||||
}}
|
||||
>
|
||||
生成草稿
|
||||
@@ -1209,6 +1241,27 @@ function buildPuzzleAnchorPack(): PuzzleAnchorPack {
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockPuzzleAgentSession(
|
||||
overrides: Partial<PuzzleAgentSessionSnapshot> = {},
|
||||
): PuzzleAgentSessionSnapshot {
|
||||
return {
|
||||
sessionId: 'puzzle-session-1',
|
||||
seedText: '暖灯猫街',
|
||||
currentTurn: 0,
|
||||
progressPercent: 0,
|
||||
stage: 'collecting_anchors',
|
||||
anchorPack: buildPuzzleAnchorPack(),
|
||||
draft: null,
|
||||
messages: [],
|
||||
lastAssistantReply: '先说一个你最想做成拼图的画面。',
|
||||
publishedProfileId: null,
|
||||
suggestedActions: [],
|
||||
resultPreview: null,
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildClearedPuzzleRun(params: {
|
||||
runId: string;
|
||||
entryProfileId: string;
|
||||
@@ -1700,6 +1753,71 @@ beforeEach(() => {
|
||||
),
|
||||
),
|
||||
);
|
||||
vi.mocked(
|
||||
match3dGeneratedModelCache.normalizeMatch3DGeneratedItemAssetsForRuntime,
|
||||
).mockImplementation((assets) => {
|
||||
if (!assets?.length) {
|
||||
return [];
|
||||
}
|
||||
const musicCarrier = assets.find((asset) =>
|
||||
asset.backgroundMusic?.audioSrc?.trim(),
|
||||
);
|
||||
if (!musicCarrier) {
|
||||
return [...assets];
|
||||
}
|
||||
return assets.map((asset, index) =>
|
||||
index === 0
|
||||
? {
|
||||
...asset,
|
||||
backgroundMusic: asset.backgroundMusic ?? musicCarrier.backgroundMusic,
|
||||
}
|
||||
: {
|
||||
...asset,
|
||||
backgroundMusic: null,
|
||||
backgroundMusicTitle: null,
|
||||
backgroundMusicStyle: null,
|
||||
backgroundMusicPrompt: null,
|
||||
}
|
||||
);
|
||||
});
|
||||
vi.mocked(
|
||||
match3dGeneratedModelCache.mergeMatch3DGeneratedItemAssetsForRuntime,
|
||||
).mockImplementation((primaryAssets, fallbackAssets) => {
|
||||
const primary = primaryAssets ?? [];
|
||||
const fallback = fallbackAssets ?? [];
|
||||
if (primary.length <= 0) {
|
||||
return match3dGeneratedModelCache.normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
fallback,
|
||||
);
|
||||
}
|
||||
if (fallback.length <= 0) {
|
||||
return match3dGeneratedModelCache.normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
primary,
|
||||
);
|
||||
}
|
||||
const fallbackById = new Map(fallback.map((asset) => [asset.itemId, asset]));
|
||||
return match3dGeneratedModelCache.normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
primary.map((asset) => {
|
||||
const fallbackAsset = fallbackById.get(asset.itemId);
|
||||
return fallbackAsset
|
||||
? {
|
||||
...asset,
|
||||
imageSrc: asset.imageSrc ?? fallbackAsset.imageSrc ?? null,
|
||||
imageObjectKey:
|
||||
asset.imageObjectKey ?? fallbackAsset.imageObjectKey ?? null,
|
||||
imageViews:
|
||||
asset.imageViews && asset.imageViews.length > 0
|
||||
? asset.imageViews
|
||||
: (fallbackAsset.imageViews ?? []),
|
||||
backgroundMusic:
|
||||
asset.backgroundMusic ?? fallbackAsset.backgroundMusic ?? null,
|
||||
backgroundAsset:
|
||||
asset.backgroundAsset ?? fallbackAsset.backgroundAsset ?? null,
|
||||
}
|
||||
: asset;
|
||||
}),
|
||||
);
|
||||
});
|
||||
vi.mocked(
|
||||
match3dGeneratedModelCache.preloadMatch3DGeneratedRuntimeAssets,
|
||||
).mockResolvedValue(undefined);
|
||||
@@ -2749,7 +2867,7 @@ test('running match3d form generation can return to draft tab and reopen progres
|
||||
await openDraftHub(user);
|
||||
|
||||
expect(await screen.findByText('抓大鹅草稿')).toBeTruthy();
|
||||
expect(screen.getAllByText('生成中').length).toBeGreaterThan(0);
|
||||
await expectDraftHubGeneratingBadgeCountAtLeast(1);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /继续创作《抓大鹅草稿》/u }),
|
||||
@@ -2761,6 +2879,93 @@ test('running match3d form generation can return to draft tab and reopen progres
|
||||
});
|
||||
});
|
||||
|
||||
test('running match3d persisted draft reopens progress instead of unfinished result', async () => {
|
||||
const user = userEvent.setup();
|
||||
const runningSession = buildMockMatch3DAgentSession({
|
||||
sessionId: 'match3d-running-persisted-session',
|
||||
draft: null,
|
||||
stage: 'collecting_config',
|
||||
});
|
||||
const persistedRunningSession = buildMockMatch3DAgentSession({
|
||||
sessionId: 'match3d-running-persisted-session',
|
||||
stage: 'draft_ready',
|
||||
draft: {
|
||||
profileId: 'match3d-running-persisted-profile',
|
||||
gameName: '赛博水果摊',
|
||||
themeText: '赛博水果摊',
|
||||
summary: '',
|
||||
tags: ['水果', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
generatedItemAssets: [],
|
||||
},
|
||||
});
|
||||
const persistedRunningWork: Match3DWorkSummary = {
|
||||
workId: 'match3d-running-persisted-work',
|
||||
profileId: 'match3d-running-persisted-profile',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'match3d-running-persisted-session',
|
||||
gameName: '赛博水果摊',
|
||||
themeText: '赛博水果摊',
|
||||
summary: '正在生成玩法素材。',
|
||||
tags: ['水果', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-14T10:30:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generatedItemAssets: [],
|
||||
};
|
||||
vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
|
||||
session: runningSession,
|
||||
});
|
||||
vi.mocked(match3dCreationClient.executeAction).mockRejectedValueOnce(
|
||||
new Error('素材生成仍在后台处理'),
|
||||
);
|
||||
vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
|
||||
session: persistedRunningSession,
|
||||
});
|
||||
vi.mocked(listMatch3DWorks).mockResolvedValue({
|
||||
items: [persistedRunningWork],
|
||||
});
|
||||
vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
|
||||
item: persistedRunningWork,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
expect(await screen.findByText('抓大鹅草稿生成进度')).toBeTruthy();
|
||||
expect(
|
||||
await screen.findAllByText('素材生成仍在后台处理'),
|
||||
).not.toHaveLength(0);
|
||||
vi.mocked(match3dCreationClient.getSession).mockClear();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
await openDraftHub(user);
|
||||
await expectDraftHubGeneratingBadgeCountAtLeast(1);
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: /继续创作《赛博水果摊》/u }),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('抓大鹅草稿生成进度')).toBeTruthy();
|
||||
expect(screen.queryByText('抓大鹅结果页')).toBeNull();
|
||||
expect(match3dCreationClient.getSession).toHaveBeenCalledWith(
|
||||
'match3d-running-persisted-session',
|
||||
);
|
||||
});
|
||||
|
||||
test('running match3d form generation keeps other creation templates available', async () => {
|
||||
const user = userEvent.setup();
|
||||
const runningSession = buildMockMatch3DAgentSession({
|
||||
@@ -2966,8 +3171,8 @@ test('running match3d form generation keeps same template generation available',
|
||||
await openDraftHub(user);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('抓大鹅草稿').length).toBeGreaterThanOrEqual(2);
|
||||
expect(screen.getAllByText('生成中').length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
await expectDraftHubGeneratingBadgeCountAtLeast(2);
|
||||
|
||||
await act(async () => {
|
||||
resolveFirstCompile({
|
||||
@@ -2983,6 +3188,126 @@ test('running match3d form generation keeps same template generation available',
|
||||
});
|
||||
});
|
||||
|
||||
test('running puzzle form generation creates a new puzzle draft on same template submit', async () => {
|
||||
const user = userEvent.setup();
|
||||
const firstSession = buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-parallel-session-1',
|
||||
});
|
||||
const secondSession = buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-parallel-session-2',
|
||||
});
|
||||
let resolveFirstCompile!: (value: {
|
||||
operation: {
|
||||
operationId: string;
|
||||
type: 'compile_puzzle_draft';
|
||||
status: 'completed';
|
||||
phaseLabel: string;
|
||||
phaseDetail: string;
|
||||
progress: number;
|
||||
};
|
||||
session: PuzzleAgentSessionSnapshot;
|
||||
}) => void;
|
||||
let resolveSecondCompile!: (value: {
|
||||
operation: {
|
||||
operationId: string;
|
||||
type: 'compile_puzzle_draft';
|
||||
status: 'completed';
|
||||
phaseLabel: string;
|
||||
phaseDetail: string;
|
||||
progress: number;
|
||||
};
|
||||
session: PuzzleAgentSessionSnapshot;
|
||||
}) => void;
|
||||
vi.mocked(createPuzzleAgentSession)
|
||||
.mockResolvedValueOnce({ session: firstSession })
|
||||
.mockResolvedValueOnce({ session: secondSession });
|
||||
vi.mocked(executePuzzleAgentAction)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveFirstCompile = resolve;
|
||||
}),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveSecondCompile = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
expect(await screen.findByText('拼图草稿生成进度')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
const puzzleTab = await screen.findByRole('tab', { name: '拼图' });
|
||||
expect((puzzleTab as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(puzzleTab);
|
||||
|
||||
expect(await screen.findByText('拼图工作区:missing-session')).toBeTruthy();
|
||||
expect(screen.getByTestId('puzzle-workspace-busy-state')).toHaveProperty(
|
||||
'textContent',
|
||||
'idle',
|
||||
);
|
||||
const secondGenerateButton = await screen.findByRole('button', {
|
||||
name: '生成草稿',
|
||||
});
|
||||
expect((secondGenerateButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(secondGenerateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createPuzzleAgentSession).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(executePuzzleAgentAction).toHaveBeenCalledTimes(2);
|
||||
expect(executePuzzleAgentAction).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'puzzle-parallel-session-1',
|
||||
expect.objectContaining({ action: 'compile_puzzle_draft' }),
|
||||
);
|
||||
expect(executePuzzleAgentAction).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'puzzle-parallel-session-2',
|
||||
expect.objectContaining({ action: 'compile_puzzle_draft' }),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('拼图草稿生成进度')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
await openDraftHub(user);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('拼图草稿').length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
await expectDraftHubGeneratingBadgeCountAtLeast(2);
|
||||
|
||||
await act(async () => {
|
||||
resolveFirstCompile({
|
||||
operation: {
|
||||
operationId: 'compile-puzzle-parallel-1',
|
||||
type: 'compile_puzzle_draft',
|
||||
status: 'completed',
|
||||
phaseLabel: '已完成',
|
||||
phaseDetail: '草稿已生成',
|
||||
progress: 1,
|
||||
},
|
||||
session: buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-parallel-session-1',
|
||||
}),
|
||||
});
|
||||
resolveSecondCompile({
|
||||
operation: {
|
||||
operationId: 'compile-puzzle-parallel-2',
|
||||
type: 'compile_puzzle_draft',
|
||||
status: 'completed',
|
||||
phaseLabel: '已完成',
|
||||
phaseDetail: '草稿已生成',
|
||||
progress: 1,
|
||||
},
|
||||
session: buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-parallel-session-2',
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('match3d result trial passes generated models into first runtime mount', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
|
||||
@@ -3318,6 +3643,134 @@ test('match3d draft generation auto starts trial and runtime back opens draft re
|
||||
expect(screen.getByText('自动试玩抓大鹅')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('completed match3d draft notice first opens trial then reopens result', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
|
||||
{
|
||||
itemId: 'match3d-notice-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/items/match3d-notice-item-1-item/image.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/items/match3d-notice-item-1-item/image.png',
|
||||
imageViews: [],
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
modelFileName: null,
|
||||
taskUuid: 'task-notice-strawberry',
|
||||
subscriptionKey: 'sub-notice-strawberry',
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
},
|
||||
];
|
||||
const runningSession = buildMockMatch3DAgentSession({
|
||||
sessionId: 'match3d-notice-session-1',
|
||||
draft: null,
|
||||
stage: 'collecting_config',
|
||||
});
|
||||
const generatedSession = buildMockMatch3DAgentSession({
|
||||
sessionId: 'match3d-notice-session-1',
|
||||
stage: 'draft_ready',
|
||||
draft: {
|
||||
profileId: 'match3d-notice-profile-1',
|
||||
gameName: '红点自动试玩抓大鹅',
|
||||
themeText: '水果',
|
||||
summary: '',
|
||||
tags: ['水果', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
generatedItemAssets,
|
||||
},
|
||||
});
|
||||
const generatedProfile: Match3DWorkSummary = {
|
||||
workId: 'match3d-notice-work-1',
|
||||
profileId: 'match3d-notice-profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'match3d-notice-session-1',
|
||||
gameName: '红点自动试玩抓大鹅',
|
||||
themeText: '水果',
|
||||
summary: '',
|
||||
tags: ['水果', '抓大鹅'],
|
||||
coverImageSrc: null,
|
||||
referenceImageSrc: null,
|
||||
clearCount: 12,
|
||||
difficulty: 4,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generatedItemAssets,
|
||||
};
|
||||
vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
|
||||
session: runningSession,
|
||||
});
|
||||
let resolveCompile!: (value: {
|
||||
session: Match3DAgentSessionSnapshot;
|
||||
}) => void;
|
||||
vi.mocked(match3dCreationClient.executeAction).mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveCompile = resolve;
|
||||
}),
|
||||
);
|
||||
vi.mocked(match3dCreationClient.getSession).mockResolvedValue({
|
||||
session: generatedSession,
|
||||
});
|
||||
vi.mocked(getMatch3DWorkDetail).mockResolvedValue({
|
||||
item: generatedProfile,
|
||||
});
|
||||
vi.mocked(listMatch3DWorks).mockResolvedValue({
|
||||
items: [generatedProfile],
|
||||
});
|
||||
match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
|
||||
run: buildMockMatch3DRun(generatedProfile.profileId),
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
expect(await screen.findByText('抓大鹅草稿生成进度')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
await openDraftHub(user);
|
||||
await expectDraftHubGeneratingBadgeCountAtLeast(1);
|
||||
|
||||
await act(async () => {
|
||||
resolveCompile({ session: generatedSession });
|
||||
});
|
||||
|
||||
expect(await screen.findByLabelText('新生成完成')).toBeTruthy();
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: /继续创作《红点自动试玩抓大鹅》/u,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/抓大鹅运行态/u)).toBeTruthy();
|
||||
expect(screen.queryByText('抓大鹅草稿生成进度')).toBeNull();
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledTimes(1);
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
await openDraftHub(user);
|
||||
expect(screen.queryByLabelText('新生成完成')).toBeNull();
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: /继续创作《红点自动试玩抓大鹅》/u,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
|
||||
expect(screen.queryByText(/抓大鹅运行态/u)).toBeNull();
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('puzzle draft generation auto starts trial and runtime back opens draft result', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedDraft: PuzzleResultDraft = {
|
||||
@@ -3364,6 +3817,22 @@ test('puzzle draft generation auto starts trial and runtime back opens draft res
|
||||
selectedCandidateId: 'candidate-1',
|
||||
coverImageSrc: '/puzzle/auto-candidate.png',
|
||||
coverAssetId: 'asset-1',
|
||||
uiBackgroundPrompt: '水果乐园竖屏纯背景',
|
||||
uiBackgroundImageSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
|
||||
uiBackgroundImageObjectKey:
|
||||
'generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
|
||||
backgroundMusic: {
|
||||
taskId: 'music-task-auto-1',
|
||||
provider: 'vector-engine-suno',
|
||||
assetObjectId: 'asset-music-auto-1',
|
||||
assetKind: 'puzzle_background_music',
|
||||
audioSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-auto-1/audio/background.mp3',
|
||||
prompt: '',
|
||||
title: '水果乐园',
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
},
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
],
|
||||
@@ -3412,6 +3881,16 @@ test('puzzle draft generation auto starts trial and runtime back opens draft res
|
||||
expect.objectContaining({
|
||||
levelName: '雨夜猫街',
|
||||
coverImageSrc: '/puzzle/auto-candidate.png',
|
||||
levels: [
|
||||
expect.objectContaining({
|
||||
uiBackgroundImageSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-auto-1/ui/background.png',
|
||||
backgroundMusic: expect.objectContaining({
|
||||
audioSrc:
|
||||
'/generated-puzzle-assets/puzzle-session-auto-1/audio/background.mp3',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
@@ -4958,9 +5437,10 @@ test('puzzle draft result back button returns to creation hub', async () => {
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
expect(await screen.findByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(await screen.findByText('拼图工作区:missing-session')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('雨夜里有一只会发光的猫站在遗迹台阶上。'),
|
||||
).toBeTruthy();
|
||||
screen.queryByText('雨夜里有一只会发光的猫站在遗迹台阶上。'),
|
||||
).toBeNull();
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -5872,7 +6352,7 @@ test('running custom world draft generation can return to creation center with s
|
||||
await openDraftHub(user);
|
||||
|
||||
expect(await screen.findByText('潮雾列岛')).toBeTruthy();
|
||||
expect(screen.getAllByText('生成中').length).toBeGreaterThan(0);
|
||||
await expectDraftHubGeneratingBadgeCountAtLeast(1);
|
||||
});
|
||||
|
||||
test('refresh restores running draft generation progress instead of agent workspace', async () => {
|
||||
|
||||
Reference in New Issue
Block a user