1503 lines
42 KiB
TypeScript
1503 lines
42 KiB
TypeScript
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 { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
|
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
|
import type { PuzzleClearGalleryCardResponse } from '../../../packages/shared/src/contracts/puzzleClear';
|
|
import type {
|
|
CustomWorldGalleryCard,
|
|
CustomWorldLibraryEntry,
|
|
} 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 type { CustomWorldProfile } from '../../types';
|
|
import {
|
|
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
|
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
|
type PlatformPublicGalleryCard,
|
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
|
import {
|
|
getPlatformPublicWorkDetailKind,
|
|
mapBarkBattlePublicDetailToWorkSummary,
|
|
mapBarkBattleWorkToPublicWorkDetail,
|
|
mapBigFishWorkToPublicWorkDetail,
|
|
mapJumpHopWorkToPublicWorkDetail,
|
|
mapPuzzleClearWorkToPublicWorkDetail,
|
|
mapPublicWorkDetailToBigFishWork,
|
|
mapPublicWorkDetailToPuzzleWork,
|
|
mapPublicWorkDetailToSquareHoleWork,
|
|
mapPuzzleWorkToPublicWorkDetail,
|
|
mapRpgGalleryCardToPublicWorkDetail,
|
|
mapSquareHoleWorkToPublicWorkDetail,
|
|
mapVisualNovelWorkToPublicWorkDetail,
|
|
mapWoodenFishWorkToPublicWorkDetail,
|
|
type PlatformPublicWorkDetailKind,
|
|
type PlatformPublicWorkDetailOpenStrategy,
|
|
type PlatformPublicWorkEditIntentDeps,
|
|
type PlatformPublicWorkStartIntentDeps,
|
|
resolveActivePlatformPublicWorkAuthorEntry,
|
|
resolvePlatformPublicWorkActionMode,
|
|
resolvePlatformPublicWorkDetailOpenDecision,
|
|
resolvePlatformPublicWorkDetailOpenStrategy,
|
|
resolvePlatformPublicWorkEditIntent,
|
|
resolvePlatformPublicWorkLikeIntent,
|
|
resolvePlatformPublicWorkRemixIntent,
|
|
resolvePlatformPublicWorkStartIntent,
|
|
resolveVisiblePuzzleDetailCoverCount,
|
|
} from './platformPublicWorkDetailFlow';
|
|
|
|
type TypedPlatformPublicGalleryCard = Extract<
|
|
PlatformPublicGalleryCard,
|
|
{ sourceType: string }
|
|
>;
|
|
type PlatformGallerySourceType = 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 {
|
|
return {
|
|
ownerUserId: 'user-1',
|
|
profileId: 'rpg-profile',
|
|
publicWorkCode: 'CW-RPG',
|
|
authorPublicUserCode: null,
|
|
visibility: 'published',
|
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
|
authorDisplayName: '玩家',
|
|
worldName: 'RPG 世界',
|
|
subtitle: '公开作品',
|
|
summaryText: '公开作品摘要',
|
|
coverImageSrc: null,
|
|
themeMode: 'martial',
|
|
playableNpcCount: 1,
|
|
landmarkCount: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildRpgLibraryEntry(
|
|
overrides: Partial<CustomWorldLibraryEntry<CustomWorldProfile>> = {},
|
|
): CustomWorldLibraryEntry<CustomWorldProfile> {
|
|
return {
|
|
...buildRpgEntry(overrides),
|
|
profile: {
|
|
id: overrides.profileId ?? 'rpg-profile',
|
|
} as unknown as CustomWorldProfile,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildTypedEntry<TSourceType extends PlatformGallerySourceType>(
|
|
sourceType: TSourceType,
|
|
overrides: TypedPlatformPublicGalleryCardOverrides<TSourceType> = {},
|
|
): Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }> {
|
|
const common = {
|
|
workId: `${sourceType}-work`,
|
|
profileId: `${sourceType}-profile`,
|
|
publicWorkCode: `${sourceType}-code`,
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '玩家',
|
|
worldName: `${sourceType} 作品`,
|
|
subtitle: '公开作品',
|
|
summaryText: '公开作品摘要',
|
|
coverImageSrc: null,
|
|
themeTags: [sourceType],
|
|
visibility: 'published' as const,
|
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
|
};
|
|
|
|
switch (sourceType) {
|
|
case 'puzzle':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
});
|
|
case 'puzzle-clear':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
themePrompt: '拼消消主题',
|
|
});
|
|
case 'big-fish':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
});
|
|
case 'match3d':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
});
|
|
case 'square-hole':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
});
|
|
case 'visual-novel':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
});
|
|
case 'jump-hop':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
});
|
|
case 'wooden-fish':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
});
|
|
case 'edutainment':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
sourceType,
|
|
templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
|
templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
|
...overrides,
|
|
});
|
|
case 'bark-battle':
|
|
return narrowTypedEntry<TSourceType>({
|
|
...common,
|
|
sourceType,
|
|
authorPublicUserCode: null,
|
|
coverRenderMode: 'image',
|
|
coverCharacterImageSrcs: [],
|
|
themeMode: 'martial',
|
|
playableNpcCount: 1,
|
|
landmarkCount: 1,
|
|
...overrides,
|
|
});
|
|
default: {
|
|
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 buildPuzzleRun(
|
|
overrides: Partial<PuzzleRunSnapshot> = {},
|
|
): PuzzleRunSnapshot {
|
|
return {
|
|
runId: 'puzzle-run',
|
|
entryProfileId: 'puzzle-profile',
|
|
clearedLevelCount: 0,
|
|
currentLevelIndex: 0,
|
|
currentGridSize: 3,
|
|
playedProfileIds: ['puzzle-profile'],
|
|
previousLevelTags: [],
|
|
currentLevel: null,
|
|
recommendedNextProfileId: null,
|
|
leaderboardEntries: [],
|
|
...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: '玩家',
|
|
themeText: '跳一跳',
|
|
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 buildPuzzleClearGalleryCard(
|
|
overrides: Partial<PuzzleClearGalleryCardResponse> = {},
|
|
): PuzzleClearGalleryCardResponse {
|
|
return {
|
|
runtimeKind: 'puzzle-clear',
|
|
publicWorkCode: 'PCLR-0001',
|
|
workId: 'puzzle-clear-work',
|
|
profileId: 'puzzle-clear-profile',
|
|
ownerUserId: 'user-1',
|
|
sourceSessionId: 'puzzle-clear-session',
|
|
authorDisplayName: '玩家',
|
|
workTitle: '拼消消作品',
|
|
workDescription: '拼消消摘要',
|
|
themePrompt: '水果',
|
|
coverImageSrc: '/puzzle-clear-cover.png',
|
|
publicationStatus: 'published',
|
|
playCount: 6,
|
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
|
publishReady: true,
|
|
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,
|
|
};
|
|
}
|
|
|
|
function buildMatch3DWork(
|
|
overrides: Partial<Match3DWorkSummary> = {},
|
|
): Match3DWorkSummary {
|
|
return {
|
|
workId: 'match3d-work',
|
|
profileId: 'match3d-profile',
|
|
ownerUserId: 'user-1',
|
|
sourceSessionId: 'match3d-session',
|
|
gameName: '抓大鹅作品',
|
|
themeText: '经典消除',
|
|
summary: '抓大鹅摘要',
|
|
tags: ['抓大鹅'],
|
|
coverImageSrc: '/match3d-cover.png',
|
|
referenceImageSrc: null,
|
|
clearCount: 12,
|
|
difficulty: 4,
|
|
publicationStatus: 'published',
|
|
playCount: 10,
|
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
|
publishReady: true,
|
|
generatedItemAssets: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildStartIntentDeps(
|
|
overrides: Partial<PlatformPublicWorkStartIntentDeps> = {},
|
|
): PlatformPublicWorkStartIntentDeps {
|
|
return {
|
|
selectedPuzzleDetail: null,
|
|
selectedRpgDetailEntry: null,
|
|
barkBattleGalleryEntries: [],
|
|
barkBattleWorks: [],
|
|
mapMatch3DWork: () => buildMatch3DWork(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildEditIntentDeps(
|
|
overrides: Partial<PlatformPublicWorkEditIntentDeps> = {},
|
|
): PlatformPublicWorkEditIntentDeps {
|
|
return {
|
|
selectedPuzzleDetail: null,
|
|
selectedRpgDetailEntry: null,
|
|
visualNovelWorks: [],
|
|
barkBattleGalleryEntries: [],
|
|
barkBattleWorks: [],
|
|
mapMatch3DWork: () => buildMatch3DWork(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('platform public work detail flow resolves detail kind for every play kind', () => {
|
|
const cases: Array<
|
|
[sourceType: PlatformGallerySourceType, kind: PlatformPublicWorkDetailKind]
|
|
> = [
|
|
['big-fish', 'big-fish'],
|
|
['puzzle', 'puzzle'],
|
|
['puzzle-clear', 'puzzle-clear'],
|
|
['jump-hop', 'jump-hop'],
|
|
['wooden-fish', 'wooden-fish'],
|
|
['match3d', 'match3d'],
|
|
['square-hole', 'square-hole'],
|
|
['visual-novel', 'visual-novel'],
|
|
['bark-battle', 'bark-battle'],
|
|
['edutainment', 'edutainment'],
|
|
];
|
|
|
|
cases.forEach(([sourceType, kind]) => {
|
|
expect(getPlatformPublicWorkDetailKind(buildTypedEntry(sourceType))).toBe(
|
|
kind,
|
|
);
|
|
});
|
|
|
|
expect(getPlatformPublicWorkDetailKind(buildRpgEntry())).toBe('rpg');
|
|
});
|
|
|
|
test('platform public work detail flow resolves open strategy', () => {
|
|
const rpgEntry = buildRpgLibraryEntry();
|
|
const cases: Array<
|
|
[
|
|
entry: PlatformPublicGalleryCard,
|
|
strategy: PlatformPublicWorkDetailOpenStrategy,
|
|
]
|
|
> = [
|
|
[
|
|
buildTypedEntry('big-fish'),
|
|
{
|
|
type: 'use-entry',
|
|
kind: 'big-fish',
|
|
},
|
|
],
|
|
[
|
|
buildTypedEntry('match3d'),
|
|
{
|
|
type: 'use-entry',
|
|
kind: 'match3d',
|
|
},
|
|
],
|
|
[
|
|
buildTypedEntry('square-hole'),
|
|
{
|
|
type: 'use-entry',
|
|
kind: 'square-hole',
|
|
},
|
|
],
|
|
[
|
|
buildTypedEntry('bark-battle'),
|
|
{
|
|
type: 'use-entry',
|
|
kind: 'bark-battle',
|
|
},
|
|
],
|
|
[
|
|
buildTypedEntry('edutainment'),
|
|
{
|
|
type: 'use-entry',
|
|
kind: 'edutainment',
|
|
},
|
|
],
|
|
[
|
|
buildTypedEntry('puzzle-clear'),
|
|
{
|
|
type: 'use-entry',
|
|
kind: 'puzzle-clear',
|
|
},
|
|
],
|
|
[
|
|
buildTypedEntry('puzzle'),
|
|
{
|
|
type: 'load-puzzle-detail',
|
|
profileId: 'puzzle-profile',
|
|
},
|
|
],
|
|
[
|
|
buildTypedEntry('jump-hop'),
|
|
{
|
|
type: 'load-jump-hop-detail',
|
|
profileId: 'jump-hop-profile',
|
|
},
|
|
],
|
|
[
|
|
buildTypedEntry('wooden-fish'),
|
|
{
|
|
type: 'load-wooden-fish-detail',
|
|
profileId: 'wooden-fish-profile',
|
|
},
|
|
],
|
|
[
|
|
buildTypedEntry('visual-novel'),
|
|
{
|
|
type: 'load-visual-novel-detail',
|
|
profileId: 'visual-novel-profile',
|
|
},
|
|
],
|
|
[
|
|
rpgEntry,
|
|
{
|
|
type: 'load-rpg-detail',
|
|
entry: rpgEntry,
|
|
},
|
|
],
|
|
];
|
|
|
|
cases.forEach(([entry, strategy]) => {
|
|
expect(resolvePlatformPublicWorkDetailOpenStrategy(entry)).toEqual(
|
|
strategy,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow maps work summaries to detail entries', () => {
|
|
const rpgEntry = buildRpgLibraryEntry();
|
|
|
|
expect(mapRpgGalleryCardToPublicWorkDetail(rpgEntry)).toEqual({
|
|
ownerUserId: rpgEntry.ownerUserId,
|
|
profileId: rpgEntry.profileId,
|
|
publicWorkCode: rpgEntry.publicWorkCode,
|
|
authorPublicUserCode: rpgEntry.authorPublicUserCode,
|
|
visibility: rpgEntry.visibility,
|
|
publishedAt: rpgEntry.publishedAt,
|
|
updatedAt: rpgEntry.updatedAt,
|
|
authorDisplayName: rpgEntry.authorDisplayName,
|
|
worldName: rpgEntry.worldName,
|
|
subtitle: rpgEntry.subtitle,
|
|
summaryText: rpgEntry.summaryText,
|
|
coverImageSrc: rpgEntry.coverImageSrc,
|
|
themeMode: rpgEntry.themeMode,
|
|
playableNpcCount: rpgEntry.playableNpcCount,
|
|
landmarkCount: rpgEntry.landmarkCount,
|
|
playCount: rpgEntry.playCount ?? 0,
|
|
remixCount: rpgEntry.remixCount ?? 0,
|
|
likeCount: rpgEntry.likeCount ?? 0,
|
|
recentPlayCount7d: rpgEntry.recentPlayCount7d ?? 0,
|
|
});
|
|
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(
|
|
mapPuzzleClearWorkToPublicWorkDetail(buildPuzzleClearGalleryCard()),
|
|
).toMatchObject({
|
|
sourceType: 'puzzle-clear',
|
|
workId: 'puzzle-clear-work',
|
|
profileId: 'puzzle-clear-profile',
|
|
publicWorkCode: 'PCLR-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 visible puzzle cover count', () => {
|
|
const puzzleEntry = buildTypedEntry('puzzle', {
|
|
profileId: 'puzzle-profile',
|
|
});
|
|
|
|
expect(resolveVisiblePuzzleDetailCoverCount(null, null)).toBe(1);
|
|
expect(
|
|
resolveVisiblePuzzleDetailCoverCount(buildTypedEntry('big-fish'), null),
|
|
).toBe(1);
|
|
expect(
|
|
resolveVisiblePuzzleDetailCoverCount(
|
|
puzzleEntry,
|
|
buildPuzzleRun({ entryProfileId: 'other-profile', clearedLevelCount: 9 }),
|
|
),
|
|
).toBe(1);
|
|
expect(
|
|
resolveVisiblePuzzleDetailCoverCount(
|
|
puzzleEntry,
|
|
buildPuzzleRun({ clearedLevelCount: 2 }),
|
|
),
|
|
).toBe(3);
|
|
expect(
|
|
resolveVisiblePuzzleDetailCoverCount(
|
|
puzzleEntry,
|
|
buildPuzzleRun({ clearedLevelCount: -1 }),
|
|
),
|
|
).toBe(1);
|
|
});
|
|
|
|
test('platform public work detail flow resolves edit mode only for owned works', () => {
|
|
const entry = buildTypedEntry('puzzle');
|
|
|
|
expect(resolvePlatformPublicWorkActionMode(entry, 'user-1')).toBe('edit');
|
|
expect(resolvePlatformPublicWorkActionMode(entry, ' user-1 ')).toBe('edit');
|
|
expect(resolvePlatformPublicWorkActionMode(entry, 'user-2')).toBe('remix');
|
|
expect(resolvePlatformPublicWorkActionMode(entry, null)).toBe('remix');
|
|
});
|
|
|
|
test('platform public work detail flow resolves like intent', () => {
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('big-fish')),
|
|
).toEqual({
|
|
type: 'like-big-fish',
|
|
profileId: 'big-fish-profile',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('puzzle')),
|
|
).toEqual({
|
|
type: 'like-puzzle',
|
|
profileId: 'puzzle-profile',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('puzzle-clear')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '拼消消点赞将在后续版本开放。',
|
|
});
|
|
expect(resolvePlatformPublicWorkLikeIntent(buildRpgEntry())).toEqual({
|
|
type: 'like-rpg-gallery',
|
|
ownerUserId: 'user-1',
|
|
profileId: 'rpg-profile',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('match3d')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '作品类型 match3d 暂不支持点赞。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('jump-hop')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '作品类型 jump-hop 暂不支持点赞。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('wooden-fish')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '作品类型 wooden-fish 暂不支持点赞。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('edutainment')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '宝贝识物点赞将在后续版本开放。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('bark-battle')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '汪汪声浪点赞将在后续版本开放。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('square-hole')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '方洞挑战点赞将在后续版本开放。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('visual-novel')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '视觉小说点赞将在后续版本开放。',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow respects configured like disable', () => {
|
|
expect(
|
|
resolvePlatformPublicWorkLikeIntent(buildTypedEntry('puzzle'), [
|
|
{
|
|
sourceType: 'puzzle',
|
|
likeEnabled: false,
|
|
remixEnabled: true,
|
|
likeDisabledMessage: '拼图点赞维护中。',
|
|
remixDisabledMessage: '拼图改造维护中。',
|
|
},
|
|
]),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '拼图点赞维护中。',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves remix intent', () => {
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('big-fish')),
|
|
).toEqual({
|
|
type: 'remix-big-fish',
|
|
profileId: 'big-fish-profile',
|
|
selectionStage: 'big-fish-result',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('puzzle')),
|
|
).toEqual({
|
|
type: 'remix-puzzle',
|
|
profileId: 'puzzle-profile',
|
|
selectionStage: 'puzzle-result',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('puzzle-clear')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '拼消消作品改造将在后续版本开放。',
|
|
});
|
|
expect(resolvePlatformPublicWorkRemixIntent(buildRpgEntry())).toEqual({
|
|
type: 'remix-rpg-gallery',
|
|
ownerUserId: 'user-1',
|
|
profileId: 'rpg-profile',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('match3d')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '抓大鹅作品改造将在后续版本开放。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('square-hole')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '方洞挑战作品改造将在后续版本开放。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('jump-hop')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '跳一跳作品改造将在后续版本开放。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('wooden-fish')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '敲木鱼作品改造将在后续版本开放。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('visual-novel')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '视觉小说作品改造将在后续版本开放。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('edutainment')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '宝贝识物作品改造将在创作链路接入后开放。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildTypedEntry('bark-battle')),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: '汪汪声浪作品改造将在后续版本开放。',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow respects configured remix disable', () => {
|
|
expect(
|
|
resolvePlatformPublicWorkRemixIntent(buildRpgEntry(), [
|
|
{
|
|
sourceType: 'custom-world',
|
|
likeEnabled: true,
|
|
remixEnabled: false,
|
|
likeDisabledMessage: 'RPG 点赞维护中。',
|
|
remixDisabledMessage: 'RPG 改造维护中。',
|
|
},
|
|
]),
|
|
).toEqual({
|
|
type: 'unsupported',
|
|
errorMessage: 'RPG 改造维护中。',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves edit intent for draft-backed works', () => {
|
|
const bigFishEntry = buildTypedEntry('big-fish');
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(bigFishEntry, buildEditIntentDeps()),
|
|
).toEqual({
|
|
type: 'edit-big-fish',
|
|
work: mapPublicWorkDetailToBigFishWork(bigFishEntry),
|
|
});
|
|
|
|
const selectedPuzzleDetail = buildPuzzleWork({
|
|
profileId: 'puzzle-profile',
|
|
sourceSessionId: 'selected-puzzle-session',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
buildTypedEntry('puzzle'),
|
|
buildEditIntentDeps({ selectedPuzzleDetail }),
|
|
),
|
|
).toEqual({
|
|
type: 'edit-puzzle',
|
|
work: selectedPuzzleDetail,
|
|
});
|
|
|
|
const puzzleEntry = buildTypedEntry('puzzle', {
|
|
profileId: 'fallback-puzzle-profile',
|
|
sourceSessionId: 'fallback-puzzle-session',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
puzzleEntry,
|
|
buildEditIntentDeps({
|
|
selectedPuzzleDetail: buildPuzzleWork({ profileId: 'stale-profile' }),
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'edit-puzzle',
|
|
work: mapPublicWorkDetailToPuzzleWork(puzzleEntry),
|
|
});
|
|
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
buildTypedEntry('puzzle', { sourceSessionId: null }),
|
|
buildEditIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '这份拼图作品缺少原草稿会话,暂时无法编辑。',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves edit intent for mapper-backed works', () => {
|
|
const match3DEntry = buildTypedEntry('match3d');
|
|
const match3DWork = buildMatch3DWork({ workId: 'editable-match3d-work' });
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
match3DEntry,
|
|
buildEditIntentDeps({
|
|
mapMatch3DWork: (entry) =>
|
|
entry === match3DEntry ? match3DWork : null,
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'edit-match3d',
|
|
work: match3DWork,
|
|
forceDraft: true,
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
match3DEntry,
|
|
buildEditIntentDeps({
|
|
mapMatch3DWork: () => buildMatch3DWork({ sourceSessionId: ' ' }),
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '这份抓大鹅作品缺少原草稿会话,暂时无法编辑。',
|
|
});
|
|
|
|
const squareHoleEntry = buildTypedEntry('square-hole', {
|
|
sourceSessionId: 'square-hole-session',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(squareHoleEntry, buildEditIntentDeps()),
|
|
).toEqual({
|
|
type: 'edit-square-hole',
|
|
work: mapPublicWorkDetailToSquareHoleWork(squareHoleEntry),
|
|
forceDraft: true,
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
buildTypedEntry('square-hole', { sourceSessionId: null }),
|
|
buildEditIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '这份方洞挑战作品缺少原草稿会话,暂时无法编辑。',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves edit intent for cached work lookups', () => {
|
|
const visualNovelWork = buildVisualNovelWork();
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
buildTypedEntry('visual-novel'),
|
|
buildEditIntentDeps({ visualNovelWorks: [visualNovelWork] }),
|
|
),
|
|
).toEqual({
|
|
type: 'edit-visual-novel',
|
|
work: visualNovelWork,
|
|
forceDraft: true,
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
buildTypedEntry('visual-novel'),
|
|
buildEditIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '这份视觉小说缺少可编辑草稿。',
|
|
});
|
|
|
|
const entry = buildTypedEntry('bark-battle');
|
|
const galleryWork = buildBarkBattleWork({
|
|
workId: 'bark-battle-work',
|
|
draftId: 'gallery-draft',
|
|
});
|
|
const loadedWork = buildBarkBattleWork({
|
|
workId: 'bark-battle-work',
|
|
draftId: 'loaded-draft',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
entry,
|
|
buildEditIntentDeps({
|
|
barkBattleGalleryEntries: [galleryWork],
|
|
barkBattleWorks: [loadedWork],
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'edit-bark-battle',
|
|
work: loadedWork,
|
|
forceDraft: true,
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
buildTypedEntry('bark-battle', { sourceSessionId: null }),
|
|
buildEditIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '这份汪汪声浪缺少可编辑草稿。',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves edit intent for unsupported and deferred works', () => {
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
buildTypedEntry('jump-hop'),
|
|
buildEditIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '这份跳一跳作品暂时请从作品架编辑。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
buildTypedEntry('puzzle-clear'),
|
|
buildEditIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '这份拼消消作品暂时请从作品架编辑。',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
buildTypedEntry('wooden-fish'),
|
|
buildEditIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '这份敲木鱼作品暂时请从作品架编辑。',
|
|
});
|
|
|
|
const edutainmentEntry = buildTypedEntry('edutainment');
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
edutainmentEntry,
|
|
buildEditIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'resolve-edutainment-draft',
|
|
entry: edutainmentEntry,
|
|
});
|
|
|
|
const rpgEntry = buildRpgLibraryEntry();
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(
|
|
rpgEntry,
|
|
buildEditIntentDeps({ selectedRpgDetailEntry: rpgEntry }),
|
|
),
|
|
).toEqual({
|
|
type: 'edit-rpg-gallery',
|
|
entry: rpgEntry,
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkEditIntent(rpgEntry, buildEditIntentDeps()),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '作品详情尚未读取完成。',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves start intent for direct launches', () => {
|
|
const bigFishEntry = buildTypedEntry('big-fish');
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(bigFishEntry, buildStartIntentDeps()),
|
|
).toEqual({
|
|
type: 'start-big-fish',
|
|
work: mapPublicWorkDetailToBigFishWork(bigFishEntry),
|
|
returnStage: 'work-detail',
|
|
});
|
|
|
|
const selectedPuzzleDetail = buildPuzzleWork({
|
|
profileId: 'puzzle-profile',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
buildTypedEntry('puzzle'),
|
|
buildStartIntentDeps({ selectedPuzzleDetail }),
|
|
),
|
|
).toEqual({
|
|
type: 'start-puzzle',
|
|
work: selectedPuzzleDetail,
|
|
returnStage: 'work-detail',
|
|
authMode: 'isolated',
|
|
});
|
|
|
|
const puzzleEntry = buildTypedEntry('puzzle', {
|
|
profileId: 'fallback-puzzle-profile',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
puzzleEntry,
|
|
buildStartIntentDeps({
|
|
selectedPuzzleDetail: buildPuzzleWork({ profileId: 'stale-profile' }),
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'start-puzzle',
|
|
work: mapPublicWorkDetailToPuzzleWork(puzzleEntry),
|
|
returnStage: 'work-detail',
|
|
authMode: 'isolated',
|
|
});
|
|
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
buildTypedEntry('jump-hop'),
|
|
buildStartIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-jump-hop',
|
|
profileId: 'jump-hop-profile',
|
|
returnStage: 'work-detail',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
buildTypedEntry('puzzle-clear'),
|
|
buildStartIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-puzzle-clear',
|
|
profileId: 'puzzle-clear-profile',
|
|
returnStage: 'work-detail',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
buildTypedEntry('wooden-fish'),
|
|
buildStartIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-wooden-fish',
|
|
profileId: 'wooden-fish-profile',
|
|
returnStage: 'work-detail',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
buildTypedEntry('visual-novel'),
|
|
buildStartIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-visual-novel',
|
|
profileId: 'visual-novel-profile',
|
|
returnStage: 'work-detail',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
buildTypedEntry('edutainment'),
|
|
buildStartIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-edutainment',
|
|
entry: buildTypedEntry('edutainment'),
|
|
returnStage: 'work-detail',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves start intent for mapper-backed launches', () => {
|
|
const match3DEntry = buildTypedEntry('match3d');
|
|
const match3DWork = buildMatch3DWork({ workId: 'mapped-match3d-work' });
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
match3DEntry,
|
|
buildStartIntentDeps({
|
|
mapMatch3DWork: (entry) =>
|
|
entry === match3DEntry ? match3DWork : null,
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'start-match3d',
|
|
work: match3DWork,
|
|
returnStage: 'work-detail',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
match3DEntry,
|
|
buildStartIntentDeps({ mapMatch3DWork: () => null }),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '当前抓大鹅作品信息不完整,暂时无法进入玩法。',
|
|
});
|
|
|
|
const squareHoleEntry = buildTypedEntry('square-hole');
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
squareHoleEntry,
|
|
buildStartIntentDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-square-hole',
|
|
work: mapPublicWorkDetailToSquareHoleWork(squareHoleEntry),
|
|
returnStage: 'work-detail',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves bark battle start work priority', () => {
|
|
const entry = buildTypedEntry('bark-battle');
|
|
const galleryWork = buildBarkBattleWork({
|
|
workId: 'bark-battle-work',
|
|
title: '作品架缓存',
|
|
});
|
|
const loadedWork = buildBarkBattleWork({
|
|
workId: 'bark-battle-work',
|
|
title: '完整作品列表',
|
|
});
|
|
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
entry,
|
|
buildStartIntentDeps({
|
|
barkBattleGalleryEntries: [galleryWork],
|
|
barkBattleWorks: [loadedWork],
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'start-bark-battle',
|
|
work: galleryWork,
|
|
returnStage: 'work-detail',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
entry,
|
|
buildStartIntentDeps({
|
|
barkBattleWorks: [loadedWork],
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'start-bark-battle',
|
|
work: loadedWork,
|
|
returnStage: 'work-detail',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(entry, buildStartIntentDeps()),
|
|
).toEqual({
|
|
type: 'start-bark-battle',
|
|
work: mapBarkBattlePublicDetailToWorkSummary(entry),
|
|
returnStage: 'work-detail',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves rpg start intent from loaded detail', () => {
|
|
const rpgEntry = buildRpgEntry();
|
|
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(
|
|
rpgEntry,
|
|
buildStartIntentDeps({ selectedRpgDetailEntry: rpgEntry }),
|
|
),
|
|
).toEqual({
|
|
type: 'record-rpg-gallery-play',
|
|
entry: rpgEntry,
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkStartIntent(rpgEntry, buildStartIntentDeps()),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorMessage: '作品详情尚未读取完成。',
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow resolves direct open decision', () => {
|
|
const entry = buildTypedEntry('match3d', {
|
|
publicWorkCode: ' M3D-001 ',
|
|
});
|
|
const buildWorkDetailPath = (publicWorkCode: string) =>
|
|
`/works/detail?work=${publicWorkCode.trim()}`;
|
|
|
|
expect(
|
|
resolvePlatformPublicWorkDetailOpenDecision(entry, {
|
|
buildWorkDetailPath,
|
|
}),
|
|
).toEqual({
|
|
type: 'open',
|
|
selectedDetail: entry,
|
|
errorMessage: null,
|
|
selectionStage: 'work-detail',
|
|
historyPath: '/works/detail?work=M3D-001',
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkDetailOpenDecision(
|
|
buildTypedEntry('match3d', { publicWorkCode: ' ' }),
|
|
{
|
|
buildWorkDetailPath,
|
|
},
|
|
),
|
|
).toEqual({
|
|
type: 'open',
|
|
selectedDetail: buildTypedEntry('match3d', { publicWorkCode: ' ' }),
|
|
errorMessage: null,
|
|
selectionStage: 'work-detail',
|
|
historyPath: null,
|
|
});
|
|
expect(
|
|
resolvePlatformPublicWorkDetailOpenDecision(entry, {
|
|
canExposeEntry: () => false,
|
|
hiddenMessage: '隐藏',
|
|
buildWorkDetailPath,
|
|
}),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
selectedDetail: null,
|
|
errorMessage: '隐藏',
|
|
selectionStage: 'platform',
|
|
historyPath: null,
|
|
});
|
|
});
|
|
|
|
test('platform public work detail flow selects author lookup entry by stage', () => {
|
|
const selectedPublicWorkDetail = buildTypedEntry('puzzle');
|
|
const publishedRpgEntry = buildRpgEntry({
|
|
visibility: 'published',
|
|
profileId: 'published-rpg-profile',
|
|
});
|
|
const draftRpgEntry = buildRpgEntry({
|
|
visibility: 'draft',
|
|
profileId: 'draft-rpg-profile',
|
|
});
|
|
|
|
expect(
|
|
resolveActivePlatformPublicWorkAuthorEntry({
|
|
selectionStage: 'work-detail',
|
|
selectedPublicWorkDetail,
|
|
selectedRpgDetailEntry: publishedRpgEntry,
|
|
}),
|
|
).toBe(selectedPublicWorkDetail);
|
|
expect(
|
|
resolveActivePlatformPublicWorkAuthorEntry({
|
|
selectionStage: 'detail',
|
|
selectedPublicWorkDetail: null,
|
|
selectedRpgDetailEntry: publishedRpgEntry,
|
|
}),
|
|
).toBe(publishedRpgEntry);
|
|
expect(
|
|
resolveActivePlatformPublicWorkAuthorEntry({
|
|
selectionStage: 'detail',
|
|
selectedPublicWorkDetail: null,
|
|
selectedRpgDetailEntry: draftRpgEntry,
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
resolveActivePlatformPublicWorkAuthorEntry({
|
|
selectionStage: 'platform',
|
|
selectedPublicWorkDetail,
|
|
selectedRpgDetailEntry: publishedRpgEntry,
|
|
}),
|
|
).toBeNull();
|
|
});
|