643 lines
26 KiB
JavaScript
643 lines
26 KiB
JavaScript
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';
|
||
|
||
const root = path.resolve(import.meta.dirname, '..');
|
||
|
||
function tick() {
|
||
return new Promise(resolve => setTimeout(resolve, 0));
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
class FakeNode {
|
||
constructor(selector = '') {
|
||
this.selector = selector;
|
||
this.properties = new Map();
|
||
this.attributes = new Map();
|
||
this.dataValues = new Map();
|
||
this.styles = new Map();
|
||
this.classes = new Set();
|
||
this.appended = [];
|
||
this.textValue = '';
|
||
this.value = '';
|
||
this.autocompleteOptions = null;
|
||
this.handlers = new Map();
|
||
}
|
||
}
|
||
|
||
class FakeJQuery {
|
||
constructor(nodes, harness) {
|
||
this.nodes = nodes;
|
||
this.harness = harness;
|
||
this.length = nodes.length;
|
||
}
|
||
|
||
_each(callback) {
|
||
this.nodes.forEach(callback);
|
||
return this;
|
||
}
|
||
|
||
text(value) {
|
||
if (value === undefined) return this.nodes[0]?.textValue ?? '';
|
||
return this._each(node => { node.textValue = String(value); });
|
||
}
|
||
|
||
html(value) {
|
||
if (value === undefined) return escapeHtml(this.nodes[0]?.textValue ?? '');
|
||
return this._each(node => { node.textValue = String(value); });
|
||
}
|
||
|
||
prop(name, value) {
|
||
if (value === undefined) return this.nodes[0]?.properties.get(name);
|
||
return this._each(node => node.properties.set(name, value));
|
||
}
|
||
|
||
attr(name, value) {
|
||
if (typeof name === 'object' && name !== null) {
|
||
return this._each(node => {
|
||
Object.entries(name).forEach(([key, item]) => node.attributes.set(key, String(item)));
|
||
});
|
||
}
|
||
if (value === undefined) return this.nodes[0]?.attributes.get(name);
|
||
return this._each(node => node.attributes.set(name, String(value)));
|
||
}
|
||
|
||
removeAttr(name) {
|
||
return this._each(node => node.attributes.delete(name));
|
||
}
|
||
|
||
addClass(name) {
|
||
return this._each(node => node.classes.add(name));
|
||
}
|
||
|
||
removeClass(name) {
|
||
return this._each(node => node.classes.delete(name));
|
||
}
|
||
|
||
css(name, value) {
|
||
if (value === undefined) return this.nodes[0]?.styles.get(name);
|
||
return this._each(node => node.styles.set(name, value));
|
||
}
|
||
|
||
empty() {
|
||
return this._each(node => { node.appended = []; });
|
||
}
|
||
|
||
append(value) {
|
||
return this._each(node => node.appended.push(String(value)));
|
||
}
|
||
|
||
val(value) {
|
||
if (value === undefined) return this.nodes[0]?.value ?? '';
|
||
return this._each(node => { node.value = String(value); });
|
||
}
|
||
|
||
data(name, value) {
|
||
if (value === undefined) return this.nodes[0]?.dataValues.get(name);
|
||
return this._each(node => node.dataValues.set(name, value));
|
||
}
|
||
|
||
on(eventName, selector, handler) {
|
||
if (typeof selector === 'function') {
|
||
this.nodes.forEach(node => {
|
||
if (!node.handlers.has(eventName)) node.handlers.set(eventName, []);
|
||
node.handlers.get(eventName).push(selector);
|
||
});
|
||
return this;
|
||
}
|
||
this.harness.delegated.set(`${eventName}:${selector}`, handler);
|
||
return this;
|
||
}
|
||
|
||
trigger(eventName) {
|
||
this.nodes.forEach(node => {
|
||
for (const handler of node.handlers.get(eventName) || []) {
|
||
handler.call(node, { type: eventName });
|
||
}
|
||
});
|
||
const delegated = this.harness.delegated.get(`${eventName}:${this.nodes[0]?.selector}`);
|
||
if (delegated) delegated.call(this.nodes[0], { type: eventName, preventDefault() {} });
|
||
return this;
|
||
}
|
||
|
||
autocomplete(options) {
|
||
return this._each(node => { node.autocompleteOptions = options; });
|
||
}
|
||
|
||
find(selector) {
|
||
return this.harness.$(`${this.nodes[0]?.selector || ''} ${selector}`);
|
||
}
|
||
|
||
closest(selector) {
|
||
const node = this.nodes[0];
|
||
if (!node) return new FakeJQuery([], this.harness);
|
||
if (selector === '.route-control' && node.classes.has('route-control')) return this;
|
||
return new FakeJQuery([], this.harness);
|
||
}
|
||
}
|
||
|
||
function createHarness() {
|
||
const document = new EventTarget();
|
||
Object.defineProperty(document, 'visibilityState', { value: 'visible', writable: true });
|
||
const nodes = new Map();
|
||
const delegated = new Map();
|
||
const documentNode = new FakeNode('document');
|
||
const globalListeners = new Map();
|
||
|
||
const harness = {
|
||
delegated,
|
||
document,
|
||
globalListeners,
|
||
node(selector) {
|
||
if (!nodes.has(selector)) nodes.set(selector, new FakeNode(selector));
|
||
return nodes.get(selector);
|
||
},
|
||
$(selector) {
|
||
if (selector === document) return new FakeJQuery([documentNode], harness);
|
||
if (selector instanceof FakeNode) return new FakeJQuery([selector], harness);
|
||
if (typeof selector === 'string' && selector.startsWith('<')) {
|
||
return new FakeJQuery([new FakeNode(selector)], harness);
|
||
}
|
||
return new FakeJQuery([harness.node(String(selector))], harness);
|
||
},
|
||
async activate(selector, target, pointer = { isPrimary: true, pointerType: 'touch', button: 0 }) {
|
||
const handler = delegated.get(`pointerup:${selector}`);
|
||
assert.ok(handler, `missing pointer handler for ${selector}`);
|
||
const event = {
|
||
originalEvent: pointer,
|
||
preventDefault() {}
|
||
};
|
||
handler.call(target, event);
|
||
await tick();
|
||
await tick();
|
||
},
|
||
async key(selector, target, key) {
|
||
const handler = delegated.get(`keydown:${selector}`);
|
||
assert.ok(handler, `missing key handler for ${selector}`);
|
||
handler.call(target, { key, preventDefault() {} });
|
||
await tick();
|
||
await tick();
|
||
},
|
||
fireDocumentPointer(target) {
|
||
const handlers = documentNode.handlers.get('pointerup') || [];
|
||
handlers.forEach(handler => handler({ target }));
|
||
},
|
||
fireDocumentKey(key) {
|
||
const handlers = documentNode.handlers.get('keydown') || [];
|
||
handlers.forEach(handler => handler({ key }));
|
||
}
|
||
};
|
||
|
||
return harness;
|
||
}
|
||
|
||
function route(id, name, source = 'server') {
|
||
return {
|
||
id,
|
||
name,
|
||
source,
|
||
local: source === 'local',
|
||
schoolName: source === 'local' ? 'Lokal gespeichert' : 'Grundschule Herpf',
|
||
description: `${name} Beschreibung`,
|
||
start: { lat: 50, lon: 10 },
|
||
distanceM: 1000,
|
||
elevationGainM: 15,
|
||
points: [
|
||
{ lat: 50, lon: 10 },
|
||
{ lat: 50.001, lon: 10.001 }
|
||
],
|
||
pois: [
|
||
{ id: 1, title: `${name} Start`, description: 'Los', lat: 50, lon: 10, sequence: 0, triggerRadiusM: 25, images: [], audioUrl: null },
|
||
{ id: 2, title: `${name} Ziel`, description: 'Ziel', lat: 50.001, lon: 10.001, sequence: 1, triggerRadiusM: 25, images: [], audioUrl: null }
|
||
]
|
||
};
|
||
}
|
||
|
||
async function loadApp({ language = 'de-DE' } = {}) {
|
||
const sourcePath = path.join(root, 'public/js/app.js');
|
||
const source = await fs.readFile(sourcePath, 'utf8');
|
||
const harness = createHarness();
|
||
const serverRoute = route('server-1', 'Serverweg');
|
||
let localRoute = route('local-1', 'Lokaler Weg', 'local');
|
||
const steps = [];
|
||
const savedStates = [];
|
||
const editor = { opened: 0, refreshed: 0 };
|
||
const apiCalls = [];
|
||
const frames = [];
|
||
const animationTimers = new Map();
|
||
let animationTimerId = 0;
|
||
function finishAnimations() {
|
||
while (frames.length) frames.shift()();
|
||
for (const [id, callback] of animationTimers) {
|
||
animationTimers.delete(id);
|
||
callback();
|
||
}
|
||
}
|
||
|
||
harness.node('#route-control-dropdown').properties.set('hidden', true);
|
||
harness.node('#route-search').value = '';
|
||
|
||
const context = {
|
||
document: harness.document,
|
||
jQuery: harness.$,
|
||
Wegwichtel: {
|
||
Api: {
|
||
async health() { apiCalls.push('health'); return { ok: true }; },
|
||
async routes() { apiCalls.push('routes'); return { routes: [serverRoute] }; },
|
||
async route(id) { apiCalls.push(`route:${id}`); return serverRoute; }
|
||
},
|
||
AudioPlayer: { load() {}, pause() {}, unload() {} },
|
||
Config: { routeRadiusKm: 5 },
|
||
Distance: {
|
||
bearing() { return 45; },
|
||
format(value) { return `${Math.round(value)} m`; },
|
||
meters(left, right) {
|
||
return Math.hypot((right.lat - left.lat) * 111000, (right.lon - left.lon) * 70000);
|
||
},
|
||
normalizeDegrees(value) { return ((value % 360) + 360) % 360; }
|
||
},
|
||
EditorUi: {
|
||
openNewLocalRoute() { editor.opened += 1; },
|
||
refresh() { editor.refreshed += 1; }
|
||
},
|
||
Geo: {
|
||
async current() { throw new Error('GPS verweigert'); },
|
||
start() {},
|
||
stop() {}
|
||
},
|
||
LocalRoutes: {
|
||
getAppRoute(id) {
|
||
if (String(id) !== 'local-1' || !localRoute) throw new Error('lokal nicht gefunden');
|
||
return structuredClone(localRoute);
|
||
},
|
||
listAppRoutes() {
|
||
return localRoute ? [structuredClone(localRoute)] : [];
|
||
}
|
||
},
|
||
Orientation: { async start() { return false; }, stop() {} },
|
||
PrivacyStorage: {
|
||
activateRoute() { return null; },
|
||
saveRouteState(...args) { savedStates.push(args); }
|
||
},
|
||
Slideshow: { show() {} },
|
||
Vibration: { Patterns: { ACTIVE_POI: [100] }, start() {}, stop() {} }
|
||
},
|
||
alert() {},
|
||
clearTimeout(id) {
|
||
if (!animationTimers.delete(id)) clearTimeout(id);
|
||
},
|
||
history: {
|
||
state: null,
|
||
back() {},
|
||
pushState(state) { this.state = state; },
|
||
replaceState(state) { this.state = state; }
|
||
},
|
||
scrollTo() {},
|
||
requestAnimationFrame: callback => frames.push(callback),
|
||
setTimeout(callback, delay) {
|
||
if (delay !== 180) return setTimeout(callback, delay);
|
||
const id = --animationTimerId;
|
||
animationTimers.set(id, callback);
|
||
return id;
|
||
},
|
||
structuredClone,
|
||
globalThis: null
|
||
};
|
||
|
||
context.addEventListener = (name, handler) => harness.globalListeners.set(name, handler);
|
||
context.globalThis = context;
|
||
context.navigator = { language };
|
||
|
||
await loadI18n(context);
|
||
|
||
vm.runInNewContext(await fs.readFile(path.join(root, 'public/js/user-messages.js'), 'utf8'), context, { filename: path.join(root, 'public/js/user-messages.js') });
|
||
await loadI18n(context);
|
||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||
|
||
return {
|
||
apiCalls,
|
||
finishAnimations,
|
||
context,
|
||
editor,
|
||
harness,
|
||
savedStates,
|
||
serverRoute,
|
||
setLocalRoute(value) { localRoute = value; },
|
||
steps
|
||
};
|
||
}
|
||
|
||
test('English browser settings localize app controls and retain user-authored trail names', async () => {
|
||
const { context, harness } = await loadApp({ language: 'en-GB' });
|
||
await context.Wegwichtel.App.initialize({ markStep() {} });
|
||
assert.equal(harness.node('#route-control-server-title').textValue, 'Public trails (1)');
|
||
assert.equal(harness.node('#route-control-local-title').textValue, 'My drafts (1)');
|
||
assert.match(harness.node('#location-hint').textValue, /You can still choose from all trails/);
|
||
assert.ok(harness.node('#all-route-list').appended.join('').includes('Serverweg'));
|
||
assert.ok(harness.node('#local-route-list').appended.join('').includes('Lokaler Weg'));
|
||
});
|
||
|
||
test('route menu animates opening and closing and can reopen during closing', async () => {
|
||
const setup = await loadApp();
|
||
const { context, harness } = setup;
|
||
await context.Wegwichtel.App.initialize({ markStep() {} });
|
||
const button = new FakeNode('toggle');
|
||
const dropdown = harness.node('#route-control-dropdown');
|
||
|
||
await harness.activate('#route-control-button', button);
|
||
assert.equal(dropdown.properties.get('hidden'), false);
|
||
assert.equal(dropdown.classes.has('is-open'), false);
|
||
setup.finishAnimations();
|
||
assert.equal(dropdown.classes.has('is-open'), true);
|
||
|
||
await harness.activate('#route-control-button', button);
|
||
assert.equal(dropdown.classes.has('is-open'), false);
|
||
assert.equal(dropdown.properties.get('hidden'), false);
|
||
assert.equal(harness.node('#route-control-button').attributes.get('aria-expanded'), 'false');
|
||
await harness.activate('#route-control-button', button);
|
||
setup.finishAnimations();
|
||
assert.equal(dropdown.classes.has('is-open'), true);
|
||
assert.equal(dropdown.properties.get('hidden'), false);
|
||
|
||
await harness.activate('#route-control-button', button);
|
||
setup.finishAnimations();
|
||
assert.equal(dropdown.properties.get('hidden'), true);
|
||
|
||
await harness.activate('#route-control-button', button);
|
||
harness.fireDocumentKey('Escape');
|
||
setup.finishAnimations();
|
||
assert.equal(dropdown.properties.get('hidden'), true);
|
||
assert.equal(dropdown.classes.has('is-open'), false);
|
||
});
|
||
|
||
test('route control runtime loads server/local routes and supports switching without geolocation', async () => {
|
||
const setup = await loadApp();
|
||
const { context, harness, apiCalls, editor } = setup;
|
||
const initContext = { markStep(name, value) { setup.steps.push([name, value]); } };
|
||
|
||
await context.Wegwichtel.App.initialize(initContext);
|
||
|
||
assert.deepEqual(apiCalls.slice(0, 2), ['health', 'routes']);
|
||
assert.deepEqual(setup.steps, [['server', 'done'], ['routes', 'done'], ['location', 'done']]);
|
||
assert.match(harness.node('#location-hint').textValue, /Standort konnte nicht gefunden werden/);
|
||
assert.match(harness.node('#route-control-server-title').textValue, /Strecken für alle \(1\)/);
|
||
assert.match(harness.node('#route-control-local-title').textValue, /Meine Entwürfe \(1\)/);
|
||
assert.ok(harness.node('#all-route-list').appended.join('').includes('Serverweg'));
|
||
assert.ok(harness.node('#local-route-list').appended.join('').includes('Lokaler Weg'));
|
||
|
||
const controlButton = new FakeNode('#route-control-button');
|
||
await harness.activate('#route-control-button', controlButton);
|
||
assert.equal(harness.node('#route-control-dropdown').properties.get('hidden'), false);
|
||
assert.equal(harness.node('#route-control-button').attributes.get('aria-expanded'), 'true');
|
||
|
||
const newButton = new FakeNode('#route-control-new');
|
||
await harness.activate('.route-control-actions #route-control-new', newButton);
|
||
assert.equal(editor.opened, 1);
|
||
setup.finishAnimations();
|
||
assert.equal(harness.node('#route-control-dropdown').properties.get('hidden'), true);
|
||
|
||
const localButton = new FakeNode('local-select');
|
||
localButton.dataValues.set('route-source', 'local');
|
||
localButton.dataValues.set('route-id', 'local-1');
|
||
await harness.activate('[data-route-select]', localButton);
|
||
|
||
assert.equal(harness.node('#route-title').textValue, 'Lokaler Weg');
|
||
assert.equal(harness.node('#route-page').attributes.has('hidden'), false);
|
||
|
||
const serverButton = new FakeNode('server-select');
|
||
serverButton.dataValues.set('route-source', 'server');
|
||
serverButton.dataValues.set('route-id', 'server-1');
|
||
await harness.key('[data-route-select]', serverButton, 'Enter');
|
||
|
||
assert.ok(apiCalls.includes('route:server-1'));
|
||
assert.equal(harness.node('#route-title').textValue, 'Serverweg');
|
||
});
|
||
|
||
test('local-route change refreshes an active draft and handles its later removal', async () => {
|
||
const setup = await loadApp();
|
||
const { context, harness, editor } = setup;
|
||
await context.Wegwichtel.App.initialize({ markStep() {} });
|
||
|
||
const localButton = new FakeNode('local-select');
|
||
localButton.dataValues.set('route-source', 'local');
|
||
localButton.dataValues.set('route-id', 'local-1');
|
||
await harness.activate('[data-route-select]', localButton);
|
||
|
||
const changed = route('local-1', 'Lokaler Weg geändert', 'local');
|
||
setup.setLocalRoute(changed);
|
||
const updateEvent = new Event('wegwichtel:local-routes-changed');
|
||
Object.defineProperty(updateEvent, 'detail', { value: { id: 'local-1' } });
|
||
harness.document.dispatchEvent(updateEvent);
|
||
await tick();
|
||
|
||
assert.equal(harness.node('#route-title').textValue, 'Lokaler Weg geändert');
|
||
assert.equal(editor.refreshed, 1);
|
||
|
||
setup.setLocalRoute(null);
|
||
const removeEvent = new Event('wegwichtel:local-routes-changed');
|
||
Object.defineProperty(removeEvent, 'detail', { value: { id: 'local-1' } });
|
||
harness.document.dispatchEvent(removeEvent);
|
||
await tick();
|
||
|
||
assert.equal(harness.node('#routes-page').attributes.has('hidden'), false);
|
||
assert.equal(editor.refreshed, 2);
|
||
});
|
||
|
||
test('route control closes on escape and reports local storage failures', async () => {
|
||
const setup = await loadApp();
|
||
const { context, harness } = setup;
|
||
await context.Wegwichtel.App.initialize({ markStep() {} });
|
||
|
||
await harness.activate('#route-control-button', new FakeNode('toggle'));
|
||
assert.equal(harness.node('#route-control-dropdown').properties.get('hidden'), false);
|
||
harness.fireDocumentKey('Escape');
|
||
setup.finishAnimations();
|
||
assert.equal(harness.node('#route-control-dropdown').properties.get('hidden'), true);
|
||
|
||
context.Wegwichtel.LocalRoutes.listAppRoutes = () => { throw new Error('LocalStorage nicht lesbar'); };
|
||
harness.globalListeners.get('pageshow')?.({});
|
||
assert.ok(harness.node('#route-control-local-list').appended.join('').includes('Deine Entwürfe konnten nicht geöffnet werden'));
|
||
});
|
||
|
||
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);
|
||
const bearingTargets = [];
|
||
context.Wegwichtel.Distance.bearing = (position, target) => {
|
||
bearingTargets.push(target);
|
||
return target.title ? 45 : 120;
|
||
};
|
||
calls.position({ lat: 50, lon: 10, accuracy: 5, heading: 90, speed: 1 });
|
||
assert.equal(harness.node('#route-navigation-arrow').properties.get('hidden'), false);
|
||
assert.equal(harness.node('#navigation-arrow').properties.get('hidden'), false);
|
||
assert.notEqual(harness.node('#route-navigation-arrow').styles.get('transform'), harness.node('#navigation-arrow').styles.get('transform'));
|
||
assert.ok(bearingTargets.some(target => target.title));
|
||
assert.ok(bearingTargets.some(target => !target.title));
|
||
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, 'Dein Standort konnte nicht gefunden werden. Prüfe die Standort-Erlaubnis auf deinem Gerät.');
|
||
await harness.activate('#pause-route', new FakeNode('pause'));
|
||
assert.match(harness.node('#tracking-status').textValue, /Du machst eine Pause/);
|
||
assert.equal(savedStates.at(-1)[1], 'paused');
|
||
assert.equal(harness.node('#route-navigation-arrow').properties.get('hidden'), true);
|
||
assert.equal(harness.node('#navigation-arrow').properties.get('hidden'), true);
|
||
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('#navigation-distance').textValue, '–');
|
||
assert.equal(harness.node('#route-navigation-mode').textValue, 'Alle Stationen und das Ziel erreicht');
|
||
await harness.activate('#stop-route', new FakeNode('stop'));
|
||
assert.equal(savedStates.at(-1)[1], 'idle');
|
||
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, /weitermachen/);
|
||
assert.equal(harness.node('#start-route').attributes.get('title'), 'Strecke 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 Stationen/);
|
||
await harness.activate('#start-route', new FakeNode('start'));
|
||
calls.position({ lat: 50, lon: 10, accuracy: 10 });
|
||
assert.equal(harness.node('#route-navigation-mode').textValue, 'Der Weg dieser Strecke fehlt.');
|
||
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, 'Strecke 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 wurde/);
|
||
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 passende Strecke gefunden/);
|
||
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() {} }), /Strecken konnten nicht geladen werden/);
|
||
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);
|
||
});
|