CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-reactpy

Reactive user interfaces with pure Python

Pending
Overview
Eval results
Files

testing.mddocs/

Testing Support

Comprehensive testing framework with fixtures and utilities for component testing. ReactPy provides robust testing capabilities for validating component behavior, user interactions, and application logic.

Capabilities

Backend Fixture

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 TestComponent

Display Fixture

Test 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 object

Usage 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"

Polling Utilities

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 succeeds
  • timeout: 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!"

Log Testing Utilities

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 match

Usage 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

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 True

Component Testing Patterns

Common 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"])

Test Configuration

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 = False

Install with Tessl CLI

npx tessl i tessl/pypi-reactpy

docs

backend.md

components.md

configuration.md

events.md

hooks.md

html-elements.md

index.md

svg-elements.md

testing.md

vdom.md

web-modules.md

widgets.md

tile.json