update database schema and update editor script
Sonarqube Scanner / Build and analyze (push) Failing after 26s
Sonarqube Scanner / Build and analyze (push) Failing after 26s
This commit is contained in:
@@ -1,7 +1,3 @@
|
||||
-- Wegwichtel editor support: route activation timestamp.
|
||||
-- Neue Strecken bleiben unveröffentlicht, solange activated_at NULL ist.
|
||||
-- Diese Migration einmalig gegen eine bestehende SQLite-Datenbank ausführen.
|
||||
-- Bei einer neu erzeugten Datenbank ohne Migrationslauf muss dieselbe Spalte
|
||||
-- direkt in der CREATE-TABLE-Anweisung der routes-Tabelle enthalten sein:
|
||||
-- activated_at DATETIME NULL
|
||||
ALTER TABLE routes ADD COLUMN activated_at DATETIME NULL;
|
||||
-- Ergänzt den manuellen Freigabezeitpunkt für Strecken.
|
||||
-- Neue Strecken bleiben unveröffentlicht, solange activated_at is NULL.
|
||||
ALTER TABLE routes ADD COLUMN activated_at TEXT NULL DEFAULT NULL;
|
||||
|
||||
+12
-1
@@ -19,10 +19,21 @@ CREATE TABLE IF NOT EXISTS routes (
|
||||
point_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TEXT,
|
||||
deleted_at TEXT DEFAULT NULL,
|
||||
activated_at TEXT DEFAULT NULL,
|
||||
trash_path TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS routes_updated_at
|
||||
AFTER UPDATE ON routes
|
||||
FOR EACH ROW
|
||||
WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE routes
|
||||
SET updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS route_points (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
route_id INTEGER NOT NULL REFERENCES routes(id),
|
||||
|
||||
+26
-28
@@ -1,40 +1,38 @@
|
||||
# Wegwichtel Streckeneditor - einfache Fassung mit Tests
|
||||
# Wegwichtel Editor v3
|
||||
|
||||
Dieses Paket ergänzt die Oberfläche um einen einfachen Streckeneditor auf der Streckenauswahl und liefert passende Tests mit.
|
||||
Dieses Paket ergänzt den einfachen Streckeneditor und die zugehörigen Tests.
|
||||
|
||||
## Enthaltene Dateien
|
||||
## Datenbankschema
|
||||
|
||||
- `public/index.html`
|
||||
- neuer Abschnitt `Strecken verwalten` mit rundem Plus-Button
|
||||
- neues Dialogfenster `#editor-dialog`
|
||||
- Einbindung von `css/editor.css` und `js/editor-ui.js`
|
||||
- `public/css/editor.css`
|
||||
- eigenständige Editor-Styles
|
||||
- `public/js/editor-ui.js`
|
||||
- Editor-Dialog, Streckenliste, Streckendetailformular, POI-Formular
|
||||
- POI-Sortierung per Pfeil nach oben/unten
|
||||
- Upload einer Audiodatei und beliebig vieler Bilder pro POI
|
||||
- `database/migrations/20260901_add_route_activated_at.sql`
|
||||
- ergänzt `routes.activated_at DATETIME NULL` für bestehende SQLite-Datenbanken
|
||||
- `test/editor-ui.test.js`
|
||||
- prüft Editor-Markup, Editor-Klassen, Pointer-Events, CRUD-/Sortier-/Upload-Anbindungen
|
||||
- `test/editor-database-schema.test.js`
|
||||
- prüft, dass `activated_at DATETIME NULL` über Schema oder Migration verfügbar ist
|
||||
Für bestehende SQLite-Datenbanken liegt weiterhin eine Migration vor:
|
||||
|
||||
## Datenbank
|
||||
|
||||
Neue Strecken bleiben fachlich Entwürfe, solange `routes.activated_at` NULL ist. Die Aktivierung wird in dieser Fassung nicht durch die Programmlogik gesetzt.
|
||||
|
||||
Für bestehende SQLite-Datenbanken:
|
||||
|
||||
```sh
|
||||
```bash
|
||||
sqlite3 <pfad-zur-wegwichtel-db> < database/migrations/20260901_add_route_activated_at.sql
|
||||
```
|
||||
|
||||
Falls `database/schema.sql` für komplett neue Datenbanken ohne Migrationslauf verwendet wird, muss dort in der `routes`-Tabelle zusätzlich diese Spalte enthalten sein:
|
||||
Für neu erzeugte Datenbanken muss `database/schema.sql` die Spalte direkt in der `routes`-Tabelle enthalten:
|
||||
|
||||
```sql
|
||||
activated_at DATETIME NULL
|
||||
```
|
||||
|
||||
Das Paket ersetzt `database/schema.sql` bewusst nicht blind, damit bestehende Tabellen, Indizes und Trigger nicht versehentlich überschrieben werden.
|
||||
Falls das Paket ohne bereits angepasste `database/schema.sql` eingespielt wird, kann die Datei einmalig im Repository angepasst werden:
|
||||
|
||||
```bash
|
||||
node scripts/apply-editor-schema.mjs
|
||||
```
|
||||
|
||||
Danach sollte `database/schema.sql` mit committed werden.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
node --check public/js/editor-ui.js
|
||||
npm test -- test/editor-ui.test.js
|
||||
npm test -- test/editor-database-schema.test.js
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## SonarQube
|
||||
|
||||
Der frühere HTML-Smell durch `role="status"` am Editorstatus wurde entfernt. Der Status ist jetzt ein semantisches `<output>` mit `aria-live="polite"`.
|
||||
|
||||
+1
-1
@@ -328,7 +328,7 @@
|
||||
<button id="editor-close" type="button" aria-label="Streckeneditor schließen" title="Schließen">×</button>
|
||||
</header>
|
||||
|
||||
<p id="editor-status" class="editor-status" role="status" aria-live="polite" hidden></p>
|
||||
<output id="editor-status" class="editor-status" aria-live="polite" hidden></output>
|
||||
|
||||
<section id="editor-route-list-view" class="editor-view" aria-labelledby="editor-route-list-title">
|
||||
<div class="editor-toolbar">
|
||||
|
||||
+46
-4
@@ -564,14 +564,56 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
return Object.values(classes);
|
||||
}
|
||||
|
||||
function init() {
|
||||
bind();
|
||||
ns.EditorUi = Object.freeze({
|
||||
function testHooks() {
|
||||
return Object.freeze({
|
||||
apiUrl,
|
||||
classNames,
|
||||
closeEditor,
|
||||
fillPoiForm,
|
||||
fillRouteForm,
|
||||
handleResponse,
|
||||
isActivation,
|
||||
loadRoute,
|
||||
loadRoutes,
|
||||
newPoi,
|
||||
newRoute,
|
||||
poiArray,
|
||||
poiFromForm,
|
||||
poiItem,
|
||||
parseJson,
|
||||
readJson,
|
||||
renderPoiList,
|
||||
renderRouteList,
|
||||
routeArray,
|
||||
routeFromForm,
|
||||
routeItem,
|
||||
savePoi,
|
||||
sendForm,
|
||||
sendJson,
|
||||
setStatus,
|
||||
savePoiOrder,
|
||||
saveRoute,
|
||||
state,
|
||||
swapPois,
|
||||
uploadPoiMedia
|
||||
});
|
||||
}
|
||||
|
||||
function editorApi() {
|
||||
const api = {
|
||||
close: closeEditor,
|
||||
classNames,
|
||||
open: openEditor,
|
||||
refresh: loadRoutes
|
||||
});
|
||||
};
|
||||
|
||||
if (globalThis.WegwichtelEditorTestHooks === true) api.testHooks = testHooks();
|
||||
return Object.freeze(api);
|
||||
}
|
||||
|
||||
function init() {
|
||||
bind();
|
||||
ns.EditorUi = editorApi();
|
||||
}
|
||||
|
||||
ready(init);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const schemaPath = path.join(root, 'database/schema.sql');
|
||||
const columnSql = ' activated_at DATETIME NULL';
|
||||
|
||||
function hasActivatedAt(schema) {
|
||||
return /\bactivated_at\s+DATETIME\s+NULL\b/i.test(schema);
|
||||
}
|
||||
|
||||
function splitRouteTableBody(body) {
|
||||
const lines = body.split('\n');
|
||||
const trailing = [];
|
||||
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
||||
trailing.unshift(lines.pop());
|
||||
}
|
||||
return { lines, trailing };
|
||||
}
|
||||
|
||||
function isTableConstraint(line) {
|
||||
return /^\s*,?\s*(?:PRIMARY\s+KEY|CONSTRAINT|FOREIGN\s+KEY|UNIQUE\s*\()/i.test(line);
|
||||
}
|
||||
|
||||
function hasLeadingComma(line) {
|
||||
return /^\s*,/.test(line);
|
||||
}
|
||||
|
||||
function ensureCommaBeforeInsert(lines, targetIndex) {
|
||||
const previousIndex = targetIndex - 1;
|
||||
if (previousIndex < 0) return;
|
||||
const previous = lines[previousIndex].trimEnd();
|
||||
if (!previous.endsWith(',')) lines[previousIndex] = `${previous},`;
|
||||
}
|
||||
|
||||
function columnForTarget(lines, insertBefore) {
|
||||
if (insertBefore === -1) return columnSql;
|
||||
return hasLeadingComma(lines[insertBefore]) ? columnSql : `${columnSql},`;
|
||||
}
|
||||
|
||||
function insertActivatedAt(schema) {
|
||||
if (hasActivatedAt(schema)) return schema;
|
||||
|
||||
const routeTable = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?routes\s*\((?<body>[\s\S]*?)\);/i.exec(schema);
|
||||
if (!routeTable?.groups?.body) {
|
||||
throw new Error('Die routes-Tabelle wurde in database/schema.sql nicht gefunden.');
|
||||
}
|
||||
|
||||
const tableSql = routeTable[0];
|
||||
const body = routeTable.groups.body;
|
||||
const { lines, trailing } = splitRouteTableBody(body);
|
||||
const insertBefore = lines.findIndex(isTableConstraint);
|
||||
const targetIndex = insertBefore === -1 ? lines.length : insertBefore;
|
||||
|
||||
ensureCommaBeforeInsert(lines, targetIndex);
|
||||
lines.splice(targetIndex, 0, columnForTarget(lines, insertBefore));
|
||||
|
||||
return schema.replace(tableSql, tableSql.replace(body, [...lines, ...trailing].join('\n')));
|
||||
}
|
||||
|
||||
const schema = await fs.readFile(schemaPath, 'utf8');
|
||||
const next = insertActivatedAt(schema);
|
||||
if (next === schema) {
|
||||
console.log('database/schema.sql enthält activated_at bereits.');
|
||||
} else {
|
||||
await fs.writeFile(schemaPath, next);
|
||||
console.log('database/schema.sql wurde um routes.activated_at ergänzt.');
|
||||
}
|
||||
@@ -9,21 +9,13 @@ async function read(relativePath) {
|
||||
return fs.readFile(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
async function readIfExists(relativePath) {
|
||||
try {
|
||||
return await read(relativePath);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return '';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
test('route activation timestamp is available for editor draft handling', async () => {
|
||||
const schema = await readIfExists('database/schema.sql');
|
||||
test('initial database schema creates routes with a nullable activation timestamp', async () => {
|
||||
const schema = await read('database/schema.sql');
|
||||
const migration = await read('database/migrations/20260901_add_route_activated_at.sql');
|
||||
const schemaDefinesColumn = /\bactivated_at\s+DATETIME\s+NULL\b/i.test(schema);
|
||||
const migrationDefinesColumn = /ALTER\s+TABLE\s+routes\s+ADD\s+COLUMN\s+activated_at\s+DATETIME\s+NULL/i.test(migration);
|
||||
const routeTable = schema.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?routes\s*\([\s\S]*?\);/i);
|
||||
|
||||
assert.ok(schemaDefinesColumn || migrationDefinesColumn);
|
||||
assert.ok(routeTable, 'database/schema.sql must define the routes table');
|
||||
assert.match(routeTable[0], /\bactivated_at\s+DATETIME\s+NULL\b/i);
|
||||
assert.match(migration, /ALTER\s+TABLE\s+routes\s+ADD\s+COLUMN\s+activated_at\s+DATETIME\s+NULL/i);
|
||||
assert.match(migration, /Neue Strecken bleiben unveröffentlicht|unpublished|activated_at is NULL/i);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
|
||||
@@ -17,6 +18,8 @@ test('route editor entry point, dialog and forms are present', async () => {
|
||||
assert.match(html, /id="editor-button"/);
|
||||
assert.match(html, /aria-label="Streckeneditor öffnen"/);
|
||||
assert.match(html, /<dialog id="editor-dialog"/);
|
||||
assert.match(html, /<output id="editor-status" class="editor-status" aria-live="polite" hidden><\/output>/);
|
||||
assert.doesNotMatch(html, /id="editor-status"[^>]*role="status"/);
|
||||
assert.match(html, /id="editor-route-list"/);
|
||||
assert.match(html, /id="editor-route-form"/);
|
||||
assert.match(html, /id="editor-poi-form"/);
|
||||
@@ -58,3 +61,293 @@ test('route editor supports route creation, route editing, POI sorting and POI m
|
||||
assert.match(ui, /\/audio`/);
|
||||
assert.match(ui, /\/images`/);
|
||||
});
|
||||
|
||||
class FakeElement extends EventTarget {
|
||||
constructor(id = '', tagName = 'div') {
|
||||
super();
|
||||
this.id = id;
|
||||
this.tagName = tagName.toUpperCase();
|
||||
this.hidden = false;
|
||||
this.open = false;
|
||||
this.value = '';
|
||||
this.textContent = '';
|
||||
this.className = '';
|
||||
this.children = [];
|
||||
this.dataset = {};
|
||||
this.files = [];
|
||||
this.parentElement = null;
|
||||
this.attributes = new Map();
|
||||
}
|
||||
|
||||
append(...children) {
|
||||
children.forEach(child => this.appendChild(child));
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
if (!child) return child;
|
||||
child.parentElement = this;
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes.set(name, String(value));
|
||||
if (name === 'open') this.open = true;
|
||||
}
|
||||
|
||||
removeAttribute(name) {
|
||||
this.attributes.delete(name);
|
||||
if (name === 'open') this.open = false;
|
||||
}
|
||||
|
||||
showModal() {
|
||||
this.open = true;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
closest(selector) {
|
||||
if (selector === '[data-route-id]') return this.dataset.routeId ? this : this.parentElement?.closest(selector) ?? null;
|
||||
if (selector === '[data-poi-id]') return this.dataset.poiId ? this : this.parentElement?.closest(selector) ?? null;
|
||||
if (selector === '[data-editor-action]') return this.dataset.editorAction ? this : this.parentElement?.closest(selector) ?? null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeFormData {
|
||||
constructor() {
|
||||
this.entries = [];
|
||||
}
|
||||
|
||||
append(name, value) {
|
||||
this.entries.push([name, value]);
|
||||
}
|
||||
}
|
||||
|
||||
function jsonResponse(value, ok = true, status = 200) {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
text: async () => JSON.stringify(value)
|
||||
};
|
||||
}
|
||||
|
||||
function textResponse(value, ok = false, status = 500) {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
text: async () => value
|
||||
};
|
||||
}
|
||||
|
||||
function pointerEvent({ pointerType = 'touch', button = 0, isPrimary = true } = {}) {
|
||||
const event = new Event('pointerup', { bubbles: true, cancelable: true });
|
||||
Object.defineProperties(event, {
|
||||
button: { value: button },
|
||||
isPrimary: { value: isPrimary },
|
||||
pointerType: { value: pointerType }
|
||||
});
|
||||
return event;
|
||||
}
|
||||
|
||||
function buildEditorContext() {
|
||||
const ids = [
|
||||
'editor-add-poi',
|
||||
'editor-button',
|
||||
'editor-close',
|
||||
'editor-dialog',
|
||||
'editor-new-route',
|
||||
'editor-poi-audio',
|
||||
'editor-poi-cancel',
|
||||
'editor-poi-description',
|
||||
'editor-poi-form',
|
||||
'editor-poi-form-view',
|
||||
'editor-poi-id',
|
||||
'editor-poi-images',
|
||||
'editor-poi-lat',
|
||||
'editor-poi-list',
|
||||
'editor-poi-lon',
|
||||
'editor-poi-radius',
|
||||
'editor-poi-title',
|
||||
'editor-route-activated',
|
||||
'editor-route-back',
|
||||
'editor-route-description',
|
||||
'editor-route-detail-view',
|
||||
'editor-route-form',
|
||||
'editor-route-gpx',
|
||||
'editor-route-id',
|
||||
'editor-route-list',
|
||||
'editor-route-list-view',
|
||||
'editor-route-name',
|
||||
'editor-status'
|
||||
];
|
||||
const elements = new Map(ids.map(id => [id, new FakeElement(id)]));
|
||||
elements.get('editor-dialog').open = false;
|
||||
elements.get('editor-route-gpx').files = [];
|
||||
elements.get('editor-poi-audio').files = [];
|
||||
elements.get('editor-poi-images').files = [];
|
||||
|
||||
const document = new EventTarget();
|
||||
document.readyState = 'complete';
|
||||
document.getElementById = id => elements.get(id) ?? null;
|
||||
document.createElement = tagName => new FakeElement('', tagName);
|
||||
|
||||
const calls = [];
|
||||
const routes = [
|
||||
{ id: 1, name: 'Waldweg', description: 'Route im Wald', activated_at: null }
|
||||
];
|
||||
const routeDetail = {
|
||||
id: 1,
|
||||
name: 'Waldweg',
|
||||
description: 'Route im Wald',
|
||||
pois: [
|
||||
{ id: 10, title: 'Start', description: 'Losgehen', lat: 50.1, lon: 10.1, radiusM: 20, sortOrder: 0 },
|
||||
{ id: 11, title: 'Ziel', description: 'Ankommen', lat: 50.2, lon: 10.2, radiusM: 30, sortOrder: 1 }
|
||||
]
|
||||
};
|
||||
|
||||
async function fetch(url, options = {}) {
|
||||
calls.push({ body: options.body, method: options.method || 'GET', url });
|
||||
if (url === '/api/routes' && (!options.method || options.method === 'GET')) return jsonResponse({ routes });
|
||||
if (url === '/api/routes/1' && (!options.method || options.method === 'GET')) return jsonResponse({ route: routeDetail });
|
||||
if (url === '/api/routes/2' && (!options.method || options.method === 'GET')) return jsonResponse({ route: { id: 2, name: 'Neue Strecke', description: 'Neu', pois: [] } });
|
||||
if (url === '/api/routes' && options.method === 'POST') return jsonResponse({ route: { id: 2, name: 'Neue Strecke', description: 'Neu' } });
|
||||
if (url === '/api/routes/1' && options.method === 'PUT') return jsonResponse({ route: routeDetail });
|
||||
if (url === '/api/routes/1/pois' && options.method === 'POST') return jsonResponse({ poi: { id: 12, title: 'Neue Station' } });
|
||||
if (/^\/api\/routes\/1\/pois\/\d+$/.test(url) && options.method === 'PUT') return jsonResponse({ poi: { id: Number(url.split('/').pop()), title: 'Station' } });
|
||||
if (/^\/api\/routes\/1\/pois\/\d+\/(audio|images)$/.test(url) && options.method === 'POST') return jsonResponse({ ok: true });
|
||||
return textResponse(`unexpected ${options.method || 'GET'} ${url}`, false, 404);
|
||||
}
|
||||
|
||||
const context = {
|
||||
document,
|
||||
Event,
|
||||
EventTarget,
|
||||
fetch,
|
||||
FormData: FakeFormData,
|
||||
globalThis: null,
|
||||
Wegwichtel: {
|
||||
Config: { apiBase: '/api/' }
|
||||
},
|
||||
WegwichtelEditorTestHooks: true
|
||||
};
|
||||
context.globalThis = context;
|
||||
return { calls, context, elements };
|
||||
}
|
||||
|
||||
test('route editor workflow is executable with draft routes and media uploads', 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 });
|
||||
const hooks = context.Wegwichtel.EditorUi.testHooks;
|
||||
|
||||
assert.deepEqual(context.Wegwichtel.EditorUi.classNames(), hooks.classNames());
|
||||
assert.equal(hooks.apiUrl('/routes'), '/api/routes');
|
||||
assert.equal(hooks.isActivation(pointerEvent()), true);
|
||||
assert.equal(hooks.isActivation(pointerEvent({ pointerType: 'mouse', button: 1 })), false);
|
||||
|
||||
await context.Wegwichtel.EditorUi.open();
|
||||
assert.equal(elements.get('editor-dialog').open, true);
|
||||
assert.equal(elements.get('editor-route-list-view').hidden, false);
|
||||
assert.equal(elements.get('editor-route-detail-view').hidden, true);
|
||||
assert.equal(elements.get('editor-route-list').children.length, 1);
|
||||
|
||||
await hooks.loadRoute({ id: 1 });
|
||||
assert.equal(elements.get('editor-route-id').value, '1');
|
||||
assert.equal(elements.get('editor-route-name').value, 'Waldweg');
|
||||
assert.match(elements.get('editor-route-activated').textContent, /Nicht veröffentlicht/);
|
||||
assert.equal(elements.get('editor-poi-list').children.length, 2);
|
||||
|
||||
hooks.swapPois(1, -1);
|
||||
assert.equal(hooks.state.pois[0].id, 11);
|
||||
await hooks.savePoiOrder();
|
||||
assert.ok(calls.some(call => call.method === 'PUT' && call.url === '/api/routes/1/pois/11'));
|
||||
|
||||
hooks.newRoute();
|
||||
elements.get('editor-route-name').value = 'Neue Strecke';
|
||||
elements.get('editor-route-description').value = 'Neu';
|
||||
await hooks.saveRoute();
|
||||
assert.ok(calls.some(call => call.method === 'POST' && call.url === '/api/routes'));
|
||||
|
||||
await hooks.loadRoute({ id: 1 });
|
||||
hooks.newPoi();
|
||||
elements.get('editor-poi-title').value = 'Neue Station';
|
||||
elements.get('editor-poi-description').value = 'Text';
|
||||
elements.get('editor-poi-lat').value = '50.123';
|
||||
elements.get('editor-poi-lon').value = '10.456';
|
||||
elements.get('editor-poi-radius').value = '42';
|
||||
elements.get('editor-poi-audio').files = [{ name: 'station.mp3' }];
|
||||
elements.get('editor-poi-images').files = [{ name: 'eins.jpg' }, { name: 'zwei.jpg' }];
|
||||
await hooks.savePoi();
|
||||
|
||||
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);
|
||||
|
||||
context.Wegwichtel.EditorUi.close();
|
||||
assert.equal(elements.get('editor-dialog').open, false);
|
||||
});
|
||||
|
||||
|
||||
test('route editor test hooks cover fallback payloads, errors and form branches', 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 });
|
||||
const hooks = context.Wegwichtel.EditorUi.testHooks;
|
||||
|
||||
assert.equal(hooks.routeArray([{ id: 'x' }]).length, 1);
|
||||
assert.equal(hooks.routeArray({ items: [{ id: 'y' }] })[0].id, 'y');
|
||||
assert.equal(hooks.routeArray(null).length, 0);
|
||||
assert.equal(hooks.poiArray({ pointsOfInterest: [{ id: 3 }] })[0].id, 3);
|
||||
assert.equal(hooks.poiArray({}).length, 0);
|
||||
|
||||
const unnamedRoute = hooks.routeItem({ id: 5 });
|
||||
assert.equal(unnamedRoute.dataset.routeId, '5');
|
||||
assert.equal(unnamedRoute.children[0].children[0].textContent, 'Unbenannte Strecke');
|
||||
const unnamedPoi = hooks.poiItem({ id: 6 }, 0);
|
||||
assert.equal(unnamedPoi.dataset.poiId, '6');
|
||||
assert.equal(unnamedPoi.children[0].children[0].textContent, 'Unbenannte Station');
|
||||
|
||||
hooks.setStatus('Fehler', true);
|
||||
assert.equal(elements.get('editor-status').hidden, false);
|
||||
assert.equal(elements.get('editor-status').dataset.kind, 'error');
|
||||
hooks.setStatus('');
|
||||
assert.equal(elements.get('editor-status').hidden, true);
|
||||
|
||||
assert.equal(Object.keys(await hooks.parseJson({ text: async () => '' })).length, 0);
|
||||
assert.equal(Object.keys(await hooks.parseJson({ text: async () => 'kein json' })).length, 0);
|
||||
await assert.rejects(
|
||||
hooks.handleResponse({ ok: false, status: 418, text: async () => '' }),
|
||||
/HTTP 418/
|
||||
);
|
||||
|
||||
await hooks.loadRoute({ id: 1 });
|
||||
elements.get('editor-route-id').value = '1';
|
||||
elements.get('editor-route-name').value = 'Waldweg geändert';
|
||||
elements.get('editor-route-description').value = 'Mit GPX';
|
||||
elements.get('editor-route-gpx').files = [{ name: 'route.gpx' }];
|
||||
await hooks.saveRoute();
|
||||
assert.ok(calls.some(call => call.method === 'PUT' && call.url === '/api/routes/1' && call.body instanceof FakeFormData));
|
||||
|
||||
elements.get('editor-poi-id').value = '10';
|
||||
elements.get('editor-poi-title').value = 'Start bearbeitet';
|
||||
elements.get('editor-poi-description').value = 'Text';
|
||||
elements.get('editor-poi-lat').value = '50.5';
|
||||
elements.get('editor-poi-lon').value = '10.5';
|
||||
elements.get('editor-poi-radius').value = '';
|
||||
elements.get('editor-poi-audio').files = [];
|
||||
elements.get('editor-poi-images').files = [];
|
||||
await hooks.savePoi();
|
||||
assert.ok(calls.some(call => call.method === 'PUT' && call.url === '/api/routes/1/pois/10'));
|
||||
|
||||
assert.equal(elements.get('editor-route-list-view').hidden, true);
|
||||
elements.get('editor-route-back').dispatchEvent(pointerEvent());
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
assert.equal(elements.get('editor-route-list-view').hidden, false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user