scopedvr-map-editor/tests/test_resize_dialog.gd
Hermes (scopedvr-dev) ae551f387a Fix: Resize-Dialog 5930 px hoch + OK/Abbrechen unterhalb des Bildschirms (Sly-Befund 20.10. „viel zu lang, kann nichts bestätigen").
Ursache (Proben im Container, xvfb): autowrap-Labels OHNE Mindestbreite
melden VOR dem ersten Layout eine explosionsartige Mindesthöhe (Wrap bei
~1 px → jedes Wort eine Zeile). popup_centered() übernimmt genau diesen
Wert als Fensterhöhe: Resize-Dialog 5930 px statt ~470, Neue-Map-Dialog
4781 px statt ~390 — beide waren betroffen. Nach dem Layout fällt der
Mindestwert zurück, die Fenstergröße bleibt aber.
Zusatz-Befund: wächst die Live-Vorschau NACH dem Öffnen (Preset 256 →
Warnband), bleibt das Fenster oben hängen → Buttons bei y≈940 in einem
900-px-Sichtbereich = unerreichbar.

Fix:
- resize_map_dialog + new_map_dialog: autowrap-Labels bekommen
  Mindestbreite ~ Dialogbreite → Umbruch rechnet mit echter Breite,
  Fenster normal hoch (Probe: 481 px).
- resize_map_dialog: Inhalt in ScrollContainer (horizontal aus), Höhe
  dynamisch = min(Inhalt, sichtbarer Platz); nach jeder Vorschau-Änderung
  wird das Fenster gedeckelt und neu zentriert (_fit_dialog/
  _fit_after_layout) → OK/Abbrechen IMMER erreichbar. unresizable.
- tests: test_resize_dialog prüft jetzt Mindesthöhe VOR dem Popup
  (absolut, < 1500 px) für beide Dialoge + echten Menü-Klick;
  Preset-Button-Suche robust über find_children.
Proben (xvfb): Dialog 662×481, nach Preset 256 → 662×631, unten 765 < 900,
Buttons sichtbar (Beweisbild work/dialog_beweis/resize_dialog_fix.png).
Suiten: test_resize_dialog 44 OK, test_theme 35 OK, test_gui_smoke 92 OK,
test_ux 137 OK, test_ui_layout 79 OK.
2026-10-20 18:33:07 +00:00

287 lines
No EOL
12 KiB
GDScript
Raw Permalink 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): R3 Resize-Dialog — Karte t_c863b93b.
## Ausführen: godot --headless --path . --script res://tests/test_resize_dialog.gd
## Exit 0 = alles grün.
##
## Deckt ab:
## 1. Dialog-Aufbau (Stil wie new_map_dialog): Presets, Clamp 256–4096,
## Info-Zeile mit aktueller Größe/Auflösung.
## 2. Preview-Texte (R2 resize_preview): Verlust-Liste beim Verkleinern,
## „nichts verloren“-Fall, Spawn-Speicher-Warnung (< 4 Punkte/Team, V11),
## Warnband beim Vergrößern über ~1024 m (Dichte-Leiter-Deckel).
## 3. Verdrahtung in main.gd: Menü-ID 6 öffnet Dialog, Bestätigen = resize_apply
## als EINE Undo-Einheit, Undo-Roundtrip speichert die alte Map
## BYTE-IDENTISCH, Redo stellt den Resize wieder her.
var failures := 0
var checks := 0
var main: Control
func _initialize() -> void:
_run_all.call_deferred()
func _run_all() -> void:
_test_dialog_basics()
_test_preview_texts()
await _test_main_wiring()
_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)
# --------------------------------------------------------------- Dialog -----
func _make_dialog() -> ConfirmationDialog:
var dlg: ConfirmationDialog = preload("res://src/ui/resize_map_dialog.gd").new()
return dlg
func _test_dialog_basics() -> void:
print("Testgruppe: Dialog-Aufbau + Clamp")
var dlg := _make_dialog()
var doc := SvmapDocument.create_new("Test", 1024.0, "mittag")
dlg.setup(doc)
_check(dlg.info_label.text.contains("1024"), "Info-Zeile zeigt aktuelle Groesse (1024 m)")
_check(dlg.info_label.text.contains("129"), "Info-Zeile zeigt Aufloesung (129 Punkte)")
_check(is_equal_approx(dlg.get_new_size_x(), 1024.0), "Freifeld startet bei aktueller Groesse")
# Preset-Button 256: beide Achsen wandern mit (Button sitzt im Preset-HBox,
# unter dem ScrollContainer — FIX 20.10.b hat den dazwischengeschoben).
var preset_btn: Button = null
for h in dlg.find_children("*", "HBoxContainer", true, false):
for b in (h as HBoxContainer).get_children():
if b is Button and b.text == "256 m":
preset_btn = b
_check(preset_btn != null, "Preset-Button 256 m vorhanden")
if preset_btn != null:
preset_btn.emit_signal("pressed")
_check(is_equal_approx(dlg.get_new_size_x(), 256.0)
and is_equal_approx(dlg.get_new_size_z(), 256.0),
"Preset 256 setzt beide Achsen")
# Clamp: SpinBox deckelt auf 256–4096.
dlg.size_x_edit.value = 99999.0
_check(is_equal_approx(dlg.get_new_size_x(), 4096.0), "Clamp oben: 4096 m")
dlg.size_x_edit.value = 1.0
_check(is_equal_approx(dlg.get_new_size_x(), 256.0), "Clamp unten: 256 m")
dlg.free()
# -------------------------------------------------------------- Preview -----
## Map mit Objekt + Bauteil + Spawn-Punkt draußen (X = ±5000 m) bauen.
func _doc_with_outsiders(size: float) -> Dictionary:
var doc := SvmapDocument.create_new("Preview", size, "mittag")
doc["objects"] = [
{"id": "drinnen", "pos": [10.0, 0.0, 10.0], "rot": 0.0, "scale": 1.0},
{"id": "draussen", "pos": [5000.0, 0.0, 0.0], "rot": 0.0, "scale": 1.0},
]
doc["gebaeude"] = [
{"pos": [-30.0, 0.0, -30.0], "rot": 0.0,
"punkte": [[-2.0, 0.0, -2.0], [2.0, 0.0, 2.0]]},
{"pos": [-5000.0, 0.0, 0.0], "rot": 0.0,
"punkte": [[-2.0, 0.0, -2.0], [2.0, 0.0, 2.0]]},
]
var zone0: Dictionary = doc["spawn_zones"]["0"]
var pts: Array = zone0["points"]
for i in pts.size():
pts[i]["pos"][0] = 5000.0 + float(i) # Team Blau komplett draußen
zone0["points"] = pts
return doc
func _test_preview_texts() -> void:
print("Testgruppe: Preview-Texte (Verlust-Liste, Spawn-Warnung, Warnband)")
var dlg := _make_dialog()
# 1) Verkleinern mit Verlust: Liste + Entfernen-Hinweis + Spawn-Warnung.
var doc := _doc_with_outsiders(1024.0)
dlg.setup(doc)
dlg.size_x_edit.value = 256.0
dlg.size_z_edit.value = 256.0
dlg._update_preview() # auerhalb des Baums feuert value_changed nicht (in-tree: live)
var text: String = dlg.preview_label.text
_check(text.contains("draußen") and text.contains("1 Objekt"), "Verlust-Liste nennt 1 Objekt draußen")
_check(text.contains("1 Bauteil"), "Verlust-Liste nennt 1 Bauteil draußen")
# 4 verschobene (Team Blau) + 4 Default-Punkte (Team Rot bei ±0,8 × 512 m) = 8 draußen.
_check(text.contains("8 Spawn-Punkt"), "Verlust-Liste nennt 8 Spawn-Punkte draußen")
_check(text.contains("ENTFERNT"), "Hinweis: draußen wird entfernt (Checkbox an)")
_check(text.contains("Team") and text.contains("4 pro Team"), "Spawn-Speicher-Warnung: Team unter 4 Punkte")
_check(dlg.get_drop_outside(), "Checkbox Entfernen ist Default-an")
# 2) Entfernen aus: draußen bleibt, Map ist dann nicht speicherbar (V9).
dlg.drop_check.button_pressed = false
dlg._update_preview()
text = dlg.preview_label.text
_check(text.contains("bleiben") and text.contains("Rand"), "Ohne Entfernen: draußen bleibt in der Map")
_check(text.contains("NICHT speicherbar"), "Ohne Entfernen: Warnung vor V9 (Speichern blockiert)")
_check(not text.contains("ENTFERNT"), "Kein Entfernen-Hinweis bei Checkbox aus")
dlg.drop_check.button_pressed = true
# 3) Verkleinern ohne Verlust: alles innen → Klartext „nichts verloren“.
var doc_leer := SvmapDocument.create_new("Leer", 512.0, "mittag")
var z0: Dictionary = doc_leer["spawn_zones"]["0"]
var z1: Dictionary = doc_leer["spawn_zones"]["1"]
for zone in [z0, z1]:
var pts2: Array = zone["points"]
for i in pts2.size():
pts2[i]["pos"][0] = 5.0 * float(i) # alle nah am Zentrum
pts2[i]["pos"][2] = 0.0 # Z ebenfalls rein (Default-Z wäre ±153 m)
zone["points"] = pts2
dlg.setup(doc_leer)
dlg.size_x_edit.value = 256.0
dlg.size_z_edit.value = 256.0
dlg._update_preview()
text = dlg.preview_label.text
_check(text.contains("nichts verloren"), "Verkleinern ohne Verlust: Klartext-Meldung")
# 4) Vergrößern > 1024 m: Warnband (Dichte grobt, Quest-Perf).
dlg.setup(SvmapDocument.create_new("Grow", 512.0, "mittag"))
dlg.size_x_edit.value = 2048.0
dlg.size_z_edit.value = 2048.0
dlg._update_preview()
text = dlg.preview_label.text
_check(text.contains("grober") and text.contains("Quest"), "Warnband bei Grow > 1024 m (Dichte + Quest-Perf)")
_check(text.contains("flaches Gras") or text.contains("Randzone"), "Grow-Hinweis: neue Randzone wird Gras")
dlg.free()
# -------------------------------------------------------- main.gd-Drähte ----
func _test_main_wiring() -> void:
print("Testgruppe: main.gd-Verdrahtung + Undo-Roundtrip")
var scene: PackedScene = load("res://scenes/main.tscn")
_check(scene != null, "scenes/main.tscn laedt")
if scene == null:
return
main = scene.instantiate()
root.add_child(main)
await process_frame
await process_frame
_check(main.resize_dialog != null, "Resize-Dialog in der Main-UI gebaut")
main._start_with_new_map()
var doc: Dictionary = main.doc
_check(is_equal_approx(float(doc["terrain"]["size_x_m"]), 1024.0), "Start-Map 1024 m")
# Team Blau komplett draußen schieben (Verkleinern 1024 → 256).
var zone0: Dictionary = doc["spawn_zones"]["0"]
var pts: Array = zone0["points"]
for i in pts.size():
pts[i]["pos"][0] = 500.0 + float(i) # drin (±512), aber draußen bei 256
zone0["points"] = pts
# Referenz-Sicherheit: history hält doc als Objekt (keine Kopie).
_check(is_same(main.history.doc, main.doc), "History haelt doc per Referenz")
# Vorher-Zustand speichern → später Byte-Identität prüfen.
var path_a := "/tmp/r3_vorher.svmap"
var path_b := "/tmp/r3_nach_undo.svmap"
var save_a := SvmapIO.save_to_file(path_a, doc)
_check(bool(save_a["ok"]), "Vorher-Zustand speicherbar")
# Dialog öffnen (Menü-Pfad): Info-Zeile + Preview gefüllt.
main._open_resize_dialog()
_check(main.resize_dialog.visible, "Menue-Eintrag oeffnet den Dialog")
_check(main.resize_dialog.info_label.text.contains("1024"), "Dialog zeigt aktuelle Groesse")
main.resize_dialog.size_x_edit.value = 256.0
main.resize_dialog.size_z_edit.value = 256.0
_check(main.resize_dialog.preview_label.text.contains("draußen"),
"Live-Preview listet Verlust beim Verkleinern")
# Bestätigen: resize_apply + EINE Undo-Einheit.
main._on_resize_confirmed()
var t: Dictionary = main.doc["terrain"]
_check(is_equal_approx(float(t["size_x_m"]), 256.0), "Terrain auf 256 m verkleinert")
_check((main.doc["spawn_zones"]["0"]["points"] as Array).is_empty(),
"Draußen liegende Spawn-Punkte entfernt (Drop an)")
_check(bool(main.dirty), "Dirty-Flag gesetzt")
_check(main.history.can_undo() and main.history.undo_desc().contains("256"),
"EINE Undo-Einheit: Karte auf 256 m")
_check(is_same(main.history.doc, main.doc), "doc-Identitaet bleibt beim Resize erhalten")
# Strg+Z: kompletter Vorher-Zustand, Map speichert BYTE-IDENTISCH.
main._do_undo()
var t2: Dictionary = main.doc["terrain"]
_check(is_equal_approx(float(t2["size_x_m"]), 1024.0), "Undo: Terrain wieder 1024 m")
_check((main.doc["spawn_zones"]["0"]["points"] as Array).size() == 4,
"Undo: entfernte Spawn-Punkte komplett zurueck")
var save_b := SvmapIO.save_to_file(path_b, main.doc)
_check(bool(save_b["ok"]), "Nach-Undo-Zustand speicherbar")
var bytes_a := FileAccess.get_file_as_bytes(path_a)
var bytes_b := FileAccess.get_file_as_bytes(path_b)
_check(bytes_a == bytes_b, "Undo-Roundtrip: alte Map BYTE-IDENTISCH")
# Strg+Y: Resize wieder hergestellt.
main._do_redo()
_check(is_equal_approx(float(main.doc["terrain"]["size_x_m"]), 256.0),
"Redo: Resize wiederhergestellt")
_check((main.doc["spawn_zones"]["0"]["points"] as Array).is_empty(),
"Redo: Spawn-Punkte wieder entfernt")
# Drop aus: draußen bleibt in der Map (Dialog-Checkbox aus) — dokumentiertes
# Verhalten: Map bleibt, ist ABER nicht speicherbar (V9: Spawn außerhalb der
# Match-Grenzen); der Dialog warnt davor.
main.resize_dialog.drop_check.button_pressed = false
main.resize_dialog.size_x_edit.value = 256.0
main.resize_dialog.size_z_edit.value = 256.0
# Erst Undo (zurück auf 1024 mit Punkten), dann Resize ohne Drop.
main._do_undo()
var before_count := (main.doc["spawn_zones"]["0"]["points"] as Array).size()
main._on_resize_confirmed()
_check((main.doc["spawn_zones"]["0"]["points"] as Array).size() == before_count,
"Drop aus: draußen liegende Spawn-Punkte bleiben in der Map")
_check(not bool(SvmapIO.save_to_file("/tmp/r3_drop_aus.svmap", main.doc)["ok"]),
"Drop aus + Punkte draußen: Speichern blockiert (V9), wie im Dialog gewarnt")
# REGRESSION 20.10. (Sly-Befund „Kartengröße ändern tut nichts“): der
# ECHTE UI-Pfad ist das id_pressed-SIGNAL des Datei-Popups. Vorher hing
# main.call.bind("_on_menu_action") daran — die Menü-ID landete als
# ARGUMENT VOR dem Methodennamen (call(6, "_on_menu_action")) →
# „Cannot convert argument 1 from int to StringName“, das GESAMTE
# Datei-Menü war tot. Diese Suite rief _open_resize_dialog() direkt auf
# und sah den Bruch nicht — jetzt wird der echte Klick nachgestellt.
var file_menu_btn: MenuButton = null
for n in main.find_children("*", "MenuButton", true, false):
if n is MenuButton and n.text == "Datei":
file_menu_btn = n
_check(file_menu_btn != null, "Datei-Menü (MenuButton) gefunden")
if file_menu_btn != null:
main.resize_dialog.hide()
(file_menu_btn.get_popup() as PopupMenu).id_pressed.emit(6)
_check(main.resize_dialog.visible,
"Echter Menü-Klick (id_pressed=6) öffnet den Resize-Dialog")
# REGRESSION 20.10.b (Sly: „Fenster viel zu lang, nichts bestätigbar“):
# autowrap-Labels ohne Mindestbreite melden VOR dem ersten Layout eine
# explosionsartige Mindesthöhe (jedes Wort eine Zeile bei ~1 px Breite) →
# popup_centered() übernimmt diesen Wert als Fensterhöhe (Probe: 5930 px,
# OK/Abbrechen unterhalb des Bildschirms). Fix: Mindestbreite ~ Dialog-
# breite an den Labels. Der Check MISST VOR dem Popup — genau da saugte
# sich das Fenster voll. Absolutwert (kein Prozentbezug, headless-Wurzel-
# fenster ist nur 64 px hoch).
var dlg_min_h: float = main.resize_dialog.get_contents_minimum_size().y
_check(dlg_min_h > 200.0 and dlg_min_h < 1500.0,
"Resize-Dialog Mindesthöhe normal vor Popup (%.0f px)" % dlg_min_h)
var nm_min_h: float = main.new_dialog.get_contents_minimum_size().y
_check(nm_min_h > 150.0 and nm_min_h < 1500.0,
"Neue-Map-Dialog Mindesthöhe normal (%.0f px)" % nm_min_h)