179 lines
4.9 KiB
TypeScript
179 lines
4.9 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
|
|
import type { PlatformPublicGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation';
|
|
import { buildPlatformRecommendedEntries } from './platformRecommendation';
|
|
|
|
const NOW_MS = Date.parse('2026-06-07T12:00:00.000Z');
|
|
|
|
type PublicCardTestParams = {
|
|
id: string;
|
|
sourceType?: 'puzzle' | 'match3d' | 'jump-hop';
|
|
subtitle?: string;
|
|
summaryText?: string;
|
|
coverImageSrc?: string | null;
|
|
themeTags?: string[];
|
|
playCount?: number;
|
|
remixCount?: number;
|
|
likeCount?: number;
|
|
recentPlayCount7d?: number;
|
|
publishedAt?: string | null;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
function buildPublicCard(
|
|
params: PublicCardTestParams,
|
|
): PlatformPublicGalleryCard {
|
|
const sourceType = params.sourceType ?? 'puzzle';
|
|
|
|
return {
|
|
sourceType,
|
|
workId: `${sourceType}-work-${params.id}`,
|
|
profileId: `${sourceType}-profile-${params.id}`,
|
|
publicWorkCode: `${sourceType.toUpperCase()}-${params.id}`,
|
|
ownerUserId: `user-${params.id}`,
|
|
authorDisplayName: `${params.id} 作者`,
|
|
worldName: `${params.id} 作品`,
|
|
subtitle: params.subtitle ?? '公开作品',
|
|
summaryText: params.summaryText ?? '公开作品摘要。',
|
|
coverImageSrc: params.coverImageSrc ?? `${params.id}.png`,
|
|
themeTags: params.themeTags ?? ['推荐'],
|
|
playCount: params.playCount ?? 0,
|
|
remixCount: params.remixCount ?? 0,
|
|
likeCount: params.likeCount ?? 0,
|
|
recentPlayCount7d: params.recentPlayCount7d ?? 0,
|
|
visibility: 'published',
|
|
publishedAt: params.publishedAt ?? '2026-06-01T12:00:00.000Z',
|
|
updatedAt:
|
|
params.updatedAt ?? params.publishedAt ?? '2026-06-01T12:00:00.000Z',
|
|
} satisfies PlatformPublicGalleryCard;
|
|
}
|
|
|
|
describe('buildPlatformRecommendedEntries', () => {
|
|
test('combines heat, freshness and featured boost after de-duplicating works', () => {
|
|
const coldEntry = buildPublicCard({
|
|
id: 'cold',
|
|
playCount: 1,
|
|
publishedAt: '2026-04-01T12:00:00.000Z',
|
|
});
|
|
const hotRecentEntry = buildPublicCard({
|
|
id: 'hot',
|
|
playCount: 8,
|
|
likeCount: 4,
|
|
recentPlayCount7d: 16,
|
|
publishedAt: '2026-06-06T12:00:00.000Z',
|
|
});
|
|
const curatedEntry = buildPublicCard({
|
|
id: 'curated',
|
|
playCount: 0,
|
|
likeCount: 0,
|
|
publishedAt: '2026-05-10T12:00:00.000Z',
|
|
});
|
|
|
|
const entries = buildPlatformRecommendedEntries(
|
|
{
|
|
featuredEntries: [curatedEntry],
|
|
latestEntries: [coldEntry, hotRecentEntry, curatedEntry],
|
|
},
|
|
{ nowMs: NOW_MS },
|
|
);
|
|
|
|
expect(entries.map((entry) => entry.profileId)).toEqual([
|
|
hotRecentEntry.profileId,
|
|
curatedEntry.profileId,
|
|
coldEntry.profileId,
|
|
]);
|
|
});
|
|
|
|
test('interleaves close-score works from different play types', () => {
|
|
const firstPuzzle = buildPublicCard({
|
|
id: 'puzzle-a',
|
|
sourceType: 'puzzle',
|
|
likeCount: 2,
|
|
});
|
|
const secondPuzzle = buildPublicCard({
|
|
id: 'puzzle-b',
|
|
sourceType: 'puzzle',
|
|
likeCount: 2,
|
|
});
|
|
const match3d = buildPublicCard({
|
|
id: 'match3d-a',
|
|
sourceType: 'match3d',
|
|
likeCount: 2,
|
|
});
|
|
|
|
const entries = buildPlatformRecommendedEntries(
|
|
{
|
|
featuredEntries: [],
|
|
latestEntries: [firstPuzzle, secondPuzzle, match3d],
|
|
},
|
|
{ nowMs: NOW_MS },
|
|
);
|
|
|
|
expect(entries.map((entry) => entry.profileId)).toEqual([
|
|
firstPuzzle.profileId,
|
|
match3d.profileId,
|
|
secondPuzzle.profileId,
|
|
]);
|
|
});
|
|
|
|
test('separates same-type candidates while alternatives remain', () => {
|
|
const hotPuzzle = buildPublicCard({
|
|
id: 'hot-puzzle',
|
|
sourceType: 'puzzle',
|
|
recentPlayCount7d: 50,
|
|
likeCount: 20,
|
|
});
|
|
const warmPuzzle = buildPublicCard({
|
|
id: 'warm-puzzle',
|
|
sourceType: 'puzzle',
|
|
recentPlayCount7d: 32,
|
|
likeCount: 12,
|
|
});
|
|
const coldMatch3d = buildPublicCard({
|
|
id: 'cold-match3d',
|
|
sourceType: 'match3d',
|
|
publishedAt: '2026-04-01T12:00:00.000Z',
|
|
});
|
|
|
|
const entries = buildPlatformRecommendedEntries(
|
|
{
|
|
featuredEntries: [],
|
|
latestEntries: [hotPuzzle, warmPuzzle, coldMatch3d],
|
|
},
|
|
{ nowMs: NOW_MS },
|
|
);
|
|
|
|
expect(entries.map((entry) => entry.profileId)).toEqual([
|
|
hotPuzzle.profileId,
|
|
coldMatch3d.profileId,
|
|
warmPuzzle.profileId,
|
|
]);
|
|
});
|
|
|
|
test('falls back to same-type adjacency when no other type remains', () => {
|
|
const firstPuzzle = buildPublicCard({
|
|
id: 'only-puzzle-a',
|
|
sourceType: 'puzzle',
|
|
recentPlayCount7d: 8,
|
|
});
|
|
const secondPuzzle = buildPublicCard({
|
|
id: 'only-puzzle-b',
|
|
sourceType: 'puzzle',
|
|
recentPlayCount7d: 4,
|
|
});
|
|
|
|
const entries = buildPlatformRecommendedEntries(
|
|
{
|
|
featuredEntries: [],
|
|
latestEntries: [firstPuzzle, secondPuzzle],
|
|
},
|
|
{ nowMs: NOW_MS },
|
|
);
|
|
|
|
expect(entries.map((entry) => entry.profileId)).toEqual([
|
|
firstPuzzle.profileId,
|
|
secondPuzzle.profileId,
|
|
]);
|
|
});
|
|
});
|