Add headless regression tests for Phase 4b features

- 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.
This commit is contained in:
2026-09-04 15:08:08 -04:00
parent d77b5e5441
commit 1f91f3d2e5
54 changed files with 7960 additions and 162 deletions
+101
View File
@@ -0,0 +1,101 @@
# test_phase4b_grid_dirty.gd
# Headless checks for the Phase 4b grid spatial dictionary lifecycle and the
# WS3 TriggerArea-move redraw bugfix:
# 1. After placing a block, its cells are in the dictionary.
# 2. Moving a block via transform_committed moves its occupancy (old cells gone).
# 3. Deleting a block removes its cells (no stale entries).
# 4. _on_transform_committed marks StageDirectorVisuals dirty (TriggerArea move
# refreshes the dashed rule connector).
#
# Run with:
# & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase4b_grid_dirty.gd --path .
#
# Prints PASS/FAIL per assertion and exits 0 on all PASS, 1 on any FAIL.
extends SceneTree
const STAGE_SCENE := preload("res://scenes/sandbox_stage.tscn")
const TERRAIN_UTILS := preload("res://scripts/terrain_utils.gd")
const STAGE_SPAWNER := preload("res://scripts/stage_spawner.gd")
const TRIGGER_AREA := preload("res://scripts/trigger_area.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("")
print("========================================================")
print(" PHASE 4b GRID DICT / DIRTY-FLAG TEST (headless)")
print("========================================================")
var stage: Node2D = STAGE_SCENE.instantiate()
root.add_child(stage)
var world: Node2D = stage._world
# --- Place a ground block at cell (0,0) through the spawner path ---
var block = stage._spawner.spawn("ground", stage._terrain_cell_center(Vector2i(0, 0)))
_check(block != null, "ground block spawns")
_check((block as TerrainBlock).spawn_id == "ground", "terrain block carries spawn_id 'ground'")
stage._rebuild_grid_cells()
var cell00 := Vector2i(0, 0)
_check(not stage._grid_cells.get(cell00, []).is_empty(),
"cell (0,0) occupied after place")
# --- Move it +400 x via the committed-transform path ---
block.position += Vector2(400.0, 0.0)
var moved_nodes: Array[Node2D] = [block as Node2D]
stage._on_transform_committed(moved_nodes)
_check(stage._grid_cells.get(cell00, []).is_empty(),
"old cell (0,0) no longer occupied after move")
var new_cell := Vector2i(25, 0)
_check(not stage._grid_cells.get(new_cell, []).is_empty(),
"new cell (%s) occupied after move" % new_cell)
# --- Delete it via the stage delete path ---
stage._selection.select_only(block)
stage.delete_selected()
_check(stage._grid_cells.get(new_cell, []).is_empty(),
"cell (%s) cleared after delete (no stale entries)" % new_cell)
await process_frame
# --- TriggerArea move -> director visuals mark_dirty ---
var visuals = stage._director_visuals
visuals._dirty = false
var area: Node2D = TRIGGER_AREA.new()
area.position = Vector2(0.0, 0.0)
world.add_child(area)
var rules_arr: Array[Dictionary] = [{
"id": 1,
"trigger": { "type": "entered_area", "source": 0, "target": area.get_instance_id(), "params": {} },
"actions": [],
}]
visuals.set_rules(rules_arr)
visuals._dirty = false
area.position += Vector2(120.0, -40.0)
var moved_area: Array[Node2D] = [area as Node2D]
stage._on_transform_committed(moved_area)
_check(visuals._dirty == true,
"_on_transform_committed marks director visuals dirty (TriggerArea move)")
stage.queue_free()
await process_frame
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 _check(condition: bool, message: String) -> void:
_checks += 1
if condition:
print("PASS: " + message)
else:
_failures += 1
print("FAIL: " + message)