feat: add romhack archive site
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
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',
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user