update tests
This commit is contained in:
@@ -402,3 +402,163 @@ test('route control closes on escape and reports local storage failures', async
|
|||||||
harness.globalListeners.get('pageshow')?.({});
|
harness.globalListeners.get('pageshow')?.({});
|
||||||
assert.ok(harness.node('#route-control-local-list').appended.join('').includes('LocalStorage nicht lesbar'));
|
assert.ok(harness.node('#route-control-local-list').appended.join('').includes('LocalStorage nicht lesbar'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function selectServer(setup) {
|
||||||
|
const button = new FakeNode('server-select');
|
||||||
|
button.dataValues.set('route-id', 'server-1');
|
||||||
|
await setup.harness.activate('[data-route-select]', button);
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeSensors(setup) {
|
||||||
|
const calls = { stopped: 0, vibrations: 0, audio: [], slides: [] };
|
||||||
|
const ns = setup.context.Wegwichtel;
|
||||||
|
ns.Geo.start = (position, error) => { calls.position = position; calls.error = error; };
|
||||||
|
ns.Geo.stop = () => { calls.stopped += 1; };
|
||||||
|
ns.Orientation.start = async heading => { calls.heading = heading; return true; };
|
||||||
|
ns.Orientation.stop = () => { calls.stopped += 1; };
|
||||||
|
ns.Vibration.start = () => { calls.vibrations += 1; };
|
||||||
|
ns.AudioPlayer.load = url => calls.audio.push(url);
|
||||||
|
ns.Slideshow.show = images => calls.slides.push(images);
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('tracking triggers each POI once, records media progress and pauses/resumes/stops sensors', async () => {
|
||||||
|
const setup = await loadApp();
|
||||||
|
const { context, harness, savedStates } = setup;
|
||||||
|
const calls = observeSensors(setup);
|
||||||
|
await context.Wegwichtel.App.initialize({ markStep() {} });
|
||||||
|
await selectServer(setup);
|
||||||
|
await harness.activate('#start-route', new FakeNode('start'));
|
||||||
|
assert.equal(harness.node('#pause-route').properties.get('disabled'), false);
|
||||||
|
calls.position({ lat: 50, lon: 10, accuracy: 5, heading: 90, speed: 1 });
|
||||||
|
assert.equal(harness.node('#poi-title').textValue, 'Serverweg Start');
|
||||||
|
assert.equal(calls.vibrations, 1);
|
||||||
|
assert.match(harness.node('#navigation-mode').textValue, /Serverweg Ziel/);
|
||||||
|
calls.position({ lat: 50, lon: 10, accuracy: 5 });
|
||||||
|
assert.equal(calls.vibrations, 1);
|
||||||
|
harness.$('#poi-audio').trigger('play');
|
||||||
|
assert.deepEqual(Array.from(savedStates.at(-1)[5]), [1]);
|
||||||
|
calls.heading(350);
|
||||||
|
calls.heading(10);
|
||||||
|
assert.match(harness.node('#navigation-arrow').styles.get('transform'), /rotate\(50\.0deg\)/);
|
||||||
|
calls.error(new Error('Signal verloren'));
|
||||||
|
assert.equal(harness.node('#tracking-status').textValue, 'GPS-Fehler: Signal verloren');
|
||||||
|
await harness.activate('#pause-route', new FakeNode('pause'));
|
||||||
|
assert.match(harness.node('#tracking-status').textValue, /pausiert/);
|
||||||
|
assert.equal(savedStates.at(-1)[1], 'paused');
|
||||||
|
await harness.activate('#start-route', new FakeNode('resume'));
|
||||||
|
calls.position({ lat: 50.001, lon: 10.001, accuracy: 5 });
|
||||||
|
assert.equal(calls.vibrations, 2);
|
||||||
|
assert.equal(harness.node('#route-control-next').textValue, 'Alle Streckenpunkte erreicht');
|
||||||
|
assert.equal(harness.node('#navigation-distance').textValue, '0 m');
|
||||||
|
await harness.activate('#stop-route', new FakeNode('stop'));
|
||||||
|
assert.equal(savedStates.at(-1)[1], 'idle');
|
||||||
|
assert.equal(harness.node('#route-control-title').textValue, 'Route wählen');
|
||||||
|
assert.equal(harness.node('#navigation-footer').properties.get('hidden'), true);
|
||||||
|
assert.ok(calls.stopped >= 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saved route resumes paused, manual POI viewing and lifecycle events persist progress', async () => {
|
||||||
|
const setup = await loadApp();
|
||||||
|
const { context, harness, savedStates } = setup;
|
||||||
|
context.Wegwichtel.PrivacyStorage.activateRoute = () => ({
|
||||||
|
routeState: 'running', routeProgressIndex: 0,
|
||||||
|
visitedPoiIds: [1], viewedPoiIds: [1], playedAudioPoiIds: [1]
|
||||||
|
});
|
||||||
|
const calls = observeSensors(setup);
|
||||||
|
await context.Wegwichtel.App.initialize({ markStep() {} });
|
||||||
|
await selectServer(setup);
|
||||||
|
assert.match(harness.node('#tracking-status').textValue, /Gespeicherter/);
|
||||||
|
assert.equal(harness.node('#start-route').attributes.get('title'), 'Route fortsetzen');
|
||||||
|
const poi = new FakeNode('poi');
|
||||||
|
poi.dataValues.set('poi-id', 2);
|
||||||
|
await harness.key('[data-poi-select]', poi, ' ');
|
||||||
|
assert.equal(harness.node('#poi-title').textValue, 'Serverweg Ziel');
|
||||||
|
assert.deepEqual(Array.from(savedStates.at(-1)[4]), [1, 2]);
|
||||||
|
await harness.activate('#start-route', new FakeNode('resume'));
|
||||||
|
calls.position({ lat: 50, lon: 10, accuracy: 5 });
|
||||||
|
assert.equal(calls.vibrations, 0);
|
||||||
|
harness.globalListeners.get('pagehide')();
|
||||||
|
harness.document.visibilityState = 'hidden';
|
||||||
|
harness.document.dispatchEvent(new Event('visibilitychange'));
|
||||||
|
const enabled = new Event('wegwichtel:progress-storage-changed');
|
||||||
|
Object.defineProperty(enabled, 'detail', { value: { enabled: true } });
|
||||||
|
const before = savedStates.length;
|
||||||
|
harness.document.dispatchEvent(enabled);
|
||||||
|
assert.equal(savedStates.length, before + 1);
|
||||||
|
harness.globalListeners.get('popstate')({ state: { page: '#route-page' } });
|
||||||
|
assert.equal(harness.node('#route-page').attributes.has('hidden'), false);
|
||||||
|
await harness.activate('#stop-route', new FakeNode('stop'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tracking handles an empty route and orientation permission resolving after pause', async () => {
|
||||||
|
const setup = await loadApp();
|
||||||
|
const { context, harness } = setup;
|
||||||
|
setup.serverRoute.pois = [];
|
||||||
|
setup.serverRoute.points = [];
|
||||||
|
const calls = observeSensors(setup);
|
||||||
|
let grant;
|
||||||
|
context.Wegwichtel.Orientation.start = () => new Promise(resolve => { grant = resolve; });
|
||||||
|
await context.Wegwichtel.App.initialize({ markStep() {} });
|
||||||
|
await selectServer(setup);
|
||||||
|
assert.match(harness.node('#poi-list').appended.join(''), /keine Streckenpunkte/);
|
||||||
|
await harness.activate('#start-route', new FakeNode('start'));
|
||||||
|
calls.position({ lat: 50, lon: 10, accuracy: 10 });
|
||||||
|
assert.equal(harness.node('#navigation-mode').textValue, 'Keine Navigationspunkte vorhanden');
|
||||||
|
await harness.activate('#pause-route', new FakeNode('pause'));
|
||||||
|
const stopped = calls.stopped;
|
||||||
|
grant(true);
|
||||||
|
await tick();
|
||||||
|
assert.equal(calls.stopped, stopped + 1);
|
||||||
|
assert.equal(harness.node('#navigation-mode').textValue, 'Route pausiert');
|
||||||
|
await harness.activate('#stop-route', new FakeNode('stop'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('route recommendations and accent-insensitive search use location without changing selection', async () => {
|
||||||
|
const setup = await loadApp();
|
||||||
|
const { context, harness } = setup;
|
||||||
|
context.Wegwichtel.Geo.current = async () => ({ lat: 50, lon: 10, accuracy: 8 });
|
||||||
|
const far = route('far', 'Äpfelweg');
|
||||||
|
far.start = { lat: 51, lon: 10 };
|
||||||
|
context.Wegwichtel.Api.routes = async () => ({ routes: [far, setup.serverRoute, null, {}] });
|
||||||
|
await context.Wegwichtel.App.initialize({ markStep() {} });
|
||||||
|
assert.match(harness.node('#location-hint').textValue, /Standort ermittelt/);
|
||||||
|
assert.match(harness.node('#nearby-route-list').appended.join(''), /Serverweg/);
|
||||||
|
assert.doesNotMatch(harness.node('#nearby-route-list').appended.join(''), /Äpfelweg/);
|
||||||
|
const position = context.Wegwichtel.App.currentPosition();
|
||||||
|
position.lat = 0;
|
||||||
|
assert.equal(context.Wegwichtel.App.currentPosition().lat, 50);
|
||||||
|
harness.$('#route-search').val('apfel').trigger('input');
|
||||||
|
assert.match(harness.node('#all-routes-summary').textValue, /1 von 2/);
|
||||||
|
let suggestions;
|
||||||
|
const autocomplete = harness.node('#route-search').autocompleteOptions;
|
||||||
|
autocomplete.source({ term: 'apfel' }, items => { suggestions = items; });
|
||||||
|
assert.equal(suggestions[0].value, 'Äpfelweg');
|
||||||
|
autocomplete.select({}, { item: { value: 'Serverweg' } });
|
||||||
|
assert.match(harness.node('#all-route-list').appended.join(''), /Serverweg/);
|
||||||
|
harness.$('#route-search').val('unbekannt').trigger('input');
|
||||||
|
assert.match(harness.node('#all-route-list').appended.join(''), /Keine Route entspricht/);
|
||||||
|
await harness.activate('#clear-route-search', new FakeNode('clear'));
|
||||||
|
assert.equal(harness.node('#route-search').value, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid API responses reject initialization and route selection errors restore the button', async () => {
|
||||||
|
const setup = await loadApp();
|
||||||
|
const { context, harness } = setup;
|
||||||
|
context.Wegwichtel.Api.routes = async () => ({ routes: null });
|
||||||
|
await assert.rejects(context.Wegwichtel.App.initialize({ markStep() {} }), /gültige Routenliste/);
|
||||||
|
const errors = [];
|
||||||
|
context.alert = message => errors.push(message);
|
||||||
|
context.Wegwichtel.Api.route = async () => { throw { responseJSON: { message: 'Strecke entfernt' } }; };
|
||||||
|
const button = new FakeNode('missing');
|
||||||
|
button.dataValues.set('route-id', 'missing');
|
||||||
|
await harness.activate('[data-route-select]', button, { isPrimary: false });
|
||||||
|
assert.equal(errors.length, 0);
|
||||||
|
await harness.activate('[data-route-select]', button, { pointerType: 'mouse', button: 2 });
|
||||||
|
assert.equal(errors.length, 0);
|
||||||
|
await harness.key('[data-route-select]', button, 'Escape');
|
||||||
|
assert.equal(errors.length, 0);
|
||||||
|
await harness.activate('[data-route-select]', button);
|
||||||
|
assert.deepEqual(errors, ['Strecke entfernt']);
|
||||||
|
assert.equal(button.properties.get('disabled'), false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
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;
|
||||||
|
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(), /keine Geolokalisierung/);
|
||||||
|
const errors = [];
|
||||||
|
ns.Geo.start(() => assert.fail('unexpected position'), error => errors.push(error));
|
||||||
|
assert.match(errors[0].message, /keine Geolokalisierung/);
|
||||||
|
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');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user