227 lines
7.4 KiB
TypeScript
227 lines
7.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
buildFacets,
|
||
applyBannerManifest,
|
||
applyGeneratedFileLinks,
|
||
normalizeCatalog,
|
||
parseReleaseDate,
|
||
slugifyStem,
|
||
sortHacks,
|
||
} from './catalog';
|
||
import type { RawCatalog } from './types';
|
||
|
||
const baseCatalog: RawCatalog = {
|
||
generated: '2026-06-08',
|
||
count: 3,
|
||
image_base: 'https://romhacks-files.ginnoir.com',
|
||
facets: {
|
||
platform: { GBA: 2, Unknown: 1 },
|
||
base: { FireRed: 1, Unknown: 2 },
|
||
status: { Complete: 2, Unknown: 1 },
|
||
type: { Difficulty: 1, Expansion: 1 },
|
||
play_status: { Unplayed: 3 },
|
||
},
|
||
hacks: [
|
||
{
|
||
stem: 'Radical Red',
|
||
title: 'Pokémon Radical Red',
|
||
platform: 'GBA',
|
||
base: 'FireRed',
|
||
status: 'Complete',
|
||
version: '4.1',
|
||
generations: ['Gen I', 'Gen II', 'Gen III', 'Gen IV', 'Gen V', 'Gen VI', 'Gen VII', 'Gen VIII', 'Gen IX'],
|
||
fakemon: 'No',
|
||
developer: 'Soupercell',
|
||
release_date: '2024',
|
||
banner: 'https://romhacks-files.ginnoir.com/_meta/radical/banner.jpg',
|
||
source: 'https://example.com/radical',
|
||
library_path: 'roms/gba/Hacks/Pokemon - Radical Red (Hack).gba',
|
||
play_status: 'Unplayed',
|
||
rating: 5,
|
||
type: ['Difficulty', 'Expansion'],
|
||
tags: ['hack'],
|
||
summary: 'Fast, difficult FireRed expansion.',
|
||
notability: 'Flagship difficulty hack.',
|
||
story: '',
|
||
features: ['Expanded Pokédex'],
|
||
links: [{ label: 'Source / info', url: 'https://example.com/radical' }],
|
||
},
|
||
{
|
||
stem: 'A FarfetchΓÇÖD Story',
|
||
title: 'Pokémon A Farfetch’D Story',
|
||
platform: 'PC',
|
||
base: null,
|
||
status: 'Complete',
|
||
version: 'completed',
|
||
developer: 'KennyCatches',
|
||
release_date: '2023-08-10',
|
||
scrape_dir: 'Pokemon_A_Farfetch_D_Story',
|
||
library_path: '/storage1/labdata/romhacks/library/Pokemon_A_Farfetch_D_Story/',
|
||
play_status: 'Unplayed',
|
||
type: ['New Experience'],
|
||
tags: [],
|
||
summary: 'A short RPGXP adventure.',
|
||
notability: '',
|
||
story: '',
|
||
features: [],
|
||
links: [],
|
||
},
|
||
{
|
||
stem: 'Unknown Base',
|
||
title: 'Pokémon Unknown Base',
|
||
platform: null,
|
||
base: '—',
|
||
status: null,
|
||
version: '',
|
||
library_path: '—',
|
||
play_status: 'Unplayed',
|
||
type: [],
|
||
tags: [],
|
||
summary: '',
|
||
notability: '',
|
||
story: '',
|
||
features: [],
|
||
links: [],
|
||
},
|
||
],
|
||
};
|
||
|
||
describe('catalog normalization', () => {
|
||
it('creates deterministic URL-safe slugs from stems', () => {
|
||
expect(slugifyStem('Radical Red')).toBe('radical-red');
|
||
expect(slugifyStem('A FarfetchΓÇÖD Story')).toBe('a-farfetch-d-story');
|
||
expect(slugifyStem('Pokémon: Crystal Clear!')).toBe('pokemon-crystal-clear');
|
||
});
|
||
|
||
it('normalizes records without rendering null-like values', () => {
|
||
const catalog = normalizeCatalog(baseCatalog);
|
||
const unknown = catalog.hacks.find((hack) => hack.stem === 'Unknown Base');
|
||
|
||
expect(catalog.count).toBe(3);
|
||
expect(catalog.facets.platform).toEqual(baseCatalog.facets.platform);
|
||
expect(unknown?.platform).toBeUndefined();
|
||
expect(unknown?.base).toBeUndefined();
|
||
expect(unknown?.status).toBeUndefined();
|
||
expect(unknown?.chips.map((chip) => chip.label)).not.toContain('—');
|
||
expect(unknown?.searchText).not.toMatch(/\bnull\b|—/i);
|
||
});
|
||
|
||
it('normalizes roster generations and Fakemon metadata into chips', () => {
|
||
const catalog = normalizeCatalog(baseCatalog);
|
||
const radical = catalog.hacks.find((hack) => hack.slug === 'radical-red');
|
||
|
||
expect(radical?.generations).toEqual([
|
||
'Gen I',
|
||
'Gen II',
|
||
'Gen III',
|
||
'Gen IV',
|
||
'Gen V',
|
||
'Gen VI',
|
||
'Gen VII',
|
||
'Gen VIII',
|
||
'Gen IX',
|
||
]);
|
||
expect(radical?.fakemon).toBe('No');
|
||
expect(radical?.chips).toEqual(
|
||
expect.arrayContaining([
|
||
{ label: 'Roster', value: 'Gen I, Gen II, Gen III, Gen IV, Gen V, Gen VI, Gen VII, Gen VIII, Gen IX', kind: 'generation' },
|
||
{ label: 'Fakemon', value: 'No', kind: 'fakemon' },
|
||
]),
|
||
);
|
||
expect(radical?.searchText).toContain('gen ix');
|
||
});
|
||
|
||
it('maps ROM and library paths to internal file-server links', () => {
|
||
const catalog = normalizeCatalog(baseCatalog);
|
||
const radical = catalog.hacks.find((hack) => hack.slug === 'radical-red');
|
||
const farfetchd = catalog.hacks.find((hack) => hack.slug === 'a-farfetch-d-story');
|
||
|
||
expect(radical?.fileLinks).toContainEqual({
|
||
label: 'ROM file',
|
||
url: 'https://romhacks-files.ginnoir.com/_roms/gba/Hacks/Pokemon%20-%20Radical%20Red%20%28Hack%29.gba',
|
||
kind: 'rom',
|
||
});
|
||
expect(farfetchd?.fileLinks).toContainEqual({
|
||
label: 'Browse related files',
|
||
url: 'https://romhacks-files.ginnoir.com/Pokemon_A_Farfetch_D_Story/',
|
||
kind: 'library',
|
||
});
|
||
});
|
||
|
||
it('builds facets from normalized records when counts are not supplied', () => {
|
||
const facets = buildFacets(normalizeCatalog(baseCatalog).hacks);
|
||
|
||
expect(facets.platform.GBA).toBe(1);
|
||
expect(facets.platform.PC).toBe(1);
|
||
expect(facets.platform.Unknown).toBe(1);
|
||
expect(facets.type.Difficulty).toBe(1);
|
||
expect(facets.type.Expansion).toBe(1);
|
||
});
|
||
|
||
it('applies downloaded banner manifest paths and placeholder fallback', () => {
|
||
const catalog = normalizeCatalog(baseCatalog);
|
||
const withBanners = applyBannerManifest(catalog, {
|
||
'radical-red': '/assets/banners/radical-red.jpg',
|
||
});
|
||
|
||
const radical = withBanners.hacks.find((hack) => hack.slug === 'radical-red');
|
||
const unknown = withBanners.hacks.find((hack) => hack.slug === 'unknown-base');
|
||
|
||
expect(radical?.bannerLocal).toBe('/assets/banners/radical-red.jpg');
|
||
expect(unknown?.bannerLocal).toBe('/assets/placeholders/hack.svg');
|
||
});
|
||
|
||
it('merges generated guide links without dropping ROM or browse links', () => {
|
||
const catalog = normalizeCatalog(baseCatalog);
|
||
const withFiles = applyGeneratedFileLinks(catalog, {
|
||
'a-farfetch-d-story': [
|
||
{
|
||
label: 'guide.pdf',
|
||
url: 'https://romhacks-files.ginnoir.com/Pokemon_A_Farfetch_D_Story/guide.pdf',
|
||
kind: 'guide',
|
||
},
|
||
],
|
||
});
|
||
const farfetchd = withFiles.hacks.find((hack) => hack.slug === 'a-farfetch-d-story');
|
||
|
||
expect(farfetchd?.fileLinks).toEqual([
|
||
{
|
||
label: 'Browse related files',
|
||
url: 'https://romhacks-files.ginnoir.com/Pokemon_A_Farfetch_D_Story/',
|
||
kind: 'library',
|
||
},
|
||
{
|
||
label: 'guide.pdf',
|
||
url: 'https://romhacks-files.ginnoir.com/Pokemon_A_Farfetch_D_Story/guide.pdf',
|
||
kind: 'guide',
|
||
},
|
||
]);
|
||
});
|
||
|
||
it('sorts by name, release date, and platform', () => {
|
||
const hacks = normalizeCatalog(baseCatalog).hacks;
|
||
|
||
expect(sortHacks(hacks, 'name').map((hack) => hack.slug)).toEqual([
|
||
'a-farfetch-d-story',
|
||
'radical-red',
|
||
'unknown-base',
|
||
]);
|
||
expect(sortHacks(hacks, 'release_date').map((hack) => hack.slug)).toEqual([
|
||
'radical-red',
|
||
'a-farfetch-d-story',
|
||
'unknown-base',
|
||
]);
|
||
expect(sortHacks(hacks, 'platform').map((hack) => hack.slug)).toEqual([
|
||
'radical-red',
|
||
'a-farfetch-d-story',
|
||
'unknown-base',
|
||
]);
|
||
});
|
||
|
||
it('parses bare years and ISO dates for descending date sort', () => {
|
||
expect(parseReleaseDate('2024')).toBe(new Date('2024-01-01').getTime());
|
||
expect(parseReleaseDate('2023-08-10')).toBe(new Date('2023-08-10').getTime());
|
||
expect(parseReleaseDate(null)).toBe(0);
|
||
});
|
||
});
|