141 lines
5.3 KiB
TypeScript
141 lines
5.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { buildRegionMap } from './map';
|
|
import { mapFeatureOwner, 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 (sub-national map). The click/hover handlers resolve the
|
|
* owning nation from a rendered map feature. MapLibre repacks a GeoJSON
|
|
* source's top-level `id` into a *numeric index* when it stores the source, so a
|
|
* feature the map hands back at interaction time has a number for `id`. The
|
|
* choropleth is now sub-national: a feature's id is a sub-region code
|
|
* (`DEU-03`) and its owner lives at the *parent country* (`DEU`), which the
|
|
* client stamps into `properties.parent` (the RegionMap is keyed by that
|
|
* country code, never the sub-region id). This pins the exact data shape and
|
|
* proves the resolution: `mapFeatureOwner` works via `properties.parent`, while
|
|
* the two wrong paths (the repacked numeric `feature.id`, and the sub-region
|
|
* `regionId`, which is not a RegionMap key) do not.
|
|
*/
|
|
describe('#6 — sub-region owner must be resolved from properties.parent, not the sub-region id / repacked feature.id', () => {
|
|
// The shape MapLibre returns for a sub-region feature after setData(): `id`
|
|
// is a number (repacked), and the stable identifiers live only in properties —
|
|
// `regionId` (the sub-region code) and `parent` (the ISO_A3 country).
|
|
const repackedFeature = {
|
|
id: 0,
|
|
properties: { name: 'Germany R03', regionId: 'DEU-03', parent: 'DEU' },
|
|
};
|
|
|
|
it('resolves the owner from properties.parent (the fix)', () => {
|
|
expect(mapFeatureOwner(regions, repackedFeature)).toEqual({
|
|
color: nationColor('de'),
|
|
owner: 'DE',
|
|
ownerId: 'de',
|
|
});
|
|
});
|
|
|
|
it('the sub-region regionId is NOT a RegionMap key — resolving from it fails', () => {
|
|
// 'DEU-03' is a sub-region id; the RegionMap is keyed by the parent 'DEU'.
|
|
expect(regionOwner(regions, repackedFeature.properties.regionId)).toBeUndefined();
|
|
});
|
|
|
|
it('the repacked numeric feature.id does NOT resolve — the bug', () => {
|
|
// String(0) === '0' is not a valid key, so the pre-fix lookup failed and
|
|
// reported "No owner" even for owned regions.
|
|
expect(regionOwner(regions, String(repackedFeature.id))).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('mapFeatureOwner', () => {
|
|
it('returns undefined for a missing feature, missing parent, or non-string parent', () => {
|
|
expect(mapFeatureOwner(regions, null)).toBeUndefined();
|
|
expect(mapFeatureOwner(regions, undefined)).toBeUndefined();
|
|
expect(mapFeatureOwner(regions, {})).toBeUndefined();
|
|
expect(mapFeatureOwner(regions, { properties: {} })).toBeUndefined();
|
|
expect(mapFeatureOwner(regions, { properties: { parent: 42 } })).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined for a parent country nobody owns', () => {
|
|
expect(mapFeatureOwner(regions, { properties: { parent: 'FRA' } })).toBeUndefined();
|
|
expect(mapFeatureOwner(regions, { properties: { parent: 'USA' } })).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%\)$/);
|
|
});
|
|
});
|