This commit is contained in:
+6
-3
@@ -238,7 +238,7 @@
|
||||
</label>
|
||||
<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>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>
|
||||
<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>
|
||||
@@ -301,7 +301,7 @@
|
||||
</label>
|
||||
<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>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">
|
||||
<span>
|
||||
<strong>Streckenfortschritt lokal speichern</strong>
|
||||
@@ -365,9 +365,12 @@
|
||||
<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">
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<div class="editor-toolbar editor-poi-toolbar">
|
||||
<h3>Stationen</h3>
|
||||
|
||||
+140
-5
@@ -42,6 +42,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
routeListView: 'editor-route-list-view',
|
||||
routeName: 'editor-route-name',
|
||||
routeSave: 'editor-route-save',
|
||||
routeUpload: 'editor-route-upload',
|
||||
status: 'editor-status'
|
||||
});
|
||||
|
||||
@@ -240,7 +241,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
async function handleResponse(response) {
|
||||
if (response.ok) return parseJson(response);
|
||||
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) {
|
||||
@@ -468,9 +471,19 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
byId(ids.routeName).value = routeName(route) === 'Unbenannte Strecke' ? '' : routeName(route);
|
||||
byId(ids.routeDescription).value = text(route?.description);
|
||||
byId(ids.routeGpx).value = '';
|
||||
|
||||
const uploadButton = byId(ids.routeUpload);
|
||||
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 {
|
||||
setHidden(uploadButton, true);
|
||||
setDisabled(uploadButton, true);
|
||||
byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at
|
||||
? `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.';
|
||||
@@ -697,7 +710,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
const file = byId(ids.routeGpx)?.files?.[0];
|
||||
data.append('name', values.name);
|
||||
data.append('description', values.description);
|
||||
if (file) data.append('file', file);
|
||||
if (file) data.append('gpx', file);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -727,7 +740,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
state.pois = saved.pois;
|
||||
await loadRoutes();
|
||||
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) {
|
||||
@@ -742,7 +755,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
state.currentRoute = { ...saved, _source: 'server' };
|
||||
await loadRoutes();
|
||||
await loadRoute({ id, _source: 'server' });
|
||||
setStatus('Strecke erfolgreich gespeichert. activated_at wurde nicht verändert.');
|
||||
setStatus('Speichern erfolgreich.');
|
||||
}
|
||||
|
||||
async function saveRoute() {
|
||||
@@ -752,6 +765,127 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
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() {
|
||||
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.newRoute), newRoute);
|
||||
activate(byId(ids.routeBack), () => guarded(loadRoutes).then(showListView));
|
||||
activate(byId(ids.routeUpload), () => guarded(uploadLocalRoute));
|
||||
activate(byId(ids.addPoi), newPoi);
|
||||
activate(byId(ids.poiAudioAdd), () => openFilePicker(ids.poiAudio));
|
||||
activate(byId(ids.poiImagesAdd), () => openFilePicker(ids.poiImages));
|
||||
|
||||
@@ -231,7 +231,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
name: String(inputRoute.name || ''),
|
||||
description: String(inputRoute.description || ''),
|
||||
createdAt: inputRoute.createdAt || now,
|
||||
updatedAt: now
|
||||
updatedAt: now,
|
||||
serverRouteId: inputRoute.serverRouteId ?? null,
|
||||
uploadedAt: inputRoute.uploadedAt || null
|
||||
};
|
||||
const pois = (routePackage.pois || []).map((poi, index) => ({
|
||||
...poi,
|
||||
|
||||
+38
-3
@@ -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.match(html, /id="editor-poi-media-preview"/);
|
||||
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.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, /'audio'/);
|
||||
assert.match(ui, /method:\s*'DELETE'/);
|
||||
assert.match(ui, /sendForm\('\/routes',\s*'POST'/);
|
||||
assert.match(ui, /serverRouteId/);
|
||||
assert.match(ui, /position\?\.accuracy/);
|
||||
});
|
||||
|
||||
@@ -140,8 +143,8 @@ class FakeFormData {
|
||||
this.entries = [];
|
||||
}
|
||||
|
||||
append(name, value) {
|
||||
this.entries.push([name, value]);
|
||||
append(name, value, filename) {
|
||||
this.entries.push([name, value, filename]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +206,7 @@ function buildEditorContext() {
|
||||
'editor-route-list',
|
||||
'editor-route-list-view',
|
||||
'editor-route-name',
|
||||
'editor-route-upload',
|
||||
'editor-status'
|
||||
];
|
||||
const elements = new Map(ids.map(id => [id, new FakeElement(id)]));
|
||||
@@ -233,7 +237,14 @@ function buildEditorContext() {
|
||||
async function fetch(url, options = {}) {
|
||||
const method = options.method || 'GET';
|
||||
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' && 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 === 'PUT') {
|
||||
const body = JSON.parse(options.body);
|
||||
@@ -278,6 +289,7 @@ function buildEditorContext() {
|
||||
};
|
||||
|
||||
const context = {
|
||||
Blob,
|
||||
document,
|
||||
Event,
|
||||
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(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-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());
|
||||
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.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 poiItem = poiList.children.find(child => child.dataset.poiId);
|
||||
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.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-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.equal('activated_at' in body, false);
|
||||
assert.equal('activatedAt' in body, false);
|
||||
assert.equal(elements.get('editor-status').textContent, 'Speichern erfolgreich.');
|
||||
});
|
||||
|
||||
@@ -325,7 +325,13 @@ test('local route package stores GPX and JSON separately and reconstructs an app
|
||||
const saved = api.save({
|
||||
id: routeId,
|
||||
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: [
|
||||
{
|
||||
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.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.serverRouteId, 42);
|
||||
assert.equal(metadata.route.uploadedAt, '2026-09-27T18:00:00.000Z');
|
||||
assert.equal(metadata.pois[0].audio.name, 'start.mp3');
|
||||
assert.equal('gpx' in metadata, false);
|
||||
assert.equal('_source' in metadata.route, false);
|
||||
|
||||
Reference in New Issue
Block a user