Testing Scenes & Input
Drive scenes with simulated input in headless tests using SceneRunner.
Some behaviour only happens when frames advance and input fires: a player walks into a pickup zone, a button is pressed, a timer expires. You cannot test these with plain assertions — the scene needs to run.
scene_runner() gives you a headless harness: instantiate a scene, pump frames, fire input events, and then assert on the result.
A complete example
A player walks into a health potion and the scene emits a collected signal. The test simulates movement, waits for frames, and checks the player's HP:
# test/test_potion_pickup.gd
extends GdTest
func test_potion_heals_player() -> void:
var runner = scene_runner(load("res://levels/test_room.tscn"))
var player = runner.scene.get_node("Player")
player.hp = 50
runner.simulate_action_pressed("move_right")
await runner.simulate_frames(20)
runner.simulate_action_released("move_right")
assert_eq(player.hp, 80)runner.scene is the instantiated root node. The runner adds it to the scene tree and frees it at the end of the test — no manual cleanup needed.
Advancing time
Use simulate_frames to advance process frames by count, or simulate_seconds to advance by game-time duration:
func test_respawn_timer() -> void:
var runner = scene_runner(load("res://player/player.tscn"))
var player = runner.scene
player.die()
await runner.simulate_seconds(3.0)
assert_true(player.is_alive())await runner.simulate_frames(10) # advance 10 process frames
await runner.simulate_seconds(1.5) # advance 1.5 seconds of game timeBoth must be awaited.
Simulating actions
Use simulate_action_pressed and simulate_action_released to fire InputMap actions:
func test_jump_increases_y_velocity() -> void:
var runner = scene_runner(load("res://player/player.tscn"))
var player = runner.scene
runner.simulate_action_pressed("jump")
await runner.simulate_frames(3)
runner.simulate_action_released("jump")
assert_lt(player.velocity.y, 0.0)Held actions are automatically released when the runner is freed — input state never leaks between tests.
Simulating keyboard input
runner.simulate_key_pressed(KEY_SPACE)
runner.simulate_key_pressed(KEY_Z, KEY_MASK_CTRL) # Ctrl+Z
runner.simulate_key_released(KEY_SPACE)Available modifier flags: KEY_MASK_CTRL, KEY_MASK_SHIFT, KEY_MASK_ALT.
Simulating mouse input
runner.simulate_mouse_move(Vector2(100, 200))
runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT, Vector2(100, 200))
runner.simulate_mouse_button_released(MOUSE_BUTTON_LEFT, Vector2(100, 200))Headless limitation: Action and keyboard events work in
--headlessmode. Mouse position events are routed by screen coordinates — aButtonhit-tested by mouse position requires a virtual display. For UI tests, preferInput.action_press/action_releaseover raw mouse events.
For the complete SceneRunner API — all method signatures and parameters — see SceneRunner API Reference.