import { renderToStaticMarkup } from 'react-dom/server'; import { expect, test } from 'vitest'; import type { CreationEntryConfig } from '../../services/creationEntryConfigService'; import { derivePlatformCreationTypes } from '../platform-entry/platformEntryCreationTypes'; import { buildCreationWorkShelfItems } from './creationWorkShelf'; import { CustomWorldCreationHub } from './CustomWorldCreationHub'; const noopCreateType = () => {}; const DAY_MS = 24 * 60 * 60 * 1000; function buildUpdatedAtDaysAgo(daysAgo: number) { return new Date(Date.now() - daysAgo * DAY_MS).toISOString(); } const testEntryConfig = { startCard: { title: '新建作品', description: '选择模板后进入对应的创作表单。', idleBadge: '模板 Tab', busyBadge: '正在开启', }, typeModal: { title: '选择创作类型', description: '先选玩法类型,再进入对应创作工作台。', }, eventBanner: { title: '泥点挑战', description: '创作活动测试横幅。', coverImageSrc: '/creation-type-references/puzzle.webp', prizePoolMudPoints: 1000, startsAtText: '2026-05-01', endsAtText: '2026-05-31', }, eventBanners: [ { title: '后台拼图赛', description: '后台配置的拼图横幅。', coverImageSrc: '/creation-type-references/puzzle.webp', prizePoolMudPoints: 1000, startsAtText: '2026-05-01', endsAtText: '2026-05-31', renderMode: 'structured', }, { title: '后台抓大鹅赛', description: '后台配置的抓大鹅横幅。', coverImageSrc: '/creation-type-references/match3d.webp', prizePoolMudPoints: 1200, startsAtText: '2026-06-01', endsAtText: '2026-06-30', renderMode: 'structured', }, ], creationTypes: [ { id: 'rpg', title: '文字冒险', subtitle: '经典 RPG 体验', badge: '可创建', imageSrc: '/creation-type-references/rpg.webp', visible: true, open: true, sortOrder: 10, categoryId: 'recommended', categoryLabel: '热门推荐', categorySortOrder: 20, updatedAtMicros: 1, }, { id: 'puzzle', title: '拼图', subtitle: '拼图关卡创作', badge: '可创建', imageSrc: '/creation-type-references/puzzle.webp', visible: true, open: true, sortOrder: 30, categoryId: 'recommended', categoryLabel: '热门推荐', categorySortOrder: 20, updatedAtMicros: 1, }, { id: 'match3d', title: '抓大鹅', subtitle: '3D 消除关卡', badge: '可创作', imageSrc: '/creation-type-references/match3d.webp', visible: true, open: true, sortOrder: 40, categoryId: 'recommended', categoryLabel: '热门推荐', categorySortOrder: 20, updatedAtMicros: 1, }, { id: 'jump-hop', title: '跳一跳', subtitle: '节奏跳跃挑战', badge: '可创建', imageSrc: '/creation-type-references/jump-hop.webp', visible: true, open: true, sortOrder: 45, categoryId: 'recommended', categoryLabel: '热门推荐', categorySortOrder: 20, updatedAtMicros: 1, }, { id: 'square-hole', title: '方洞', subtitle: '形状投放挑战', badge: '可创建', imageSrc: '/creation-type-references/square-hole.webp', visible: false, open: true, sortOrder: 50, categoryId: 'recommended', categoryLabel: '热门推荐', categorySortOrder: 20, updatedAtMicros: 1, }, { id: 'visual-novel', title: '视觉小说', subtitle: '分支叙事体验', badge: '敬请期待', imageSrc: '/creation-type-references/visual-novel.webp', visible: false, open: false, sortOrder: 60, categoryId: 'recommended', categoryLabel: '热门推荐', categorySortOrder: 20, updatedAtMicros: 1, }, { id: 'airp', title: 'AI RPG', subtitle: '原生角色扮演', badge: '即将开放', imageSrc: '/creation-type-references/airp.webp', visible: true, open: false, sortOrder: 70, categoryId: 'recommended', categoryLabel: '热门推荐', categorySortOrder: 20, updatedAtMicros: 1, }, ], } satisfies CreationEntryConfig; const testCreationTypes = derivePlatformCreationTypes( testEntryConfig.creationTypes, ); test('creation hub draft card renders compiled work summary fields', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} />, ); expect(html).toContain('一个被潮雾切开的列岛世界'); expect(html).toContain('玩家是失职返乡的守灯人'); expect(html).toContain('守灯会与沉船商盟争夺航道解释权'); expect(html).toContain('拼图'); expect(html).toContain('拼图关卡创作'); expect(html).toContain('抓大鹅'); expect(html).toContain('3D 消除关卡'); expect(html).toContain('文字冒险'); expect(html).toContain('经典 RPG 体验'); expect(html).not.toContain('大鱼吃小鱼'); }); test('creation start card renders reference-aligned banner and template metadata', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} mode="start-only" />, ); expect(html).toContain('creation-event-banner'); expect(html).toContain('creation-event-banner__track'); expect(html).toContain('creation-event-banner__slide'); expect(html).toContain('creation-event-banner__timebar'); expect(html).toContain('后台拼图赛'); expect(html).toContain('后台抓大鹅赛'); expect(html).toContain('1,000'); expect(html).toContain('1,200'); expect(html).toContain('泥点数'); expect(html).not.toContain('泥点挑战'); expect(html).not.toContain('拼图主题创作赛'); expect(html).not.toContain('抓大鹅主题创作赛'); expect(html).not.toContain('最近创作'); expect(html).toMatch( /creation-event-banner__timebar[\s\S]*creation-event-banner__pager[\s\S]*creation-template-card/u, ); expect(html).toContain('creation-template-card__body'); expect(html).toContain('creation-template-card__cost-badge'); expect(html).toContain('拼图关卡创作'); expect(html).toContain('10泥点数'); expect(html).toContain('即将开放'); expect(html).toContain('data-locked="true"'); expect(html).toContain('暂未开放'); expect(html).not.toContain('可创建'); expect(html).not.toContain('可创作'); expect(html).not.toContain('creation-event-banner__counter'); expect(html).not.toContain('预计消耗 10-20 泥点'); expect(html).not.toContain('platform-creation-reference-card'); }); test('locked creation template card replaces mud point cost with unavailable state', () => { const lockedEntryConfig = { ...testEntryConfig, creationTypes: [ { id: 'airp', title: 'AI RPG', subtitle: '原生角色扮演', badge: '即将开放', imageSrc: '/creation-type-references/airp.webp', visible: true, open: false, sortOrder: 70, categoryId: 'recommended', categoryLabel: '热门推荐', categorySortOrder: 20, updatedAtMicros: 1, }, ], } satisfies CreationEntryConfig; const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={lockedEntryConfig} creationTypes={derivePlatformCreationTypes( lockedEntryConfig.creationTypes, )} mode="start-only" />, ); expect(html).toContain('data-locked="true"'); expect(html).toContain('即将开放'); expect(html).toContain('暂未开放'); expect(html).not.toContain('10泥点数'); }); test('creation template card renders mud point cost from unified creation spec', () => { const config = { ...testEntryConfig, creationTypes: [ { id: 'puzzle', title: '拼图', subtitle: '拼图关卡创作', badge: '可创建', imageSrc: '/creation-type-references/puzzle.webp', visible: true, open: true, sortOrder: 30, categoryId: 'recommended', categoryLabel: '热门推荐', categorySortOrder: 20, updatedAtMicros: 1, unifiedCreationSpec: { playId: 'puzzle', title: '拼图', mudPointCost: 12, workspaceStage: 'puzzle-agent-workspace', generationStage: 'puzzle-generating', resultStage: 'puzzle-result', fields: [ { id: 'pictureDescription', kind: 'text', label: '画面描述', required: true, }, ], }, }, ], } satisfies CreationEntryConfig; const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={config} creationTypes={derivePlatformCreationTypes(config.creationTypes)} mode="start-only" />, ); expect(html).toContain('12泥点数'); expect(html).not.toContain('10泥点数'); }); test('creation start card falls back to legacy single banner when eventBanners is empty', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={{ ...testEntryConfig, eventBanners: [], }} creationTypes={testCreationTypes} mode="start-only" />, ); expect(html).toContain('泥点挑战'); expect(html).not.toContain('后台拼图赛'); }); test('creation start card renders html banner in an empty-permission sandbox', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={{ ...testEntryConfig, eventBanners: [ { ...testEntryConfig.eventBanner, title: 'HTML 后台横幅', renderMode: 'html', htmlCode: '

自定义横幅

', }, ], }} creationTypes={testCreationTypes} mode="start-only" />, ); expect(html).toContain('title="HTML 后台横幅"'); expect(html).toContain('sandbox=""'); expect(html).toContain( '<section><h1>自定义横幅</h1></section>', ); }); test('creation start card renders recent tab with the same template cards', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} mode="start-only" />, ); expect(html).toContain('aria-label="创作入口页签"'); expect(html).toContain('role="tab"'); expect(html).toContain('aria-selected="true"'); expect(html).toContain('creation-template-list__grid'); expect(html).toContain('creation-template-card'); expect(html).toContain('最近创作'); expect(html).toContain('仅显示最近7天内使用过的模板'); expect(html).toContain('文字冒险'); expect(html).toContain('经典 RPG 体验'); expect(html).not.toContain('creation-recent-work-grid'); expect(html).not.toContain('打开最近创作'); expect(html).not.toContain('后端返回的最近草稿'); expect(html).not.toContain('这条内容来自作品架摘要'); }); test('creation start card prefers backend recent summaries over local pending placeholders', () => { const recentWorkItems = buildCreationWorkShelfItems({ rpgItems: [ { workId: 'draft:backend-session', sourceType: 'agent_session', status: 'draft', title: '后端最近草稿', subtitle: '真实作品架摘要', summary: '最近创作应该只读取后端摘要。', coverImageSrc: null, updatedAt: buildUpdatedAtDaysAgo(1), publishedAt: null, stage: 'failed', stageLabel: '生成失败', playableNpcCount: 0, landmarkCount: 0, sessionId: 'backend-session', profileId: null, canResume: true, canEnterWorld: false, }, ], bigFishItems: [], puzzleItems: [], }); const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} mode="start-only" />, ); expect(html).toContain('最近创作'); expect(html).toContain('文字冒险'); expect(html).toContain('经典 RPG 体验'); expect(html).not.toContain('后端最近草稿'); expect(html).not.toContain('最近创作应该只读取后端摘要'); expect(html).not.toContain('本地生成中占位'); }); test('creation start card excludes works older than the recent window', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} mode="start-only" />, ); expect(html).not.toContain('最近创作'); expect(html).not.toContain('仅显示最近7天内使用过的模板'); expect(html).not.toContain('八天前的草稿'); expect(html).not.toContain('这条草稿已经超过最近创作期限'); }); test('creation start card maps backend jump-hop draft to template card', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} onOpenJumpHopDetail={() => {}} mode="start-only" />, ); expect(html).toContain('最近创作'); expect(html).toContain('跳一跳'); expect(html).toContain('节奏跳跃挑战'); expect(html).toContain('creation-template-card'); expect(html).not.toContain('跳一跳生成草稿'); expect(html).not.toContain('后端仍在生成跳一跳玩法'); }); test('creation start card includes failed drafts in the recent tab', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} mode="start-only" />, ); expect(html).toContain('最近创作'); expect(html).toContain('creation-template-list__grid'); expect(html).toContain('文字冒险'); expect(html).toContain('经典 RPG 体验'); expect(html).not.toContain('creation-recent-work-grid'); expect(html).not.toContain('失败但仍可恢复的草稿'); expect(html).not.toContain('失败草稿也来自真实作品架摘要'); }); test('creation start card maps failed mini-game drafts into recent template cards', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} match3dItems={[ { workId: 'match3d-failed-work', profileId: 'match3d-failed-profile', ownerUserId: 'user-1', gameName: '失败抓大鹅草稿', themeText: '水果', summary: '失败的小玩法草稿也应该进入最近创作。', tags: [], coverImageSrc: null, clearCount: 0, difficulty: 1, publicationStatus: 'draft', playCount: 0, updatedAt: buildUpdatedAtDaysAgo(1), publishedAt: null, publishReady: false, generationStatus: 'failed', }, ]} mode="start-only" />, ); expect(html).toContain('最近创作'); expect(html).toContain('抓大鹅'); expect(html).toContain('3D 消除关卡'); expect(html).toContain('creation-template-card'); expect(html).not.toContain('失败抓大鹅草稿'); expect(html).not.toContain('失败的小玩法草稿也应该进入最近创作。'); }); test('creation start card keeps typography in compact UI scale', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} mode="start-only" />, ); expect(html).toMatch(/creation-template-card__title[^"]*\btext-sm\b/u); expect(html).toMatch(/creation-template-card__subtitle[^"]*\btext-xs\b/u); expect(html).toMatch( /creation-template-card__cost-badge[^"]*\btext-\[11px\](?:\s|")/u, ); expect(html).not.toMatch( /\b(text-lg|text-xl|sm:text-base|sm:text-lg|sm:text-xl|text-\[1\.08rem\])\b/u, ); }); test('creation start card removes the outer template list frame and tightens card grid', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} mode="start-only" />, ); expect(html).toContain('creation-template-list'); expect(html).toMatch(/creation-template-list__grid[^"]*\bgap-2\b/u); expect(html).toMatch(/creation-template-card[^"]*\bmin-h-\[12\.5rem\]/u); expect(html).not.toMatch( /creation-template-list[^"]*\bborder\b[^"]*\bborder-\[#f0dfd6\]/u, ); }); test('creation hub renders puzzle works in the same unified list with puzzle tag', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} onOpenPuzzleDetail={() => {}} />, ); expect(html).toContain('潮雾拼图'); expect(html).toContain('拼图'); expect(html).toContain('aria-label="游玩 12次"'); expect(html).toContain('aria-label="改造 3次"'); expect(html).toContain('aria-label="点赞 4赞"'); expect(html).not.toContain('作品号'); expect(html).not.toContain('PZ-PROFILE1'); expect(html).not.toContain('潮雾'); expect(html).not.toContain('港口'); expect(html).not.toContain('我的拼图作品'); }); test('creation hub marks generating and newly completed drafts', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} onOpenPuzzleDetail={() => {}} getWorkState={(item) => item.kind === 'puzzle' ? { isGenerating: true, hasUnreadUpdate: true } : null } />, ); expect(html).toContain('生成中'); expect(html).toContain('aria-label="新生成完成"'); expect(html).toContain('生成中...'); expect(html).toContain('creation-work-card__spinner'); }); test('creation hub does not mask completed puzzle drafts while a later level image is generating', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} onOpenPuzzleDetail={() => {}} />, ); expect(html).not.toContain('生成中...'); expect(html).not.toContain('creation-work-card__spinner'); expect(html).toContain('继续创作《潮雾拼图草稿》'); }); test('creation hub published work uses unified list card layout', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} onOpenPuzzleDetail={() => {}} />, ); expect(html).toContain('creation-work-list'); expect(html).toContain('platform-category-game-item'); expect(html).toContain('creation-work-card__side-cover'); expect(html).not.toContain('col-span-2 sm:col-span-1'); }); test('creation hub draft cards use cover background and hide updated time', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} onOpenPuzzleDetail={() => {}} />, ); const newerIndex = html.indexOf('新草稿'); const olderIndex = html.indexOf('旧草稿'); expect(newerIndex).toBeGreaterThanOrEqual(0); expect(olderIndex).toBeGreaterThanOrEqual(0); expect(newerIndex).toBeLessThan(olderIndex); expect(html).toContain( 'class="absolute inset-0 h-full w-full object-cover" src="/covers/new-draft.webp"', ); expect(html).toContain('creation-work-card__side-cover'); expect(html).toContain('src="/covers/new-draft.webp"'); expect(html).toContain( '--creation-work-card-cover-fallback:url(/creation-type-references/puzzle.webp)', ); expect(html).not.toContain('1778457601.234567Z'); expect(html).not.toContain('2026-05-07'); expect(html).not.toContain('更新于'); expect(html).not.toContain('最后修改'); }); test('creation hub draft cards fall back to creation type cover when cover is missing', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} onOpenPuzzleDetail={() => {}} />, ); expect(html).toContain('缺少封面的拼图草稿'); expect(html).toContain( 'class="absolute inset-0 h-full w-full object-cover" src="/creation-type-references/puzzle.webp"', ); expect(html).toContain( '--creation-work-card-cover-fallback:url(/creation-type-references/puzzle.webp)', ); expect(html).not.toContain('>封面'); }); test('creation hub published card keeps publish info without fixed action text', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} onOpenPuzzleDetail={() => {}} onClaimPuzzlePointIncentive={() => {}} />, ); expect(html).toContain('积分激励'); expect(html).toContain('待领取'); expect(html).toContain('游玩'); expect(html).toContain('改造'); expect(html).toContain('点赞'); expect(html).toContain('creation-work-card__side-cover'); expect(html).not.toContain('creation-work-card__action'); expect(html).not.toContain('>查看详情<'); }); test('creation hub root keeps the remap theme hook without the page card shell', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} mode="works-only" />, ); expect(html).toContain('platform-remap-surface'); expect(html).not.toContain('platform-page-stage'); }); test('creation hub draft tabs use discover-style channel labels', () => { const html = renderToStaticMarkup( {}} onCreateType={noopCreateType} onOpenDraft={() => {}} onEnterPublished={() => {}} entryConfig={testEntryConfig} creationTypes={testCreationTypes} puzzleItems={[ { workId: 'puzzle:works-tab', profileId: 'puzzle-profile-works-tab', ownerUserId: 'user-1', authorDisplayName: '测试作者', levelName: '测试草稿', summary: '测试草稿', themeTags: [], coverImageSrc: null, publicationStatus: 'draft', updatedAt: '2026-05-07T00:00:00.000Z', publishedAt: null, playCount: 0, remixCount: 0, likeCount: 0, publishReady: false, }, ]} onOpenPuzzleDetail={() => {}} />, ); expect(html).toContain('platform-mobile-home-channel'); expect(html).toContain('platform-mobile-home-channel--active'); expect(html).not.toContain('platform-tab--active'); });