---
title: Testing
description: Test interactive Prefab UIs without a browser using the headless Simulator.
icon: flask
---

<Warning>
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.
</Warning>

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.

```python
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": "alice@example.com"}]
        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:

```python
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:

```python
# 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:

```python
# 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:

```python
# 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`:

```python
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:

```python
import pytest
from prefab_ui.response import UIResponse
from prefab_ui.components import Column
from prefab_ui.components import Button, DataTable, DataTableColumn, Input
from prefab_ui.actions import CallTool, ShowToast
from prefab_ui.testing import Simulator, ActionResult

USERS = [
    {"name": "Alice", "email": "alice@co.com"},
    {"name": "Bob", "email": "bob@co.com"},
]


def build_search_ui() -> UIResponse:
    with Column(gap=4) as view:
        Input(name="query", placeholder="Search...")
        Button(
            "Search",
            on_click=CallTool(
                "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

<Card icon="code" title="Simulator">
<ParamField body="handler" type="ActionHandler">
  Async callable `(name: str, arguments: dict) -> ActionResult` that handles `toolCall` actions.
</ParamField>
</Card>

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

<Card icon="code" title="ActionResult">
<ParamField body="content" type="dict" default="{}">
  Response data. Should match the Prefab envelope structure with `state` and/or `view` keys.
</ParamField>
<ParamField body="is_error" type="bool" default="false">
  Whether this result represents an error.
</ParamField>
<ParamField body="error_text" type="str | None" default="None">
  Error message, available as `$error` in `onError` callbacks.
</ParamField>
</Card>
