From fdcdf7fa9fa206729a0854797cad21817f825e1c Mon Sep 17 00:00:00 2001 From: Thor Tveito Date: Thu, 5 Nov 2026 10:34:55 +0100 Subject: [PATCH] Refactor: Standardize country names in filters and sightings --- frontend/e2e/accessibility.spec.ts | 2 +- frontend/e2e/submit-sighting.spec.ts | 2 +- frontend/src/components/SearchFilters.tsx | 15 ++-------- frontend/src/tests/SearchPage.test.tsx | 10 ++++--- .../src/tests/SightingDetailPage.test.tsx | 24 ++++++++++----- frontend/src/tests/SightingList.test.tsx | 8 +++-- frontend/src/tests/SightingMap.test.tsx | 6 ++-- .../src/tests/SubmitSightingPage.test.tsx | 2 +- frontend/src/tests/format-sighting.test.ts | 30 +++++++++++++++---- frontend/src/utils/format-sighting.ts | 24 +++++++++++---- 10 files changed, 81 insertions(+), 42 deletions(-) diff --git a/frontend/e2e/accessibility.spec.ts b/frontend/e2e/accessibility.spec.ts index 64763e5..14bf6ae 100644 --- a/frontend/e2e/accessibility.spec.ts +++ b/frontend/e2e/accessibility.spec.ts @@ -32,7 +32,7 @@ const pages: { path: './#/sightings/3', ready: (page) => expect( - page.getByRole('heading', { level: 1, name: 'Oslo, NO' }), + page.getByRole('heading', { level: 1, name: 'Oslo, Norway' }), ).toBeVisible(), }, { diff --git a/frontend/e2e/submit-sighting.spec.ts b/frontend/e2e/submit-sighting.spec.ts index 7b63a52..11c6c69 100644 --- a/frontend/e2e/submit-sighting.spec.ts +++ b/frontend/e2e/submit-sighting.spec.ts @@ -123,7 +123,7 @@ test('fill in and submit the form with the keyboard', async ({ page }) => { await tabTo(page, page.getByRole('link', { name: 'View your sighting' })) await page.keyboard.press('Enter') await expect( - page.getByRole('heading', { level: 1, name: 'Trondheim, NO' }), + page.getByRole('heading', { level: 1, name: 'Trondheim, Norway' }), ).toBeVisible() await expect(page.getByText('User submitted')).toBeVisible() await expect( diff --git a/frontend/src/components/SearchFilters.tsx b/frontend/src/components/SearchFilters.tsx index a72e7c1..a1664e0 100644 --- a/frontend/src/components/SearchFilters.tsx +++ b/frontend/src/components/SearchFilters.tsx @@ -1,18 +1,9 @@ import { useQuery } from '@apollo/client/react' import { FILTER_OPTIONS_QUERY } from '../graphql/queries' import { createRetry } from '../utils/create-retry' +import { formatCountry } from '../utils/format-sighting' import './SearchFilters.css' -const countryNames = new Intl.DisplayNames(['en'], { type: 'region' }) - -function countryName(code: string) { - try { - return countryNames.of(code.toUpperCase()) ?? code - } catch { - return code - } -} - export interface SearchFiltersValue { shape: string country: string @@ -38,7 +29,7 @@ export default function SearchFilters({ const countries = [ ...new Set([...(data?.countries ?? []), filters.country].filter(Boolean)), ] - .map((code) => ({ code, name: countryName(code) })) + .map((code) => ({ code, name: formatCountry(code) })) .sort((left, right) => left.name.localeCompare(right.name, 'en')) const chips: { key: keyof SearchFiltersValue; label: string }[] = [ { @@ -47,7 +38,7 @@ export default function SearchFilters({ }, { key: 'country', - label: filters.country && `Country: ${countryName(filters.country)}`, + label: filters.country && `Country: ${formatCountry(filters.country)}`, }, { key: 'from', label: filters.from && `From: ${filters.from}` }, { key: 'to', label: filters.to && `To: ${filters.to}` }, diff --git a/frontend/src/tests/SearchPage.test.tsx b/frontend/src/tests/SearchPage.test.tsx index 2bd403f..6eee20d 100644 --- a/frontend/src/tests/SearchPage.test.tsx +++ b/frontend/src/tests/SearchPage.test.tsx @@ -147,8 +147,10 @@ describe('SearchPage', () => { target: { value: 'nz' }, }) expect(await screen.findByText('1 result')).toBeInTheDocument() - expect(await screen.findByText('Gisborne, NZ')).toBeInTheDocument() - expect(screen.queryByText('San Marcos, TX, US')).not.toBeInTheDocument() + expect(await screen.findByText('Gisborne, New Zealand')).toBeInTheDocument() + expect( + screen.queryByText('San Marcos, TX, United States'), + ).not.toBeInTheDocument() expect(requests.at(-1)?.filter).toEqual({ country: 'nz' }) expect(screen.getByLabelText('Current URL')).toHaveTextContent('country=nz') }) @@ -314,7 +316,7 @@ describe('SearchPage', () => { expect(screen.getByRole('searchbox')).toHaveValue('oslo') expect( - await screen.findByRole('link', { name: 'Oslo, NO' }), + await screen.findByRole('link', { name: 'Oslo, Norway' }), ).toBeInTheDocument() expect(requests).toEqual([{ search: 'oslo', sort: 'DATE_DESC', first: 20 }]) }) @@ -385,7 +387,7 @@ describe('SearchPage', () => { fireEvent.click(screen.getByRole('button', { name: 'Try again' })) expect( - await screen.findByRole('link', { name: 'Oslo, NO' }), + await screen.findByRole('link', { name: 'Oslo, Norway' }), ).toBeInTheDocument() expect(screen.queryByRole('alert')).not.toBeInTheDocument() expect(requests).toHaveLength(2) diff --git a/frontend/src/tests/SightingDetailPage.test.tsx b/frontend/src/tests/SightingDetailPage.test.tsx index 10678c4..d12e80b 100644 --- a/frontend/src/tests/SightingDetailPage.test.tsx +++ b/frontend/src/tests/SightingDetailPage.test.tsx @@ -72,11 +72,13 @@ describe('SightingDetailPage', () => { screen.getByRole('heading', { name: 'Loading sighting…' }), ).toBeInTheDocument() expect( - await screen.findByRole('heading', { name: 'San Marcos, TX, US' }), + await screen.findByRole('heading', { + name: 'San Marcos, TX, United States', + }), ).toBeInTheDocument() expect(container.querySelectorAll('[aria-live]')).toHaveLength(1) expect(container.querySelector('[aria-live]')).toBe(announcement) - expect(announcement).toHaveTextContent('San Marcos, TX, US') + expect(announcement).toHaveTextContent('San Marcos, TX, United States') expect(screen.getByRole('article')).toBe(content) expect(content).toHaveAttribute('aria-busy', 'false') expect(screen.getByText('45 min')).toBeInTheDocument() @@ -92,7 +94,7 @@ describe('SightingDetailPage', () => { 'true', ) expect(screen.getByTestId('detail-map')).toHaveAttribute('data-zoom', '11') - expect(document.title).toBe('San Marcos, TX, US · UFOund it') + expect(document.title).toBe('San Marcos, TX, United States · UFOund it') }) it('shows fallbacks for a missing shape and hides missing comments', async () => { @@ -107,7 +109,9 @@ describe('SightingDetailPage', () => { ]) expect( - await screen.findByRole('heading', { name: 'San Marcos, TX, US' }), + await screen.findByRole('heading', { + name: 'San Marcos, TX, United States', + }), ).toBeInTheDocument() expect(screen.getByText('Unknown')).toBeInTheDocument() expect(screen.queryByText(/This event took place/)).not.toBeInTheDocument() @@ -159,7 +163,7 @@ describe('SightingDetailPage', () => { expect(await screen.findByText('User submitted')).toBeInTheDocument() // Outside the heading, so the page title is still just the location. expect( - screen.getByRole('heading', { name: 'San Marcos, TX, US' }), + screen.getByRole('heading', { name: 'San Marcos, TX, United States' }), ).toBeInTheDocument() }) @@ -168,7 +172,9 @@ describe('SightingDetailPage', () => { renderDetailPage('/sightings/1', [found]) expect( - await screen.findByRole('heading', { name: 'San Marcos, TX, US' }), + await screen.findByRole('heading', { + name: 'San Marcos, TX, United States', + }), ).toBeInTheDocument() expect(screen.queryByText('User submitted')).not.toBeInTheDocument() }) @@ -241,11 +247,13 @@ describe('SightingDetailPage', () => { expect(screen.queryByRole('alert')).not.toBeInTheDocument() expect( - await screen.findByRole('heading', { name: 'San Marcos, TX, US' }), + await screen.findByRole('heading', { + name: 'San Marcos, TX, United States', + }), ).toBeInTheDocument() expect(container.querySelectorAll('[aria-live]')).toHaveLength(1) expect(container.querySelector('[aria-live]')).toBe(announcement) - expect(announcement).toHaveTextContent('San Marcos, TX, US') + expect(announcement).toHaveTextContent('San Marcos, TX, United States') expect(screen.getByRole('article')).toHaveAttribute('aria-busy', 'false') }) }) diff --git a/frontend/src/tests/SightingList.test.tsx b/frontend/src/tests/SightingList.test.tsx index cca71f1..db62a32 100644 --- a/frontend/src/tests/SightingList.test.tsx +++ b/frontend/src/tests/SightingList.test.tsx @@ -46,7 +46,7 @@ describe('SightingList', () => { expect(screen.getByText('1 result')).toBeInTheDocument() expect( - screen.getByRole('link', { name: 'San Marcos, TX, US' }), + screen.getByRole('link', { name: 'San Marcos, TX, United States' }), ).toHaveAttribute('href', '/sightings/1') expect(screen.getByText('cylinder')).toBeInTheDocument() expect(screen.getByText('45 min')).toBeInTheDocument() @@ -67,7 +67,9 @@ describe('SightingList', () => { expect(screen.getAllByText('User submitted')).toHaveLength(1) // The badge is not part of the link, so its name stays the location. - const link = screen.getByRole('link', { name: 'San Marcos, TX, US' }) + const link = screen.getByRole('link', { + name: 'San Marcos, TX, United States', + }) expect(link.closest('li')).toHaveTextContent('User submitted') }) @@ -184,7 +186,7 @@ describe('SightingList', () => { expect(screen.getByRole('list')).toHaveAttribute('aria-busy', 'true') expect( - screen.getByRole('link', { name: 'San Marcos, TX, US' }), + screen.getByRole('link', { name: 'San Marcos, TX, United States' }), ).toBeInTheDocument() }) diff --git a/frontend/src/tests/SightingMap.test.tsx b/frontend/src/tests/SightingMap.test.tsx index bb23a95..c22641c 100644 --- a/frontend/src/tests/SightingMap.test.tsx +++ b/frontend/src/tests/SightingMap.test.tsx @@ -87,7 +87,9 @@ describe('SightingMap', () => { 'data-position', `${sighting.latitude},${sighting.longitude}`, ) - expect(screen.getByText('San Marcos, TX, US')).toBeInTheDocument() + expect( + screen.getByText('San Marcos, TX, United States'), + ).toBeInTheDocument() }) it('shows a placeholder instead of a map when coordinates are missing', () => { @@ -126,7 +128,7 @@ describe('SightingMap', () => { expect(leafletContainer).toHaveAttribute('role', 'region') expect(leafletContainer).toHaveAttribute( 'aria-label', - 'Map showing San Marcos, TX, US', + 'Map showing San Marcos, TX, United States', ) }) }) diff --git a/frontend/src/tests/SubmitSightingPage.test.tsx b/frontend/src/tests/SubmitSightingPage.test.tsx index d6f67b2..3016ae0 100644 --- a/frontend/src/tests/SubmitSightingPage.test.tsx +++ b/frontend/src/tests/SubmitSightingPage.test.tsx @@ -532,7 +532,7 @@ describe('SubmitSightingPage', () => { expect(screen.getByText('13 results')).toBeInTheDocument(), ) expect( - screen.getByRole('link', { name: 'trondheim, NO' }), + screen.getByRole('link', { name: 'trondheim, Norway' }), ).toBeInTheDocument() }) }) diff --git a/frontend/src/tests/format-sighting.test.ts b/frontend/src/tests/format-sighting.test.ts index 32f8c51..75ec312 100644 --- a/frontend/src/tests/format-sighting.test.ts +++ b/frontend/src/tests/format-sighting.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import type { UfoSighting } from '../types/ufo-sighting' import { formatCoordinates, + formatCountry, formatDate, formatDuration, formatLocation, @@ -30,10 +31,29 @@ describe('sighting formatters', () => { expect(formatDuration(125)).toBe('2 min 5 sec') }) - it('formats a location with available region codes in uppercase', () => { - expect(formatLocation(sighting)).toBe('Oslo, OS, NO') - expect(formatLocation({ ...sighting, state: '' })).toBe('Oslo, NO') - expect(formatLocation({ ...sighting, state: '', country: '' })).toBe('Oslo') + it('names a known country code in either case', () => { + expect(formatCountry('no')).toBe('Norway') + expect(formatCountry('GB')).toBe('United Kingdom') + }) + + it('shows a code that is not a country as it is, in uppercase', () => { + expect(formatCountry('eu')).toBe('EU') + expect(formatCountry('xx')).toBe('XX') + }) + + it('says when the country is missing', () => { + expect(formatCountry(null)).toBe('Unknown country') + expect(formatCountry('')).toBe('Unknown country') + expect(formatCountry(' ')).toBe('Unknown country') + }) + + it('formats a location with the state code and the country name', () => { + expect(formatLocation(sighting)).toBe('Oslo, OS, Norway') + expect(formatLocation({ ...sighting, state: '' })).toBe('Oslo, Norway') + expect(formatLocation({ ...sighting, country: 'xx' })).toBe('Oslo, OS, XX') + expect(formatLocation({ ...sighting, state: null, country: null })).toBe( + 'Oslo, Unknown country', + ) }) it('falls back to readable text when the API returns null', () => { @@ -42,7 +62,7 @@ describe('sighting formatters', () => { expect(formatDuration(null)).toBe('Unknown') expect(formatShape(null)).toBe('Unknown') expect(formatLocation({ ...sighting, city: null })).toBe( - 'Unknown location, OS, NO', + 'Unknown location, OS, Norway', ) expect( formatLocation({ ...sighting, city: null, state: null, country: null }), diff --git a/frontend/src/utils/format-sighting.ts b/frontend/src/utils/format-sighting.ts index 8a20c2c..f427228 100644 --- a/frontend/src/utils/format-sighting.ts +++ b/frontend/src/utils/format-sighting.ts @@ -1,4 +1,5 @@ import type { UfoSighting } from '../types/ufo-sighting' +import { countryNameFromCode } from './countries' export function formatDate(date: string | null) { if (!date) return 'Unknown date' @@ -21,14 +22,27 @@ export function formatDuration(seconds: number | null) { return `${minutes} min ${remainingSeconds} sec` } +/** + * Names a country code, e.g. "no" as "Norway". A code that is not an ISO + * country stays visible in uppercase instead of being hidden behind a + * guessed name, and a missing one says so. + */ +export function formatCountry(code: string | null) { + if (!code?.trim()) return 'Unknown country' + return countryNameFromCode(code.trim()) ?? code.trim().toUpperCase() +} + export function formatLocation(sighting: UfoSighting) { - const city = sighting.city ?? 'Unknown location' - const region = [sighting.state, sighting.country] + const { city, state, country } = sighting + if (!city && !state && !country) return 'Unknown location' + + return [ + city ?? 'Unknown location', + state?.toUpperCase(), + formatCountry(country), + ] .filter(Boolean) .join(', ') - .toUpperCase() - - return region ? `${city}, ${region}` : city } export function formatShape(shape: string | null) {