# Phase 9 Prop Editor Round 3 - headless regression checks for the pure static # helpers (window title, mode button text) and PropFile.save_safe atomic writes. # Run with: # & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --path "C:\tmp\stickman" --script "res://tests/test_prop_editor_round3.gd" extends SceneTree const PROP_EDITOR := preload("res://scripts/prop_editor.gd") const PROP_FILE := preload("res://scripts/prop_file.gd") var _checks: int = 0 var _failures: int = 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("\n=== PHASE 9 PROP EDITOR ROUND 3 TEST ===") await _test_window_title() await _test_mode_button_text() await _test_save_safe() await _test_recent_list() print("----------------------------------------") if _failures == 0: print("RESULT: ALL PASSED (%d assertions)" % _checks) quit(0) else: print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks]) quit(1) func check(label: String, condition: bool) -> void: _checks += 1 if condition: print("PASS: %s" % label) else: _failures += 1 print("FAIL: %s" % label) func _test_window_title() -> void: check("untitled no path no dirty", PROP_EDITOR.build_window_title("", "", false) == "Untitled") check("name dirty", PROP_EDITOR.build_window_title("My Crate", "", true) == "My Crate *") check("name path clean", PROP_EDITOR.build_window_title("My Crate", "res://props/mycrate.prop", false) == "My Crate — mycrate.prop") check("untitled path dirty", PROP_EDITOR.build_window_title("", "res://props/mycrate.prop", true) == "Untitled — mycrate.prop *") check("whitespace name falls back", PROP_EDITOR.build_window_title(" ", "", false) == "Untitled") check("extension kept in title", PROP_EDITOR.build_window_title("X", "res://props/ball.prop", false).ends_with("ball.prop")) func _test_mode_button_text() -> void: check("arrange label", PROP_EDITOR.mode_button_text(0) == "🖐️ Arrange") check("reshape label", PROP_EDITOR.mode_button_text(1) == "✏️ Reshape") check("out-of-range clamps to last", PROP_EDITOR.mode_button_text(99) == "✏️ Reshape") check("negative clamps to first", PROP_EDITOR.mode_button_text(-5) == "🖐️ Arrange") func _test_save_safe() -> void: var path := "user://round3_autosave_test.prop" var data := { "version": "1.0", "prop_name": "X", "material_preset": "none", "shapes": [], "metadata": {"autosave": true, "source_path": "", "modified_unix": 123}, } check("save_safe first write", PROP_FILE.save_safe(path, data)) var loaded: Dictionary = PROP_FILE.load(path) check("save_safe round trip", not loaded.is_empty() and loaded.get("prop_name", "") == "X" and bool((loaded.get("metadata", {}) as Dictionary).get("autosave", false))) check("no tmp leftover", not FileAccess.file_exists(path + ".tmp")) check("no bak leftover", not FileAccess.file_exists(path + ".bak")) data["prop_name"] = "Y" check("save_safe overwrite", PROP_FILE.save_safe(path, data)) var loaded2: Dictionary = PROP_FILE.load(path) check("overwrite persisted", loaded2.get("prop_name", "") == "Y") check("no leftovers after overwrite", not FileAccess.file_exists(path + ".tmp") and not FileAccess.file_exists(path + ".bak")) DirAccess.remove_absolute(path) func _test_recent_list() -> void: var list: Array = PROP_EDITOR.update_recent_list([], "res://props/a.prop", 5) check("recent empty list inserts", list == ["res://props/a.prop"]) list = PROP_EDITOR.update_recent_list(["res://props/b.prop", "res://props/a.prop"], "res://props/a.prop", 5) check("recent dedupe moves to front", list == ["res://props/a.prop", "res://props/b.prop"]) list = PROP_EDITOR.update_recent_list(["res://props/b.prop", "res://props/c.prop"], "res://props/a.prop", 5) check("recent insert keeps most-recent-first", list == ["res://props/a.prop", "res://props/b.prop", "res://props/c.prop"]) list = PROP_EDITOR.update_recent_list(["res://props/b.prop", "res://props/c.prop", "res://props/d.prop", "res://props/e.prop", "res://props/f.prop"], "res://props/a.prop", 5) check("recent trims to max", list.size() == 5 and list[0] == "res://props/a.prop" and list[4] == "res://props/e.prop")