Multiple improvements
This commit is contained in:
@@ -277,6 +277,8 @@ async function loadApp({ language = 'de-DE' } = {}) {
|
||||
stop() {}
|
||||
},
|
||||
LocalRoutes: {
|
||||
async getAppPoi(_id, poiId) { return structuredClone(localRoute?.pois.find(poi => Number(poi.id) === Number(poiId)) || null); },
|
||||
releasePoiMedia() {},
|
||||
getAppRoute(id) {
|
||||
if (String(id) !== 'local-1' || !localRoute) throw new Error('lokal nicht gefunden');
|
||||
return structuredClone(localRoute);
|
||||
@@ -640,3 +642,43 @@ test('invalid API responses reject initialization and route selection errors res
|
||||
assert.deepEqual(errors, ['Strecke entfernt']);
|
||||
assert.equal(button.properties.get('disabled'), false);
|
||||
});
|
||||
|
||||
|
||||
test('local station playback loads only the selected station and ignores media arriving after leaving', async () => {
|
||||
const setup = await loadApp();
|
||||
const { context, harness } = setup;
|
||||
const ns = context.Wegwichtel;
|
||||
await ns.App.initialize({ markStep() {} });
|
||||
const button = new FakeNode('local-select');
|
||||
button.dataValues.set('route-source', 'local');
|
||||
button.dataValues.set('route-id', 'local-1');
|
||||
const requests = [];
|
||||
const released = [];
|
||||
ns.LocalRoutes.getAppPoi = (routeId, poiId) => new Promise(resolve => requests.push({ routeId, poiId, resolve }));
|
||||
ns.LocalRoutes.releasePoiMedia = poi => released.push(poi);
|
||||
const calls = observeSensors(setup);
|
||||
await harness.activate('[data-route-select]', button);
|
||||
assert.equal(requests.length, 0);
|
||||
const poiButton = new FakeNode('station');
|
||||
poiButton.dataValues.set('poi-id', 10);
|
||||
// Use the first actual local station ID rather than assuming server IDs.
|
||||
const local = ns.LocalRoutes.getAppRoute('local-1');
|
||||
poiButton.dataValues.set('poi-id', local.pois[0].id);
|
||||
await harness.activate('[data-poi-select]', poiButton);
|
||||
assert.equal(requests.length, 1);
|
||||
assert.equal(requests[0].routeId, 'local-1');
|
||||
assert.equal(requests[0].poiId, local.pois[0].id);
|
||||
const firstLoaded = { images: [{ url: 'blob:shown' }], audioUrl: 'blob:shown-audio' };
|
||||
requests[0].resolve(firstLoaded);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(calls.audio.at(-1), 'blob:shown-audio');
|
||||
assert.equal(calls.slides.at(-1)[0].url, 'blob:shown');
|
||||
await harness.activate('[data-poi-select]', poiButton);
|
||||
assert.equal(released[0], firstLoaded);
|
||||
await harness.activate('#stop-route', new FakeNode('stop'));
|
||||
const loaded = { images: [{ url: 'blob:late' }], audioUrl: 'blob:late-audio' };
|
||||
requests[1].resolve(loaded);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(released[1], loaded);
|
||||
assert.equal(calls.audio.includes('blob:late-audio'), false);
|
||||
});
|
||||
|
||||
@@ -149,7 +149,13 @@ test('route, POI, media and storage services cover successful and rejected lifec
|
||||
const indoorStation = routes.createPoi(db, indoor.id, { title: 'Ohne GPS', lat: null, lon: null });
|
||||
assert.equal(routes.getRoute(db, indoor.id).pois[0].lat, null);
|
||||
routes.updatePoi(db, indoor.id, indoorStation.id, { lat: 50, lon: 10 });
|
||||
assert.equal(routes.getRoute(db, indoor.id).pointCount, 1);
|
||||
assert.equal(db.prepare('SELECT min_lat FROM routes WHERE id=?').get(indoor.id).min_lat, 50);
|
||||
routes.updatePoi(db, indoor.id, indoorStation.id, { lat: 51, lon: 11 });
|
||||
const movedStationRoute = routes.getRoute(db, indoor.id);
|
||||
assert.equal(movedStationRoute.pointCount, 1);
|
||||
assert.equal(movedStationRoute.points[0].lat, 51);
|
||||
assert.equal(db.prepare('SELECT min_lat FROM routes WHERE id=?').get(indoor.id).min_lat, 51);
|
||||
routes.deletePoi(db, indoor.id, indoorStation.id);
|
||||
assert.equal(routes.getRoute(db, indoor.id).pointCount, 0);
|
||||
assert.equal(db.prepare('SELECT min_lat FROM routes WHERE id=?').get(indoor.id).min_lat, null);
|
||||
|
||||
@@ -302,6 +302,13 @@ function buildEditorContext() {
|
||||
type: file.type || 'application/octet-stream'
|
||||
}),
|
||||
get: id => packages.get(String(id)) || null,
|
||||
getPoiWithMedia: async (id, poiId) => {
|
||||
const value = packages.get(String(id))?.pois.find(poi => String(poi.id) === String(poiId));
|
||||
if (!value) return null;
|
||||
return { ...value, audio: value.audio ? { ...value.audio, url: 'blob:test-audio' } : null,
|
||||
images: (value.images || []).map(image => ({ ...image, url: 'blob:test-image' })) };
|
||||
},
|
||||
releasePoiMedia() {},
|
||||
getWithMedia: async id => {
|
||||
const value = packages.get(String(id));
|
||||
if (!value) return null;
|
||||
@@ -659,6 +666,7 @@ test('editing an activated server route uses PUT and never writes activation sta
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
|
||||
const routeList = elements.get('editor-route-list');
|
||||
await tick();
|
||||
const serverItem = routeList.children.find(child => child.dataset.routeSource === 'server');
|
||||
const serverActions = serverItem.children[1];
|
||||
assert.equal(serverActions.children.length, 1);
|
||||
@@ -737,3 +745,94 @@ test('local media picker keeps File System Access handles for later upload', asy
|
||||
assert.equal(imageStore.handle, handle);
|
||||
assert.equal(imageStore.file, imageFile);
|
||||
});
|
||||
|
||||
|
||||
test('local drafts can be opened and saved while the server request is still pending', async () => {
|
||||
const { context, elements, packages } = buildEditorContext();
|
||||
context.fetch = () => new Promise(() => {});
|
||||
await loadI18n(context);
|
||||
vm.runInNewContext(await fs.readFile(path.join(root, 'public/js/editor-ui.js'), 'utf8'), context);
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
const list = elements.get('editor-route-list');
|
||||
const item = list.children.find(child => child.dataset.routeId === 'local-existing');
|
||||
assert.ok(item);
|
||||
list.dispatchEvent(pointerEvent(item.children[1].children[0]));
|
||||
await tick();
|
||||
assert.equal(elements.get('editor-route-id').value, 'local-existing');
|
||||
elements.get('editor-route-name').value = 'Offline geändert';
|
||||
elements.get('editor-route-form').dispatchEvent(submitEvent());
|
||||
await tick(); await tick();
|
||||
assert.equal(packages.get('local-existing').route.name, 'Offline geändert');
|
||||
assert.equal(elements.get('editor-status').textContent, 'Gespeichert.');
|
||||
});
|
||||
|
||||
test('a failed public route request preserves local drafts and late replies use the latest local data', async () => {
|
||||
const { context, elements, packages } = buildEditorContext();
|
||||
const requests = [];
|
||||
context.fetch = () => new Promise((resolve, reject) => requests.push({ resolve, reject }));
|
||||
await loadI18n(context);
|
||||
vm.runInNewContext(await fs.readFile(path.join(root, 'public/js/editor-ui.js'), 'utf8'), context);
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
requests[0].reject(new Error('offline')); await tick();
|
||||
assert.match(elements.get('editor-status').textContent, /nicht erreichbar/);
|
||||
assert.ok(elements.get('editor-route-list').children.some(child => child.dataset.routeId === 'local-existing'));
|
||||
await context.Wegwichtel.EditorUi.refresh();
|
||||
packages.delete('local-existing');
|
||||
requests[1].resolve(jsonResponse({ routes: [{ id: 7, name: 'Veröffentlicht' }] }));
|
||||
await tick(); await tick();
|
||||
assert.equal(elements.get('editor-route-list').children.some(child => child.dataset.routeId === 'local-existing'), false);
|
||||
assert.ok(elements.get('editor-route-list').children.some(child => child.dataset.routeSource === 'server'));
|
||||
});
|
||||
|
||||
|
||||
test('opening and saving a draft does not load files; editing a station loads only that station', async () => {
|
||||
const { context, elements, packages } = buildEditorContext();
|
||||
const local = packages.get('local-existing');
|
||||
local.pois = [0, 1].map(id => ({ id, title: `Station ${id}`, sequence: id, images: [{ id: `image-${id}`, mediaKey: `key-${id}` }] }));
|
||||
const loads = [];
|
||||
context.Wegwichtel.LocalRoutes.getWithMedia = () => { throw new Error('Full route hydration must not run'); };
|
||||
const original = context.Wegwichtel.LocalRoutes.getPoiWithMedia;
|
||||
context.Wegwichtel.LocalRoutes.getPoiWithMedia = (...args) => { loads.push(args); return original(...args); };
|
||||
await loadI18n(context);
|
||||
vm.runInNewContext(await fs.readFile(path.join(root, 'public/js/editor-ui.js'), 'utf8'), context);
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
const list = elements.get('editor-route-list');
|
||||
const item = list.children.find(child => child.dataset.routeId === 'local-existing');
|
||||
list.dispatchEvent(pointerEvent(item.children[1].children[0])); await tick();
|
||||
assert.equal(loads.length, 0);
|
||||
elements.get('editor-route-name').value = 'Nur Name geändert';
|
||||
elements.get('editor-route-form').dispatchEvent(submitEvent()); await tick(); await tick();
|
||||
assert.equal(loads.length, 0);
|
||||
const poiList = elements.get('editor-poi-list');
|
||||
const poiItem = poiList.children.find(child => child.dataset.poiId === '1');
|
||||
poiList.dispatchEvent(pointerEvent(poiItem.children[1].children[2])); await tick();
|
||||
assert.equal(loads.length, 1);
|
||||
assert.equal(loads[0][1], 1);
|
||||
assert.equal(elements.get('editor-poi-media-preview').children.length, 1);
|
||||
elements.get('editor-poi-title').value = 'Titel geändert';
|
||||
elements.get('editor-poi-form').dispatchEvent(submitEvent()); await tick(); await tick();
|
||||
assert.equal(loads.length, 1);
|
||||
assert.equal(packages.get('local-existing').pois[1].title, 'Titel geändert');
|
||||
});
|
||||
|
||||
test('late station previews are released after the editor closes without replacing unsaved input', async () => {
|
||||
const { context, elements, packages } = buildEditorContext();
|
||||
packages.get('local-existing').pois = [{ id: 0, title: 'Station', images: [] }];
|
||||
let finish;
|
||||
const released = [];
|
||||
context.Wegwichtel.LocalRoutes.getPoiWithMedia = () => new Promise(resolve => { finish = resolve; });
|
||||
context.Wegwichtel.LocalRoutes.releasePoiMedia = poi => { if(poi) released.push(poi); };
|
||||
await loadI18n(context);
|
||||
vm.runInNewContext(await fs.readFile(path.join(root, 'public/js/editor-ui.js'), 'utf8'), context);
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
const list = elements.get('editor-route-list');
|
||||
const item = list.children.find(child => child.dataset.routeId === 'local-existing');
|
||||
list.dispatchEvent(pointerEvent(item.children[1].children[0])); await tick();
|
||||
const poiList = elements.get('editor-poi-list');
|
||||
poiList.dispatchEvent(pointerEvent(poiList.children[0].children[1].children[2])); await tick();
|
||||
elements.get('editor-poi-title').value = 'Noch nicht gespeichert';
|
||||
context.Wegwichtel.EditorUi.close();
|
||||
finish({ id: 0, title: 'Alter Titel', images: [{ url: 'blob:late' }] }); await tick();
|
||||
assert.equal(released.length, 1);
|
||||
assert.equal(elements.get('editor-poi-title').value, 'Noch nicht gespeichert');
|
||||
});
|
||||
|
||||
+71
-1
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { parseGpx } from '../src/services/gpx.js';
|
||||
import { parseGpx, gpxWithWaypoints } from '../src/services/gpx.js';
|
||||
import { SAMPLE_GPX } from '../test-support/gpx-fixtures.js';
|
||||
|
||||
test('parseGpx reads synthetic track data without repository example files', () => {
|
||||
@@ -11,3 +11,73 @@ test('parseGpx reads synthetic track data without repository example files', ()
|
||||
assert.equal(parsed.points[0].lat, 52.5200);
|
||||
assert.equal(parsed.points[2].lon, 13.4090);
|
||||
});
|
||||
|
||||
const point = (tag, lat, lon, content = '') => `<${tag} lat="${lat}" lon="${lon}">${content}</${tag}>`;
|
||||
const gpx = (waypoints, path = '', extra = '') => `<gpx version="1.1" creator="Test" xmlns:wegwichtel="urn:wegwichtel:poi">${waypoints}${path}${extra}</gpx>`;
|
||||
const track = points => `<trk><trkseg>${points}</trkseg></trk>`;
|
||||
const locations = points => points.map(({ lat, lon }) => [lat, lon]);
|
||||
|
||||
test('waypoints alone become route points in station order and contribute to distance and bounds', () => {
|
||||
const parsed = parseGpx(gpx(point('wpt', 50, 10) + point('wpt', 50.01, 10.02)));
|
||||
assert.deepEqual(locations(parsed.points), [[50, 10], [50.01, 10.02]]);
|
||||
assert.equal(parsed.metrics.pointCount, 2);
|
||||
assert.ok(parsed.metrics.distanceM > 0);
|
||||
assert.equal(parsed.metrics.maxLon, 10.02);
|
||||
assert.match(parsed.normalizedXml, /<wpt\b/);
|
||||
assert.match(parsed.normalizedXml, /<rtept\b/);
|
||||
assert.deepEqual(locations(parseGpx(parsed.normalizedXml).points), locations(parsed.points));
|
||||
});
|
||||
|
||||
test('track points and stations both shape the path without dropping nearby station anchors', () => {
|
||||
const wp = point('wpt', 50, 10.00001) + point('wpt', 50.005, 10.015);
|
||||
const recorded = point('trkpt', 50, 10) + point('trkpt', 50, 10.00002)
|
||||
+ point('trkpt', 50, 10.01) + point('trkpt', 50, 10.02);
|
||||
const parsed = parseGpx(gpx(wp, track(recorded)));
|
||||
assert.deepEqual(locations(parsed.points), [[50, 10], [50, 10.00001], [50, 10.01], [50.005, 10.015], [50, 10.02]]);
|
||||
assert.equal(parsed.metrics.maxLat, 50.005);
|
||||
assert.equal(parsed.points.length, 5);
|
||||
assert.deepEqual(parsed.points.map(p => p.sequence), [0, 1, 2, 3, 4]);
|
||||
assert.deepEqual(locations(parseGpx(parsed.normalizedXml).points), locations(parsed.points));
|
||||
});
|
||||
|
||||
test('duplicate station locations do not multiply route points and station order is retained on loops', () => {
|
||||
const stations = point('wpt', 50, 10.02) + point('wpt', 50, 10.02) + point('wpt', 50, 10.01);
|
||||
const recorded = [10, 10.01, 10.02, 10.01, 10].map(lon => point('trkpt', 50, lon)).join('');
|
||||
const parsed = parseGpx(gpx(stations, track(recorded)));
|
||||
assert.deepEqual(locations(parsed.points), [[50, 10], [50, 10.01], [50, 10.02], [50, 10.01], [50, 10]]);
|
||||
assert.deepEqual(locations(parseGpx(parsed.normalizedXml).points), locations(parsed.points));
|
||||
});
|
||||
|
||||
test('existing routes and replacement points incorporate stations, including stations before and after the path', () => {
|
||||
const stations = [9.99, 10.005, 10.02].map(lon => point('wpt', 50, lon)).join('');
|
||||
const route = `<rte>${point('rtept', 50, 10)}${point('rtept', 50, 10.01)}</rte>`;
|
||||
const xml = gpx(stations, route);
|
||||
const parsed = parseGpx(xml);
|
||||
assert.deepEqual(locations(parsed.points), [[50, 9.99], [50, 10], [50, 10.005], [50, 10.01], [50, 10.02]]);
|
||||
assert.deepEqual(locations(parseGpx(xml, [{ lat: 50, lon: 10 }, { lat: 50, lon: 10.01 }]).points), locations(parsed.points));
|
||||
});
|
||||
|
||||
test('missing and invalid station coordinates are excluded while GPS gaps remain available for editing', () => {
|
||||
const stations = '<wpt><name>Ohne Ort</name></wpt>' + point('wpt', 91, 10) + point('wpt', 50, 181) + point('wpt', 50, 10);
|
||||
const extra = '<extensions><wegwichtel:gpsGap time="2026-10-04T10:00:00Z"/><wegwichtel:pendingWaypoint><name>Später</name></wegwichtel:pendingWaypoint></extensions>';
|
||||
const parsed = parseGpx(gpx(stations, '', extra));
|
||||
assert.deepEqual(locations(parsed.points), [[50, 10]]);
|
||||
assert.match(parsed.normalizedXml, /wegwichtel:gpsGap/);
|
||||
assert.match(parsed.normalizedXml, /wegwichtel:pendingWaypoint/);
|
||||
const empty = parseGpx(gpx('', '', extra));
|
||||
assert.equal(empty.metrics.pointCount, 0);
|
||||
assert.equal(empty.metrics.minLat, null);
|
||||
});
|
||||
|
||||
|
||||
test('moving or deleting a station replaces its generated route anchor without removing recorded points', () => {
|
||||
const original = parseGpx(gpx(point('wpt', 50.01, 10.005), track(point('trkpt', 50, 10) + point('trkpt', 50, 10.01))));
|
||||
assert.match(original.normalizedXml, /wegwichtel:waypointRoutePoint/);
|
||||
const updated = parseGpx(gpxWithWaypoints(original.normalizedXml, [{ id: 1, title: 'Neu', lat: 50.02, lon: 10.006 }]));
|
||||
assert.equal(updated.points.length, 3);
|
||||
assert.equal(updated.metrics.maxLat, 50.02);
|
||||
assert.equal(updated.points.some(p => p.lat === 50.01), false);
|
||||
const removed = parseGpx(gpxWithWaypoints(updated.normalizedXml, []));
|
||||
assert.deepEqual(locations(removed.points), [[50, 10], [50, 10.01]]);
|
||||
assert.deepEqual(locations(parseGpx(updated.normalizedXml).points), locations(updated.points));
|
||||
});
|
||||
|
||||
@@ -45,7 +45,8 @@ test('information button opens scrollable overlay pages for privacy, imprint, te
|
||||
assert.match(html, /data-info-target="technology"/);
|
||||
assert.match(html, /<h3>Technisch verantwortlich<\/h3>\s*<p>Florian Zumpe<br>/);
|
||||
assert.match(html, /\[Anschrift von Florian Zumpe ergänzen\]/);
|
||||
assert.match(html, /\[E-Mail-Adresse von Florian Zumpe ergänzen\]/);
|
||||
assert.match(html, /E-Mail: <a href="mailto:info@wegwichtel\.de">info@wegwichtel\.de<\/a>/);
|
||||
assert.doesNotMatch(html, /\[E-Mail-Adresse von Florian Zumpe ergänzen\]|\[Kontakt für Rechtefragen ergänzen\]/);
|
||||
const storage = await read('public/js/privacy-storage.js');
|
||||
assert.match(storage, /wegwichtel\.privacy\.v1/);
|
||||
assert.match(storage, /wegwichtel\.route-progress\.v1/);
|
||||
|
||||
@@ -730,3 +730,35 @@ test('creator identity never falls back to insecure randomness or replaces corru
|
||||
assert.throws(() => api.creatorId(), /Gerätekennung/);
|
||||
assert.equal(localStorage.getItem('wegwichtel.creator-id.v1'), 'broken');
|
||||
});
|
||||
|
||||
|
||||
test('route selection reads metadata only and station media is loaded and released separately', async () => {
|
||||
const { api, context } = await loadLocalRoutes();
|
||||
const reads = [0, 0];
|
||||
const revoked = [];
|
||||
let urlNumber = 0;
|
||||
context.URL.createObjectURL = () => `blob:lazy-${++urlNumber}`;
|
||||
context.URL.revokeObjectURL = url => revoked.push(url);
|
||||
const files = [];
|
||||
for (let id = 0; id < 2; id++) {
|
||||
const file = Object.assign(new Blob(['image']), { name: `${id}.jpg` });
|
||||
const handle = { async getFile() { reads[id]++; return file; } };
|
||||
files.push(await api.storeMediaFile('lazy', id, 'image', id, file, handle));
|
||||
}
|
||||
api.save({ id: 'lazy', route: { name: 'Lazy' }, pois: files.map((image, id) => ({ id, images: [image] })) });
|
||||
await api.getAppRoute('lazy');
|
||||
api.get('lazy');
|
||||
assert.deepEqual(reads, [0, 0]);
|
||||
const first = await api.getPoiWithMedia('lazy', 0, { requestPermission: true });
|
||||
assert.deepEqual(reads, [1, 0]);
|
||||
assert.equal(first.images[0].url, 'blob:lazy-1');
|
||||
const second = await api.getAppPoi('lazy', 1);
|
||||
assert.deepEqual(reads, [1, 1]);
|
||||
assert.equal(second.images[0].url, 'blob:lazy-2');
|
||||
api.releasePoiMedia(first); api.releasePoiMedia(first); api.releasePoiMedia(second);
|
||||
api.releasePoiMedia({ images: [{ url: 'https://example.org/image.jpg' }] });
|
||||
assert.deepEqual(revoked, ['blob:lazy-1', 'blob:lazy-2']);
|
||||
assert.equal(await api.getPoiWithMedia('lazy', 9), null);
|
||||
assert.equal(await api.getAppPoi('missing', 0), null);
|
||||
assert.equal(api.get('lazy').pois[0].images[0].url, undefined);
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ async function setup() {
|
||||
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 ids = ['route-recording-control', 'route-recording-status', 'route-recording-pause', 'route-recording-stop', 'editor-recorded-positions', 'editor-add-position', 'route-recording-toggle'];
|
||||
const nodes = new Map(ids.map(id => [id, new Element()]));
|
||||
const document = new EventTarget();
|
||||
document.readyState = 'complete';
|
||||
@@ -23,7 +23,7 @@ async function setup() {
|
||||
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; },
|
||||
setInterval(callback, delay) { assert.equal(delay, 120000); intervals.set(1, callback); return 1; },
|
||||
clearInterval(id) { intervals.delete(id); },
|
||||
Wegwichtel: { Geo: { current: async () => ({ lat: 50, lon: 10, timestamp: Date.now() }) },
|
||||
LocalRoutes: {
|
||||
@@ -47,7 +47,7 @@ function activate(node, type = 'pointerup', key) {
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
test('recording persists every 30 seconds during an editing session, pauses and finishes', async () => {
|
||||
test('recording persists every two minutes during an editing session, pauses and finishes', async () => {
|
||||
const { recorder, packages, intervals, nodes } = await setup();
|
||||
recorder.start('draft'); await tick();
|
||||
assert.equal(recorder.status, 'recording');
|
||||
@@ -106,10 +106,14 @@ test('missing recorded locations can be filled manually while newer samples rema
|
||||
|
||||
|
||||
test('ending editing stops sampling, reopening permits recording and a fresh app remains inactive', async () => {
|
||||
const { recorder, intervals, packages } = await setup();
|
||||
const { recorder, intervals, packages, nodes } = await setup();
|
||||
assert.equal(recorder.status, 'idle');
|
||||
recorder.beginEditing('draft');
|
||||
await tick();
|
||||
assert.equal(intervals.size, 0);
|
||||
nodes.get('route-recording-toggle').checked = true;
|
||||
nodes.get('route-recording-toggle').dispatchEvent(new Event('change'));
|
||||
await tick();
|
||||
assert.equal(intervals.size, 1);
|
||||
recorder.pause();
|
||||
recorder.beginEditing('draft');
|
||||
@@ -121,8 +125,68 @@ test('ending editing stops sampling, reopening permits recording and a fresh app
|
||||
assert.equal(JSON.parse(packages.get('draft').gpx).length, 1);
|
||||
recorder.beginEditing('draft');
|
||||
await tick();
|
||||
assert.equal(recorder.status, 'recording');
|
||||
assert.equal(recorder.status, 'idle');
|
||||
assert.equal(nodes.get('route-recording-toggle').checked, false);
|
||||
const fresh = await setup();
|
||||
assert.equal(fresh.recorder.status, 'idle');
|
||||
assert.equal(fresh.intervals.size, 0);
|
||||
});
|
||||
|
||||
|
||||
test('the switch can prepare a new draft and late GPS from an older session is ignored', async () => {
|
||||
const { context, recorder, nodes, packages, intervals } = await setup();
|
||||
const answers = [];
|
||||
context.Wegwichtel.Geo.current = () => new Promise(resolve => answers.push(resolve));
|
||||
const toggle = nodes.get('route-recording-toggle');
|
||||
toggle.checked = true; toggle.dispatchEvent(new Event('change'));
|
||||
assert.equal(answers.length, 0);
|
||||
recorder.beginEditing('draft');
|
||||
assert.equal(answers.length, 1);
|
||||
toggle.checked = false; toggle.dispatchEvent(new Event('change'));
|
||||
toggle.checked = true; toggle.dispatchEvent(new Event('change'));
|
||||
assert.equal(answers.length, 2);
|
||||
answers[1]({ lat: 52, lon: 12 }); await tick();
|
||||
answers[0]({ lat: 50, lon: 10 }); await tick();
|
||||
assert.equal(JSON.parse(packages.get('draft').gpx).length, 1);
|
||||
assert.equal(JSON.parse(packages.get('draft').gpx)[0].lat, 52);
|
||||
context.Wegwichtel.Geo.current = async () => ({ lat: 53, lon: 13 });
|
||||
context.document.visibilityState = 'visible';
|
||||
context.document.dispatchEvent(new Event('visibilitychange')); await tick();
|
||||
assert.equal(JSON.parse(packages.get('draft').gpx).length, 2);
|
||||
recorder.endEditing();
|
||||
assert.equal(intervals.size, 0);
|
||||
assert.equal(toggle.checked, false);
|
||||
});
|
||||
|
||||
test('each crosshair fills only its own point without starting recording or saving', async () => {
|
||||
const { recorder, nodes, packages } = await setup();
|
||||
recorder.renderPoints('[{"lat":1,"lon":2},{"lat":3,"lon":4}]');
|
||||
const [first, second] = nodes.get('editor-recorded-positions').children;
|
||||
second.children[5].dispatchEvent(new Event('click')); await tick();
|
||||
assert.equal(first.children[2].value, '1');
|
||||
assert.equal(second.children[2].value, '50');
|
||||
assert.equal(second.children[4].value, '10');
|
||||
assert.equal(recorder.status, 'idle');
|
||||
assert.equal(packages.get('draft').gpx, '[]');
|
||||
assert.match(second.children[6].textContent, /Speichere/);
|
||||
});
|
||||
|
||||
test('manual location errors preserve coordinates and stale or duplicate requests cannot overwrite points', async () => {
|
||||
const { context, recorder, nodes } = await setup();
|
||||
recorder.renderPoints('[{"lat":1,"lon":2}]');
|
||||
const row = nodes.get('editor-recorded-positions').children[0];
|
||||
context.Wegwichtel.Geo.current = async () => { throw new Error('denied'); };
|
||||
row.children[5].dispatchEvent(new Event('click')); await tick();
|
||||
assert.equal(row.children[2].value, '1');
|
||||
assert.match(row.children[6].textContent, /nicht gefunden/);
|
||||
let finish;
|
||||
let requests = 0;
|
||||
context.Wegwichtel.Geo.current = () => { requests++; return new Promise(resolve => { finish = resolve; }); };
|
||||
row.children[5].dispatchEvent(new Event('click'));
|
||||
row.children[5].dispatchEvent(new Event('click'));
|
||||
assert.equal(requests, 1);
|
||||
recorder.renderPoints('[]');
|
||||
finish({ lat: 51, lon: 11 }); await tick();
|
||||
assert.equal(row.children[2].value, '1');
|
||||
assert.equal(row.children[5].disabled, false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user