Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6059748f9b | ||
|
|
9f705ad7e4 |
@@ -13,3 +13,5 @@ minimax_*.png
|
||||
config.json
|
||||
*.service
|
||||
.backup/
|
||||
.venv/
|
||||
tests/__pycache__/
|
||||
|
||||
+28
-89
@@ -330,38 +330,6 @@
|
||||
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
/* BUG-05: Save-Status-Indikator (ersetzt den redundanten Speichern-Button) */
|
||||
.save-indicator {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
margin-left: auto; /* rechtsbündig im toolbar */
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.82em;
|
||||
color: var(--fg-muted);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
.save-indicator .save-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--ok);
|
||||
box-shadow: 0 0 6px rgba(74, 222, 128, 0.5);
|
||||
transition: background 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
/* Zustände */
|
||||
.save-indicator[data-state="idle"] .save-dot { background: var(--ok); }
|
||||
.save-indicator[data-state="pending"] .save-dot {
|
||||
background: var(--warn); animation: savePulse 1s ease-in-out infinite;
|
||||
}
|
||||
.save-indicator[data-state="saving"] .save-dot {
|
||||
background: var(--info); animation: savePulse 0.6s ease-in-out infinite;
|
||||
}
|
||||
.save-indicator[data-state="error"] .save-dot {
|
||||
background: var(--alert); box-shadow: 0 0 6px rgba(248, 113, 113, 0.5);
|
||||
}
|
||||
.save-indicator[data-state="error"] { color: var(--alert); }
|
||||
@keyframes savePulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
.add-form {
|
||||
display: flex; gap: 8px; align-items: center;
|
||||
background: var(--surface-2);
|
||||
@@ -1046,12 +1014,7 @@
|
||||
<button type="submit" class="primary">+ Hinzufügen</button>
|
||||
</form>
|
||||
<button onclick="packAll()" title="Alle Items automatisch anordnen">Auto-Pack</button>
|
||||
<!-- BUG-05: Speichern-Button entfernt (redundant zu debouncedSave).
|
||||
Stattdessen subtiler Save-Status-Indikator rechts. -->
|
||||
<div class="save-indicator" id="saveIndicator" data-state="idle">
|
||||
<span class="save-dot"></span>
|
||||
<span class="save-label">Gespeichert</span>
|
||||
</div>
|
||||
<button onclick="saveLayout()" class="primary" title="Layout auf Server speichern">Speichern</button>
|
||||
</div>
|
||||
<div id="gridPreview" class="grid-preview"></div>
|
||||
<div class="layout-hint">
|
||||
@@ -1281,50 +1244,8 @@
|
||||
updateClock();
|
||||
|
||||
// ============ DRAG & DROP LAYOUT EDITOR ============
|
||||
async function saveLayout() {
|
||||
// BUG-05: Status-Indikator statt redundantem "Speichern"-Button.
|
||||
// Status-Lebenszyklus:
|
||||
// pending → saving → idle (success) | error
|
||||
setSaveState('saving');
|
||||
const r = await fetch('/api/layout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: layoutItems }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) {
|
||||
setSaveState('error', 'Fehler: ' + (j.error || '?'));
|
||||
toast('Layout speichern fehlgeschlagen: ' + (j.error || '?'), 'error', 5000);
|
||||
return;
|
||||
}
|
||||
layoutItems = j.items;
|
||||
renderGrid();
|
||||
setTimeout(refreshPreview, 500);
|
||||
setSaveState('idle', 'Gespeichert');
|
||||
// nicht mehr länger toasten — der Indicator reicht
|
||||
}
|
||||
|
||||
// BUG-05: Save-Status-Indikator Helper
|
||||
function setSaveState(state, label) {
|
||||
const el = document.getElementById('saveIndicator');
|
||||
if (!el) return;
|
||||
el.dataset.state = state;
|
||||
const lbl = el.querySelector('.save-label');
|
||||
if (lbl && label) lbl.textContent = label;
|
||||
// Idle-Reset: nach kurzer Anzeige "Gespeichert" zurück
|
||||
if (state === 'saving' && !label) {
|
||||
const lblEl = el.querySelector('.save-label');
|
||||
if (lblEl) lblEl.textContent = 'Speichern…';
|
||||
}
|
||||
if (state === 'idle' && !label) {
|
||||
const lblEl = el.querySelector('.save-label');
|
||||
if (lblEl) lblEl.textContent = 'Gespeichert';
|
||||
}
|
||||
}
|
||||
// BUG-05: debouncedSave setzt pending-Status
|
||||
let saveTimer = null;
|
||||
function debouncedSave() {
|
||||
setSaveState('pending', 'Ungespeichert…');
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(saveLayout, 500);
|
||||
}
|
||||
@@ -1376,7 +1297,6 @@
|
||||
// Place items into their origin cell
|
||||
const occ = occupiedCells();
|
||||
layoutItems.forEach((it, idx) => {
|
||||
const cellIdx = it.x + ',' + it.y;
|
||||
const originCell = cont.querySelector(`[data-cell="${it.x + it.y * gridCols}"]`);
|
||||
|
||||
const div = document.createElement('div');
|
||||
@@ -1384,7 +1304,7 @@
|
||||
div.className = 'grid-item';
|
||||
if (it.w > 1 || it.h > 1) div.classList.add('span-2x2');
|
||||
div.dataset.idx = idx;
|
||||
div.draggable = true;
|
||||
div.draggable = true; // legacy HTML5-DnD bleibt für Move; Resize nutzt separaten Handler
|
||||
|
||||
// OOB marker
|
||||
const hasOOB = it.y + it.h > gridRows || it.x + it.w > gridCols || it.y < 0;
|
||||
@@ -1400,12 +1320,19 @@
|
||||
<div class="grid-item-resize" data-resize="${idx}" title="Größe ändern (Ecke ziehen)"></div>
|
||||
`;
|
||||
|
||||
if (originCell) {
|
||||
originCell.classList.add('occupied');
|
||||
originCell.appendChild(div);
|
||||
} else {
|
||||
cont.appendChild(div);
|
||||
}
|
||||
// BUG-03: Span-Geometrie per CSS Grid (grid-column/grid-row) statt
|
||||
// per Cell-DOM-Anker. Item wird direkt in den Grid-Container gehängt,
|
||||
// nicht in die Origin-Cell. Damit:
|
||||
// - NxN-Items rendern visuell über NxN Cells
|
||||
// - Drag-Events auf Nachbar-Cells werden nicht vom Item-DOM
|
||||
// verschluckt (Item ist nicht mehr Kind der Cell)
|
||||
// - Resize (applySize in startResizePointer) kann den Span nahtlos
|
||||
// aktualisieren ohne den DOM-Anker zu wechseln
|
||||
div.style.gridColumn = `${it.x + 1} / span ${it.w}`;
|
||||
div.style.gridRow = `${it.y + 1} / span ${it.h}`;
|
||||
cont.appendChild(div);
|
||||
// Origin-Cell nur als "occupied" markieren, damit sie ihre leere Optik verliert
|
||||
if (originCell) originCell.classList.add('occupied');
|
||||
});
|
||||
|
||||
// Mark occupied cells
|
||||
@@ -1687,7 +1614,19 @@
|
||||
renderGrid();
|
||||
toast('Widget hinzugefügt', 'success', 2000);
|
||||
}
|
||||
// BUG-05: alte saveLayout entfernt — siehe oben (mit setSaveState).
|
||||
async function saveLayout() {
|
||||
const r = await fetch('/api/layout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: layoutItems }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) { toast('Layout speichern fehlgeschlagen: ' + (j.error || '?'), 'error', 5000); return; }
|
||||
layoutItems = j.items;
|
||||
renderGrid();
|
||||
setTimeout(refreshPreview, 500);
|
||||
toast('Layout gespeichert', 'success');
|
||||
}
|
||||
async function packAll() {
|
||||
const ok = await modalConfirm({
|
||||
icon: 'warn',
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""RED-Test für BUG-04: Verifiziert dass die echte admin.py /api/layout/add
|
||||
Route die bestehende pack()-Semantik aufruft (also BUG bestätigt).
|
||||
|
||||
Verwendet Flask test_client, kein Live-Server.
|
||||
"""
|
||||
import sys, os, json, tempfile
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# admin.py benutzt dashboard_mod.load_config etc. — wir mocken das minimal.
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
def make_admin_app():
|
||||
"""Importiert admin mit gemockten dashboard-Funktionen. Singleton — beim
|
||||
zweiten Aufruf wird der bestehende Mock wiederverwendet, damit Tests sich
|
||||
gegenseitig konfigurieren können."""
|
||||
import importlib
|
||||
import types
|
||||
|
||||
# bestehender Mock? dann wiederverwenden
|
||||
existing = sys.modules.get("dashboard")
|
||||
if existing is None:
|
||||
fake_dashboard = types.ModuleType("dashboard")
|
||||
fake_dashboard.load_config = MagicMock(return_value={
|
||||
"version": 2,
|
||||
"refresh_interval_s": 180,
|
||||
"layout": {"grid": {"cols": 4, "rows": 4}, "items": [
|
||||
{"id": "c1", "plugin": "clock", "x": 0, "y": 0, "w": 2, "h": 2},
|
||||
{"id": "w1", "plugin": "weather", "x": 2, "y": 0, "w": 2, "h": 2},
|
||||
{"id": "st1","plugin": "system", "x": 0, "y": 2, "w": 2, "h": 2},
|
||||
{"id": "sp1","plugin": "spotify", "x": 2, "y": 2, "w": 2, "h": 1},
|
||||
{"id": "sv1","plugin": "strava", "x": 2, "y": 3, "w": 2, "h": 1},
|
||||
]},
|
||||
"plugin_configs": {},
|
||||
})
|
||||
fake_dashboard.save_config = MagicMock()
|
||||
fake_dashboard.get_widget_classes = MagicMock(return_value={"hello": MagicMock()})
|
||||
sys.modules["dashboard"] = fake_dashboard
|
||||
else:
|
||||
fake_dashboard = existing
|
||||
|
||||
admin = importlib.import_module("admin")
|
||||
admin.app.config["TESTING"] = True
|
||||
admin.require_auth = lambda: None
|
||||
return admin, fake_dashboard
|
||||
|
||||
|
||||
class TestAddRouteDoesNotRepack(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Reset Mock-Return auf den Default vor jedem Test
|
||||
admin, fake = make_admin_app()
|
||||
self._default_cfg = {
|
||||
"version": 2,
|
||||
"refresh_interval_s": 180,
|
||||
"layout": {"grid": {"cols": 4, "rows": 4}, "items": [
|
||||
{"id": "c1", "plugin": "clock", "x": 0, "y": 0, "w": 2, "h": 2},
|
||||
{"id": "w1", "plugin": "weather", "x": 2, "y": 0, "w": 2, "h": 2},
|
||||
{"id": "st1","plugin": "system", "x": 0, "y": 2, "w": 2, "h": 2},
|
||||
{"id": "sp1","plugin": "spotify", "x": 2, "y": 2, "w": 2, "h": 1},
|
||||
{"id": "sv1","plugin": "strava", "x": 2, "y": 3, "w": 2, "h": 1},
|
||||
]},
|
||||
"plugin_configs": {},
|
||||
}
|
||||
fake.load_config.return_value = self._default_cfg
|
||||
fake.save_config.reset_mock()
|
||||
self.admin, self.fake = admin, fake
|
||||
self.client = admin.app.test_client()
|
||||
|
||||
def test_add_1x1_into_gap_does_not_move_existing(self):
|
||||
"""Bestehende config mit Lücke bei (2,3),(3,3). Add hello (1x1) soll
|
||||
auf (2,3) gehen (erste scan-line freie Zelle) — alle anderen UNVERÄNDERT."""
|
||||
self.fake.load_config.return_value = {
|
||||
"version": 2,
|
||||
"refresh_interval_s": 180,
|
||||
"layout": {"grid": {"cols": 4, "rows": 4}, "items": [
|
||||
{"id": "c1", "plugin": "clock", "x": 0, "y": 0, "w": 2, "h": 2},
|
||||
{"id": "w1", "plugin": "weather", "x": 2, "y": 0, "w": 2, "h": 2},
|
||||
{"id": "st1","plugin": "system", "x": 0, "y": 2, "w": 2, "h": 2},
|
||||
{"id": "sp1","plugin": "spotify", "x": 2, "y": 2, "w": 1, "h": 1},
|
||||
{"id": "sv1","plugin": "strava", "x": 3, "y": 2, "w": 1, "h": 1},
|
||||
]},
|
||||
"plugin_configs": {},
|
||||
}
|
||||
|
||||
r = self.client.post("/api/layout/add", data={"plugin": "hello"})
|
||||
self.assertEqual(r.status_code, 200, r.get_data(as_text=True))
|
||||
j = r.get_json()
|
||||
self.assertTrue(j["ok"], j)
|
||||
items = j["items"]
|
||||
by_id = {it["id"]: it for it in items}
|
||||
# Existierende UNVERÄNDERT
|
||||
self.assertEqual((by_id["c1"]["x"], by_id["c1"]["y"]), (0, 0), "clock moved!")
|
||||
self.assertEqual((by_id["w1"]["x"], by_id["w1"]["y"]), (2, 0), "weather moved!")
|
||||
self.assertEqual((by_id["st1"]["x"], by_id["st1"]["y"]), (0, 2), "system moved!")
|
||||
self.assertEqual((by_id["sp1"]["x"], by_id["sp1"]["y"]), (2, 2), "spotify moved!")
|
||||
self.assertEqual((by_id["sv1"]["x"], by_id["sv1"]["y"]), (3, 2), "strava moved!")
|
||||
# Neues hello auf (2,3) — erste scan-line freie Zelle
|
||||
new_items = [it for it in items if it["plugin"] == "hello" and it["id"] not in ("c1","w1","st1","sp1","sv1")]
|
||||
self.assertEqual(len(new_items), 1, f"expected exactly 1 new hello, got {new_items}")
|
||||
new = new_items[0]
|
||||
self.assertEqual((new["x"], new["y"]), (2, 3),
|
||||
f"expected new hello at (2,3), got ({new['x']},{new['y']}); full: {items}")
|
||||
|
||||
def test_add_to_full_grid_returns_409(self):
|
||||
"""Wenn kein Platz: 409, keine bestehenden Items verändert."""
|
||||
self.fake.load_config.return_value["layout"]["items"] = [
|
||||
{"id": f"f{i}", "plugin": "x", "x": (i % 4), "y": (i // 4),
|
||||
"w": 1, "h": 1} for i in range(16)
|
||||
]
|
||||
before = [(it["id"], it["x"], it["y"]) for it in
|
||||
self.fake.load_config.return_value["layout"]["items"]]
|
||||
r = self.client.post("/api/layout/add", data={"plugin": "hello"})
|
||||
self.assertEqual(r.status_code, 409, r.get_data(as_text=True))
|
||||
j = r.get_json()
|
||||
self.assertFalse(j["ok"])
|
||||
self.assertIn("Auto-Pack", j.get("error", ""))
|
||||
# In-memory config unverändert
|
||||
after = [(it["id"], it["x"], it["y"]) for it in
|
||||
self.fake.load_config.return_value["layout"]["items"]]
|
||||
self.assertEqual(before, after, "full-grid add still mutated state")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Tests für layout.first_fit (BUG-04).
|
||||
|
||||
Behauptung: Add-Item darf bestehende Items NICHT verschieben.
|
||||
- Leeres Layout → Add 2×2 hello → Position (0,0), kein Pack
|
||||
- Layout mit Lücke → Add 1×1 hello → landet in erster Lücke, andere bleiben
|
||||
- Layout voll → Add → None, kein anderes Item verändert
|
||||
"""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import unittest
|
||||
from layout import Item, first_fit, GRID_COLS, GRID_ROWS
|
||||
|
||||
|
||||
class TestFirstFit(unittest.TestCase):
|
||||
def test_first_fit_empty_grid(self):
|
||||
"""Auf leerem Grid wird Item bei (0,0) platziert — kein Pack nötig."""
|
||||
new_item = Item("new1", "hello", 0, 0, 1, 1)
|
||||
placed = first_fit(new_item, [])
|
||||
self.assertIsNotNone(placed)
|
||||
self.assertEqual((placed.x, placed.y), (0, 0))
|
||||
|
||||
def test_first_fit_does_not_move_existing(self):
|
||||
"""Items mit Lücke: hello wird in Lücke gesetzt, andere bleiben."""
|
||||
existing = [
|
||||
Item("a", "clock", 0, 0, 2, 2),
|
||||
Item("b", "weather", 2, 0, 2, 2),
|
||||
Item("c", "system", 0, 2, 2, 2),
|
||||
Item("d", "spotify", 2, 2, 2, 1),
|
||||
]
|
||||
before = {(it.id, it.x, it.y) for it in existing}
|
||||
new_item = Item("new", "hello", 0, 0, 1, 1)
|
||||
placed = first_fit(new_item, existing)
|
||||
self.assertIsNotNone(placed)
|
||||
after = {(it.id, it.x, it.y) for it in existing}
|
||||
self.assertEqual(before, after, f"EXISTING MOVED! before={before} after={after}")
|
||||
self.assertEqual((placed.x, placed.y), (2, 3))
|
||||
|
||||
def test_first_fit_full_grid_returns_none(self):
|
||||
"""Wenn kein Platz: None zurück, keine Mutation."""
|
||||
existing = [
|
||||
Item("a", "x", 0, 0, 2, 2),
|
||||
Item("b", "y", 2, 0, 2, 2),
|
||||
Item("c", "z", 0, 2, 2, 2),
|
||||
Item("d", "w", 2, 2, 2, 2),
|
||||
]
|
||||
before = {(it.id, it.x, it.y, it.w, it.h) for it in existing}
|
||||
new_item = Item("new", "hello", 0, 0, 1, 1)
|
||||
placed = first_fit(new_item, existing)
|
||||
self.assertIsNone(placed)
|
||||
after = {(it.id, it.x, it.y, it.w, it.h) for it in existing}
|
||||
self.assertEqual(before, after)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -1,74 +0,0 @@
|
||||
// BUG-05 Test: Speichern-Button entfernt, Save-Indicator hinzugefügt.
|
||||
//
|
||||
// Erwartung:
|
||||
// 1) KEIN <button onclick="saveLayout()"> im Layout-Toolbar mehr
|
||||
// 2) NEUER #saveIndicator im Toolbar mit data-state="idle"
|
||||
// 3) setSaveState() existiert und akzeptiert (state, label?)
|
||||
// 4) setSaveState setzt el.dataset.state und label-Text
|
||||
// 5) debouncedSave setzt Status auf "pending" BEVOR es speichert
|
||||
// 6) saveLayout setzt Status auf "saving" am Anfang, "idle" am Ende (success)
|
||||
// 7) saveLayout setzt Status auf "error" bei j.ok=false
|
||||
// 8) CSS: .save-indicator[data-state="..."] für idle/pending/saving/error
|
||||
|
||||
const fs = require('fs');
|
||||
const html = fs.readFileSync('templates/index.html', 'utf8');
|
||||
const js = html.match(/<script>\s*\n([\s\S]*?)<\/script>/)[1];
|
||||
const css = html.match(/<style>([\s\S]*?)<\/style>/g).join('\n');
|
||||
|
||||
function check(name, fn) {
|
||||
const r = fn();
|
||||
console.log((r ? '✓' : '✗') + ' ' + name);
|
||||
if (!r) process.exitCode = 1;
|
||||
}
|
||||
|
||||
// 1) Kein Speichern-Button mehr
|
||||
check('Speichern-Button entfernt', () => {
|
||||
// Suchen wir den toolbar-Bereich
|
||||
const toolbarMatch = html.match(/<div class="layout-toolbar">([\s\S]*?)<\/div>\s*<div id="gridPreview"/);
|
||||
if (!toolbarMatch) return false; // Struktur geändert?
|
||||
return !/onclick="saveLayout\(\)"/.test(toolbarMatch[1]);
|
||||
});
|
||||
|
||||
// 2) Save-Indicator
|
||||
check('Save-Indicator #saveIndicator vorhanden', () =>
|
||||
/id="saveIndicator"/.test(html) && /data-state="idle"/.test(html));
|
||||
|
||||
// 3) setSaveState existiert
|
||||
check('setSaveState() Funktion definiert', () =>
|
||||
/function setSaveState\(/.test(js));
|
||||
|
||||
// 4) setSaveState setzt state und label
|
||||
check('setSaveState setzt dataset.state', () =>
|
||||
/setSaveState[\s\S]{0,300}dataset\.state/.test(js));
|
||||
|
||||
// 5) debouncedSave setzt pending
|
||||
check('debouncedSave setzt pending-Status', () =>
|
||||
/function debouncedSave\(\)\s*\{[^}]*setSaveState\(['"]pending['"]/.test(js));
|
||||
|
||||
// 6) saveLayout setzt saving→idle
|
||||
check('saveLayout setzt saving am Anfang', () =>
|
||||
/async function saveLayout\(\)[\s\S]{0,300}setSaveState\(['"]saving['"]/.test(js));
|
||||
|
||||
check('saveLayout setzt idle bei Erfolg', () =>
|
||||
/saveLayout[\s\S]{0,800}setSaveState\(['"]idle['"]/.test(js));
|
||||
|
||||
// 7) saveLayout setzt error bei Fehler
|
||||
check('saveLayout setzt error bei j.ok=false', () =>
|
||||
/!j\.ok[\s\S]{0,300}setSaveState\(['"]error['"]/.test(js));
|
||||
|
||||
// 8) CSS für alle States
|
||||
const cssStates = ['idle', 'pending', 'saving', 'error'].map(s =>
|
||||
new RegExp(`\\.save-indicator\\[data-state="${s}"\\]`));
|
||||
check('CSS für alle 4 Indicator-States', () =>
|
||||
cssStates.every(re => re.test(css)));
|
||||
|
||||
// 9) delete success-toast raus (Indicator reicht)
|
||||
check('Kein success-toast in saveLayout mehr', () =>
|
||||
!/function saveLayout[\s\S]{0,1000}toast\([^)]*['"]success['"][^)]*['"]Layout gespeichert['"]/.test(js));
|
||||
|
||||
// 10) margin-left:auto für rechtsbündige Position
|
||||
check('Save-Indicator rechtsbündig (margin-left:auto)', () =>
|
||||
/\.save-indicator\s*\{[^}]*margin-left:\s*auto/.test(css));
|
||||
|
||||
console.log('\n========');
|
||||
process.exit(process.exitCode || 0);
|
||||
@@ -0,0 +1,56 @@
|
||||
// Test für BUG-03: Span-Geometrie im Initial-Render.
|
||||
//
|
||||
// Erwartung nach Fix:
|
||||
// 1) renderGrid setzt grid-column/grid-row am Item direkt (per JS).
|
||||
// 2) Item wird in den Container gehängt (cont.appendChild), nicht in originCell.
|
||||
// 3) Origin-Cell bekommt nur die "occupied"-Klasse.
|
||||
// 4) 2x2-Item hat style.gridColumn === '<x+1> / span 2' und gridRow === '<y+1> / span 2'.
|
||||
|
||||
const fs = require('fs');
|
||||
const html = fs.readFileSync('templates/index.html', 'utf8');
|
||||
const js = html.match(/<script>\s*\n([\s\S]*?)<\/script>/)[1];
|
||||
|
||||
function check(name, fn) {
|
||||
const r = fn();
|
||||
console.log((r ? '✓' : '✗') + ' ' + name);
|
||||
if (!r) process.exitCode = 1;
|
||||
}
|
||||
|
||||
// 1) renderGrid setzt style.gridColumn/gridRow am Item
|
||||
check('gridColumn wird per JS gesetzt', () =>
|
||||
/\.style\.gridColumn\s*=/.test(js));
|
||||
|
||||
check('gridRow wird per JS gesetzt', () =>
|
||||
/\.style\.gridRow\s*=/.test(js));
|
||||
|
||||
// 2) Item wird in cont.appendChild gehängt, NICHT in originCell.appendChild
|
||||
check('Item wird in Container (cont) gehängt', () =>
|
||||
/cont\.appendChild\(div\)/.test(js));
|
||||
|
||||
check('Item wird NICHT mehr in Origin-Cell gehängt', () =>
|
||||
!/originCell\.appendChild\(div\)/.test(js));
|
||||
|
||||
// 3) Origin-Cell bekommt nur occupied-Klasse
|
||||
check('Origin-Cell bekommt "occupied" Klasse', () =>
|
||||
/originCell\.classList\.add\(['"]occupied['"]\)/.test(js));
|
||||
|
||||
// 4) Format: `${it.x + 1} / span ${it.w}` (CSS-Grid-Notation)
|
||||
check('gridColumn Format: <x+1> / span <w>', () =>
|
||||
/it\.x\s*\+\s*1[^`]*\$\{it\.w\}/.test(js) || /\$\{it\.x\s*\+\s*1\}[^`]*span[^`]*\$\{it\.w\}/.test(js));
|
||||
|
||||
// 5) Kein 100%/100% Trick auf Items (das war der Bug, der Span verhindert hat)
|
||||
check('Keine "width: 100%" mehr im Item-CSS-Block', () => {
|
||||
const css = html.match(/<style>([\s\S]*?)<\/style>/g).join('\n');
|
||||
// .grid-item soll nicht width:100% haben
|
||||
const itemCssMatch = /\.grid-item\s*\{([^}]*)\}/.exec(css);
|
||||
if (!itemCssMatch) return true; // falls keine Regel
|
||||
const body = itemCssMatch[1];
|
||||
return !/width:\s*100%/.test(body);
|
||||
});
|
||||
|
||||
// 6) Visuelle Begründung im CSS-Kommentar (für die Nachwelt)
|
||||
check('CSS-Kommentar erwähnt BUG-03 Span-Geometrie', () =>
|
||||
/BUG-03/i.test(html));
|
||||
|
||||
console.log('\n========');
|
||||
process.exit(process.exitCode || 0);
|
||||
Reference in New Issue
Block a user