/** * server/regions.test.ts — the sub-region data module (v0.19). * * The first suite loads the REAL `public/regions.geo.json` (the same file the * production server loads at boot), so expansion/migration runs against the * actual 1,796-region dataset. The second suite exercises the graceful * "geo missing" path (module not loaded) via the `_resetRegionsForTests` seam, * and pins the `_setRegionsForTests` fixture seam used by the other unit tests. * Module state is per vitest file, so these suites never collide with other * test files. */ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import type { GameState } from '../shared/types'; import { _resetRegionsForTests, _setRegionsForTests, expandTerritory, hasRegionData, loadRegions, migrateTerritoryToRegions, regionLabel, regionNamesFor, } from './regions'; const here = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(here, '..'); function makeState(territory: string[]): GameState { return { id: 'g', presetId: 'p', presetTitle: 'P', playerNationId: 'uk', turn: 1, date: '1939-09-01', nations: { uk: { id: 'uk', name: 'UK', type: 'major', stats: { military: 80, economy: 70, stability: 60, population: 50 }, territory, relations: {}, }, }, events: [], pending: [], version: 1, }; } describe('server/regions (real geo)', () => { beforeAll(() => { _resetRegionsForTests(); loadRegions(root); }); it('loads public/regions.geo.json (hasRegionData becomes true)', () => { expect(hasRegionData()).toBe(true); }); it('loadRegions is idempotent: a second call keeps the loaded data', () => { loadRegions(root); expect(hasRegionData()).toBe(true); }); it('expandTerritory expands an ISO_A3 into its sub-region ids (unique, all with that parent)', () => { const ids = expandTerritory(['FRA']); expect(ids.length).toBeGreaterThan(1); // France is subdivided, not one region expect(new Set(ids).size).toBe(ids.length); // deduped // every expanded id resolves back to the same parent country const names = regionNamesFor(ids); for (const id of ids) expect(names[id]).toMatch(/\(FRA\)$/); }); it('expandTerritory is order-preserving and dedupes repeated country codes', () => { expect(expandTerritory(['FRA', 'FRA'])).toEqual(expandTerritory(['FRA'])); // two countries: the union, in first-seen order (the ids are disjoint) expect(expandTerritory(['FRA', 'DEU'])).toEqual([ ...expandTerritory(['FRA']), ...expandTerritory(['DEU']), ]); }); it('expandTerritory of an empty or unknown list yields no regions', () => { expect(expandTerritory([])).toEqual([]); expect(expandTerritory(['XXX'])).toEqual([]); // no country with that code }); it('regionNamesFor maps every known id to "name (parent)"', () => { const ids = expandTerritory(['DEU']); const names = regionNamesFor(ids); expect(Object.keys(names).sort()).toEqual([...ids].sort()); for (const id of ids) expect(names[id]).toMatch(/\(DEU\)$/); }); it('regionNamesFor skips ids that are not in the loaded index', () => { expect(regionNamesFor(['NO-SUCH-01'])).toEqual({}); }); it('regionLabel is "id — name (parent)" for known ids and the bare id for unknown', () => { const [id] = expandTerritory(['FRA']); expect(regionLabel(id)).toMatch(new RegExp(`^${id} — .+ \\(FRA\\)$`)); expect(regionLabel('NO-SUCH-01')).toBe('NO-SUCH-01'); }); it('migrateTerritoryToRegions rewrites ISO_A3 entries to region ids, keeps existing region ids, and is idempotent', () => { const [deId] = expandTerritory(['DEU']); // a real region id of another country const state = makeState(['FRA', deId]); // mixed: legacy ISO_A3 + already migrated const originalArray = state.nations.uk.territory; // it mutates and returns the (fresh) state it is given — callers pass a // just-parsed object (db.load), so the in-place mutation is safe const out = migrateTerritoryToRegions(state); expect(out).toBe(state); // the original array object is never mutated in place — a new one is // installed on the nation, and the original stays as it was expect(out.nations.uk.territory).not.toBe(originalArray); expect(originalArray).toEqual(['FRA', deId]); // the ISO_A3 entry is gone, replaced by its sub-region ids… expect(out.nations.uk.territory).not.toContain('FRA'); for (const id of expandTerritory(['FRA'])) expect(out.nations.uk.territory).toContain(id); // …and the existing region id survives, exactly once expect(out.nations.uk.territory.filter((t) => t === deId)).toEqual([deId]); // idempotent: a second migration changes nothing expect(migrateTerritoryToRegions(out).nations.uk.territory).toEqual(out.nations.uk.territory); }); }); describe('server/regions (fallback: geo not loaded)', () => { beforeAll(() => { _resetRegionsForTests(); }); afterAll(() => { // restore the real geo for anything in this file that runs later _resetRegionsForTests(); loadRegions(root); }); it('hasRegionData is false after a reset', () => { expect(hasRegionData()).toBe(false); }); it('expandTerritory returns its input unchanged (whole-country fallback)', () => { expect(expandTerritory(['FRA'])).toEqual(['FRA']); expect(expandTerritory([])).toEqual([]); }); it('regionNamesFor is empty and regionLabel falls back to the bare id', () => { expect(regionNamesFor(['FRA'])).toEqual({}); expect(regionLabel('FRA')).toBe('FRA'); }); it('migrateTerritoryToRegions is a no-op (returns the state untouched)', () => { const state = makeState(['FRA']); const out = migrateTerritoryToRegions(state); expect(out).toBe(state); expect(out.nations.uk.territory).toEqual(['FRA']); }); it('_setRegionsForTests loads a fixture and drives the pure helpers', () => { _setRegionsForTests( { 'GB-01': { name: 'Greater London', parent: 'GBR' }, 'GB-02': { name: 'Scotland', parent: 'GBR' }, }, { GBR: ['GB-01', 'GB-02'] }, ); expect(hasRegionData()).toBe(true); expect(expandTerritory(['GBR'])).toEqual(['GB-01', 'GB-02']); expect(regionNamesFor(['GB-01'])).toEqual({ 'GB-01': 'Greater London (GBR)' }); const state = makeState(['GBR']); expect(migrateTerritoryToRegions(state).nations.uk.territory).toEqual(['GB-01', 'GB-02']); }); });