125 lines
4.7 KiB
TypeScript
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);
|
|
});
|
|
});
|