- Lightning layer on PrecipitationRadar: toggleable ⚡ button (persisted via showLightning setting). When on, fetches recent strikes from the planned CORS relay and renders them as markers that pan/zoom with the slippy map. - New src/lib/api/lightning.js: fetchLightningStrikes() reads https://cors.thecookiejar.me/lightning?lat&lon&radiusKm&limit (relay NOT deployed yet) with an 8s abort timeout and graceful failure - never blocks or crashes the map, shows an unavailable banner instead. Exports pure projectStrikeToLayer() for Mercator tile-layer marker projection. - showLightning setting added to DEFAULT_SETTINGS + app store; toggle persists. - Tests: fetch URL/parsing/error handling + layer projection. 71 total pass.
80 lines
3.0 KiB
JavaScript
80 lines
3.0 KiB
JavaScript
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
import { fetchLightningStrikes, projectStrikeToLayer } from '../../../src/lib/api/lightning.js'
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('fetchLightningStrikes', () => {
|
|
it('builds the relay URL with lat/lon/radius/limit', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ source: 'blitzortung', strikes: [] }),
|
|
})
|
|
globalThis.fetch = fetchMock
|
|
|
|
await fetchLightningStrikes({ lat: 36.35, lon: -95.81, radiusKm: 100, limit: 50 })
|
|
|
|
const [url] = fetchMock.mock.calls[0]
|
|
expect(url).toContain('https://cors.thecookiejar.me/lightning?')
|
|
expect(url).toContain('lat=36.35')
|
|
expect(url).toContain('lon=-95.81')
|
|
expect(url).toContain('radiusKm=100')
|
|
expect(url).toContain('limit=50')
|
|
})
|
|
|
|
it('returns the strike list on a valid response', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
source: 'blitzortung',
|
|
strikes: [
|
|
{ lat: 36.3, lon: -95.8, time: 1724671234, strength: -12 },
|
|
{ lat: 36.5, lon: -95.9, time: 1724671299 },
|
|
],
|
|
}),
|
|
})
|
|
|
|
const strikes = await fetchLightningStrikes({ lat: 36.35, lon: -95.81 })
|
|
expect(strikes).toHaveLength(2)
|
|
expect(strikes[0]).toMatchObject({ lat: 36.3, lon: -95.8, strength: -12 })
|
|
})
|
|
|
|
it('throws on a non-OK relay response', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: false, status: 502, statusText: 'Bad Gateway' })
|
|
await expect(fetchLightningStrikes({ lat: 0, lon: 0 })).rejects.toThrow(/Lightning relay error: 502/)
|
|
})
|
|
|
|
it('throws on a malformed response (no strikes array)', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ foo: 1 }) })
|
|
await expect(fetchLightningStrikes({ lat: 0, lon: 0 })).rejects.toThrow(/invalid response/)
|
|
})
|
|
|
|
it('throws when the relay is unreachable (network failure)', async () => {
|
|
globalThis.fetch = vi.fn().mockRejectedValue(new TypeError('Failed to fetch'))
|
|
await expect(fetchLightningStrikes({ lat: 0, lon: 0 })).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('projectStrikeToLayer', () => {
|
|
// Center tile is (0.5, 0.5) at zoom 0 (lon=0,lat=0). A strike exactly at the
|
|
// location should land at the center of the 5x5 layer: (2.5, 2.5) tiles.
|
|
it('maps a strike at the center location to the layer center', () => {
|
|
const { x, y } = projectStrikeToLayer(0, 0, { zoom: 0, cx: 0.5, cy: 0.5, tileSize: 256 })
|
|
expect(x).toBeCloseTo(640)
|
|
expect(y).toBeCloseTo(640)
|
|
})
|
|
|
|
it('scales with tile size', () => {
|
|
const { x } = projectStrikeToLayer(0, 0, { zoom: 0, cx: 0.5, cy: 0.5, tileSize: 128 })
|
|
expect(x).toBeCloseTo(320)
|
|
})
|
|
|
|
it('moves the strike right as lon increases', () => {
|
|
const a = projectStrikeToLayer(0, 1, { zoom: 0, cx: 0.5, cy: 0.5, tileSize: 256 })
|
|
const b = projectStrikeToLayer(0, 5, { zoom: 0, cx: 0.5, cy: 0.5, tileSize: 256 })
|
|
expect(a.x).toBeGreaterThan(640)
|
|
expect(b.x).toBeGreaterThan(a.x)
|
|
})
|
|
})
|