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:
@@ -0,0 +1,120 @@
|
||||
# test_phase4b_terrain.gd
|
||||
# Headless checks for Phase 4b terrain drag-painting batch commit semantics
|
||||
# (spec §2.6.4/2.6.5 + §5.5/5.6):
|
||||
# 1. Dragging across an EMPTY open region commits one block per path cell.
|
||||
# 2. Dragging over an existing same-type block commits ZERO new blocks
|
||||
# (same-type overlap skip; advisory dictionary, no double-create).
|
||||
# 3. A cell occupied by a conflicting prop is skipped while its empty
|
||||
# neighbours still spawn (Case C).
|
||||
# 4. A drag never leaves ghost terrain behind in the World.
|
||||
#
|
||||
# Run with:
|
||||
# & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase4b_terrain.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_BLOCK := preload("res://scripts/terrain_block.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("")
|
||||
print("========================================================")
|
||||
print(" PHASE 4b TERRAIN DRAG COMMIT TEST (headless)")
|
||||
print("========================================================")
|
||||
var stage: Node2D = STAGE_SCENE.instantiate()
|
||||
root.add_child(stage)
|
||||
var world: Node2D = stage._world
|
||||
|
||||
# --- Empty-region drag: 3 path cells -> 3 blocks ---
|
||||
var before := _terrain_count(world)
|
||||
stage.set_mode(stage.StageMode.EDIT)
|
||||
stage.set_placement_mode("ground")
|
||||
stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(0, 0)))
|
||||
stage._update_terrain_drag(stage._terrain_cell_center(Vector2i(2, 0)))
|
||||
_check(stage._drag_cells.size() == 3, "path has 3 cells (got %d)" % stage._drag_cells.size())
|
||||
stage._commit_terrain_drag()
|
||||
var after_empty := _terrain_count(world)
|
||||
_check(after_empty - before == 3, "empty drag committed 3 blocks (got %d)" % (after_empty - before))
|
||||
|
||||
# --- Same-type overlap drag: over a region now occupied by ground -> 0 ---
|
||||
var b2 := _terrain_count(world)
|
||||
stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(0, 0)))
|
||||
stage._update_terrain_drag(stage._terrain_cell_center(Vector2i(2, 0)))
|
||||
stage._commit_terrain_drag()
|
||||
var a2 := _terrain_count(world)
|
||||
_check(a2 - b2 == 0, "same-type overlap drag committed 0 blocks (got %d)" % (a2 - b2))
|
||||
|
||||
# --- Conflicting-object skip: put a crate in a far empty region, drag over it ---
|
||||
# With block stride (Ground = 200x32), the crate's 48x48 AABB covers exactly
|
||||
# one block cell (70,0), not a 16-px span.
|
||||
var crate := _spawn_crate(world, stage._terrain_cell_center(Vector2i(70, 0)))
|
||||
stage._rebuild_grid_cells()
|
||||
var b3 := _terrain_count(world)
|
||||
# Drag cells (65,0)..(75,0): the middle cell (70,0) conflict-red, edges empty.
|
||||
stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(65, 0)))
|
||||
stage._update_terrain_drag(stage._terrain_cell_center(Vector2i(75, 0)))
|
||||
_check(stage._drag_cells.size() == 11, "conflict drag path has 11 cells (got %d)" % stage._drag_cells.size())
|
||||
var cls_edge: int = stage._classify_cell(Vector2i(65, 0))
|
||||
var cls_mid: int = stage._classify_cell(Vector2i(70, 0))
|
||||
_check(cls_mid == 3, "crate cell classified conflict (3) (got %d)" % cls_mid)
|
||||
_check(cls_edge == 1, "edge cell (65,0) empty (1) (got %d)" % cls_edge)
|
||||
stage._commit_terrain_drag()
|
||||
var a3 := _terrain_count(world)
|
||||
# Path length 11 minus the single crate cell that gets skipped = 10 commits.
|
||||
_check(a3 - b3 == 10, "conflict drag committed only empty cells (expected 10, got %d)" % (a3 - b3))
|
||||
|
||||
# --- No ghost terrain leaked into World ---
|
||||
var ghost_blocks := 0
|
||||
for child: Node in world.get_children():
|
||||
if child is TERRAIN_BLOCK:
|
||||
ghost_blocks += 1
|
||||
_check(ghost_blocks == a3, "World contains only committed blocks (got %d, expected %d)" % [ghost_blocks, a3])
|
||||
|
||||
crate.queue_free()
|
||||
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 _spawn_crate(world: Node2D, pos: Vector2) -> Node2D:
|
||||
var crate = preload("res://scripts/prop_block.gd").new()
|
||||
crate.name = "Crate"
|
||||
crate.polygon_points = PackedVector2Array([
|
||||
Vector2(-24, -24), Vector2(24, -24), Vector2(24, 24), Vector2(-24, 24),
|
||||
])
|
||||
crate.position = pos
|
||||
world.add_child(crate)
|
||||
return crate
|
||||
|
||||
|
||||
func _terrain_count(world: Node2D) -> int:
|
||||
var n := 0
|
||||
for child: Node in world.get_children():
|
||||
if child is TERRAIN_BLOCK:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print("PASS: " + message)
|
||||
else:
|
||||
_failures += 1
|
||||
print("FAIL: " + message)
|
||||
Reference in New Issue
Block a user