scopedvr-map-editor/tests/test_gui_smoke.gd

318 lines
15 KiB
GDScript
Raw 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): GUI-Smoke — Karte t_d3b93793.
## Ausführen: godot --headless --path . --script res://tests/test_gui_smoke.gd
## Exit 0 = alles grün.
##
## ANDERS als die übrigen Tests instanziert dieser Test die ECHTE Main-UI
## (scenes/main.tscn → src/ui/main.gd) und deckt damit Runtime-UI-Pfade ab,
## die die logik-only-Suite nie sieht (GUI-Start-Befund 15.10.: Terrain-Preset-
## Crash beim Panel-Bau + `command_pressed`-Spam bei jeder Taste — beides
## headless unsichtbar, weil das UI nie instanziert wurde).
##
## Was hier geprüft wird:
## 1. Panel-Bau überlebt _ready (kein abgebrochener _build_right_panel).
## 2. Terrain-Größen-Presets sind initial korrekt belegt (Mittel = 12 m)
## und klickbar (Klick stellt Radius-Slider + Label um).
## 3. Keyboard-Shortcut-Pfad (_unhandled_key_input) läuft ohne
## Property-Fehler und setzt Strg+Z wirklich in Undo um.
var failures := 0
var checks := 0
var main: Control
func _initialize() -> void:
_run_all.call_deferred()
func _run_all() -> void:
var scene: PackedScene = load("res://scenes/main.tscn")
_check(scene != null, "scenes/main.tscn laedt")
if scene == null:
_finish()
return
main = scene.instantiate()
root.add_child(main)
# Zwei Frames: _ready + Layout laufen lassen (auch deferred UI-Arbeit).
await process_frame
await process_frame
_test_ui_built()
_test_terrain_presets()
_test_shortcut_undo()
_test_terrain_hotkeys()
_test_shortcuts_karte3()
_finish()
func _finish() -> void:
if failures == 0:
print("ALL OK: %d checks" % checks)
quit(0)
else:
print("FAILED: %d/%d checks" % [failures, checks])
quit(1)
func _check(cond: bool, label: String) -> void:
checks += 1
if cond:
print(" ok: %s" % label)
else:
failures += 1
print(" FAIL: %s" % label)
## Abnahme 1: Der komplette UI-Bau (_ready → _build_ui → _build_right_panel)
## läuft durch — genau dort crashte der Terrain-Preset-Handler (main.gd:659).
func _test_ui_built() -> void:
print("Testgruppe: UI-Bau überlebt _ready")
_check(main != null, "Main-Instanz existiert")
_check(main.terrain_size_btn_small != null, "Preset-Button Klein gebaut")
_check(main.terrain_size_btn_medium != null, "Preset-Button Mittel gebaut")
_check(main.terrain_size_btn_large != null, "Preset-Button Groß gebaut")
_check(main.terrain_radius_slider != null, "Radius-Slider gebaut")
_check(main.undo_btn != null and main.redo_btn != null,
"Undo/Redo-Buttons gebaut (Panel-Bau lief komplett durch)")
## Abnahme 2a: Initialzustand ohne Nebeneffekt-Callback (set_pressed_no_signal):
## Mittel aktiv, Slider passend 12 m.
func _test_terrain_presets() -> void:
print("Testgruppe: Terrain-Größen-Presets")
_check(main.terrain_size_btn_medium.button_pressed,
"Mittel ist initial aktiv")
_check(is_equal_approx(float(main.terrain_radius_slider.value), 12.0),
"Slider startet bei 12 m (passend zum aktiven Preset)")
# Klick auf Groß: button_pressed=true feuert toggled → Handler muss
# jetzt (mit existierendem Slider) Radius + Label setzen.
main.terrain_size_btn_large.button_pressed = true
_check(is_equal_approx(float(main.terrain_radius_slider.value), 32.0),
"Klick auf Groß setzt Slider auf 32 m")
_check(str(main.terrain_radius_label.text).contains("32"),
"Pinsel-Label zeigt 32 m")
_check(main.terrain_size_btn_medium.button_pressed == false,
"ButtonGroup: Mittel beim Klick auf Groß deaktiviert")
# Klick auf Klein (Position-Klick simuliert echten User-Klick).
main.terrain_size_btn_small.button_pressed = true
_check(is_equal_approx(float(main.terrain_radius_slider.value), 4.0),
"Klick auf Klein setzt Slider auf 4 m")
func _press_key(keycode: Key, ctrl: bool, shift := false) -> InputEventKey:
var ev := InputEventKey.new()
ev.keycode = keycode
ev.physical_keycode = keycode
ev.pressed = true
ev.ctrl_pressed = ctrl
ev.shift_pressed = shift
return ev
## Abnahme 2b + 3: Der Shortcut-Pfad muss ohne Property-Fehler durchlaufen
## (vorher crashte `event.command_pressed` bei JEDER Taste) und Strg+Z
## tatsächlich in ein Undo übersetzen.
func _test_shortcut_undo() -> void:
print("Testgruppe: Keyboard-Shortcuts (Strg+Z)")
# Echte History-Aktion in die laufende Editor-History legen.
var before: Dictionary = main.history.snapshot(["objects"])
(main.doc["objects"] as Array).append(
{"id": "test_objekt", "pos": [10.0, 0.0, 10.0], "scale": 1.0,
"rot_y_deg": 0.0})
main.history.commit("Test: Objekt platzieren", before,
main.history.snapshot(["objects"]))
_check((main.doc["objects"] as Array).size() == 1, "Test-Objekt platziert")
# Karte 1 (t_c46d31c2) hat F belegt: F zentriert die Auswahl — ohne
# Auswahl gibt es den freundlichen Hinweis (kein Crash, kein Wirken).
main._unhandled_key_input(_press_key(KEY_F, false))
_check(str(main.status_label.text).contains("Nichts ausgewählt"),
"F ohne Auswahl: freundlicher Hinweis, läuft sauber durch")
main._unhandled_key_input(_press_key(KEY_Z, true))
_check((main.doc["objects"] as Array).is_empty(), "Strg+Z hat das Objekt entfernt (Undo wirkt)")
_check(str(main.status_label.text).contains("Rückgängig gemacht"),
"Statusleiste meldet das Undo")
main._unhandled_key_input(_press_key(KEY_Y, true))
_check((main.doc["objects"] as Array).size() == 1, "Strg+Y stellt wieder her (Redo wirkt)")
# Leere History + Strg+Z: freundliche Meldung statt Crash.
# Erst das Redo-Ergebnis wieder weg-undoen, DANN ist die History leer.
main._unhandled_key_input(_press_key(KEY_Z, true))
_check((main.doc["objects"] as Array).is_empty(), "Strg+Z nach Redo: History leer")
main._unhandled_key_input(_press_key(KEY_Z, true))
_check(str(main.status_label.text).contains("Nichts zum Rückgängigmachen"),
"Strg+Z bei leerer History: Statusmeldung, kein Absturz")
## Karte 2/3 (t_55257792): Terrain-Ziffern 1–4 + [ / ] am ECHTEN main —
## Buttons/Slider/Label müssen mitwandern, Ziffern außerhalb des Terrain-
## Werkzeugs dürfen NICHTS tun (Konfliktfreiheit).
func _test_terrain_hotkeys() -> void:
print("Testgruppe: Terrain-Pinsel per Tastatur (1–4, [ und ])")
main._set_tool_mode("terrain")
_check(str(main.get("tool_mode")) == "terrain", "Terrain-Modus aktiv")
# Ziffer 2 = Senken: Modus-Button + Pinsel-Modus wandern mit.
main._unhandled_key_input(_press_key(KEY_2, false))
_check(main.terrain_mode_lower.button_pressed, "Ziffer 2: Button „Senken“ aktiv")
_check(int(main.terrain_tool.get("brush_mode")) == TerrainField.Mode.LOWER,
"Ziffer 2: Pinsel-Modus = Senken")
# Ziffer 4 = Malen: Palette sichtbar, Modus wirklich PAINT (alter
# set_brush_mode-Clamp hätte das still zu Glätten degradiert).
main._unhandled_key_input(_press_key(KEY_4, false))
_check(int(main.terrain_tool.get("brush_mode")) == TerrainField.Mode.PAINT,
"Ziffer 4: Pinsel-Modus = Malen (Clamp-Fix)")
_check(main.terrain_palette.visible, "Ziffer 4: Material-Palette sichtbar")
# Ziffer 1 zurück: Palette verschwindet wieder.
main._unhandled_key_input(_press_key(KEY_1, false))
_check(int(main.terrain_tool.get("brush_mode")) == TerrainField.Mode.RAISE,
"Ziffer 1: Pinsel-Modus = Heben")
_check(not main.terrain_palette.visible, "Ziffer 1: Material-Palette weg")
# [ / ] : Preset-Zyklus Klein(4)/Mittel(12)/Groß(32), Slider + Label mit.
# Slider steht nach dem Klein-Klick oben noch auf 4 m — die Kette startet
# also von Klein: ] ] ] = 12 → 32 → wrap 4, [ = 32, [ [ = 12 → 4.
main._unhandled_key_input(_press_key(KEY_BRACKETRIGHT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 12.0),
"] ab 4 m: Slider auf 12 m (Mittel)")
_check(str(main.terrain_radius_label.text).contains("12"), "]: Label zeigt 12 m")
_check(main.terrain_size_btn_medium.button_pressed, "]: Preset-Button Mittel aktiv")
main._unhandled_key_input(_press_key(KEY_BRACKETRIGHT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 32.0),
"] ab 12 m: Slider auf 32 m (Groß)")
_check(str(main.terrain_radius_label.text).contains("32"), "]: Label zeigt 32 m")
_check(main.terrain_size_btn_large.button_pressed, "]: Preset-Button Groß aktiv")
main._unhandled_key_input(_press_key(KEY_BRACKETRIGHT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 4.0),
"] ab 32 m: Zyklus wrappt auf Klein (4 m)")
main._unhandled_key_input(_press_key(KEY_BRACKETLEFT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 32.0),
"[ ab 4 m: wrappt zurück auf Groß (32 m)")
main._unhandled_key_input(_press_key(KEY_BRACKETLEFT, false))
main._unhandled_key_input(_press_key(KEY_BRACKETLEFT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), 4.0),
"[ zweimal ab 32 m: 12 → 4 (Klein)")
# Konfliktfreiheit: außerhalb des Terrain-Werkzeugs tun Ziffern/[ NICHTS.
main._set_tool_mode("objects")
var slider_vorher: float = main.terrain_radius_slider.value
main._unhandled_key_input(_press_key(KEY_2, false))
main._unhandled_key_input(_press_key(KEY_BRACKETRIGHT, false))
_check(is_equal_approx(float(main.terrain_radius_slider.value), slider_vorher),
"Ziffern/[ im Objekt-Modus: Pinsel bleibt unangetastet")
_check(int(main.terrain_tool.get("brush_mode")) == TerrainField.Mode.RAISE,
"Ziffern im Objekt-Modus: Pinsel-Modus bleibt unangetastet")
## Karte 3/3 (t_11b5c25b): F1–F4 Werkzeugwechsel, F5 Test, H-Hilfe,
## Strg+O/N/S + Dirty-Dialog am ECHTEN main (Dialoge als --mock: sichtbar
## prüfen statt bedienen — headless gibt es keine echte Maus).
func _test_shortcuts_karte3() -> void:
print("Testgruppe: Karte 3/3 — F-Tasten, Hilfe, Datei, Dirty-Dialog")
# --- F1–F4: Werkzeugwechsel (Slys Weiche: F-Tasten statt Ziffern) --------
main._set_tool_mode("objects")
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "terrain", "F1: Terrain-Werkzeug aktiv")
_check(str(main.tool_label.text).contains("Terrain"), "Statuszeile: Werkzeug-Label zeigt Terrain")
main._unhandled_key_input(_press_key(KEY_F2, false))
_check(str(main.get("tool_mode")) == "objects", "F2: zurück zum Objekt-Werkzeug")
main._unhandled_key_input(_press_key(KEY_F3, false))
_check(str(main.get("tool_mode")) == "bauen", "F3: Bauen-Werkzeug aktiv")
_check(str(main.tool_label.text).contains("Bauen"), "Statuszeile: Werkzeug-Label zeigt Bauen")
main._unhandled_key_input(_press_key(KEY_F4, false))
_check(str(main.get("tool_mode")) == "spawns", "F4: Spawn-Werkzeug aktiv")
main._unhandled_key_input(_press_key(KEY_F2, false))
_check(str(main.get("tool_mode")) == "objects", "F2 aus dem Spawn-Werkzeug: sauber zurück")
# --- Fokus-Regel: Textfeld fokussiert → F-Tasten wirken NICHT ------------
main.prop_name.grab_focus()
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "objects",
"F1 bei fokussiertem Textfeld: Werkzeug bleibt (Tippen geht vor)")
main.prop_name.release_focus()
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "terrain", "F1 nach Release-Focus: wirkt wieder")
main._set_tool_mode("objects")
# --- Dialog-Regel: offener Dialog → F-Tasten wirken NICHT ----------------
main.error_dialog.popup_centered()
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "objects",
"F1 bei offenem Dialog: Werkzeug bleibt")
main.error_dialog.hide()
# --- H: Kurzhilfe-Overlay an/aus (Esc schließt ebenfalls) ----------------
main._unhandled_key_input(_press_key(KEY_H, false))
_check(main.help_overlay.visible, "H: Kurzhilfe-Overlay sichtbar")
var help_txt: String = main.help_overlay_text.text
_check(help_txt.contains("Strg+Z") and help_txt.contains("F1") and help_txt.contains("Terrain"),
"Overlay enthält die Tastenliste aus docs/tasten.md")
_check(str(main.status_label.text).contains("Kurzhilfe an"), "Statuszeile meldet Overlay an")
main._unhandled_key_input(_press_key(KEY_ESCAPE, false))
_check(not main.help_overlay.visible, "Esc schließt das Overlay")
_check(str(main.get("tool_mode")) == "objects", "Esc am Overlay: Objekt-Werkzeug unangetastet")
main._unhandled_key_input(_press_key(KEY_H, false))
main._unhandled_key_input(_press_key(KEY_H, false))
_check(not main.help_overlay.visible, "H zweimal: Overlay wieder zu (Toggle)")
# --- F5: Karte testen (echter Flow existiert — hier läuft er in den
# „Client nicht gefunden“-Zweig, Hauptsache die Route ist verdrahtet) -----
main._unhandled_key_input(_press_key(KEY_F5, false))
_check(str(main.status_label.text).contains("Karte testen"),
"F5: Karte-testen-Flow angesprungen (Statuszeile meldet)")
_check(main.error_dialog.visible, "F5: Fehler-Dialog im nicht-gefunden-Zweig geöffnet")
main.error_dialog.hide()
# --- Dirty-Schutz: Strg+N bei ungespeicherten Änderungen FRAGT ZUERST ----
main.dirty = true
main._unhandled_key_input(_press_key(KEY_N, true))
_check(main.dirty_dialog.visible, "Strg+N bei dirty: Bestätigungs-Dialog ZUERST")
_check(not main.new_dialog.visible, "Strg+N bei dirty: Neue-Map-Dialog wartet hinterher")
_check(str(main.dirty_dialog.dialog_text).contains("ungespeicherte Änderungen"),
"Dirty-Dialog: deutsche Klartext-Meldung")
# „Verwerfen“ (confirmed) → jetzt erst geht der Neue-Map-Dialog auf.
main.dirty_dialog.confirmed.emit()
_check(main.new_dialog.visible, "Verwerfen bestätigt: Neue-Map-Dialog öffnet jetzt")
main.new_dialog.hide()
main.dirty = false
# --- Strg+O ohne dirty: Öffnen-Dialog DIREKT -----------------------------
main._unhandled_key_input(_press_key(KEY_O, true))
_check(main.open_dialog.visible, "Strg+N/Strg+O ohne dirty: Öffnen-Dialog direkt")
main.open_dialog.hide()
# --- Strg+S: speichert an bekannten Pfad, dirty wird aufgeräumt ----------
main.current_path = "/tmp/svmap_karte3_test.svmap"
main.dirty = true
main._unhandled_key_input(_press_key(KEY_S, true))
_check(FileAccess.file_exists("/tmp/svmap_karte3_test.svmap"), "Strg+S: .svmap geschrieben")
_check(not main.dirty, "Strg+S: dirty zurückgesetzt")
_check(str(main.status_label.text).contains("gespeichert"), "Strg+S: Statuszeile meldet Speichern")
# --- Strg+Umschalt+S: Speichern-unter-Dialog ------------------------------
main._unhandled_key_input(_press_key(KEY_S, true, true))
_check(main.save_dialog.visible, "Strg+Umschalt+S: Speichern-unter-Dialog")
main.save_dialog.hide()
# --- Strg+Umschalt+Z: der zweite Redo-Weg --------------------------------
var before: Dictionary = main.history.snapshot(["objects"])
(main.doc["objects"] as Array).append(
{"id": "test_redo_objekt", "pos": [4.0, 0.0, 4.0], "scale": 1.0,
"rot_y_deg": 0.0})
main.history.commit("Test: Redo-Objekt", before, main.history.snapshot(["objects"]))
main._unhandled_key_input(_press_key(KEY_Z, true))
_check((main.doc["objects"] as Array).size() == 0, "Strg+Z: Objekt weg (Undo)")
main._unhandled_key_input(_press_key(KEY_Z, true, true))
_check((main.doc["objects"] as Array).size() == 1, "Strg+Umschalt+Z: Redo-Weg 2 stellt her")
main._unhandled_key_input(_press_key(KEY_Z, true))
main._unhandled_key_input(_press_key(KEY_Y, true))
_check((main.doc["objects"] as Array).size() == 1, "Strg+Y: Redo-Weg 1 stellt auch her")
# Aufräumen: Undo, damit spätere Gruppen (falls Testreihenfolge sich ändert)
# nicht von diesem Objekt ausgehen.
main._unhandled_key_input(_press_key(KEY_Z, true))