Testing¶
FluxUI ships a FluxTestClient that renders pages and dispatches events without a
real browser or network connection. Tests are fully synchronous — no asyncio.run
or pytest-asyncio setup required.
Installation¶
The test client is included in the base fluxui package. No extra install needed.
For the test runner, install pytest:
FluxTestClient¶
Constructor¶
Use as a context manager to ensure the internal event loop is cleaned up:
Methods¶
connect(route="/")¶
Renders the page at route and establishes a fake session. Must be called before
any other method.
html(component_id=None)¶
Returns the current rendered HTML. If component_id is given, returns only
that component's HTML.
text(component_id)¶
Returns the visible text content of a component with all HTML tags stripped.
click(label_or_id)¶
Finds a Button by its label text or component ID and fires a click event.
input(label_or_id, value)¶
Types a value into a TextInput, NumberInput, or Textarea.
Finds the component by its label= text or component ID.
select(label_or_id, value)¶
Selects an option in a Select component.
check(label_or_id, checked=True)¶
Toggles a Checkbox or Switch.
Examples¶
Counter test¶
import fluxui as ui
from fluxui.testing import FluxTestClient
def make_counter_app():
app = ui.App("test")
@app.page("/")
def home(session):
count = ui.state(0)
ui.Button("Increment", on_click=lambda: count.set(count.value + 1))
ui.Button("Reset", on_click=lambda: count.set(0))
ui.Text(lambda: str(count.value), id="counter")
return app
def test_counter_increments():
with FluxTestClient(make_counter_app()) as client:
client.connect("/")
assert client.text("counter") == "0"
client.click("Increment")
assert client.text("counter") == "1"
client.click("Increment")
client.click("Increment")
assert client.text("counter") == "3"
def test_counter_resets():
with FluxTestClient(make_counter_app()) as client:
client.connect("/")
client.click("Increment")
client.click("Increment")
assert client.text("counter") == "2"
client.click("Reset")
assert client.text("counter") == "0"
Form validation test¶
def test_form_shows_error_on_empty_submit():
app = ui.App("test")
@app.page("/")
def home(session):
name = ui.state("")
error = ui.state("")
def submit():
if not name.peek().strip():
error.set("Name is required")
ui.TextInput(label="Name", value=name, on_change=name.set)
ui.Text(error, id="error-msg")
ui.Button("Submit", on_click=submit)
with FluxTestClient(app) as client:
client.connect("/")
assert client.text("error-msg") == ""
client.click("Submit")
assert client.text("error-msg") == "Name is required"
client.input("Name", "Alice")
client.click("Submit")
assert client.text("error-msg") == ""
Routing test¶
def test_navigation():
app = ui.App("test")
@app.page("/")
def home(session):
ui.Text("Home page", id="page-title")
ui.Button("Go to about", on_click=lambda: session.navigate("/about"))
@app.page("/about")
def about(session):
ui.Text("About page", id="page-title")
with FluxTestClient(app) as client:
client.connect("/")
assert "Home page" in client.html()
# Directly connect to the about page
client.connect("/about")
assert "About page" in client.html()
DataGrid and Select test¶
def test_filtered_grid():
USERS = [
{"name": "Alice", "role": "admin"},
{"name": "Bob", "role": "user"},
{"name": "Carol", "role": "admin"},
]
app = ui.App("test")
@app.page("/")
def home(session):
role_filter = ui.state("all")
def rows():
if role_filter.value == "all":
return USERS
return [u for u in USERS if u["role"] == role_filter.value]
ui.Select(
label="Role",
options=["all", "admin", "user"],
value=role_filter,
on_change=role_filter.set,
)
ui.DataGrid(
data=rows,
columns=[{"key": "name"}, {"key": "role"}],
)
with FluxTestClient(app) as client:
client.connect("/")
full = client.html()
assert "Alice" in full and "Bob" in full and "Carol" in full
client.select("Role", "admin")
filtered = client.html()
assert "Alice" in filtered
assert "Carol" in filtered
assert "Bob" not in filtered
Tips¶
- Use explicit
id=on components you want to assert on withclient.text(). client.html()returns the merged HTML after all reactive updates have flushed.- Each
FluxTestClientinstance creates an isolated event loop — tests are fully independent. - You can reuse an app instance across tests but create a fresh
FluxTestClientfor each test.