Files
stickman/tests/test_prop_editor_round3.gd
T
ryan 00abacc7b2 Add regression tests for prop attachment, centering, and editor UX
- Implemented test_phase9a_prop_attach.gd to validate anchor schema, alignment math, and preview isolation.
- Implemented test_phase9b_prop_centering.gd to check canonical origin presentation, bounds, centering, and save threshold.
- Implemented test_phase9c_prop_editor_ux.gd to assess the user experience in the prop editor, including anchor renaming and UI responsiveness.
- Added corresponding .uid files for each test.
- Created sync_sources.py script to manage markdown file synchronization with the notebook.
- Added .gitignore for virtual environment files.
2026-09-21 16:48:24 -04:00

91 lines
4.2 KiB
GDScript

# 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("attach label", PROP_EDITOR.mode_button_text(2) == "📌 Attach")
check("out-of-range clamps to last", PROP_EDITOR.mode_button_text(99) == "📌 Attach")
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")