Skip to main content
The Simulator API predates Prefab’s current architecture. It was written when every UI originated as an MCP tool call, and its invoke / handler model reflects that assumption. Expect this API to change significantly as we update it to support Prefab’s broader usage patterns.
Prefab UIs are declarative — they’re JSON trees with state and actions. The Simulator reimplements the renderer’s client-side loop in Python, so you can test the full interaction cycle (load UI → find components → simulate clicks → assert on state) entirely in pytest.

Setup

The Simulator takes an action handler — a function that responds to toolCall actions the same way your server tools would.
from prefab_ui.testing import Simulator, ActionResult


async def handler(name: str, arguments: dict) -> ActionResult:
    if name == "search_users":
        query = arguments.get("q", "")
        users = [{"name": "Alice", "email": "[email protected]"}]
        results = [u for u in users if query.lower() in u["name"].lower()]
        return ActionResult(content={
            "state": {"results": results},
            "view": {"type": "Text", "content": "Found users"},
        })
    return ActionResult(is_error=True, error_text=f"Unknown tool: {name}")


sim = Simulator(handler)
ActionResult wraps a tool’s response. Set is_error=True with error_text for failures. On success, content should be a dict matching the Prefab envelope structure (state, view).

Loading a UI

Call invoke with a tool name and arguments to load a UI into the Simulator:
await sim.invoke("my_ui_tool", {"page": "users"})
This calls your handler, then populates sim.state and sim.view from the response — just like the renderer does on first load.

Finding Components

Query the component tree by type and props:
# Find the first match (raises ComponentNotFoundError if missing)
button = sim.find("Button", label="Search")
input_field = sim.find("Input", name="query")

# Find all matches
cards = sim.find_all("Card")
buttons = sim.find_all("Button", variant="destructive")
The returned values are raw JSON dicts (the component nodes from the tree), which you can inspect or pass to interaction methods.

Simulating Interactions

Three interaction methods mirror what a user does in the browser:
# Click a button — executes its onClick action chain
await sim.click(button)

# Type into an input — sets the value, syncs named state, runs onChange
await sim.set_value(input_field, "alice")

# Submit a form — executes its onSubmit action chain
form = sim.find("Form")
await sim.submit(form)
Each method resolves {{ templates }} against current state, executes actions (including toolCall round-trips through your handler), handles onSuccess/onError callbacks, and updates sim.state with results.

Asserting on State

After interactions, check sim.state for expected values:
# State was seeded by invoke
assert sim.state["query"] == ""

# set_value auto-synced the named input
await sim.set_value(input_field, "alice")
assert sim.state["query"] == "alice"

# toolCall wrote results via resultKey
await sim.click(button)
assert len(sim.state["results"]) == 1
assert sim.state["results"][0]["name"] == "Alice"

Checking Toasts

ShowToast actions append to sim.toasts:
assert len(sim.toasts) == 1
assert sim.toasts[0]["message"] == "Found 1 user"
assert sim.toasts[0]["variant"] == "success"

A Complete Test

Putting it together — a test for a search UI that calls a server tool and displays results:
import pytest
from prefab_ui import UIResponse, Column
from prefab_ui.components import Button, DataTable, DataTableColumn, Input
from prefab_ui.actions import ToolCall, ShowToast
from prefab_ui.testing import Simulator, ActionResult

USERS = [
    {"name": "Alice", "email": "[email protected]"},
    {"name": "Bob", "email": "[email protected]"},
]


def build_search_ui() -> UIResponse:
    with Column(gap=4) as view:
        Input(name="query", placeholder="Search...")
        Button(
            "Search",
            on_click=ToolCall(
                "search_users",
                arguments={"q": "{{ query }}"},
                result_key="results",
                on_error=ShowToast("{{ $error }}", variant="error"),
            ),
        )
        DataTable(
            columns=[DataTableColumn(key="name", header="Name")],
            rows="{{ results }}",
        )
    return UIResponse(view=view, state={"query": "", "results": []})


async def handler(name: str, arguments: dict) -> ActionResult:
    if name == "build_search_ui":
        return ActionResult(content=build_search_ui().to_json())
    if name == "search_users":
        q = arguments.get("q", "").lower()
        results = [u for u in USERS if q in u["name"].lower()]
        return ActionResult(content={"state": {"results": results}})
    return ActionResult(is_error=True, error_text=f"Unknown: {name}")


async def test_search_flow():
    sim = Simulator(handler)
    await sim.invoke("build_search_ui", {})

    # Initial state is empty
    assert sim.state["results"] == []

    # Type a query and search
    input_field = sim.find("Input", name="query")
    await sim.set_value(input_field, "alice")
    assert sim.state["query"] == "alice"

    button = sim.find("Button", label="Search")
    await sim.click(button)

    # Results populated via resultKey
    assert len(sim.state["results"]) == 1
    assert sim.state["results"][0]["name"] == "Alice"

    # No error toasts
    assert len(sim.toasts) == 0

API Reference

Simulator

handler
ActionHandler
Async callable (name: str, arguments: dict) -> ActionResult that handles toolCall actions.

Simulator Methods

invoke
async (name, arguments) -> None
Call a tool and load the returned UI into the simulator.
find
(type, **props) -> dict
Find the first component matching type and props. Raises ComponentNotFoundError if not found.
find_all
(type, **props) -> list[dict]
Find all components matching type and props.
click
async (component) -> None
Execute a component’s onClick action chain.
set_value
async (component, value) -> None
Set a component’s value, sync named state, and execute onChange.
submit
async (component) -> None
Execute a form’s onSubmit action chain.
state
dict[str, Any]
Current client-side state.
toasts
list[dict]
List of toast notifications triggered by ShowToast actions.

ActionResult

content
dict
default:"{}"
Response data. Should match the Prefab envelope structure with state and/or view keys.
is_error
bool
default:"false"
Whether this result represents an error.
error_text
str | None
default:"None"
Error message, available as $error in onError callbacks.