refactor: 收口公开作品详情映射

This commit is contained in:
2026-06-04 00:17:31 +08:00
parent dd52848e9c
commit 39522f3b96
8 changed files with 675 additions and 232 deletions

View File

@@ -1,6 +1,13 @@
import { expect, test } from 'vitest';
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish';
import {
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
@@ -8,6 +15,18 @@ import {
} from '../rpg-entry/rpgEntryWorldPresentation';
import {
getPlatformPublicWorkDetailKind,
mapBarkBattlePublicDetailToWorkSummary,
mapBarkBattleWorkToPublicWorkDetail,
mapBigFishWorkToPublicWorkDetail,
mapJumpHopWorkToPublicWorkDetail,
mapPublicWorkDetailToBigFishWork,
mapPublicWorkDetailToPuzzleWork,
mapPublicWorkDetailToSquareHoleWork,
mapPuzzleWorkToPublicWorkDetail,
mapRpgGalleryCardToPublicWorkDetail,
mapSquareHoleWorkToPublicWorkDetail,
mapVisualNovelWorkToPublicWorkDetail,
mapWoodenFishWorkToPublicWorkDetail,
type PlatformPublicWorkDetailKind,
type PlatformPublicWorkDetailOpenStrategy,
resolveActivePlatformPublicWorkAuthorEntry,
@@ -21,10 +40,21 @@ type TypedPlatformPublicGalleryCard = Extract<
{ sourceType: string }
>;
type PlatformGallerySourceType = TypedPlatformPublicGalleryCard['sourceType'];
type TypedPlatformPublicGalleryCardOverrides = Partial<
Omit<TypedPlatformPublicGalleryCard, 'sourceType'>
type TypedPlatformPublicGalleryCardOverrides<
TSourceType extends PlatformGallerySourceType,
> = Partial<
Omit<
Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }>,
'sourceType'
>
>;
function narrowTypedEntry<TSourceType extends PlatformGallerySourceType>(
entry: TypedPlatformPublicGalleryCard,
): Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }> {
return entry as Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }>;
}
function buildRpgEntry(
overrides: Partial<CustomWorldGalleryCard> = {},
): CustomWorldGalleryCard {
@@ -48,10 +78,10 @@ function buildRpgEntry(
};
}
function buildTypedEntry(
sourceType: PlatformGallerySourceType,
overrides: TypedPlatformPublicGalleryCardOverrides = {},
): PlatformPublicGalleryCard {
function buildTypedEntry<TSourceType extends PlatformGallerySourceType>(
sourceType: TSourceType,
overrides: TypedPlatformPublicGalleryCardOverrides<TSourceType> = {},
): Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }> {
const common = {
workId: `${sourceType}-work`,
profileId: `${sourceType}-profile`,
@@ -70,31 +100,30 @@ function buildTypedEntry(
switch (sourceType) {
case 'puzzle':
return { ...common, ...overrides, sourceType };
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
case 'big-fish':
return { ...common, ...overrides, sourceType };
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
case 'match3d':
return { ...common, ...overrides, sourceType };
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
case 'square-hole':
return { ...common, ...overrides, sourceType };
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
case 'visual-novel':
return { ...common, ...overrides, sourceType };
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
case 'jump-hop':
return { ...common, ...overrides, sourceType };
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
case 'wooden-fish':
return { ...common, ...overrides, sourceType };
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
case 'edutainment':
return {
return narrowTypedEntry<TSourceType>({
...common,
...overrides,
sourceType,
templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
};
case 'bark-battle':
return {
...common,
...overrides,
});
case 'bark-battle':
return narrowTypedEntry<TSourceType>({
...common,
sourceType,
authorPublicUserCode: null,
coverRenderMode: 'image',
@@ -102,14 +131,190 @@ function buildTypedEntry(
themeMode: 'martial',
playableNpcCount: 1,
landmarkCount: 1,
};
...overrides,
});
default: {
const exhaustive: never = sourceType;
return exhaustive;
throw new Error(`Unsupported source type: ${sourceType}`);
}
}
}
function buildPuzzleWork(
overrides: Partial<PuzzleWorkSummary> = {},
): PuzzleWorkSummary {
return {
workId: 'puzzle-work',
profileId: 'puzzle-profile',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-session',
authorDisplayName: '玩家',
workTitle: '拼图作品',
workDescription: '拼图描述',
levelName: '第一关',
summary: '拼图摘要',
themeTags: ['拼图'],
coverImageSrc: '/puzzle-cover.png',
publicationStatus: 'published',
updatedAt: '2026-06-01T01:00:00.000Z',
publishedAt: '2026-06-01T00:00:00.000Z',
playCount: 3,
remixCount: 2,
likeCount: 1,
publishReady: true,
...overrides,
};
}
function buildBigFishWork(
overrides: Partial<BigFishWorkSummary> = {},
): BigFishWorkSummary {
return {
workId: 'big-fish-work',
sourceSessionId: 'big-fish-session',
ownerUserId: 'user-1',
authorDisplayName: '玩家',
title: '大鱼作品',
subtitle: '大鱼吃小鱼',
summary: '大鱼摘要',
coverImageSrc: '/big-fish-cover.png',
status: 'published',
updatedAt: '2026-06-01T01:00:00.000Z',
publishedAt: '2026-06-01T00:00:00.000Z',
publishReady: true,
levelCount: 12,
levelMainImageReadyCount: 0,
levelMotionReadyCount: 0,
backgroundReady: true,
playCount: 4,
remixCount: 3,
likeCount: 2,
...overrides,
};
}
function buildSquareHoleWork(
overrides: Partial<SquareHoleWorkSummary> = {},
): SquareHoleWorkSummary {
return {
workId: 'square-hole-work',
profileId: 'square-hole-profile',
ownerUserId: 'user-1',
sourceSessionId: 'square-hole-session',
gameName: '方洞作品',
themeText: '形状',
twistRule: '反直觉',
summary: '方洞摘要',
tags: ['方洞'],
coverImageSrc: '/square-hole-cover.png',
backgroundPrompt: '方洞背景',
backgroundImageSrc: '/square-hole-bg.png',
shapeOptions: [],
holeOptions: [],
shapeCount: 8,
difficulty: 4,
publicationStatus: 'published',
playCount: 5,
updatedAt: '2026-06-01T01:00:00.000Z',
publishedAt: '2026-06-01T00:00:00.000Z',
publishReady: true,
...overrides,
};
}
function buildVisualNovelWork(
overrides: Partial<VisualNovelWorkSummary> = {},
): VisualNovelWorkSummary {
return {
runtimeKind: 'visual-novel',
profileId: 'visual-novel-profile',
ownerUserId: 'user-1',
title: '视觉小说作品',
description: '视觉小说摘要',
coverImageSrc: '/visual-novel-cover.png',
tags: ['视觉小说'],
publishStatus: 'published',
publishReady: true,
playCount: 6,
updatedAt: '2026-06-01T01:00:00.000Z',
publishedAt: '2026-06-01T00:00:00.000Z',
...overrides,
};
}
function buildJumpHopGalleryCard(
overrides: Partial<JumpHopGalleryCardResponse> = {},
): JumpHopGalleryCardResponse {
return {
publicWorkCode: 'JH-0001',
workId: 'jump-hop-work',
profileId: 'jump-hop-profile',
ownerUserId: 'user-1',
authorDisplayName: '玩家',
workTitle: '跳一跳作品',
workDescription: '跳一跳摘要',
coverImageSrc: '/jump-hop-cover.png',
themeTags: ['跳一跳'],
difficulty: 'standard',
stylePreset: 'paper-toy',
publicationStatus: 'published',
playCount: 7,
updatedAt: '2026-06-01T01:00:00.000Z',
publishedAt: '2026-06-01T00:00:00.000Z',
generationStatus: 'ready',
...overrides,
};
}
function buildWoodenFishGalleryCard(
overrides: Partial<WoodenFishGalleryCardResponse> = {},
): WoodenFishGalleryCardResponse {
return {
publicWorkCode: 'WF-0001',
workId: 'wooden-fish-work',
profileId: 'wooden-fish-profile',
ownerUserId: 'user-1',
authorDisplayName: '玩家',
workTitle: '木鱼作品',
workDescription: '木鱼摘要',
coverImageSrc: '/wooden-fish-cover.png',
themeTags: ['敲木鱼'],
publicationStatus: 'published',
playCount: 8,
updatedAt: '2026-06-01T01:00:00.000Z',
publishedAt: '2026-06-01T00:00:00.000Z',
generationStatus: 'ready',
...overrides,
};
}
function buildBarkBattleWork(
overrides: Partial<BarkBattleWorkSummary> = {},
): BarkBattleWorkSummary {
return {
workId: 'bark-battle-work',
draftId: 'bark-battle-draft',
ownerUserId: 'user-1',
authorDisplayName: '玩家',
title: '汪汪声浪作品',
summary: '汪汪摘要',
themeDescription: '森林擂台',
playerImageDescription: '小狗',
opponentImageDescription: '对手',
playerCharacterImageSrc: '/player.png',
opponentCharacterImageSrc: '/opponent.png',
uiBackgroundImageSrc: '/bark-bg.png',
difficultyPreset: 'normal',
status: 'published',
generationStatus: 'ready',
publishReady: true,
playCount: 9,
recentPlayCount7d: 2,
updatedAt: '2026-06-01T01:00:00.000Z',
publishedAt: '2026-06-01T00:00:00.000Z',
...overrides,
};
}
test('platform public work detail flow resolves detail kind for every play kind', () => {
const cases: Array<
[sourceType: PlatformGallerySourceType, kind: PlatformPublicWorkDetailKind]
@@ -221,6 +426,173 @@ test('platform public work detail flow resolves open strategy', () => {
});
});
test('platform public work detail flow maps work summaries to detail entries', () => {
const rpgEntry = buildRpgEntry();
expect(mapRpgGalleryCardToPublicWorkDetail(rpgEntry)).toBe(rpgEntry);
expect(mapPuzzleWorkToPublicWorkDetail(buildPuzzleWork())).toMatchObject({
sourceType: 'puzzle',
workId: 'puzzle-work',
profileId: 'puzzle-profile',
playCount: 3,
remixCount: 2,
likeCount: 1,
});
expect(mapBigFishWorkToPublicWorkDetail(buildBigFishWork())).toMatchObject({
sourceType: 'big-fish',
workId: 'big-fish-work',
profileId: 'big-fish-session',
playCount: 4,
});
expect(
mapSquareHoleWorkToPublicWorkDetail(buildSquareHoleWork()),
).toMatchObject({
sourceType: 'square-hole',
workId: 'square-hole-work',
profileId: 'square-hole-profile',
backgroundPrompt: '方洞背景',
});
expect(
mapVisualNovelWorkToPublicWorkDetail(buildVisualNovelWork()),
).toMatchObject({
sourceType: 'visual-novel',
workId: 'visual-novel-profile',
profileId: 'visual-novel-profile',
playCount: 6,
});
expect(
mapJumpHopWorkToPublicWorkDetail(buildJumpHopGalleryCard()),
).toMatchObject({
sourceType: 'jump-hop',
workId: 'jump-hop-work',
profileId: 'jump-hop-profile',
publicWorkCode: 'JH-0001',
});
expect(
mapWoodenFishWorkToPublicWorkDetail(buildWoodenFishGalleryCard()),
).toMatchObject({
sourceType: 'wooden-fish',
workId: 'wooden-fish-work',
profileId: 'wooden-fish-profile',
publicWorkCode: 'WF-0001',
});
expect(
mapBarkBattleWorkToPublicWorkDetail(buildBarkBattleWork()),
).toMatchObject({
sourceType: 'bark-battle',
workId: 'bark-battle-work',
sourceSessionId: 'bark-battle-draft',
coverRenderMode: 'scene_with_roles',
coverCharacterImageSrcs: ['/player.png', '/opponent.png'],
});
});
test('platform public work detail flow maps detail entries back to work summaries', () => {
expect(
mapPublicWorkDetailToPuzzleWork({
...buildTypedEntry('puzzle', {
coverSlides: [
{
id: 'level-1',
imageSrc: '/level-1.png',
label: '第一关',
},
],
playCount: 10,
remixCount: 4,
likeCount: 3,
}),
sourceSessionId: 'puzzle-session',
}),
).toMatchObject({
workId: 'puzzle-work',
profileId: 'puzzle-profile',
sourceSessionId: 'puzzle-session',
playCount: 10,
remixCount: 4,
likeCount: 3,
pointIncentiveTotalPoints: 0,
levels: [
{
levelId: 'level-1',
levelName: '第一关',
coverImageSrc: '/level-1.png',
generationStatus: 'ready',
},
],
});
expect(
mapPublicWorkDetailToBigFishWork(
buildTypedEntry('big-fish', {
themeTags: ['大鱼', '12级'],
coverImageSrc: '/big-fish-cover.png',
}),
),
).toMatchObject({
workId: 'big-fish-work',
sourceSessionId: 'big-fish-profile',
levelCount: 12,
backgroundReady: true,
});
expect(
mapPublicWorkDetailToBigFishWork(
buildTypedEntry('big-fish', { themeTags: ['大鱼'] }),
)?.levelCount,
).toBe(0);
expect(
mapPublicWorkDetailToSquareHoleWork(
buildTypedEntry('square-hole', { themeTags: [] }),
),
).toMatchObject({
workId: 'square-hole-work',
profileId: 'square-hole-profile',
themeText: '方洞挑战',
backgroundPrompt: '方洞挑战运行背景',
shapeOptions: [],
holeOptions: [],
shapeCount: 8,
difficulty: 4,
});
expect(
mapBarkBattlePublicDetailToWorkSummary(
{
...buildTypedEntry('bark-battle', {
themeTags: ['森林', '小狗', '对手'],
coverImageSrc: '/bark-bg.png',
coverCharacterImageSrcs: ['/player.png', '/opponent.png'],
playCount: 11,
recentPlayCount7d: 5,
}),
sourceSessionId: 'bark-draft',
},
),
).toMatchObject({
workId: 'bark-battle-work',
draftId: 'bark-draft',
themeDescription: '森林',
playerImageDescription: '小狗',
opponentImageDescription: '对手',
playerCharacterImageSrc: '/player.png',
opponentCharacterImageSrc: '/opponent.png',
uiBackgroundImageSrc: '/bark-bg.png',
difficultyPreset: 'normal',
playCount: 11,
recentPlayCount7d: 5,
});
expect(mapPublicWorkDetailToPuzzleWork(buildTypedEntry('big-fish'))).toBeNull();
expect(mapPublicWorkDetailToBigFishWork(buildTypedEntry('puzzle'))).toBeNull();
expect(
mapPublicWorkDetailToSquareHoleWork(buildTypedEntry('puzzle')),
).toBeNull();
expect(
mapBarkBattlePublicDetailToWorkSummary(buildTypedEntry('puzzle')),
).toBeNull();
});
test('platform public work detail flow resolves edit mode only for owned works', () => {
const entry = buildTypedEntry('puzzle');