Files
Flight-Ops/lib/mapStyles.test.ts
T

125 lines
4.7 KiB
TypeScript

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<Record<string, unknown>>) {
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<string, { attribution?: string }> };
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);
});
});