1
This commit is contained in:
@@ -4,6 +4,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { Match3DWorkProfile } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import * as assetReadUrlService from '../../services/assetReadUrlService';
|
||||
import * as hyper3dService from '../../services/hyper3dModelGenerationService';
|
||||
import * as match3dWorksService from '../../services/match3d-works';
|
||||
import { Match3DResultView } from './Match3DResultView';
|
||||
|
||||
@@ -19,13 +21,44 @@ vi.mock('../ResolvedAssetImage', () => ({
|
||||
}) => (src ? <img src={src} alt={alt} className={className} /> : null),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useResolvedAssetReadUrl', () => ({
|
||||
useResolvedAssetReadUrl: (src?: string | null) => ({
|
||||
resolvedUrl: src?.startsWith('/generated-')
|
||||
? `https://signed.example.com${src}`
|
||||
: (src ?? ''),
|
||||
isResolving: false,
|
||||
shouldResolve: Boolean(src?.startsWith('/generated-')),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/assetReadUrlService', () => ({
|
||||
readAssetBytes: vi.fn(() =>
|
||||
Promise.resolve(
|
||||
new Response(new Uint8Array([104, 101, 108, 108, 111]), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/match3d-works', () => ({
|
||||
publishMatch3DWork: vi.fn(),
|
||||
updateMatch3DWork: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/hyper3dModelGenerationService', () => ({
|
||||
getHyper3dDownloads: vi.fn(),
|
||||
getHyper3dTaskStatus: vi.fn(),
|
||||
submitHyper3dImageToModel: vi.fn(),
|
||||
submitHyper3dTextToModel: vi.fn(),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function createProfile(
|
||||
@@ -100,4 +133,265 @@ describe('Match3DResultView', () => {
|
||||
fireEvent.click(publishButton);
|
||||
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('结果页提供多 Tab,并能进入 Rodin 3D 素材详情', () => {
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={createProfile({ themeText: '水果' })}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: '作品信息' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '玩法配置' })).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole('button', { name: '3D素材' }));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /水果核心物件/u }));
|
||||
|
||||
expect(screen.getByText('素材名称')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '文生模型' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '图生模型' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Rodin 文生模型提交使用 Hyper3D 代理', async () => {
|
||||
vi.mocked(hyper3dService.submitHyper3dTextToModel).mockResolvedValue({
|
||||
ok: true,
|
||||
provider: 'hyper3d-rodin',
|
||||
mode: 'text-to-model',
|
||||
taskUuid: 'task-1',
|
||||
subscriptionKey: 'sub-1',
|
||||
jobUuids: ['job-1'],
|
||||
message: 'submitted',
|
||||
tier: 'Gen-2',
|
||||
});
|
||||
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={createProfile({ themeText: '水果' })}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '3D素材' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /水果核心物件/u }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(hyper3dService.submitHyper3dTextToModel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
geometryFileFormat: 'glb',
|
||||
material: 'PBR',
|
||||
meshMode: 'Quad',
|
||||
prompt: expect.stringContaining('水果核心物件'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('排队中').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Rodin 图生模型没有参考图时阻止提交', async () => {
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={createProfile({ themeText: '水果' })}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '3D素材' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /水果核心物件/u }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '图生模型' }));
|
||||
|
||||
const generateButton = screen.getByRole('button', { name: '生成' });
|
||||
expect(generateButton).toHaveProperty('disabled', true);
|
||||
expect(hyper3dService.submitHyper3dImageToModel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('结果页优先预览生成出来的物品图片和模型文件', () => {
|
||||
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: '/generated-match3d-assets/session/profile/items/strawberry/model/model.glb',
|
||||
modelObjectKey:
|
||||
'generated-match3d-assets/session/profile/items/strawberry/model/model.glb',
|
||||
modelFileName: 'strawberry.glb',
|
||||
taskUuid: 'task-strawberry',
|
||||
subscriptionKey: 'sub-strawberry',
|
||||
status: 'model_ready',
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '3D素材' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /草莓/u }));
|
||||
|
||||
expect(screen.getByDisplayValue('草莓')).toBeTruthy();
|
||||
expect(screen.getAllByText('已完成').length).toBeGreaterThan(0);
|
||||
const modelLink = screen.getByRole('link', { name: /strawberry\.glb/u });
|
||||
expect(modelLink.getAttribute('href')).toBe(
|
||||
'https://signed.example.com/generated-match3d-assets/session/profile/items/strawberry/model/model.glb',
|
||||
);
|
||||
});
|
||||
|
||||
test('草稿阶段仅有切割图片时展示图片已就绪,不要求模型文件', () => {
|
||||
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: '3D素材' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /草莓/u }));
|
||||
|
||||
expect(screen.getByDisplayValue('草莓')).toBeTruthy();
|
||||
expect(screen.getAllByText('图片已就绪').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('0 文件')).toBeTruthy();
|
||||
expect(screen.queryByRole('link', { name: /\.glb/u })).toBeNull();
|
||||
});
|
||||
|
||||
test('重进草稿页时从持久化 profile 素材恢复 3D 素材列表', () => {
|
||||
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: '3D素材' }));
|
||||
|
||||
expect(screen.getByRole('button', { name: /草莓/u })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /苹果/u })).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: /水果核心物件/u })).toBeNull();
|
||||
});
|
||||
|
||||
test('Rodin 图生模型提交前会把 generated 参考图转成 data URL', async () => {
|
||||
vi.mocked(hyper3dService.submitHyper3dImageToModel).mockResolvedValue({
|
||||
ok: true,
|
||||
provider: 'hyper3d-rodin',
|
||||
mode: 'image-to-model',
|
||||
taskUuid: 'task-image',
|
||||
subscriptionKey: 'sub-image',
|
||||
jobUuids: ['job-image'],
|
||||
message: 'submitted',
|
||||
tier: 'Gen-2',
|
||||
});
|
||||
vi.stubGlobal('fetch', vi.fn());
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '3D素材' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /草莓/u }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(assetReadUrlService.readAssetBytes).toHaveBeenCalledWith(
|
||||
'/generated-match3d-assets/session/profile/items/match3d-item-1-item/image.png',
|
||||
expect.objectContaining({ expireSeconds: 300 }),
|
||||
);
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
expect(hyper3dService.submitHyper3dImageToModel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
imageDataUrls: ['data:image/png;base64,aGVsbG8='],
|
||||
prompt: expect.stringContaining('草莓'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user