group editor implemented
Sonarqube Scanner / Prepare environment (push) Successful in 6s
Sonarqube Scanner / Build and test (push) Successful in 27s
Sonarqube Scanner / Deploy to Server (push) Failing after 7s
Sonarqube Scanner / SonarQube analysis (push) Failing after 1m14s

This commit is contained in:
Florian Zumpe
2026-10-18 16:33:12 +02:00
parent 9c4d5f6324
commit 31768ae907
28 changed files with 2361 additions and 55 deletions
+141 -1
View File
@@ -826,7 +826,7 @@ test('editing an activated server route uses PUT and never writes activation sta
const update = calls.find(call => call.method === 'PUT' && call.url === '/api/routes/1');
assert.ok(update);
const body = JSON.parse(update.body);
assert.deepEqual(Object.keys(body).sort(), ['description', 'name']);
assert.deepEqual(Object.keys(body).sort(), ['creator_id', 'description', 'name']);
assert.equal('activated_at' in body, false);
assert.equal('activatedAt' in body, false);
assert.equal(elements.get('editor-status').textContent, 'Gespeichert.');
@@ -963,3 +963,143 @@ test('late station previews are released after the editor closes without replaci
assert.equal(released.length, 1);
assert.equal(elements.get('editor-poi-title').value, 'Noch nicht gespeichert');
});
test('shared drafts use guarded server saves, preserve the creator and retain unsaved input on conflicts', async () => {
const h = buildEditorContext();
const shared = { id: 44, name: 'Gemeinsamer Waldweg', description: '', creator_id: 'b'.repeat(64), pois: [] };
let conflict = false;
let refreshed = 0;
const originalFetch = h.context.fetch;
h.context.Wegwichtel.Groups = {
headersFor: path => path.startsWith('/routes/44') ? { 'If-Match': '3' } : {},
observe() {}, refresh() { refreshed += 1; }
};
h.context.fetch = async (url, options = {}) => {
if (url === '/api/routes/44') {
h.calls.push({ url, ...options, method: options.method || 'GET' });
if (options.method === 'PUT') {
if (conflict) return { ...jsonResponse({ message: 'Conflict' }, false, 409), headers: { get: () => '44' } };
Object.assign(shared, JSON.parse(options.body));
}
return jsonResponse(shared);
}
return originalFetch(url, options);
};
await loadI18n(h.context);
const messages = path.join(root, 'public/js/user-messages.js');
vm.runInNewContext(await fs.readFile(messages, 'utf8'), h.context, { filename: messages });
const filename = path.join(root, 'public/js/editor-ui.js');
vm.runInNewContext(await fs.readFile(filename, 'utf8'), h.context, { filename });
await h.context.Wegwichtel.EditorUi.openGroup(shared);
assert.equal(h.controls.recording, null);
assert.ok(refreshed > 0);
h.elements.get('editor-route-name').value = 'Gemeinsam geändert';
h.elements.get('editor-route-form').dispatchEvent(submitEvent());
for (let i = 0; i < 6; i += 1) await tick();
const saved = h.calls.find(call => call.method === 'PUT' && call.url === '/api/routes/44');
assert.equal(saved.headers['If-Match'], '3');
assert.equal(JSON.parse(saved.body).creator_id, 'b'.repeat(64));
assert.equal(h.packages.has('44'), false);
assert.equal(shared.name, 'Gemeinsam geändert');
conflict = true;
h.elements.get('editor-route-name').value = 'Noch nicht gespeichert';
h.elements.get('editor-route-form').dispatchEvent(submitEvent());
for (let i = 0; i < 6; i += 1) await tick();
assert.equal(shared.name, 'Gemeinsam geändert');
assert.equal(h.elements.get('editor-route-name').value, 'Noch nicht gespeichert');
assert.match(h.elements.get('editor-status').textContent, /inzwischen geändert/);
});
test('sharing a local draft asks before sending and opens a separate shared copy', async () => {
const h = buildEditorContext();
const sharedIds = [];
h.context.confirm = () => false;
h.context.Wegwichtel.Groups = {
shareButton(action) { const button = new FakeElement('', 'button'); button.dataset.editorAction = action; return button; },
async share(id) { sharedIds.push(id); }
};
await loadI18n(h.context);
const filename = path.join(root, 'public/js/editor-ui.js');
vm.runInNewContext(await fs.readFile(filename, 'utf8'), h.context, { filename });
await h.context.Wegwichtel.EditorUi.open();
const list = h.elements.get('editor-route-list');
const item = list.children.find(child => child.dataset.routeId === 'local-existing');
list.dispatchEvent(pointerEvent(item.children[1].children[1]));
for (let i = 0; i < 4; i += 1) await tick();
assert.equal(h.calls.some(call => call.method === 'POST'), false);
h.context.confirm = () => true;
list.dispatchEvent(pointerEvent(item.children[1].children[1]));
list.dispatchEvent(pointerEvent(item.children[1].children[1]));
for (let i = 0; i < 14; i += 1) await tick();
assert.deepEqual(sharedIds, ['22']);
assert.equal(h.packages.get('local-existing').route.serverRouteId, '22');
assert.ok(h.calls.some(call => call.method === 'POST' && call.url === '/api/routes'));
});
test('shared station saves persist files before sending, retain lost-lock changes and release locks on exit', async () => {
const h = buildEditorContext();
const poi = { id: 8, title: 'Wiese', description: 'Blumen', lat: 50, lon: 10, sequence: 0, images: [], audioUrl: null };
const route = { id: 44, name: 'Gruppenweg', creator_id: 'a'.repeat(64), pois: [poi] };
const events = [], drafts = new Map();
let allowed = false;
h.context.Wegwichtel.GroupPoints = {
async begin() { events.push('lock'); return { poi }; },
async release() { events.push('release'); },
async read(id, poiId) { return drafts.get(String(poiId)); },
async remember(route, poiId, values, media) { const draft = { route, poiId, values, media }; events.push('backup'); drafts.set(String(poiId), structuredClone(draft)); return draft; },
async prepare() { events.push('prepare'); return allowed ? { poi } : false; },
async remove(id, poiId) { events.push('remove'); drafts.delete(String(poiId)); },
headersFor: () => ({ 'X-Poi-Lock': 'secret', 'If-Poi-Match': 'v1' }), observe() {},
async acceptNew() {}
};
const original = h.context.fetch;
h.context.fetch = async (url, options = {}) => {
if (url.startsWith('/api/routes/44')) {
h.calls.push({ url, ...options, method: options.method || 'GET' });
if (url === '/api/routes/44') return jsonResponse(route);
if (options.method === 'PUT') { events.push('send'); Object.assign(poi, JSON.parse(options.body)); return jsonResponse({ poi }); }
if (options.method === 'DELETE') { events.push('delete'); return jsonResponse({ deleted: true }); }
}
return original(url, options);
};
await loadI18n(h.context);
vm.runInNewContext(await fs.readFile('public/js/editor-ui.js', 'utf8'), h.context, { filename: path.resolve('public/js/editor-ui.js') });
await h.context.Wegwichtel.EditorUi.openGroup(route);
const list = h.elements.get('editor-poi-list');
list.dispatchEvent(pointerEvent(list.children[0].children[1].children[2])); await tick();
h.elements.get('editor-poi-title').value = 'Meine Blumen';
const file = new Blob(['photo'], { type: 'image/png' }); file.name = 'blume.png';
h.elements.get('editor-poi-images').files = [file];
h.elements.get('editor-poi-images').dispatchEvent(new Event('change')); await tick();
h.elements.get('editor-poi-form').dispatchEvent(submitEvent()); await tick();
assert.equal(events.includes('send'), false);
assert.equal(drafts.get('8').values.title, 'Meine Blumen');
assert.equal(drafts.get('8').media.images.length, 1);
assert.match(h.elements.get('editor-status').textContent, /auf diesem Gerät gesichert/);
// Remove the selected picture, then retry the same text edit with the lock held.
await h.context.Wegwichtel.EditorUi.openPendingPoint({ route, poiId: 8, basePoi: poi, values: poi, media: {} });
allowed = true;
h.elements.get('editor-poi-title').value = 'Gemeinsam gespeichert';
h.elements.get('editor-poi-form').dispatchEvent(submitEvent());
h.elements.get('editor-poi-form').dispatchEvent(submitEvent());
for (let i = 0; i < 6; i += 1) await tick();
assert.equal(events.filter(event => event === 'send').length, 1);
assert.ok(events.indexOf('backup') < events.indexOf('send'));
assert.equal(drafts.size, 0);
assert.equal(poi.title, 'Gemeinsam gespeichert');
const saved = h.calls.find(call => call.method === 'PUT');
assert.equal(saved.headers['X-Poi-Lock'], 'secret');
allowed = false;
const latestList = h.elements.get('editor-poi-list');
latestList.dispatchEvent(pointerEvent(latestList.children[0].children[1].children[3]));
for (let i = 0; i < 4; i += 1) await tick();
assert.equal(drafts.get('8').media.deletePoint, true);
assert.equal(events.includes('delete'), false);
allowed = true;
h.elements.get('editor-poi-form').dispatchEvent(submitEvent());
for (let i = 0; i < 5; i += 1) await tick();
assert.equal(events.includes('delete'), true);
assert.equal(drafts.size, 0);
h.context.Wegwichtel.EditorUi.close();
assert.equal(events.at(-1), 'release');
});