Skip to content

Testing

wybthon.testing

testing

Render components in plain CPython and query them like a user would.

wybthon.testing mounts views into an in-memory DOM through the real reconciler, kernel protocol, scheduler, and event delegation. Nothing is mocked: the commands a test applies are exactly the commands the browser kernel applies.

from wybthon.testing import cleanup, fire, render


def test_counter():
    screen = render(Counter(label="Clicks"))
    fire.click(screen.get_by_role("button", name="+"))
    assert screen.get_by_text("Clicks: 1")
    cleanup()

Queries come in three forms, as in Testing Library:

  • get_by_* returns the single match and raises when there are none or several;
  • query_by_* returns the match or None;
  • get_all_by_* returns every match (raising when there are none).

They search by visible text, ARIA role (with an optional accessible name), label text, and data-testid. fire dispatches events through Wybthon's delegated handlers and flushes the resulting updates, so assertions see the settled DOM.

Use render as a context manager to unmount automatically, or call cleanup at the end of each test (an autouse pytest fixture is a convenient place).

Classes:

Name Description
TestNode

An in-memory DOM node: an element, a text node, or a comment.

TestDocument

An in-memory document for the kernel's Python backend.

Screen

A rendered view and the queries that find nodes in it.

Functions:

Name Description
render

Mount view into a fresh in-memory container and flush it.

cleanup

Unmount every view rendered with render that's still mounted.

reactive_scope

Own test computations and dispose them deterministically at scope exit.

tick

Drain ready asyncio continuations and flush reactive work.

wait_for

Wait for an observable condition, raising TimeoutError on failure.

Attributes:

Name Type Description
fire

Dispatch events: fire.click(node), fire.input(node, "text"), fire(node, "dblclick").

fire module-attribute

fire = _Fire()

Dispatch events: fire.click(node), fire.input(node, "text"), fire(node, "dblclick").

TestNode

TestNode(tag: str | None = None, text: str | None = None)

An in-memory DOM node: an element, a text node, or a comment.

It implements the subset of the DOM the kernel protocol uses, plus the properties tests read (text_content, attributes, value, checked).

Methods:

Name Description
focus

Make this node the document's active element (no events are dispatched).

blur

Clear the active element if it's this node.

click

Dispatch a click through Wybthon's delegated handlers (like fire.click).

Attributes:

Name Type Description
is_element bool

Whether this node is an element (not text or a comment).

text_content str

The concatenated text of this node and its descendants (comments excluded).

is_element property

is_element: bool

Whether this node is an element (not text or a comment).

text_content property

text_content: str

The concatenated text of this node and its descendants (comments excluded).

focus

focus() -> None

Make this node the document's active element (no events are dispatched).

blur

blur() -> None

Clear the active element if it's this node.

click

click() -> None

Dispatch a click through Wybthon's delegated handlers (like fire.click).

TestDocument

TestDocument()

An in-memory document for the kernel's Python backend.

Screen

Screen(root: Any, container: TestNode)

A rendered view and the queries that find nodes in it.

Attributes:

Name Type Description
container

The TestNode the view is mounted into.

root

The Root returned by render.

Methods:

Name Description
unmount

Dispose the view and release its container.

html

The container's inner HTML, without Wybthon's comment markers.

text

The container's text content, with whitespace collapsed.

get_by_text

Return the one element whose text matches.

query_by_text

Return the element whose text matches, or None.

get_all_by_text

Return every element whose text matches.

get_by_role

Return the one element with this ARIA role (and accessible name).

query_by_role

Return the element with this role (and name), or None.

get_all_by_role

Return every element with this role (and name).

get_by_label_text

Return the one form control labelled by text.

query_by_label_text

Return the form control labelled by text, or None.

get_by_test_id

Return the one element with data-testid equal to test_id.

query_by_test_id

Return the element with this data-testid, or None.

get_all_by_test_id

Return every element with this data-testid.

unmount

unmount() -> None

Dispose the view and release its container.

html

html() -> str

The container's inner HTML, without Wybthon's comment markers.

text

text() -> str

The container's text content, with whitespace collapsed.

get_by_text

get_by_text(text: str | Pattern[str], *, exact: bool = True) -> TestNode

Return the one element whose text matches.

query_by_text

query_by_text(text: str | Pattern[str], *, exact: bool = True) -> TestNode | None

