overhaul gpx creation and usage
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
import { loadI18n } from '../test-support/i18n.js';
|
||||
|
||||
const sourcePath = new URL('../public/js/route-recorder.js', import.meta.url);
|
||||
const tick = () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
async function setup() {
|
||||
class Element extends EventTarget {
|
||||
constructor() { super(); this.children = []; this.value = ''; this.attributes = {}; }
|
||||
setAttribute(key, value) { this.attributes[key] = value; }
|
||||
appendChild(child) { this.children.push(child); }
|
||||
replaceChildren() { this.children = []; }
|
||||
}
|
||||
const ids = ['route-recording-control', 'route-recording-status', 'route-recording-pause', 'route-recording-stop', 'editor-recorded-positions', 'editor-add-position'];
|
||||
const nodes = new Map(ids.map(id => [id, new Element()]));
|
||||
const document = new EventTarget();
|
||||
document.readyState = 'complete';
|
||||
document.getElementById = id => nodes.get(id);
|
||||
document.createElement = () => new Element();
|
||||
const intervals = new Map();
|
||||
const packages = new Map([['draft', { id: 'draft', route: { name: 'Entwurf' }, gpx: '[]', pois: [] }]]);
|
||||
const context = { document, Date, console,
|
||||
setInterval(callback, delay) { assert.equal(delay, 30000); intervals.set(1, callback); return 1; },
|
||||
clearInterval(id) { intervals.delete(id); },
|
||||
Wegwichtel: { Geo: { current: async () => ({ lat: 50, lon: 10, timestamp: Date.now() }) },
|
||||
LocalRoutes: {
|
||||
get: id => packages.get(id),
|
||||
save: value => packages.set(value.id, value),
|
||||
appendTrackPoint: (gpx, position) => JSON.stringify([...JSON.parse(gpx), position]),
|
||||
recordedPositions: JSON.parse,
|
||||
replaceRecordedPositions: (_gpx, positions) => JSON.stringify(positions)
|
||||
}
|
||||
}
|
||||
};
|
||||
context.globalThis = context;
|
||||
await loadI18n(context);
|
||||
vm.runInNewContext(await fs.readFile(sourcePath, 'utf8'), context, { filename: sourcePath.pathname });
|
||||
return { context, nodes, intervals, packages, recorder: context.Wegwichtel.RouteRecorder };
|
||||
}
|
||||
|
||||
function activate(node, type = 'pointerup', key) {
|
||||
const event = new Event(type, { cancelable: true });
|
||||
if (key) Object.defineProperty(event, 'key', { value: key });
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
test('recording persists every 30 seconds independently of the editor, pauses and finishes', async () => {
|
||||
const { recorder, packages, intervals, nodes } = await setup();
|
||||
recorder.start('draft'); await tick();
|
||||
assert.equal(recorder.status, 'recording');
|
||||
assert.equal(JSON.parse(packages.get('draft').gpx).length, 1);
|
||||
await intervals.get(1)();
|
||||
assert.equal(JSON.parse(packages.get('draft').gpx).length, 2);
|
||||
activate(nodes.get('route-recording-pause'));
|
||||
assert.equal(recorder.status, 'paused'); assert.equal(intervals.size, 0);
|
||||
activate(nodes.get('route-recording-pause'), 'keydown', 'Enter'); await tick();
|
||||
assert.equal(recorder.status, 'recording');
|
||||
activate(nodes.get('route-recording-stop'));
|
||||
assert.equal(recorder.status, 'idle'); assert.equal(intervals.size, 0);
|
||||
assert.equal(nodes.get('route-recording-control').hidden, true);
|
||||
});
|
||||
|
||||
test('GPS failures remain as empty samples and late results after stopping are discarded', async () => {
|
||||
const { context, recorder, packages, intervals } = await setup();
|
||||
context.Wegwichtel.Geo.current = async () => { throw new Error('GPS fehlt'); };
|
||||
recorder.start('draft'); await tick();
|
||||
const samples = JSON.parse(packages.get('draft').gpx);
|
||||
assert.equal(samples.length, 1); assert.equal(samples[0].lat, undefined);
|
||||
let finish;
|
||||
context.Wegwichtel.Geo.current = () => new Promise(resolve => { finish = resolve; });
|
||||
const pending = intervals.get(1)();
|
||||
recorder.stop(); finish({ lat: 51, lon: 11 }); await pending;
|
||||
assert.equal(JSON.parse(packages.get('draft').gpx).length, 1);
|
||||
});
|
||||
|
||||
test('storage errors pause recording and deleted drafts cannot be recreated by the recorder', async () => {
|
||||
const { context, recorder, packages, nodes } = await setup();
|
||||
context.Wegwichtel.LocalRoutes.save = () => { throw new Error('voll'); };
|
||||
recorder.start('draft'); await tick();
|
||||
assert.equal(recorder.status, 'paused');
|
||||
assert.match(nodes.get('route-recording-status').textContent, /Schaffe Platz/);
|
||||
packages.delete('draft');
|
||||
context.document.dispatchEvent(new Event('wegwichtel:local-routes-changed'));
|
||||
assert.equal(recorder.status, 'idle');
|
||||
recorder.resume(); assert.equal(recorder.status, 'idle');
|
||||
});
|
||||
|
||||
test('missing recorded locations can be filled manually while newer samples remain intact', async () => {
|
||||
const { recorder, nodes } = await setup();
|
||||
const initial = JSON.stringify([{ lat: null, lon: null, timestamp: '2026-10-03T10:00:00Z' }]);
|
||||
recorder.renderPoints(initial);
|
||||
const row = nodes.get('editor-recorded-positions').children[0];
|
||||
row.children[2].value = '50'; row.children[4].value = '10';
|
||||
const latest = JSON.stringify([...JSON.parse(initial), { lat: 51, lon: 11 }]);
|
||||
const result = JSON.parse(recorder.editGpx(latest, 'Entwurf'));
|
||||
assert.equal(result[0].lat, 50); assert.equal(result[1].lat, 51);
|
||||
row.children[4].value = '';
|
||||
assert.throws(() => recorder.editGpx(latest, 'Entwurf'));
|
||||
recorder.renderPoints('[]');
|
||||
activate(nodes.get('editor-add-position'));
|
||||
assert.equal(JSON.parse(recorder.editGpx('[]', 'Entwurf')).length, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user