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.

For multi-line strings (containing \n), returns the widest line, matching the real Draw2D.text_width contract.

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

Approximate text height: 14 * 1.2 * scale per line.

text_size(text: str, scale: float = 1.0) tuple[float, float][source]
fit_scale(text: str, max_width: float, *, base_scale: float = 1.0, min_scale: float | None = None) float[source]

Largest scale ≤ base_scale that fits text within max_width.

Mirrors :meth:simvx.graphics.draw2d.Draw2D.fit_scale: when min_scale is None the readable-pixel floor (~10px / 14 logical) is applied; explicit values bypass the safety net.

push_clip(x: float, y: float, w: float, h: float)[source]
pop_clip()[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 | None = None, *, colour: tuple = (1, 1, 1, 1), scale: float = 1.0, rect: tuple | None = None, alignment: str = 'left', vertical_alignment: str = 'top', fit_to_width: bool = False, min_scale: float | None = None)[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: simvx.core.input.enums.MouseButton = MouseButton.LEFT)[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: simvx.core.input.enums.MouseButton = MouseButton.LEFT)[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: simvx.core.input.enums.MouseButton = MouseButton.LEFT)[source]

Press mouse button without releasing.

mouse_up(target: simvx.core.ui.core.Control | simvx.core.math.types.Vec2 | tuple[float, float], button: simvx.core.input.enums.MouseButton = MouseButton.LEFT)[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: simvx.core.input.enums.MouseButton = MouseButton.LEFT)[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.

Accepts string key names (e.g. "escape", "ctrl+s") which the UI router understands directly: sim’s press_key works on Key enums and only drives the scene-tree path, so keep the string-routed UI path here.

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