Files

58 lines
3.8 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createServices } from './services.mjs';
import { distance, validateZones, nearbyZones, routeProgress, instruction } from '../ui/geo.js';
import { palette } from '../ui/theme.js';
test('geographic distances, segment projection and French instructions', () => {
assert.equal(distance({lat:48,lon:2},{lat:48,lon:2}), 0);
assert.ok(Math.abs(distance({lat:0,lon:0},{lat:0,lon:1}) - 111195) < 1);
const p = routeProgress([[2,48],[2.1,48]], {lon:2.05,lat:48});
assert.ok(p.away < 1 && Math.abs(p.fraction - .5) < .001);
assert.match(instruction({maneuver:{type:'turn',modifier:'left'},name:'Rue A'}), /gauche.*Rue A/);
});
const now = Date.UTC(2026,8,12);
const collection = {type:'FeatureCollection',features:[{type:'Feature',geometry:{type:'Point',coordinates:[2,48]},properties:{id:'a',country:'FR',radiusM:1000,source:'Test',updatedAt:'2026-09-01',expiresAt:'2026-10-01'}}]};
test('zone validation and alerts exclude expired, foreign, stale and inaccurate positions', () => {
const zones = validateZones(collection);
const gps = {lat:48,lon:2,source:'gps',accuracy:10,timestamp:now};
assert.equal(nearbyZones(zones,gps,'FR',now).length,1);
for (const change of [{source:'manual'},{accuracy:200},{timestamp:now-16000}]) assert.equal(nearbyZones(zones,{...gps,...change},'FR',now).length,0);
assert.equal(nearbyZones(zones,gps,'BE',now).length,0);
assert.equal(nearbyZones(zones,{...gps,timestamp:now+40*86400000},'FR',now+40*86400000).length,0);
assert.throws(()=>validateZones({type:'FeatureCollection',features:[{}]}));
const invalid = structuredClone(collection); invalid.features[0].properties.radiusM = 0;
assert.throws(()=>validateZones(invalid));
});
test('custom extreme colors are adjusted for light and dark contrast', () => {
assert.notEqual(palette('#ffffff',true).accent,'#ffffff');
assert.notEqual(palette('#000000',false).accent,'#000000');
assert.equal(palette('invalid').accent,palette('#d2ef92').accent);
});
test('weather cache, coordinate validation, upstream errors, search normalization', async () => {
let calls = 0, time = 100000;
const service = createServices({now:()=>time,env:{},fetcher:async url=>{
calls++;
if (url.hostname.includes('photon')) return {ok:true,json:async()=>({features:[{geometry:{type:'Point',coordinates:[2,48]},properties:{name:'Paris',city:'Paris',country:'France'}}]})};
return {ok:true,json:async()=>({current:{temperature_2m:20,wind_speed_10m:5,weather_code:0,time:'2026-09-12T12:00'},daily:{temperature_2m_min:[12],temperature_2m_max:[22]}})};
}});
const params = new URLSearchParams({lat:'48',lon:'2'});
const responses = await Promise.all([service('/api/weather',params),service('/api/weather',params)]);
assert.equal(responses[0].temperature,20); assert.equal(calls,1);
await service('/api/weather',params); assert.equal(calls,1);
await assert.rejects(service('/api/weather',new URLSearchParams({lat:'',lon:'2'})),{status:400});
await assert.rejects(service('/api/weather',new URLSearchParams({lat:'91',lon:'2'})),{status:400});
await assert.rejects(service('/api/weather',new URLSearchParams({lat:'49',lon:'2'})),{status:429});
time += 2000;
const places = await service('/api/search',new URLSearchParams({q:'Paris'}));
assert.equal(places[0].label,'Paris, France');
const broken = createServices({env:{},fetcher:async()=>{throw new Error('Offline');}});
await assert.rejects(broken('/api/weather',params),{status:502});
});
test('routing coordinates and NoRoute response are handled', async () => {
let target;
const service = createServices({env:{},fetcher:async url=>{target=url;return {ok:true,json:async()=>({code:'NoRoute'})};}});
await assert.rejects(service('/api/route',new URLSearchParams({fromLat:'48',fromLon:'2',toLat:'49',toLon:'3'})),{status:422});
assert.match(target.pathname,/2,48;3,49$/);
});