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.__all__

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

class simvx.core.ui.testing.DrawCall

One recorded draw command.

type: str

None

x: float

0.0

y: float

0.0

w: float

0.0

h: float

0.0

color: tuple[float, ...]

()

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

1.0

x2: float

0.0

y2: float

0.0

class simvx.core.ui.testing.DrawLog

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()

Discard all recorded calls.

draw_filled_rect(x: float, y: float, w: float, h: float, color: tuple = (1, 1, 1, 1))
draw_rect_colored(x: float, y: float, w: float, h: float, color: tuple = (1, 1, 1, 1))
draw_line_colored(x1: float, y1: float, x2: float, y2: float, color: tuple = (1, 1, 1, 1))
draw_text_colored(text: str, x: float, y: float, scale: float = 1.0, color: tuple = (1, 1, 1, 1))
text_width(text: str, scale: float = 1.0) float

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

push_clip(x: float, y: float, w: float, h: float)
pop_clip()
new_layer()
set_color(r=255, g=255, b=255, a=255)

Track current colour (no-op for recording; color is per-call).

fill_rect(x: float, y: float, w: float, h: float, color: tuple = (1, 1, 1, 1))
draw_rect(x: float, y: float, w: float, h: float, color: tuple = (1, 1, 1, 1))
draw_line(x1: float, y1: float, x2: float, y2: float, color: tuple = (1, 1, 1, 1))
draw_text(text: str, pos: tuple, scale: float = 1.0, color: tuple = (1, 1, 1, 1))
fill_circle(cx: float, cy: float, radius: float, segments: int = 24)
draw_thick_line(x1: float, y1: float, x2: float, y2: float, width: float = 2.0)
draw_circle(center, radius_or_y=None, radius=None, segments=24)
draw_lines(points, closed=True, color=None)
fill_triangle(x1, y1, x2, y2, x3, y3)
texts() list[str]

All rendered text strings.

texts_containing(substring: str) list[str]

Text strings containing a substring.

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

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

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

All calls of a specific type.

has_text(text: str) bool

Check if exact text was rendered.

has_text_containing(substring: str) bool

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))

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
teardown()

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)

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)

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)

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)

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)

Press mouse button without releasing.

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

Release mouse button.

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

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)

Simulate mouse drag with intermediate moves.

type_text(text: str)

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

press_key(key: str, release: bool = True)

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)

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

set_focus(control: simvx.core.ui.core.Control)

Directly focus a control.

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

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]

Find all widgets of a given type in the tree.

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

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

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

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

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

Assert widget is visible.

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

Assert widget is not visible.

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

Assert widget has focus.

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

Assert widget.text matches expected.

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

Assert widget.text contains substring.

assert_drawn_text(text: str, msg: str = '')

Assert text appears in the draw log.

assert_drawn_text_containing(substring: str, msg: str = '')

Assert any drawn text contains substring.

assert_signal_emitted(signal, msg: str = '') list

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