sonarqube fix
This commit is contained in:
+223
-315
@@ -19,66 +19,31 @@ test('editor markup and source describe local draft creation instead of immediat
|
||||
const css = await read('public/css/editor.css');
|
||||
const ui = await read('public/js/editor-ui.js');
|
||||
|
||||
assert.match(html, /id="editor-button"[^>]+aria-label="Strecken verwalten"/);
|
||||
assert.match(html, /id="route-control-button"[\s\S]*?<img[^>]+src="images\/icons\/route-management\.svg"[^>]+alt=""[^>]*>/);
|
||||
assert.doesNotMatch(html, /class="editor-button-icon"/);
|
||||
assert.match(html, /id="editor-button"/);
|
||||
assert.match(html, /<dialog id="editor-dialog"/);
|
||||
assert.match(html, /id="editor-route-form"/);
|
||||
assert.match(html, /id="editor-poi-form"/);
|
||||
assert.match(html, /id="editor-route-gpx"[^>]+accept="[^\"]*\.gpx/);
|
||||
assert.match(html, /id="editor-poi-audio-add"[^>]+type="button"/);
|
||||
assert.match(html, /id="editor-poi-images-add"[^>]+type="button"/);
|
||||
assert.match(html, /id="editor-poi-audio"[^>]+accept="audio\/\*"[^>]+hidden/);
|
||||
assert.match(html, /id="editor-poi-images"[^>]+accept="image\/\*"[^>]+multiple[^>]+hidden/);
|
||||
assert.match(html, /id="editor-poi-lat"[^>]+type="hidden"/);
|
||||
assert.match(html, /id="editor-poi-lon"[^>]+type="hidden"/);
|
||||
assert.match(html, /id="editor-poi-radius"[^>]+type="hidden"/);
|
||||
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, />Streckenpunkte<\/h3>/);
|
||||
assert.match(html, /aria-label="Streckenpunkt hinzufügen"/);
|
||||
assert.match(html, /id="editor-route-upload"[^>]+type="button"[^>]+hidden/);
|
||||
assert.doesNotMatch(html, /accept="video\//);
|
||||
assert.match(html, /id="editor-poi-audio"[^>]+accept="audio\/\*"/);
|
||||
assert.match(html, /id="editor-poi-images"[^>]+accept="image\/\*"[^>]+multiple/);
|
||||
assert.match(html, /ausschließlich in diesem Browser/i);
|
||||
await fs.access(path.join(root, 'public/images/icons/route-management.svg'));
|
||||
|
||||
assert.match(css, /\.editor-dialog/);
|
||||
assert.match(css, /\.editor-poi-list/);
|
||||
assert.match(css, /\.editor-route-group/);
|
||||
assert.match(css, /\.editor-media-button/);
|
||||
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(css, /\.route-control-button img/);
|
||||
assert.doesNotMatch(css, /\.editor-button-icon/);
|
||||
|
||||
assert.match(ui, /readJson\(['"]\/routes\?activatedOnly=true['"]\)/);
|
||||
assert.match(ui, /ns\.LocalRoutes\.save\(/);
|
||||
assert.match(ui, /ns\.LocalRoutes\.parseWaypoints\(/);
|
||||
assert.match(ui, /ns\.LocalRoutes\.storeMediaFile\(/);
|
||||
assert.match(ui, /showOpenFilePicker/);
|
||||
assert.match(ui, /requestPermission:\s*true/);
|
||||
assert.match(ui, /ns\.LocalRoutes\.gpxForUpload\(/);
|
||||
assert.match(ui, /ns\.LocalRoutes\.fileToDataUrl\(/);
|
||||
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['"]/);
|
||||
assert.doesNotMatch(ui, /activated_at['"],\s*['"]/);
|
||||
assert.doesNotMatch(ui, /activatedAt:\s*null/);
|
||||
assert.doesNotMatch(ui, /addEventListener\(['"]click['"]/);
|
||||
assert.match(ui, /\/pictures/);
|
||||
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/);
|
||||
assert.match(ui, /setHidden\(byId\(ids\.routeGpxGroup\), state\.currentSource !== 'server'\)/);
|
||||
});
|
||||
|
||||
class FakeElement extends EventTarget {
|
||||
@@ -99,7 +64,6 @@ class FakeElement extends EventTarget {
|
||||
this.title = '';
|
||||
this.type = '';
|
||||
this._textContent = '';
|
||||
this.clickCount = 0;
|
||||
}
|
||||
|
||||
get textContent() {
|
||||
@@ -132,10 +96,6 @@ class FakeElement extends EventTarget {
|
||||
if (name === 'open') this.open = false;
|
||||
}
|
||||
|
||||
click() {
|
||||
this.clickCount += 1;
|
||||
}
|
||||
|
||||
showModal() {
|
||||
this.open = true;
|
||||
}
|
||||
@@ -157,8 +117,8 @@ class FakeFormData {
|
||||
this.entries = [];
|
||||
}
|
||||
|
||||
append(name, value, filename) {
|
||||
this.entries.push([name, value, filename]);
|
||||
append(name, value) {
|
||||
this.entries.push([name, value]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +141,13 @@ function pointerEvent(target = null, { pointerType = 'touch', button = 0, isPrim
|
||||
return event;
|
||||
}
|
||||
|
||||
function keydownEvent(key, target = null) {
|
||||
const event = new Event('keydown', { cancelable: true });
|
||||
Object.defineProperty(event, 'key', { value: key });
|
||||
if (target) Object.defineProperty(event, 'target', { value: target });
|
||||
return event;
|
||||
}
|
||||
|
||||
function submitEvent() {
|
||||
return new Event('submit', { cancelable: true });
|
||||
}
|
||||
@@ -193,21 +160,16 @@ function buildEditorContext() {
|
||||
'editor-dialog',
|
||||
'editor-new-route',
|
||||
'editor-poi-audio',
|
||||
'editor-poi-audio-add',
|
||||
'editor-poi-cancel',
|
||||
'editor-poi-delete',
|
||||
'editor-poi-description',
|
||||
'editor-poi-form',
|
||||
'editor-poi-form-title',
|
||||
'editor-poi-form-view',
|
||||
'editor-poi-id',
|
||||
'editor-poi-images',
|
||||
'editor-poi-images-add',
|
||||
'editor-poi-lat',
|
||||
'editor-poi-list',
|
||||
'editor-poi-lon',
|
||||
'editor-poi-media-preview',
|
||||
'editor-poi-media-status',
|
||||
'editor-poi-radius',
|
||||
'editor-poi-title',
|
||||
'editor-route-activated',
|
||||
@@ -216,12 +178,10 @@ function buildEditorContext() {
|
||||
'editor-route-detail-view',
|
||||
'editor-route-form',
|
||||
'editor-route-gpx',
|
||||
'editor-route-gpx-group',
|
||||
'editor-route-id',
|
||||
'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)]));
|
||||
@@ -237,7 +197,10 @@ function buildEditorContext() {
|
||||
id: 'local-existing',
|
||||
gpx: '<gpx/>',
|
||||
route: { id: 'local-existing', name: 'Lokaler Waldweg', description: 'Lokal' },
|
||||
pois: []
|
||||
pois: [
|
||||
{ id: 21, title: 'Lokaler Start', description: 'Start', lat: 50.2, lon: 10.2, triggerRadiusM: 25, sequence: 0 },
|
||||
{ id: 22, title: 'Lokales Ziel', description: 'Ziel', lat: 50.3, lon: 10.3, triggerRadiusM: 25, sequence: 1 }
|
||||
]
|
||||
}]
|
||||
]);
|
||||
|
||||
@@ -246,41 +209,35 @@ function buildEditorContext() {
|
||||
name: 'Freigegebener Waldweg',
|
||||
description: 'Serverroute',
|
||||
activatedAt: '2026-09-01 08:00:00',
|
||||
pois: [{ id: 10, title: 'Start', description: 'Los', lat: 50.1, lon: 10.1, triggerRadiusM: 20, sequence: 0 }]
|
||||
pois: [
|
||||
{ id: 10, title: 'Start', description: 'Los', lat: 50.1, lon: 10.1, triggerRadiusM: 20, sequence: 0 },
|
||||
{ id: 11, title: 'Ziel', description: 'Ankommen', lat: 50.2, lon: 10.2, triggerRadiusM: 20, sequence: 1 }
|
||||
]
|
||||
};
|
||||
|
||||
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 === 'DELETE') return jsonResponse({ deleted: true });
|
||||
if (url === '/api/routes/1' && method === 'PUT') {
|
||||
if (options.body instanceof FakeFormData) {
|
||||
const values = Object.fromEntries(options.body.entries);
|
||||
return jsonResponse({ route: { ...serverRoute, name: values.name, description: values.description } });
|
||||
}
|
||||
const body = JSON.parse(options.body);
|
||||
return jsonResponse({ route: { ...serverRoute, ...body } });
|
||||
}
|
||||
if (url === '/api/routes/1/pois' && method === 'POST') return jsonResponse({ poi: { id: 12, title: 'Neue Serverstation' } });
|
||||
if (/^\/api\/routes\/1\/pois\/\d+$/.test(url) && method === 'PUT') return jsonResponse({ poi: { id: Number(url.split('/').pop()), title: 'Station' } });
|
||||
if (/^\/api\/routes\/1\/pois\/\d+\/(audio|pictures)$/.test(url) && (method === 'POST' || method === 'PUT')) return jsonResponse({ ok: true });
|
||||
if (/^\/api\/routes\/1\/pois\/\d+\/audio$/.test(url) && method === 'DELETE') return jsonResponse({ deleted: true });
|
||||
if (/^\/api\/routes\/1\/pois\/\d+\/pictures\/\d+$/.test(url) && method === 'DELETE') return jsonResponse({ deleted: true });
|
||||
if (/^\/api\/routes\/1\/pois\/\d+$/.test(url) && method === 'DELETE') return jsonResponse({ deleted: true });
|
||||
if (/^\/api\/routes\/1\/pois\/\d+\/(audio|images)$/.test(url) && method === 'POST') return jsonResponse({ ok: true });
|
||||
return { ok: false, status: 404, text: async () => `unexpected ${method} ${url}` };
|
||||
}
|
||||
|
||||
const mediaBlobs = new Map();
|
||||
const storedMediaCalls = [];
|
||||
const LocalRoutes = {
|
||||
createPoiId: () => 9001,
|
||||
createRouteId: () => 'local-new',
|
||||
emptyGpx: name => `<gpx><metadata><name>${name}</name></metadata><trk><trkseg></trkseg></trk></gpx>`,
|
||||
emptyGpx: name => `<gpx><metadata><name>${name}</name></metadata></gpx>`,
|
||||
fileToDataUrl: async file => ({
|
||||
dataUrl: `data:${file.type || 'application/octet-stream'};base64,TEST`,
|
||||
name: file.name,
|
||||
@@ -288,50 +245,17 @@ function buildEditorContext() {
|
||||
type: file.type || 'application/octet-stream'
|
||||
}),
|
||||
get: id => packages.get(String(id)) || null,
|
||||
getWithMedia: async id => {
|
||||
const value = packages.get(String(id));
|
||||
if (!value) return null;
|
||||
return {
|
||||
...value,
|
||||
pois: value.pois.map(poi => ({
|
||||
...poi,
|
||||
audio: poi.audio ? { ...poi.audio, url: 'blob:test-audio' } : null,
|
||||
images: (poi.images || []).map(image => ({ ...image, url: 'blob:test-image' }))
|
||||
}))
|
||||
};
|
||||
},
|
||||
gpxForUpload: routePackage => routePackage.gpx,
|
||||
list: () => [...packages.values()],
|
||||
parseWaypoints: () => [],
|
||||
readFileAsText: file => file.text(),
|
||||
mediaBlob: async media => mediaBlobs.get(media.mediaKey) || new Blob(['media'], { type: media.type || 'application/octet-stream' }),
|
||||
removeMedia: async media => { if (media?.mediaKey) mediaBlobs.delete(media.mediaKey); },
|
||||
remove: async id => packages.delete(String(id)),
|
||||
storeMediaFile: async (routeId, poiId, kind, mediaId, file, handle = null) => {
|
||||
storedMediaCalls.push({ routeId, poiId, kind, mediaId, file, handle });
|
||||
const mediaKey = `${routeId}:${poiId}:${kind}:${mediaId}`;
|
||||
mediaBlobs.set(mediaKey, file);
|
||||
return {
|
||||
id: String(mediaId),
|
||||
mediaKey,
|
||||
storage: handle ? 'file-handle' : 'indexeddb',
|
||||
name: file.name,
|
||||
size: file.size || 1,
|
||||
type: file.type || 'application/octet-stream'
|
||||
};
|
||||
},
|
||||
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 gpxPoints = 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: `<gpx><trk><trkseg>${gpxPoints}</trkseg></trk></gpx>`,
|
||||
gpx: input.gpx,
|
||||
route: { ...input.route, id, updatedAt: now },
|
||||
pois
|
||||
pois: input.pois.map((poi, sequence) => ({ ...poi, sequence }))
|
||||
};
|
||||
packages.set(id, saved);
|
||||
saves.push(structuredClone(saved));
|
||||
@@ -340,7 +264,6 @@ function buildEditorContext() {
|
||||
};
|
||||
|
||||
const context = {
|
||||
Blob,
|
||||
document,
|
||||
Event,
|
||||
EventTarget,
|
||||
@@ -348,18 +271,17 @@ function buildEditorContext() {
|
||||
FormData: FakeFormData,
|
||||
globalThis: null,
|
||||
structuredClone,
|
||||
URL: { createObjectURL: () => 'blob:test-preview', revokeObjectURL() {} },
|
||||
Wegwichtel: {
|
||||
App: { currentPosition: () => ({ lat: 50.123456789, lon: 10.987654321, accuracy: 4.321 }) },
|
||||
App: { currentPosition: () => ({ lat: 50.5, lon: 10.5 }) },
|
||||
Config: { apiBase: '/api/' },
|
||||
LocalRoutes
|
||||
}
|
||||
};
|
||||
context.globalThis = context;
|
||||
return { calls, context, elements, packages, saves, storedMediaCalls };
|
||||
return { calls, context, elements, packages, saves, serverRoute };
|
||||
}
|
||||
|
||||
test('public editor workflow saves new route and GPX Streckenpunkt locally without POSTing a route', async () => {
|
||||
test('public editor workflow saves new route and GPX POI 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();
|
||||
@@ -372,7 +294,6 @@ test('public editor workflow saves new route and GPX Streckenpunkt locally witho
|
||||
assert.ok(calls.some(call => call.method === 'GET' && call.url === '/api/routes?activatedOnly=true'));
|
||||
|
||||
await context.Wegwichtel.EditorUi.openNewLocalRoute();
|
||||
assert.equal(elements.get('editor-route-gpx-group').hidden, true);
|
||||
elements.get('editor-route-name').value = 'Neue lokale Strecke';
|
||||
elements.get('editor-route-description').value = 'Nur im Browser';
|
||||
elements.get('editor-route-form').dispatchEvent(submitEvent());
|
||||
@@ -383,191 +304,34 @@ test('public editor workflow saves new route and GPX Streckenpunkt locally witho
|
||||
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.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);
|
||||
assert.equal(elements.get('editor-route-gpx-group').hidden, true);
|
||||
|
||||
elements.get('editor-add-poi').dispatchEvent(pointerEvent());
|
||||
assert.equal(elements.get('editor-poi-audio').clickCount, 0);
|
||||
assert.equal(elements.get('editor-poi-images').clickCount, 0);
|
||||
elements.get('editor-poi-audio-add').dispatchEvent(pointerEvent());
|
||||
elements.get('editor-poi-images-add').dispatchEvent(pointerEvent());
|
||||
assert.equal(elements.get('editor-poi-audio').clickCount, 1);
|
||||
assert.equal(elements.get('editor-poi-images').clickCount, 1);
|
||||
assert.equal(elements.get('editor-poi-lat').value, '50.123456789');
|
||||
assert.equal(elements.get('editor-poi-lon').value, '10.987654321');
|
||||
assert.equal(elements.get('editor-poi-radius').value, '4.321');
|
||||
elements.get('editor-poi-title').value = 'Aussicht';
|
||||
elements.get('editor-poi-description').value = 'Text zum Streckenpunkt';
|
||||
elements.get('editor-poi-description').value = 'Text zur Station';
|
||||
elements.get('editor-poi-lat').value = '50.51';
|
||||
elements.get('editor-poi-lon').value = '10.52';
|
||||
elements.get('editor-poi-radius').value = '35';
|
||||
elements.get('editor-poi-audio').files = [{ name: 'aussicht.mp3', size: 5, type: 'audio/mpeg' }];
|
||||
elements.get('editor-poi-audio').dispatchEvent(new Event('change'));
|
||||
elements.get('editor-poi-images').files = [{ name: 'aussicht.jpg', size: 7, type: 'image/jpeg' }];
|
||||
elements.get('editor-poi-images').dispatchEvent(new Event('change'));
|
||||
await tick();
|
||||
elements.get('editor-poi-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
let local = packages.get('local-new');
|
||||
const local = packages.get('local-new');
|
||||
assert.equal(local.pois.length, 1);
|
||||
assert.equal(local.pois[0].title, 'Aussicht');
|
||||
assert.equal(local.pois[0].lat, 50.123456789);
|
||||
assert.equal(local.pois[0].lon, 10.987654321);
|
||||
assert.equal(local.pois[0].triggerRadiusM, 4.321);
|
||||
assert.equal(local.pois[0].audio.storage, 'indexeddb');
|
||||
assert.ok(local.pois[0].audio.mediaKey);
|
||||
assert.equal(local.pois[0].lat, 50.51);
|
||||
assert.equal(local.pois[0].lon, 10.52);
|
||||
assert.equal(local.pois[0].triggerRadiusM, 35);
|
||||
assert.match(local.pois[0].audio.dataUrl, /^data:audio\/mpeg/);
|
||||
assert.equal(local.pois[0].images.length, 1);
|
||||
assert.equal(local.pois[0].images[0].storage, 'indexeddb');
|
||||
assert.ok(local.pois[0].images[0].mediaKey);
|
||||
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());
|
||||
await tick();
|
||||
await tick();
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
const routeUpload = calls.find(call => call.method === 'POST' && call.url === '/api/routes');
|
||||
assert.ok(routeUpload);
|
||||
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'));
|
||||
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];
|
||||
poiList.dispatchEvent(pointerEvent(poiEditButton));
|
||||
const mediaPreview = elements.get('editor-poi-media-preview');
|
||||
assert.equal(mediaPreview.children.length, 2);
|
||||
|
||||
let imageCard = mediaPreview.children.find(card => card.children[1]?.dataset.mediaKind === 'image');
|
||||
mediaPreview.dispatchEvent(pointerEvent(imageCard.children[0]));
|
||||
imageCard = mediaPreview.children.find(card => card.children[1]?.dataset.mediaKind === 'image');
|
||||
assert.equal(imageCard.children[1].hidden, false);
|
||||
mediaPreview.dispatchEvent(pointerEvent(imageCard.children[1]));
|
||||
await tick();
|
||||
await tick();
|
||||
local = packages.get('local-new');
|
||||
assert.equal(local.pois[0].images.length, 0);
|
||||
|
||||
let audioCard = elements.get('editor-poi-media-preview').children.find(card => card.children[1]?.dataset.mediaKind === 'audio');
|
||||
elements.get('editor-poi-media-preview').dispatchEvent(pointerEvent(audioCard.children[0]));
|
||||
audioCard = elements.get('editor-poi-media-preview').children.find(card => card.children[1]?.dataset.mediaKind === 'audio');
|
||||
elements.get('editor-poi-media-preview').dispatchEvent(pointerEvent(audioCard.children[1]));
|
||||
await tick();
|
||||
await tick();
|
||||
local = packages.get('local-new');
|
||||
assert.equal(local.pois[0].audio, null);
|
||||
|
||||
assert.equal(elements.get('editor-poi-delete').hidden, false);
|
||||
elements.get('editor-poi-delete').dispatchEvent(pointerEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
local = packages.get('local-new');
|
||||
assert.equal(local.pois.length, 0);
|
||||
assert.match(local.pois[0].images[0].dataUrl, /^data:image\/jpeg/);
|
||||
|
||||
context.Wegwichtel.EditorUi.close();
|
||||
assert.equal(elements.get('editor-dialog').open, false);
|
||||
assert.equal(elements.get('editor-button').attributes.get('aria-expanded'), 'false');
|
||||
});
|
||||
|
||||
test('local Streckenpunkt ids start at zero and increment from the highest existing id', 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 = 'ID-Test';
|
||||
elements.get('editor-route-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
elements.get('editor-add-poi').dispatchEvent(pointerEvent());
|
||||
assert.equal(elements.get('editor-poi-id').value, '0');
|
||||
elements.get('editor-poi-title').value = 'Erster Streckenpunkt';
|
||||
elements.get('editor-poi-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
elements.get('editor-add-poi').dispatchEvent(pointerEvent());
|
||||
assert.equal(elements.get('editor-poi-id').value, '1');
|
||||
elements.get('editor-poi-title').value = 'Zweiter Streckenpunkt';
|
||||
elements.get('editor-poi-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
const saved = packages.get('local-new');
|
||||
assert.equal(saved.pois.length, 2);
|
||||
assert.deepEqual(Array.from(saved.pois, poi => poi.id), [0, 1]);
|
||||
assert.deepEqual(Array.from(saved.pois, poi => poi.title), ['Erster Streckenpunkt', 'Zweiter Streckenpunkt']);
|
||||
|
||||
const pointItems = elements.get('editor-poi-list').children.filter(child => child.dataset.poiId);
|
||||
assert.equal(pointItems.length, 2);
|
||||
});
|
||||
|
||||
|
||||
test('route and Streckenpunkt 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 = 'Streckenpunkt 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');
|
||||
@@ -578,27 +342,13 @@ test('editing an activated server route uses PUT and never writes activation sta
|
||||
|
||||
const routeList = elements.get('editor-route-list');
|
||||
const serverItem = routeList.children.find(child => child.dataset.routeSource === 'server');
|
||||
const serverActions = serverItem.children[1];
|
||||
assert.equal(serverActions.children.length, 1);
|
||||
assert.equal(serverActions.children[0].dataset.editorAction, 'edit-route');
|
||||
|
||||
const forgedDelete = new FakeElement('', 'button');
|
||||
forgedDelete.dataset.editorAction = 'delete-route';
|
||||
serverActions.appendChild(forgedDelete);
|
||||
routeList.dispatchEvent(pointerEvent(forgedDelete));
|
||||
await tick();
|
||||
await tick();
|
||||
assert.equal(calls.some(call => call.method === 'DELETE' && call.url === '/api/routes/1'), false);
|
||||
|
||||
const editButton = serverActions.children[0];
|
||||
const editButton = serverItem.children[1].children[0];
|
||||
routeList.dispatchEvent(pointerEvent(editButton));
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
assert.equal(elements.get('editor-route-id').value, '1');
|
||||
assert.equal(elements.get('editor-route-gpx-group').hidden, false);
|
||||
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';
|
||||
@@ -612,42 +362,200 @@ 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.');
|
||||
});
|
||||
|
||||
|
||||
test('local media picker keeps File System Access handles for later upload', async () => {
|
||||
test('editor supports keyboard activation and the dialog fallback without native dialog methods', async () => {
|
||||
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
||||
const source = await fs.readFile(sourcePath, 'utf8');
|
||||
const { context, elements, storedMediaCalls } = buildEditorContext();
|
||||
const imageFile = Object.assign(new Blob(['photo'], { type: 'image/jpeg' }), { name: 'foto.jpg' });
|
||||
const handle = {
|
||||
name: 'foto.jpg',
|
||||
async getFile() { return imageFile; }
|
||||
};
|
||||
context.showOpenFilePicker = async options => {
|
||||
assert.equal(options.multiple, true);
|
||||
return [handle];
|
||||
};
|
||||
const { context, elements } = buildEditorContext();
|
||||
const dialog = elements.get('editor-dialog');
|
||||
dialog.showModal = undefined;
|
||||
dialog.close = undefined;
|
||||
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
await context.Wegwichtel.EditorUi.openNewLocalRoute();
|
||||
elements.get('editor-route-name').value = 'Handle-Strecke';
|
||||
|
||||
elements.get('editor-button').dispatchEvent(keydownEvent('Enter'));
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
assert.equal(dialog.open, true);
|
||||
assert.equal(elements.get('editor-button').attributes.get('aria-expanded'), 'true');
|
||||
|
||||
elements.get('editor-close').dispatchEvent(pointerEvent());
|
||||
assert.equal(dialog.open, false);
|
||||
assert.equal(elements.get('editor-button').attributes.get('aria-expanded'), 'false');
|
||||
});
|
||||
|
||||
test('server route GPX upload uses multipart form data and keeps activation state untouched', async () => {
|
||||
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
||||
const source = await fs.readFile(sourcePath, 'utf8');
|
||||
const { calls, context, elements } = buildEditorContext();
|
||||
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
|
||||
const serverItem = elements.get('editor-route-list').children.find(child => child.dataset.routeSource === 'server');
|
||||
const editButton = serverItem.children[1].children[0];
|
||||
elements.get('editor-route-list').dispatchEvent(pointerEvent(editButton));
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
elements.get('editor-route-name').value = 'Serverroute mit GPX';
|
||||
elements.get('editor-route-description').value = 'Multipart';
|
||||
elements.get('editor-route-gpx').files = [{ name: 'route.gpx', type: 'application/gpx+xml' }];
|
||||
elements.get('editor-route-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
const update = calls.find(call => call.method === 'PUT' && call.url === '/api/routes/1' && call.body instanceof FakeFormData);
|
||||
assert.ok(update);
|
||||
assert.deepEqual(update.body.entries.map(([name]) => name), ['name', 'description', 'file']);
|
||||
});
|
||||
|
||||
test('server POI workflow creates media, edits POIs and persists reordered sequence values', async () => {
|
||||
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
||||
const source = await fs.readFile(sourcePath, 'utf8');
|
||||
const { calls, context, elements } = buildEditorContext();
|
||||
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
|
||||
const routeList = elements.get('editor-route-list');
|
||||
const serverItem = routeList.children.find(child => child.dataset.routeSource === 'server');
|
||||
routeList.dispatchEvent(pointerEvent(serverItem.children[1].children[0]));
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
elements.get('editor-add-poi').dispatchEvent(pointerEvent());
|
||||
elements.get('editor-poi-title').value = 'Streckenpunkt mit Bild';
|
||||
elements.get('editor-poi-images-add').dispatchEvent(pointerEvent());
|
||||
await tick();
|
||||
assert.equal(elements.get('editor-poi-images').clickCount, 0);
|
||||
assert.equal(elements.get('editor-poi-lat').value, '50.5');
|
||||
assert.equal(elements.get('editor-poi-lon').value, '10.5');
|
||||
|
||||
elements.get('editor-poi-title').value = 'Neue Serverstation';
|
||||
elements.get('editor-poi-description').value = 'Mit Medien';
|
||||
elements.get('editor-poi-audio').files = [{ name: 'station.mp3', type: 'audio/mpeg' }];
|
||||
elements.get('editor-poi-images').files = [
|
||||
{ name: 'eins.jpg', type: 'image/jpeg' },
|
||||
{ name: 'zwei.jpg', type: 'image/jpeg' }
|
||||
];
|
||||
elements.get('editor-poi-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
const imageStore = storedMediaCalls.find(call => call.kind === 'image');
|
||||
assert.ok(imageStore);
|
||||
assert.equal(imageStore.handle, handle);
|
||||
assert.equal(imageStore.file, imageFile);
|
||||
await tick();
|
||||
|
||||
assert.ok(calls.some(call => call.method === 'POST' && call.url === '/api/routes/1/pois'));
|
||||
assert.ok(calls.some(call => call.method === 'POST' && call.url === '/api/routes/1/pois/12/audio'));
|
||||
assert.equal(calls.filter(call => call.method === 'POST' && call.url === '/api/routes/1/pois/12/images').length, 2);
|
||||
|
||||
const poiList = elements.get('editor-poi-list');
|
||||
const firstPoi = poiList.children.find(child => child.dataset.poiId === '10');
|
||||
const secondPoi = poiList.children.find(child => child.dataset.poiId === '11');
|
||||
|
||||
poiList.dispatchEvent(pointerEvent(secondPoi.children[1].children[2]));
|
||||
assert.equal(elements.get('editor-poi-id').value, '11');
|
||||
assert.equal(elements.get('editor-poi-title').value, 'Ziel');
|
||||
|
||||
elements.get('editor-poi-title').value = 'Ziel geändert';
|
||||
elements.get('editor-poi-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
assert.ok(calls.some(call => call.method === 'PUT' && call.url === '/api/routes/1/pois/11'));
|
||||
|
||||
const refreshedFirstPoi = elements.get('editor-poi-list').children.find(child => child.dataset.poiId === '10');
|
||||
elements.get('editor-poi-list').dispatchEvent(pointerEvent(refreshedFirstPoi.children[1].children[1]));
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
const reorderCalls = calls.filter(call => call.method === 'PUT' && /^\/api\/routes\/1\/pois\/(10|11)$/.test(call.url));
|
||||
assert.ok(reorderCalls.length >= 2);
|
||||
assert.ok(reorderCalls.some(call => JSON.parse(call.body).sortOrder === 0));
|
||||
assert.ok(reorderCalls.some(call => JSON.parse(call.body).sortOrder === 1));
|
||||
|
||||
// Moving the first element upward is a no-op and must not trigger another request.
|
||||
const countBeforeNoop = calls.length;
|
||||
const firstAfterReorder = elements.get('editor-poi-list').children.find(child => child.dataset.poiId === '11');
|
||||
elements.get('editor-poi-list').dispatchEvent(pointerEvent(firstAfterReorder.children[1].children[0]));
|
||||
await tick();
|
||||
assert.equal(calls.length, countBeforeNoop);
|
||||
});
|
||||
|
||||
test('local route import, local reordering and validation errors remain browser-only', 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();
|
||||
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
await context.Wegwichtel.EditorUi.openNewLocalRoute();
|
||||
|
||||
context.Wegwichtel.LocalRoutes.parseWaypoints = () => [
|
||||
{ id: 700, title: 'Importiert', description: '', lat: 50.7, lon: 10.7, triggerRadiusM: 25, sequence: 0, images: [], audio: null }
|
||||
];
|
||||
elements.get('editor-route-name').value = 'Importierte Strecke';
|
||||
elements.get('editor-route-gpx').files = [{ text: async () => '<gpx><wpt/></gpx>' }];
|
||||
elements.get('editor-route-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
assert.equal(packages.get('local-new').pois[0].id, 700);
|
||||
assert.equal(calls.some(call => call.method === 'POST' && call.url === '/api/routes'), false);
|
||||
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
const localItem = elements.get('editor-route-list').children.find(child => child.dataset.routeId === 'local-existing');
|
||||
elements.get('editor-route-list').dispatchEvent(pointerEvent(localItem.children[1].children[0]));
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
const localFirst = elements.get('editor-poi-list').children.find(child => child.dataset.poiId === '21');
|
||||
elements.get('editor-poi-list').dispatchEvent(pointerEvent(localFirst.children[1].children[1]));
|
||||
await tick();
|
||||
await tick();
|
||||
assert.deepEqual(saves.at(-1).pois.map(poi => poi.id), [22, 21]);
|
||||
|
||||
context.Wegwichtel.App.currentPosition = () => null;
|
||||
elements.get('editor-add-poi').dispatchEvent(pointerEvent());
|
||||
elements.get('editor-poi-title').value = 'Ungültige Position';
|
||||
elements.get('editor-poi-lat').value = 'keine-breite';
|
||||
elements.get('editor-poi-lon').value = 'keine-laenge';
|
||||
elements.get('editor-poi-form').dispatchEvent(submitEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
assert.equal(elements.get('editor-status').dataset.kind, 'error');
|
||||
assert.match(elements.get('editor-status').textContent, /gültige Breiten- und Längengrade/);
|
||||
});
|
||||
|
||||
test('editor handles empty, malformed and failing route responses without stale status', async () => {
|
||||
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
||||
const source = await fs.readFile(sourcePath, 'utf8');
|
||||
|
||||
{
|
||||
const { context, elements } = buildEditorContext();
|
||||
context.Wegwichtel.LocalRoutes.list = () => [];
|
||||
context.fetch = async () => ({ ok: true, status: 200, text: async () => '{' });
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
const emptyItems = elements.get('editor-route-list').children.filter(child => !child.dataset.routeId);
|
||||
assert.ok(emptyItems.length >= 4);
|
||||
assert.equal(elements.get('editor-status').hidden, true);
|
||||
}
|
||||
|
||||
{
|
||||
const { context, elements } = buildEditorContext();
|
||||
context.Wegwichtel.LocalRoutes.list = () => { throw new Error('Lokaler Speicher defekt'); };
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
assert.equal(elements.get('editor-status').dataset.kind, 'error');
|
||||
assert.match(elements.get('editor-status').textContent, /Lokaler Speicher defekt/);
|
||||
}
|
||||
|
||||
{
|
||||
const { context, elements } = buildEditorContext();
|
||||
context.fetch = async () => ({ ok: false, status: 503, text: async () => 'Dienst nicht verfügbar' });
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
elements.get('editor-button').dispatchEvent(pointerEvent());
|
||||
await tick();
|
||||
await tick();
|
||||
assert.equal(elements.get('editor-status').dataset.kind, 'error');
|
||||
assert.match(elements.get('editor-status').textContent, /Dienst nicht verfügbar/);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user