simvx.core.testing

SimVX Testing API – headless scene testing, input simulation, and diagnostics.

Usage: from simvx.core.testing import SceneRunner, InputSimulator, scene_diff

# Test a game scene
runner = SceneRunner()
runner.load(MyGameScene())
runner.advance_frames(10)
assert runner.find("Player").position == Vec2(100, 200)

# Simulate input
runner.simulate_key_press("space")
runner.advance_frames(1)
assert runner.find("Player").velocity.y < 0  # Jumped

# Compare scene states
diff = scene_diff(before_state, after_state)
assert "Player.position" in diff.changed

Module Contents

Classes

SceneRunner

Headless scene runner for testing game logic without rendering.

InputSimulator

Simulate input events for headless testing.

NodeCounter

Count nodes by type in a scene tree. Useful for debugging and assertions.

FrameTimer

Measure frame timing for performance testing.

RecordedEvent

A single recorded input event.

TestRecorder

Records user input events and generates replayable test scripts.

Functions

scene_diff

Compare two scene snapshots and return a list of human-readable differences.

scene_describe

Return a tree-formatted text description of the scene hierarchy.

ui_describe

Return a tree-formatted text description of a UI widget hierarchy.

Data

API

simvx.core.testing.__all__

[‘SceneRunner’, ‘InputSimulator’, ‘scene_diff’, ‘NodeCounter’, ‘FrameTimer’, ‘scene_describe’, ‘ui_d…

class simvx.core.testing.SceneRunner(screen_size: tuple[float, float] = (800, 600))

Headless scene runner for testing game logic without rendering.

Manages a SceneTree, advances frames, and provides query helpers.

Initialization

load(root_node: simvx.core.engine.Node) simvx.core.testing.SceneRunner

Load a scene by setting the root node.

advance_frames(count: int = 1, dt: float | None = None, draw: bool = False) simvx.core.testing.SceneRunner

Advance the scene by N frames (process + physics_process each).

If draw is True, also calls tree.draw() with a mock renderer each frame. This exercises draw() methods and catches errors that would otherwise only surface with a real Vulkan renderer.

advance_time(seconds: float, dt: float | None = None) simvx.core.testing.SceneRunner

Advance the scene by approximately N seconds worth of frames.

find(name_or_type, recursive: bool = True) simvx.core.engine.Node | None

Find a node by name (str) or by type in the scene tree.

find_all(node_type: type) list[simvx.core.engine.Node]

Find all nodes of a given type in the scene.

property root: simvx.core.engine.Node | None
property frame_count: int
property elapsed_time: float
snapshot() dict

Capture current scene state as a serializable dict.

class simvx.core.testing.InputSimulator

Simulate input events for headless testing.

Works by directly manipulating the Input singleton state, the same mechanism that platform adapters (GLFW, SDL3) use.

Usage: from simvx.core.input import Key sim = InputSimulator() sim.press_key(Key.SPACE) runner.advance_frames(1) sim.release_key(Key.SPACE)

press_key(key) None

Simulate a key press. Accepts Key enum or int.

release_key(key) None

Simulate a key release.

tap_key(key) None

Press and release a key in one call (instant).

press_mouse(button: int = 1, position: tuple[float, float] | None = None) None

Simulate mouse button press, optionally at a position.

release_mouse(button: int = 1) None

Simulate mouse button release.

click(position: tuple[float, float], button: int = 1) None

Click at a screen position (press + release).

move_mouse(x: float, y: float) None

Move the mouse cursor to (x, y).

scroll(dx: float = 0.0, dy: float = -1.0) None

Simulate scroll wheel. dy < 0 = scroll down, dy > 0 = scroll up.

reset() None

Reset all input state to defaults.

simvx.core.testing.scene_diff(before: dict, after: dict, _path: str = '') list[str]

Compare two scene snapshots and return a list of human-readable differences.

Each entry describes a single change, e.g.: “Root/Player.position: (0, 0) -> (10, 5)” “Root/Enemy: REMOVED” “Root/PowerUp: ADDED (Node2D)”

class simvx.core.testing.NodeCounter

Count nodes by type in a scene tree. Useful for debugging and assertions.

static count(root: simvx.core.engine.Node) dict[str, int]

Return a dict mapping type name to count.

static total(root: simvx.core.engine.Node) int

Return total number of nodes in the tree.

class simvx.core.testing.FrameTimer

Measure frame timing for performance testing.

Usage: timer = FrameTimer() for _ in range(100): timer.begin_frame() runner.advance_frames(1) timer.end_frame() print(f”Average: {timer.average_ms:.2f}ms, FPS: {timer.fps:.0f}”)

Initialization

begin_frame() None
end_frame() None
property average_ms: float
property max_ms: float
property min_ms: float
property fps: float
property frame_count: int
reset() None
simvx.core.testing.scene_describe(root: simvx.core.engine.Node, include_properties: bool = True, include_layout: bool = True) str

Return a tree-formatted text description of the scene hierarchy.

Uses box-drawing chars for hierarchy. Each line shows the node name, type, and relevant state (position, rotation, scale, visibility, Property values). Designed for LLM consumption — an LLM can read this to understand what’s on screen.

simvx.core.testing.ui_describe(root, include_layout: bool = True) str

Return a tree-formatted text description of a UI widget hierarchy.

Focused on UI-specific info: widget type, text content, rect, focus, visibility. Designed for LLM consumption — an LLM can read this to understand the UI state.

class simvx.core.testing.RecordedEvent

A single recorded input event.

timestamp: float

None

event_type: str

None

x: float

0.0

y: float

0.0

button: int

0

key: str = <Multiline-String>
char: str = <Multiline-String>
target_path: str = <Multiline-String>
class simvx.core.testing.TestRecorder(format: str = 'harness', **kwargs)

Bases: simvx.core.engine.Node

Records user input events and generates replayable test scripts.

Attach to a scene to record interactions. Call stop_recording() to get Python source code that replays the session using UITestHarness, DemoRunner steps, or raw InputSimulator API.

Toggle with F9 or programmatically via start/stop_recording().

Initialization

format: str

‘harness’

start_recording() None

Start capturing input events.

stop_recording() str

Stop recording and return generated test code in the current format.

save_recording(path: pathlib.Path, format: str | None = None) None

Save recorded script to a file.

property is_recording: bool
property event_count: int
record_event(event: simvx.core.testing.RecordedEvent) None

Manually inject a recorded event (for testing the recorder itself).

process(dt: float) None
script_error_raised

‘Signal(…)’

classmethod __init_subclass__(**kwargs)
property name: str
property process_mode: simvx.core.descriptors.ProcessMode
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
find_child(name: str, recursive: bool = False) simvx.core.node.Node | None
find(node_type: type) simvx.core.node.Node | None
find_all(node_type: type, recursive: bool = True) list
property path: str
add_to_group(group: str)
remove_from_group(group: str)
is_in_group(group: str) bool
ready() None
enter_tree() None
exit_tree() None
physics_process(dt: float) None
draw(renderer) None
input_event(event: simvx.core.events.InputEvent) None
input(event: simvx.core.events.TreeInputEvent) None
unhandled_input(event: simvx.core.events.TreeInputEvent) None
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle
stop_coroutine(gen_or_handle)
destroy()
queue_free

None

property tree: simvx.core.scene_tree.SceneTree
get_tree() simvx.core.scene_tree.SceneTree
__getitem__(key: str)
classmethod get_properties() dict[str, simvx.core.descriptors.Property]
get_settings

None

__repr__()