simvx.core.ui.testing

Headless UI test harness — validate widgets without GPU or window.

Provides UITestHarness for frame simulation, input injection, and draw capture. All tests run on CPU only; no Vulkan, GLFW, or display required.

Example: from simvx.core import Button, VBoxContainer, SceneTree from simvx.core.ui.testing import UITestHarness

def test_button_click():
    root = VBoxContainer()
    btn = root.add_child(Button("Save"))
    harness = UITestHarness(root)

    clicks = []
    btn.pressed.connect(lambda: clicks.append(1))

    harness.click(btn)
    assert len(clicks) == 1

Module Contents

Classes

DrawCall

One recorded draw command.

DrawLog

Mock renderer that records draw commands for assertion.

UITestHarness

Headless UI test harness for programmatic widget testing.

Data

API

simvx.core.ui.testing.log

‘getLogger(…)’

simvx.core.ui.testing.__all__

[‘UITestHarness’, ‘DrawLog’, ‘DrawCall’]

class simvx.core.ui.testing.DrawCall[source]

One recorded draw command.

type: str

None

x: float

0.0

y: float

0.0

w: float

0.0

h: float

0.0

colour: tuple[float, ...]

()

text: str = <Multiline-String>
scale: float

1.0

x2: float

0.0

y2: float

0.0

class simvx.core.ui.testing.DrawLog[source]

Mock renderer that records draw commands for assertion.

Implements the same interface as Draw2D so widgets can render into it. All geometry is captured but not rasterized.

Example: harness.tick() assert “Save” in harness.draw_log.texts() assert harness.draw_log.rects_at(100, 50)

Initialization

clear()[source]

Discard all recorded calls.

text_width(text: str, scale: float = 1.0) float[source]

Approximate text width: 8px per character at scale 1.0.

push_clip(x: float, y: float, w: float, h: float)[source]
pop_clip()[source]
new_layer()[source]
push_transform(a, b, c, d, tx, ty)[source]
pop_transform()[source]
draw_rect(pos, size, *, colour: tuple = (1, 1, 1, 1), filled: bool = False, thickness: float = 1.0)[source]

Canonical rect entry. pos and size must be 2-tuples or Vec2s.

draw_line(a, b, *, colour: tuple = (1, 1, 1, 1), thickness: float = 1.0)[source]

Canonical line entry. a and b must be 2-tuples or Vec2s.

draw_text(text: str, pos: tuple, *, colour: tuple = (1, 1, 1, 1), scale: float = 1.0)[source]
draw_circle(center, radius, *, colour=None, filled: bool = False, segments: int = 32)[source]
fill_rect_gradient(x: float, y: float, w: float, h: float, colour_top: tuple, colour_bottom: tuple)[source]
draw_gradient_rect(x: float, y: float, w: float, h: float, colour_top: tuple, colour_bottom: tuple)[source]
draw_thick_line(x1: float, y1: float, x2: float, y2: float, width: float = 2.0, *, colour=(1, 1, 1, 1))[source]
draw_lines(points, closed=True, colour=None)[source]
fill_triangle(x1, y1, x2, y2, x3, y3, *, colour=(1, 1, 1, 1))[source]
fill_quad(x1, y1, x2, y2, x3, y3, x4, y4, *, colour=(1, 1, 1, 1))[source]
draw_texture(texture_id, x, y, w, h, colour=None, rotation=0.0)[source]
texts() list[str][source]

All rendered text strings.

texts_containing(substring: str) list[str][source]

Text strings containing a substring.

rects_at(x: float, y: float) list[simvx.core.ui.testing.DrawCall][source]

All fill_rect calls whose bounds contain (x, y).

calls_of_type(call_type: str) list[simvx.core.ui.testing.DrawCall][source]

All calls of a specific type.

has_text(text: str) bool[source]

Check if exact text was rendered.

has_text_containing(substring: str) bool[source]

Check if any rendered text contains substring.

class simvx.core.ui.testing.UITestHarness(root: simvx.core.ui.core.Control, screen_size: tuple[float, float] = (1280, 720))[source]

Headless UI test harness for programmatic widget testing.

