change trackpoint format to match server expectation
Sonarqube Scanner / Build and analyze (push) Successful in 1m9s
Sonarqube Scanner / Build and analyze (push) Successful in 1m9s
This commit is contained in:
+68
-4
@@ -34,6 +34,8 @@ test('editor markup and source describe local draft creation instead of immediat
|
||||
assert.doesNotMatch(html, /id="editor-poi-(?:lat|lon)"[^>]+step=/);
|
||||
assert.match(html, /id="editor-poi-media-preview"/);
|
||||
assert.match(html, /id="editor-poi-delete"[^>]+editor-danger-button/);
|
||||
assert.match(html, />Trackpunkte<\/h3>/);
|
||||
assert.match(html, /aria-label="Trackpunkt hinzufügen"/);
|
||||
assert.match(html, /id="editor-route-upload"[^>]+type="button"[^>]+hidden/);
|
||||
assert.doesNotMatch(html, /accept="video\//);
|
||||
assert.match(html, /ausschließlich in diesem Browser/i);
|
||||
@@ -45,13 +47,17 @@ test('editor markup and source describe local draft creation instead of immediat
|
||||
assert.match(css, /\.editor-media-preview/);
|
||||
assert.match(css, /\.editor-media-remove/);
|
||||
assert.match(css, /\.editor-danger-button/);
|
||||
assert.match(css, /\.editor-icon-danger/);
|
||||
|
||||
assert.match(ui, /readJson\(['"]\/routes\?activatedOnly=true['"]\)/);
|
||||
assert.match(ui, /ns\.LocalRoutes\.save\(/);
|
||||
assert.match(ui, /ns\.LocalRoutes\.parseWaypoints\(/);
|
||||
assert.match(ui, /ns\.LocalRoutes\.fileToDataUrl\(/);
|
||||
assert.match(ui, /ns\.LocalRoutes\.gpxForUpload\(/);
|
||||
assert.match(ui, /move-poi-up/);
|
||||
assert.match(ui, /move-poi-down/);
|
||||
assert.match(ui, /delete-route/);
|
||||
assert.match(ui, /delete-poi/);
|
||||
assert.match(ui, /sortOrder:\s*index/);
|
||||
assert.match(ui, /pointerup/);
|
||||
assert.doesNotMatch(ui, /sendJson\(['"]\/routes['"],\s*['"]POST['"]/);
|
||||
@@ -246,6 +252,7 @@ function buildEditorContext() {
|
||||
if (/^\/api\/routes\/22\/pois\/23\/(audio|pictures)$/.test(url) && method === 'POST') return jsonResponse({ ok: true }, true, 201);
|
||||
if (url === '/api/routes/22' && method === 'DELETE') return jsonResponse({ deleted: true });
|
||||
if (url === '/api/routes/1' && method === 'GET') return jsonResponse({ route: serverRoute });
|
||||
if (url === '/api/routes/1' && method === 'DELETE') return jsonResponse({ deleted: true });
|
||||
if (url === '/api/routes/1' && method === 'PUT') {
|
||||
const body = JSON.parse(options.body);
|
||||
return jsonResponse({ route: { ...serverRoute, ...body } });
|
||||
@@ -262,7 +269,7 @@ function buildEditorContext() {
|
||||
const LocalRoutes = {
|
||||
createPoiId: () => 9001,
|
||||
createRouteId: () => 'local-new',
|
||||
emptyGpx: name => `<gpx><metadata><name>${name}</name></metadata></gpx>`,
|
||||
emptyGpx: name => `<gpx><metadata><name>${name}</name></metadata><trk><trkseg></trkseg></trk></gpx>`,
|
||||
fileToDataUrl: async file => ({
|
||||
dataUrl: `data:${file.type || 'application/octet-stream'};base64,TEST`,
|
||||
name: file.name,
|
||||
@@ -270,17 +277,23 @@ function buildEditorContext() {
|
||||
type: file.type || 'application/octet-stream'
|
||||
}),
|
||||
get: id => packages.get(String(id)) || null,
|
||||
gpxForUpload: routePackage => routePackage.gpx,
|
||||
list: () => [...packages.values()],
|
||||
parseWaypoints: () => [],
|
||||
readFileAsText: file => file.text(),
|
||||
remove: id => packages.delete(String(id)),
|
||||
save: input => {
|
||||
const id = String(input.id);
|
||||
const now = '2026-09-19T14:00:00.000Z';
|
||||
const pois = input.pois.map((poi, sequence) => ({ ...poi, sequence }));
|
||||
const trackPoints = pois
|
||||
.map(poi => `<trkpt lat="${poi.lat}" lon="${poi.lon}"><name>${poi.title || ''}</name><extensions><wegwichtel:poiId>${poi.id}</wegwichtel:poiId></extensions></trkpt>`)
|
||||
.join('');
|
||||
const saved = {
|
||||
id,
|
||||
gpx: input.gpx,
|
||||
gpx: `<gpx><trk><trkseg>${trackPoints}</trkseg></trk></gpx>`,
|
||||
route: { ...input.route, id, updatedAt: now },
|
||||
pois: input.pois.map((poi, sequence) => ({ ...poi, sequence }))
|
||||
pois
|
||||
};
|
||||
packages.set(id, saved);
|
||||
saves.push(structuredClone(saved));
|
||||
@@ -307,7 +320,7 @@ function buildEditorContext() {
|
||||
return { calls, context, elements, packages, saves };
|
||||
}
|
||||
|
||||
test('public editor workflow saves new route and GPX POI locally without POSTing a route', async () => {
|
||||
test('public editor workflow saves new route and GPX track point locally without POSTing a route', async () => {
|
||||
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
||||
const source = await fs.readFile(sourcePath, 'utf8');
|
||||
const { calls, context, elements, packages, saves } = buildEditorContext();
|
||||
@@ -364,6 +377,12 @@ test('public editor workflow saves new route and GPX POI locally without POSTing
|
||||
assert.match(local.pois[0].audio.dataUrl, /^data:audio\/mpeg/);
|
||||
assert.equal(local.pois[0].images.length, 1);
|
||||
assert.match(local.pois[0].images[0].dataUrl, /^data:image\/jpeg/);
|
||||
assert.match(local.gpx, /<trkpt lat="50\.123456789" lon="10\.987654321">/);
|
||||
|
||||
const pointListBeforeUpload = elements.get('editor-poi-list');
|
||||
const pointItemBeforeUpload = pointListBeforeUpload.children.find(child => child.dataset.poiId);
|
||||
assert.equal(pointItemBeforeUpload.children[1].children[3].textContent, '×');
|
||||
assert.match(pointItemBeforeUpload.children[1].children[3].className, /editor-icon-danger/);
|
||||
|
||||
elements.get('editor-route-name').value = '--- Neue lokale Strecke ---';
|
||||
elements.get('editor-route-upload').dispatchEvent(pointerEvent());
|
||||
@@ -377,6 +396,7 @@ test('public editor workflow saves new route and GPX POI locally without POSTing
|
||||
const gpxUpload = routeUpload.body.entries.find(([name]) => name === 'gpx');
|
||||
assert.ok(gpxUpload);
|
||||
assert.equal(gpxUpload[2], 'Neue-lokale-Strecke.gpx');
|
||||
assert.match(await gpxUpload[1].text(), /<trkpt lat=\"50\.123456789\" lon=\"10\.987654321\">/);
|
||||
assert.ok(calls.some(call => call.method === 'POST' && call.url === '/api/routes/22/pois'));
|
||||
assert.ok(calls.some(call => call.method === 'POST' && call.url === '/api/routes/22/pois/23/audio'));
|
||||
assert.ok(calls.some(call => call.method === 'POST' && call.url === '/api/routes/22/pois/23/pictures'));
|
||||
@@ -425,6 +445,50 @@ test('public editor workflow saves new route and GPX POI locally without POSTing
|
||||
assert.equal(elements.get('editor-button').attributes.get('aria-expanded'), 'false');
|
||||
});
|
||||
|
||||
test('route and track point list expose red delete actions and remove local data', async () => {
|
||||
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
||||
const source = await fs.readFile(sourcePath, 'utf8');
|
||||
const { context, elements, packages } = buildEditorContext();
|
||||
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
await context.Wegwichtel.EditorUi.openNewLocalRoute();
|
||||
elements.get('editor-route-name').value = 'Löschtest';
|
||||
elements.get('editor-route-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
elements.get('editor-add-poi').dispatchEvent(pointerEvent());
|
||||
elements.get('editor-poi-title').value = 'Trackpunkt zum Löschen';
|
||||
elements.get('editor-poi-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
let pointList = elements.get('editor-poi-list');
|
||||
let pointItem = pointList.children.find(child => child.dataset.poiId);
|
||||
const pointDelete = pointItem.children[1].children[3];
|
||||
assert.equal(pointDelete.textContent, '×');
|
||||
assert.match(pointDelete.className, /editor-icon-danger/);
|
||||
pointList.dispatchEvent(pointerEvent(pointDelete));
|
||||
await tick();
|
||||
await tick();
|
||||
assert.equal(packages.get('local-new').pois.length, 0);
|
||||
|
||||
elements.get('editor-route-back').dispatchEvent(pointerEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
const routeList = elements.get('editor-route-list');
|
||||
const localItem = routeList.children.find(child => child.dataset.routeId === 'local-new');
|
||||
const routeDelete = localItem.children[1].children[1];
|
||||
assert.equal(routeDelete.textContent, '×');
|
||||
assert.match(routeDelete.className, /editor-icon-danger/);
|
||||
routeList.dispatchEvent(pointerEvent(routeDelete));
|
||||
await tick();
|
||||
await tick();
|
||||
assert.equal(packages.has('local-new'), false);
|
||||
assert.equal(elements.get('editor-status').textContent, 'Strecke gelöscht.');
|
||||
});
|
||||
|
||||
|
||||
test('editing an activated server route uses PUT and never writes activation state', async () => {
|
||||
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
||||
const source = await fs.readFile(sourcePath, 'utf8');
|
||||
|
||||
@@ -324,7 +324,7 @@ test('local route package stores GPX and JSON separately and reconstructs an app
|
||||
|
||||
const saved = api.save({
|
||||
id: routeId,
|
||||
gpx: sampleGpx,
|
||||
gpx: api.emptyGpx('Neue Route'),
|
||||
route: {
|
||||
id: routeId,
|
||||
name: 'Neue Route',
|
||||
@@ -365,9 +365,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, /<wpt lat="50\.1" lon="10\.1">/);
|
||||
assert.match(gpx, /<trkpt lat="50\.1" lon="10\.1">/);
|
||||
assert.match(gpx, /<wegwichtel:poiId>101<\/wegwichtel:poiId>/);
|
||||
assert.ok(gpx.indexOf('<wpt') < gpx.indexOf('<trk>'), 'GPX waypoints must be serialized before the track');
|
||||
assert.doesNotMatch(gpx, /<wpt/);
|
||||
assert.equal((gpx.match(/<trkpt /g) || []).length, 2);
|
||||
assert.equal(metadata.route.name, 'Neue Route');
|
||||
assert.equal(metadata.route.serverRouteId, 42);
|
||||
assert.equal(metadata.route.uploadedAt, '2026-09-27T18:00:00.000Z');
|
||||
@@ -383,7 +384,7 @@ test('local route package stores GPX and JSON separately and reconstructs an app
|
||||
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(Math.round(appRoute.elevationGainM), 12);
|
||||
assert.equal(Math.round(appRoute.elevationGainM), 0);
|
||||
const listed = api.listAppRoutes();
|
||||
assert.equal(listed.length, 1);
|
||||
assert.equal(listed[0].id, routeId);
|
||||
@@ -405,10 +406,49 @@ test('GPX waypoint import reads stable POI ids and validates coordinates', async
|
||||
|
||||
const waypointOnly = api.syncGpx(
|
||||
`<gpx version="1.1" xmlns="${GPX_NAMESPACE}"></gpx>`,
|
||||
{ name: 'Nur POI' },
|
||||
[{ id: 2, title: 'Einzelner POI', lat: 50.2, lon: 10.2, sequence: 0 }]
|
||||
{ name: 'Nur Trackpunkt' },
|
||||
[{ id: 2, title: 'Einzelner Trackpunkt', lat: 50.2, lon: 10.2, sequence: 0 }]
|
||||
);
|
||||
assert.match(waypointOnly, /<wpt lat="50.2" lon="10.2">/);
|
||||
assert.match(waypointOnly, /<trkpt lat="50.2" lon="10.2">/);
|
||||
assert.doesNotMatch(waypointOnly, /<wpt\b/);
|
||||
});
|
||||
|
||||
test('local points are stored directly as managed GPX track points and uploaded unchanged', async () => {
|
||||
const { api } = await loadLocalRoutes();
|
||||
const saved = api.save({
|
||||
id: 'local-upload',
|
||||
gpx: api.emptyGpx('Lokale Strecke'),
|
||||
route: { name: 'Lokale Strecke' },
|
||||
pois: [
|
||||
{ id: 1, title: 'Start', lat: 50.123456789, lon: 10.987654321, sequence: 0 },
|
||||
{ id: 2, title: 'Ziel', lat: 50.223456789, lon: 11.087654321, sequence: 1 }
|
||||
]
|
||||
});
|
||||
|
||||
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, /<wegwichtel:poiId>1<\/wegwichtel:poiId>/);
|
||||
assert.equal((saved.gpx.match(/<trkpt /g) || []).length, 2);
|
||||
assert.doesNotMatch(saved.gpx, /<wpt\b/);
|
||||
assert.equal(api.gpxForUpload(saved), saved.gpx, 'upload must use the already stored track without conversion');
|
||||
});
|
||||
|
||||
test('sync keeps imported foreign track points and upload rejects a route without track points', async () => {
|
||||
const { api } = await loadLocalRoutes();
|
||||
const synced = api.syncGpx(sampleGpx, { name: 'Import' }, [
|
||||
{ id: 200, title: 'Neuer Trackpunkt', lat: 50.2, lon: 10.2, sequence: 0 }
|
||||
]);
|
||||
|
||||
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(api.gpxForUpload({ gpx: sampleGpx, route: { name: 'Import' }, pois: [] }), sampleGpx);
|
||||
assert.throws(() => api.gpxForUpload({
|
||||
gpx: api.emptyGpx('Leer'),
|
||||
route: { name: 'Leer' },
|
||||
pois: []
|
||||
}), /noch keine GPX-Trackpunkte/);
|
||||
});
|
||||
|
||||
test('remove updates index and dispatches a local-route change event', async () => {
|
||||
|
||||
Reference in New Issue
Block a user