Reactive user interfaces with pure Python
—
Comprehensive testing framework with fixtures and utilities for component testing. ReactPy provides robust testing capabilities for validating component behavior, user interactions, and application logic.
Test fixture for backend integration testing:
class BackendFixture:
def __init__(self, implementation): ...
async def mount(self, component: ComponentType) -> None: ...
async def unmount(self) -> None: ...
def get_component(self) -> ComponentType: ...Usage Examples:
import pytest
from reactpy.testing import BackendFixture
from reactpy.backend.fastapi import create_development_app
@pytest.fixture
async def backend():
fixture = BackendFixture(create_development_app)
yield fixture
await fixture.unmount()
@pytest.mark.asyncio
async def test_component_mounting(backend):
@component
def TestComponent():
return html.h1("Test Component")
await backend.mount(TestComponent)
mounted_component = backend.get_component()
assert mounted_component is TestComponentTest fixture for browser-based testing with Playwright integration:
class DisplayFixture:
def __init__(self, backend_fixture: BackendFixture): ...
def goto(self, path: str) -> None: ...
async def mount(self, component: ComponentType) -> None: ...
@property
def page(self) -> Page: ... # Playwright Page objectUsage Examples:
import pytest
from reactpy.testing import DisplayFixture, BackendFixture
@pytest.fixture
async def display(backend):
fixture = DisplayFixture(backend)
yield fixture
@pytest.mark.asyncio
async def test_component_rendering(display):
@component
def ClickableButton():
count, set_count = use_state(0)
return html.div(
html.h1(f"Count: {count}", id="count"),
html.button(
{"onClick": lambda: set_count(count + 1), "id": "increment"},
"Increment"
)
)
await display.mount(ClickableButton)
# Test initial state
count_element = await display.page.locator("#count")
assert await count_element.text_content() == "Count: 0"
# Test interaction
button = await display.page.locator("#increment")
await button.click()
# Verify state update
assert await count_element.text_content() == "Count: 1"Utility for waiting on asynchronous conditions:
async def poll(coroutine: Callable[[], Awaitable[T]], timeout: float = None) -> T: ...Parameters:
coroutine: Async function to poll until it succeedstimeout: Maximum time to wait (uses REACTPY_TESTING_DEFAULT_TIMEOUT if None)Returns: Result of the coroutine when successful
Usage Examples:
from reactpy.testing import poll
@pytest.mark.asyncio
async def test_async_state_update(display):
@component
def AsyncComponent():
data, set_data = use_state(None)
async def load_data():
# Simulate async data loading
await asyncio.sleep(0.1)
set_data("Loaded!")
use_effect(lambda: asyncio.create_task(load_data()), [])
return html.div(
html.p(data or "Loading...", id="status")
)
await display.mount(AsyncComponent)
# Poll until data is loaded
async def check_loaded():
status = await display.page.locator("#status")
text = await status.text_content()
assert text == "Loaded!"
return text
result = await poll(check_loaded, timeout=5.0)
assert result == "Loaded!"Assert logging behavior in components:
def assert_reactpy_did_log(caplog, *patterns) -> None: ...
def assert_reactpy_did_not_log(caplog, *patterns) -> None: ...Parameters:
caplog: pytest caplog fixture*patterns: Log message patterns to matchUsage Examples:
import logging
from reactpy.testing import assert_reactpy_did_log, assert_reactpy_did_not_log
def test_component_logging(caplog):
@component
def LoggingComponent():
logging.info("Component rendered")
return html.div("Content")
# Render component
layout = Layout(LoggingComponent)
layout.render()
# Assert logging occurred
assert_reactpy_did_log(caplog, "Component rendered")
assert_reactpy_did_not_log(caplog, "Error occurred")Static event handler for testing without browser interaction:
class StaticEventHandler:
def __init__(self, function: Callable): ...
def __call__(self, event_data: dict) -> Any: ...Usage Examples:
from reactpy.testing import StaticEventHandler
def test_event_handler_logic():
clicked = False
def handle_click(event_data):
nonlocal clicked
clicked = True
handler = StaticEventHandler(handle_click)
# Simulate event
handler({"type": "click", "target": {"id": "button"}})
assert clicked is TrueCommon patterns for testing ReactPy components:
@pytest.mark.asyncio
async def test_form_submission(display):
submitted_data = None
@component
def ContactForm():
name, set_name = use_state("")
email, set_email = use_state("")
def handle_submit(event_data):
nonlocal submitted_data
submitted_data = {"name": name, "email": email}
return html.form(
{"onSubmit": handle_submit, "id": "contact-form"},
html.input({
"id": "name",
"value": name,
"onChange": lambda e: set_name(e["target"]["value"])
}),
html.input({
"id": "email",
"type": "email",
"value": email,
"onChange": lambda e: set_email(e["target"]["value"])
}),
html.button({"type": "submit"}, "Submit")
)
await display.mount(ContactForm)
# Fill form
await display.page.locator("#name").fill("John Doe")
await display.page.locator("#email").fill("john@example.com")
# Submit form
await display.page.locator("#contact-form").dispatch_event("submit")
# Wait for form processing
async def check_submission():
assert submitted_data is not None
assert submitted_data["name"] == "John Doe"
assert submitted_data["email"] == "john@example.com"
await poll(check_submission)
@pytest.mark.asyncio
async def test_conditional_rendering(display):
@component
def ConditionalComponent():
show_content, set_show_content = use_state(False)
return html.div(
html.button(
{"id": "toggle", "onClick": lambda: set_show_content(not show_content)},
"Toggle"
),
html.div(
{"id": "content", "style": {"display": "block" if show_content else "none"}},
"Hidden Content"
) if show_content else None
)
await display.mount(ConditionalComponent)
# Initially hidden
content = display.page.locator("#content")
assert await content.count() == 0
# Toggle visibility
await display.page.locator("#toggle").click()
# Now visible
await poll(lambda: content.count() == 1)
assert await content.text_content() == "Hidden Content"
@pytest.mark.asyncio
async def test_state_persistence(display):
@component
def CounterWithPersistence():
count, set_count = use_state(0)
# Persist to localStorage
use_effect(
lambda: display.page.evaluate(f"localStorage.setItem('count', {count})"),
[count]
)
return html.div(
html.span(f"Count: {count}", id="count"),
html.button(
{"id": "increment", "onClick": lambda: set_count(count + 1)},
"+"
)
)
await display.mount(CounterWithPersistence)
# Increment counter
await display.page.locator("#increment").click()
await display.page.locator("#increment").click()
# Check persistence
stored_count = await display.page.evaluate("localStorage.getItem('count')")
assert stored_count == "2"
def test_hook_behavior():
"""Test hooks outside of browser context"""
@component
def HookTestComponent():
# Test use_state
count, set_count = use_state(10)
assert count == 10
# Test use_ref
ref = use_ref("initial")
assert ref.current == "initial"
ref.current = "modified"
assert ref.current == "modified"
# Test use_memo
expensive_result = use_memo(
lambda: sum(range(100)),
[]
)
assert expensive_result == 4950
return html.div(f"Count: {count}")
# Create layout to test component
layout = Layout(HookTestComponent)
result = layout.render()
# Verify VDOM structure
assert result["body"]["root"]["tagName"] == "div"
assert "Count: 10" in str(result["body"]["root"]["children"])Configure testing environment:
import pytest
from reactpy import config
@pytest.fixture(autouse=True)
def setup_test_config():
# Set test-specific configuration
config.REACTPY_DEBUG_MODE = True
config.REACTPY_TESTING_DEFAULT_TIMEOUT = 10.0
yield
# Reset configuration after tests
config.REACTPY_DEBUG_MODE = FalseInstall with Tessl CLI
npx tessl i tessl/pypi-reactpy