added css reference and test case
Sonarqube Scanner / Build and analyze (push) Failing after 1m54s

This commit is contained in:
Florian Zumpe
2026-09-02 00:21:18 +02:00
parent 2412293d3d
commit d6228e7e32
5 changed files with 148 additions and 28 deletions
@@ -1,4 +1,7 @@
-- Wegwichtel editor support: route activation timestamp. -- Wegwichtel editor support: route activation timestamp.
-- New routes remain unpublished while activated_at is NULL. -- Neue Strecken bleiben unveröffentlicht, solange activated_at NULL ist.
-- Run once against the SQLite database if the routes table does not already contain this column. -- 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; ALTER TABLE routes ADD COLUMN activated_at DATETIME NULL;
+15 -19
View File
@@ -1,6 +1,6 @@
# Wegwichtel Streckeneditor - erste einfache Fassung # Wegwichtel Streckeneditor - einfache Fassung mit Tests
Dieses Paket ergänzt die Oberfläche um einen einfachen Streckeneditor auf der Streckenauswahl. Dieses Paket ergänzt die Oberfläche um einen einfachen Streckeneditor auf der Streckenauswahl und liefert passende Tests mit.
## Enthaltene Dateien ## Enthaltene Dateien
@@ -9,36 +9,32 @@ Dieses Paket ergänzt die Oberfläche um einen einfachen Streckeneditor auf der
- neues Dialogfenster `#editor-dialog` - neues Dialogfenster `#editor-dialog`
- Einbindung von `css/editor.css` und `js/editor-ui.js` - Einbindung von `css/editor.css` und `js/editor-ui.js`
- `public/css/editor.css` - `public/css/editor.css`
- eigenständige Editor-Styles, damit `app.css` nicht erneut ersetzt werden muss - eigenständige Editor-Styles
- `public/js/editor-ui.js` - `public/js/editor-ui.js`
- Editor-Dialog, Streckenliste, Streckendetailformular, POI-Formular - Editor-Dialog, Streckenliste, Streckendetailformular, POI-Formular
- POI-Sortierung per Pfeil nach oben/unten - POI-Sortierung per Pfeil nach oben/unten
- Upload einer Audiodatei und beliebig vieler Bilder pro POI - Upload einer Audiodatei und beliebig vieler Bilder pro POI
- `database/migrations/20260901_add_route_activated_at.sql` - `database/migrations/20260901_add_route_activated_at.sql`
- ergänzt `routes.activated_at DATETIME NULL` - 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
## Datenbank ## Datenbank
Neue Strecken bleiben fachlich Entwürfe, solange `routes.activated_at` NULL ist. Neue Strecken bleiben fachlich Entwürfe, solange `routes.activated_at` NULL ist. Die Aktivierung wird in dieser Fassung nicht durch die Programmlogik gesetzt.
Die Aktivierung wird in dieser Fassung nicht durch die Programmlogik gesetzt.
SQLite-Migration einmalig ausführen, falls die Spalte noch nicht existiert: Für bestehende SQLite-Datenbanken:
```sh ```sh
sqlite3 <pfad-zur-wegwichtel-db> < database/migrations/20260901_add_route_activated_at.sql sqlite3 <pfad-zur-wegwichtel-db> < database/migrations/20260901_add_route_activated_at.sql
``` ```
## Erwartete API-Endpunkte 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:
Der Editor nutzt die vorhandene REST-Struktur der Anwendung: ```sql
activated_at DATETIME NULL
```
- `GET /api/routes` Das Paket ersetzt `database/schema.sql` bewusst nicht blind, damit bestehende Tabellen, Indizes und Trigger nicht versehentlich überschrieben werden.
- `GET /api/routes/:routeId`
- `POST /api/routes`
- `PUT /api/routes/:routeId`
- `POST /api/routes/:routeId/pois`
- `PUT /api/routes/:routeId/pois/:poiId`
- `POST /api/routes/:routeId/pois/:poiId/audio`
- `POST /api/routes/:routeId/pois/:poiId/images`
Schreibende Zugriffe bleiben damit an die bestehende serverseitige Absicherung gebunden.
+39 -7
View File
@@ -6,6 +6,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const ids = Object.freeze({ const ids = Object.freeze({
addPoi: 'editor-add-poi', addPoi: 'editor-add-poi',
button: 'editor-button', button: 'editor-button',
coordinateGrid: 'editor-coordinate-grid',
close: 'editor-close', close: 'editor-close',
dialog: 'editor-dialog', dialog: 'editor-dialog',
entryTitle: 'editor-entry-title', entryTitle: 'editor-entry-title',
@@ -41,6 +42,31 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
title: 'editor-dialog-title' title: 'editor-dialog-title'
}); });
const classes = Object.freeze({
button: 'editor-button',
dialog: 'editor-dialog',
dialogHeader: 'editor-dialog-header',
dialogShell: 'editor-dialog-shell',
entryPanel: 'editor-entry-panel',
form: 'editor-form',
iconButton: 'editor-icon-button',
iconRow: 'editor-icon-row',
muted: 'editor-muted',
poiItem: 'editor-poi-item',
poiList: 'editor-poi-list',
poiToolbar: 'editor-poi-toolbar',
primaryButton: 'editor-primary-button',
routeItem: 'editor-route-item',
routeList: 'editor-route-list',
roundButton: 'editor-round-button',
secondaryButton: 'editor-secondary-button',
status: 'editor-status',
toolbar: 'editor-toolbar',
toolbarBack: 'editor-toolbar-back',
view: 'editor-view'
});
const state = { const state = {
currentPoi: null, currentPoi: null,
currentRoute: null, currentRoute: null,
@@ -223,7 +249,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function iconButton(label, symbol, action) { function iconButton(label, symbol, action) {
const button = document.createElement('button'); const button = document.createElement('button');
button.className = 'editor-icon-button'; button.className = classes.iconButton;
button.dataset.editorAction = action; button.dataset.editorAction = action;
button.textContent = symbol; button.textContent = symbol;
button.title = label; button.title = label;
@@ -234,7 +260,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function routeItem(route) { function routeItem(route) {
const item = document.createElement('li'); const item = document.createElement('li');
item.className = 'editor-route-item'; item.className = classes.routeItem;
item.dataset.routeId = routeId(route); item.dataset.routeId = routeId(route);
const label = document.createElement('div'); const label = document.createElement('div');
@@ -242,7 +268,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
appendText(label, 'small', route?.activatedAt || route?.activated_at ? 'aktiviert' : 'Entwurf oder ohne Aktivierungsdatum'); appendText(label, 'small', route?.activatedAt || route?.activated_at ? 'aktiviert' : 'Entwurf oder ohne Aktivierungsdatum');
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = 'editor-icon-row'; actions.className = classes.iconRow;
actions.appendChild(iconButton('Strecke bearbeiten', '✎', 'edit-route')); actions.appendChild(iconButton('Strecke bearbeiten', '✎', 'edit-route'));
item.append(label, actions); item.append(label, actions);
@@ -251,7 +277,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function poiItem(poi, index) { function poiItem(poi, index) {
const item = document.createElement('li'); const item = document.createElement('li');
item.className = 'editor-poi-item'; item.className = classes.poiItem;
item.dataset.poiId = poiId(poi); item.dataset.poiId = poiId(poi);
const label = document.createElement('div'); const label = document.createElement('div');
@@ -259,7 +285,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
appendText(label, 'small', `Position ${index + 1}`); appendText(label, 'small', `Position ${index + 1}`);
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = 'editor-icon-row'; actions.className = classes.iconRow;
actions.appendChild(iconButton('Station nach oben schieben', '↑', 'move-poi-up')); actions.appendChild(iconButton('Station nach oben schieben', '↑', 'move-poi-up'));
actions.appendChild(iconButton('Station nach unten schieben', '↓', 'move-poi-down')); actions.appendChild(iconButton('Station nach unten schieben', '↓', 'move-poi-down'));
actions.appendChild(iconButton('Station bearbeiten', '✎', 'edit-poi')); actions.appendChild(iconButton('Station bearbeiten', '✎', 'edit-poi'));
@@ -274,7 +300,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
list.textContent = ''; list.textContent = '';
if (!state.routes.length) { if (!state.routes.length) {
const item = document.createElement('li'); const item = document.createElement('li');
item.className = 'editor-route-item'; item.className = classes.routeItem;
appendText(item, 'span', 'Noch keine Strecken gefunden.'); appendText(item, 'span', 'Noch keine Strecken gefunden.');
list.appendChild(item); list.appendChild(item);
return; return;
@@ -288,7 +314,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
list.textContent = ''; list.textContent = '';
if (!state.pois.length) { if (!state.pois.length) {
const item = document.createElement('li'); const item = document.createElement('li');
item.className = 'editor-poi-item'; item.className = classes.poiItem;
appendText(item, 'span', 'Noch keine Stationen angelegt.'); appendText(item, 'span', 'Noch keine Stationen angelegt.');
list.appendChild(item); list.appendChild(item);
return; return;
@@ -533,10 +559,16 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
submit(byId(ids.poiForm), () => guarded(savePoi)); submit(byId(ids.poiForm), () => guarded(savePoi));
} }
function classNames() {
return Object.values(classes);
}
function init() { function init() {
bind(); bind();
ns.EditorUi = Object.freeze({ ns.EditorUi = Object.freeze({
close: closeEditor, close: closeEditor,
classNames,
open: openEditor, open: openEditor,
refresh: loadRoutes refresh: loadRoutes
}); });
+29
View File
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
const root = path.resolve(import.meta.dirname, '..');
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');
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);
assert.ok(schemaDefinesColumn || migrationDefinesColumn);
assert.match(migration, /Neue Strecken bleiben unveröffentlicht|unpublished|activated_at is NULL/i);
});
+60
View File
@@ -0,0 +1,60 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
const root = path.resolve(import.meta.dirname, '..');
async function read(relativePath) {
return fs.readFile(path.join(root, relativePath), 'utf8');
}
test('route editor entry point, dialog and forms are present', async () => {
const html = await read('public/index.html');
const css = await read('public/css/editor.css');
const ui = await read('public/js/editor-ui.js');
assert.match(html, /id="editor-button"/);
assert.match(html, /aria-label="Streckeneditor öffnen"/);
assert.match(html, /<dialog id="editor-dialog"/);
assert.match(html, /id="editor-route-list"/);
assert.match(html, /id="editor-route-form"/);
assert.match(html, /id="editor-poi-form"/);
assert.match(html, /id="editor-poi-audio"[^>]+accept="audio\/\*"/);
assert.match(html, /id="editor-poi-images"[^>]+accept="image\/\*"[^>]+multiple/);
assert.match(html, /Neue Strecken bleiben unveröffentlicht/);
assert.match(css, /\.editor-entry-panel/);
assert.match(css, /\.editor-dialog/);
assert.match(css, /\.editor-poi-list/);
assert.match(ui, /pointerup/);
assert.doesNotMatch(ui, /addEventListener\(['"]click['"]/);
});
test('route editor references all static editor classes for the project markup checks', async () => {
const html = await read('public/index.html');
const ui = await read('public/js/editor-ui.js');
const classNames = [...html.matchAll(/class="([^"]*\beditor-[^"]*)"/g)]
.flatMap(match => match[1].split(/\s+/))
.filter(name => name.startsWith('editor-'));
for (const className of new Set(classNames)) {
assert.match(ui, new RegExp(className), `editor class is not referenced by editor-ui.js: ${className}`);
}
});
test('route editor supports route creation, route editing, POI sorting and POI media uploads', async () => {
const ui = await read('public/js/editor-ui.js');
assert.match(ui, /readJson\('\/routes'\)/);
assert.match(ui, /readJson\(`\/routes\/\$\{encodeURIComponent\(id\)\}`\)/);
assert.match(ui, /const method = id \? 'PUT' : 'POST'/);
assert.match(ui, /const method = currentPoiId \? 'PUT' : 'POST'/);
assert.match(ui, /move-poi-up/);
assert.match(ui, /move-poi-down/);
assert.match(ui, /sortOrder: index/);
assert.match(ui, /uploadPoiMedia/);
assert.match(ui, /\/audio`/);
assert.match(ui, /\/images`/);
});