Testing without a window
Render Blender components into memory, drive them with simulated clicks, drags and keys, and check values, pixels and frame times in ordinary tests.
- Python
A UI you can’t test is a UI you’re afraid to change. With BUI you can test yours like any other code: no window, no screen, no sleeping while a robot moves the mouse. You render into memory, press and drag with function calls, and assert on the results. It runs anywhere Python runs, including CI servers without a display.
BUI’s own test suite works this way: bindings/python/tests/test_components.py tests every component, and checks that frames stay inside a 120 Hz budget.
Render a frame
app.render_raster() lays out and draws the whole window into a raster, an image in memory, without opening anything:
from blenderui.components import App, NumberField
size = NumberField("Size", 1.0)app = App("Test", (400, 300), size)
raster = app.render_raster() # draws the window at its size, (400, 300)print(raster.size) # (400, 300)print(size.rect) # where the field ended up: (x, y, w, h)print(raster.pixel(10, 10)) # (r, g, b)render_raster(w=None, h=None, full=False)uses the app’s size unless you give one.- After rendering, every component knows its place:
x,y,w,handrect. - The raster is kept between calls. Later calls redraw only what changed, exactly as in a window; pass
full=Trueto redraw everything.
Simulate input
The App has the same input methods a window uses. Coordinates are pixels in the window:
| Method | Simulates |
|---|---|
app.press(x, y, mods=None) |
Pressing the left mouse button |
app.motion(x, y, mods=None) |
Moving the pointer (hover, or drag while pressed) |
app.release(x, y, mods=None) |
Releasing the button |
app.key(char, keysym, keycode=0, mods=None) |
A key press: char is the typed text, keysym its name ("a", "Return", "BackSpace", "Tab") |
app.wheel(x, y, delta, mods=None) |
The mouse wheel |
app.context_menu(x, y, mods=None) |
A right-click |
mods holds modifier keys as bits. Import them from blenderui.components.core: SHIFT, CTRL, ALT, and BUTTON1 for “the left button is held”. Combine them with |, for example CTRL | SHIFT. An object with shift, ctrl, alt and button1 attributes works too.
A complete test file
from blenderui.components import App, Checkbox, NumberField, TextFieldfrom blenderui.components.core import CTRL
def center(c): x, y, w, h = c.rect return x + w // 2, y + h // 2
def test_dragging_scrubs_and_undo_restores(): size = NumberField("Size", 1.0) app = App("test", (400, 300), size) app.render_raster() x, y = center(size) app.press(x, y) app.motion(x + 40, y) # drag to the right app.release(x + 40, y) assert size.value > 1.0 app.key("z", "z", mods=CTRL) # Ctrl+Z assert size.value == 1.0
def test_typing_a_name(): seen = [] name = TextField("Name", "Cube", on_change=seen.append) app = App("test", (400, 300), name) app.render_raster() x, y = center(name) app.press(x, y) app.release(x, y) # click: start editing app.render_raster() app.key("a", "a", mods=CTRL) # select all for ch in "Suzanne": app.key(ch, ch) app.key("", "Return") assert name.value == "Suzanne" assert seen == ["Suzanne"]
def test_checkbox_click_changes_pixels(): box = Checkbox("Smooth", False) app = App("test", (300, 100), box) raster = app.render_raster() x, y = box.x + 8, box.y + box.h // 2 before = raster.pixel(x, y) app.press(x, y) app.release(x, y) raster = app.render_raster() assert box.value is True assert raster.pixel(x, y) != beforeThese are plain functions with assert, so pytest finds and runs them. Each test builds its own App, so they don’t share state.
Check what was redrawn
BUI only redraws the parts of the window that changed. Your tests can check that too, through the context’s render, which returns how many regions it redrew:
from blenderui.components import App, Column, NumberField, Button
field = NumberField("N", 1.0)app = App("test", (400, 600), Column(field, *[Button("b%d" % i) for i in range(20)]))raster = app.render_raster()w, h = app.size
assert app.ctx.render(w, h, raster) == 0 # idle: nothing to drawapp.motion(field.x + 10, field.y + 5) # hover the fieldassert app.ctx.render(w, h, raster) >= 1 # something was redrawnprint(raster.damage()) # the redrawn rectanglesraster.damage() lists the rectangles of the last frame. BUI’s suite uses it to check that hovering a field redraws only that widget’s tiles, never the whole window.
Frame budgets
You can time frames the same way. BUI’s own test renders the full components demo 50 times and requires:
| Frame | Budget in test_components.py |
|---|---|
| A full redraw of the components demo | under 8.3 ms (one frame at 120 Hz) |
| A hover over a number field | under 1 ms |
import time
def test_frames_are_fast(): app = build_my_app() # your App w, h = app.size raster = app.render_raster() t0 = time.perf_counter() for _ in range(50): app.ctx.render(w, h, raster, True) # full redraws full_ms = (time.perf_counter() - t0) / 50 * 1000 assert full_ms < 8.3Timing tests depend on the machine. Keep the budget generous enough for your slowest CI runner.
More from the context
app.ctx is the kit context. A few of its methods are handy in tests:
app.ctx.popup_count(): how many menus or popups are open.app.ctx.undo()andapp.ctx.redo(): undo and redo without keys.app.ctx.set_host(schedule=..., cursor=..., warp=...): receive what a window would get, such as timer requests, cursor changes and pointer warps. The test suite uses it to check that the cursor hides and warps during a drag.
Next steps
Last updated