extends SceneTree ## Editor-Test (headless): Assets-Bereich Unity-artig — Karte t_69687afc (U4). ## Ausführen: godot --headless --path . --script res://tests/test_assets_panel.gd ## Exit 0 = alles grün. ## ## Instanziert die ECHTE Main-UI (Muster tests/test_library_browser_ui.gd) und prüft: ## 1. Kategorien-Leiste: ein Chip je Bibliotheks-Gruppe (keine eigene Liste), ## Summe der Gruppen-Anzahlen = Gesamtzahl des Manifests, „Alle" = Gesamtzahl. ## 2. Suche findet NAME und ID (case-insensitive), Zähler „N von M sichtbar". ## 3. Kategorie-Chip filtert auf genau diese Gruppe (Zähler M = Gruppengröße). ## 4. Fehlendes Modell (glb-Eintrag, keine Quelle) → Kachel trägt die ## Markierung „Modell fehlt" + Tooltip-Hinweis; Primitive ohne glb nie. ## 5. Auswahl-Signal: Klick (Selection) löst object_selected GENAU EINMAL je ## NEUER Auswahl aus (Wiederholung derselben Kachel kein zweites Signal). ## 6. Hover vergrößert die ID-Schrift auf der Kachel. ## 7. Doppelklick-Aktion startet das Platzier-Werkzeug wie bisher und nochmal ## auf derselben Kachel bricht ab (alter An/Aus-Vertrag). ## 8. „Zuletzt benutzt" wird je Auswahl in user://scopedvr_editor.cfg ## gemerkt, überlebt Neu-Instanz und taucht als Chip + Ansicht auf. const GLB_QUELLEN_SETTING := "scopedvr/editor/glb_quellen" var failures := 0 var checks := 0 var main: Control var assets # src/ui/panels/assets_panel.gd var selected_count := 0 var activated_count := 0 func _initialize() -> void: _run_all.call_deferred() func _run_all() -> void: _clear_library_cfg_section() 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 assets = main.assets_mod _check(assets != null, "Assets-Panel über Registry angemeldet") if assets == null: _finish() return assets.object_selected.connect(_on_selected) assets.object_activated.connect(_on_activated) _test_klapp_zustaende_cfg_robus() _test_kategorien() _test_suche_und_zaehler() _test_kategorie_filter() _test_fehlendes_modell() _test_auswahl_signal() _test_hover() _test_doppelklick() await _test_zuletzt_persistenz() _finish() func _on_selected(_oid: String) -> void: selected_count += 1 func _on_activated(_oid: String) -> void: activated_count += 1 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) func _clear_library_cfg_section() -> void: # Nur UNSEREN Abschnitt leeren — [test]/[update]/[fenster] bleiben. var cfg := ConfigFile.new() cfg.load("user://scopedvr_editor.cfg") if cfg.has_section("library"): cfg.erase_section("library") cfg.save("user://scopedvr_editor.cfg") # --------------------------------------- 0 Klapp-Zustände aus kaputter cfg ---- func _test_klapp_zustaende_cfg_robus() -> void: print("Testgruppe: Klapp-Zustände laden sauber aus kaputter cfg (t_efb3d022)") # Kaputte cfg: [library] enthält einen Array-Eintrag (zuletzt_benutzt) und # einen korrupten Eintrag (String statt Bool) neben einem echten Bool. var cfg := ConfigFile.new() cfg.set_value("library", "zuletzt_benutzt", PackedStringArray(["box_002", "box_001"])) cfg.set_value("library", "gruppen_kisten", true) cfg.set_value("library", "gruppen_bauteile", "kaputt") cfg.set_value("library", "gruppen_spawn", 42) cfg.save("user://scopedvr_editor.cfg") # Direkt aufrufen: darf KEINEN SCRIPT ERROR werfen (bool() war der # alte Crash) und muss die sauberen Bool-Zustände trotzdem liefern. var states: Dictionary = assets._library_load_open_states() _check(states.size() == 3, "3 Zustände geladen, Array-Eintrag übersprungen (ist %d)" % states.size()) _check(states.get("gruppen_kisten") == true, "echter Bool bleibt true") _check(states.get("gruppen_bauteile") == true, "String-Eintrag auf Fallback true") _check(states.get("gruppen_spawn") == true, "Int-Eintrag auf Fallback true") # Und der Persistenz-Weg (save → load) liefert die Zustände unverändert: assets._library_save_open_state("gruppen_kisten", false) var states2: Dictionary = assets._library_load_open_states() _check(states2.get("gruppen_kisten") == false, "save→load Rundtrip: false kommt zurück") _check(states2.get("gruppen_bauteile") == true, "kaputter Eintrag nach Rundtrip weiter Fallback true") # Aufräumen: nächste Testgruppen brauchen eine saubere cfg. _clear_library_cfg_section() # ------------------------------------------------------------- 1 Kategorien ---- func _test_kategorien() -> void: print("Testgruppe: Kategorien-Leiste (aus der Bibliothek, Summe = Gesamtzahl)") var lib: Dictionary = main.manifest.get("objects", {}) var groups := LibraryBrowser.groups_in_order(main.manifest) var chip_keys: Array = assets._cat_buttons.keys() _check(chip_keys.has("alle"), "Chip „Alle“ existiert") _check(chip_keys.has("zuletzt"), "Chip „Zuletzt benutzt“ angelegt (noch versteckt)") var alle_chip: Button = assets._cat_buttons.get("alle") _check(int(alle_chip.get_meta("cat_count")) == lib.size(), "„Alle“ zeigt Gesamtzahl (%d)" % int(alle_chip.get_meta("cat_count"))) var sum := 0 var group_keys := {} for group in groups: if (group["ids"] as Array).is_empty(): continue group_keys[str(group["key"])] = true var chip: Button = assets._cat_buttons.get(str(group["key"])) _check(chip != null, "Chip für Gruppe „%s“ existiert" % str(group["label"])) if chip == null: continue sum += int(chip.get_meta("cat_count")) _check(chip.text.contains(str(group["label"])), "Chip-Text trägt Gruppenname (%s)" % chip.text) _check(chip_keys.size() == group_keys.size() + 2, "keine erfundenen Kategorien (%d Chips für %d Gruppen + Alle/Zuletzt)" % [chip_keys.size(), group_keys.size()]) _check(sum == lib.size(), "Summe der Gruppen-Anzahlen = Gesamtzahl (%d = %d)" % [sum, lib.size()]) _check(alle_chip.button_pressed, "„Alle“ ist Start-Kategorie") _check(not (assets._cat_buttons.get("zuletzt") as Button).visible, "Zuletzt-Chip startet versteckt (noch nichts benutzt)") # ---------------------------------------------------- 2 Suche + Zähler ---- func _zaehler_n() -> int: var text: String = assets.library_counter.text var vor: String = text.split(" ")[0] return int(vor) func _zaehler_m() -> int: var text: String = assets.library_counter.text var teile: PackedStringArray = text.split(" ") return int(teile[2]) func _test_suche_und_zaehler() -> void: print("Testgruppe: Suche (Name + ID) und Zähler „N von M sichtbar“") var lib_size := (main.manifest.get("objects", {}) as Dictionary).size() # Namens-Suche (deutsch, case egal): var erwartet_zelt := 0 for oid2 in (main.manifest.get("objects", {}) as Dictionary).keys(): var e2: Dictionary = (main.manifest["objects"] as Dictionary)[oid2] if str(e2.get("name_de", "")).to_lower().contains("zelt") or str(oid2).contains("zelt"): erwartet_zelt += 1 main.library_search.text = "ZELT" assets._apply_library_filter("ZELT") var pilze := 0 for oid in (main.manifest.get("objects", {}) as Dictionary).keys(): var btn := main.library_buttons.get(str(oid)) as Button if btn != null and btn.visible: pilze += 1 _check(pilze == erwartet_zelt and pilze >= 10, "„ZELT“ (Name, groß) findet %d Kacheln (ist %d)" % [erwartet_zelt, pilze]) _check(assets.library_counter.text == "%d von %d sichtbar" % [erwartet_zelt, lib_size], "Zähler bei Suche: „%s“" % assets.library_counter.text) # ID-Suche (Kurz-ID): main.library_search.text = "" assets._apply_library_filter("water_tower_004") var wt := main.library_buttons.get("water_tower_004") as Button _check(wt != null and wt.visible, "ID-Suche „water_tower_004“ findet den Wasserturm") main.library_search.text = "" assets._apply_library_filter("") _check(assets.library_counter.text == "%d von %d sichtbar" % [assets._library_group_rows[0]["count"], lib_size], "Zähler bei „Alle“ (1 Gruppe offen): „%s“" % assets.library_counter.text) # -------------------------------------------------- 3 Kategorie-Filter ---- func _test_kategorie_filter() -> void: print("Testgruppe: Kategorie-Chip filtert auf die Gruppe") var zelte_n := 0 for oid2 in (main.manifest.get("objects", {}) as Dictionary).keys(): if str(((main.manifest["objects"] as Dictionary)[oid2] as Dictionary).get("kategorie", "")) == "zelte": zelte_n += 1 assets._select_category("zelte") var sichtbar := 0 for oid in (main.manifest.get("objects", {}) as Dictionary).keys(): var btn := main.library_buttons.get(str(oid)) as Button if btn != null and btn.visible: sichtbar += 1 _check(sichtbar == zelte_n and zelte_n > 0, "Kategorie „Zelte“: %d Kacheln sichtbar (ist %d)" % [zelte_n, sichtbar]) _check(assets.library_counter.text == "%d von %d sichtbar" % [zelte_n, zelte_n], "Zähler in Kategorie: „%s“" % assets.library_counter.text) var pilze_chip: Button = assets._cat_buttons.get("zelte") _check(pilze_chip.button_pressed, "Zelte-Chip gedrückt") assets._select_category("alle") _check((assets._cat_buttons.get("alle") as Button).button_pressed, "Zurück auf „Alle“") # -------------------------------------------------- 4 Fehlendes Modell ---- func _test_fehlendes_modell() -> void: print("Testgruppe: Fehlendes Modell wird markiert (nicht stillschweigend)") # Hermetisch: GLB-Quellen auf ein garantiert leeres Lager umlenken → # JEDES Katalog-Objekt mit glb-Eintrag gilt als fehlend, egal wie die # Umgebung aussieht. (Leere Liste greift nicht — quellen() fällt auf # DEFAULT_QUELLEN zurück.) var alter_cache: Dictionary = GlbMeshStore._cache.duplicate() GlbMeshStore._cache.clear() ProjectSettings.set_setting(GLB_QUELLEN_SETTING, ["/nonexistent_glb_lager_u4/assets/objekte"]) var fehlt_oid := "" for oid in (main.manifest.get("objects", {}) as Dictionary).keys(): var entry: Dictionary = (main.manifest.get("objects", {}) as Dictionary).get(oid, {}) if not str(entry.get("glb", "")).is_empty(): fehlt_oid = str(oid) break _check(not fehlt_oid.is_empty(), "Katalog hat glb-Objekte (Probe: %s)" % fehlt_oid) if fehlt_oid.is_empty(): ProjectSettings.set_setting(GLB_QUELLEN_SETTING, GlbMeshStore.DEFAULT_QUELLEN) return _check(GlbMeshStore.fehlt(fehlt_oid), "GlbMeshStore.fehlt ohne Lager = true (%s)" % fehlt_oid) _check(assets._tile_is_missing(fehlt_oid, main.manifest.get("objects", {}).get(fehlt_oid, {})), "_tile_is_missing(%s) = true ohne Quellen" % fehlt_oid) _check(not assets._tile_is_missing("box_001", main.manifest.get("objects", {}).get("box_001", {})), "Primitive ohne glb-Eintrag gelten NIE als fehlend (box_001)") var probe_grid := GridContainer.new() root.add_child(probe_grid) var tile: Button = assets._build_tile(fehlt_oid) probe_grid.add_child(tile) var warn: Label = null var dim_thumb := false var stack: Array = [tile] while not stack.is_empty(): var node: Node = stack.pop_back() if node is Label and (node as Label).text == "Modell fehlt": warn = node if node is TextureRect and (node as TextureRect).modulate.a < 0.9: dim_thumb = true for c in node.get_children(): stack.push_back(c) _check(warn != null, "Kachel trägt Warn-Label „Modell fehlt“") _check(dim_thumb, "Vorschaubild bei fehlendem Modell abgedunkelt") _check(tile.tooltip_text.contains("MODELL FEHLT"), "Tooltip nennt das fehlende Modell") probe_grid.queue_free() ProjectSettings.set_setting(GLB_QUELLEN_SETTING, GlbMeshStore.DEFAULT_QUELLEN) GlbMeshStore._cache = alter_cache # ------------------------------------------------------- 5 Auswahl-Signal ---- func _test_auswahl_signal() -> void: print("Testgruppe: Auswahl-Signal genau einmal je neuer Auswahl") selected_count = 0 assets._select_tile("box_001") _check(assets.selected_oid == "box_001", "selected_oid gesetzt") _check(selected_count == 1, "1. Auswahl → genau 1 Signal (ist %d)" % selected_count) assets._select_tile("box_001") _check(selected_count == 1, "gleiche Kachel nochmal → KEIN zweites Signal (ist %d)" % selected_count) assets._select_tile("box_002") _check(selected_count == 2, "Wechsel → 2. Signal (ist %d)" % selected_count) var sel_btn := main.library_buttons.get("box_002") as Button _check(sel_btn != null and sel_btn.modulate.a > 0.95, "gewählte Kachel voll sichtbar") var unsel_btn := main.library_buttons.get("box_001") as Button _check(unsel_btn != null and unsel_btn.modulate.a < 0.95, "andere Kachel abgedimmt") # --------------------------------------------------------------- 6 Hover ---- func _test_hover() -> void: print("Testgruppe: Hover zeigt die ID größer") var btn := main.library_buttons.get("box_002") as Button var id_lbl_normal: int = assets._id_font_normal assets._set_tile_hover("box_002", true) var id_lbl: Label = assets._find_id_label(btn) _check(id_lbl != null, "Kachel hat ID-Label") if id_lbl != null: var gross: int = id_lbl.get_theme_font_size("font_size") assets._set_tile_hover("box_002", false) var klein: int = id_lbl.get_theme_font_size("font_size") _check(gross > klein, "Hover vergrößert die ID (%d > %d)" % [gross, klein]) _check(klein == id_lbl_normal, "ohne Hover wieder Normalgröße (%d)" % klein) # --------------------------------------------------------- 7 Doppelklick ---- func _test_doppelklick() -> void: print("Testgruppe: Doppelklick startet Werkzeug wie bisher, nochmal = aus") activated_count = 0 assets._activate_tile("box_001") _check(main.interaction.placing and main.interaction.place_oid == "box_001", "Doppelklick: Platzier-Werkzeug läuft für box_001") _check(activated_count == 1, "object_activated genau 1× (ist %d)" % activated_count) assets._activate_tile("box_001") _check(not main.interaction.placing, "nochmal dieselbe Kachel: Platzieren abgebrochen") assets._activate_tile("box_002") _check(main.interaction.placing and main.interaction.place_oid == "box_002", "Wechsel per Doppelklick auf box_002") main.interaction.cancel_placement() _check(not main.interaction.placing, "aufgeräumt: kein Platzieren mehr aktiv") # --------------------------------------------------- 8 Zuletzt benutzt ---- func _test_zuletzt_persistenz() -> void: print("Testgruppe: „Zuletzt benutzt“ — cfg + Neu-Instanz") var cfg := ConfigFile.new() cfg.load("user://scopedvr_editor.cfg") var lst: Array = cfg.get_value("library", "zuletzt_benutzt", []) _check(lst.size() >= 2, "cfg [library] zuletzt_benutzt gefüllt (%d)" % lst.size()) _check(str(lst[0]) == "box_002", "neueste Benutzung zuerst (ist %s)" % str(lst[0])) var chip := assets._cat_buttons.get("zuletzt") as Button _check(chip != null and chip.visible, "Zuletzt-Chip jetzt sichtbar") _check(chip.text.contains("2"), "Zuletzt-Chip zählt Einträge (%s)" % chip.text) # Ansicht: Kategorie „zuletzt" zeigt nur die Gemerkten. assets._select_category("zuletzt") _check(assets._recent_box.visible, "Zuletzt-Ansicht sichtbar") var tiles := 0 for oid in assets._recent_ids: if assets._recent_tiles.has(str(oid)): tiles += 1 _check(tiles == 2, "Zuletzt-Ansicht zeigt 2 Kacheln (ist %d)" % tiles) # Neu-Instanz = „Neustart": Chip + Liste kommen zurück. main.queue_free() await process_frame await process_frame main = (load("res://scenes/main.tscn") as PackedScene).instantiate() root.add_child(main) await process_frame await process_frame assets = main.assets_mod var chip2 := assets._cat_buttons.get("zuletzt") as Button _check(chip2 != null and chip2.visible, "nach Neu-Instanz: Zuletzt-Chip sichtbar") _check(assets._recent_ids.size() == 2, "nach Neu-Instanz: 2 gemerkte IDs (ist %d)" % assets._recent_ids.size()) _check(str(assets._recent_ids[0]) == "box_002", "Reihenfolge hält den Neustart")