This commit is contained in:
2026-05-10 22:20:54 +08:00
parent d6219f1a0c
commit 192accd796
92 changed files with 7045 additions and 1559 deletions

View File

@@ -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('草莓'),
}),
);
});
});
});