diff --git a/client/src/pages/AtlasPage.test.tsx b/client/src/pages/AtlasPage.test.tsx index 7f7572fe2..07824234f 100644 --- a/client/src/pages/AtlasPage.test.tsx +++ b/client/src/pages/AtlasPage.test.tsx @@ -1578,6 +1578,61 @@ describe('AtlasPage', () => { }); }); + describe('FE-PAGE-ATLAS-048: clicking a visited (place-derived, not manually-marked) region opens the hide-region popup', () => { + it('offers to remove a region that came from real place data, not just a manually-marked one', async () => { + // FR keeps real tripCount/placeCount from atlasStatsResponse so the country layer's + // own click handler (only active for a zero-count country) stays a no-op — this test + // is only about the region layer. The region entry has no `manuallyMarked` flag, + // which used to make a region click open the country-detail view instead of an + // unmark option at all. + server.use( + http.get('/api/addons/atlas/regions', () => HttpResponse.json({ + regions: { FR: [{ code: 'FR-IDF', name: 'Île-de-France', placeCount: 3 }] }, + })), + http.get('/api/addons/atlas/countries/geo', () => HttpResponse.json({ + type: 'FeatureCollection', + features: [{ type: 'Feature', properties: { ISO_A2: 'FR', ADM0_A3: 'FRA', ISO_A3: 'FRA', NAME: 'France', ADMIN: 'France' }, geometry: null }], + })), + http.get('/api/addons/atlas/regions/geo', () => HttpResponse.json({ + type: 'FeatureCollection', + features: [{ type: 'Feature', properties: { iso_a2: 'FR', iso_3166_2: 'FR-IDF', name: 'Île-de-France', name_en: 'Île-de-France', admin: 'France' }, geometry: null }], + })), + http.delete('/api/addons/atlas/region/:code/mark', () => HttpResponse.json({ success: true })), + ); + + render(); + + // Wait for initial data to load before the region layer can build (loadRegionsForViewport + // needs country_layer_by_a2_ref already populated, which only happens once the country + // layer's own effect has run) — see FE-PAGE-ATLAS-045 for the same recipe. Toggling dark + // mode re-initializes the map, re-registers zoomend, and by then FR's country layer + // already exists, so loadRegionsForViewport actually fetches /regions/geo this time. + await waitFor(() => { + expect(screen.getAllByText(/countries/i).length).toBeGreaterThan(0); + }); + seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: true }) }); + + // The layer mock immediately invokes click callbacks once the region layer is built: + // FR-IDF is visited (place-derived) → confirmAction becomes { type: 'unmark-region', ... } + await waitFor(() => { + expect( + screen.queryAllByRole('button').some((b) => b.textContent?.trim() === 'Remove'), + ).toBe(true); + }, { timeout: 5000 }); + + const removeBtn = screen.queryAllByRole('button').find((b) => b.textContent?.trim() === 'Remove'); + if (removeBtn) { + fireEvent.click(removeBtn); + } + + await waitFor(() => { + expect(screen.queryAllByRole('button').some((b) => b.textContent?.trim() === 'Remove')).toBe(false); + }, { timeout: 3000 }).catch(() => {}); + + expect(screen.getAllByText(/countries/i).length).toBeGreaterThan(0); + }); + }); + describe('FE-PAGE-ATLAS-039: bucket item with lat/lng renders on map (markers useEffect)', () => { it('renders bucket items with coordinates causing marker useEffect to run', async () => { server.use( diff --git a/client/src/pages/AtlasPage.tsx b/client/src/pages/AtlasPage.tsx index 13964d81c..81554d49a 100644 --- a/client/src/pages/AtlasPage.tsx +++ b/client/src/pages/AtlasPage.tsx @@ -338,12 +338,18 @@ export default function AtlasPage(): React.ReactElement { if (remaining.length === 0) delete next[countryCode] return next }) - // If no manually-marked regions remain, also remove country if it has no trips/places + // If no visible regions remain at all (not just manually-marked ones — + // the server now hides a region regardless of how it was derived, and + // cascades to the country the same way), remove the country too, but + // only when it has no real place/trip data of its own: a country with + // real places is never actually hidden server-side (#1490), so + // optimistically removing it here would just flash and reappear on + // the next reload. setData(prev => { if (!prev) return prev const c = prev.countries.find(c => c.code === countryCode) if (!c || c.placeCount > 0 || c.tripCount > 0) return prev - const remainingRegions = (visitedRegions[countryCode] || []).filter(r => r.code !== rCode && r.manuallyMarked) + const remainingRegions = (visitedRegions[countryCode] || []).filter(r => r.code !== rCode) if (remainingRegions.length > 0) return prev const cont = continentForCountry(countryCode) return { diff --git a/client/src/pages/atlas/useAtlas.ts b/client/src/pages/atlas/useAtlas.ts index c8c4bd1f2..eba7ebc2c 100644 --- a/client/src/pages/atlas/useAtlas.ts +++ b/client/src/pages/atlas/useAtlas.ts @@ -536,18 +536,17 @@ export function useAtlas() { layer.on('click', () => { if (!countryA2) return if (visited) { - const regionEntry = visitedRegions[countryA2]?.find(r => r.code === regionCode || normalizeRegionName(r.name) === normalizeRegionName(regionNameEn)) - if (regionEntry?.manuallyMarked) { - setConfirmActionRef.current({ - type: 'unmark-region', - code: countryA2, - name: regionName, - regionCode, - countryName, - }) - } else { - loadCountryDetailRef.current(countryA2) - } + // Any visited region can be hidden now, not just a manually-marked one — a + // region derived from a real place (e.g. one a border-simplification gap + // misassigned) is exactly the case that needs it. Country details remain + // reachable via the country search/sidebar. + setConfirmActionRef.current({ + type: 'unmark-region', + code: countryA2, + name: regionName, + regionCode, + countryName, + }) } else { setConfirmActionRef.current({ type: 'choose-region',