GdUnit4 Scene Runner
The Scene Runner enables integration testing with input simulation.
Creating a Scene Runner
extends GdUnitTestSuite
var runner: GdUnitSceneRunner
func before_test() -> void:
runner = scene_runner("res://scenes/main.tscn")
func after_test() -> void:
runner.free()
Accessing the Scene
# Get the root node of the loaded scene
var game = runner.scene()
# Find child nodes
var player = runner.find_child("Player")
var ui = runner.find_child("UI/HUD")
Mouse Input
Position
# Set mouse position (viewport coordinates)
runner.set_mouse_position(Vector2(100, 200))
# Get current mouse position
var pos = runner.get_mouse_position()
Buttons
# Press (and hold) mouse button
runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)
# Release mouse button
runner.simulate_mouse_button_released(MOUSE_BUTTON_LEFT)
# Press and immediately release
runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT)
# Right click
runner.simulate_mouse_button_press(MOUSE_BUTTON_RIGHT)
# Middle click
runner.simulate_mouse_button_press(MOUSE_BUTTON_MIDDLE)
Click at Position
# Move and click
runner.set_mouse_position(Vector2(100, 200))
runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT)
await runner.await_input_processed()
Double Click
runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT, true) # double_click=true
Drag and Drop
# Start position
runner.set_mouse_position(Vector2(100, 100))
runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)
await runner.await_input_processed()
# Move while holding
runner.set_mouse_position(Vector2(300, 300))
await runner.await_input_processed()
# Release
runner.simulate_mouse_button_released(MOUSE_BUTTON_LEFT)
await runner.await_input_processed()
Keyboard Input
Single Keys
# Press key (and hold)
runner.simulate_key_pressed(KEY_SPACE)
# Release key
runner.simulate_key_released(KEY_SPACE)
# Press and release
runner.simulate_key_press(KEY_SPACE)
Key Codes
Common key codes:
KEY_SPACE,KEY_ENTER,KEY_ESCAPE,KEY_TABKEY_UP,KEY_DOWN,KEY_LEFT,KEY_RIGHTKEY_AthroughKEY_ZKEY_0throughKEY_9KEY_F1throughKEY_F12
Modifier Keys
# Ctrl+S (shift, ctrl, alt, meta)
runner.simulate_key_pressed(KEY_S, false, true, false, false)
# Shift+A
runner.simulate_key_pressed(KEY_A, true, false, false, false)
# Ctrl+Shift+Z
runner.simulate_key_pressed(KEY_Z, true, true, false, false)
Parameters: key, shift=false, ctrl=false, alt=false, meta=false
Input Actions
# Press action (as defined in Input Map)
runner.simulate_action_pressed("jump")
# Release action
runner.simulate_action_released("jump")
# Press and release
runner.simulate_action_press("jump")
Waiting
Essential Wait
Always wait after simulating input:
runner.simulate_key_press(KEY_SPACE)
await runner.await_input_processed() # Required!
Frame Waiting
# Wait one idle frame
await runner.await_idle_frame()
# Wait multiple frames
for i in range(10):
await runner.await_idle_frame()
Signal Waiting
# Wait for signal (with timeout)
await runner.await_signal("game_over", [], 5000) # 5 second timeout
# Wait for signal with arguments
await runner.await_signal("score_changed", [100], 2000)
Condition Waiting
# Wait until function returns expected value
var game = runner.scene()
await runner.await_func(game, "is_game_over").is_true()
# With timeout
await runner.await_func(game, "get_health").is_less(50).wait_until(3000)
Touch Input (Mobile)
# Touch press
runner.simulate_screen_touch_pressed(0, Vector2(100, 200)) # index, position
# Touch release
runner.simulate_screen_touch_released(0)
# Double tap
runner.simulate_screen_touch_pressed(0, Vector2(100, 200), true) # double_tap=true
Complete Example
extends GdUnitTestSuite
var runner: GdUnitSceneRunner
func before_test() -> void:
runner = scene_runner("res://scenes/game.tscn")
func after_test() -> void:
runner.free()
func test_player_moves_right() -> void:
await runner.await_idle_frame()
var player = runner.find_child("Player")
var initial_x = player.position.x
# Hold right arrow for 0.5 seconds
runner.simulate_key_pressed(KEY_RIGHT)
for i in range(30): # ~0.5 seconds at 60fps
await runner.await_idle_frame()
runner.simulate_key_released(KEY_RIGHT)
assert_that(player.position.x).is_greater(initial_x)
func test_button_click_starts_game() -> void:
await runner.await_idle_frame()
var start_button = runner.find_child("UI/StartButton")
var button_center = start_button.global_position + start_button.size / 2
runner.set_mouse_position(button_center)
runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT)
await runner.await_input_processed()
var game = runner.scene()
assert_that(game.is_game_active()).is_true()
func test_game_over_signal() -> void:
await runner.await_idle_frame()
var game = runner.scene()
game.trigger_game_over()
await runner.await_signal("game_over", [], 2000)
# If we get here, signal was emitted
Tips
- Always await - Input simulation is async; always use
await - Find nodes carefully - Use
find_child()which searches recursively - Set position first - Move mouse before clicking
- Use actions - Prefer
simulate_action_*over raw keys for game input - Handle timing - Some animations need multiple frames to complete