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¶
One recorded draw command. |
|
Mock renderer that records draw commands for assertion. |
|
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
- 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 realDraw2D.text_widthcontract.
- text_height(text: str, scale: float = 1.0) float[source]¶
Approximate text height: 14 * 1.2 * scale per line.
- 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: whenmin_scaleisNonethe readable-pixel floor (~10px / 14 logical) is applied; explicit values bypass the safety net.
- 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]¶
- 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]¶
- 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.
- 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.
- 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’spress_keyworks onKeyenums 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 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.