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, *, cell_width: float = 0.0) float[source]

Width of text from the font’s real glyph advances.

Measured with the same metrics widgets size themselves by and the real renderer draws with, so a layout that fits here fits on screen. For multi-line strings (containing \n) it returns the widest line, matching the Draw2D.text_width contract.

cell_width is the caller’s character-grid pitch and means what it does on Draw2D.text_width: a character nothing can draw is reserved one cell. A widget that lays its text out on a grid passes it here as it does on screen, so what a test measures is what a player sees.

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

Approximate text height: 16 * 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, cell_width: float = 0.0) 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 / 16 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: float, b: float, c: float, d: float, tx: float, ty: float)[source]
pop_transform()[source]
draw_rect(pos, size, *, colour: tuple | None = None, filled: bool = False, thickness: float = 1.0, screen_space: bool = False, blend: str = 'alpha')[source]

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

draw_line(a, b, *, colour: tuple | None = None, thickness: float = 1.0, screen_space: bool = False)[source]

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

draw_text(text: str, pos=None, *, colour: tuple | None = None, 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, outline: float = 0.0, outline_colour: tuple | None = None, screen_space: bool = False, cell_width: float = 0.0)[source]
draw_circle(center, radius: float, *, colour: tuple | None = None, filled: bool = False, segments: int = 32, screen_space: bool = False)[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: tuple | None = None)[source]
draw_lines(points, closed: bool = True, colour: tuple | None = None)[source]
fill_triangle(x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, *, colour: tuple | None = None)[source]
fill_quad(x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, x4: float, y4: float, *, colour: tuple | None = None)[source]
draw_texture(texture_id: int, pos, size, *, colour: tuple | None = None, rotation: float = 0.0, blend: str = 'alpha', screen_space: bool = False)[source]
draw_texture_region(texture_id: int, pos, size, uv0=(0.0, 0.0), uv1=(1.0, 1.0), colour: tuple | None = None, rotation: float = 0.0, *, blend: str = 'alpha', screen_space: bool = False)[source]
draw_image(path, x: float, y: float, w: float, h: float, *, colour: tuple | None = None, rotation: float = 0.0, filter: str = 'linear', blend: str = 'alpha')[source]
draw_polygon(vertices, *, colour: tuple | None = None, filled: bool = True, blend: str = 'alpha')[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.

The held modifiers are restored even when a handler raises. Without that, one failing assertion inside a widget leaves the modifier down for the rest of the process, and every later test in it runs with a phantom key held: the first failure is real and the ones that follow are not, which is the expensive way round.

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