Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
1858 lines
62 KiB
TypeScript
1858 lines
62 KiB
TypeScript
// @vitest-environment jsdom
|
||
|
||
import {
|
||
fireEvent,
|
||
render,
|
||
screen,
|
||
waitFor,
|
||
within,
|
||
} from '@testing-library/react';
|
||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||
|
||
import type { Match3DWorkProfile } from '../../../packages/shared/src/contracts/match3dWorks';
|
||
import * as match3dWorksService from '../../services/match3d-works';
|
||
import { clearMatch3DGeneratedModelBytesCache } from '../../services/match3dGeneratedModelCache';
|
||
import { Match3DResultView } from './Match3DResultView';
|
||
|
||
const match3dSpritesheetParser = vi.hoisted(() => ({
|
||
loadMatch3DSpritesheetAssetRegions: vi.fn(),
|
||
}));
|
||
|
||
vi.mock('../ResolvedAssetImage', () => ({
|
||
ResolvedAssetImage: ({
|
||
src,
|
||
alt,
|
||
className,
|
||
}: {
|
||
src?: string | null;
|
||
alt?: string;
|
||
className?: string;
|
||
}) => (src ? <img src={src} alt={alt} className={className} /> : null),
|
||
}));
|
||
|
||
vi.mock('../../services/assetReadUrlService', () => ({
|
||
isGeneratedLegacyPath: (value: string) =>
|
||
/^\/?generated-[^/?#]+\/.+/u.test(value.trim()),
|
||
resolveAssetReadUrl: vi.fn((value: string) =>
|
||
Promise.resolve(`https://signed.example.com/${value.replace(/^\/+/u, '')}`),
|
||
),
|
||
}));
|
||
|
||
vi.mock('../../services/match3d-works', () => ({
|
||
generateMatch3DBackgroundImage: vi.fn(),
|
||
generateMatch3DContainerImage: vi.fn(),
|
||
generateMatch3DCoverImage: vi.fn(),
|
||
generateMatch3DItemAssets: vi.fn(),
|
||
generateMatch3DWorkTags: vi.fn(),
|
||
publishMatch3DWork: vi.fn(),
|
||
updateMatch3DGeneratedItemAssets: vi.fn(),
|
||
updateMatch3DWork: vi.fn(),
|
||
}));
|
||
|
||
vi.mock('../../services/match3dSpritesheetParser', async (importOriginal) => {
|
||
const actual =
|
||
await importOriginal<typeof import('../../services/match3dSpritesheetParser')>();
|
||
return {
|
||
...actual,
|
||
loadMatch3DSpritesheetAssetRegions:
|
||
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions,
|
||
};
|
||
});
|
||
|
||
afterEach(() => {
|
||
clearMatch3DGeneratedModelBytesCache();
|
||
vi.clearAllMocks();
|
||
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions.mockReset();
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
function createDeferred<T>() {
|
||
let resolve!: (value: T) => void;
|
||
let reject!: (reason?: unknown) => void;
|
||
const promise = new Promise<T>((promiseResolve, promiseReject) => {
|
||
resolve = promiseResolve;
|
||
reject = promiseReject;
|
||
});
|
||
return { promise, reject, resolve };
|
||
}
|
||
|
||
function stubMatch3DCoverUpload(dataUrl: string) {
|
||
class MockFileReader {
|
||
result: string | ArrayBuffer | null = dataUrl;
|
||
onload: null | (() => void) = null;
|
||
onerror: null | (() => void) = null;
|
||
|
||
readAsDataURL() {
|
||
this.onload?.();
|
||
}
|
||
}
|
||
|
||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
|
||
}
|
||
|
||
function confirmPointCost() {
|
||
const dialogs = screen.getAllByRole('dialog', { name: '确认消耗泥点' });
|
||
const dialog = dialogs[dialogs.length - 1]!;
|
||
fireEvent.click(
|
||
dialog.querySelector('button:last-of-type') as HTMLButtonElement,
|
||
);
|
||
}
|
||
|
||
function createProfile(
|
||
overrides: Partial<Match3DWorkProfile> = {},
|
||
): Match3DWorkProfile {
|
||
return {
|
||
workId: 'match3d-work-1',
|
||
profileId: 'match3d-profile-1',
|
||
ownerUserId: 'user-1',
|
||
sourceSessionId: 'match3d-session-1',
|
||
gameName: '水果抓大鹅',
|
||
themeText: '水果',
|
||
summary: '',
|
||
tags: ['水果', '抓大鹅', '经典消除'],
|
||
coverImageSrc: null,
|
||
referenceImageSrc: null,
|
||
clearCount: 12,
|
||
difficulty: 4,
|
||
publicationStatus: 'draft',
|
||
playCount: 0,
|
||
updatedAt: '2026-05-01T00:00:00.000Z',
|
||
publishedAt: null,
|
||
publishReady: false,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function createReadyGeneratedItemAsset(index: number) {
|
||
return {
|
||
itemId: `match3d-item-${index}`,
|
||
itemName: `物品${index}`,
|
||
imageSrc: `/generated-match3d-assets/session/profile/items/item-${index}/image.png`,
|
||
imageObjectKey: `generated-match3d-assets/session/profile/items/item-${index}/image.png`,
|
||
imageViews: [1, 2, 3, 4, 5].map((viewIndex) => ({
|
||
viewId: `view-${String(viewIndex).padStart(2, '0')}`,
|
||
viewIndex,
|
||
imageSrc: `/generated-match3d-assets/session/profile/items/item-${index}/views/view-${String(viewIndex).padStart(2, '0')}.png`,
|
||
imageObjectKey: `generated-match3d-assets/session/profile/items/item-${index}/views/view-${String(viewIndex).padStart(2, '0')}.png`,
|
||
})),
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: `task-${index}`,
|
||
subscriptionKey: `sub-${index}`,
|
||
status: 'image_ready' as const,
|
||
error: null,
|
||
};
|
||
}
|
||
|
||
describe('Match3DResultView', () => {
|
||
test('作品信息 Tab 字段命名对齐拼图草稿且描述可为空', async () => {
|
||
const profile = createProfile();
|
||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||
item: profile,
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByLabelText('作品名称')).toHaveProperty(
|
||
'value',
|
||
'水果抓大鹅',
|
||
);
|
||
expect(screen.getByLabelText('作品描述')).toHaveProperty('value', '');
|
||
expect(screen.getByText('作品标签')).toBeTruthy();
|
||
expect(screen.getByText('水果')).toBeTruthy();
|
||
expect(screen.getByText('抓大鹅')).toBeTruthy();
|
||
expect(screen.queryByRole('button', { name: '封面图' })).toBeNull();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||
|
||
await waitFor(() => {
|
||
expect(match3dWorksService.updateMatch3DWork).toHaveBeenCalledWith(
|
||
'match3d-profile-1',
|
||
expect.objectContaining({
|
||
gameName: '水果抓大鹅',
|
||
summary: '',
|
||
tags: ['水果', '抓大鹅', '经典消除'],
|
||
}),
|
||
);
|
||
});
|
||
});
|
||
|
||
test('作品标签支持 AI 生成并写回标签编辑区', async () => {
|
||
vi.mocked(match3dWorksService.generateMatch3DWorkTags).mockResolvedValue({
|
||
tags: ['果园', '抓大鹅', '经典消除', '轻量休闲'],
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({ tags: [] })}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: 'AI生成作品标签' }));
|
||
|
||
await waitFor(() => {
|
||
expect(match3dWorksService.generateMatch3DWorkTags).toHaveBeenCalledWith({
|
||
gameName: '水果抓大鹅',
|
||
themeText: '水果',
|
||
summary: '',
|
||
});
|
||
expect(screen.getByText('果园')).toBeTruthy();
|
||
expect(screen.getByText('轻量休闲')).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
test('发布面板内支持引用物品素材作为多参考图生成封面', async () => {
|
||
const profile = createProfile({
|
||
generatedItemAssets: [
|
||
{
|
||
itemId: 'match3d-item-1',
|
||
itemName: '草莓',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/i1/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/i1/image.png',
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: null,
|
||
subscriptionKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
],
|
||
});
|
||
const nextProfile = createProfile({
|
||
...profile,
|
||
coverImageSrc:
|
||
'/generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
});
|
||
const onSaved = vi.fn();
|
||
vi.mocked(match3dWorksService.generateMatch3DCoverImage).mockResolvedValue({
|
||
item: nextProfile,
|
||
coverImageSrc:
|
||
'/generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
coverImageObjectKey:
|
||
'generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
prompt: '草莓抓大鹅封面图',
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onSaved={onSaved}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '发布' }));
|
||
const publishDialog = screen.getByRole('dialog', {
|
||
name: '发布抓大鹅作品',
|
||
});
|
||
fireEvent.click(
|
||
within(publishDialog).getByRole('button', { name: '引用草莓' }),
|
||
);
|
||
fireEvent.change(within(publishDialog).getByLabelText('封面描述'), {
|
||
target: { value: '草莓抓大鹅封面图' },
|
||
});
|
||
fireEvent.click(
|
||
within(publishDialog).getByRole('button', { name: '生成封面图' }),
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
match3dWorksService.generateMatch3DCoverImage,
|
||
).toHaveBeenCalledWith(profile.profileId, {
|
||
prompt: '草莓抓大鹅封面图',
|
||
referenceImageSrcs: [
|
||
'generated-match3d-assets/session/profile/items/i1/image.png',
|
||
],
|
||
uploadedImageSrc: null,
|
||
});
|
||
expect(onSaved).toHaveBeenCalledWith(nextProfile);
|
||
expect(
|
||
screen.getByRole('dialog', { name: '发布抓大鹅作品' }),
|
||
).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
test('生成封面图只更新封面字段,不用旧回包覆盖当前物品素材和配置', async () => {
|
||
const generatedItemAssets = [createReadyGeneratedItemAsset(1)];
|
||
const profile = createProfile({
|
||
clearCount: 12,
|
||
difficulty: 4,
|
||
generatedItemAssets,
|
||
});
|
||
const staleResponseProfile = createProfile({
|
||
...profile,
|
||
coverImageSrc:
|
||
'/generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
clearCount: 8,
|
||
difficulty: 2,
|
||
generatedItemAssets: [],
|
||
});
|
||
const onSaved = vi.fn();
|
||
vi.mocked(match3dWorksService.generateMatch3DCoverImage).mockResolvedValue({
|
||
item: staleResponseProfile,
|
||
coverImageSrc:
|
||
'/generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
coverImageObjectKey:
|
||
'generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
prompt: '水果封面图',
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onSaved={onSaved}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '发布' }));
|
||
const publishDialog = screen.getByRole('dialog', {
|
||
name: '发布抓大鹅作品',
|
||
});
|
||
fireEvent.change(within(publishDialog).getByLabelText('封面描述'), {
|
||
target: { value: '水果封面图' },
|
||
});
|
||
fireEvent.click(
|
||
within(publishDialog).getByRole('button', { name: '生成封面图' }),
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(onSaved).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
coverImageSrc:
|
||
'/generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
clearCount: 12,
|
||
difficulty: 4,
|
||
generatedItemAssets: expect.arrayContaining([
|
||
expect.objectContaining({
|
||
itemId: 'match3d-item-1',
|
||
imageViews: expect.arrayContaining([
|
||
expect.objectContaining({ viewId: 'view-01' }),
|
||
]),
|
||
}),
|
||
]),
|
||
}),
|
||
);
|
||
});
|
||
});
|
||
|
||
test('封面图上传后对齐拼图入口显示 AI 重绘开关和删除按钮', async () => {
|
||
const uploadedDataUrl = 'data:image/png;base64,match3d-cover-uploaded';
|
||
stubMatch3DCoverUpload(uploadedDataUrl);
|
||
const profile = createProfile();
|
||
const nextProfile = createProfile({
|
||
coverImageSrc:
|
||
'/generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
});
|
||
vi.mocked(match3dWorksService.generateMatch3DCoverImage).mockResolvedValue({
|
||
item: nextProfile,
|
||
coverImageSrc:
|
||
'/generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
coverImageObjectKey:
|
||
'generated-match3d-assets/session/profile/cover/task/cover.png',
|
||
prompt: '保留构图,改成节日果园',
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '发布' }));
|
||
const publishDialog = screen.getByRole('dialog', {
|
||
name: '发布抓大鹅作品',
|
||
});
|
||
fireEvent.change(
|
||
within(publishDialog).getByLabelText('上传封面图', {
|
||
selector: 'input',
|
||
}),
|
||
{
|
||
target: {
|
||
files: [new File(['x'], 'cover.png', { type: 'image/png' })],
|
||
},
|
||
},
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
within(publishDialog).getByRole('switch', { name: 'AI重绘' }),
|
||
).toBeTruthy();
|
||
expect(
|
||
within(publishDialog).getByRole('button', { name: '移除封面图' }),
|
||
).toBeTruthy();
|
||
expect(within(publishDialog).getByLabelText('AI重绘要求')).toBeTruthy();
|
||
});
|
||
expect(within(publishDialog).queryByText('参考图')).toBeNull();
|
||
|
||
fireEvent.change(within(publishDialog).getByLabelText('AI重绘要求'), {
|
||
target: { value: '保留构图,改成节日果园' },
|
||
});
|
||
fireEvent.click(
|
||
within(publishDialog).getByRole('button', { name: '生成封面图' }),
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
match3dWorksService.generateMatch3DCoverImage,
|
||
).toHaveBeenCalledWith(profile.profileId, {
|
||
prompt: '保留构图,改成节日果园',
|
||
uploadedImageSrc: uploadedDataUrl,
|
||
referenceImageSrcs: [],
|
||
});
|
||
});
|
||
});
|
||
|
||
test('试玩只要求基础配置可保存,不被发布封面门槛阻断', async () => {
|
||
const profile = createProfile();
|
||
const onStartTestRun = vi.fn();
|
||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||
item: profile,
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={onStartTestRun}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||
|
||
await waitFor(() => {
|
||
expect(match3dWorksService.updateMatch3DWork).toHaveBeenCalledWith(
|
||
'match3d-profile-1',
|
||
expect.objectContaining({
|
||
clearCount: 12,
|
||
difficulty: 4,
|
||
gameName: '水果抓大鹅',
|
||
}),
|
||
);
|
||
});
|
||
expect(onStartTestRun).toHaveBeenCalledWith(profile, {
|
||
itemTypeCountOverride: 1,
|
||
});
|
||
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('试玩前保存响应缺少素材时仍把当前生成 2D 素材带入运行态', async () => {
|
||
const generatedItemAssets = [createReadyGeneratedItemAsset(1)];
|
||
const profile = createProfile({ generatedItemAssets });
|
||
const savedProfile = createProfile({ generatedItemAssets: [] });
|
||
const persistedProfile = createProfile({ generatedItemAssets });
|
||
const onStartTestRun = vi.fn();
|
||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||
item: savedProfile,
|
||
});
|
||
vi.mocked(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).mockResolvedValue({
|
||
item: persistedProfile,
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={onStartTestRun}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).toHaveBeenCalledWith(
|
||
profile.profileId,
|
||
expect.objectContaining({
|
||
generatedItemAssets: [
|
||
expect.objectContaining({
|
||
itemId: 'match3d-item-1',
|
||
imageViews: expect.arrayContaining([
|
||
expect.objectContaining({ viewId: 'view-01' }),
|
||
]),
|
||
status: 'image_ready',
|
||
}),
|
||
],
|
||
}),
|
||
);
|
||
expect(onStartTestRun).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
profileId: profile.profileId,
|
||
generatedItemAssets: [
|
||
expect.objectContaining({
|
||
itemId: 'match3d-item-1',
|
||
imageViews: expect.arrayContaining([
|
||
expect.objectContaining({ viewId: 'view-01' }),
|
||
]),
|
||
status: 'image_ready',
|
||
}),
|
||
],
|
||
}),
|
||
{
|
||
itemTypeCountOverride: 1,
|
||
},
|
||
);
|
||
});
|
||
});
|
||
|
||
test('发布仍要求封面和标签数量满足门槛', () => {
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({ tags: ['水果', '抓大鹅'] })}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
const publishButton = screen.getByRole('button', { name: '发布' });
|
||
expect(publishButton).toHaveProperty('disabled', false);
|
||
|
||
fireEvent.click(publishButton);
|
||
const publishDialog = screen.getByRole('dialog', {
|
||
name: '发布抓大鹅作品',
|
||
});
|
||
expect(within(publishDialog).getByText('封面图不能为空。')).toBeTruthy();
|
||
expect(
|
||
within(publishDialog).getByText('标签数量需要在 3 到 6 个之间。'),
|
||
).toBeTruthy();
|
||
expect(
|
||
within(publishDialog).getByRole('button', { name: '发布到广场' }),
|
||
).toHaveProperty('disabled', true);
|
||
fireEvent.click(
|
||
within(publishDialog).getByRole('button', { name: '发布到广场' }),
|
||
);
|
||
expect(within(publishDialog).getByText('封面图不能为空。')).toBeTruthy();
|
||
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('发布要求当前难度素材都具备五个视角', () => {
|
||
const generatedItemAssets = [
|
||
createReadyGeneratedItemAsset(1),
|
||
createReadyGeneratedItemAsset(2),
|
||
{
|
||
...createReadyGeneratedItemAsset(3),
|
||
imageViews: createReadyGeneratedItemAsset(3).imageViews?.slice(0, 4),
|
||
},
|
||
];
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({
|
||
summary: '轻松消除水果',
|
||
coverImageSrc: 'data:image/png;base64,cover',
|
||
clearCount: 8,
|
||
difficulty: 2,
|
||
generatedItemAssets,
|
||
})}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
const publishButton = screen.getByRole('button', { name: '发布' });
|
||
expect(publishButton).toHaveProperty('disabled', false);
|
||
fireEvent.click(publishButton);
|
||
const publishDialog = screen.getByRole('dialog', {
|
||
name: '发布抓大鹅作品',
|
||
});
|
||
expect(
|
||
within(publishDialog).getByText(
|
||
'当前难度需要 3 种物品,已生成 2 种,请先在素材配置中补齐。',
|
||
),
|
||
).toBeTruthy();
|
||
expect(
|
||
within(publishDialog).getByRole('button', { name: '发布到广场' }),
|
||
).toHaveProperty('disabled', true);
|
||
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
|
||
fireEvent.click(within(publishDialog).getByRole('button', { name: '取消' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '难度配置' }));
|
||
expect(screen.getByText('已生成物品种类')).toBeTruthy();
|
||
expect(screen.getAllByText('2 种').length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('发布前会先把当前 2D 多视角素材写回 profile', async () => {
|
||
const generatedItemAssets = [
|
||
createReadyGeneratedItemAsset(1),
|
||
createReadyGeneratedItemAsset(2),
|
||
createReadyGeneratedItemAsset(3),
|
||
createReadyGeneratedItemAsset(4),
|
||
createReadyGeneratedItemAsset(5),
|
||
createReadyGeneratedItemAsset(6),
|
||
createReadyGeneratedItemAsset(7),
|
||
createReadyGeneratedItemAsset(8),
|
||
createReadyGeneratedItemAsset(9),
|
||
];
|
||
const profile = createProfile({
|
||
summary: '轻松消除水果',
|
||
coverImageSrc: 'data:image/png;base64,cover',
|
||
generatedItemAssets,
|
||
});
|
||
const savedProfile = createProfile({
|
||
...profile,
|
||
generatedItemAssets: [],
|
||
});
|
||
const onPublished = vi.fn();
|
||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||
item: savedProfile,
|
||
});
|
||
vi.mocked(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).mockResolvedValue({
|
||
item: profile,
|
||
});
|
||
vi.mocked(match3dWorksService.publishMatch3DWork).mockResolvedValue({
|
||
item: createProfile({
|
||
...profile,
|
||
publicationStatus: 'published',
|
||
generatedItemAssets: [],
|
||
}),
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onPublished={onPublished}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '发布' }));
|
||
fireEvent.click(
|
||
within(
|
||
screen.getByRole('dialog', { name: '发布抓大鹅作品' }),
|
||
).getByRole('button', { name: '发布到广场' }),
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).toHaveBeenCalledWith(
|
||
profile.profileId,
|
||
expect.objectContaining({
|
||
generatedItemAssets: expect.arrayContaining([
|
||
expect.objectContaining({
|
||
itemId: 'match3d-item-1',
|
||
imageViews: expect.arrayContaining([
|
||
expect.objectContaining({ viewId: 'view-01' }),
|
||
]),
|
||
status: 'image_ready',
|
||
}),
|
||
]),
|
||
}),
|
||
);
|
||
expect(match3dWorksService.publishMatch3DWork).toHaveBeenCalledWith(
|
||
profile.profileId,
|
||
);
|
||
expect(onPublished).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
publicationStatus: 'published',
|
||
generatedItemAssets: expect.arrayContaining([
|
||
expect.objectContaining({
|
||
itemId: 'match3d-item-1',
|
||
imageViews: expect.arrayContaining([
|
||
expect.objectContaining({ viewId: 'view-01' }),
|
||
]),
|
||
}),
|
||
]),
|
||
}),
|
||
);
|
||
});
|
||
const assetPersistCallOrder = vi.mocked(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).mock.invocationCallOrder[0];
|
||
const publishCallOrder = vi.mocked(match3dWorksService.publishMatch3DWork)
|
||
.mock.invocationCallOrder[0];
|
||
expect(assetPersistCallOrder).toBeDefined();
|
||
expect(publishCallOrder).toBeDefined();
|
||
expect(assetPersistCallOrder!).toBeLessThan(publishCallOrder!);
|
||
});
|
||
|
||
test('结果页提供多 Tab,物品素材点击后进入独立预览面板', () => {
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({ themeText: '水果' })}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByRole('button', { name: '作品信息' })).toBeTruthy();
|
||
expect(screen.getByRole('button', { name: '难度配置' })).toBeTruthy();
|
||
expect(screen.getByRole('button', { name: '素材配置' })).toBeTruthy();
|
||
expect(screen.queryByRole('button', { name: '音乐' })).toBeNull();
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
expect(screen.getByRole('button', { name: '物品' })).toBeTruthy();
|
||
expect(screen.getByRole('button', { name: 'UI素材' })).toBeTruthy();
|
||
expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
|
||
expect(screen.getAllByRole('button', { name: /打开.+物品素材/u }))
|
||
.toHaveLength(20);
|
||
|
||
fireEvent.click(
|
||
screen.getByRole('button', { name: '打开水果核心物件物品素材' }),
|
||
);
|
||
|
||
expect(screen.getByRole('dialog', { name: /水果核心物件/u })).toBeTruthy();
|
||
expect(screen.getByText('素材名称')).toBeTruthy();
|
||
expect(screen.queryByText('暂无音效')).toBeNull();
|
||
expect(screen.queryByLabelText('生成点击音效,10泥点')).toBeNull();
|
||
expect(screen.queryByRole('button', { name: '重新生成' })).toBeNull();
|
||
expect(screen.queryByText('用途')).toBeNull();
|
||
});
|
||
|
||
test('物品素材列表支持删除单项并写回剩余素材', async () => {
|
||
const profile = createProfile({
|
||
generatedItemAssets: [
|
||
{
|
||
itemId: 'match3d-item-1',
|
||
itemName: '草莓',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/i1/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/i1/image.png',
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: null,
|
||
subscriptionKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
{
|
||
itemId: 'match3d-item-2',
|
||
itemName: '苹果',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/i2/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/i2/image.png',
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: null,
|
||
subscriptionKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
],
|
||
});
|
||
vi.mocked(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).mockResolvedValue({
|
||
item: createProfile({
|
||
generatedItemAssets: [profile.generatedItemAssets![1]!],
|
||
}),
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(
|
||
screen.getAllByRole('button', { name: '删除物品素材' })[0]!,
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).toHaveBeenCalledWith(
|
||
profile.profileId,
|
||
expect.objectContaining({
|
||
generatedItemAssets: expect.arrayContaining([
|
||
expect.objectContaining({
|
||
itemId: 'match3d-item-2',
|
||
itemName: '苹果',
|
||
}),
|
||
]),
|
||
}),
|
||
);
|
||
});
|
||
expect(
|
||
screen.queryByRole('button', { name: '打开草莓物品素材' }),
|
||
).toBeNull();
|
||
expect(
|
||
screen.getByRole('button', { name: '打开苹果物品素材' }),
|
||
).toBeTruthy();
|
||
});
|
||
|
||
test('批量新增物品会解析名称并调用作品素材生成接口', async () => {
|
||
const generatedItemAssets = [
|
||
{
|
||
itemId: 'match3d-item-1',
|
||
itemName: '草莓',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/i1/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/i1/image.png',
|
||
modelSrc:
|
||
'/generated-match3d-assets/session/profile/items/i1/model/model.glb',
|
||
modelObjectKey:
|
||
'generated-match3d-assets/session/profile/items/i1/model/model.glb',
|
||
modelFileName: 'model.glb',
|
||
taskUuid: 'task-1',
|
||
subscriptionKey: 'sub-1',
|
||
status: 'model_ready',
|
||
error: null,
|
||
},
|
||
];
|
||
const profile = createProfile({ generatedItemAssets: [] });
|
||
const onSaved = vi.fn();
|
||
vi.mocked(match3dWorksService.generateMatch3DItemAssets).mockResolvedValue({
|
||
item: createProfile({ generatedItemAssets }),
|
||
generatedItemAssets,
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onSaved={onSaved}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '批量新增' }));
|
||
fireEvent.change(screen.getByLabelText('物品名称 1'), {
|
||
target: { value: '草莓' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '新增物品名称' }));
|
||
fireEvent.change(screen.getByLabelText('物品名称 2'), {
|
||
target: { value: '苹果' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '新增物品名称' }));
|
||
fireEvent.change(screen.getByLabelText('物品名称 3'), {
|
||
target: { value: '蓝莓' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '新增物品名称' }));
|
||
fireEvent.change(screen.getByLabelText('物品名称 4'), {
|
||
target: { value: '苹果' },
|
||
});
|
||
expect(
|
||
screen.getByRole('button', { name: /生成物品素材 · 2泥点/u }),
|
||
).toBeTruthy();
|
||
fireEvent.click(screen.getByRole('button', { name: /生成物品素材/u }));
|
||
confirmPointCost();
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
match3dWorksService.generateMatch3DItemAssets,
|
||
).toHaveBeenCalledWith(profile.profileId, {
|
||
itemNames: ['草莓', '苹果', '蓝莓'],
|
||
});
|
||
expect(onSaved).toHaveBeenCalledWith(
|
||
expect.objectContaining({ generatedItemAssets }),
|
||
);
|
||
});
|
||
expect(screen.getByRole('dialog', { name: '批量新增物品' })).toBeTruthy();
|
||
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
|
||
expect(screen.queryByRole('dialog', { name: '批量新增物品' })).toBeNull();
|
||
expect(
|
||
screen.getByRole('button', { name: '打开草莓物品素材' }),
|
||
).toBeTruthy();
|
||
});
|
||
|
||
test('批量新增面板关闭后素材列表继续显示生成进度', async () => {
|
||
const deferred = createDeferred<{
|
||
item: Match3DWorkProfile;
|
||
generatedItemAssets: NonNullable<
|
||
Match3DWorkProfile['generatedItemAssets']
|
||
>;
|
||
}>();
|
||
vi.mocked(match3dWorksService.generateMatch3DItemAssets).mockReturnValue(
|
||
deferred.promise,
|
||
);
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({ generatedItemAssets: [] })}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '批量新增' }));
|
||
fireEvent.change(screen.getByLabelText('物品名称 1'), {
|
||
target: { value: '草莓' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: /生成物品素材/u }));
|
||
confirmPointCost();
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getAllByText('生成中').length).toBeGreaterThan(0);
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
|
||
|
||
expect(screen.queryByRole('dialog', { name: '批量新增物品' })).toBeNull();
|
||
expect(screen.getByLabelText('物品素材生成进度')).toBeTruthy();
|
||
|
||
deferred.resolve({
|
||
item: createProfile({
|
||
generatedItemAssets: [createReadyGeneratedItemAsset(1)],
|
||
}),
|
||
generatedItemAssets: [createReadyGeneratedItemAsset(1)],
|
||
});
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('生成完成')).toBeTruthy();
|
||
expect(
|
||
screen.getByRole('button', { name: '打开物品1物品素材' }),
|
||
).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
test('批量重新生成会收集已有物品名称并按替换模式调用素材生成接口', async () => {
|
||
const generatedItemAssets = [
|
||
{ ...createReadyGeneratedItemAsset(1), itemName: '草莓' },
|
||
{ ...createReadyGeneratedItemAsset(2), itemName: '苹果' },
|
||
];
|
||
const regeneratedAssets = [
|
||
{
|
||
...generatedItemAssets[0]!,
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/item-1/new-image.png',
|
||
},
|
||
generatedItemAssets[1]!,
|
||
];
|
||
const profile = createProfile({ generatedItemAssets });
|
||
const onSaved = vi.fn();
|
||
vi.mocked(match3dWorksService.generateMatch3DItemAssets).mockResolvedValue({
|
||
item: createProfile({ generatedItemAssets: regeneratedAssets }),
|
||
generatedItemAssets: regeneratedAssets,
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onSaved={onSaved}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '批量重新生成' }));
|
||
expect(
|
||
screen.getByRole('dialog', { name: '批量重新生成物品' }),
|
||
).toBeTruthy();
|
||
expect(screen.getByLabelText('重新生成物品名称 1')).toHaveProperty(
|
||
'value',
|
||
'草莓',
|
||
);
|
||
expect(screen.getByLabelText('重新生成物品名称 2')).toHaveProperty(
|
||
'value',
|
||
'苹果',
|
||
);
|
||
fireEvent.change(screen.getByLabelText('重新生成物品名称 2'), {
|
||
target: { value: '' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: /重新生成物品素材/u }));
|
||
confirmPointCost();
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
match3dWorksService.generateMatch3DItemAssets,
|
||
).toHaveBeenCalledWith(profile.profileId, {
|
||
itemNames: ['草莓'],
|
||
mode: 'replace',
|
||
});
|
||
expect(onSaved).toHaveBeenCalledWith(
|
||
expect.objectContaining({ generatedItemAssets: regeneratedAssets }),
|
||
);
|
||
expect(
|
||
screen.getAllByText('已重新生成 1 种物品素材').length,
|
||
).toBeGreaterThan(0);
|
||
});
|
||
expect(
|
||
screen.getByRole('button', { name: '打开草莓物品素材' }),
|
||
).toBeTruthy();
|
||
});
|
||
|
||
test('批量重新生成只提交能匹配到的已有物品名称', async () => {
|
||
const generatedItemAssets = [
|
||
{ ...createReadyGeneratedItemAsset(1), itemName: '草莓' },
|
||
{ ...createReadyGeneratedItemAsset(2), itemName: '苹果' },
|
||
{ ...createReadyGeneratedItemAsset(3), itemName: '梨子' },
|
||
{ ...createReadyGeneratedItemAsset(4), itemName: '香蕉' },
|
||
{ ...createReadyGeneratedItemAsset(5), itemName: '葡萄' },
|
||
{ ...createReadyGeneratedItemAsset(6), itemName: '橙子' },
|
||
];
|
||
const profile = createProfile({ generatedItemAssets });
|
||
vi.mocked(match3dWorksService.generateMatch3DItemAssets).mockResolvedValue({
|
||
item: profile,
|
||
generatedItemAssets,
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '批量重新生成' }));
|
||
fireEvent.change(screen.getByLabelText('重新生成物品名称 1'), {
|
||
target: { value: '草莓' },
|
||
});
|
||
fireEvent.change(screen.getByLabelText('重新生成物品名称 2'), {
|
||
target: { value: '不存在' },
|
||
});
|
||
fireEvent.change(screen.getByLabelText('重新生成物品名称 3'), {
|
||
target: { value: '梨子' },
|
||
});
|
||
fireEvent.change(screen.getByLabelText('重新生成物品名称 4'), {
|
||
target: { value: '' },
|
||
});
|
||
fireEvent.change(screen.getByLabelText('重新生成物品名称 5'), {
|
||
target: { value: '' },
|
||
});
|
||
fireEvent.change(screen.getByLabelText('重新生成物品名称 6'), {
|
||
target: { value: '' },
|
||
});
|
||
|
||
expect(
|
||
screen.getByRole('button', { name: /重新生成物品素材 · 2泥点/u }),
|
||
).toBeTruthy();
|
||
fireEvent.click(screen.getByRole('button', { name: /重新生成物品素材/u }));
|
||
confirmPointCost();
|
||
|
||
await waitFor(() => {
|
||
expect(
|
||
match3dWorksService.generateMatch3DItemAssets,
|
||
).toHaveBeenCalledWith(profile.profileId, {
|
||
itemNames: ['草莓', '梨子'],
|
||
mode: 'replace',
|
||
});
|
||
});
|
||
});
|
||
|
||
test('难度配置对齐入口页并派生消除次数与物品数量', async () => {
|
||
const onSaved = vi.fn();
|
||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||
item: createProfile({ clearCount: 21, difficulty: 8 }),
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({ clearCount: 12, difficulty: 4 })}
|
||
onBack={() => {}}
|
||
onSaved={onSaved}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '难度配置' }));
|
||
|
||
expect(screen.getByRole('button', { name: '轻松 8次 · 3种' })).toBeTruthy();
|
||
const difficultySlider = screen.getByRole('slider', { name: '难度' });
|
||
expect((difficultySlider as HTMLInputElement).value).toBe('1');
|
||
expect(
|
||
screen
|
||
.getByRole('button', { name: '标准 12次 · 9种' })
|
||
.getAttribute('aria-pressed'),
|
||
).toBe('true');
|
||
expect(screen.getByText('36 件')).toBeTruthy();
|
||
expect(screen.getAllByText('9 种').length).toBeGreaterThan(0);
|
||
|
||
fireEvent.change(difficultySlider, { target: { value: '3' } });
|
||
|
||
await waitFor(() => {
|
||
expect(match3dWorksService.updateMatch3DWork).toHaveBeenCalledWith(
|
||
'match3d-profile-1',
|
||
expect.objectContaining({
|
||
clearCount: 21,
|
||
difficulty: 8,
|
||
}),
|
||
);
|
||
});
|
||
expect(screen.getByText('63 件')).toBeTruthy();
|
||
expect(screen.getAllByText('20 种').length).toBeGreaterThan(0);
|
||
expect(onSaved).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
clearCount: 21,
|
||
difficulty: 8,
|
||
}),
|
||
);
|
||
});
|
||
|
||
test('试玩使用当前 2D 多视角素材进入运行态', async () => {
|
||
const generatedItemAssets = [createReadyGeneratedItemAsset(1)];
|
||
const profile = createProfile({ generatedItemAssets });
|
||
const onStartTestRun = vi.fn();
|
||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||
item: profile,
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={onStartTestRun}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||
|
||
await waitFor(() => {
|
||
expect(onStartTestRun).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
generatedItemAssets: [
|
||
expect.objectContaining({
|
||
itemId: 'match3d-item-1',
|
||
imageViews: expect.arrayContaining([
|
||
expect.objectContaining({ viewId: 'view-01' }),
|
||
]),
|
||
}),
|
||
],
|
||
}),
|
||
{
|
||
itemTypeCountOverride: 1,
|
||
},
|
||
);
|
||
});
|
||
});
|
||
|
||
test('结果页优先展示生成出来的 2D 视角素材', () => {
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({
|
||
clearCount: 3,
|
||
generatedItemAssets: [createReadyGeneratedItemAsset(1)],
|
||
})}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '打开物品1物品素材' }));
|
||
|
||
expect(screen.getByDisplayValue('物品1')).toBeTruthy();
|
||
expect(
|
||
[...document.querySelectorAll('img')].some((image) =>
|
||
image
|
||
.getAttribute('src')
|
||
?.includes(
|
||
'generated-match3d-assets/session/profile/items/item-1/views/view-01.png',
|
||
),
|
||
),
|
||
).toBe(true);
|
||
});
|
||
|
||
test('物品详情五视角预览不混入兼容首图', () => {
|
||
const generatedAsset = createReadyGeneratedItemAsset(1);
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({
|
||
clearCount: 3,
|
||
generatedItemAssets: [
|
||
{
|
||
...generatedAsset,
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/item-1/legacy-primary.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/item-1/legacy-primary.png',
|
||
},
|
||
],
|
||
})}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '打开物品1物品素材' }));
|
||
|
||
const imageSources = [...document.querySelectorAll('img')].map(
|
||
(image) => image.getAttribute('src') ?? '',
|
||
);
|
||
expect(
|
||
imageSources.some((source) => source.includes('legacy-primary.png')),
|
||
).toBe(false);
|
||
expect(
|
||
imageSources.some((source) => source.includes('views/view-05.png')),
|
||
).toBe(true);
|
||
});
|
||
|
||
test('物品详情五视角预览使用上方大图和底部缩略图栏', () => {
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({
|
||
clearCount: 3,
|
||
generatedItemAssets: [createReadyGeneratedItemAsset(1)],
|
||
})}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '打开物品1物品素材' }));
|
||
|
||
const preview = screen.getByLabelText('物品1五视角预览');
|
||
const stage = screen.getByTestId('match3d-item-preview-stage');
|
||
const focusImage = screen.getByTestId('match3d-item-preview-focus-image');
|
||
const thumbnails = screen.getByTestId('match3d-item-preview-thumbnails');
|
||
expect(stage.className).toContain('aspect-square');
|
||
expect(stage.className).toContain('max-w-[22rem]');
|
||
expect(focusImage.className).toContain('place-items-center');
|
||
expect(focusImage.querySelector('img')?.className).toContain('p-3');
|
||
expect(thumbnails.style.gridAutoColumns).toBe('calc((100% - 1.5rem) / 4)');
|
||
expect(preview.querySelectorAll('img')).toHaveLength(6);
|
||
expect(
|
||
screen
|
||
.getByRole('button', { name: '切换物品1视角3' })
|
||
.getAttribute('aria-pressed'),
|
||
).toBe('true');
|
||
expect(
|
||
screen.queryByTestId('match3d-item-preview-focus-frame'),
|
||
).toBeNull();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '切换物品1视角5' }));
|
||
expect(
|
||
screen
|
||
.getByTestId('match3d-item-preview-focus-image')
|
||
.getAttribute('data-preview-src'),
|
||
).toContain('views/view-05.png');
|
||
});
|
||
|
||
test('草稿阶段仅有切割图片时展示 2D 素材', () => {
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({
|
||
clearCount: 3,
|
||
generatedItemAssets: [
|
||
{
|
||
itemId: 'match3d-item-1',
|
||
itemName: '草莓',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/strawberry/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/strawberry/image.png',
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: null,
|
||
subscriptionKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
],
|
||
})}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '打开草莓物品素材' }));
|
||
|
||
expect(screen.getByDisplayValue('草莓')).toBeTruthy();
|
||
expect(
|
||
[...document.querySelectorAll('img')].some((image) =>
|
||
image.getAttribute('src')?.includes('items/strawberry/image.png'),
|
||
),
|
||
).toBe(true);
|
||
expect(screen.queryByRole('link', { name: /\.glb/u })).toBeNull();
|
||
});
|
||
|
||
test('重进草稿页时从持久化 profile 素材恢复 2D 素材列表', () => {
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({
|
||
clearCount: 3,
|
||
generatedItemAssets: [
|
||
{
|
||
itemId: 'match3d-item-1',
|
||
itemName: '草莓',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: null,
|
||
subscriptionKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
{
|
||
itemId: 'match3d-item-2',
|
||
itemName: '苹果',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/match3d-item-2-item/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/match3d-item-2-item/image.png',
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: null,
|
||
subscriptionKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
],
|
||
})}
|
||
draft={null}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
|
||
expect(
|
||
screen.getByRole('button', { name: '打开草莓物品素材' }),
|
||
).toBeTruthy();
|
||
expect(
|
||
screen.getByRole('button', { name: '打开苹果物品素材' }),
|
||
).toBeTruthy();
|
||
expect(
|
||
screen.queryByRole('button', { name: '打开水果核心物件物品素材' }),
|
||
).toBeNull();
|
||
});
|
||
|
||
test('素材配置 UI素材子 Tab 仅预览背景图和UI spritesheet', () => {
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({
|
||
backgroundPrompt: '果园主题抓大鹅竖屏背景',
|
||
backgroundImageSrc:
|
||
'/generated-match3d-assets/session/profile/background/background.png',
|
||
generatedBackgroundAsset: {
|
||
prompt: '果园主题抓大鹅竖屏背景',
|
||
levelScenePrompt: '果园完整关卡画面',
|
||
levelSceneImageSrc:
|
||
'/generated-match3d-assets/session/profile/level-scene/scene.png',
|
||
levelSceneImageObjectKey: null,
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/background/background.png',
|
||
imageObjectKey: null,
|
||
uiSpritesheetPrompt: 'UI spritesheet',
|
||
uiSpritesheetImageSrc:
|
||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||
uiSpritesheetImageObjectKey: null,
|
||
containerPrompt: null,
|
||
containerImageSrc: null,
|
||
containerImageObjectKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
})}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
|
||
|
||
expect(screen.getByAltText('游戏背景图').getAttribute('src')).toBe(
|
||
'/generated-match3d-assets/session/profile/background/background.png',
|
||
);
|
||
expect(screen.getByAltText('UI素材图').getAttribute('src')).toBe(
|
||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||
);
|
||
expect(
|
||
screen.queryByLabelText('UI背景图画面描述提示词'),
|
||
).toBeNull();
|
||
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
|
||
expect(screen.getByRole('dialog', { name: 'UI预览' })).toBeTruthy();
|
||
expect(screen.getByText('第 1 关')).toBeTruthy();
|
||
expect(screen.getByText('抓大鹅')).toBeTruthy();
|
||
expect(screen.getByText('1:30')).toBeTruthy();
|
||
const previewBoard = screen.getByTestId('match3d-ui-preview-board');
|
||
expect(previewBoard.className).toContain('bg-transparent');
|
||
expect(previewBoard.className).not.toContain('rounded-full');
|
||
const containerImage = document.querySelector(
|
||
'img[src="/match3d-background-references/pot-fused-reference.png"]',
|
||
);
|
||
expect(containerImage).toBeTruthy();
|
||
expect(containerImage?.className).toContain('w-[min(116vw,42rem)]');
|
||
expect(containerImage?.className).toContain('-translate-x-1/2');
|
||
expect(
|
||
document.querySelector('.animate-spin, [class*="border-l-transparent"]'),
|
||
).toBeNull();
|
||
expect(
|
||
document.querySelector(
|
||
'svg[class*="lucide-settings"], [data-lucide="settings"]',
|
||
),
|
||
).toBeTruthy();
|
||
});
|
||
|
||
test('素材配置 UI素材子 Tab 从物品挂载资产展示生成背景和UI spritesheet', async () => {
|
||
const onStartTestRun = vi.fn();
|
||
const profile = createProfile({
|
||
backgroundPrompt: null,
|
||
backgroundImageSrc: null,
|
||
backgroundImageObjectKey: null,
|
||
generatedBackgroundAsset: null,
|
||
generatedItemAssets: [
|
||
{
|
||
...createReadyGeneratedItemAsset(1),
|
||
itemName: '草莓',
|
||
backgroundAsset: {
|
||
prompt: '果园背景',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/background/background.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/background/background.png',
|
||
uiSpritesheetPrompt: '果园UI素材',
|
||
uiSpritesheetImageSrc:
|
||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||
uiSpritesheetImageObjectKey:
|
||
'generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||
containerPrompt: null,
|
||
containerImageSrc: null,
|
||
containerImageObjectKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
},
|
||
],
|
||
});
|
||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||
item: createProfile({ generatedItemAssets: [] }),
|
||
});
|
||
vi.mocked(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).mockResolvedValue({
|
||
item: profile,
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={onStartTestRun}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
|
||
|
||
expect(screen.getByAltText('游戏背景图').getAttribute('src')).toBe(
|
||
'/generated-match3d-assets/session/profile/background/background.png',
|
||
);
|
||
expect(screen.getByAltText('UI素材图').getAttribute('src')).toBe(
|
||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||
);
|
||
expect(screen.queryByLabelText('UI背景图画面描述提示词')).toBeNull();
|
||
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
|
||
expect(
|
||
document.querySelector(
|
||
'img[src="/generated-match3d-assets/session/profile/background/background.png"]',
|
||
),
|
||
).toBeTruthy();
|
||
expect(
|
||
document.querySelector(
|
||
'img[src="/generated-match3d-assets/session/profile/ui-spritesheet/ui.png"]',
|
||
),
|
||
).toBeTruthy();
|
||
expect(
|
||
document.querySelector(
|
||
'img[src="/match3d-background-references/pot-fused-reference.png"]',
|
||
),
|
||
).toBeTruthy();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||
|
||
await waitFor(() => {
|
||
expect(onStartTestRun).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
backgroundPrompt: '果园背景',
|
||
backgroundImageSrc:
|
||
'/generated-match3d-assets/session/profile/background/background.png',
|
||
generatedBackgroundAsset: expect.objectContaining({
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/background/background.png',
|
||
uiSpritesheetImageSrc:
|
||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||
}),
|
||
}),
|
||
{
|
||
itemTypeCountOverride: 1,
|
||
},
|
||
);
|
||
});
|
||
});
|
||
|
||
test('素材配置 UI素材子 Tab 预览物品spritesheet解析结果', async () => {
|
||
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions.mockResolvedValue(
|
||
Array.from({ length: 100 }, (_, index) => ({
|
||
label: `素材${index + 1}`,
|
||
x: index * 2,
|
||
y: 0,
|
||
width: 2,
|
||
height: 2,
|
||
sheetWidth: 200,
|
||
sheetHeight: 2,
|
||
imageSrc: `data:image/png;base64,item-${index + 1}`,
|
||
})),
|
||
);
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={createProfile({
|
||
generatedItemAssets: [
|
||
{ ...createReadyGeneratedItemAsset(1), itemName: '草莓' },
|
||
{ ...createReadyGeneratedItemAsset(2), itemName: '苹果' },
|
||
],
|
||
generatedBackgroundAsset: {
|
||
prompt: '果园主题抓大鹅竖屏背景',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/background/background.png',
|
||
imageObjectKey: null,
|
||
uiSpritesheetPrompt: 'UI spritesheet',
|
||
uiSpritesheetImageSrc:
|
||
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
|
||
uiSpritesheetImageObjectKey: null,
|
||
itemSpritesheetPrompt: '物品 spritesheet',
|
||
itemSpritesheetImageSrc:
|
||
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
|
||
itemSpritesheetImageObjectKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
})}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
|
||
|
||
expect(screen.getByAltText('物品素材图').getAttribute('src')).toBe(
|
||
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
|
||
);
|
||
await waitFor(() => {
|
||
expect(
|
||
screen
|
||
.getByTestId('match3d-item-spritesheet-preview-0-0')
|
||
.getAttribute('src'),
|
||
).toBe('data:image/png;base64,item-1');
|
||
expect(
|
||
screen
|
||
.getByTestId('match3d-item-spritesheet-preview-1-4')
|
||
.getAttribute('src'),
|
||
).toBe('data:image/png;base64,item-10');
|
||
});
|
||
expect(screen.getByText('草莓')).toBeTruthy();
|
||
expect(screen.getByText('苹果')).toBeTruthy();
|
||
expect(
|
||
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions,
|
||
).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
maxRegions: 100,
|
||
source:
|
||
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
|
||
}),
|
||
);
|
||
});
|
||
|
||
test('素材配置 UI素材子 Tab 不提供背景或容器重新生成入口', () => {
|
||
const profile = createProfile({
|
||
generatedItemAssets: [
|
||
{
|
||
itemId: 'match3d-item-1',
|
||
itemName: '草莓',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: null,
|
||
subscriptionKey: null,
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
],
|
||
generatedBackgroundAsset: {
|
||
prompt: '旧背景提示词',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/background/old/background.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/background/old/background.png',
|
||
containerPrompt: '旧容器提示词',
|
||
containerImageSrc:
|
||
'/generated-match3d-assets/session/profile/ui-container/old/container.png',
|
||
containerImageObjectKey:
|
||
'generated-match3d-assets/session/profile/ui-container/old/container.png',
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
|
||
|
||
expect(screen.queryByLabelText('UI背景图画面描述提示词')).toBeNull();
|
||
expect(screen.queryByLabelText('容器形象画面描述提示词')).toBeNull();
|
||
expect(screen.queryByRole('button', { name: '容器形象' })).toBeNull();
|
||
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
|
||
expect(match3dWorksService.generateMatch3DBackgroundImage).not.toHaveBeenCalled();
|
||
expect(match3dWorksService.generateMatch3DContainerImage).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('历史草稿同时带旧 draft 和 profile 素材时以 profile 多视角素材补齐试玩资产', async () => {
|
||
const draftAsset = {
|
||
...createReadyGeneratedItemAsset(1),
|
||
imageViews: [],
|
||
};
|
||
const profileAsset = {
|
||
...createReadyGeneratedItemAsset(1),
|
||
itemName: '草莓',
|
||
};
|
||
const profile = createProfile({ generatedItemAssets: [profileAsset] });
|
||
const onStartTestRun = vi.fn();
|
||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||
item: createProfile({ generatedItemAssets: [] }),
|
||
});
|
||
vi.mocked(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).mockResolvedValue({
|
||
item: createProfile({ generatedItemAssets: [profileAsset] }),
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
draft={{
|
||
profileId: profile.profileId,
|
||
gameName: profile.gameName,
|
||
themeText: profile.themeText,
|
||
summary: profile.summary,
|
||
tags: profile.tags,
|
||
coverImageSrc: null,
|
||
referenceImageSrc: null,
|
||
clearCount: profile.clearCount,
|
||
difficulty: profile.difficulty,
|
||
generatedItemAssets: [draftAsset],
|
||
}}
|
||
onBack={() => {}}
|
||
onStartTestRun={onStartTestRun}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '打开草莓物品素材' }));
|
||
|
||
expect(screen.getByDisplayValue('草莓')).toBeTruthy();
|
||
expect(
|
||
[...document.querySelectorAll('img')].some((image) =>
|
||
image.getAttribute('src')?.includes('views/view-01.png'),
|
||
),
|
||
).toBe(true);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||
|
||
await waitFor(() => {
|
||
expect(onStartTestRun).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
generatedItemAssets: [
|
||
expect.objectContaining({
|
||
itemId: 'match3d-item-1',
|
||
imageViews: expect.arrayContaining([
|
||
expect.objectContaining({ viewId: 'view-01' }),
|
||
]),
|
||
status: 'image_ready',
|
||
}),
|
||
],
|
||
}),
|
||
{
|
||
itemTypeCountOverride: 1,
|
||
},
|
||
);
|
||
});
|
||
});
|
||
|
||
test('物品详情隐藏点击音效生成入口', () => {
|
||
const profile = createProfile({
|
||
generatedItemAssets: [
|
||
{
|
||
itemId: 'match3d-item-1',
|
||
itemName: '草莓',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: null,
|
||
subscriptionKey: null,
|
||
soundPrompt: '草莓清脆点击音效',
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
],
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '打开草莓物品素材' }));
|
||
|
||
expect(screen.getByRole('dialog', { name: /草莓/u })).toBeTruthy();
|
||
expect(screen.queryByLabelText('草莓点击音效提示词')).toBeNull();
|
||
expect(screen.queryByRole('button', { name: /生成点击音效/u })).toBeNull();
|
||
expect(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('素材配置隐藏背景音乐子 Tab', () => {
|
||
const profile = createProfile({
|
||
generatedItemAssets: [
|
||
{
|
||
itemId: 'match3d-item-1',
|
||
itemName: '草莓',
|
||
imageSrc:
|
||
'/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
|
||
imageObjectKey:
|
||
'generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
|
||
modelSrc: null,
|
||
modelObjectKey: null,
|
||
modelFileName: null,
|
||
taskUuid: null,
|
||
subscriptionKey: null,
|
||
soundPrompt: '草莓点击音效',
|
||
backgroundMusicTitle: '果园轻舞',
|
||
backgroundMusicStyle: '轻快, 休闲',
|
||
backgroundMusicPrompt: '果园主题循环背景音乐',
|
||
status: 'image_ready',
|
||
error: null,
|
||
},
|
||
],
|
||
});
|
||
|
||
render(
|
||
<Match3DResultView
|
||
profile={profile}
|
||
onBack={() => {}}
|
||
onStartTestRun={() => {}}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||
expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
|
||
expect(screen.queryByLabelText('抓大鹅背景音乐曲名')).toBeNull();
|
||
expect(screen.queryByLabelText('抓大鹅背景音乐风格')).toBeNull();
|
||
expect(screen.queryByLabelText('抓大鹅背景音乐提示词')).toBeNull();
|
||
expect(screen.queryByRole('button', { name: /生成音乐/u })).toBeNull();
|
||
expect(
|
||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||
).not.toHaveBeenCalled();
|
||
});
|
||
|
||
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: '素材配置' }));
|
||
expect(screen.queryByRole('button', { name: '背景音乐' })).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 }),
|
||
);
|
||
});
|
||
});
|
||
});
|