add upload
Sonarqube Scanner / Build and analyze (push) Failing after 1m4s

This commit is contained in:
Florian Zumpe
2026-09-27 18:11:08 +02:00
parent becd48b487
commit d2f31e39e8
5 changed files with 197 additions and 14 deletions
+7 -4
View File
@@ -238,7 +238,7 @@
</label> </label>
<h3>5. Lokale Streckenentwürfe</h3> <h3>5. Lokale Streckenentwürfe</h3>
<p>Neu angelegte Strecken werden zunächst ausschließlich im Local Storage dieses Browsers gespeichert. Die GPX-Daten enthalten den Streckenverlauf und die POI-Koordinaten. Ergänzende JSON-Daten ordnen den POIs Texte, Audiodateien und Bilder zu. Medien werden dafür als lokale Browserdaten gespeichert.</p> <p>Neu angelegte Strecken werden zunächst ausschließlich im Local Storage dieses Browsers gespeichert. Die GPX-Daten enthalten den Streckenverlauf und die POI-Koordinaten. Ergänzende JSON-Daten ordnen den POIs Texte, Audiodateien und Bilder zu. Medien werden dafür als lokale Browserdaten gespeichert.</p>
<p>Lokale Streckenentwürfe sind bewusst von der freiwilligen Fortschrittsspeicherung getrennt. Sie werden nicht durch die 24-Stunden-Bereinigung entfernt und beim Ausschalten der Fortschrittsspeicherung nicht gelöscht. Sie verbleiben im Browser, bis sie ausdrücklich gelöscht werden oder die Browserdaten entfernt werden. Eine Übertragung an den Server findet derzeit nicht statt.</p> <p>Lokale Streckenentwürfe sind bewusst von der freiwilligen Fortschrittsspeicherung getrennt. Sie werden nicht durch die 24-Stunden-Bereinigung entfernt und beim Ausschalten der Fortschrittsspeicherung nicht gelöscht. Sie verbleiben im Browser, bis sie ausdrücklich gelöscht werden oder die Browserdaten entfernt werden. Eine Übertragung an den Server erfolgt nur nach ausdrücklicher Betätigung des Upload-Buttons im Editor.</p>
<h3>6. Cookies und Tracking</h3> <h3>6. Cookies und Tracking</h3>
<p>Die Anwendung setzt derzeit keine Cookies. Sie verwendet keine Reichweitenmessung, keine personalisierte Werbung und keine Trackingdienste. Eine technisch notwendige Local-Storage-Einstellung merkt sich lediglich, ob die freiwillige Speicherung des Streckenfortschritts aktiviert wurde.</p> <p>Die Anwendung setzt derzeit keine Cookies. Sie verwendet keine Reichweitenmessung, keine personalisierte Werbung und keine Trackingdienste. Eine technisch notwendige Local-Storage-Einstellung merkt sich lediglich, ob die freiwillige Speicherung des Streckenfortschritts aktiviert wurde.</p>
<h3>7. Inhalte von Schülerinnen und Schülern</h3> <h3>7. Inhalte von Schülerinnen und Schülern</h3>
@@ -301,7 +301,7 @@
</label> </label>
<h3>Local Storage</h3> <h3>Local Storage</h3>
<p>Der Browser kann auf Wunsch Streckenfortschritte lokal speichern. Verwendet werden die Schlüssel <code>wegwichtel.privacy.v1</code> für die gewählte Einstellung und <code>wegwichtel.route-progress.v1</code> für die freiwilligen Streckendaten. Die Streckendaten werden nach 24 Stunden bereinigt oder beim Ausschalten sofort entfernt.</p> <p>Der Browser kann auf Wunsch Streckenfortschritte lokal speichern. Verwendet werden die Schlüssel <code>wegwichtel.privacy.v1</code> für die gewählte Einstellung und <code>wegwichtel.route-progress.v1</code> für die freiwilligen Streckendaten. Die Streckendaten werden nach 24 Stunden bereinigt oder beim Ausschalten sofort entfernt.</p>
<p>Lokale Streckenentwürfe werden getrennt davon unter <code>wegwichtel.local-routes.v1.*</code> gespeichert. Pro Entwurf werden GPX- und JSON-Daten vorgehalten; Bilder und Audiodateien liegen als Bestandteil der lokalen JSON-Daten im Browser. Diese Entwürfe werden nicht nach 24 Stunden bereinigt und sind nicht vom Schalter für den Streckenfortschritt abhängig. Durch Mediendateien kann das Speicherlimit des Browsers vergleichsweise schnell erreicht werden.</p> <p>Lokale Streckenentwürfe werden getrennt davon unter <code>wegwichtel.local-routes.v1.*</code> gespeichert. Pro Entwurf werden GPX- und JSON-Daten vorgehalten; Bilder und Audiodateien liegen als Bestandteil der lokalen JSON-Daten im Browser. Diese Entwürfe werden nicht nach 24 Stunden bereinigt und sind nicht vom Schalter für den Streckenfortschritt abhängig. Durch Mediendateien kann das Speicherlimit des Browsers vergleichsweise schnell erreicht werden. Eine Serverübertragung erfolgt ausschließlich nach ausdrücklicher Betätigung des Upload-Buttons im Editor.</p>
<label class="switch-row compact-switch"> <label class="switch-row compact-switch">
<span> <span>
<strong>Streckenfortschritt lokal speichern</strong> <strong>Streckenfortschritt lokal speichern</strong>
@@ -365,9 +365,12 @@
<label for="editor-route-gpx">GPX-Datei importieren oder ersetzen</label> <label for="editor-route-gpx">GPX-Datei importieren oder ersetzen</label>
<input id="editor-route-gpx" type="file" accept=".gpx,application/gpx+xml,text/xml,application/xml"> <input id="editor-route-gpx" type="file" accept=".gpx,application/gpx+xml,text/xml,application/xml">
<p id="editor-route-activated" class="editor-muted">Neue Strecken werden als lokaler GPX-/JSON-Entwurf gespeichert und nicht an den Server übertragen.</p> <p id="editor-route-activated" class="editor-muted">Neue Strecken werden zunächst lokal gespeichert. Der Upload-Button überträgt den Entwurf als unveröffentlichte Strecke zur Prüfung an den Server.</p>
<button id="editor-route-save" class="editor-primary-button" type="submit">Strecke speichern</button> <div class="editor-form-actions">
<button id="editor-route-save" class="editor-primary-button" type="submit">Strecke speichern</button>
<button id="editor-route-upload" class="editor-secondary-button" type="button" hidden>An Server übertragen</button>
</div>
</form> </form>
<div class="editor-toolbar editor-poi-toolbar"> <div class="editor-toolbar editor-poi-toolbar">
<h3>Stationen</h3> <h3>Stationen</h3>
+140 -5
View File
@@ -42,6 +42,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
routeListView: 'editor-route-list-view', routeListView: 'editor-route-list-view',
routeName: 'editor-route-name', routeName: 'editor-route-name',
routeSave: 'editor-route-save', routeSave: 'editor-route-save',
routeUpload: 'editor-route-upload',
status: 'editor-status' status: 'editor-status'
}); });
@@ -240,7 +241,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function handleResponse(response) { async function handleResponse(response) {
if (response.ok) return parseJson(response); if (response.ok) return parseJson(response);
const message = await response.text(); const message = await response.text();
throw new Error(message || `HTTP ${response.status}`); const error = new Error(message || `HTTP ${response.status}`);
error.status = response.status;
throw error;
} }
async function readJson(path) { async function readJson(path) {
@@ -468,9 +471,19 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
byId(ids.routeName).value = routeName(route) === 'Unbenannte Strecke' ? '' : routeName(route); byId(ids.routeName).value = routeName(route) === 'Unbenannte Strecke' ? '' : routeName(route);
byId(ids.routeDescription).value = text(route?.description); byId(ids.routeDescription).value = text(route?.description);
byId(ids.routeGpx).value = ''; byId(ids.routeGpx).value = '';
const uploadButton = byId(ids.routeUpload);
if (state.currentSource === 'local') { if (state.currentSource === 'local') {
byId(ids.routeActivated).textContent = 'Lokaler Entwurf: GPX, Texte und Medien bleiben ausschließlich im Browser. Eine Server-Freigabe ist noch nicht implementiert.'; const uploaded = Boolean(route?.serverRouteId);
setHidden(uploadButton, false);
setDisabled(uploadButton, uploaded);
if (uploadButton) uploadButton.textContent = uploaded ? 'Bereits übertragen' : 'An Server übertragen';
byId(ids.routeActivated).textContent = uploaded
? `Lokaler Entwurf. Bereits als unveröffentlichte Server-Strecke ${route.serverRouteId} zur Prüfung übertragen.`
: 'Lokaler Entwurf: GPX, Texte und Medien bleiben im Browser, bis „An Server übertragen“ gewählt wird.';
} else { } else {
setHidden(uploadButton, true);
setDisabled(uploadButton, true);
byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at
? `Veröffentlicht am ${route.activatedAt || route.activated_at}. Der Editor verändert die Freigabe nicht.` ? `Veröffentlicht am ${route.activatedAt || route.activated_at}. Der Editor verändert die Freigabe nicht.`
: 'Server-Strecke ohne Freigabedatum. Der Editor verändert activated_at nicht.'; : 'Server-Strecke ohne Freigabedatum. Der Editor verändert activated_at nicht.';
@@ -697,7 +710,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const file = byId(ids.routeGpx)?.files?.[0]; const file = byId(ids.routeGpx)?.files?.[0];
data.append('name', values.name); data.append('name', values.name);
data.append('description', values.description); data.append('description', values.description);
if (file) data.append('file', file); if (file) data.append('gpx', file);
return data; return data;
} }
@@ -727,7 +740,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.pois = saved.pois; state.pois = saved.pois;
await loadRoutes(); await loadRoutes();
await loadRoute({ id: saved.id, _source: 'local' }); await loadRoute({ id: saved.id, _source: 'local' });
setStatus('Strecke erfolgreich lokal gespeichert. Es wurden keine Daten an den Server übertragen.'); setStatus('Speichern erfolgreich.');
} }
async function saveServerRoute(values) { async function saveServerRoute(values) {
@@ -742,7 +755,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.currentRoute = { ...saved, _source: 'server' }; state.currentRoute = { ...saved, _source: 'server' };
await loadRoutes(); await loadRoutes();
await loadRoute({ id, _source: 'server' }); await loadRoute({ id, _source: 'server' });
setStatus('Strecke erfolgreich gespeichert. activated_at wurde nicht verändert.'); setStatus('Speichern erfolgreich.');
} }
async function saveRoute() { async function saveRoute() {
@@ -752,6 +765,127 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
else await saveLocalRoute(values); else await saveLocalRoute(values);
} }
function safeGpxFilename(name) {
const base = text(name || 'wegwichtel-route')
.normalize('NFKD')
.replaceAll(/[^a-zA-Z0-9_-]+/g, '-')
.replaceAll(/^-+|-+$/g, '')
.slice(0, 80);
return `${base || 'wegwichtel-route'}.gpx`;
}
function localRouteUploadForm(routePackage) {
const data = new FormData();
data.append('name', routePackage.route?.name || 'Unbenannte Strecke');
data.append('description', routePackage.route?.description || '');
const gpx = new Blob([routePackage.gpx], { type: 'application/gpx+xml' });
data.append('gpx', gpx, safeGpxFilename(routePackage.route?.name));
return data;
}
async function storedMediaBlob(media) {
if (!media?.dataUrl) return null;
const response = await fetch(media.dataUrl);
if (!response.ok || typeof response.blob !== 'function') {
throw new Error(`Die Mediendatei ${media.name || ''} konnte nicht für den Upload vorbereitet werden.`.trim());
}
return response.blob();
}
async function uploadStoredMedia(path, fieldName, media, fallbackName) {
const blob = await storedMediaBlob(media);
if (!blob) return;
const data = new FormData();
data.append(fieldName, blob, media.name || fallbackName);
await sendForm(path, 'POST', data);
}
async function uploadLocalPoiMedia(serverRouteId, serverPoiId, poi) {
const basePath = `/routes/${encodeURIComponent(serverRouteId)}/pois/${encodeURIComponent(serverPoiId)}`;
if (poi.audio?.dataUrl) {
await uploadStoredMedia(`${basePath}/audio`, 'audio', poi.audio, 'audio.bin');
}
for (const [index, image] of (poi.images || []).entries()) {
if (!image?.dataUrl) continue;
await uploadStoredMedia(`${basePath}/pictures`, 'picture', image, `bild-${index + 1}.bin`);
}
}
async function createServerRouteFromLocal(routePackage) {
let createdRouteId = null;
try {
const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage));
const serverRoute = payload.route || payload;
createdRouteId = routeId(serverRoute);
if (!createdRouteId) throw new Error('Der Server hat keine Strecken-ID für den Upload zurückgegeben.');
const orderedPois = [...(routePackage.pois || [])]
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
for (const [index, poi] of orderedPois.entries()) {
const poiPayload = await sendJson(`/routes/${encodeURIComponent(createdRouteId)}/pois`, 'POST', {
description: poi.description || '',
lat: poi.lat,
lon: poi.lon,
sequence: index,
title: poi.title || `Station ${index + 1}`,
triggerRadiusM: poi.triggerRadiusM || 25
});
const serverPoi = poiPayload.poi || poiPayload;
const serverPoiId = poiId(serverPoi);
if (!serverPoiId) throw new Error(`Für ${poi.title || `Station ${index + 1}`} wurde keine Server-POI-ID zurückgegeben.`);
await uploadLocalPoiMedia(createdRouteId, serverPoiId, poi);
}
return createdRouteId;
} catch (error) {
if (createdRouteId) {
try {
await sendDelete(`/routes/${encodeURIComponent(createdRouteId)}`);
} catch {
// Der ursprüngliche Uploadfehler ist wichtiger als ein fehlgeschlagener Rollback.
}
}
throw error;
}
}
async function uploadLocalRoute() {
if (state.currentSource !== 'local') throw new Error('Nur lokale Strecken können hochgeladen werden.');
const values = routeFromForm();
if (!values.name) throw new Error('Der Name der Strecke ist erforderlich.');
if (state.currentRoute?.serverRouteId) {
setStatus('Diese Strecke wurde bereits an den Server übertragen.');
return;
}
const uploadButton = byId(ids.routeUpload);
setDisabled(uploadButton, true);
try {
await saveLocalRoute(values);
setStatus('Übertragung läuft …');
const localId = routeId(state.currentRoute);
const routePackage = ns.LocalRoutes.get(localId);
if (!routePackage) throw new Error('Der lokale Streckenentwurf wurde nicht gefunden.');
const serverRouteId = await createServerRouteFromLocal(routePackage);
const uploadedAt = new Date().toISOString();
const saved = ns.LocalRoutes.save({
...routePackage,
route: {
...routePackage.route,
serverRouteId,
uploadedAt
}
});
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
state.pois = saved.pois;
fillRouteForm(state.currentRoute);
await loadRoutes();
setStatus('Upload erfolgreich.');
} finally {
if (!state.currentRoute?.serverRouteId) setDisabled(uploadButton, false);
}
}
function currentPosition() { function currentPosition() {
return typeof ns.App?.currentPosition === 'function' ? ns.App.currentPosition() : null; return typeof ns.App?.currentPosition === 'function' ? ns.App.currentPosition() : null;
} }
@@ -1072,6 +1206,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
activate(byId(ids.close), closeEditor); activate(byId(ids.close), closeEditor);
activate(byId(ids.newRoute), newRoute); activate(byId(ids.newRoute), newRoute);
activate(byId(ids.routeBack), () => guarded(loadRoutes).then(showListView)); activate(byId(ids.routeBack), () => guarded(loadRoutes).then(showListView));
activate(byId(ids.routeUpload), () => guarded(uploadLocalRoute));
activate(byId(ids.addPoi), newPoi); activate(byId(ids.addPoi), newPoi);
activate(byId(ids.poiAudioAdd), () => openFilePicker(ids.poiAudio)); activate(byId(ids.poiAudioAdd), () => openFilePicker(ids.poiAudio));
activate(byId(ids.poiImagesAdd), () => openFilePicker(ids.poiImages)); activate(byId(ids.poiImagesAdd), () => openFilePicker(ids.poiImages));
+3 -1
View File
@@ -231,7 +231,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
name: String(inputRoute.name || ''), name: String(inputRoute.name || ''),
description: String(inputRoute.description || ''), description: String(inputRoute.description || ''),
createdAt: inputRoute.createdAt || now, createdAt: inputRoute.createdAt || now,
updatedAt: now updatedAt: now,
serverRouteId: inputRoute.serverRouteId ?? null,
uploadedAt: inputRoute.uploadedAt || null
}; };
const pois = (routePackage.pois || []).map((poi, index) => ({ const pois = (routePackage.pois || []).map((poi, index) => ({
...poi, ...poi,
+38 -3
View File
@@ -34,6 +34,7 @@ test('editor markup and source describe local draft creation instead of immediat
assert.doesNotMatch(html, /id="editor-poi-(?:lat|lon)"[^>]+step=/); assert.doesNotMatch(html, /id="editor-poi-(?:lat|lon)"[^>]+step=/);
assert.match(html, /id="editor-poi-media-preview"/); assert.match(html, /id="editor-poi-media-preview"/);
assert.match(html, /id="editor-poi-delete"[^>]+editor-danger-button/); assert.match(html, /id="editor-poi-delete"[^>]+editor-danger-button/);
assert.match(html, /id="editor-route-upload"[^>]+type="button"[^>]+hidden/);
assert.doesNotMatch(html, /accept="video\//); assert.doesNotMatch(html, /accept="video\//);
assert.match(html, /ausschließlich in diesem Browser/i); assert.match(html, /ausschließlich in diesem Browser/i);
@@ -61,6 +62,8 @@ test('editor markup and source describe local draft creation instead of immediat
assert.match(ui, /'picture'/); assert.match(ui, /'picture'/);
assert.match(ui, /'audio'/); assert.match(ui, /'audio'/);
assert.match(ui, /method:\s*'DELETE'/); assert.match(ui, /method:\s*'DELETE'/);
assert.match(ui, /sendForm\('\/routes',\s*'POST'/);
assert.match(ui, /serverRouteId/);
assert.match(ui, /position\?\.accuracy/); assert.match(ui, /position\?\.accuracy/);
}); });
@@ -140,8 +143,8 @@ class FakeFormData {
this.entries = []; this.entries = [];
} }
append(name, value) { append(name, value, filename) {
this.entries.push([name, value]); this.entries.push([name, value, filename]);
} }
} }
@@ -203,6 +206,7 @@ function buildEditorContext() {
'editor-route-list', 'editor-route-list',
'editor-route-list-view', 'editor-route-list-view',
'editor-route-name', 'editor-route-name',
'editor-route-upload',
'editor-status' 'editor-status'
]; ];
const elements = new Map(ids.map(id => [id, new FakeElement(id)])); const elements = new Map(ids.map(id => [id, new FakeElement(id)]));
@@ -233,7 +237,14 @@ function buildEditorContext() {
async function fetch(url, options = {}) { async function fetch(url, options = {}) {
const method = options.method || 'GET'; const method = options.method || 'GET';
calls.push({ body: options.body, method, url }); calls.push({ body: options.body, method, url });
if (String(url).startsWith('data:')) {
return { ok: true, status: 200, blob: async () => new Blob(['media'], { type: 'application/octet-stream' }) };
}
if (url === '/api/routes?activatedOnly=true' && method === 'GET') return jsonResponse({ routes: [serverRoute] }); if (url === '/api/routes?activatedOnly=true' && method === 'GET') return jsonResponse({ routes: [serverRoute] });
if (url === '/api/routes' && method === 'POST') return jsonResponse({ id: 22, name: 'Neue lokale Strecke', pois: [] }, true, 201);
if (url === '/api/routes/22/pois' && method === 'POST') return jsonResponse({ id: 23, title: 'Aussicht' }, true, 201);
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 === 'GET') return jsonResponse({ route: serverRoute });
if (url === '/api/routes/1' && method === 'PUT') { if (url === '/api/routes/1' && method === 'PUT') {
const body = JSON.parse(options.body); const body = JSON.parse(options.body);
@@ -278,6 +289,7 @@ function buildEditorContext() {
}; };
const context = { const context = {
Blob,
document, document,
Event, Event,
EventTarget, EventTarget,
@@ -318,7 +330,9 @@ test('public editor workflow saves new route and GPX POI locally without POSTing
assert.equal(saves.at(-1).route.name, 'Neue lokale Strecke'); assert.equal(saves.at(-1).route.name, 'Neue lokale Strecke');
assert.equal(calls.some(call => call.method === 'POST' && call.url === '/api/routes'), false); assert.equal(calls.some(call => call.method === 'POST' && call.url === '/api/routes'), false);
assert.match(elements.get('editor-route-activated').textContent, /Lokaler Entwurf/); assert.match(elements.get('editor-route-activated').textContent, /Lokaler Entwurf/);
assert.match(elements.get('editor-status').textContent, /erfolgreich lokal gespeichert/i); assert.equal(elements.get('editor-status').textContent, 'Speichern erfolgreich.');
assert.equal(elements.get('editor-route-upload').hidden, false);
assert.equal(elements.get('editor-route-upload').disabled, false);
elements.get('editor-add-poi').dispatchEvent(pointerEvent()); elements.get('editor-add-poi').dispatchEvent(pointerEvent());
assert.equal(elements.get('editor-poi-audio').clickCount, 0); assert.equal(elements.get('editor-poi-audio').clickCount, 0);
@@ -351,6 +365,25 @@ test('public editor workflow saves new route and GPX POI locally without POSTing
assert.equal(local.pois[0].images.length, 1); assert.equal(local.pois[0].images.length, 1);
assert.match(local.pois[0].images[0].dataUrl, /^data:image\/jpeg/); assert.match(local.pois[0].images[0].dataUrl, /^data:image\/jpeg/);
elements.get('editor-route-upload').dispatchEvent(pointerEvent());
await tick();
await tick();
await tick();
await tick();
const routeUpload = calls.find(call => call.method === 'POST' && call.url === '/api/routes');
assert.ok(routeUpload);
assert.ok(routeUpload.body.entries.some(([name]) => name === 'gpx'));
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'));
local = packages.get('local-new');
assert.equal(local.route.serverRouteId, '22');
assert.ok(local.route.uploadedAt);
assert.equal(elements.get('editor-route-upload').disabled, true);
assert.equal(elements.get('editor-route-upload').textContent, 'Bereits übertragen');
assert.equal(elements.get('editor-status').textContent, 'Upload erfolgreich.');
const poiList = elements.get('editor-poi-list'); const poiList = elements.get('editor-poi-list');
const poiItem = poiList.children.find(child => child.dataset.poiId); const poiItem = poiList.children.find(child => child.dataset.poiId);
const poiEditButton = poiItem.children[1].children[2]; const poiEditButton = poiItem.children[1].children[2];
@@ -406,6 +439,7 @@ test('editing an activated server route uses PUT and never writes activation sta
assert.equal(elements.get('editor-route-id').value, '1'); assert.equal(elements.get('editor-route-id').value, '1');
assert.match(elements.get('editor-route-activated').textContent, /Veröffentlicht am/); assert.match(elements.get('editor-route-activated').textContent, /Veröffentlicht am/);
assert.equal(elements.get('editor-route-upload').hidden, true);
elements.get('editor-route-name').value = 'Freigegebener Waldweg geändert'; elements.get('editor-route-name').value = 'Freigegebener Waldweg geändert';
elements.get('editor-route-description').value = 'Bearbeitet'; elements.get('editor-route-description').value = 'Bearbeitet';
@@ -419,4 +453,5 @@ test('editing an activated server route uses PUT and never writes activation sta
assert.deepEqual(Object.keys(body).sort(), ['description', 'name']); assert.deepEqual(Object.keys(body).sort(), ['description', 'name']);
assert.equal('activated_at' in body, false); assert.equal('activated_at' in body, false);
assert.equal('activatedAt' in body, false); assert.equal('activatedAt' in body, false);
assert.equal(elements.get('editor-status').textContent, 'Speichern erfolgreich.');
}); });
+9 -1
View File
@@ -325,7 +325,13 @@ test('local route package stores GPX and JSON separately and reconstructs an app
const saved = api.save({ const saved = api.save({
id: routeId, id: routeId,
gpx: sampleGpx, gpx: sampleGpx,
route: { id: routeId, name: 'Neue Route', description: 'Lokal gespeichert' }, route: {
id: routeId,
name: 'Neue Route',
description: 'Lokal gespeichert',
serverRouteId: 42,
uploadedAt: '2026-09-27T18:00:00.000Z'
},
pois: [ pois: [
{ {
id: 101, id: 101,
@@ -363,6 +369,8 @@ test('local route package stores GPX and JSON separately and reconstructs an app
assert.match(gpx, /<wegwichtel:poiId>101<\/wegwichtel:poiId>/); 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.ok(gpx.indexOf('<wpt') < gpx.indexOf('<trk>'), 'GPX waypoints must be serialized before the track');
assert.equal(metadata.route.name, 'Neue Route'); assert.equal(metadata.route.name, 'Neue Route');
assert.equal(metadata.route.serverRouteId, 42);
assert.equal(metadata.route.uploadedAt, '2026-09-27T18:00:00.000Z');
assert.equal(metadata.pois[0].audio.name, 'start.mp3'); assert.equal(metadata.pois[0].audio.name, 'start.mp3');
assert.equal('gpx' in metadata, false); assert.equal('gpx' in metadata, false);
assert.equal('_source' in metadata.route, false); assert.equal('_source' in metadata.route, false);