refactor: 收口公开作品详情映射
This commit is contained in:
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user