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(), ); });