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))