48c7cce1ba
合入 master 最新的认证、玩法契约与推荐页改动。 修复拼图草稿生成、推荐页下一关和公开详情访客试玩回归。 修复抓大鹅草稿试玩鉴权与首屏推荐详情测试入口。 补齐相关测试夹具、文档与团队记忆更新。
912 lines
26 KiB
TypeScript
912 lines
26 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 { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
|
import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
|
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
|
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
|
import {
|
|
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
|
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
|
type PlatformPublicGalleryCard,
|
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
|
import {
|
|
buildPlatformPublicGalleryFeeds,
|
|
getPlatformPublicGalleryEntryKey,
|
|
getPlatformPublicGalleryEntryTime,
|
|
getPlatformRecommendRuntimeKind,
|
|
isPlatformRecommendRuntimeReadyForEntry,
|
|
isSamePlatformPublicGalleryEntry,
|
|
mergePlatformPublicGalleryEntries,
|
|
type PlatformRecommendRuntimeStartIntentDeps,
|
|
type RecommendRuntimeKind,
|
|
resolvePlatformRecommendRuntimeAutoStartDecision,
|
|
resolvePlatformRecommendRuntimeStartIntent,
|
|
} from './platformPublicGalleryFlow';
|
|
import {
|
|
mapBarkBattlePublicDetailToWorkSummary,
|
|
mapPublicWorkDetailToBigFishWork,
|
|
mapPublicWorkDetailToPuzzleWork,
|
|
mapPublicWorkDetailToSquareHoleWork,
|
|
} from './platformPublicWorkDetailFlow';
|
|
|
|
type TypedPlatformPublicGalleryCard = Extract<
|
|
PlatformPublicGalleryCard,
|
|
{ sourceType: string }
|
|
>;
|
|
type PlatformGallerySourceType = TypedPlatformPublicGalleryCard['sourceType'];
|
|
type TypedPlatformPublicGalleryCardOverrides = Partial<
|
|
Omit<TypedPlatformPublicGalleryCard, 'sourceType'>
|
|
>;
|
|
|
|
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 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-01T02:00:00.000Z',
|
|
publishedAt: '2026-06-01T02:00:00.000Z',
|
|
publishReady: true,
|
|
levelCount: 1,
|
|
levelMainImageReadyCount: 1,
|
|
levelMotionReadyCount: 1,
|
|
backgroundReady: true,
|
|
playCount: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildBabyObjectMatchDraft(
|
|
overrides: Partial<BabyObjectMatchDraft> = {},
|
|
): BabyObjectMatchDraft {
|
|
const itemAsset = {
|
|
itemId: 'item-a',
|
|
itemName: '苹果',
|
|
imageSrc: '/apple.png',
|
|
assetObjectId: null,
|
|
generationProvider: 'placeholder' as const,
|
|
prompt: '苹果',
|
|
};
|
|
return {
|
|
draftId: 'baby-draft',
|
|
profileId: 'baby-profile',
|
|
templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
|
templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
|
workTitle: '宝贝识物',
|
|
workDescription: '认识水果。',
|
|
itemNames: ['苹果', '香蕉'],
|
|
itemAssets: [itemAsset, { ...itemAsset, itemId: 'item-b', itemName: '香蕉' }],
|
|
visualPackage: null,
|
|
themeTags: ['寓教于乐'],
|
|
publicationStatus: 'published',
|
|
createdAt: '2026-06-01T00:00:00.000Z',
|
|
updatedAt: '2026-06-01T03:00:00.000Z',
|
|
publishedAt: '2026-06-01T03:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildJumpHopEntry(
|
|
overrides: Partial<JumpHopGalleryCardResponse> = {},
|
|
): JumpHopGalleryCardResponse {
|
|
return {
|
|
publicWorkCode: 'JH-JUMP',
|
|
workId: 'jump-hop-work',
|
|
profileId: 'jump-hop-profile',
|
|
ownerUserId: 'user-1',
|
|
authorDisplayName: '玩家',
|
|
themeText: '一路向前',
|
|
workTitle: '跳一跳',
|
|
workDescription: '一路向前。',
|
|
coverImageSrc: '/jump-hop-cover.png',
|
|
themeTags: ['跳一跳'],
|
|
difficulty: 'standard',
|
|
stylePreset: 'minimal-blocks',
|
|
publicationStatus: 'published',
|
|
playCount: 1,
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
generationStatus: 'ready',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildTypedEntry(
|
|
sourceType: PlatformGallerySourceType,
|
|
overrides: TypedPlatformPublicGalleryCardOverrides = {},
|
|
): PlatformPublicGalleryCard {
|
|
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 { ...common, ...overrides, sourceType };
|
|
case 'puzzle-clear':
|
|
return {
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
themePrompt: '拼消消主题',
|
|
};
|
|
case 'big-fish':
|
|
return { ...common, ...overrides, sourceType };
|
|
case 'match3d':
|
|
return { ...common, ...overrides, sourceType };
|
|
case 'square-hole':
|
|
return { ...common, ...overrides, sourceType };
|
|
case 'visual-novel':
|
|
return { ...common, ...overrides, sourceType };
|
|
case 'jump-hop':
|
|
return { ...common, ...overrides, sourceType };
|
|
case 'wooden-fish':
|
|
return { ...common, ...overrides, sourceType };
|
|
case 'edutainment':
|
|
return {
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
|
templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
|
};
|
|
case 'bark-battle':
|
|
return {
|
|
...common,
|
|
...overrides,
|
|
sourceType,
|
|
authorPublicUserCode: null,
|
|
coverRenderMode: 'image',
|
|
coverCharacterImageSrcs: [],
|
|
themeMode: 'martial',
|
|
playableNpcCount: 1,
|
|
landmarkCount: 1,
|
|
};
|
|
default: {
|
|
const exhaustive: never = sourceType;
|
|
return exhaustive;
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildPuzzleWork(
|
|
overrides: Partial<PuzzleWorkSummary> = {},
|
|
): PuzzleWorkSummary {
|
|
return {
|
|
workId: 'puzzle-work',
|
|
profileId: 'puzzle-profile',
|
|
ownerUserId: 'user-1',
|
|
sourceSessionId: 'puzzle-session',
|
|
authorDisplayName: '玩家',
|
|
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,
|
|
pointIncentiveTotalHalfPoints: 0,
|
|
pointIncentiveClaimedPoints: 0,
|
|
pointIncentiveTotalPoints: 0,
|
|
pointIncentiveClaimablePoints: 0,
|
|
publishReady: true,
|
|
...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 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 buildRecommendRuntimeStartDeps(
|
|
overrides: Partial<PlatformRecommendRuntimeStartIntentDeps> = {},
|
|
): PlatformRecommendRuntimeStartIntentDeps {
|
|
return {
|
|
selectedPuzzleDetail: null,
|
|
barkBattleGalleryEntries: [],
|
|
mapMatch3DWork: () => buildMatch3DWork(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('platform public gallery flow resolves stable key and runtime kind for every play kind', () => {
|
|
const cases: Array<
|
|
[sourceType: PlatformGallerySourceType, keyKind: string, kind: RecommendRuntimeKind]
|
|
> = [
|
|
['big-fish', 'big-fish', 'big-fish'],
|
|
['puzzle', 'puzzle', 'puzzle'],
|
|
['jump-hop', 'jump-hop', 'jump-hop'],
|
|
['wooden-fish', 'wooden-fish', 'wooden-fish'],
|
|
['match3d', 'match3d', 'match3d'],
|
|
['square-hole', 'square-hole', 'square-hole'],
|
|
['visual-novel', 'visual-novel', 'visual-novel'],
|
|
['bark-battle', 'bark-battle', 'bark-battle'],
|
|
[
|
|
'edutainment',
|
|
`edutainment:${EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID}`,
|
|
'edutainment',
|
|
],
|
|
];
|
|
|
|
cases.forEach(([sourceType, keyKind, kind]) => {
|
|
const entry = buildTypedEntry(sourceType);
|
|
|
|
expect(getPlatformPublicGalleryEntryKey(entry)).toBe(
|
|
`${keyKind}:user-1:${sourceType}-profile`,
|
|
);
|
|
expect(getPlatformRecommendRuntimeKind(entry)).toBe(kind);
|
|
});
|
|
|
|
const rpgEntry = buildRpgEntry();
|
|
|
|
expect(getPlatformPublicGalleryEntryKey(rpgEntry)).toBe(
|
|
'rpg:user-1:rpg-profile',
|
|
);
|
|
expect(getPlatformRecommendRuntimeKind(rpgEntry)).toBe('rpg');
|
|
});
|
|
|
|
test('platform public gallery flow compares entries by resolved identity', () => {
|
|
const left = buildTypedEntry('puzzle');
|
|
const sameIdentity = buildTypedEntry('puzzle', {
|
|
workId: 'other-work',
|
|
worldName: '新标题',
|
|
});
|
|
const otherKind = buildTypedEntry('match3d', {
|
|
ownerUserId: left.ownerUserId,
|
|
profileId: left.profileId,
|
|
});
|
|
|
|
expect(isSamePlatformPublicGalleryEntry(left, sameIdentity)).toBe(true);
|
|
expect(isSamePlatformPublicGalleryEntry(left, otherKind)).toBe(false);
|
|
});
|
|
|
|
test('platform public gallery flow resolves recommend runtime start intent', () => {
|
|
const bigFishEntry = buildTypedEntry('big-fish');
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
bigFishEntry,
|
|
buildRecommendRuntimeStartDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-big-fish',
|
|
work: mapPublicWorkDetailToBigFishWork(bigFishEntry),
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
|
|
const selectedPuzzleDetail = buildPuzzleWork({
|
|
profileId: 'puzzle-profile',
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
buildTypedEntry('puzzle'),
|
|
buildRecommendRuntimeStartDeps({ selectedPuzzleDetail }),
|
|
),
|
|
).toEqual({
|
|
type: 'start-puzzle',
|
|
work: selectedPuzzleDetail,
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
|
|
const puzzleEntry = buildTypedEntry('puzzle', {
|
|
profileId: 'fallback-puzzle-profile',
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
puzzleEntry,
|
|
buildRecommendRuntimeStartDeps({
|
|
selectedPuzzleDetail: buildPuzzleWork({ profileId: 'stale-profile' }),
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'start-puzzle',
|
|
work: mapPublicWorkDetailToPuzzleWork(puzzleEntry),
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
buildTypedEntry('jump-hop'),
|
|
buildRecommendRuntimeStartDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-jump-hop',
|
|
profileId: 'jump-hop-profile',
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
buildTypedEntry('wooden-fish'),
|
|
buildRecommendRuntimeStartDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-wooden-fish',
|
|
profileId: 'wooden-fish-profile',
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
buildTypedEntry('visual-novel'),
|
|
buildRecommendRuntimeStartDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-visual-novel',
|
|
profileId: 'visual-novel-profile',
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
buildTypedEntry('edutainment'),
|
|
buildRecommendRuntimeStartDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-edutainment',
|
|
entry: buildTypedEntry('edutainment'),
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
buildRpgEntry(),
|
|
buildRecommendRuntimeStartDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'mark-ready',
|
|
});
|
|
});
|
|
|
|
test('platform public gallery flow resolves recommend runtime mapper-backed start intent', () => {
|
|
const match3DEntry = buildTypedEntry('match3d');
|
|
const match3DWork = buildMatch3DWork({ workId: 'mapped-match3d-work' });
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
match3DEntry,
|
|
buildRecommendRuntimeStartDeps({
|
|
mapMatch3DWork: (entry) =>
|
|
entry === match3DEntry ? match3DWork : null,
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'start-match3d',
|
|
work: match3DWork,
|
|
returnStage: 'work-detail',
|
|
embedded: true,
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
match3DEntry,
|
|
buildRecommendRuntimeStartDeps({ mapMatch3DWork: () => null }),
|
|
),
|
|
).toEqual({
|
|
type: 'blocked',
|
|
errorTarget: 'match3d',
|
|
errorMessage: '当前抓大鹅作品信息不完整,暂时无法进入玩法。',
|
|
});
|
|
|
|
const squareHoleEntry = buildTypedEntry('square-hole');
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
squareHoleEntry,
|
|
buildRecommendRuntimeStartDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-square-hole',
|
|
work: mapPublicWorkDetailToSquareHoleWork(squareHoleEntry),
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
});
|
|
|
|
test('platform public gallery flow resolves recommend runtime bark battle priority', () => {
|
|
const entry = buildTypedEntry('bark-battle');
|
|
const galleryWork = buildBarkBattleWork({
|
|
workId: 'bark-battle-work',
|
|
title: '推荐缓存',
|
|
});
|
|
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
entry,
|
|
buildRecommendRuntimeStartDeps({
|
|
barkBattleGalleryEntries: [galleryWork],
|
|
}),
|
|
),
|
|
).toEqual({
|
|
type: 'start-bark-battle',
|
|
work: galleryWork,
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeStartIntent(
|
|
entry,
|
|
buildRecommendRuntimeStartDeps(),
|
|
),
|
|
).toEqual({
|
|
type: 'start-bark-battle',
|
|
work: mapBarkBattlePublicDetailToWorkSummary(entry),
|
|
returnStage: 'platform',
|
|
embedded: true,
|
|
});
|
|
});
|
|
|
|
test('platform public gallery flow resolves recommend runtime readiness', () => {
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('big-fish'), {
|
|
activeKind: 'puzzle',
|
|
hasBigFishRun: true,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('big-fish'), {
|
|
activeKind: 'big-fish',
|
|
hasBigFishRun: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('jump-hop'), {
|
|
activeKind: 'jump-hop',
|
|
hasJumpHopRun: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('wooden-fish'), {
|
|
activeKind: 'wooden-fish',
|
|
hasWoodenFishRun: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('match3d'), {
|
|
activeKind: 'match3d',
|
|
hasMatch3DRun: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('square-hole'), {
|
|
activeKind: 'square-hole',
|
|
hasSquareHoleRun: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('visual-novel'), {
|
|
activeKind: 'visual-novel',
|
|
hasVisualNovelRun: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('bark-battle'), {
|
|
activeKind: 'bark-battle',
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildRpgEntry(), {
|
|
activeKind: 'rpg',
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
test('platform public gallery flow resolves puzzle and edutainment readiness details', () => {
|
|
const puzzleEntry = buildTypedEntry('puzzle', {
|
|
profileId: 'puzzle-profile',
|
|
});
|
|
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(puzzleEntry, {
|
|
activeKind: 'puzzle',
|
|
puzzleRunEntryProfileId: 'other-profile',
|
|
puzzleRunCurrentLevelProfileId: 'puzzle-profile',
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(puzzleEntry, {
|
|
activeKind: 'puzzle',
|
|
puzzleRunEntryProfileId: 'other-profile',
|
|
puzzleRunCurrentLevelProfileId: 'another-profile',
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('edutainment'), {
|
|
activeKind: 'edutainment',
|
|
hasBabyObjectMatchDraft: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isPlatformRecommendRuntimeReadyForEntry(buildTypedEntry('edutainment'), {
|
|
activeKind: 'edutainment',
|
|
hasBabyObjectMatchDraft: false,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
test('platform public gallery flow resolves recommend runtime auto-start gates', () => {
|
|
const entry = buildTypedEntry('big-fish');
|
|
const baseInput: Parameters<
|
|
typeof resolvePlatformRecommendRuntimeAutoStartDecision
|
|
>[0] = {
|
|
isDesktopLayout: false,
|
|
selectionStage: 'platform',
|
|
platformTab: 'home',
|
|
isLoadingPlatform: false,
|
|
entries: [entry],
|
|
activeEntryKey: null,
|
|
isStarting: false,
|
|
readyState: { activeKind: null },
|
|
};
|
|
|
|
expect(
|
|
resolvePlatformRecommendRuntimeAutoStartDecision({
|
|
...baseInput,
|
|
isDesktopLayout: true,
|
|
}),
|
|
).toEqual({ type: 'noop' });
|
|
expect(
|
|
resolvePlatformRecommendRuntimeAutoStartDecision({
|
|
...baseInput,
|
|
platformTab: 'discover',
|
|
}),
|
|
).toEqual({ type: 'noop' });
|
|
expect(
|
|
resolvePlatformRecommendRuntimeAutoStartDecision({
|
|
...baseInput,
|
|
entries: [],
|
|
}),
|
|
).toEqual({ type: 'clear' });
|
|
expect(
|
|
resolvePlatformRecommendRuntimeAutoStartDecision({
|
|
...baseInput,
|
|
isStarting: true,
|
|
}),
|
|
).toEqual({ type: 'noop' });
|
|
expect(
|
|
resolvePlatformRecommendRuntimeAutoStartDecision({
|
|
...baseInput,
|
|
activeEntryKey: getPlatformPublicGalleryEntryKey(entry),
|
|
hasStartError: true,
|
|
}),
|
|
).toEqual({ type: 'noop' });
|
|
});
|
|
|
|
test('platform public gallery flow resolves recommend runtime auto-start target', () => {
|
|
const firstEntry = buildTypedEntry('big-fish', {
|
|
profileId: 'big-fish-first',
|
|
});
|
|
const activeEntry = buildTypedEntry('puzzle', {
|
|
profileId: 'puzzle-active',
|
|
});
|
|
const activeEntryKey = getPlatformPublicGalleryEntryKey(activeEntry);
|
|
const baseInput: Parameters<
|
|
typeof resolvePlatformRecommendRuntimeAutoStartDecision
|
|
>[0] = {
|
|
isDesktopLayout: false,
|
|
selectionStage: 'platform',
|
|
platformTab: 'home',
|
|
isLoadingPlatform: false,
|
|
entries: [firstEntry, activeEntry],
|
|
activeEntryKey,
|
|
isStarting: false,
|
|
readyState: { activeKind: 'puzzle' },
|
|
};
|
|
|
|
expect(
|
|
resolvePlatformRecommendRuntimeAutoStartDecision({
|
|
...baseInput,
|
|
readyState: {
|
|
activeKind: 'puzzle',
|
|
puzzleRunEntryProfileId: 'puzzle-active',
|
|
},
|
|
}),
|
|
).toEqual({ type: 'noop' });
|
|
expect(resolvePlatformRecommendRuntimeAutoStartDecision(baseInput)).toEqual({
|
|
type: 'start',
|
|
entry: activeEntry,
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeAutoStartDecision({
|
|
...baseInput,
|
|
readyState: { activeKind: null },
|
|
}),
|
|
).toEqual({
|
|
type: 'start',
|
|
entry: activeEntry,
|
|
});
|
|
expect(
|
|
resolvePlatformRecommendRuntimeAutoStartDecision({
|
|
...baseInput,
|
|
activeEntryKey: 'missing-entry',
|
|
}),
|
|
).toEqual({
|
|
type: 'start',
|
|
entry: firstEntry,
|
|
});
|
|
});
|
|
|
|
test('platform public gallery flow merges duplicate identities and sorts newest first', () => {
|
|
const staleRpgEntry = buildRpgEntry({
|
|
profileId: 'shared-rpg',
|
|
worldName: '旧版 RPG',
|
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
|
});
|
|
const freshRpgEntry = buildRpgEntry({
|
|
profileId: 'shared-rpg',
|
|
worldName: '新版 RPG',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
});
|
|
const middleRpgEntry = buildRpgEntry({
|
|
profileId: 'middle-rpg',
|
|
worldName: '中间 RPG',
|
|
publishedAt: '2026-06-02T00:00:00.000Z',
|
|
});
|
|
const updatedOnlyEntry = buildTypedEntry('big-fish', {
|
|
profileId: 'updated-only',
|
|
publishedAt: null,
|
|
updatedAt: '2026-06-03T00:00:00.000Z',
|
|
});
|
|
const invalidTimeEntry = buildTypedEntry('puzzle', {
|
|
profileId: 'invalid-time',
|
|
publishedAt: 'not-a-date',
|
|
updatedAt: 'still-not-a-date',
|
|
});
|
|
|
|
const merged = mergePlatformPublicGalleryEntries(
|
|
[staleRpgEntry, middleRpgEntry],
|
|
[invalidTimeEntry, updatedOnlyEntry, freshRpgEntry],
|
|
);
|
|
|
|
expect(merged).toHaveLength(4);
|
|
expect(merged.map((entry) => entry.profileId)).toEqual([
|
|
'shared-rpg',
|
|
'updated-only',
|
|
'middle-rpg',
|
|
'invalid-time',
|
|
]);
|
|
expect(merged[0]?.worldName).toBe('新版 RPG');
|
|
expect(getPlatformPublicGalleryEntryTime(invalidTimeEntry)).toBe(0);
|
|
});
|
|
|
|
test('platform public gallery flow builds feeds with visibility gates and bark battle fallback', () => {
|
|
const hiddenBigFish = buildBigFishWork({
|
|
workId: 'hidden-big-fish',
|
|
sourceSessionId: 'hidden-big-fish-session',
|
|
});
|
|
const hiddenBabyDraft = buildBabyObjectMatchDraft({
|
|
profileId: 'hidden-baby',
|
|
});
|
|
const publishedBarkFallback = buildBarkBattleWork({
|
|
workId: 'fallback-bark',
|
|
publishedAt: '2026-06-04T00:00:00.000Z',
|
|
updatedAt: '2026-06-04T00:00:00.000Z',
|
|
});
|
|
const draftBarkFallback = buildBarkBattleWork({
|
|
workId: 'draft-bark',
|
|
status: 'draft',
|
|
});
|
|
|
|
const hiddenFeeds = buildPlatformPublicGalleryFeeds({
|
|
rpgEntries: [
|
|
buildRpgEntry({
|
|
profileId: 'rpg-visible',
|
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
|
}),
|
|
],
|
|
bigFishEntries: [hiddenBigFish],
|
|
match3dEntries: [],
|
|
puzzleEntries: [],
|
|
puzzleClearEntries: [],
|
|
barkBattleGalleryEntries: [],
|
|
barkBattleWorks: [draftBarkFallback, publishedBarkFallback],
|
|
jumpHopEntries: [],
|
|
woodenFishEntries: [],
|
|
squareHoleEntries: [],
|
|
visualNovelEntries: [],
|
|
babyObjectMatchDrafts: [hiddenBabyDraft],
|
|
isBigFishCreationVisible: false,
|
|
isBabyObjectMatchVisible: false,
|
|
isVisualNovelCreationOpen: false,
|
|
});
|
|
|
|
expect(
|
|
hiddenFeeds.latestEntries.map((entry) =>
|
|
'sourceType' in entry ? entry.sourceType : 'rpg',
|
|
),
|
|
).toEqual(['bark-battle', 'rpg']);
|
|
expect(hiddenFeeds.latestEntries[0]?.profileId).toBe('fallback-bark');
|
|
|
|
const visibleFeeds = buildPlatformPublicGalleryFeeds({
|
|
rpgEntries: [],
|
|
bigFishEntries: [hiddenBigFish],
|
|
match3dEntries: [],
|
|
puzzleEntries: [],
|
|
puzzleClearEntries: [],
|
|
barkBattleGalleryEntries: [
|
|
buildBarkBattleWork({
|
|
workId: 'gallery-bark',
|
|
publishedAt: '2026-06-05T00:00:00.000Z',
|
|
updatedAt: '2026-06-05T00:00:00.000Z',
|
|
}),
|
|
],
|
|
barkBattleWorks: [publishedBarkFallback],
|
|
jumpHopEntries: [],
|
|
woodenFishEntries: [],
|
|
squareHoleEntries: [],
|
|
visualNovelEntries: [],
|
|
babyObjectMatchDrafts: [hiddenBabyDraft],
|
|
isBigFishCreationVisible: true,
|
|
isBabyObjectMatchVisible: true,
|
|
isVisualNovelCreationOpen: false,
|
|
});
|
|
|
|
expect(visibleFeeds.latestEntries.map((entry) => entry.profileId)).toEqual([
|
|
'gallery-bark',
|
|
'hidden-baby',
|
|
'hidden-big-fish-session',
|
|
]);
|
|
expect(visibleFeeds.featuredEntries).toEqual(
|
|
visibleFeeds.latestEntries.slice(0, 6),
|
|
);
|
|
});
|
|
|
|
test('platform public gallery flow preserves feed tie order and featured slice', () => {
|
|
const sameTime = '2026-06-04T00:00:00.000Z';
|
|
const tieFeeds = buildPlatformPublicGalleryFeeds({
|
|
rpgEntries: [],
|
|
bigFishEntries: [],
|
|
match3dEntries: [],
|
|
puzzleEntries: [],
|
|
puzzleClearEntries: [],
|
|
barkBattleGalleryEntries: [],
|
|
barkBattleWorks: [
|
|
buildBarkBattleWork({
|
|
workId: 'fallback-bark',
|
|
publishedAt: sameTime,
|
|
updatedAt: sameTime,
|
|
}),
|
|
],
|
|
jumpHopEntries: [
|
|
buildJumpHopEntry({
|
|
profileId: 'jump-hop',
|
|
publishedAt: sameTime,
|
|
updatedAt: sameTime,
|
|
}),
|
|
],
|
|
woodenFishEntries: [],
|
|
squareHoleEntries: [],
|
|
visualNovelEntries: [],
|
|
babyObjectMatchDrafts: [],
|
|
isBigFishCreationVisible: false,
|
|
isBabyObjectMatchVisible: false,
|
|
isVisualNovelCreationOpen: false,
|
|
});
|
|
|
|
expect(tieFeeds.latestEntries.map((entry) => entry.profileId)).toEqual([
|
|
'jump-hop',
|
|
'fallback-bark',
|
|
]);
|
|
|
|
const sliceFeeds = buildPlatformPublicGalleryFeeds({
|
|
rpgEntries: Array.from({ length: 7 }, (_, index) =>
|
|
buildRpgEntry({
|
|
profileId: `rpg-${index}`,
|
|
publishedAt: `2026-06-0${index + 1}T00:00:00.000Z`,
|
|
updatedAt: `2026-06-0${index + 1}T00:00:00.000Z`,
|
|
}),
|
|
),
|
|
bigFishEntries: [],
|
|
match3dEntries: [],
|
|
puzzleEntries: [],
|
|
puzzleClearEntries: [],
|
|
barkBattleGalleryEntries: [],
|
|
barkBattleWorks: [],
|
|
jumpHopEntries: [],
|
|
woodenFishEntries: [],
|
|
squareHoleEntries: [],
|
|
visualNovelEntries: [],
|
|
babyObjectMatchDrafts: [],
|
|
isBigFishCreationVisible: false,
|
|
isBabyObjectMatchVisible: false,
|
|
isVisualNovelCreationOpen: false,
|
|
});
|
|
expect(sliceFeeds.featuredEntries).toHaveLength(6);
|
|
});
|