scopedvr-map-editor/tests/test_inspector.gd

264 lines
10 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): 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: Klemmung 0,05–20 + Marker-Rescale (echtes Modell) + Zurücksetzen.
## 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
_t1_einzelauswahl()
_t2_tippen()
_t3_ziehen()
_t4_mehrfach_drehung()
_t5_mixed_angleichen()
_t6_groesse()
_t7_auf_boden()
_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 „—“")
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 and absf(x1 - 3.0) < 0.0001,
"T5 Tippen gleicht beide X auf 3.0 an")
_check(_undo_steps_to_clear() == 1, "T5 Angleichen = GENAU EIN Undo-Schritt")
func _t6_groesse() -> void:
_select(0)
var w = insp.widget("scale")
w.edit.text = "100"
w.edit.text_submitted.emit("100")
_check(absf(float(_sel_obj(0).get("scale", 1.0)) - 20.0) < 0.0001,
"T6 Größe auf 100 geklemmt auf 20")
var marker = main.view_builder.object_markers[0]
var mi: MeshInstance3D = marker.get_child(0) as MeshInstance3D
if glb_ok:
_check(mi != null and mi.scale.length() > 19.9, "T6 Marker-Mesh skaliert live (echtes Modell)")
_check(float(marker.get_meta("half_height", 0.0)) > 0.0
and absf(float(marker.get_meta("pick_radius", 0.0))) > 0.0,
"T6 Pick-Metas nach Rescale gesetzt")
_check(_undo_steps_to_clear() == 1, "T6 Größen-Tippen = GENAU EIN Undo-Schritt")
# Zurücksetzen-Knopf (Undo hat die Auswahl geleert → neu anwählen).
_select(0)
w = insp.widget("scale")
w.edit.text = "3"
w.edit.text_submitted.emit("3")
_check(absf(float(_sel_obj(0).get("scale", 1.0)) - 3.0) < 0.0001, "T6 Größe 3.0 gesetzt")
insp.get("_reset_btn").pressed.emit()
_check(absf(float(_sel_obj(0).get("scale", 1.0)) - 1.0) < 0.0001,
"T6 Zurücksetzen stellt 1.0 her")
# (Vorher: Tippen 3.0 = 1 Schritt, Zurücksetzen = 1 Schritt → EIN Undo
# holt das Zurücksetzen zurück.)
_check(main.history.can_undo(), "T6 Zurücksetzen erzeugt Undo-Eintrag")
main.call("_do_undo")
_check(absf(float(_sel_obj(0).get("scale", 1.0)) - 3.0) < 0.0001,
"T6 EIN Undo-Schritt holt 3.0 zurück (kein Frame-/Tasten-Splitting)")
_undo_steps_to_clear()
# Platzhalter-Pfad (crate_wood, scale 2.0 → 1.0)
main.interaction.call("select_object", 1)
await process_frame
var w2 = insp.widget("scale")
w2.edit.text = "1"
w2.edit.text_submitted.emit("1")
_check(absf(float(_sel_obj(1).get("scale", 1.0)) - 1.0) < 0.0001,
"T6 Platzhalter-Größe geändert")
var marker2 = main.view_builder.object_markers[1]
var mi2: MeshInstance3D = marker2.get_child(0) as MeshInstance3D
_check(mi2 != null and absf(mi2.scale.x - 0.5) < 0.01,
"T6 Platzhalter-Mesh relativ reskaliert (2.0 → 1.0 = Faktor 0.5)")
_undo_steps_to_clear()
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)