extends SceneTree ## Editor-Test (headless): Objekt-Inspektor — Karte t_bb0d11c9. ## Ausführen: godot --headless --path . --script res://tests/test_inspector.gd ## Exit 0 = alles grün. ## ## Instanziert die ECHTE Main-UI (wie test_gui_smoke) und prüft: ## T1 Kopf/Sub/Info füllen bei Einzel-Auswahl (Name, ID, Kategorie, ## „N von M“, Poly, Kollision, echtes Modell ja/nein). ## T2 Position TIPPEN → doc geändert + 3D-Marker live; EIN Undo-Schritt. ## T3 Position ZIEHEN (Probe-Drag wie Maus) → doc geändert; EIN Undo-Schritt. ## T4 Mehrfachauswahl: Drehung tippen → beide Objekte gleich; EIN Undo-Schritt. ## T5 Uneinheitliche Werte → „—“; Tippen gleicht ALLE an. ## T6 Größe FEST (t_e166e656): scale-Zeile + Reset-Knopf entfernt, Wert bleibt. ## T7 „Auf Boden setzen“ nutzt die Boden-Logik; EIN Undo-Schritt. ## T8 Keine Auswahl → Felder aus, Hinweis an. var failures := 0 var checks := 0 var main var insp var glb_ok := false # GLB-Quelle verfügbar? (sonst wie test_glb_view umgebungsabhängig) func _initialize() -> void: _run_all.call_deferred() func _check(cond: bool, label: String) -> void: checks += 1 print("%s: %s" % ["ok " if cond else "FAIL", label]) if not cond: failures += 1 func _undo_steps_to_clear() -> int: ## Wie viele Undo-Schritte nötig, bis die History leer ist (Baseline clear). var n := 0 while main.history.can_undo() and n < 50: main.call("_do_undo") n += 1 return n 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) await process_frame await process_frame insp = main.get("inspector") _check(insp != null, "Inspektor ist in main eingebaut") if insp == null: _finish() return # Zwei Objekte ins Doc: barrel_001 (echtes GLB) + crate_wood (Platzhalter). (main.doc["objects"] as Array).append({ "id": "barrel_001", "pos": [4.0, 0.0, 4.0], "scale": 1.0, "rot_y_deg": 0.0}) (main.doc["objects"] as Array).append({ "id": "crate_wood", "pos": [-6.0, 0.0, 2.0], "scale": 2.0, "rot_y_deg": 10.0}) main.call("_after_structure_change") main.history.clear() await process_frame glb_ok = load("res://src/viewport/glb_mesh_store.gd").mesh_fuer("barrel_001") != null # await PFLICHT: T4/T5/T8 enthalten process_frame-Awaits — ohne await # lief _finish() sofort durch und die Mehrfach-Checks liefen NIEMALS # (Suite zeigte „ALL OK: 33 checks" und skippte still T4/T5/T8). await _t1_einzelauswahl() await _t2_tippen() await _t3_ziehen() await _t4_mehrfach_drehung() await _t5_mixed_angleichen() await _t6_groesse() await _t7_auf_boden() await _t8_leer() _finish() func _select(idx: int) -> void: # select_object ist synchron; das Panel füllt im selben Aufruf (refresh). main.interaction.call("select_object", idx) func _sel_obj(idx: int) -> Dictionary: return (main.doc["objects"] as Array)[idx] func _t1_einzelauswahl() -> void: _select(0) var head: String = str(insp.get("_head").text) var sub: String = str(insp.get("_sub").text) var info: String = str(insp.get("_info").text) _check(not head.is_empty() and head != "Objekt-Inspektor", "T1 Kopf zeigt Objekt-Namen") _check(sub.contains("ID: barrel_001"), "T1 Sub zeigt ID") _check(sub.contains("Kategorie: props"), "T1 Sub zeigt Kategorie") _check(sub.contains("1 von 2 Objekten markiert"), "T1 Sub zeigt 'N von M markiert'") _check(sub.contains("Poly: 184"), "T1 Sub zeigt Poly-Zahl") _check(info.contains("Kollision:"), "T1 Info zeigt Kollisionsform") # „Echtes Modell ja“ braucht die GLB-Quellen (Geschwister-Ordner) — fehlen # sie im Workspace-Klon, zeigt das Panel korrekt „nein“ (wie test_glb_view # umgebungsabhängig; der „nein“-Pfad wird über crate_wood geprüft). if glb_ok: _check(info.contains("Echtes Modell: ja"), "T1 echtes Modell ja (barrel_001 mit GLB)") else: print("SKIP: T1 echtes-Modell-ja (keine GLB-Quelle im Workspace — Platzhalter-„nein“ ist geprüft)") _check(str(insp.widget("pos_x").edit.text).contains("4.00"), "T1 pos_x zeigt 4.00") _check(str(insp.widget("rot_y").edit.text).contains("0.0"), "T1 rot_y zeigt 0.0") # Auswahl-Beweis per Klick im Viewport-Kontext: Panel ist sichtbar. _check(insp.get("_grid").visible, "T1 Felder sichtbar bei Auswahl") # Fix Vorbestand: das alte „Auswahl“-Feld füllt sich jetzt auch bei # select_object (Vorher stand dort dauerhaft „Nichts ausgewählt.“). _check(str(main.get("sel_label").text).contains("Barrel"), "T1 altes Auswahl-Feld füllt bei 3D-Klick-Weg") func _t2_tippen() -> void: _select(0) var w = insp.widget("pos_x") w.edit.text = "12.5" w.edit.text_submitted.emit("12.5") var px: float = float(_sel_obj(0).get("pos", [0, 0, 0])[0]) _check(absf(px - 12.5) < 0.0001, "T2 Tippen ändert doc-Position auf 12.5") var marker = main.view_builder.object_markers[0] _check(marker != null and absf(marker.position.x - 12.5) < 0.0001, "T2 3D-Marker folgt live (position.x = 12.5)") _check(_undo_steps_to_clear() == 1, "T2 Tippen = GENAU EIN Undo-Schritt") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[0]) - 4.0) < 0.0001, "T2 Undo stellt 4.0 wieder her") func _t3_ziehen() -> void: _select(0) var base: float = float(_sel_obj(0).get("pos", [0, 0, 0])[2]) var w = insp.widget("pos_z") w.probe_drag(40.0) # 40 px × 0,05 m/px = +2,0 m var now: float = float(_sel_obj(0).get("pos", [0, 0, 0])[2]) _check(absf(now - (base + 2.0)) < 0.0001, "T3 Ziehen am Label ändert Z um +2,0 m") var marker = main.view_builder.object_markers[0] _check(marker != null and absf(marker.position.z - now) < 0.0001, "T3 3D-Marker folgt live beim Ziehen") _check(_undo_steps_to_clear() == 1, "T3 Ziehen (Loslassen) = GENAU EIN Undo-Schritt") func _t4_mehrfach_drehung() -> void: main.interaction.call("select_many", [0, 1]) await process_frame var w = insp.widget("rot_y") w.edit.text = "45" w.edit.text_submitted.emit("45") var r0: float = float(_sel_obj(0).get("rot_y_deg", 0.0)) var r1: float = float(_sel_obj(1).get("rot_y_deg", 0.0)) _check(absf(r0 - 45.0) < 0.0001 and absf(r1 - 45.0) < 0.0001, "T4 Mehrfachauswahl: beide Drehungen = 45°") _check(_undo_steps_to_clear() == 1, "T4 Mehrfach-Änderung = GENAU EIN Undo-Schritt") _check(absf(float(_sel_obj(0).get("rot_y_deg", 0.0))) < 0.0001 and absf(float(_sel_obj(1).get("rot_y_deg", 0.0)) - 10.0) < 0.0001, "T4 Undo stellt 0°/10° wieder her") func _t5_mixed_angleichen() -> void: main.interaction.call("select_many", [0, 1]) await process_frame var w = insp.widget("pos_x") _check(str(w.edit.text) == "—", "T5 ungleiche X-Werte zeigen „—“") # Karte t_bceb0965: Position bleibt RELATIV — der getippte Wert gilt für # den Anker (erstes markiertes Objekt), die anderen wandern mit demselben # Delta mit (Abstände bleiben). Vorher: x0=4.0, x1=-6.0 → Abstand 10.0. var x0_vor: float = float(_sel_obj(0).get("pos", [0, 0, 0])[0]) var x1_vor: float = float(_sel_obj(1).get("pos", [0, 0, 0])[0]) w.edit.text = "3" w.edit.text_submitted.emit("3") var x0: float = float(_sel_obj(0).get("pos", [0, 0, 0])[0]) var x1: float = float(_sel_obj(1).get("pos", [0, 0, 0])[0]) _check(absf(x0 - 3.0) < 0.0001, "T5 Anker (Objekt 0) landet auf 3.0") _check(absf((x1 - x0) - (x1_vor - x0_vor)) < 0.0001, "T5 Relativ: Abstand zwischen beiden bleibt (%.1f m)" % absf(x1 - x0)) _check(_undo_steps_to_clear() == 1, "T5 Relativ-Move = GENAU EIN Undo-Schritt") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[0]) - x0_vor) < 0.0001 and absf(float(_sel_obj(1).get("pos", [0, 0, 0])[0]) - x1_vor) < 0.0001, "T5 Undo stellt beide Ausgangs-X wieder her") # Drehung/Größe bleiben ABSOLUT bei Mehrfachauswahl (Karten-Auftrag): main.interaction.call("select_many", [0, 1]) await process_frame var wr = insp.widget("rot_y") wr.edit.text = "45" wr.edit.text_submitted.emit("45") var r1: float = float(_sel_obj(1).get("rot_y_deg", 0.0)) _check(absf(r1 - 45.0) < 0.0001, "T5 Drehung bei Mehrfachauswahl = absolut (beide 45°)") _undo_steps_to_clear() func _t6_groesse() -> void: # Karte t_e166e656: Scale ist FEST — die Zeile ist aus dem Inspektor # entfernt (Sly-Wunsch „Realismus“). Kein Widget, kein Reset-Knopf, der # gespeicherte Wert bleibt unangetastet. _select(0) _check(insp.widget("scale") == null, "T6 scale-Zeile ist ENTFERNT (kein Widget)") _check(insp.get("_reset_btn") == null, "T6 Reset-Knopf entfernt") var vor: float = float(_sel_obj(0).get("scale", 1.0)) _check(absf(vor - 1.0) < 0.0001, "T6 Objekt behält sein scale (1.0)") # Ausgewählt lassen und prüfen: der Inspektor zeigt den lesenden Hinweis. insp.call("refresh") _check(insp.get("_ground_btn") != null and insp.get("_ground_btn").visible, "T6 Boden-Knopf bleibt bedienbar (Größe-Sektion zeigt nur Hinweis)") func _t7_auf_boden() -> void: _select(0) var w = insp.widget("pos_y") w.edit.text = "5" w.edit.text_submitted.emit("5") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[1]) - 5.0) < 0.0001, "T7 Höhe getippt: y = 5") insp.get("_ground_btn").pressed.emit() var floor_y: float = float(_sel_obj(0).get("pos", [0, 0, 0])[1]) var expected: float = SvmapDocument.terrain_height_at(main.doc, 4.0, 4.0) _check(absf(floor_y - expected) < 0.01, "T7 'Auf Boden setzen' setzt auf Terrain-Höhe") # Zwei Schritte in der History: Tippen (0→5) + Boden (5→Boden). Jedes EIN Schritt. _check(main.history.can_undo(), "T7 Boden-Setzen hat Undo-Eintrag") main.call("_do_undo") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[1]) - 5.0) < 0.0001, "T7 1. Undo holt das Boden-Setzen zurück (y = 5)") main.call("_do_undo") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[1]) - 0.0) < 0.0001, "T7 2. Undo holt das Tippen zurück (y = 0, Ausgangswert)") _check(not main.history.can_undo(), "T7 genau 2 Undo-Schritte, dann History leer") # Y unter Boden tippen wird geklemmt (nichts begraben). w.edit.text = "-3" w.edit.text_submitted.emit("-3") _check(absf(float(_sel_obj(0).get("pos", [0, 0, 0])[1]) - expected) < 0.01, "T7 y unter dem Boden wird auf Terrain-Höhe geklemmt") _undo_steps_to_clear() func _t8_leer() -> void: main.interaction.call("clear_selection") await process_frame _check(insp.get("_hint").visible, "T8 ohne Auswahl: Hinweis sichtbar") _check(not insp.get("_grid").visible, "T8 ohne Auswahl: Felder aus") _check(not insp.get("_ground_btn").visible, "T8 ohne Auswahl: Knöpfe aus") func _finish() -> void: if failures == 0: print("ALL OK: %d checks" % checks) quit(0) else: print("FAILED: %d/%d checks" % [failures, checks]) quit(1)