import { describe, expect, it } from 'vitest'; import { buildRegionMap } from './map'; import { nationColor, regionOwner, type RegionMap } from '../shared/protocol'; import type { GameState } from '../shared/types'; const state: GameState = { 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: ['GBR', 'IRL'], relations: {}, }, de: { id: 'de', name: 'DE', type: 'major', stats: { military: 90, economy: 70, stability: 70, population: 90 }, territory: ['DEU'], relations: {}, }, }, events: [], pending: [], version: 1, }; // The RegionMap built from the fixture: the authoritative ISO_A3 → owner source. const regions: RegionMap = buildRegionMap(state); describe('buildRegionMap', () => { it('maps each ISO_A3 region to its owner + that nation\'s stable color', () => { const m = buildRegionMap(state); expect(m.GBR).toEqual({ color: nationColor('uk'), owner: 'UK', ownerId: 'uk' }); expect(m.IRL).toEqual({ color: nationColor('uk'), owner: 'UK', ownerId: 'uk' }); expect(m.DEU).toEqual({ color: nationColor('de'), owner: 'DE', ownerId: 'de' }); // a region nobody owns is simply absent expect(m.FRA).toBeUndefined(); }); }); describe('regionOwner', () => { it('resolves an owned region\'s ISO_A3 to its owner record', () => { expect(regionOwner(regions, 'GBR')).toEqual({ color: nationColor('uk'), owner: 'UK', ownerId: 'uk', }); expect(regionOwner(regions, 'DEU')).toEqual({ color: nationColor('de'), owner: 'DE', ownerId: 'de', }); }); it('returns undefined for an unowned region (no RegionMap entry)', () => { expect(regionOwner(regions, 'FRA')).toBeUndefined(); expect(regionOwner(regions, 'USA')).toBeUndefined(); }); it('returns undefined for an empty or missing code', () => { expect(regionOwner(regions, '')).toBeUndefined(); expect(regionOwner(regions, null)).toBeUndefined(); expect(regionOwner(regions, undefined)).toBeUndefined(); }); }); /** * #6 regression guard. The click/hover handlers resolve the owning nation from a * map feature. MapLibre repacks a GeoJSON source's top-level `id` (the ISO_A3 * string) into a *numeric index* when it stores the source, so a feature the map * hands back at interaction time has a number for `id` and the ISO_A3 only lives * in `properties.regionId`. This test pins the exact data shape and proves the * fix: resolution via `properties.regionId` works, while the pre-fix path that * read `feature.id` does not. */ describe('#6 — owner must be resolved from properties.regionId, not the repacked feature.id', () => { // The shape MapLibre returns for a source feature after setData(): `id` is a // number (here 0, as observed in the live map), and the ISO_A3 is preserved // only in properties (the client stamps it as `regionId`). const repackedFeature = { id: 0, properties: { name: 'DE', regionId: 'DEU' }, }; it('resolves the owner from properties.regionId (the fix)', () => { const code = typeof repackedFeature.properties.regionId === 'string' ? repackedFeature.properties.regionId : ''; expect(regionOwner(regions, code)).toEqual({ color: nationColor('de'), owner: 'DE', ownerId: 'de', }); }); it('the old path (feature.id as a string key) does NOT resolve — the bug', () => { // String(0) === '0' is not a valid ISO_A3 key, so the pre-fix lookup failed // and the click popup reported "No owner" even for owned regions. expect(regions[String(repackedFeature.id)]).toBeUndefined(); expect(regionOwner(regions, String(repackedFeature.id))).toBeUndefined(); }); }); describe('nationColor', () => { it('is deterministic per id and distinct across different ids', () => { expect(nationColor('uk')).toBe(nationColor('uk')); expect(nationColor('uk')).not.toBe(nationColor('de')); // always a valid HSL string expect(nationColor('uk')).toMatch(/^hsl\(\d+ 58% 46%\)$/); }); });