Files
romhack-archive-site/src/lib/catalog.test.ts
T

227 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 FarfetchD 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);
});
});