Creates a SceneTree with the given root control, provides input injection helpers, frame simulation, draw capture, and widget lookup.

Example: panel = VBoxContainer() btn = panel.add_child(Button(“OK”)) harness = UITestHarness(panel)

harness.click(btn)
harness.type_text("hello")
harness.press_key("enter")

harness.tick()
assert harness.draw_log.has_text("OK")

Initialization

property root: simvx.core.ui.core.Control[source]
teardown()[source]

Tear down the scene tree, breaking reference cycles to free memory.

Idempotent — safe to call multiple times. Recursively severs every parent↔child and node↔tree back-reference so the entire node graph becomes immediately reclaimable by refcount alone (no GC needed).

tick(dt: float = 1 / 60, count: int = 1)[source]

Advance count frames: process → draw for each.

Draw log is cleared before the first frame, then accumulates across all frames in this tick call.

process_only(dt: float = 1 / 60, count: int = 1)[source]

Advance frames without drawing (faster for logic-only tests).

click(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float], button: int = 1)[source]

Click (press + release) at widget center or screen position.

double_click(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float], button: int = 1)[source]

Two rapid clicks at the same position.

mouse_down(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float], button: int = 1)[source]

Press mouse button without releasing.

mouse_up(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float], button: int = 1)[source]

Release mouse button.

mouse_move(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float])[source]

Move mouse to position (triggers hover/mouse_over updates).

drag(start: simvx.core.ui.core.Control | simvx.core.math.types.Vec2, end: simvx.core.ui.core.Control | simvx.core.math.types.Vec2, steps: int = 5, button: int = 1)[source]

Simulate mouse drag with intermediate moves.

type_text(text: str)[source]

Send character events for each char in text to the focused widget.

press_key(key: str, release: bool = True)[source]

Send key press (and optionally release) event.

Supports modifier combos: “ctrl+s”, “shift+enter”, “escape”. For modifier combos, temporarily sets Input._keys so SceneTree builds the correct combo_key string.

scroll(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float], direction: str = 'down', amount: int = 1)[source]

Send scroll events. direction is ‘up’ or ‘down’.

touch(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float], finger_id: int = 0)[source]

Touch down on a control (like a tap press). Routes through UI as mouse.

touch_up(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float], finger_id: int = 0)[source]

Touch release on a control.

tap(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float], finger_id: int = 0)[source]

Full tap gesture (touch down + release).

set_focus(control: simvx.core.ui.core.Control)[source]

Directly focus a control.

find(path: str) simvx.core.ui.core.Control | None[source]

Find widget by slash-separated name path from root.

Example: harness.find(“Layout/TabContainer/Editor”)

find_all(widget_type: type) list[simvx.core.ui.core.Control][source]

Find all widgets of a given type in the tree.

find_by_text(text: str) simvx.core.ui.core.Control | None[source]

Find first Label or Button whose .text contains the given string.

find_by_name(name: str) simvx.core.ui.core.Control | None[source]

Find first widget with the given name (depth-first).

assert_visible(widget: simvx.core.ui.core.Control, msg: str = '')[source]

Assert widget is visible.

assert_hidden(widget: simvx.core.ui.core.Control, msg: str = '')[source]

Assert widget is not visible.

assert_focused(widget: simvx.core.ui.core.Control, msg: str = '')[source]

Assert widget has focus.

assert_text(widget: simvx.core.ui.core.Control, expected: str, msg: str = '')[source]

Assert widget.text matches expected.

assert_text_contains(widget: simvx.core.ui.core.Control, substring: str, msg: str = '')[source]

Assert widget.text contains substring.

assert_drawn_text(text: str, msg: str = '')[source]

Assert text appears in the draw log.

assert_drawn_text_containing(substring: str, msg: str = '')[source]

Assert any drawn text contains substring.

assert_signal_emitted(signal, msg: str = '') list[source]

Connect to a signal and return a list that collects emissions.

Call this BEFORE the action that should emit. Check the list after.

Example: emissions = harness.assert_signal_emitted(btn.pressed) harness.click(btn) assert len(emissions) == 1