Skip to content

Using the engine from plain Python

The whole pipeline also runs headlessly — that's how you unit-test components without a browser:

from pyweft import load_component, Renderer, diff

App = load_component("myapp/app.weft")
app = App()
renderer = Renderer(app)

tree = renderer.render()              # virtual DOM (Element/Text nodes)

# find a handler id on a rendered element and fire it, as the browser would
button = tree[0].children[1]
renderer.dispatch(button.events["click"], {})

new_tree = renderer.render()
patches = diff(tree, new_tree)        # JSON-serialisable patch ops
assert app.cheers == 1

Effects and on_after_render run when the host reports that a render is on the page, so a test that exercises them calls renderer.after_render() after each render. It returns True when an effect changed state; render again and repeat until it returns False.

Components with async handlers or an async on_init need an event loop, so test them inside asyncio.run. Call renderer.init_root() to run the root's on_init (child components get theirs automatically), and await renderer.idle() to wait for every pending handler and task:

import asyncio

async def main():
    app = App()
    renderer = Renderer(app)
    renderer.init_root()
    tree = renderer.render()                       # "Loading…"
    renderer.dispatch(tree[0].events["click"], {})
    await renderer.idle()                          # the handler has finished
    assert not app.loading
    new_tree = renderer.render()                   # the finished state

asyncio.run(main())

run_in_background calls run on a thread in tests, through the same lookup-by-name and pickling as the browser's worker, so a function or argument the worker could not handle fails in the test too.

Errors in async handlers have no caller to raise to; set renderer.on_error to a function to receive them, as the browser does to show them in the overlay.

To see changes made outside a handler, set renderer.on_invalidate to a function; it is called whenever state_has_changed() runs and no render is already on its way. The browser uses it to schedule a render; a test can use it to assert that one was requested, and renderer.needs_render() reports whether any component is waiting to be redrawn.

examples/demo.py walks the full render → event → diff → apply cycle in the terminal, tests/test_compose.py shows the patterns for testing composed components, tests/test_effects.py the patterns for effects, and tests/test_async.py the patterns for async handlers and tasks.