editor overhaul
Sonarqube Scanner / Prepare environment (push) Successful in 7s
Sonarqube Scanner / Build and test (push) Successful in 23s
Sonarqube Scanner / SonarQube analysis (push) Successful in 56s
Sonarqube Scanner / Deploy to Server (push) Successful in 8s

This commit is contained in:
Florian Zumpe
2026-10-18 14:26:25 +02:00
parent d33a67b8ae
commit 9c4d5f6324
17 changed files with 359 additions and 55 deletions
+128 -1
View File
@@ -268,7 +268,7 @@ function buildEditorContext() {
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)(?:\/24)?$/.test(url) && (method === 'POST' || method === 'PUT')) return jsonResponse({ id: 24 }, true, 201);
if (url === '/api/routes/22' && method === 'GET') return jsonResponse({ id: 22, activatedAt: controls.approved ? '2026-10-03' : null, pois: [{ id: 23, audioUrl: '/audio', images: [{ id: 24 }] }] });
if (url === '/api/routes/22?includeDeleted=true' && method === 'GET') return jsonResponse({ id: 22, activatedAt: controls.approved ? '2026-10-03' : null, pois: [{ id: 23, audioUrl: '/audio', images: [{ id: 24 }] }] });
if (url === '/api/routes/22' && method === 'PUT') return jsonResponse({ id: 22 });
if (url === '/api/routes/22/pois/23' && method === 'PUT') return jsonResponse({ id: 23 });
if (/^\/api\/routes\/22\/pois\/23(?:\/audio|\/pictures\/24)?$/.test(url) && method === 'DELETE') return jsonResponse({ deleted: true });
@@ -389,6 +389,133 @@ function buildEditorContext() {
return { calls, context, elements, packages, saves, storedMediaCalls, controls };
}
async function submittedEditor(response) {
const harness = buildEditorContext();
const { context, packages, calls, elements } = harness;
packages.set('local-existing', {
id: 'local-existing', gpx: '<gpx/>',
route: { id: 'local-existing', name: 'Waldweg', creator_id: 'a'.repeat(64),
serverRouteId: '99', uploadedAt: '2026-10-01T10:00:00Z' },
pois: [{ id: 7, title: 'Aussicht', description: 'Wiese', sequence: 0,
lat: 50, lon: 10, serverPoiId: 98, serverAudio: true,
audio: { name: 'ton.mp3', mediaKey: 'audio-key', type: 'audio/mpeg' },
images: [{ id: 'local-image', name: 'bild.png', mediaKey: 'picture-key',
type: 'image/png', serverPictureId: 97 }] }]
});
const originalFetch = context.fetch;
context.fetch = async (url, options = {}) => {
if (url === '/api/routes/99?includeDeleted=true') {
calls.push({ url, method: 'GET' });
return response();
}
return originalFetch(url, options);
};
await loadI18n(context);
const sourcePath = path.join(root, 'public/js/editor-ui.js');
vm.runInNewContext(await fs.readFile(sourcePath, 'utf8'), context, { filename: sourcePath });
await context.Wegwichtel.EditorUi.open();
for (let i = 0; i < 4; i += 1) await tick();
const list = elements.get('editor-route-list');
const item = list.children.find(child => child.dataset.routeId === 'local-existing');
list.dispatchEvent(pointerEvent(item.children[1].children[0]));
for (let i = 0; i < 4; i += 1) await tick();
return harness;
}
async function submitDraft(harness) {
harness.elements.get('editor-route-upload').dispatchEvent(pointerEvent());
for (let i = 0; i < 12; i += 1) await tick();
}
test('removed and rejected submissions show their saved status and create fresh route and media IDs', async t => {
for (const status of ['removed', 'rejected']) {
await t.test(status, async () => {
const response = status === 'removed'
? () => jsonResponse({ message: 'Strecke nicht gefunden.' }, false, 404)
: () => jsonResponse({ id: 99, status: 'deleted', activatedAt: '2026-10-01', pois: [] });
const harness = await submittedEditor(response);
const { calls, packages, elements } = harness;
assert.equal(packages.get('local-existing').route.submissionStatus, status);
const expectedLabel = status === 'removed' ? 'Entfernt' : 'Abgelehnt';
const item = elements.get('editor-route-list').children.find(child => child.dataset.routeId === 'local-existing');
assert.ok(item.children[0].children.some(child => child.textContent === expectedLabel));
assert.match(elements.get('editor-route-activated').textContent, status === 'removed' ? /nicht mehr online/ : /abgelehnt/);
await submitDraft(harness);
const saved = packages.get('local-existing');
assert.equal(saved.route.serverRouteId, '22');
assert.equal(saved.route.submissionStatus, null);
assert.equal(saved.route.creator_id, 'a'.repeat(64));
assert.equal(saved.pois[0].serverPoiId, '23');
assert.equal(saved.pois[0].images[0].serverPictureId, 24);
assert.equal(saved.pois[0].images[0].mediaKey, 'picture-key');
assert.equal(saved.pois[0].audio.mediaKey, 'audio-key');
assert.equal(elements.get('editor-status').textContent, 'Die Strecke wurde neu angelegt und zur Prüfung gesendet.');
assert.deepEqual(calls.filter(call => call.method === 'POST').map(call => call.url), [
'/api/routes', '/api/routes/22/pois', '/api/routes/22/pois/23/audio', '/api/routes/22/pois/23/pictures'
]);
assert.equal(calls.some(call => call.method === 'PUT' || call.method === 'DELETE' || call.url.includes('/restore')), false);
await submitDraft(harness);
assert.equal(calls.filter(call => call.method === 'POST' && call.url === '/api/routes').length, 1);
assert.ok(calls.some(call => call.method === 'PUT' && call.url === '/api/routes/22'));
});
}
});
test('submission status refresh preserves unsaved form text and clears rejection after restoration', async () => {
let status = 'active';
const harness = await submittedEditor(() => jsonResponse({ id: 99, status }));
harness.elements.get('editor-route-name').value = 'Noch nicht gespeichert';
for (const serverStatus of ['deleted', 'active']) {
status = serverStatus;
await harness.context.Wegwichtel.EditorUi.refresh();
for (let i = 0; i < 4; i += 1) await tick();
assert.equal(harness.packages.get('local-existing').route.submissionStatus, status === 'deleted' ? 'rejected' : null);
assert.equal(harness.elements.get('editor-route-name').value, 'Noch nicht gespeichert');
}
});
test('authorization, server and network failures preserve the submitted IDs and never create a route', async t => {
for (const status of [401, 403, 500, 'offline']) {
await t.test(String(status), async () => {
const harness = await submittedEditor(() => {
if (status === 'offline') throw new TypeError('Failed to fetch');
return jsonResponse({ message: 'Testfehler' }, false, status);
});
await submitDraft(harness);
assert.equal(harness.calls.some(call => call.method !== 'GET'), false);
const saved = harness.packages.get('local-existing');
assert.equal(saved.route.serverRouteId, '99');
assert.equal(saved.pois[0].serverPoiId, 98);
assert.equal(saved.pois[0].images[0].serverPictureId, 97);
assert.equal(harness.elements.get('editor-route-upload').disabled, false);
});
}
});
test('failed fresh creation preserves local files and retries without obsolete server references', async () => {
const harness = await submittedEditor(() => jsonResponse({}, false, 404));
const originalFetch = harness.context.fetch;
let fail = true;
harness.context.fetch = async (url, options = {}) => {
if (fail && url === '/api/routes' && options.method === 'POST') {
fail = false;
return jsonResponse({ message: 'Testfehler' }, false, 500);
}
return originalFetch(url, options);
};
await submitDraft(harness);
const saved = harness.packages.get('local-existing');
assert.equal(saved.route.serverRouteId, null);
assert.equal(saved.route.uploadedAt, null);
assert.equal(saved.route.submissionStatus, 'removed');
assert.equal(saved.pois[0].serverPoiId, undefined);
assert.equal(saved.pois[0].serverAudio, undefined);
assert.equal(saved.pois[0].images[0].serverPictureId, undefined);
assert.equal(saved.pois[0].audio.mediaKey, 'audio-key');
await submitDraft(harness);
assert.equal(harness.packages.get('local-existing').route.serverRouteId, '22');
});
test('public editor workflow saves new route and GPX Station locally without POSTing a route', async () => {
const sourcePath = path.join(root, 'public/js/editor-ui.js');
const source = await fs.readFile(sourcePath, 'utf8');