slugifyStem carried `.replaceAll('ΓÇÖ', "'")`, which patched a corrupted
apostrophe at the slug layer so three notes with mojibake filenames still
produced correct URLs. The underlying filenames are now repaired, so no stem
contains that sequence and the line is dead code that encodes an assumption
which is no longer true.
Proved it is dead rather than assuming: rebuilt and diffed all 411 generated
slugs against the previous build - identical, nothing moved.
The unit test asserted against the corrupted string, so it was testing the
workaround rather than the behaviour. It now uses the real repaired stem,
'A Farfetch’D Story', which exercises the same thing that actually matters -
that U+2019 becomes a hyphen - and still expects 'a-farfetch-d-story'. Added a
case for 'Fused Dimensions' to pin the non-breaking-space folding, since that
name previously carried U+00A0.
Also syncs the catalog for the Fused Dimensions rename. Verified: npm run check
(0 errors, vitest 9/9), npm run build (486 pages), CI=1 npm run test:ui (8/8),
deployed; live 200 on all seven repaired URLs and the h1 renders without NBSP.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
228 lines
7.4 KiB
TypeScript
228 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('Fused Dimensions')).toBe('fused-dimensions');
|
||
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);
|
||
});
|
||
});
|