scopedvr-map-editor/tests/test_ui_scale.gd
slyphax 3f0d0f3f33 Map-Editor: UI aufgeräumt, Auswahl-Sync, Drag&Drop, Bauen (Wände bündig, Geschosshöhe, große Dächer)
- Tabs Auswahl/Prüfen/Test-Start/Einstellungen/Symmetrie weg; Spawn-Zonen, Test-Start, Symmetrie im Map-Tab; Einstellungen-Fenster (Gitter, Höhenlinien, Symmetrie-Modus, UI-Größe, Raster-Snap, Gizmo-Schritt, Karte prüfen)
- Bauen-Tab nur Bauteile; Objekt-Werkzeuge entfernt
- Klick in der Map-Inhalt-Liste wählt in der Szene aus (SELECT_MULTI feuert nur multi_selected)
- Objekt-Kachel per Ziehen in die Szene setzen
- Wände bündig in der Zelle, gleiche Geschosshöhe mit/ohne Decke, Dächer/Giebelwände 4 m und 6 m

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:32:32 +01:00

218 lines
9.2 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends SceneTree
## Editor-Test (headless): UI-Skalierung — Karte t_297221b5.
## Ausführen: godot --headless --path . -s res://tests/test_ui_scale.gd
## Exit 0 = alles grün.
##
## Geprüft wird:
## 1. Stufen-API: 3 Stufen (Klein 1.0 / Mittel 1.25 / Groß 1.5), Labels/Keys.
## 2. Rechenhelfer: px/f/v2/font_size multiplizieren korrekt (inkl. Runden).
## 3. Config-Roundtrip: Stufe übersteht reload() (= Neustart) UND der
## [library]-Abschnitt (Klapp-Zustand, Karte t_59fc0f21) bleibt erhalten.
## 4. ECHTE Main-UI (scenes/main.tscn): Theme-Schrift, Knopf-/Kachel-/
## Thumbnail-Mindestmaße skalieren bei jeder Stufe — sofort nach dem
## Rebuild (derselbe Pfad wie der Menü-Klick „UI-Größe“).
## 5. Rebuild erhält den Map-Zustand (doc-Objekt bleibt, Map-Eigenschaften
## werden wieder eingefüllt, Objektzahl unverändert).
## 6. Ungültige Menü-Indices und headless-Fenster-Aufrufe sind harmlos.
##
## Die Suite ist hermetisch: SCOPEDVR_UI_CFG zeigt auf eine Temp-Config,
## damit weder die Nutzer-Config noch andere Suite-Läufe gestört werden.
const UIScale := preload("res://src/ui/ui_scale.gd")
var failures := 0
var checks := 0
var main: Control
func _initialize() -> void:
# Hermetische Config (Temp-Pfad im user://-Bereich):
OS.set_environment("SCOPEDVR_UI_CFG", "user://ui_scale_test.cfg")
_run_all.call_deferred()
func _check(cond: bool, label: String) -> void:
checks += 1
if cond:
print(" ok: %s" % label)
else:
failures += 1
print(" FAIL: %s" % label)
func _finish() -> void:
# Sauber hinterlassen: Test-Config löschen, Env-Umleitung aufheben.
var dir := DirAccess.open("user://")
if dir != null and dir.file_exists("ui_scale_test.cfg"):
dir.remove("ui_scale_test.cfg")
OS.set_environment("SCOPEDVR_UI_CFG", "")
print("CHECKS: %d FAILURES: %d" % [checks, failures])
print("UI_SCALE_TEST_OK" if failures == 0 else "UI_SCALE_TEST_FAIL")
quit(1 if failures > 0 else 0)
func _run_all() -> void:
UIScale.set_step_index(0)
_test_steps_api()
_test_math()
_test_cfg_roundtrip()
await _test_real_ui_scaling()
await _test_state_survives_rebuild()
_test_headless_safety()
_finish()
func _test_steps_api() -> void:
print("Testgruppe: Stufen-API")
_check(UIScale.step_count() == 3, "Genau 3 Stufen")
_check(UIScale.step_label(0) == "Klein" and UIScale.step_label(1) == "Mittel" and UIScale.step_label(2) == "Groß",
"Labels Klein/Mittel/Groß")
UIScale.set_step_index(0)
_check(is_equal_approx(UIScale.factor(), 1.0), "Klein = Faktor 1.0")
UIScale.set_step_index(1)
_check(is_equal_approx(UIScale.factor(), 1.25), "Mittel = Faktor 1.25")
UIScale.set_step_index(2)
_check(is_equal_approx(UIScale.factor(), 1.5), "Groß = Faktor 1.5")
_check(UIScale.step_index_from_key("mittel") == 1, "Key 'mittel' → Index 1")
_check(UIScale.step_index_from_key("quatsch") == 0, "Unbekannter Key → Klein (0)")
_check(UIScale.step_index_from_key("") == 0, "Leerer Key → Klein (0)")
func _test_math() -> void:
print("Testgruppe: Rechenhelfer")
UIScale.set_step_index(0)
_check(UIScale.px(40) == 40 and UIScale.px(16) == 16, "Klein: px unverändert (1.0)")
_check(UIScale.font_size() == 16, "Klein: Standardschrift 16 (wie Vorher)")
UIScale.set_step_index(1)
_check(UIScale.px(40) == 50, "Mittel: 40 px → 50 px")
_check(UIScale.px(126) == 158, "Mittel: 126 px → 158 px (gerundet)")
_check(UIScale.px(280) == 350, "Mittel: Panel 280 px → 350 px")
_check(is_equal_approx(UIScale.f(-320.0), -400.0), "Mittel: split_offset -320 → -400")
_check(UIScale.font_size() == 20, "Mittel: Standardschrift 20")
var v := UIScale.v2(140, 40)
_check(v.x == 175 and v.y == 50, "Mittel: v2(140,40) → (175,50)")
UIScale.set_step_index(2)
_check(UIScale.px(40) == 60 and UIScale.font_size() == 24, "Groß: 40 px → 60 px, Schrift 24")
_check(UIScale.px(96) == 144, "Groß: Thumbnail 96 → 144 (px-Rechnung)")
func _test_cfg_roundtrip() -> void:
print("Testgruppe: Config-Roundtrip (Neustart-Festigkeit + [library]-Schutz)")
UIScale.set_step_index(1) # schreibt [ui] groesse=mittel
# [library]-Abschnitt anlegen (wie Karte t_59fc0f21 es tut):
var cfg := ConfigFile.new()
cfg.load(UIScale.cfg_path())
cfg.set_value("library", "gebaeude", true)
cfg.save(UIScale.cfg_path())
# „Neustart“: Cache verwerfen, frisch aus der Datei lesen.
UIScale.reload()
_check(UIScale.step_index() == 1, "Stufe Mittel übersteht reload() (= Neustart)")
var cfg2 := ConfigFile.new()
cfg2.load(UIScale.cfg_path())
_check(bool(cfg2.get_value("library", "gebaeude", false)), "[library]-Abschnitt bleibt beim Schreiben erhalten")
_check(str(cfg2.get_value("ui", "groesse", "")) == "mittel", "[ui] groesse=mittel steht in der Datei")
func _test_real_ui_scaling() -> void:
print("Testgruppe: Echte Main-UI skaliert (Build + Rebuild)")
var scene: PackedScene = load("res://scenes/main.tscn")
_check(scene != null, "scenes/main.tscn lädt")
if scene == null:
return
UIScale.set_step_index(0)
main = scene.instantiate()
root.add_child(main)
await process_frame
await process_frame
_check(main != null and main.undo_btn != null, "Main-UI gebaut")
_check(int(main.theme.default_font_size) == 16, "Klein: Theme-Schrift 16")
_check(main.undo_btn.custom_minimum_size == Vector2(140, 40), "Klein: Undo-Knopf 140×40")
_check(main.redo_btn.custom_minimum_size == Vector2(150, 40), "Klein: Redo-Knopf 150×40")
_check(main.size_menu != null and main.size_menu is OptionButton, "UI-Größe-Auswahl im Einstellungen-Fenster")
_check(main.size_menu.item_count == 3, "Auswahl hat 3 Stufen")
_check(main.size_menu.selected == 0, "Auswahl steht auf aktueller Stufe (Klein)")
# Rebuild-Pfad (derselbe wie beim Menü-Klick) → Mittel:
UIScale.set_step_index(1)
main._rebuild_ui_for_scale()
await process_frame
await process_frame
_check(is_instance_valid(main.undo_btn), "Rebuild: Knöpfe neu gebaut")
_check(int(main.theme.default_font_size) == 20, "Mittel: Theme-Schrift 20 (sofort wirksam)")
_check(main.undo_btn.custom_minimum_size == Vector2(175, 50), "Mittel: Undo-Knopf 175×50")
_check(main.library_search.custom_minimum_size.y == 45, "Mittel: Suchfeld 36 → 45")
var tile := _find_tile_button(235)
_check(tile != null and tile.custom_minimum_size.y == 235, "Mittel: Bibliotheks-Kachel 188 → 235")
var thumb := _find_thumb_rect(160)
_check(thumb != null and thumb.custom_minimum_size.x == 160, "Mittel: Thumbnail 128 → 160")
_check(main.size_menu.selected == 1, "Auswahl wandert auf Mittel (neues Fenster)")
# Groß:
main._on_ui_size_selected(2)
await process_frame
await process_frame
_check(int(main.theme.default_font_size) == 24, "Groß: Theme-Schrift 24 (per Menü-Handler)")
_check(main.undo_btn.custom_minimum_size == Vector2(210, 60), "Groß: Undo-Knopf 210×60")
_check(main.status_label != null and is_instance_valid(main.status_label), "Statuszeile existiert (Groß)")
_check(main.content_tree != null and is_instance_valid(main.content_tree), "Inhalts-Baum existiert (Groß)")
# Zurück auf Klein: wieder exakt die Vorher-Maße.
main._on_ui_size_selected(0)
await process_frame
await process_frame
_check(main.size_menu.selected == 0, "Auswahl wieder auf Klein")
_check(int(main.theme.default_font_size) == 16, "Zurück auf Klein: Schrift wieder 16")
_check(main.undo_btn.custom_minimum_size == Vector2(140, 40), "Zurück auf Klein: Undo-Knopf 140×40")
func _find_tile_button(y: int) -> Button:
# Bibliotheks-Kacheln: Buttons mit custom_minimum_size.y == Stufenhöhe
# unterhalb der Haupt-VBox suchen — der Rebuild baut sie jedes Mal neu.
var stack: Array = [main]
while not stack.is_empty():
var node: Node = stack.pop_back()
if node is Button and node.custom_minimum_size.y == y and node.custom_minimum_size.x == 0:
return node
for c in node.get_children():
stack.push_back(c)
return null
func _find_thumb_rect(x: int) -> TextureRect:
var stack: Array = [main]
while not stack.is_empty():
var node: Node = stack.pop_back()
if node is TextureRect and node.custom_minimum_size.x == x:
return node
for c in node.get_children():
stack.push_back(c)
return null
func _test_state_survives_rebuild() -> void:
print("Testgruppe: Rebuild erhält Map-Zustand")
if main == null:
return
var count_before := SvmapDocument.object_count(main.doc)
main.doc["info"]["name"] = "Skalierungs-Test"
main._rebuild_ui_for_scale()
await process_frame
await process_frame
_check(SvmapDocument.object_count(main.doc) == count_before, "Map-Dokument unverändert (Objektzahl)")
_check(str(main.prop_name.text) == "Skalierungs-Test", "Map-Eigenschaften nach Rebuild wieder eingefüllt")
_check(main.doc == main.history.doc, "Undo-History zeigt weiter auf dasselbe doc")
_check(main.camera != null and is_instance_valid(main.camera), "3D-Kamera nach Rebuild vorhanden")
_check(main.view_builder != null and is_instance_valid(main.view_builder), "Map-Ansicht nach Rebuild vorhanden")
func _test_headless_safety() -> void:
print("Testgruppe: Headless-Sicherheit")
# Headless: Fenster-Aufrufe dürfen nicht krachen (Guards springen an).
main._apply_window_state()
main._save_window_state()
_check(true, "Fenster-Aufrufe headless ohne Crash")
# Ungültige Menü-Indices harmlos:
main._on_ui_size_selected(-1)
main._on_ui_size_selected(99)
_check(true, "Ungültige Menü-Indices ohne Crash")
_check(UIScale.step_index() == 0, "Stufe bleibt Klein nach ungültigen Aufrufen")