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$/); });