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, 'sourceType' > >; function narrowTypedEntry( entry: TypedPlatformPublicGalleryCard, ): Extract { return entry as Extract< TypedPlatformPublicGalleryCard, { sourceType: TSourceType } >; } function buildRpgEntry( overrides: Partial = {}, ): 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 { return { ...buildRpgEntry(overrides), profile: { id: overrides.profileId ?? 'rpg-profile', } as unknown as CustomWorldProfile, ...overrides, }; } function buildTypedEntry( sourceType: TSourceType, overrides: TypedPlatformPublicGalleryCardOverrides = {}, ): Extract { 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({ ...common, ...overrides, sourceType, }); case 'puzzle-clear': return narrowTypedEntry({ ...common, ...overrides, sourceType, themePrompt: '拼消消主题', }); case 'big-fish': return narrowTypedEntry({ ...common, ...overrides, sourceType, }); case 'match3d': return narrowTypedEntry({ ...common, ...overrides, sourceType, }); case 'square-hole': return narrowTypedEntry({ ...common, ...overrides, sourceType, }); case 'visual-novel': return narrowTypedEntry({ ...common, ...overrides, sourceType, }); case 'jump-hop': return narrowTypedEntry({ ...common, ...overrides, sourceType, }); case 'wooden-fish': return narrowTypedEntry({ ...common, ...overrides, sourceType, }); case 'edutainment': return narrowTypedEntry({ ...common, sourceType, templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID, templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME, ...overrides, }); case 'bark-battle': return narrowTypedEntry({ ...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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { return { selectedPuzzleDetail: null, selectedRpgDetailEntry: null, barkBattleGalleryEntries: [], barkBattleWorks: [], mapMatch3DWork: () => buildMatch3DWork(), ...overrides, }; } function buildEditIntentDeps( overrides: Partial = {}, ): 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(); });