simvx.core.testing.input_sim

InputSimulator – simulate keyboard/mouse/touch input for headless testing.

Each public method does three things in one call:

  1. State injection – writes into the Input singleton’s typed state sets, matching the bytes the platform adapter would write (so Input.is_mouse_button_pressed() and friends see the press).

  2. Scene-tree event propagation – posts a TreeInputEvent to the active SceneTree so @on_input decorators fire.

  3. UI-tree event propagation – posts a UIInputEvent via tree.ui_input(...) so Control._on_gui_input handlers fire.

If no SceneTree is active (pure logic tests with no tree), steps 2 and 3 are silently skipped.

Module Contents

Classes

InputSimulator

Simulate input events for headless testing.

Data

API

simvx.core.testing.input_sim.__all__

[‘InputSimulator’]

class simvx.core.testing.input_sim.InputSimulator(tree: simvx.core.scene_tree.SceneTree | None = None)[source]

Simulate input events for headless testing.

Drives the engine the same way real platform adapters do: state writes, scene-tree event propagation, and UI-tree event propagation, so a single sim.click(pos) call fires polling state, @on_input decorators, and Control._on_gui_input in lockstep.

The target SceneTree is either bound at construction time (when a test explicitly knows which tree to drive, as UITestHarness does) or resolved lazily via SceneTree.current() on each call (so simple one-tree scenarios work without plumbing).

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

Initialization

press_key(key: simvx.core.input.Key | int) None[source]

Simulate a key press. Accepts Key enum or int.

release_key(key: simvx.core.input.Key | int) None[source]

Simulate a key release.

tap_key(key: simvx.core.input.Key | int) None[source]

Press now, schedule release for the next frame boundary.

Pressing AND releasing in the same Python tick lights up both is_action_just_pressed AND is_action_just_released on the same frame, which breaks edge-triggered game logic (PyDew Valley and the Tier-1 Balatro port both hit this). Instead, press immediately so the current frame’s tree.tick() sees is_action_just_pressed, then queue the release for the next frame boundary.

SceneRunner.advance_frames drains the queue at the end of every iteration (after Input._new_frame), so the typical flow sim.tap_key(); runner.advance_frames(2) observes is_action_just_pressed on the first iteration and is_action_just_released on the second. Callers driving frames outside SceneRunner can flush manually via

Meth:

flush_pending_releases.

classmethod flush_pending_releases() None[source]

Release every key queued by :meth:tap_key since the last flush.

Writes the release into the typed Input state AND propagates a

Class:

TreeInputEvent to the active SceneTree so polling (is_action_just_released) and @on_input decorators both observe the release edge.

Snapshots the queue before iterating so a release handler that re-queues a release does not extend the current drain.

press_mouse(button: simvx.core.input.MouseButton | int = MouseButton.LEFT, position: tuple[float, float] | None = None) None[source]

Simulate mouse button press, optionally at a position.

release_mouse(button: simvx.core.input.MouseButton | int = MouseButton.LEFT) None[source]

Simulate mouse button release.

click(position: tuple[float, float], button: simvx.core.input.MouseButton | int = MouseButton.LEFT) None[source]

Click at a screen position (press + release).

move_mouse(x: float, y: float) None[source]

Move the mouse cursor to (x, y).

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

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

touch_down(finger_id: int = 0, position: tuple[float, float] = (0, 0), pressure: float = 1.0) None[source]

Simulate a touch press (finger down).

touch_move(finger_id: int = 0, position: tuple[float, float] = (0, 0), pressure: float = 1.0) None[source]

Simulate a touch move (finger drag).

touch_up(finger_id: int = 0, position: tuple[float, float] = (0, 0)) None[source]

Simulate a touch release (finger up).

reset() None[source]

Reset all input state to defaults.

Also drains the class-level :attr:_pending_releases queue so a tap_key from an earlier test cannot fire a stale release the first time the next test advances a frame.