Return the element whose text matches, or None.

get_all_by_text

get_all_by_text(text: str | Pattern[str], *, exact: bool = True) -> list[TestNode]

Return every element whose text matches.

get_by_role

get_by_role(role: str, *, name: str | Pattern[str] | None = None) -> TestNode

Return the one element with this ARIA role (and accessible name).

query_by_role

query_by_role(role: str, *, name: str | Pattern[str] | None = None) -> TestNode | None

Return the element with this role (and name), or None.

get_all_by_role

get_all_by_role(role: str, *, name: str | Pattern[str] | None = None) -> list[TestNode]

Return every element with this role (and name).

get_by_label_text

get_by_label_text(text: str | Pattern[str]) -> TestNode

Return the one form control labelled by text.

query_by_label_text

query_by_label_text(text: str | Pattern[str]) -> TestNode | None

Return the form control labelled by text, or None.

get_by_test_id

get_by_test_id(test_id: str) -> TestNode

Return the one element with data-testid equal to test_id.

query_by_test_id

query_by_test_id(test_id: str) -> TestNode | None

Return the element with this data-testid, or None.

get_all_by_test_id

get_all_by_test_id(test_id: str) -> list[TestNode]

Return every element with this data-testid.

render

render(view: Any) -> Screen

Mount view into a fresh in-memory container and flush it.

Parameters:

Name Type Description Default
view Any

Anything wybthon.render accepts: a component call, an element, a list, or a reactive expression.

required

Returns:

Type Description
Screen

A Screen for querying the result.

cleanup

cleanup() -> None

Unmount every view rendered with render that's still mounted.

reactive_scope

reactive_scope() -> Iterator[Owner]

Own test computations and dispose them deterministically at scope exit.

tick async

tick(rounds: int = 2) -> None

Drain ready asyncio continuations and flush reactive work.

wait_for async

wait_for(predicate: Callable[[], bool], *, timeout: float = 1, interval: float = 0.001) -> None

Wait for an observable condition, raising TimeoutError on failure.

What's in this module

wybthon.testing renders components in plain CPython, through the real reconciler, kernel protocol, scheduler, and event delegation, into an in-memory DOM. Nothing is mocked, so a passing test exercises the same commands the browser kernel applies. The queries follow Testing Library's conventions.

Name Description
render render(view) -> Screen: mount a view (a VNode or a zero-arg callable) into a fresh in-memory document.
Screen Queries plus html(), text(), and unmount(); also a context manager that unmounts on exit.
fire Dispatch events through delegated handlers and flush: fire.click(node), fire.input(node, value), fire.change(node, value=None, checked=None), fire.submit(form), fire.key_down(node, key), fire.focus(node), fire.blur(node), or fire(node, "dblclick", **payload).
cleanup Unmount everything render mounted; call it after each test, or from an autouse fixture.
TestNode, TestDocument The in-memory DOM the screen renders into.
reactive_scope, tick, wait_for Helpers for testing reactive code and async updates outside a component.
Query Finds Variants
get_by_text(text, *, exact=True) Elements by visible text, or a compiled regex query_by_text, get_all_by_text
get_by_role(role, *, name=None) Elements by ARIA role and accessible name query_by_role, get_all_by_role
get_by_label_text(text) Form controls by their label query_by_label_text
get_by_test_id(test_id) Elements by data-testid query_by_test_id, get_all_by_test_id

get_by_* raises unless exactly one element matches, query_by_* returns the match or None, and get_all_by_* returns every match.

import pytest

from wybthon import Prop, Props, button, component, create_signal, div, p, prop
from wybthon.testing import cleanup, fire, render


class CounterProps(Props):
    label: Prop[str] = prop(default="Count")


@component
def Counter(props: CounterProps):
    count, set_count = create_signal(0)
    return div(p(props.label, t": {count}"), button("+", on_click=lambda: set_count(lambda n: n + 1)))


@pytest.fixture(autouse=True)
def _cleanup():
    yield
    cleanup()


def test_counter():
    screen = render(Counter(label="Clicks"))
    fire.click(screen.get_by_role("button", name="+"))
    assert screen.get_by_text("Clicks: 1")
    assert screen.html() == "<div><p>Clicks: 1</p><button>+</button></div>"

See also