Testing¶
SimVX provides headless testing tools for game logic, UI widgets, and input: no GPU required.
For visual/pixel-level testing with the Vulkan renderer, see Visual Testing.
SceneRunner¶
SceneRunner drives the scene tree without a window. Load a node, advance frames, and inspect state:
from simvx.core.testing import SceneRunner
runner = SceneRunner(screen_size=(800, 600))
runner.load(MyGameScene())
runner.advance_frames(10)
player = runner.find("Player") # Find by name
enemies = runner.find_all(Enemy) # Find all of a type
assert player.health > 0
API¶
SceneRunner(screen_size=(800, 600))
runner.load(root_node) -> SceneRunner # Chainable
runner.advance_frames(count=1, dt=None) -> SceneRunner
runner.advance_time(seconds, dt=None) -> SceneRunner
runner.find(name_or_type, recursive=True) # str or type
runner.find_all(node_type) -> list[Node]
runner.snapshot() -> dict # Capture scene state
runner.root # Root node
runner.frame_count # Frames processed
runner.elapsed_time # Seconds elapsed
InputSimulator¶
InputSimulator drives the engine the same way real platform adapters do.
A single call writes Input state, fires @on_input decorators (via
tree.propagate_input), and routes through the UI tree (via
tree.ui_input), so widget tests, decorator tests, and polling tests all
see the click from one sim.click(pos):
from simvx.core.testing import InputSimulator, SceneRunner
from simvx.core import Key, MouseButton
sim = InputSimulator()
runner = SceneRunner()
runner.load(MyScene())
sim.tap_key(Key.SPACE) # Press + release
runner.advance_frames(1)
assert runner.find("Player").jumped
sim.click((400, 300)) # Left-click at position
sim.click((400, 300), MouseButton.RIGHT) # Right-click variant
runner.advance_frames(1)
sim.move_mouse(500, 400) # Move cursor
sim.press_key(Key.W) # Hold key
runner.advance_frames(60) # Hold for 60 frames
sim.release_key(Key.W)
sim.reset() # Clear all pressed state
The simulator finds the target SceneTree via SceneTree.current() (the
most-recently activated tree). When multiple trees coexist: e.g. the
editor opens a new scene tab during ready(): bind explicitly:
sim = InputSimulator(tree=my_tree)
API¶
Method |
Description |
|---|---|
|
Hold a key down ( |
|
Release a key |
|
Press now, release at the next frame boundary |
|
Press mouse button |
|
Release mouse button |
|
Press + release at position |
|
Move cursor |
|
Scroll wheel |
|
Hold or release a pad button |
|
Press now, release at the next frame boundary |
|
Set one axis on the typed state |
|
Publish a whole pad snapshot by name |
|
Clear all pressed state |
Keys go through InputRouter, the object every window backend feeds, so a
simulated key carries the modifier flags a real one does: press the modifier
first and the chord is spelled for you.
sim.press_key(Key.LEFT_CONTROL)
sim.press_key(Key.S) # reaches handlers and the UI as ctrl+s
set_gamepad writes the per-pad string-keyed state as a platform adapter does,
so Input.is_gamepad_pressed(0, "a") and Input.get_gamepad_axis(0, "lt") are
drivable from a test. Named entries merge into the pad’s current snapshot; an
unknown name raises.
sim.set_gamepad(0, buttons={"a": True}, axes={"lt": 0.5})
Scene Snapshots¶
Compare scene state before and after an operation:
from simvx.core.testing import SceneRunner, scene_diff
runner = SceneRunner().load(MyScene())
before = runner.snapshot()
runner.advance_frames(100)
after = runner.snapshot()
changes = scene_diff(before, after)
for change in changes:
print(change) # e.g. "Player.position: (0, 0) -> (100, 50)"
Describe¶
scene_describe() prints a human-readable tree of all nodes with their properties:
from simvx.core.testing import scene_describe
print(scene_describe(runner.root))
# MyScene
# Player (Node2D) position=(100, 50) health=3
# Sprite (Sprite2D)
# Enemy (Node2D) position=(400, 300)
UITestHarness¶
For widget-level testing without a GPU. Wraps a root Control with a mock scene tree and draw-command logger:
from simvx.core.ui.testing import UITestHarness
from simvx.core import Button, VBoxContainer
root = VBoxContainer()
btn = Button(text="Click Me")
root.add_child(btn)
harness = UITestHarness(root)
harness.click(btn) # Click by widget reference
assert btn.pressed_count > 0
harness.type_text("Hello") # Type into focused widget
harness.press_key("escape")
harness.scroll(dy=-3)
Draw Assertions¶
UITestHarness captures all draw commands in a DrawLog. Use it to verify rendering without a GPU:
harness.tick()
log = harness.draw_log
assert log.has_text("Click Me")
assert log.has_text_containing("Score")
texts = log.texts() # All rendered text strings
rects = log.rects_at(100, 50) # Rects drawn at a point
Utilities¶
Function/Class |
Description |
|---|---|
|
List of human-readable changes between snapshots |
|
Tree-format string of all nodes and properties |
|
UI-focused tree format |
|
Dict of |
|
Total node count |
|
Measure frame times ( |
Example self-checks and their exit codes¶
Examples carry a headless self-check behind --test. It has three possible
outcomes, and each gets its own process exit code:
Code |
Meaning |
|---|---|
|
The self-check ran and every claim held |
|
The self-check ran and something it asserts is broken |
|
The self-check could not run here: a capability it needs is absent |
Without the third, an example needing an optional package (or a device this
machine has not got) either fails as though the mechanic were broken or passes
as though the fallback it measured were the thing under test. Raise
Unsupported to say so, and let run_selftest map the outcome:
from simvx.core.testing import Unsupported, run_selftest
def _selftest() -> bool:
...
if backend != "JoltPhysics":
raise Unsupported("install simvx-physics-jolt to check the Jolt half")
print("SELFTEST:", "PASS" if ok else "FAIL")
return ok
if "--test" in sys.argv:
sys.exit(run_selftest(_selftest))
Report the checks that DID run before raising: a check that ran on a fallback
still says something, and a failure among them is a genuine failure, so raise
only once nothing else has gone wrong. An exception that is not Unsupported
propagates, because an unexpected traceback is worth seeing in full.
API Reference¶
See simvx.core.testing and simvx.core.ui.testing for the complete testing API.