## Scene testing utilities for gdUnit4 tests. ## ## Loads a scene, instantiates it into the test suite's node tree, ## and provides helpers for node existence, signal, and node-path queries. ## ## Usage (from a GdUnitTestSuite subclass): ## var helper := SceneHelper.create(self, "res://scenes/main.tscn") ## helper.assert_node_exists("World") ## var world := helper.get_node_at("World") ## helper.monitor_signal(world, "ready") ## # ... trigger something ... ## helper.assert_signal_emitted(world, "ready") ## ## Design constraints (D-030): server-free, no running autoload dependencies. class_name SceneHelper extends RefCounted var _suite # GdUnitTestSuite — untyped to avoid load-order dependency var _scene: Node # signal_key -> int. Key is ":" for uniqueness. var _signal_hits: Dictionary = {} ## Load, instantiate, and attach a scene to the test suite's node tree. ## The scene node is registered for auto-free by gdUnit4. ## Returns a helper instance; fails the test if the scene cannot be loaded. static func create(suite: GdUnitTestSuite, scene_path: String) -> SceneHelper: var helper := SceneHelper.new() helper._suite = suite var packed: PackedScene = load(scene_path) if packed == null: suite.assert_that(packed).override_failure_message( "SceneHelper: could not load scene at '%s'" % scene_path ).is_not_null() return helper helper._scene = packed.instantiate() suite.auto_free(helper._scene) suite.add_child(helper._scene) return helper ## Returns the scene root node. func scene() -> Node: return _scene ## Assert that a node at node_path exists under the scene root. ## Fails the current test if the node is absent. func assert_node_exists(node_path: String) -> void: var node := _scene.get_node_or_null(NodePath(node_path)) _suite.assert_that(node).override_failure_message( "SceneHelper: expected node at path '%s' — not found" % node_path ).is_not_null() ## Return the node at node_path under the scene root, or null if absent. func get_node_at(node_path: String) -> Node: return _scene.get_node_or_null(NodePath(node_path)) ## Begin tracking emissions of signal_name on node. ## Must be called before the action that triggers the signal. ## Fails the test if node does not have the named signal. func monitor_signal(node: Node, signal_name: String) -> void: if not node.has_signal(signal_name): _suite.assert_that(false).override_failure_message( "SceneHelper: node '%s' has no signal '%s'" % [node.name, signal_name] ).is_true() return var key := _signal_key(node, signal_name) _signal_hits[key] = 0 # Lambda accepts up to 4 positional args to tolerate signals with up to 4 params. # GDScript default-param lambdas handle being called with fewer args correctly. node.connect(signal_name, func(a := null, b := null, c := null, d := null): _signal_hits[key] = _signal_hits.get(key, 0) + 1 ) ## Assert that signal_name was emitted at least once since monitor_signal(). ## Fails the test if monitor_signal() was not called first, or if count is zero. func assert_signal_emitted(node: Node, signal_name: String) -> void: var key := _signal_key(node, signal_name) if not _signal_hits.has(key): _suite.assert_that(false).override_failure_message( "SceneHelper: '%s' was not monitored — call monitor_signal() first" % signal_name ).is_true() return var count: int = _signal_hits[key] _suite.assert_int(count).override_failure_message( "SceneHelper: signal '%s' on '%s' was not emitted (count=%d)" % [ signal_name, node.name, count ] ).is_greater(0) static func _signal_key(node: Node, signal_name: String) -> String: return "%d:%s" % [node.get_instance_id(), signal_name]