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() _test_contours_tool_coupling() _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")) == "objects" and (main.ui_right_panel as TabContainer).current_tab == main.bauen_mod.get_index(), "F3: Bauen-Tab geöffnet") 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 (Karte t_1cdc1a72): startet den Lauf-Modus im # Builder selbst — kein Client, kein Fehler-Dialog. ESC verlässt ihn # sauber (Panels wieder sichtbar), danach geht der Test weiter. --------- main._unhandled_key_input(_press_key(KEY_F5, false)) _check(str(main.status_label.text).contains("Testlauf läuft"), "F5: Lauf-Modus gestartet (Statuszeile meldet)") _check(not main.error_dialog.visible, "F5: kein Fehler-Dialog (Lauf-Modus braucht keinen Client)") _check(main.get("playtest") != null, "F5: PlaytestMode ist aktiv") main._unhandled_key_input(_press_key(KEY_ESCAPE, false)) _check(main.get("playtest") == null, "F5+ESC: Lauf-Modus sauber beendet") _check(main.error_dialog == null or not main.error_dialog.visible, "F5+ESC: kein Fehler-Dialog offen") # --- 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)) ## Karte t_89c0ec95: Höhenlinien NUR im Terrain-Werkzeug — Kopplung am ## Werkzeugwechsel (F1 ein, F2/F3/F4 aus), Fix über L / Button. Der Test ## drückt alles SYNCHRON durch _set_tool_mode bzw. _unhandled_key_input: ## die Sichtbarkeit kippt im selben Aufruf — der Beweis, dass kein ## _process-Poll nötig ist (0 Frames). func _test_contours_tool_coupling() -> void: print("Testgruppe: Höhenlinien-Werkzeug-Kopplung (t_89c0ec95)") # Aufräumen aus der karte3-Gruppe: programmisches confirmed.emit() schließt # das dirty-Dialog-Window NICHT — ein sichtbares Window sperrt das # F/L-Tasten-Gate (ShortcutPolicy). Deshalb hier alle Windows zu. for child in main.get_children(): if child is Window: child.hide() main._set_tool_mode("objects") var terrain_view = main.view_builder.get("terrain_view") _check(terrain_view != null, "Terrain-View existiert im Builder") _check(str(main.get("tool_mode")) == "objects", "Start: Objekt-Werkzeug") _check(not bool(terrain_view.get("_contours_pref")), "Start: Höhenlinien AUS (kein Fix, kein Terrain-Werkzeug)") # Regression zur obigen Root-Cause: Struktur-Änderung (Undo) baut das # Terrain-Mesh NEU — die Linien dürfen NICHT auf Default „an“ zurück- # springen (vor t_89c0ec95 geschah genau das nach jedem Strg+Z). var before_obj: Dictionary = main.history.snapshot(["objects"]) (main.doc["objects"] as Array).append( {"id": "test_contour_objekt", "pos": [2.0, 0.0, 2.0], "scale": 1.0, "rot_y_deg": 0.0}) main.history.commit("Test: Kontur-Objekt", before_obj, main.history.snapshot(["objects"])) main._do_undo() terrain_view = main.view_builder.get("terrain_view") # Rebuild → neue View _check(terrain_view != null, "Undo: Terrain-View frisch vorhanden") _check(not bool(terrain_view.get("_contours_pref")), "Struktur-Änderung (Undo + Rebuild): Höhenlinien bleiben AUS im Objekt-Werkzeug") # F1 → Terrain: Linien EIN, synchron im Wechselpunkt. main._unhandled_key_input(_press_key(KEY_F1, false)) _check(str(main.get("tool_mode")) == "terrain", "F1: Terrain-Werkzeug an") _check(bool(terrain_view.get("_contours_pref")), "F1: Höhenlinien EIN (synchron, 0 Frames Poll)") # Jedes andere Werkzeug: Linien wieder AUS. main._unhandled_key_input(_press_key(KEY_F2, false)) _check(str(main.get("tool_mode")) == "objects", "F2: zurück zum Objekt-Werkzeug") _check(not bool(terrain_view.get("_contours_pref")), "F2: Höhenlinien AUS") main._unhandled_key_input(_press_key(KEY_F3, false)) _check(not bool(terrain_view.get("_contours_pref")), "F3 (Bauen): Höhenlinien AUS") main._unhandled_key_input(_press_key(KEY_F4, false)) _check(not bool(terrain_view.get("_contours_pref")), "F4 (Spawns): Höhenlinien AUS") main._unhandled_key_input(_press_key(KEY_F2, false)) # L = Fixieren: sichtbar sogar AUSSERHALB des Terrain-Werkzeugs. main._unhandled_key_input(_press_key(KEY_L, false)) _check(bool(terrain_view.get("_contours_pref")), "L: Höhenlinien FIXIERT AN (sichtbar im Objekt-Werkzeug)") _check(main.contours_btn.button_pressed, "L: Button „≈ Höhenlinien“ mitgesynced (pressed)") _check(str(main.status_label.text).contains("FIXIERT"), "L: Statuszeile meldet Fix AN") # Fix bleibt über Werkzeugwechsel bestehen … main._unhandled_key_input(_press_key(KEY_F1, false)) _check(bool(terrain_view.get("_contours_pref")), "Fix an: F1 lässt Linien an") # … aber ein Werkzeugwechsel KANN bei Fix-an nie ausblenden. L im Terrain- # Werkzeug löst den Fix: Linien bleiben (Werkzeug-Kopplung trägt weiter). main._unhandled_key_input(_press_key(KEY_L, false)) _check(not bool(main.get("_contours_on")), "L im Terrain: Fix gelöst") _check(bool(terrain_view.get("_contours_pref")), "L im Terrain: Linien bleiben AN (Werkzeug-Kopplung trägt)") _check(not main.contours_btn.button_pressed, "L im Terrain: Button-Fix mitgesynced (unpressed)") main._unhandled_key_input(_press_key(KEY_F2, false)) _check(not bool(terrain_view.get("_contours_pref")), "F2 nach Fix-Lösung: Höhenlinien wieder AUS") # Button-Weg (ohne L): Drücken des Buttons fixiert ditto. main.contours_btn.set_pressed(true) # feuert toggled → _on_contours_toggled _check(bool(terrain_view.get("_contours_pref")), "Button: Fix AN fixiert Linien im Objekt-Werkzeug") main.contours_btn.set_pressed(false) _check(not bool(terrain_view.get("_contours_pref")), "Button: Fix AUS blendet zurück (nur Terrain-Werkzeug)")