Skip to content

Commit 69f6b5a

Browse files
committed
Merge branch 'fix-geocode-test-flake' into fix-dashboard-clip-audio
2 parents b7a0a37 + fb59aa8 commit 69f6b5a

3 files changed

Lines changed: 156 additions & 6 deletions

File tree

.github/workflows/build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212
jobs:
1313
test:
1414
runs-on: ubuntu-latest
15-
timeout-minutes: 1
15+
timeout-minutes: 2
1616
steps:
1717
- uses: actions/checkout@v4
1818
- uses: oven-sh/setup-bun@v2

src/map/geocode.live.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, test } from 'vitest'
2+
3+
import { getFullAddress, getPlaceName, reverseGeocode } from './geocode'
4+
5+
const describeLive = process.env.RUN_LIVE_MAP_TESTS === '1' ? describe : describe.skip
6+
7+
describeLive('live geocode smoke tests', () => {
8+
test('reverseGeocode returns a feature for a known coordinate', async () => {
9+
expect(await reverseGeocode([-0.10664, 51.514209])).not.toBeNull()
10+
}, 15000)
11+
12+
test('getFullAddress returns a non-empty string', async () => {
13+
const fullAddress = await getFullAddress([-0.10664, 51.514209])
14+
expect(fullAddress).toBeTruthy()
15+
expect(typeof fullAddress).toBe('string')
16+
}, 15000)
17+
18+
test('getPlaceName returns a non-empty string', async () => {
19+
const placeName = await getPlaceName([-117.168638, 32.723695])
20+
expect(placeName).toBeTruthy()
21+
expect(typeof placeName).toBe('string')
22+
}, 15000)
23+
})

src/map/geocode.spec.ts

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,158 @@
1-
import { describe, expect, test } from 'vitest'
1+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
2+
import type { Position } from 'geojson'
23

4+
import type { ReverseGeocodingFeature, ReverseGeocodingResponse } from './api-types'
35
import { getFullAddress, getPlaceName, reverseGeocode } from './geocode'
6+
import { MAPBOX_TOKEN } from './config'
7+
8+
const fetchMock = vi.fn()
9+
10+
type MockContext = Partial<ReverseGeocodingFeature['properties']['context']>
11+
12+
function createFeature(fullAddress: string, context: MockContext = {}): ReverseGeocodingFeature {
13+
return {
14+
type: 'Feature',
15+
geometry: {
16+
type: 'Point',
17+
coordinates: [0, 0],
18+
},
19+
properties: {
20+
feature_type: 'address',
21+
name: fullAddress,
22+
name_preferred: fullAddress,
23+
place_formatted: fullAddress,
24+
full_address: fullAddress,
25+
context,
26+
},
27+
} as ReverseGeocodingFeature
28+
}
29+
30+
function createResponse(feature: ReverseGeocodingFeature): ReverseGeocodingResponse {
31+
return {
32+
type: 'FeatureCollection',
33+
attribution: 'test fixture',
34+
features: [feature],
35+
} as ReverseGeocodingResponse
36+
}
37+
38+
function coordinateKey(position: Position): string {
39+
return `${position[0].toFixed(6)},${position[1].toFixed(6)}`
40+
}
41+
42+
function mockReverseGeocode(featuresByCoordinate: Map<string, ReverseGeocodingFeature>) {
43+
fetchMock.mockImplementation(async (input: string | URL | Request) => {
44+
const rawUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
45+
const url = new URL(rawUrl)
46+
const key = `${url.searchParams.get('longitude')},${url.searchParams.get('latitude')}`
47+
const feature = featuresByCoordinate.get(key)
48+
if (!feature) throw new Error(`Unexpected reverse geocode lookup for ${key}`)
49+
50+
return new Response(JSON.stringify(createResponse(feature)), {
51+
status: 200,
52+
headers: { 'Content-Type': 'application/json' },
53+
})
54+
})
55+
}
56+
57+
beforeEach(() => {
58+
fetchMock.mockReset()
59+
vi.stubGlobal('fetch', fetchMock)
60+
})
61+
62+
afterEach(() => {
63+
vi.restoreAllMocks()
64+
vi.unstubAllGlobals()
65+
})
466

