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.
-- New routes remain unpublished while activated_at is NULL.
-- Run once against the SQLite database if the routes table does not already contain this column.
-- 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;
+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
@@ -9,36 +9,32 @@ Dieses Paket ergänzt die Oberfläche um einen einfachen Streckeneditor auf der
- neues Dialogfenster `#editor-dialog`
- Einbindung von `css/editor.css` und `js/editor-ui.js`
- `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`
- 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`
- 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
Neue Strecken bleiben fachlich Entwürfe, solange `routes.activated_at` NULL ist.
Die Aktivierung wird in dieser Fassung nicht durch die Programmlogik gesetzt.
Neue Strecken bleiben fachlich Entwürfe, solange `routes.activated_at` NULL ist. 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
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`
- `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.
Das Paket ersetzt `database/schema.sql` bewusst nicht blind, damit bestehende Tabellen, Indizes und Trigger nicht versehentlich überschrieben werden.
+39 -7
View File
@@ -6,6 +6,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const ids = Object.freeze({
addPoi: 'editor-add-poi',
button: 'editor-button',
coordinateGrid: 'editor-coordinate-grid',
close: 'editor-close',
dialog: 'editor-dialog',
entryTitle: 'editor-entry-title',
@@ -41,6 +42,31 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
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 = {
currentPoi: null,
currentRoute: null,
@@ -223,7 +249,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function iconButton(label, symbol, action) {
const button = document.createElement('button');
button.className = 'editor-icon-button';
button.className = classes.iconButton;
button.dataset.editorAction = action;
button.textContent = symbol;
button.title = label;
@@ -234,7 +260,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function routeItem(route) {
const item = document.createElement('li');
item.className = 'editor-route-item';
item.className = classes.routeItem;
item.dataset.routeId = routeId(route);
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');
const actions = document.createElement('div');
actions.className = 'editor-icon-row';
actions.className = classes.iconRow;
actions.appendChild(iconButton('Strecke bearbeiten', '✎', 'edit-route'));
item.append(label, actions);
@@ -251,7 +277,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function poiItem(poi, index) {
const item = document.createElement('li');
item.className = 'editor-poi-item';
item.className = classes.poiItem;
item.dataset.poiId = poiId(poi);
const label = document.createElement('div');
@@ -259,7 +285,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
appendText(label, 'small', `Position ${index + 1}`);
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 unten schieben', '↓', 'move-poi-down'));
actions.appendChild(iconButton('Station bearbeiten', '✎', 'edit-poi'));
@@ -274,7 +300,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
list.textContent = '';
if (!state.routes.length) {
const item = document.createElement('li');
item.className = 'editor-route-item';
item.className = classes.routeItem;
appendText(item, 'span', 'Noch keine Strecken gefunden.');
list.appendChild(item);
return;
@@ -288,7 +314,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
list.textContent = '';
if (!state.pois.length) {
const item = document.createElement('li');
item.className = 'editor-poi-item';
item.className = classes.poiItem;
appendText(item, 'span', 'Noch keine Stationen angelegt.');
list.appendChild(item);
return;
@@ -533,10 +559,16 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
submit(byId(ids.poiForm), () => guarded(savePoi));
}
function classNames() {
return Object.values(classes);
}
function init() {
bind();
ns.EditorUi = Object.freeze({
close: closeEditor,
classNames,
open: openEditor,
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`/);
});