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[source]

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

draw_filled_rect(x: float, y: float, w: float, h: float, colour: tuple = (1, 1, 1, 1))[source]
draw_rect_coloured(x: float, y: float, w: float, h: float, colour: tuple = (1, 1, 1, 1))[source]
draw_line_coloured(x1: float, y1: float, x2: float, y2: float, colour: tuple = (1, 1, 1, 1))[source]
draw_text_coloured(text: str, x: float, y: float, scale: float = 1.0, colour: tuple = (1, 1, 1, 1))[source]
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]
set_colour(r=255, g=255, b=255, a=255)[source]

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

fill_rect(x: float, y: float, w: float, h: float, colour: tuple = (1, 1, 1, 1))[source]
draw_rect(x: float, y: float, w: float, h: float, colour: tuple = (1, 1, 1, 1))[source]
draw_line(x1: float, y1: float, x2: float, y2: float, colour: tuple = (1, 1, 1, 1))[source]
draw_text(text: str, pos: tuple, scale: float = 1.0, colour: tuple = (1, 1, 1, 1))[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]
fill_circle(cx: float, cy: float, radius: float, segments: int = 24)[source]
draw_filled_circle(cx: float, cy: float, radius: float, colour: tuple = (1, 1, 1, 1), segments: int = 24)[source]
draw_thick_line(x1: float, y1: float, x2: float, y2: float, width: float = 2.0)[source]
draw_circle(center, radius_or_y=None, radius=None, segments=24, colour=None)[source]
draw_lines(points, closed=True, colour=None)[source]
fill_triangle(x1, y1, x2, y2, x3, y3)[source]
draw_filled_triangle(x1, y1, x2, y2, x3, y3, colour=(1, 1, 1, 1))[source]
fill_quad(x1, y1, x2, y2, x3, y3, x4, y4)[source]
draw_filled_quad(x1, y1, x2, y2, x3, y3, x4, y4, colour=(1, 1, 1, 1))[source]
draw_thick_line_coloured(x1, y1, x2, y2, width=2.0, 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
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