overhaul gpx creation and usage
Sonarqube Scanner / Prepare environment (push) Successful in 8s
Sonarqube Scanner / Build and test (push) Failing after 22s
Sonarqube Scanner / SonarQube analysis (push) Skipped
Sonarqube Scanner / Deploy to Server (push) Successful in 6s

This commit is contained in:
Florian Zumpe
2026-10-03 12:40:26 +02:00
parent 1cb3977269
commit 171ea88d55
29 changed files with 969 additions and 344 deletions
+34 -13
View File
@@ -361,6 +361,27 @@ const sampleGpx = `<?xml version="1.0" encoding="UTF-8"?>
</trkseg></trk>
</gpx>`;
test('missing GPS samples and stations survive storage and later manual coordinates', async () => {
const { api } = await loadLocalRoutes();
let gpx = api.appendTrackPoint(api.emptyGpx('Ohne Empfang'), { timestamp: 1000 }, 'Ohne Empfang');
gpx = api.appendTrackPoint(gpx, { lat: 50, lon: 10, timestamp: 2000 }, 'Ohne Empfang');
const saved = api.save({ id: 'missing-gps', gpx, route: { name: 'Ohne Empfang' }, pois: [
{ id: 42, title: 'Drinnen', lat: null, lon: null, sequence: 0 }
] });
const positions = api.recordedPositions(saved.gpx);
assert.equal(positions.length, 2);
assert.equal(positions[0].lat, null);
assert.equal(api.parseWaypoints(saved.gpx)[0].lat, null);
assert.match(api.gpxForUpload(saved), /gpsGap/);
positions[0].lat = 49.999;
positions[0].lon = 9.999;
const edited = api.replaceRecordedPositions(saved.gpx, positions, 'Ohne Empfang');
assert.doesNotMatch(edited, /gpsGap/);
assert.equal(api.recordedPositions(edited)[0].lat, 49.999);
assert.equal(api.parseWaypoints(edited)[0].title, 'Drinnen');
assert.equal(api.parseWaypoints(edited)[0].lat, null);
});
test('route id generation uses randomUUID when available', async () => {
const { api } = await loadLocalRoutes();
@@ -448,10 +469,10 @@ test('local route package stores GPX and JSON separately and reconstructs an app
const gpx = localStorage.getItem(`${prefix}.${encodedId}.gpx`);
const metadata = JSON.parse(localStorage.getItem(`${prefix}.${encodedId}.json`));
assert.match(gpx, /<trkpt lat="50\.1" lon="10\.1">/);
assert.match(gpx, /<wpt lat="50\.1" lon="10\.1">/);
assert.match(gpx, /<wegwichtel:poiId>101<\/wegwichtel:poiId>/);
assert.doesNotMatch(gpx, /<wpt/);
assert.equal((gpx.match(/<trkpt /g) || []).length, 2);
assert.doesNotMatch(gpx, /<trkpt/);
assert.equal((gpx.match(/<wpt /g) || []).length, 2);
assert.equal(metadata.route.name, 'Neue Strecke');
assert.equal(metadata.route.serverRouteId, 42);
assert.equal(metadata.route.uploadedAt, '2026-09-27T18:00:00.000Z');
@@ -465,8 +486,8 @@ test('local route package stores GPX and JSON separately and reconstructs an app
assert.equal(appRoute.pois.length, 2);
assert.equal(appRoute.pois[0].audioUrl, 'data:audio/mpeg;base64,QQ==');
assert.equal(appRoute.pois[0].images[0].url, 'data:image/jpeg;base64,Qg==');
assert.equal(appRoute.points.length, 2);
assert.ok(appRoute.distanceM > 0);
assert.equal(appRoute.points.length, 0);
assert.equal(appRoute.distanceM, 0);
assert.equal(Math.round(appRoute.elevationGainM), 0);
const listed = api.listAppRoutes();
assert.equal(listed.length, 1);
@@ -485,15 +506,15 @@ test('GPX waypoint import reads stable POI ids and validates coordinates', async
assert.match(api.emptyGpx('A & B'), /<name>A &amp; B<\/name>/);
assert.throws(() => api.parseWaypoints('<not-gpx></not-gpx>'), /keinen lesbaren Weg/);
assert.throws(() => api.syncGpx(sampleGpx, { name: 'Fehler' }, [{ id: 1, title: 'Ohne Position' }]), /Ort von Ohne Position fehlt/);
assert.match(api.syncGpx(sampleGpx, { name: 'Fehler' }, [{ id: 1, title: 'Ohne Position' }]), /pendingWaypoint/);
const waypointOnly = api.syncGpx(
`<gpx version="1.1" xmlns="${GPX_NAMESPACE}"></gpx>`,
{ name: 'Nur Station' },
[{ id: 2, title: 'Einzelner Station', lat: 50.2, lon: 10.2, sequence: 0 }]
);
assert.match(waypointOnly, /<trkpt lat="50.2" lon="10.2">/);
assert.doesNotMatch(waypointOnly, /<wpt\b/);
assert.match(waypointOnly, /<wpt lat="50.2" lon="10.2">/);
assert.doesNotMatch(waypointOnly, /<trkpt\b/);
});
test('local points are stored directly as managed GPX Stationen and uploaded unchanged', async () => {
@@ -508,11 +529,11 @@ test('local points are stored directly as managed GPX Stationen and uploaded unc
]
});
assert.match(saved.gpx, /<trkpt lat="50\.123456789" lon="10\.987654321">/);
assert.match(saved.gpx, /<trkpt lat="50\.223456789" lon="11\.087654321">/);
assert.match(saved.gpx, /<wpt lat="50\.123456789" lon="10\.987654321">/);
assert.match(saved.gpx, /<wpt lat="50\.223456789" lon="11\.087654321">/);
assert.match(saved.gpx, /<wegwichtel:poiId>1<\/wegwichtel:poiId>/);
assert.equal((saved.gpx.match(/<trkpt /g) || []).length, 2);
assert.doesNotMatch(saved.gpx, /<wpt\b/);
assert.equal((saved.gpx.match(/<wpt /g) || []).length, 2);
assert.doesNotMatch(saved.gpx, /<trkpt\b/);
assert.equal(api.gpxForUpload(saved), saved.gpx, 'upload must use the already stored track without conversion');
});
@@ -525,7 +546,7 @@ test('sync keeps imported foreign Stationen and upload rejects a route without S
assert.match(synced, /<ele>100<\/ele>/);
assert.match(synced, /<ele>112<\/ele>/);
assert.match(synced, /<wegwichtel:poiId>200<\/wegwichtel:poiId>/);
assert.equal((synced.match(/<trkpt /g) || []).length, 3);
assert.equal((synced.match(/<trkpt /g) || []).length, 2);
assert.equal(api.gpxForUpload({ gpx: sampleGpx, route: { name: 'Import' }, pois: [] }), sampleGpx);
assert.throws(() => api.gpxForUpload({
gpx: api.emptyGpx('Leer'),