import { loadI18n } from '../test-support/i18n.js'; import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import test from 'node:test'; import vm from 'node:vm'; async function load(name, options = {}) { const filename = path.resolve(import.meta.dirname, '../public/js', name); const context = { Wegwichtel: { Config: { apiBase: '/api', routeRadiusKm: 5, geolocationTimeoutMs: 7000 } }, ...options }; context.globalThis = context; await loadI18n(context); vm.runInNewContext(await fs.readFile(filename, 'utf8'), context, { filename }); return context; } const rawPosition = { coords: { latitude: 50, longitude: 10, accuracy: 5, altitude: null, heading: 90, speed: 2 }, timestamp: 1234 }; test('GPS normalizes positions, forwards errors and clears replaced watches including watch zero', async () => { const cleared = []; let watchSuccess; let watchFailure; let currentOptions; let watchOptions; let watch = 0; const error = new Error('GPS verweigert'); const service = { getCurrentPosition(success, failure, options) { currentOptions = options; success(rawPosition); }, watchPosition(success, failure, options) { watchSuccess = success; watchFailure = failure; watchOptions = options; return watch++; }, clearWatch(id) { cleared.push(id); } }; const { Wegwichtel: ns } = await load('geolocation.js', { navigator: { geolocation: service } }); const current = await ns.Geo.current(); assert.equal(current.lat, 50); assert.equal(current.lon, 10); assert.equal(current.timestamp, 1234); assert.equal(current.altitude, null); assert.equal(currentOptions.timeout, 7000); assert.equal(currentOptions.maximumAge, 30000); service.getCurrentPosition = (success, failure) => failure(error); await assert.rejects(ns.Geo.current(), value => value === error); const positions = []; const errors = []; ns.Geo.start(value => positions.push(value), value => errors.push(value)); watchSuccess(rawPosition); watchFailure(error); assert.equal(positions[0].heading, 90); assert.equal(positions[0].speed, 2); assert.equal(watchOptions.enableHighAccuracy, true); assert.equal(watchOptions.timeout, 20000); assert.deepEqual(errors, [error]); ns.Geo.start(() => {}); assert.deepEqual(cleared, [0]); ns.Geo.stop(); ns.Geo.stop(); assert.deepEqual(cleared, [0, 1]); }); test('GPS unsupported devices reject current location and optionally report tracking errors', async () => { for (const navigator of [undefined, {}]) { const { Wegwichtel: ns } = await load('geolocation.js', { navigator }); await assert.rejects(ns.Geo.current(), /Standort nicht finden/); const errors = []; ns.Geo.start(() => assert.fail('unexpected position'), error => errors.push(error)); assert.match(errors[0].message, /Standort nicht finden/); ns.Geo.start(() => {}); ns.Geo.stop(); } }); test('orientation permission denial and rejection do not subscribe to device events', async () => { for (const DeviceOrientationEvent of [undefined, { requestPermission: async () => 'denied' }, { requestPermission: async () => { throw new Error('Permission unavailable'); } } ]) { const context = await load('orientation.js', { DeviceOrientationEvent, addEventListener() { assert.fail('unexpected orientation listener'); } }); assert.equal(await context.Wegwichtel.Orientation.start(() => {}), false); context.Wegwichtel.Orientation.stop(); } }); test('orientation normalizes compass/absolute headings, ignores relative readings and removes old listeners', async () => { for (const DeviceOrientationEvent of [{}, { requestPermission: async () => 'granted' }]) { const listeners = new Map(); const removed = []; const context = await load('orientation.js', { DeviceOrientationEvent, addEventListener(name, listener) { listeners.set(name, listener); }, removeEventListener(name, listener) { assert.equal(listeners.get(name), listener); listeners.delete(name); removed.push(name); } }); const values = []; const ns = context.Wegwichtel; assert.equal(await ns.Orientation.start(value => values.push(value)), true); listeners.get('deviceorientation')({ webkitCompassHeading: -10 }); listeners.get('deviceorientation')({ absolute: true, alpha: 90 }); listeners.get('deviceorientationabsolute')({ type: 'deviceorientationabsolute', alpha: 370 }); listeners.get('deviceorientation')({ alpha: 90, absolute: false }); listeners.get('deviceorientation')({ absolute: true, alpha: Number.NaN }); assert.deepEqual(values, [350, 270, 350]); await ns.Orientation.start(value => values.push(value)); assert.equal(removed.length, 2); ns.Orientation.stop(); assert.equal(listeners.size, 0); ns.Orientation.stop(); assert.equal(removed.length, 4); } }); test('API requests restrict listing to released routes, encode identifiers and propagate failures', async () => { const requests = []; const context = await load('api-client.js', { URLSearchParams, jQuery: { ajax(options) { requests.push(options); return Promise.resolve({ ok: true }); } } }); const api = context.Wegwichtel.Api; await api.health(); await api.routes(null); await api.routes({ lat: 0, lon: -10 }); await api.route('a/b ?'); assert.deepEqual(requests.map(value => value.url), [ '/api/health', '/api/routes?activatedOnly=true', '/api/routes?activatedOnly=true&lat=0&lon=-10&radiusKm=5', '/api/routes/a%2Fb%20%3F' ]); for (const request of requests) { assert.equal(request.method, 'GET'); assert.equal(request.dataType, 'json'); assert.equal(request.cache, false); } const error = new Error('Server nicht erreichbar'); context.jQuery.ajax = () => Promise.reject(error); await assert.rejects(api.health(), value => value === error); }); test('distance calculates cardinal bearings, geographic distance and the meter/kilometer boundary', async () => { const { Wegwichtel: { Distance } } = await load('distance.js'); const origin = { lat: 0, lon: 0 }; assert.equal(Distance.meters(origin, origin), 0); assert.ok(Math.abs(Distance.meters(origin, { lat: 1, lon: 0 }) - 111195) < 1); assert.equal(Distance.bearing(origin, { lat: 1, lon: 0 }), 0); assert.equal(Distance.bearing(origin, { lat: 0, lon: 1 }), 90); assert.equal(Distance.bearing(origin, { lat: -1, lon: 0 }), 180); assert.equal(Distance.bearing(origin, { lat: 0, lon: -1 }), 270); assert.equal(Distance.normalizeDegrees(-370), 350); assert.equal(Distance.format(999), '999 m'); assert.equal(Distance.format(1000), '1.0 km'); });