import { afterEach, describe, expect, it, vi } from 'vitest'; import { DEFAULT_STYLE_ID, REMOTE_STYLE_BASE, STYLES, isStyleId, rewriteStyle, styleUrl, styleUrlFor } from './mapStyles'; // Mirrors the real OFM style JSONs (verified against tiles.openfreemap.org): same // glyphs/sprite values and source shapes, no attribution anywhere. const RAW_OFM_STYLE = { version: 8, glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf', sprite: 'https://tiles.openfreemap.org/sprites/ofm_f384/ofm', sources: { openmaptiles: { type: 'vector', url: 'https://tiles.openfreemap.org/planet' }, ne2_shaded: { maxzoom: 6, tileSize: 256, tiles: ['https://tiles.openfreemap.org/natural_earth/ne2sr/{z}/{x}/{y}.png'], type: 'raster', }, }, layers: [], }; describe('STYLES registry', () => { it('lists exactly the five OFM styles', () => { expect(STYLES.map((s) => s.id)).toEqual(['liberty', 'bright', 'positron', 'dark', 'fiord']); for (const s of STYLES) expect(s.label.length).toBeGreaterThan(0); }); it('defaults to fiord', () => { expect(DEFAULT_STYLE_ID).toBe('fiord'); }); it('isStyleId accepts only registered ids and styleUrl points at local files', () => { for (const s of STYLES) { expect(isStyleId(s.id)).toBe(true); expect(styleUrl(s.id)).toBe(`/styles/${s.id}.json`); } expect(isStyleId('nope')).toBe(false); }); }); describe('rewriteStyle', () => { it('rewrites glyphs and sprite to local paths, leaves tiles remote', () => { const out = rewriteStyle(RAW_OFM_STYLE) as typeof RAW_OFM_STYLE; expect(out.glyphs).toBe('/fonts/{fontstack}/{range}.pbf'); expect(out.sprite).toBe('/sprites/ofm'); // Vector + raster tile urls must stay remote (tiles are not self-hosted yet). const vector = out.sources.openmaptiles as { url: string }; expect(vector.url).toContain('tiles.openfreemap.org/planet'); const raster = out.sources.ne2_shaded as { tiles: string[] }; expect(raster.tiles[0]).toContain('tiles.openfreemap.org/natural_earth/ne2sr/'); }); it('adds OSM attribution to every source lacking one, without touching others', () => { const out = rewriteStyle(RAW_OFM_STYLE) as typeof RAW_OFM_STYLE; for (const src of Object.values(out.sources) as Array>) { expect(src.attribution).toContain('OpenStreetMap'); } // A source that already has an attribution keeps it. const withAttr = { ...RAW_OFM_STYLE, sources: { a: { type: 'vector', url: 'x', attribution: 'custom' } }, }; const out2 = rewriteStyle(withAttr) as { sources: Record }; expect(out2.sources.a.attribution).toBe('custom'); }); it('returns non-objects unchanged and does not mutate the input', () => { expect(rewriteStyle(null)).toBeNull(); expect(rewriteStyle('nope')).toBe('nope'); const before = JSON.stringify(RAW_OFM_STYLE); rewriteStyle(RAW_OFM_STYLE); expect(JSON.stringify(RAW_OFM_STYLE)).toBe(before); }); }); describe('styleUrlFor', () => { it('points at the local style file when the asset pack is available', () => { for (const s of STYLES) { expect(styleUrlFor(s.id, true)).toBe(`/styles/${s.id}.json`); } }); it('points at the exact remote OFM style url otherwise', () => { for (const s of STYLES) { expect(styleUrlFor(s.id, false)).toBe(`${REMOTE_STYLE_BASE}/${s.id}`); expect(styleUrlFor(s.id, false)).toBe(`https://tiles.openfreemap.org/styles/${s.id}`); } }); }); describe('localAssetsAvailable', () => { // The probe caches its promise at module level, so each test gets a fresh module // instance (resetModules + dynamic import) with its own stubbed fetch. afterEach(() => { vi.unstubAllGlobals(); vi.resetModules(); }); it('resolves false on non-200 responses', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 404 })); const mod = await import('./mapStyles'); await expect(mod.localAssetsAvailable()).resolves.toBe(false); }); it('resolves true on 200 and caches the probe (one fetch across awaits)', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 }); vi.stubGlobal('fetch', fetchMock); const mod = await import('./mapStyles'); const [a, b] = await Promise.all([mod.localAssetsAvailable(), mod.localAssetsAvailable()]); expect(a).toBe(true); expect(b).toBe(true); expect(fetchMock).toHaveBeenCalledTimes(1); }); it('resolves false on network errors', async () => { vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('offline'))); const mod = await import('./mapStyles'); await expect(mod.localAssetsAvailable()).resolves.toBe(false); }); });