mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
Merge pull request #660 from mauriceboe/fix/atlas-country-matching-france-norway-review
fix(atlas): add A3 fallback when ISO_A2 is invalid + cleanup
This commit is contained in:
@@ -127,6 +127,23 @@ const geoJsonWithFR = {
|
||||
],
|
||||
};
|
||||
|
||||
const makeGeoJsonWithA3Fallback = (a3: string, name: string) => ({
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
ISO_A2: '-99',
|
||||
ADM0_A3: a3,
|
||||
ISO_A3: a3,
|
||||
NAME: name,
|
||||
ADMIN: name,
|
||||
},
|
||||
geometry: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// ── Atlas API response fixture ────────────────────────────────────────────────
|
||||
const atlasStatsResponse = {
|
||||
countries: [{ code: 'FR', tripCount: 2, placeCount: 5, firstVisit: '2023-01-01', lastVisit: '2024-06-01' }],
|
||||
@@ -1323,6 +1340,36 @@ describe('AtlasPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-ATLAS-041: country search falls back from A3 when ISO_A2 is invalid', () => {
|
||||
it.each([
|
||||
{ a3: 'FRA', name: 'France', query: 'france' },
|
||||
{ a3: 'NOR', name: 'Norway', query: 'norway' },
|
||||
])('returns $name in search results when GeoJSON provides ADM0_A3=$a3 but ISO_A2 is -99', async ({ a3, name, query }) => {
|
||||
vi.spyOn(global, 'fetch').mockImplementation((url) => {
|
||||
const urlStr = String(url);
|
||||
if (urlStr.includes('geojson') || urlStr.includes('githubusercontent')) {
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(makeGeoJsonWithA3Fallback(a3, name)) } as Response);
|
||||
}
|
||||
return Promise.reject(new Error(`Unmocked fetch: ${urlStr}`));
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<AtlasPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(/search a country/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/search a country/i);
|
||||
await user.type(searchInput, query);
|
||||
|
||||
await waitFor(() => {
|
||||
const countryButton = screen.getAllByRole('button').find((button) => button.textContent?.includes(name));
|
||||
expect(countryButton).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-ATLAS-042: bucket form submit with actual name value', () => {
|
||||
it('submitting bucket form with a non-empty name covers handleAddBucketItem', async () => {
|
||||
server.use(
|
||||
|
||||
@@ -109,10 +109,14 @@ function useCountryNames(language: string): (code: string) => string {
|
||||
return resolver
|
||||
}
|
||||
|
||||
// Map visited country codes to ISO-3166 alpha3 (GeoJSON uses alpha3)
|
||||
// Built dynamically from GeoJSON + hardcoded fallbacks
|
||||
const A2_TO_A3_BASE: Record<string, string> = {"AF":"AFG","AL":"ALB","DZ":"DZA","AD":"AND","AO":"AGO","AG":"ATG","AR":"ARG","AM":"ARM","AU":"AUS","AT":"AUT","AZ":"AZE","BS":"BHS","BH":"BHR","BD":"BGD","BB":"BRB","BY":"BLR","BE":"BEL","BZ":"BLZ","BJ":"BEN","BT":"BTN","BO":"BOL","BA":"BIH","BW":"BWA","BR":"BRA","BN":"BRN","BG":"BGR","BF":"BFA","BI":"BDI","CV":"CPV","KH":"KHM","CM":"CMR","CA":"CAN","CF":"CAF","TD":"TCD","CL":"CHL","CN":"CHN","CO":"COL","KM":"COM","CG":"COG","CD":"COD","CR":"CRI","CI":"CIV","HR":"HRV","CU":"CUB","CY":"CYP","CZ":"CZE","DK":"DNK","DJ":"DJI","DM":"DMA","DO":"DOM","EC":"ECU","EG":"EGY","SV":"SLV","GQ":"GNQ","ER":"ERI","EE":"EST","SZ":"SWZ","ET":"ETH","FJ":"FJI","FI":"FIN","FR":"FRA","GA":"GAB","GM":"GMB","GE":"GEO","DE":"DEU","GH":"GHA","GR":"GRC","GD":"GRD","GT":"GTM","GN":"GIN","GW":"GNB","GY":"GUY","HT":"HTI","HN":"HND","HU":"HUN","IS":"ISL","IN":"IND","ID":"IDN","IR":"IRN","IQ":"IRQ","IE":"IRL","IL":"ISR","IT":"ITA","JM":"JAM","JP":"JPN","JO":"JOR","KZ":"KAZ","KE":"KEN","KI":"KIR","KP":"PRK","KR":"KOR","KW":"KWT","KG":"KGZ","LA":"LAO","LV":"LVA","LB":"LBN","LS":"LSO","LR":"LBR","LY":"LBY","LI":"LIE","LT":"LTU","LU":"LUX","MG":"MDG","MW":"MWI","MY":"MYS","MV":"MDV","ML":"MLI","MT":"MLT","MR":"MRT","MU":"MUS","MX":"MEX","MD":"MDA","MN":"MNG","ME":"MNE","MA":"MAR","MZ":"MOZ","MM":"MMR","NA":"NAM","NP":"NPL","NL":"NLD","NZ":"NZL","NI":"NIC","NE":"NER","NG":"NGA","MK":"MKD","NO":"NOR","OM":"OMN","PK":"PAK","PA":"PAN","PG":"PNG","PY":"PRY","PE":"PER","PH":"PHL","PL":"POL","PT":"PRT","QA":"QAT","RO":"ROU","RU":"RUS","RW":"RWA","SA":"SAU","SN":"SEN","RS":"SRB","SL":"SLE","SG":"SGP","SK":"SVK","SI":"SVN","SB":"SLB","SO":"SOM","ZA":"ZAF","SS":"SSD","ES":"ESP","LK":"LKA","SD":"SDN","SR":"SUR","SE":"SWE","CH":"CHE","SY":"SYR","TW":"TWN","TJ":"TJK","TZ":"TZA","TH":"THA","TL":"TLS","TG":"TGO","TT":"TTO","TN":"TUN","TR":"TUR","TM":"TKM","UG":"UGA","UA":"UKR","AE":"ARE","GB":"GBR","US":"USA","UY":"URY","UZ":"UZB","VU":"VUT","VE":"VEN","VN":"VNM","YE":"YEM","ZM":"ZMB","ZW":"ZWE"}
|
||||
let A2_TO_A3: Record<string, string> = { ...A2_TO_A3_BASE }
|
||||
// ISO-3166-1 alpha-2 → alpha-3 mapping. Two sources feed this table:
|
||||
// 1. Hardcoded entries below — REQUIRED for any country whose Natural Earth GeoJSON record
|
||||
// has ISO_A2='-99' (e.g. France=FRA, Norway=NOR). The runtime augmentation loop
|
||||
// (see geoData useEffect below) skips '-99' features, so those countries MUST be
|
||||
// listed here or the atlas_country_options A3-fallback will silently fail.
|
||||
// 2. Runtime augmentation — the geoData load effect adds entries for every feature
|
||||
// that has a valid ISO_A2, covering territories not present below.
|
||||
const A2_TO_A3: Record<string, string> = {"AF":"AFG","AL":"ALB","DZ":"DZA","AD":"AND","AO":"AGO","AG":"ATG","AR":"ARG","AM":"ARM","AU":"AUS","AT":"AUT","AZ":"AZE","BS":"BHS","BH":"BHR","BD":"BGD","BB":"BRB","BY":"BLR","BE":"BEL","BZ":"BLZ","BJ":"BEN","BT":"BTN","BO":"BOL","BA":"BIH","BW":"BWA","BR":"BRA","BN":"BRN","BG":"BGR","BF":"BFA","BI":"BDI","CV":"CPV","KH":"KHM","CM":"CMR","CA":"CAN","CF":"CAF","TD":"TCD","CL":"CHL","CN":"CHN","CO":"COL","KM":"COM","CG":"COG","CD":"COD","CR":"CRI","CI":"CIV","HR":"HRV","CU":"CUB","CY":"CYP","CZ":"CZE","DK":"DNK","DJ":"DJI","DM":"DMA","DO":"DOM","EC":"ECU","EG":"EGY","SV":"SLV","GQ":"GNQ","ER":"ERI","EE":"EST","SZ":"SWZ","ET":"ETH","FJ":"FJI","FI":"FIN","FR":"FRA","GA":"GAB","GM":"GMB","GE":"GEO","DE":"DEU","GH":"GHA","GR":"GRC","GD":"GRD","GT":"GTM","GN":"GIN","GW":"GNB","GY":"GUY","HT":"HTI","HN":"HND","HU":"HUN","IS":"ISL","IN":"IND","ID":"IDN","IR":"IRN","IQ":"IRQ","IE":"IRL","IL":"ISR","IT":"ITA","JM":"JAM","JP":"JPN","JO":"JOR","KZ":"KAZ","KE":"KEN","KI":"KIR","KP":"PRK","KR":"KOR","KW":"KWT","KG":"KGZ","LA":"LAO","LV":"LVA","LB":"LBN","LS":"LSO","LR":"LBR","LY":"LBY","LI":"LIE","LT":"LTU","LU":"LUX","MG":"MDG","MW":"MWI","MY":"MYS","MV":"MDV","ML":"MLI","MT":"MLT","MR":"MRT","MU":"MUS","MX":"MEX","MD":"MDA","MN":"MNG","ME":"MNE","MA":"MAR","MZ":"MOZ","MM":"MMR","NA":"NAM","NP":"NPL","NL":"NLD","NZ":"NZL","NI":"NIC","NE":"NER","NG":"NGA","MK":"MKD","NO":"NOR","OM":"OMN","PK":"PAK","PA":"PAN","PG":"PNG","PY":"PRY","PE":"PER","PH":"PHL","PL":"POL","PT":"PRT","QA":"QAT","RO":"ROU","RU":"RUS","RW":"RWA","SA":"SAU","SN":"SEN","RS":"SRB","SL":"SLE","SG":"SGP","SK":"SVK","SI":"SVN","SB":"SLB","SO":"SOM","ZA":"ZAF","SS":"SSD","ES":"ESP","LK":"LKA","SD":"SDN","SR":"SUR","SE":"SWE","CH":"CHE","SY":"SYR","TW":"TWN","TJ":"TJK","TZ":"TZA","TH":"THA","TL":"TLS","TG":"TGO","TT":"TTO","TN":"TUN","TR":"TUR","TM":"TKM","UG":"UGA","UA":"UKR","AE":"ARE","GB":"GBR","US":"USA","UY":"URY","UZ":"UZB","VU":"VUT","VE":"VEN","VN":"VNM","YE":"YEM","ZM":"ZMB","ZW":"ZWE"}
|
||||
|
||||
export default function AtlasPage(): React.ReactElement {
|
||||
const { t, language } = useTranslation()
|
||||
@@ -186,15 +190,24 @@ export default function AtlasPage(): React.ReactElement {
|
||||
|
||||
const atlas_country_options = useMemo(() => {
|
||||
if (!geoData) return []
|
||||
// Precompute A3 → A2 reverse lookup once per geoData change instead of
|
||||
// scanning A2_TO_A3 for every feature that needs the fallback.
|
||||
const a3ToA2 = new Map<string, string>()
|
||||
for (const [a2Key, a3Val] of Object.entries(A2_TO_A3)) a3ToA2.set(a3Val, a2Key)
|
||||
|
||||
const opts: { code: string; label: string }[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const f of (geoData as any).features || []) {
|
||||
const a2 = f?.properties?.ISO_A2
|
||||
if (!a2 || a2 === '-99' || typeof a2 !== 'string' || a2.length !== 2) continue
|
||||
if (seen.has(a2)) continue
|
||||
seen.add(a2)
|
||||
const label = String(resolveName(a2) || f?.properties?.NAME || f?.properties?.ADMIN || a2)
|
||||
opts.push({ code: a2, label })
|
||||
const rawA2 = f?.properties?.ISO_A2
|
||||
let resolvedA2: string | null = (typeof rawA2 === 'string' && rawA2.length === 2 && rawA2 !== '-99') ? rawA2 : null
|
||||
if (!resolvedA2) {
|
||||
const a3 = f?.properties?.ADM0_A3 || f?.properties?.ISO_A3 || f?.properties?.['ISO3166-1-Alpha-3'] || null
|
||||
if (a3 && a3 !== '-99') resolvedA2 = a3ToA2.get(a3) ?? null
|
||||
}
|
||||
if (!resolvedA2 || seen.has(resolvedA2)) continue
|
||||
seen.add(resolvedA2)
|
||||
const label = String(resolveName(resolvedA2) || f?.properties?.NAME || f?.properties?.ADMIN || resolvedA2)
|
||||
opts.push({ code: resolvedA2, label })
|
||||
}
|
||||
opts.sort((a, b) => a.label.localeCompare(b.label))
|
||||
return opts
|
||||
|
||||
Reference in New Issue
Block a user