Files
Genarrative/src/components/custom-world-home/creationWorkShelf.test.ts
高物 a45e358e83 Add generationStatus and match3d/runtime fixes
Introduce persistent generationStatus to work summaries (puzzle & match3d) and propagate generation recovery rules across docs and frontend/backends so "generating" is restored from server-side work summary rather than ephemeral front-end notices. Update API server image/asset handling (improve match3d material sheet green/alpha decontamination and promote generatedItemAssets background fields) and add runtime improvements: alpha-based hotspot hit-testing, tray insertion/three-match animation behavior, and session re-read on client-side VectorEngine timeouts/lock-screen interruptions. Many docs, tests and related frontend modules updated/added to reflect these contract and behavior changes.
2026-05-16 22:59:02 +08:00

683 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test, vi } from 'vitest';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import {
buildCreationWorkShelfItems,
getCreationWorkShelfItemTime,
} from './creationWorkShelf';
test('buildCreationWorkShelfItems maps visual novel items with VN public code', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
visualNovelItems: [
{
runtimeKind: 'visual-novel',
profileId: 'vn-profile-demo-12345678',
ownerUserId: 'user-1',
title: '雨夜终章',
description: '失踪列车上的选择。',
coverImageSrc: '/vn-cover.png',
tags: ['悬疑', '列车'],
publishStatus: 'published',
publishReady: true,
playCount: 12,
updatedAt: '2026-05-07T00:00:00.000Z',
publishedAt: '2026-05-07T00:00:00.000Z',
},
{
runtimeKind: 'visual-novel',
profileId: 'vn-profile-draft-00000001',
ownerUserId: 'user-1',
title: '',
description: '',
coverImageSrc: null,
tags: [],
publishStatus: 'draft',
publishReady: false,
playCount: 0,
updatedAt: '2026-05-06T00:00:00.000Z',
publishedAt: null,
},
],
});
expect(items[0]?.kind).toBe('visual-novel');
expect(items[0]?.publicWorkCode).toBe('VN-12345678');
expect(items[0]?.sharePath).toContain('/works/detail?work=VN-12345678');
expect(items[1]?.status).toBe('draft');
expect(items[1]?.publicWorkCode).toBeNull();
});
test('buildCreationWorkShelfItems attaches open and delete actions through shelf adapters', () => {
const onOpenPuzzleDetail = vi.fn();
const onDeletePuzzle = vi.fn();
const puzzleWork = {
workId: 'puzzle:work-action',
profileId: 'puzzle-profile-action',
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
levelName: '动作拼图',
summary: '验证作品架动作 Adapter。',
themeTags: [],
coverImageSrc: null,
publicationStatus: 'draft' as const,
updatedAt: '2026-05-08T00:00:00.000Z',
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: false,
};
const [item] = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [puzzleWork],
onOpenPuzzleDetail,
onDeletePuzzle,
});
item?.actions.open();
item?.actions.delete?.();
expect(onOpenPuzzleDetail).toHaveBeenCalledWith(puzzleWork);
expect(onDeletePuzzle).toHaveBeenCalledWith(puzzleWork);
});
test('buildCreationWorkShelfItems restores persisted generation state for puzzle and match3d drafts', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [
{
workId: 'puzzle:generating',
profileId: 'puzzle-profile-generating',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-session-generating',
authorDisplayName: '测试作者',
levelName: '生成中拼图',
summary: '退出产品后仍应显示生成中。',
themeTags: [],
coverImageSrc: null,
publicationStatus: 'draft',
updatedAt: '2026-05-08T00:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'generating',
},
],
match3dItems: [
{
workId: 'match3d:generating',
profileId: 'match3d-profile-generating',
ownerUserId: 'user-1',
sourceSessionId: 'match3d-session-generating',
gameName: '生成中抓鹅',
themeText: '糖果厨房',
summary: '退出产品后仍应显示生成中。',
tags: [],
coverImageSrc: null,
clearCount: 18,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-07T00:00:00.000Z',
publishReady: false,
generationStatus: 'generating',
},
],
});
expect(items.find((item) => item.kind === 'puzzle')?.isGenerating).toBe(
true,
);
expect(items.find((item) => item.kind === 'match3d')?.isGenerating).toBe(
true,
);
});
test('buildCreationWorkShelfItems maps baby object match local drafts', () => {
const onOpenBabyObjectMatchDetail = vi.fn();
const onDeleteBabyObjectMatch = vi.fn();
const baseDraft: BabyObjectMatchDraft = {
draftId: 'baby-object-draft-1',
profileId: 'baby-object-profile-12345678',
templateId: 'baby-object-match',
templateName: '宝贝识物',
workTitle: '宝贝识物',
workDescription: '苹果和香蕉识物分类',
itemNames: ['苹果', '香蕉'],
itemAssets: [
{
itemId: 'baby-object-item-1',
itemName: '苹果',
imageSrc: '/apple.png',
assetObjectId: null,
generationProvider: 'placeholder',
prompt: '苹果',
},
{
itemId: 'baby-object-item-2',
itemName: '香蕉',
imageSrc: '/banana.png',
assetObjectId: null,
generationProvider: 'placeholder',
prompt: '香蕉',
},
],
visualPackage: null,
themeTags: ['寓教于乐'],
publicationStatus: 'draft',
createdAt: '2026-05-11T00:00:00.000Z',
updatedAt: '2026-05-11T00:00:00.000Z',
publishedAt: null,
};
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
babyObjectMatchItems: [
baseDraft,
{
...baseDraft,
draftId: 'baby-object-draft-2',
profileId: 'baby-object-profile-87654321',
publicationStatus: 'published',
publishedAt: '2026-05-11T01:00:00.000Z',
updatedAt: '2026-05-11T01:00:00.000Z',
},
],
canDeleteBabyObjectMatch: true,
onOpenBabyObjectMatchDetail,
onDeleteBabyObjectMatch,
});
items[1]?.actions.open();
items[1]?.actions.delete?.();
expect(items[0]?.kind).toBe('baby-object-match');
expect(items[0]?.status).toBe('published');
expect(items[0]?.publicWorkCode).toBe('BO-87654321');
expect(items[0]?.sharePath).toContain('/works/detail?work=BO-87654321');
expect(items[1]?.status).toBe('draft');
expect(items[1]?.publicWorkCode).toBeNull();
expect(items[1]?.canDelete).toBe(true);
expect(onOpenBabyObjectMatchDetail).toHaveBeenCalledWith(baseDraft);
expect(onDeleteBabyObjectMatch).toHaveBeenCalledWith(baseDraft);
});
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('buildCreationWorkShelfItems falls back to available gameplay images as covers', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [
{
workId: 'puzzle:level-cover',
profileId: 'puzzle-profile-level-cover',
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
levelName: '关卡封面拼图',
summary: '作品自身封面为空时使用关卡正式图。',
themeTags: [],
coverImageSrc: null,
publicationStatus: 'draft',
updatedAt: '2026-05-08T00:00:00.000Z',
publishedAt: null,
publishReady: false,
levels: [
{
levelId: 'level-1',
levelName: '第一关',
pictureDescription: '港口雨夜。',
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle-candidate.png',
assetId: 'asset-1',
prompt: '港口雨夜',
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'ready',
},
],
},
],
match3dItems: [
{
workId: 'match3d:asset-cover',
profileId: 'match3d-profile-asset-cover',
ownerUserId: 'user-1',
gameName: '素材封面抓鹅',
themeText: '糖果厨房',
summary: '作品自身封面为空时使用素材图。',
tags: [],
coverImageSrc: null,
clearCount: 18,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-07T00:00:00.000Z',
publishReady: false,
generatedItemAssets: [
{
itemId: 'item-1',
itemName: '糖果',
imageSrc: '/match3d-item.png',
status: 'image_ready',
},
],
},
],
squareHoleItems: [
{
workId: 'square-hole:background-cover',
profileId: 'square-hole-profile-background-cover',
ownerUserId: 'user-1',
gameName: '背景封面方洞',
themeText: '星空玩具箱',
twistRule: '旋转洞口',
summary: '作品自身封面为空时使用背景图。',
tags: [],
coverImageSrc: null,
backgroundPrompt: '星空玩具箱',
backgroundImageSrc: '/square-hole-background.png',
shapeOptions: [],
holeOptions: [],
shapeCount: 3,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-06T00:00:00.000Z',
publishReady: false,
},
],
});
expect(items.find((item) => item.kind === 'puzzle')?.coverImageSrc).toBe(
'/puzzle-candidate.png',
);
expect(items.find((item) => item.kind === 'match3d')?.coverImageSrc).toBe(
'/match3d-item.png',
);
expect(items.find((item) => item.kind === 'square-hole')?.coverImageSrc).toBe(
'/square-hole-background.png',
);
});
test('buildCreationWorkShelfItems uses generated object keys as cover sources', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [
{
workId: 'puzzle:level-object-key',
profileId: 'puzzle-profile-level-object-key',
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
levelName: '关卡对象拼图',
summary: '作品摘要带关卡图对象路径时用关卡图做卡片背景。',
themeTags: [],
coverImageSrc: null,
publicationStatus: 'draft',
updatedAt: '2026-05-08T00:00:00.000Z',
publishedAt: null,
publishReady: false,
levels: [
{
levelId: 'level-1',
levelName: '第一关',
pictureDescription: '港口雨夜。',
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '',
assetId: 'asset-1',
prompt: '港口雨夜',
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc:
'generated-puzzle-assets/session/profile/level-cover.png',
coverAssetId: null,
generationStatus: 'ready',
},
],
},
],
match3dItems: [
{
workId: 'match3d:object-key-cover',
profileId: 'match3d-profile-object-key-cover',
ownerUserId: 'user-1',
gameName: '对象路径抓鹅',
themeText: '糖果厨房',
summary: '背景图或物品图只有 object key 时也应展示。',
tags: [],
coverImageSrc: null,
clearCount: 18,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-07T00:00:00.000Z',
publishReady: false,
generatedBackgroundAsset: {
prompt: '糖果厨房背景',
imageObjectKey:
'generated-match3d-assets/session/profile/background/image.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/background/container.png',
status: 'ready',
},
generatedItemAssets: [
{
itemId: 'item-1',
itemName: '糖果',
imageObjectKey:
'generated-match3d-assets/session/profile/items/item-1/image.png',
imageViews: [
{
viewId: 'view-1',
viewIndex: 1,
imageObjectKey:
'generated-match3d-assets/session/profile/items/item-1/views/view-1.png',
},
],
status: 'image_ready',
},
],
},
],
});
expect(items.find((item) => item.kind === 'puzzle')?.coverImageSrc).toBe(
'generated-puzzle-assets/session/profile/level-cover.png',
);
expect(items.find((item) => item.kind === 'match3d')?.coverImageSrc).toBe(
'generated-match3d-assets/session/profile/background/container.png',
);
});
test('buildCreationWorkShelfItems falls back to match3d item object key without background', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
match3dItems: [
{
workId: 'match3d:item-object-key-cover',
profileId: 'match3d-profile-item-object-key-cover',
ownerUserId: 'user-1',
gameName: '物品对象路径抓鹅',
themeText: '糖果厨房',
summary: '背景图缺失时用物品视角图对象路径。',
tags: [],
coverImageSrc: null,
clearCount: 18,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-07T00:00:00.000Z',
publishReady: false,
generatedItemAssets: [
{
itemId: 'item-1',
itemName: '糖果',
imageObjectKey:
'generated-match3d-assets/session/profile/items/item-1/image.png',
imageViews: [
{
viewId: 'view-1',
viewIndex: 1,
imageObjectKey:
'generated-match3d-assets/session/profile/items/item-1/views/view-1.png',
},
],
status: 'image_ready',
},
],
},
],
});
expect(items.find((item) => item.kind === 'match3d')?.coverImageSrc).toBe(
'generated-match3d-assets/session/profile/items/item-1/views/view-1.png',
);
});
test('buildCreationWorkShelfItems ignores puzzle theme reference cover and uses first level image', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [
{
workId: 'puzzle:theme-reference-cover',
profileId: 'puzzle-profile-theme-reference-cover',
ownerUserId: 'user-1',
authorDisplayName: '测试作者',
levelName: '主题兜底拼图',
summary: '摘要里的封面是玩法参考图时,用第一关画面兜底。',
themeTags: [],
coverImageSrc: '/creation-type-references/puzzle.webp',
publicationStatus: 'draft',
updatedAt: '2026-05-08T00:00:00.000Z',
publishedAt: null,
publishReady: false,
levels: [
{
levelId: 'level-1',
levelName: '第一关',
pictureDescription: '第一关画面。',
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle-first-level-candidate.png',
assetId: 'asset-1',
prompt: '第一关画面',
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: '/puzzle-first-level-cover.png',
coverAssetId: 'asset-1',
generationStatus: 'ready',
},
],
},
],
});
expect(items.find((item) => item.kind === 'puzzle')?.coverImageSrc).toBe(
'/puzzle-first-level-cover.png',
);
});
test('buildCreationWorkShelfItems ignores match3d theme reference cover and uses container image', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
match3dItems: [
{
workId: 'match3d:theme-reference-cover',
profileId: 'match3d-profile-theme-reference-cover',
ownerUserId: 'user-1',
gameName: '主题兜底抓鹅',
themeText: '糖果厨房',
summary: '摘要里的封面是玩法参考图时用UI背景图兜底。',
tags: [],
coverImageSrc: '/creation-type-references/match3d.webp',
clearCount: 18,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-07T00:00:00.000Z',
publishReady: false,
generatedBackgroundAsset: {
prompt: '糖果厨房竖屏UI背景',
imageSrc: '/match3d-ui-background.png',
containerImageSrc: '/match3d-container.png',
status: 'image_ready',
},
generatedItemAssets: [
{
itemId: 'item-1',
itemName: '糖果',
imageSrc: '/match3d-item.png',
status: 'image_ready',
},
],
},
],
});
expect(items.find((item) => item.kind === 'match3d')?.coverImageSrc).toBe(
'/match3d-container.png',
);
});
test('buildCreationWorkShelfItems uses match3d container asset before background and item image', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
match3dItems: [
{
workId: 'match3d:item-background-asset-cover',
profileId: 'match3d-profile-item-background-asset-cover',
ownerUserId: 'user-1',
gameName: '背景资产抓鹅',
themeText: '糖果厨房',
summary: '顶层背景缺失时从素材携带的UI背景兜底。',
tags: [],
coverImageSrc: '/creation-type-references/match3d.webp',
clearCount: 18,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-07T00:00:00.000Z',
publishReady: false,
generatedItemAssets: [
{
itemId: 'item-1',
itemName: '糖果',
imageSrc: '/match3d-item.png',
backgroundAsset: {
prompt: '糖果厨房竖屏UI背景',
imageObjectKey:
'generated-match3d-assets/session/profile/background/image.png',
containerImageObjectKey:
'generated-match3d-assets/session/profile/ui-container/container.png',
status: 'image_ready',
},
status: 'image_ready',
},
],
},
],
});
expect(items.find((item) => item.kind === 'match3d')?.coverImageSrc).toBe(
'generated-match3d-assets/session/profile/ui-container/container.png',
);
});
test('buildCreationWorkShelfItems uses match3d transparent container reference as last fallback', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
match3dItems: [
{
workId: 'match3d:container-reference-fallback',
profileId: 'match3d-profile-container-reference-fallback',
ownerUserId: 'user-1',
sourceSessionId: 'session-1',
gameName: '水果抓大鹅',
themeText: '水果',
summary: '',
tags: [],
coverImageSrc: null,
referenceImageSrc: null,
backgroundPrompt: '',
backgroundImageSrc: null,
backgroundImageObjectKey: null,
generatedBackgroundAsset: null,
generatedItemAssets: [],
clearCount: 3,
difficulty: 2,
publicationStatus: 'draft',
publishReady: false,
playCount: 0,
updatedAt: '2026-05-01T00:00:00.000Z',
publishedAt: null,
},
],
});
expect(items.find((item) => item.kind === 'match3d')?.coverImageSrc).toBe(
'/match3d-background-references/pot-fused-reference.png',
);
});
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(),
);
});