@@ -205,10 +205,16 @@ describe('Match3DResultView', () => {
|
||||
];
|
||||
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
|
||||
@@ -221,10 +227,34 @@ describe('Match3DResultView', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||||
).toHaveBeenCalledWith(
|
||||
profile.profileId,
|
||||
expect.objectContaining({
|
||||
generatedItemAssets: [
|
||||
expect.objectContaining({
|
||||
itemId: 'match3d-item-1',
|
||||
modelSrc:
|
||||
'/generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
|
||||
status: 'model_ready',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(onStartTestRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
profileId: profile.profileId,
|
||||
generatedItemAssets,
|
||||
generatedItemAssets: [
|
||||
expect.objectContaining({
|
||||
itemId: 'match3d-item-1',
|
||||
modelSrc:
|
||||
'/generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
|
||||
modelObjectKey:
|
||||
'generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
|
||||
status: 'model_ready',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -246,6 +276,105 @@ describe('Match3DResultView', () => {
|
||||
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('发布前会先把历史草稿 3D 模型素材写回 profile', async () => {
|
||||
const 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:
|
||||
'generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
|
||||
modelFileName: 'strawberry.glb',
|
||||
taskUuid: 'task-strawberry',
|
||||
subscriptionKey: 'sub-strawberry',
|
||||
status: 'model_ready',
|
||||
error: null,
|
||||
},
|
||||
];
|
||||
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: '发布' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||||
).toHaveBeenCalledWith(
|
||||
profile.profileId,
|
||||
expect.objectContaining({
|
||||
generatedItemAssets: [
|
||||
expect.objectContaining({
|
||||
itemId: 'match3d-item-1',
|
||||
modelObjectKey:
|
||||
'generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
|
||||
status: 'model_ready',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(match3dWorksService.publishMatch3DWork).toHaveBeenCalledWith(
|
||||
profile.profileId,
|
||||
);
|
||||
expect(onPublished).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
publicationStatus: 'published',
|
||||
generatedItemAssets: [
|
||||
expect.objectContaining({
|
||||
itemId: 'match3d-item-1',
|
||||
modelObjectKey:
|
||||
'generated-match3d-assets/session/profile/items/match3d-item-1-item/model/model.glb',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
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,并能进入 Rodin 3D 素材详情', () => {
|
||||
render(
|
||||
<Match3DResultView
|
||||
@@ -483,6 +612,11 @@ describe('Match3DResultView', () => {
|
||||
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
|
||||
item: createProfile({ generatedItemAssets: [] }),
|
||||
});
|
||||
vi.mocked(
|
||||
match3dWorksService.updateMatch3DGeneratedItemAssets,
|
||||
).mockResolvedValue({
|
||||
item: createProfile({ generatedItemAssets: [profileAsset] }),
|
||||
});
|
||||
|
||||
render(
|
||||
<Match3DResultView
|
||||
|
||||
@@ -178,6 +178,70 @@ function hasMatch3DGeneratedModelSource(asset: Match3DGeneratedItemAsset) {
|
||||
return Boolean(asset.modelSrc?.trim() || asset.modelObjectKey?.trim());
|
||||
}
|
||||
|
||||
function hasPersistableMatch3DGeneratedItemAsset(
|
||||
asset: Match3DGeneratedItemAsset,
|
||||
) {
|
||||
return Boolean(
|
||||
asset.imageSrc?.trim() ||
|
||||
asset.imageObjectKey?.trim() ||
|
||||
asset.modelSrc?.trim() ||
|
||||
asset.modelObjectKey?.trim() ||
|
||||
asset.taskUuid?.trim() ||
|
||||
asset.subscriptionKey?.trim() ||
|
||||
asset.backgroundMusic ||
|
||||
asset.clickSound,
|
||||
);
|
||||
}
|
||||
|
||||
function getMatch3DGeneratedItemAssetPersistenceSignature(
|
||||
asset: Match3DGeneratedItemAsset,
|
||||
) {
|
||||
return [
|
||||
asset.itemId.trim(),
|
||||
asset.itemName.trim(),
|
||||
asset.imageSrc?.trim() ?? '',
|
||||
asset.imageObjectKey?.trim() ?? '',
|
||||
asset.modelSrc?.trim() ?? '',
|
||||
asset.modelObjectKey?.trim() ?? '',
|
||||
asset.modelFileName?.trim() ?? '',
|
||||
asset.taskUuid?.trim() ?? '',
|
||||
asset.subscriptionKey?.trim() ?? '',
|
||||
asset.status.trim(),
|
||||
asset.backgroundMusic?.audioSrc?.trim() ??
|
||||
asset.backgroundMusic?.assetObjectId?.trim() ??
|
||||
asset.backgroundMusic?.taskId?.trim() ??
|
||||
'',
|
||||
asset.clickSound?.audioSrc?.trim() ??
|
||||
asset.clickSound?.assetObjectId?.trim() ??
|
||||
asset.clickSound?.taskId?.trim() ??
|
||||
'',
|
||||
asset.error?.trim() ?? '',
|
||||
].join('\u001f');
|
||||
}
|
||||
|
||||
function shouldPersistGeneratedItemAssets(
|
||||
currentAssets: readonly Match3DGeneratedItemAsset[],
|
||||
savedAssets: readonly Match3DGeneratedItemAsset[] = [],
|
||||
) {
|
||||
if (currentAssets.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (currentAssets.length !== savedAssets.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return currentAssets.some(
|
||||
(asset, index) => {
|
||||
const savedAsset = savedAssets[index];
|
||||
return (
|
||||
!savedAsset ||
|
||||
getMatch3DGeneratedItemAssetPersistenceSignature(asset) !==
|
||||
getMatch3DGeneratedItemAssetPersistenceSignature(savedAsset)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function mergeMatch3DGeneratedItemAsset(
|
||||
base: Match3DGeneratedItemAsset,
|
||||
override: Match3DGeneratedItemAsset,
|
||||
@@ -636,6 +700,19 @@ function attachMatch3DGeneratedItemAssets(
|
||||
};
|
||||
}
|
||||
|
||||
function buildPersistableGeneratedItemAssets(
|
||||
assetDrafts: Match3DRodinAssetDraft[],
|
||||
generatedItemAssets: readonly Match3DGeneratedItemAsset[],
|
||||
) {
|
||||
if (generatedItemAssets.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return createGeneratedAssetsFromDrafts(assetDrafts, generatedItemAssets).filter(
|
||||
hasPersistableMatch3DGeneratedItemAsset,
|
||||
);
|
||||
}
|
||||
|
||||
function Match3DResultHeader({
|
||||
autoSaveState,
|
||||
isBusy,
|
||||
@@ -1459,8 +1536,12 @@ export function Match3DResultView({
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const playableItem = attachMatch3DGeneratedItemAssets(
|
||||
item,
|
||||
generatedItemAssets,
|
||||
);
|
||||
setAutoSaveState('saved');
|
||||
onSaved?.(item);
|
||||
onSaved?.(playableItem);
|
||||
})
|
||||
.catch((saveError) => {
|
||||
if (cancelled) {
|
||||
@@ -1477,7 +1558,7 @@ export function Match3DResultView({
|
||||
cancelled = true;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [editState, onSaved, profile]);
|
||||
}, [editState, generatedItemAssets, onSaved, profile]);
|
||||
|
||||
const saveNow = async () => {
|
||||
const payload = buildSavePayload(editState);
|
||||
@@ -1489,10 +1570,34 @@ export function Match3DResultView({
|
||||
setAutoSaveState('saving');
|
||||
setLocalError(null);
|
||||
const { item } = await updateMatch3DWork(profile.profileId, payload);
|
||||
const playableItem = attachMatch3DGeneratedItemAssets(
|
||||
item,
|
||||
const currentGeneratedItemAssets = buildPersistableGeneratedItemAssets(
|
||||
assetDrafts,
|
||||
generatedItemAssets,
|
||||
);
|
||||
let playableItem = attachMatch3DGeneratedItemAssets(
|
||||
item,
|
||||
currentGeneratedItemAssets.length > 0
|
||||
? currentGeneratedItemAssets
|
||||
: generatedItemAssets,
|
||||
);
|
||||
if (
|
||||
shouldPersistGeneratedItemAssets(
|
||||
currentGeneratedItemAssets,
|
||||
item.generatedItemAssets ?? [],
|
||||
)
|
||||
) {
|
||||
// 中文注释:历史草稿可能只在页面 draft 中带 3D 模型;试玩和发布前必须先把当前可见素材写回 profile。
|
||||
const { item: persistedItem } = await updateMatch3DGeneratedItemAssets(
|
||||
profile.profileId,
|
||||
{
|
||||
generatedItemAssets: currentGeneratedItemAssets,
|
||||
},
|
||||
);
|
||||
playableItem = attachMatch3DGeneratedItemAssets(
|
||||
persistedItem,
|
||||
currentGeneratedItemAssets,
|
||||
);
|
||||
}
|
||||
setAutoSaveState('saved');
|
||||
onSaved?.(playableItem);
|
||||
return playableItem;
|
||||
@@ -1829,7 +1934,12 @@ export function Match3DResultView({
|
||||
const { item } = await publishMatch3DWork(
|
||||
savedProfile?.profileId ?? profile.profileId,
|
||||
);
|
||||
onPublished?.(item);
|
||||
onPublished?.(
|
||||
attachMatch3DGeneratedItemAssets(
|
||||
item,
|
||||
savedProfile?.generatedItemAssets ?? generatedItemAssets,
|
||||
),
|
||||
);
|
||||
setLocalError(null);
|
||||
} catch (caughtError) {
|
||||
setLocalError(
|
||||
|
||||
Reference in New Issue
Block a user