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) }) })