Files
Genarrative/src/components/custom-world-home/creationWorkShelf.test.ts

1163 lines
37 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 { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { expect, test, vi } from 'vitest';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import {
buildCreationWorkShelfItems,
getCreationWorkShelfItemTime,
hasBarkBattleRequiredImages,
isPersistedBarkBattleDraftGenerating,
type CreationWorkShelfItem,
} from './creationWorkShelf';
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
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 maps wooden fish items with WF public code', () => {
const onOpenWoodenFishDetail = vi.fn();
const woodenFishWork = {
runtimeKind: 'wooden-fish' as const,
workId: 'wooden-fish-work-1',
profileId: 'wooden-fish-profile-12345678',
ownerUserId: 'user-1',
sourceSessionId: 'wooden-fish-session-1',
workTitle: '苹果敲木鱼',
workDescription: '苹果主题木鱼。',
themeTags: ['苹果', '休闲'],
coverImageSrc: '/wooden-fish/apple-cover.png',
publicationStatus: 'published',
playCount: 9,
updatedAt: '2026-05-20T00:00:00.000Z',
publishedAt: '2026-05-20T00:00:00.000Z',
publishReady: true,
generationStatus: 'ready' as const,
};
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
woodenFishItems: [woodenFishWork],
onOpenWoodenFishDetail,
});
items[0]?.actions.open();
expect(items).toHaveLength(1);
expect(items[0]?.kind).toBe('wooden-fish');
expect(items[0]?.status).toBe('published');
expect(items[0]?.publicWorkCode).toBe('WF-12345678');
expect(items[0]?.sharePath).toContain('/works/detail?work=WF-12345678');
expect(items[0]?.openActionLabel).toBe('查看详情');
expect(items[0]?.badges.some((badge) => badge.label === '敲木鱼')).toBe(true);
expect(items[0]?.metrics.find((metric) => metric.id === 'play-count')?.value).toBe(9);
expect(onOpenWoodenFishDetail).toHaveBeenCalledWith(woodenFishWork);
});
test('buildCreationWorkShelfItems keeps published bark battle over duplicate draft', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
barkBattleItems: [
{
workId: 'bark-battle-work-1',
draftId: 'bark-battle-draft-1',
ownerUserId: 'user-1',
authorDisplayName: '草稿作者',
title: '汪汪测试杯',
summary: '',
themeDescription: '阳光草坪声浪竞技场',
playerImageDescription: '戴红色围巾的柯基选手',
opponentImageDescription: '蓝色护目镜哈士奇对手',
playerCharacterImageSrc: '/generated-bark-battle/player.png',
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
difficultyPreset: 'normal',
status: 'draft',
generationStatus: 'ready',
publishReady: true,
playCount: 0,
updatedAt: '2026-05-14T10:01:00.000Z',
publishedAt: null,
},
{
workId: 'bark-battle-work-1',
draftId: 'bark-battle-draft-1',
ownerUserId: 'user-1',
authorDisplayName: '测试玩家',
title: '汪汪测试杯',
summary: '',
themeDescription: '阳光草坪声浪竞技场',
playerImageDescription: '戴红色围巾的柯基选手',
opponentImageDescription: '蓝色护目镜哈士奇对手',
playerCharacterImageSrc: '/generated-bark-battle/player.png',
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
difficultyPreset: 'normal',
status: 'published',
generationStatus: 'ready',
publishReady: true,
playCount: 0,
updatedAt: '2026-05-14T10:02:00.000Z',
publishedAt: '2026-05-14T10:02:00.000Z',
},
],
});
expect(items).toHaveLength(1);
expect(items[0]?.kind).toBe('bark-battle');
expect(items[0]?.status).toBe('published');
expect(items[0]?.publicWorkCode).toBe('BB-TLEWORK1');
expect(items[0]?.authorDisplayName).toBe('测试玩家');
});
test('buildCreationWorkShelfItems keeps separate bark battle draft and published works visible', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
barkBattleItems: [
{
workId: 'BB-DRAFT001',
draftId: 'bark-battle-draft-visible',
ownerUserId: 'user-1',
authorDisplayName: '草稿作者',
title: '草稿声浪赛',
summary: '',
themeDescription: '草地声浪挑战',
playerImageDescription: '柯基选手',
opponentImageDescription: '哈士奇对手',
playerCharacterImageSrc: '/draft-player.png',
opponentCharacterImageSrc: '/draft-opponent.png',
uiBackgroundImageSrc: '/draft-background.png',
difficultyPreset: 'easy',
status: 'draft',
generationStatus: 'ready',
publishReady: false,
playCount: 0,
updatedAt: '2026-05-20T00:00:00.000Z',
publishedAt: null,
},
{
workId: 'BB-PUB00001',
draftId: 'bark-battle-draft-published',
ownerUserId: 'user-1',
authorDisplayName: '发布作者',
title: '已发布声浪赛',
summary: '',
themeDescription: '霓虹声浪挑战',
playerImageDescription: '柴犬选手',
opponentImageDescription: '机器人对手',
playerCharacterImageSrc: '/published-player.png',
opponentCharacterImageSrc: '/published-opponent.png',
uiBackgroundImageSrc: '/published-background.png',
difficultyPreset: 'normal',
status: 'published',
generationStatus: 'ready',
publishReady: true,
playCount: 3,
updatedAt: '2026-05-21T00:00:00.000Z',
publishedAt: '2026-05-21T00:00:00.000Z',
},
],
});
expect(items).toHaveLength(2);
expect(items.find((item) => item.status === 'draft')?.id).toBe('BB-DRAFT001');
expect(items.find((item) => item.status === 'published')?.id).toBe(
'BB-PUB00001',
);
expect(items.find((item) => item.status === 'published')?.publicWorkCode).toBe(
'BB-PUB00001',
);
});
test('buildCreationWorkShelfItems falls back to deterministic RPG public work code when library entry is missing', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [
{
workId: 'rpg-work-published',
sourceType: 'published_profile',
status: 'published',
title: '潮雾列岛已发布版',
subtitle: '旧灯塔与失控航路',
summary: '已经发布的群岛世界作品。',
coverImageSrc: null,
updatedAt: '2026-04-20T10:00:00.000Z',
publishedAt: '2026-04-20T10:00:00.000Z',
stage: 'published',
stageLabel: '已发布',
playableNpcCount: 3,
landmarkCount: 4,
sessionId: null,
profileId: 'world-public-1',
canResume: false,
canEnterWorld: true,
},
],
bigFishItems: [],
puzzleItems: [],
});
expect(items).toHaveLength(1);
expect(items[0]?.publicWorkCode).toBe('CW-00000001');
expect(items[0]?.sharePath).toContain('/works/detail?work=CW-00000001');
expect(items[0]?.canShare).toBe(true);
});
test('buildCreationWorkShelfItems gives bark battle draft cover from character or reference fallback', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
barkBattleItems: [
{
workId: 'BB-COVER001',
draftId: 'bark-battle-draft-cover',
ownerUserId: 'user-1',
authorDisplayName: '草稿作者',
title: '角色封面声浪赛',
summary: '',
themeDescription: '草地声浪挑战',
playerImageDescription: '柯基选手',
opponentImageDescription: '哈士奇对手',
playerCharacterImageSrc: '/draft-player-cover.png',
opponentCharacterImageSrc: '/draft-opponent-cover.png',
uiBackgroundImageSrc: null,
difficultyPreset: 'easy',
status: 'draft',
generationStatus: 'partial_failed',
publishReady: false,
playCount: 0,
updatedAt: '2026-05-20T00:00:00.000Z',
publishedAt: null,
},
{
workId: 'BB-COVER002',
draftId: 'bark-battle-draft-cover-fallback',
ownerUserId: 'user-1',
authorDisplayName: '草稿作者',
title: '默认封面声浪赛',
summary: '',
themeDescription: '夜市声浪挑战',
playerImageDescription: '柴犬选手',
opponentImageDescription: '机器人对手',
playerCharacterImageSrc: null,
opponentCharacterImageSrc: null,
uiBackgroundImageSrc: null,
difficultyPreset: 'normal',
status: 'draft',
generationStatus: 'pending_assets',
publishReady: false,
playCount: 0,
updatedAt: '2026-05-19T00:00:00.000Z',
publishedAt: null,
},
],
});
expect(items.find((item) => item.id === 'BB-COVER001')?.coverImageSrc).toBe(
'/draft-player-cover.png',
);
expect(items.find((item) => item.id === 'BB-COVER001')?.coverCharacterImageSrcs).toEqual([
'/draft-player-cover.png',
'/draft-opponent-cover.png',
]);
expect(items.find((item) => item.id === 'BB-COVER002')?.coverImageSrc).toBe(
'/creation-type-references/bark-battle.webp',
);
});
test('buildCreationWorkShelfItems keeps bark battle draft author display name', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
barkBattleItems: [
{
workId: 'bark-battle-work-draft-author',
draftId: 'bark-battle-draft-author',
ownerUserId: 'user-1',
authorDisplayName: '草稿作者',
title: '草稿声浪赛',
summary: '',
themeDescription: '草地声浪挑战',
playerImageDescription: '柯基选手',
opponentImageDescription: '哈士奇对手',
playerCharacterImageSrc: '/player.png',
opponentCharacterImageSrc: '/opponent.png',
uiBackgroundImageSrc: '/background.png',
difficultyPreset: 'easy',
status: 'draft',
generationStatus: 'ready',
publishReady: false,
playCount: 0,
updatedAt: '2026-05-20T00:00:00.000Z',
publishedAt: null,
},
],
});
expect(items[0]?.kind).toBe('bark-battle');
expect(items[0]?.status).toBe('draft');
expect(items[0]?.authorDisplayName).toBe('草稿作者');
});
test('buildCreationWorkShelfItems falls back unknown authors to player label', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
match3dItems: [
{
workId: 'match3d-work-author-fallback',
profileId: 'match3d-profile-author-fallback',
ownerUserId: 'user-1',
gameName: '水果抓大鹅',
themeText: '水果',
summary: '把水果从透明罐里抓出来。',
tags: [],
coverImageSrc: null,
clearCount: 0,
difficulty: 1,
publicationStatus: 'published',
playCount: 0,
updatedAt: '2026-05-20T00:00:00.000Z',
publishedAt: '2026-05-20T00:00:00.000Z',
publishReady: true,
},
],
});
expect(items[0]?.kind).toBe('match3d');
expect(items[0]?.authorDisplayName).toBe('玩家');
});
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('buildCreationWorkShelfItems maps bark battle works with scene role cover and BB code', () => {
const onOpenBarkBattleDetail = vi.fn();
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
barkBattleItems: [
{
workId: 'bark-battle-work-12345678',
draftId: 'bark-battle-draft-1',
ownerUserId: 'user-1',
authorDisplayName: '玩家',
title: '公园声浪赛',
summary: '柯基和哈士奇比拼声浪。',
themeDescription: '傍晚公园擂台',
playerImageDescription: '红围巾柯基',
opponentImageDescription: '蓝头带哈士奇',
playerCharacterImageSrc: '/generated-bark-battle/player.png',
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
difficultyPreset: 'normal',
status: 'published',
generationStatus: 'ready',
publishReady: true,
playCount: 6,
updatedAt: '2026-05-20T00:00:00.000Z',
publishedAt: '2026-05-20T00:00:00.000Z',
},
],
onOpenBarkBattleDetail,
});
const item = items[0];
item?.actions.open();
expect(item?.kind).toBe('bark-battle');
expect(item?.publicWorkCode).toBe('BB-12345678');
expect(item?.sharePath).toContain('/works/detail?work=BB-12345678');
expect(item?.coverImageSrc).toBe('/generated-bark-battle/background.png');
expect(item?.coverRenderMode).toBe('scene_with_roles');
expect(item?.coverCharacterImageSrcs).toEqual([
'/generated-bark-battle/player.png',
'/generated-bark-battle/opponent.png',
]);
expect(onOpenBarkBattleDetail).toHaveBeenCalledWith(
expect.objectContaining({ workId: 'bark-battle-work-12345678' }),
);
});
test('bark battle draft generating state follows pending assets or missing three images', () => {
const draft = {
workId: 'bark-battle-work-draft',
draftId: 'bark-battle-draft-1',
ownerUserId: 'user-1',
authorDisplayName: '玩家',
title: '草稿声浪赛',
summary: '',
themeDescription: '草地',
playerImageDescription: '柯基',
opponentImageDescription: '哈士奇',
playerCharacterImageSrc: '/player.png',
opponentCharacterImageSrc: null,
uiBackgroundImageSrc: '/background.png',
difficultyPreset: 'easy' as const,
status: 'draft' as const,
generationStatus: 'pending_assets',
publishReady: false,
playCount: 0,
updatedAt: '2026-05-20T00:00:00.000Z',
publishedAt: null,
};
expect(hasBarkBattleRequiredImages(draft)).toBe(false);
expect(isPersistedBarkBattleDraftGenerating(draft)).toBe(true);
expect(
isPersistedBarkBattleDraftGenerating({
...draft,
opponentCharacterImageSrc: '/opponent.png',
generationStatus: 'ready',
}),
).toBe(false);
});
test('CustomWorldWorkCard hides author on shelf draft and published cards', () => {
const buildItem = (
status: CreationWorkShelfItem['status'],
authorDisplayName: string,
): CreationWorkShelfItem => ({
id: `card-${status}`,
kind: 'bark-battle',
status,
authorDisplayName,
title: status === 'draft' ? '草稿声浪赛' : '发布声浪赛',
summary: '一场轻快的汪汪声浪对决。',
updatedAt: '2026-05-20T00:00:00.000Z',
coverImageSrc: null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
publicWorkCode: null,
sharePath: null,
openActionLabel: status === 'draft' ? '继续创作' : '查看详情',
canDelete: false,
canShare: false,
badges: [
{ id: 'status', label: status === 'draft' ? '草稿' : '已发布', tone: 'neutral' },
{ id: 'type', label: '汪汪', tone: 'neutral' },
],
metrics: [],
actions: { open: () => {} },
source: {
kind: 'bark-battle',
item: {
workId: `bark-battle-${status}`,
draftId: `draft-${status}`,
ownerUserId: 'user-1',
authorDisplayName,
title: status === 'draft' ? '草稿声浪赛' : '发布声浪赛',
summary: '一场轻快的汪汪声浪对决。',
themeDescription: '公园舞台',
playerImageDescription: '柯基选手',
opponentImageDescription: '哈士奇对手',
playerCharacterImageSrc: null,
opponentCharacterImageSrc: null,
uiBackgroundImageSrc: null,
difficultyPreset: 'normal',
status,
generationStatus: 'ready',
publishReady: status === 'published',
playCount: 0,
updatedAt: '2026-05-20T00:00:00.000Z',
publishedAt: status === 'published' ? '2026-05-20T00:00:00.000Z' : null,
},
},
});
const draftHtml = renderToStaticMarkup(
createElement(CustomWorldWorkCard, {
item: buildItem('draft', '草稿作者'),
onOpen: () => {},
}),
);
const publishedHtml = renderToStaticMarkup(
createElement(CustomWorldWorkCard, {
item: buildItem('published', '发布作者'),
onOpen: () => {},
}),
);
expect(draftHtml).not.toContain('作者:草稿作者');
expect(publishedHtml).not.toContain('作者:发布作者');
});
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(),
);
});