- Implement test for popup anchor behavior in rule-builder menus to ensure consistent anchor positioning during menu transitions. - Create tests for stage logic, including mode transitions, toolbar visibility, and status bar updates. - Add terrain drag-painting tests to verify correct block placement behavior and conflict handling. - Introduce walk waypoint tests to check for arrival conditions and position stability after navigation.
290 lines
11 KiB
GDScript
290 lines
11 KiB
GDScript
# test_phase3b_library.gd
|
|
# Headless tests for Phase 3b Asset Library (no pixel rendering):
|
|
# 1. StickmanLibrary.scan() returns 3 entries (test/basic/break) sorted by
|
|
# display name; empty stickman_name falls back to the filename basename.
|
|
# 2. Corrupt / body-parts-less .stk files are skipped (scan dir override).
|
|
# 3. make_entry() valid path -> name+data; missing/corrupt path -> {}.
|
|
# 4. PropLibrary.get_entries() -> 4 templates; get_default_id() == "crate".
|
|
# 5. StageSpawner registry ids == [ground, ramp, step, prop, stickman, area].
|
|
# 6. _spawn_prop honors selected_prop_id (crate polygon / ball circle).
|
|
# 7. _spawn_stickman honors selected_stickman_path (basic.stk spawns non-null).
|
|
# 8. ThumbnailCache key/png formatting.
|
|
# 9. AssetSelector pagination math (PAGE_SIZE == 12; page_bounds slicing).
|
|
# 10. Scene load checks (sandbox_stage.tscn + asset_selector.tscn parse/load).
|
|
#
|
|
# Run with:
|
|
# & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase3b_library.gd --path .
|
|
#
|
|
# Prints PASS/FAIL per assertion and exits 0 on all PASS, 1 on any FAIL.
|
|
|
|
extends SceneTree
|
|
|
|
const STICKMAN_LIBRARY := preload("res://scripts/stickman_library.gd")
|
|
const PROP_LIBRARY := preload("res://scripts/prop_library.gd")
|
|
const THUMBNAIL_CACHE := preload("res://scripts/thumbnails/thumbnail_cache.gd")
|
|
const ASSET_SELECTOR := preload("res://scripts/asset_selector.gd")
|
|
const STAGE_SPAWNER := preload("res://scripts/stage_spawner.gd")
|
|
const PROP_BLOCK := preload("res://scripts/prop_block.gd")
|
|
const RIG := preload("res://scripts/stickman_rig.gd")
|
|
const STAGE_SCENE := preload("res://scenes/sandbox_stage.tscn")
|
|
const ASSET_SELECTOR_SCENE := preload("res://scenes/asset_selector.tscn")
|
|
|
|
const TEST_DIR := "user://phase3b_test"
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("")
|
|
print("========================================================")
|
|
print(" PHASE 3b ASSET LIBRARY TEST (headless)")
|
|
print("========================================================")
|
|
|
|
_test_scan()
|
|
_test_corrupt_skip()
|
|
_test_make_entry()
|
|
_test_prop_library()
|
|
_test_spawner_registry()
|
|
_test_spawn_prop()
|
|
_test_spawn_stickman()
|
|
_test_thumbnail_cache()
|
|
_test_pagination()
|
|
_test_scene_loads()
|
|
|
|
print("--------------------------------------------------------")
|
|
if _failures == 0:
|
|
print("RESULT: ALL PASSED (%d assertions, 0 failures)" % _checks)
|
|
quit(0)
|
|
else:
|
|
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
|
quit(1)
|
|
|
|
|
|
func _test_scan() -> void:
|
|
print("")
|
|
print("--- StickmanLibrary.scan() ---")
|
|
var lib = STICKMAN_LIBRARY.new()
|
|
var entries: Array[Dictionary] = lib.scan()
|
|
_check(entries.size() == 3, "scan returns 3 entries (got %d)" % entries.size())
|
|
var names: Array[String] = []
|
|
for e: Dictionary in entries:
|
|
names.append(String(e["name"]))
|
|
_check(names == ["Basic", "break", "test"],
|
|
"entries sorted by display name (got %s)" % str(names))
|
|
_check(String(_by_path(entries, "basic.stk")["name"]) == "Basic",
|
|
"basic.stk display name is 'Basic' (stickman_name)")
|
|
_check(String(_by_path(entries, "test.stk")["name"]) == "test",
|
|
"test.stk falls back to filename basename")
|
|
_check(String(_by_path(entries, "break.stk")["name"]) == "break",
|
|
"break.stk falls back to filename basename")
|
|
_check(not _by_path(entries, "basic.stk").is_empty(), "basic.stk entry has data")
|
|
var found: Dictionary = lib.find_by_path("res://stickmen/basic.stk")
|
|
_check(not found.is_empty() and String(found["name"]) == "Basic",
|
|
"find_by_path resolves basic.stk")
|
|
|
|
|
|
func _test_corrupt_skip() -> void:
|
|
print("")
|
|
print("--- Corrupt / body-parts-less .stk skipped ---")
|
|
_ensure_test_dir()
|
|
_write_file(TEST_DIR + "/good.stk", "{\"stickman_name\":\"Good\",\"body_parts\":{}}")
|
|
_write_file(TEST_DIR + "/corrupt.stk", "{ not valid json !!!")
|
|
_write_file(TEST_DIR + "/nobody.stk", "{\"version\":\"1.5\"}")
|
|
var lib = STICKMAN_LIBRARY.new()
|
|
var entries: Array[Dictionary] = lib.scan(TEST_DIR)
|
|
_check(entries.size() == 1,
|
|
"scan skips corrupt + body-parts-less files (got %d)" % entries.size())
|
|
if entries.size() == 1:
|
|
_check(String(entries[0]["name"]) == "Good",
|
|
"only the valid file survives (name='%s')" % String(entries[0]["name"]))
|
|
_clean_test_dir()
|
|
|
|
|
|
func _test_make_entry() -> void:
|
|
print("")
|
|
print("--- make_entry() ---")
|
|
var lib = STICKMAN_LIBRARY.new()
|
|
var entry := lib.make_entry("res://stickmen/basic.stk")
|
|
_check(not entry.is_empty(), "make_entry on valid path returns an entry")
|
|
_check(String(entry["name"]) == "Basic", "make_entry name == 'Basic'")
|
|
_check(not (entry["data"] as Dictionary).is_empty(), "make_entry carries parsed data")
|
|
_check(lib.make_entry("res://stickmen/does_not_exist.stk").is_empty(),
|
|
"make_entry on missing path returns {}")
|
|
_ensure_test_dir()
|
|
_write_file(TEST_DIR + "/bad.stk", "nope nope")
|
|
_check(lib.make_entry(TEST_DIR + "/bad.stk").is_empty(),
|
|
"make_entry on corrupt file returns {}")
|
|
_clean_test_dir()
|
|
|
|
|
|
func _test_prop_library() -> void:
|
|
print("")
|
|
print("--- PropLibrary ---")
|
|
var entries: Array[Dictionary] = PROP_LIBRARY.get_entries()
|
|
_check(entries.size() == 4, "4 prop templates (got %d)" % entries.size())
|
|
_check(PROP_LIBRARY.get_default_id() == "crate", "default prop id == 'crate'")
|
|
var ids: Array[String] = PROP_LIBRARY.get_ids()
|
|
_check(ids == ["crate", "ball", "plank", "triangle"],
|
|
"prop ids in order (got %s)" % str(ids))
|
|
var crate: Dictionary = PROP_LIBRARY.get_entry("crate")
|
|
_check(int(crate["material_preset"]) == PROP_BLOCK.MaterialPreset.WOOD,
|
|
"crate material preset == WOOD")
|
|
_check(String(crate["material_label"]) == "Wood", "crate material label == 'Wood'")
|
|
var ball: Dictionary = PROP_LIBRARY.get_entry("ball")
|
|
_check(int(ball["material_preset"]) == PROP_BLOCK.MaterialPreset.RUBBER,
|
|
"ball material preset == RUBBER")
|
|
_check(PROP_LIBRARY.get_entry("nope").is_empty(), "unknown prop id returns {}")
|
|
|
|
|
|
func _test_spawner_registry() -> void:
|
|
print("")
|
|
print("--- StageSpawner registry ---")
|
|
var world := Node2D.new()
|
|
root.add_child(world)
|
|
var spawner = STAGE_SPAWNER.new(world)
|
|
var ids: Array[String] = spawner.get_spawnable_ids()
|
|
_check(ids == ["ground", "ramp", "step", "prop", "stickman", "area"],
|
|
"registry ids == [ground, ramp, step, prop, stickman, area] (got %s)" % str(ids))
|
|
_check(not ids.has("crate") and not ids.has("ball"),
|
|
"crate/ball registry entries removed")
|
|
_check(spawner.get_selected_prop_id() == "crate", "default selected_prop_id == 'crate'")
|
|
_check(spawner.get_selected_stickman_path() == "res://stickmen/test.stk",
|
|
"default selected_stickman_path == test.stk")
|
|
world.queue_free()
|
|
|
|
|
|
func _test_spawn_prop() -> void:
|
|
print("")
|
|
print("--- _spawn_prop honors selected_prop_id ---")
|
|
var world := Node2D.new()
|
|
root.add_child(world)
|
|
var spawner = STAGE_SPAWNER.new(world)
|
|
var crate: Node2D = spawner.spawn("prop", Vector2.ZERO)
|
|
_check(crate != null, "prop spawns (default crate)")
|
|
_check(crate is PROP_BLOCK, "default prop is a PropBlock")
|
|
_check((crate as PROP_BLOCK).shape_type == PROP_BLOCK.ShapeType.POLYGON,
|
|
"default crate is a POLYGON prop")
|
|
spawner.selected_prop_id = "ball"
|
|
var ball: Node2D = spawner.spawn("prop", Vector2(100, 0))
|
|
_check(ball != null, "ball prop spawns after selected_prop_id = 'ball'")
|
|
_check((ball as PROP_BLOCK).shape_type == PROP_BLOCK.ShapeType.CIRCLE,
|
|
"ball prop is a CIRCLE prop")
|
|
world.queue_free()
|
|
|
|
|
|
func _test_spawn_stickman() -> void:
|
|
print("")
|
|
print("--- _spawn_stickman honors selected_stickman_path ---")
|
|
var world := Node2D.new()
|
|
root.add_child(world)
|
|
var spawner = STAGE_SPAWNER.new(world)
|
|
spawner.selected_stickman_path = "res://stickmen/basic.stk"
|
|
var rig: Node2D = spawner.spawn("stickman", Vector2.ZERO)
|
|
_check(rig != null, "stickman spawns from basic.stk")
|
|
_check(rig is RIG, "basic.stk spawns a StickmanRig")
|
|
world.queue_free()
|
|
|
|
|
|
func _test_thumbnail_cache() -> void:
|
|
print("")
|
|
print("--- ThumbnailCache key/png formatting ---")
|
|
var cache = THUMBNAIL_CACHE.new()
|
|
var mtime: int = FileAccess.get_modified_time("res://stickmen/test.stk")
|
|
var key := cache.stickman_key("res://stickmen/test.stk")
|
|
_check(key == "test_%d" % mtime, "stickman_key embeds basename + mtime (got '%s')" % key)
|
|
_check(cache.stickman_png(key) == "user://thumbnails/stickmen/" + key + ".png",
|
|
"stickman_png path formatting (got '%s')" % cache.stickman_png(key))
|
|
_check(cache.prop_png("crate") == "user://thumbnails/props/crate_v" + str(THUMBNAIL_CACHE.PROP_VERSION) + ".png",
|
|
"prop_png embeds id + version (got '%s')" % cache.prop_png("crate"))
|
|
_check(THUMBNAIL_CACHE.PROP_VERSION == 1, "PROP_VERSION == 1")
|
|
_check(cache.load_png("user://does_not_exist_phase3b.png") == null,
|
|
"load_png on missing file returns null")
|
|
|
|
|
|
func _test_pagination() -> void:
|
|
print("")
|
|
print("--- AssetSelector pagination ---")
|
|
_check(ASSET_SELECTOR.PAGE_SIZE == 12, "PAGE_SIZE == 12 (got %d)" % ASSET_SELECTOR.PAGE_SIZE)
|
|
_check(ASSET_SELECTOR.COLUMNS == 4 and ASSET_SELECTOR.ROWS == 3,
|
|
"COLUMNS=4, ROWS=3")
|
|
var b0: Dictionary = ASSET_SELECTOR.page_bounds(20, 0)
|
|
_check(int(b0["start"]) == 0 and int(b0["end"]) == 12 and int(b0["page_count"]) == 2,
|
|
"page 0 slices [0,12) of 20, page_count=2")
|
|
var b1: Dictionary = ASSET_SELECTOR.page_bounds(20, 1)
|
|
_check(int(b1["start"]) == 12 and int(b1["end"]) == 20,
|
|
"page 1 slices [12,20) of 20")
|
|
var b4: Dictionary = ASSET_SELECTOR.page_bounds(4, 0)
|
|
_check(int(b4["start"]) == 0 and int(b4["end"]) == 4 and int(b4["page_count"]) == 1,
|
|
"4 entries -> single page")
|
|
|
|
|
|
func _test_scene_loads() -> void:
|
|
print("")
|
|
print("--- Scene load checks ---")
|
|
_check(load("res://scenes/sandbox_stage.tscn") != null, "sandbox_stage.tscn loads")
|
|
_check(load("res://scenes/asset_selector.tscn") != null, "asset_selector.tscn loads")
|
|
var selector: AssetSelector = ASSET_SELECTOR_SCENE.instantiate() as AssetSelector
|
|
_check(selector != null, "asset_selector.tscn instantiates as AssetSelector")
|
|
root.add_child(selector)
|
|
_check(selector._title_label != null and selector._grid != null,
|
|
"asset_selector unique-name markers resolve")
|
|
_check(selector._browse_button != null and selector._refresh_button != null,
|
|
"asset_selector footer buttons resolve")
|
|
selector.queue_free()
|
|
await process_frame
|
|
var stage: Node2D = STAGE_SCENE.instantiate()
|
|
root.add_child(stage)
|
|
_check(stage._selector != null, "stage builds the AssetSelector")
|
|
_check(stage._stickman_library != null, "stage builds the StickmanLibrary")
|
|
_check(stage._stickman_thumb != null and stage._prop_thumb != null,
|
|
"stage builds the thumbnail renderers")
|
|
_check(stage._spawner.get_spawnable_ids() == ["ground", "ramp", "step", "prop", "stickman", "area"],
|
|
"stage spawner registry excludes crate/ball")
|
|
stage.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _by_path(entries: Array[Dictionary], suffix: String) -> Dictionary:
|
|
for e: Dictionary in entries:
|
|
if String(e["path"]).ends_with(suffix):
|
|
return e
|
|
return {}
|
|
|
|
|
|
func _ensure_test_dir() -> void:
|
|
DirAccess.make_dir_recursive_absolute(TEST_DIR)
|
|
|
|
|
|
func _write_file(path: String, text: String) -> void:
|
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
|
if f != null:
|
|
f.store_string(text)
|
|
f.close()
|
|
|
|
|
|
func _clean_test_dir() -> void:
|
|
var dir := DirAccess.open(TEST_DIR)
|
|
if dir != null:
|
|
dir.list_dir_begin()
|
|
var fname := dir.get_next()
|
|
while fname != "":
|
|
if not dir.current_is_dir():
|
|
DirAccess.remove_absolute(TEST_DIR + "/" + fname)
|
|
fname = dir.get_next()
|
|
dir.list_dir_end()
|
|
DirAccess.remove_absolute(TEST_DIR)
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print("PASS: " + message)
|
|
else:
|
|
_failures += 1
|
|
print("FAIL: " + message)
|