将统一创作契约泥点消耗改为数字字段并由前端格式化展示 将后台契约编辑从 JSON 文本改为结构化卡片与弹窗表单 隐藏玩法阶段等内部标识并按玩法默认映射自动带出 更新创作入口文档、团队记忆和回归测试
1193 lines
37 KiB
TypeScript
1193 lines
37 KiB
TypeScript
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(
|
|
<CustomWorldCreationHub
|
|
items={[
|
|
{
|
|
workId: 'draft:session-1',
|
|
sourceType: 'agent_session',
|
|
status: 'draft',
|
|
title: '一个被潮雾切开的列岛世界',
|
|
subtitle: '补齐关键锚点',
|
|
summary:
|
|
'玩家是失职返乡的守灯人 · 核心冲突:守灯会与沉船商盟争夺航道解释权',
|
|
coverImageSrc: null,
|
|
updatedAt: new Date('2026-04-13T12:00:00.000Z').toISOString(),
|
|
publishedAt: null,
|
|
stage: 'clarifying',
|
|
stageLabel: '补齐关键锚点',
|
|
playableNpcCount: 0,
|
|
landmarkCount: 0,
|
|
sessionId: 'session-1',
|
|
profileId: null,
|
|
canResume: true,
|
|
canEnterWorld: false,
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
onCreateType={noopCreateType}
|
|
onOpenDraft={() => {}}
|
|
onEnterPublished={() => {}}
|
|
entryConfig={{
|
|
...testEntryConfig,
|
|
eventBanners: [
|
|
{
|
|
...testEntryConfig.eventBanner,
|
|
title: 'HTML 后台横幅',
|
|
renderMode: 'html',
|
|
htmlCode: '<section><h1>自定义横幅</h1></section>',
|
|
},
|
|
],
|
|
}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[
|
|
{
|
|
workId: 'draft:recent-session',
|
|
sourceType: 'agent_session',
|
|
status: 'draft',
|
|
title: '后端返回的最近草稿',
|
|
subtitle: '待完善草稿',
|
|
summary: '这条内容来自作品架摘要。',
|
|
coverImageSrc: null,
|
|
updatedAt: buildUpdatedAtDaysAgo(1),
|
|
publishedAt: null,
|
|
stage: 'clarifying',
|
|
stageLabel: '待完善草稿',
|
|
playableNpcCount: 0,
|
|
landmarkCount: 0,
|
|
sessionId: 'recent-session',
|
|
profileId: null,
|
|
canResume: true,
|
|
canEnterWorld: false,
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[
|
|
{
|
|
workId: 'draft:local-pending-session',
|
|
sourceType: 'agent_session',
|
|
status: 'draft',
|
|
title: '本地生成中占位',
|
|
subtitle: '本地占位',
|
|
summary: '这条占位不应该进入最近创作。',
|
|
coverImageSrc: null,
|
|
updatedAt: buildUpdatedAtDaysAgo(0),
|
|
publishedAt: null,
|
|
stage: 'generating',
|
|
stageLabel: '生成中',
|
|
playableNpcCount: 0,
|
|
landmarkCount: 0,
|
|
sessionId: 'local-pending-session',
|
|
profileId: null,
|
|
canResume: true,
|
|
canEnterWorld: false,
|
|
},
|
|
]}
|
|
recentWorkItems={recentWorkItems}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[
|
|
{
|
|
workId: 'draft:old-session',
|
|
sourceType: 'agent_session',
|
|
status: 'draft',
|
|
title: '八天前的草稿',
|
|
subtitle: '旧草稿',
|
|
summary: '这条草稿已经超过最近创作期限。',
|
|
coverImageSrc: null,
|
|
updatedAt: buildUpdatedAtDaysAgo(8),
|
|
publishedAt: null,
|
|
stage: 'clarifying',
|
|
stageLabel: '待完善草稿',
|
|
playableNpcCount: 0,
|
|
landmarkCount: 0,
|
|
sessionId: 'old-session',
|
|
profileId: null,
|
|
canResume: true,
|
|
canEnterWorld: false,
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
jumpHopItems={[
|
|
{
|
|
runtimeKind: 'jump-hop',
|
|
workId: 'jump-hop-work-1',
|
|
profileId: 'jump-hop-profile-1',
|
|
ownerUserId: 'user-1',
|
|
sourceSessionId: 'jump-hop-session-1',
|
|
themeText: '跳一跳生成草稿',
|
|
workTitle: '跳一跳生成草稿',
|
|
workDescription: '后端仍在生成跳一跳玩法。',
|
|
themeTags: ['跳一跳'],
|
|
difficulty: 'standard',
|
|
stylePreset: 'minimal-blocks',
|
|
coverImageSrc: null,
|
|
publicationStatus: 'draft',
|
|
playCount: 0,
|
|
updatedAt: buildUpdatedAtDaysAgo(1),
|
|
publishedAt: null,
|
|
publishReady: false,
|
|
generationStatus: 'generating',
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[
|
|
{
|
|
workId: 'draft:failed-session',
|
|
sourceType: 'agent_session',
|
|
status: 'draft',
|
|
title: '失败但仍可恢复的草稿',
|
|
subtitle: '生成失败',
|
|
summary: '失败草稿也来自真实作品架摘要。',
|
|
coverImageSrc: null,
|
|
updatedAt: buildUpdatedAtDaysAgo(1),
|
|
publishedAt: null,
|
|
stage: 'failed',
|
|
stageLabel: '生成失败',
|
|
playableNpcCount: 0,
|
|
landmarkCount: 0,
|
|
sessionId: 'failed-session',
|
|
profileId: null,
|
|
canResume: true,
|
|
canEnterWorld: false,
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
puzzleItems={[
|
|
{
|
|
workId: 'puzzle:work-1',
|
|
profileId: 'puzzle-profile-1',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
levelName: '潮雾拼图',
|
|
summary: '一张被切成拼图的潮雾港口主视觉。',
|
|
themeTags: ['潮雾', '港口'],
|
|
coverImageSrc: null,
|
|
publicationStatus: 'published',
|
|
updatedAt: new Date('2026-04-22T10:00:00.000Z').toISOString(),
|
|
publishedAt: new Date('2026-04-22T10:05:00.000Z').toISOString(),
|
|
playCount: 12,
|
|
remixCount: 3,
|
|
likeCount: 4,
|
|
publishReady: true,
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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('潮雾</span>');
|
|
expect(html).not.toContain('港口</span>');
|
|
expect(html).not.toContain('我的拼图作品');
|
|
});
|
|
|
|
test('creation hub marks generating and newly completed drafts', () => {
|
|
const html = renderToStaticMarkup(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
puzzleItems={[
|
|
{
|
|
workId: 'puzzle-work-session-1',
|
|
profileId: 'puzzle-profile-session-1',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '潮雾拼图草稿',
|
|
workDescription: '正在生成首张拼图主视觉。',
|
|
levelName: '潮雾拼图',
|
|
summary: '正在生成首张拼图主视觉。',
|
|
themeTags: ['潮雾'],
|
|
coverImageSrc: null,
|
|
publicationStatus: 'draft',
|
|
updatedAt: new Date('2026-04-22T10:00:00.000Z').toISOString(),
|
|
publishedAt: null,
|
|
publishReady: false,
|
|
sourceSessionId: 'puzzle-session-1',
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
puzzleItems={[
|
|
{
|
|
workId: 'puzzle-work-session-1',
|
|
profileId: 'puzzle-profile-session-1',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '潮雾拼图草稿',
|
|
workDescription: '已经有可查看的首关结果。',
|
|
levelName: '潮雾拼图',
|
|
summary: '已经有可查看的首关结果。',
|
|
themeTags: ['潮雾'],
|
|
coverImageSrc: '/generated-puzzle-assets/session/cover.png',
|
|
publicationStatus: 'draft',
|
|
updatedAt: new Date('2026-04-22T10:00:00.000Z').toISOString(),
|
|
publishedAt: null,
|
|
publishReady: false,
|
|
sourceSessionId: 'puzzle-session-1',
|
|
generationStatus: 'generating',
|
|
levels: [
|
|
{
|
|
levelId: 'puzzle-level-1',
|
|
levelName: '潮雾拼图',
|
|
pictureDescription: '潮雾港口。',
|
|
pictureReference: null,
|
|
candidates: [
|
|
{
|
|
candidateId: 'candidate-1',
|
|
imageSrc: '/generated-puzzle-assets/session/cover.png',
|
|
assetId: 'asset-1',
|
|
prompt: '潮雾港口',
|
|
actualPrompt: null,
|
|
sourceType: 'generated',
|
|
selected: true,
|
|
},
|
|
],
|
|
selectedCandidateId: 'candidate-1',
|
|
coverImageSrc: '/generated-puzzle-assets/session/cover.png',
|
|
coverAssetId: 'asset-1',
|
|
generationStatus: 'ready',
|
|
},
|
|
{
|
|
levelId: 'puzzle-level-2',
|
|
levelName: '灯塔',
|
|
pictureDescription: '灯塔新关卡。',
|
|
pictureReference: null,
|
|
candidates: [],
|
|
selectedCandidateId: null,
|
|
coverImageSrc: null,
|
|
coverAssetId: null,
|
|
generationStatus: 'generating',
|
|
},
|
|
],
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
puzzleItems={[
|
|
{
|
|
workId: 'puzzle:work-1',
|
|
profileId: 'puzzle-profile-1',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
levelName: '潮雾拼图',
|
|
summary: '一张被切成拼图的潮雾港口主视觉。',
|
|
themeTags: ['潮雾', '港口'],
|
|
coverImageSrc: null,
|
|
publicationStatus: 'published',
|
|
updatedAt: new Date('2026-04-22T10:00:00.000Z').toISOString(),
|
|
publishedAt: new Date('2026-04-22T10:05:00.000Z').toISOString(),
|
|
playCount: 12,
|
|
likeCount: 0,
|
|
publishReady: true,
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
mode="works-only"
|
|
items={[]}
|
|
puzzleItems={[
|
|
{
|
|
workId: 'puzzle:old-draft',
|
|
profileId: 'puzzle-profile-old',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '旧草稿',
|
|
workDescription: '先前修改的拼图草稿。',
|
|
levelName: '旧草稿',
|
|
summary: '先前修改的拼图草稿。',
|
|
themeTags: [],
|
|
coverImageSrc: '/covers/old-draft.webp',
|
|
publicationStatus: 'draft',
|
|
updatedAt: '2026-05-07T00:00:00.000Z',
|
|
publishedAt: null,
|
|
publishReady: false,
|
|
},
|
|
{
|
|
workId: 'puzzle:new-draft',
|
|
profileId: 'puzzle-profile-new',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '新草稿',
|
|
workDescription: '最近修改的拼图草稿。',
|
|
levelName: '新草稿',
|
|
summary: '最近修改的拼图草稿。',
|
|
themeTags: [],
|
|
coverImageSrc: '/covers/new-draft.webp',
|
|
publicationStatus: 'draft',
|
|
updatedAt: '1778457601.234567Z',
|
|
publishedAt: null,
|
|
publishReady: false,
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
mode="works-only"
|
|
items={[]}
|
|
puzzleItems={[
|
|
{
|
|
workId: 'puzzle:no-cover-draft',
|
|
profileId: 'puzzle-profile-no-cover',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '缺少封面的拼图草稿',
|
|
workDescription: '没有生成封面时也需要保留图像背景。',
|
|
levelName: '缺少封面的拼图草稿',
|
|
summary: '没有生成封面时也需要保留图像背景。',
|
|
themeTags: [],
|
|
coverImageSrc: null,
|
|
publicationStatus: 'draft',
|
|
updatedAt: '2026-05-07T00:00:00.000Z',
|
|
publishedAt: null,
|
|
publishReady: false,
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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('>封面</div>');
|
|
});
|
|
|
|
test('creation hub published card keeps publish info without fixed action text', () => {
|
|
const html = renderToStaticMarkup(
|
|
<CustomWorldCreationHub
|
|
mode="works-only"
|
|
items={[]}
|
|
puzzleItems={[
|
|
{
|
|
workId: 'puzzle:published-card',
|
|
profileId: 'puzzle-profile-published',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '测试作者',
|
|
workTitle: '统一卡片作品',
|
|
workDescription: '作品卡仍要保留原有的积分与统计信息。',
|
|
levelName: '统一卡片作品',
|
|
summary: '作品卡仍要保留原有的积分与统计信息。',
|
|
themeTags: ['潮雾'],
|
|
coverImageSrc: '/covers/unified-card.webp',
|
|
publicationStatus: 'published',
|
|
updatedAt: '2026-05-07T00:00:00.000Z',
|
|
publishedAt: '2026-05-07T00:00:00.000Z',
|
|
playCount: 88,
|
|
remixCount: 9,
|
|
likeCount: 6,
|
|
publishReady: true,
|
|
pointIncentiveTotalPoints: 4,
|
|
pointIncentiveClaimablePoints: 1,
|
|
},
|
|
]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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(
|
|
<CustomWorldCreationHub
|
|
mode="works-only"
|
|
items={[]}
|
|
loading={false}
|
|
error={null}
|
|
onRetry={() => {}}
|
|
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');
|
|
});
|