567
describe('reverseGeocode', () => {
6-
test('return null if coords are [0, 0]', async () => {
68+
test('return null if coords are [0, 0] without calling fetch', async () => {
769
expect(await reverseGeocode([0, 0])).toBeNull()
70+
expect(fetchMock).not.toHaveBeenCalled()
71+
})
72+
73+
test('return first feature from reverse geocode response', async () => {
74+
const position: Position = [-0.10664, 51.514209]
75+
const feature = createFeature('133 Fleet Street, City of London, London, EC4A 2BB, United Kingdom')
76+
mockReverseGeocode(new Map([[coordinateKey(position), feature]]))
77+
78+
expect(await reverseGeocode(position)).toEqual(feature)
79+
expect(fetchMock).toHaveBeenCalledOnce()
80+
})
81+
82+
test('request includes expected Mapbox URL, params, and cache mode', async () => {
83+
const position: Position = [-0.10664, 51.514209]
84+
const feature = createFeature('133 Fleet Street, City of London, London, EC4A 2BB, United Kingdom')
85+
mockReverseGeocode(new Map([[coordinateKey(position), feature]]))
86+
87+
await reverseGeocode(position)
88+
89+
expect(fetchMock).toHaveBeenCalledOnce()
90+
const [input, init] = fetchMock.mock.calls[0] as [string | URL | Request, RequestInit | undefined]
91+
const rawUrl = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
92+
const url = new URL(rawUrl)
93+
expect(`${url.origin}${url.pathname}`).toBe('https://api.mapbox.com/search/geocode/v6/reverse')
94+
expect(url.searchParams.get('longitude')).toBe('-0.106640')
95+
expect(url.searchParams.get('latitude')).toBe('51.514209')
96+
expect(url.searchParams.get('access_token')).toBe(MAPBOX_TOKEN)
97+
expect(init).toMatchObject({ cache: 'force-cache' })
98+
})
99+
100+
test('return null when fetch rejects', async () => {
101+
vi.spyOn(console, 'error').mockImplementation(() => {})
102+
fetchMock.mockRejectedValueOnce(new Error('network down'))
103+
104+
expect(await reverseGeocode([-0.10664, 51.514209])).toBeNull()
105+
})
106+
107+
test('return null on non-ok response', async () => {
108+
fetchMock.mockResolvedValueOnce(new Response('denied', { status: 429, statusText: 'Too Many Requests' }))
109+
110+
expect(await reverseGeocode([-0.10664, 51.514209])).toBeNull()
111+
})
112+
113+
test('return null when response json cannot be parsed', async () => {
114+
fetchMock.mockResolvedValueOnce(new Response('{', { status: 200, headers: { 'Content-Type': 'application/json' } }))
115+
116+
expect(await reverseGeocode([-0.10664, 51.514209])).toBeNull()
8117
})
9118
})
10119

11120
describe('getFullAddress', () => {
12-
test('return null if coords are [0, 0]', async () => {
121+
test('return null if coords are [0, 0] without calling fetch', async () => {
13122
expect(await getFullAddress([0, 0])).toBeNull()
123+
expect(fetchMock).not.toHaveBeenCalled()
14124
})
15125

16126
test('normal usage', async () => {
17-
// expect(await getFullAddress([-77.036574, 38.8976765])).toBe('1600 Pennsylvania Avenue Northwest, Washington, District of Columbia 20500, United States')
127+
mockReverseGeocode(
128+
new Map([
129+
[coordinateKey([-0.10664, 51.514209]), createFeature('133 Fleet Street, City of London, London, EC4A 2BB, United Kingdom')],
130+
[coordinateKey([-2.076843, 51.894799]), createFeature('4 Montpellier Drive, Cheltenham, GL50 1TX, United Kingdom')],
131+
]),
132+
)
133+
18134
expect(await getFullAddress([-0.10664, 51.514209])).toBe('133 Fleet Street, City of London, London, EC4A 2BB, United Kingdom')
19135
expect(await getFullAddress([-2.076843, 51.894799])).toBe('4 Montpellier Drive, Cheltenham, GL50 1TX, United Kingdom')
20136
})
21137
})
22138

23139
describe('getPlaceName', () => {
24-
test('return null if coords are [0, 0]', async () => {
140+
test('return null if coords are [0, 0] without calling fetch', async () => {
25141
expect(await getPlaceName([0, 0])).toBeNull()
142+
expect(fetchMock).not.toHaveBeenCalled()
26143
})
27144

28145
test('normal usage', async () => {
146+
mockReverseGeocode(
147+
new Map([
148+
[coordinateKey([-117.168638, 32.723695]), createFeature('', { neighborhood: { name: 'Little Italy' } })],
149+
[coordinateKey([-118.192757, 33.763015]), createFeature('', { place: { name: 'Downtown Long Beach' } })],
150+
[coordinateKey([-0.113643, 51.504546]), createFeature('', { neighborhood: { name: 'Waterloo' } })],
151+
[coordinateKey([5.572254, 50.64428]), createFeature('', { locality: { name: 'Liège' } })],
152+
[coordinateKey([-2.236802, 53.480931]), createFeature('', { neighborhood: { name: 'Northern Quarter' } })],
153+
]),
154+
)
155+
29156
expect(await getPlaceName([-117.168638, 32.723695])).toBe('Little Italy')
30157
expect(await getPlaceName([-118.192757, 33.763015])).toBe('Downtown Long Beach')
31158
expect(await getPlaceName([-0.113643, 51.504546])).toBe('Waterloo')

0 commit comments

Comments
 (0)