Beim Hinzufügen eines neuen Widgets via /api/layout/add rief der Server
layout_mod.pack() auf alle Items auf — pack() sortiert nach Fläche
absteigend und platziert scan-line greedy. Ein 1x1 hello konnte dabei
einen 2x1 spotify aus seiner Position drängen, weil die Sort-Reihenfolge
sich ändert sobald ein neues Item im Mix ist.
Reproduktion vor dem Fix:
Bestehende config: c1@(0,0) w1@(2,0) st1@(0,2) sp1@(2,2) sv1@(2,3)
Add hello (1x1) → w1 wurde nach (0,2) verschoben, hello landete bei (2,0).
Siehe RED-Test in tests/test_add_route_no_repack.py.
Fix:
- layout.py: neue Funktion first_fit(item, others) — platziert ein Item in
der ersten freien scan-line-Zelle OHNE andere Items zu verändern.
- admin.py /api/layout/add nutzt first_fit. Wenn kein Platz: HTTP 409 mit
{ok:false, error:'kein Platz für WxH-Item', hint:'use_auto_pack'}.
- templates/index.html addItem(): 409 als 'warn'-Toast mit Auto-Pack-Hinweis.
Akzeptanzkriterien (BUG-04):
- bestehende Items bleiben bei Add unverändert an (x,y)
- neues Item landet in erster freier Zelle
- voller Grid → 409, kein bestehendes Item verschoben
- Auto-Pack bleibt als expliziter User-Wunsch erhalten (BUG-06)
Tests:
- tests/test_layout_firstfit.py: 3 unit tests (empty, gappy, full grid)
- tests/test_add_route_no_repack.py: 2 integration tests gegen /api/layout/add
mit gemocktem dashboard-Modul + Flask test_client
Closes #6
57 lines
2.2 KiB
Python
57 lines
2.2 KiB
Python
"""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)
|