0
# Testing Support
1
2
Comprehensive testing framework with fixtures and utilities for component testing. ReactPy provides robust testing capabilities for validating component behavior, user interactions, and application logic.
3
4
## Capabilities
5
6
### Backend Fixture
7
8
Test fixture for backend integration testing:
9
10
```python { .api }
11
class BackendFixture:
12
def __init__(self, implementation): ...
13
async def mount(self, component: ComponentType) -> None: ...
14
async def unmount(self) -> None: ...
15
def get_component(self) -> ComponentType: ...
16
```
17
18
**Usage Examples:**
19
20
```python
21
import pytest
22
from reactpy.testing import BackendFixture
23
from reactpy.backend.fastapi import create_development_app
24
25
@pytest.fixture
26
async def backend():
27
fixture = BackendFixture(create_development_app)
28
yield fixture
29
await fixture.unmount()
30
31
@pytest.mark.asyncio
32
async def test_component_mounting(backend):
33
@component
34
def TestComponent():
35
return html.h1("Test Component")
36
37
await backend.mount(TestComponent)
38
mounted_component = backend.get_component()
39
assert mounted_component is TestComponent
40
```
41
42
### Display Fixture
43
44
Test fixture for browser-based testing with Playwright integration:
45
46
```python { .api }
47
class DisplayFixture:
48
def __init__(self, backend_fixture: BackendFixture): ...
49
def goto(self, path: str) -> None: ...
50
async def mount(self, component: ComponentType) -> None: ...
51
@property
52
def page(self) -> Page: ... # Playwright Page object
53
```
54
55
**Usage Examples:**
56
57
```python
58
import pytest
59
from reactpy.testing import DisplayFixture, BackendFixture
60
61
@pytest.fixture
62
async def display(backend):
63
fixture = DisplayFixture(backend)
64
yield fixture
65
66
@pytest.mark.asyncio
67
async def test_component_rendering(display):
68
@component
69
def ClickableButton():
70
count, set_count = use_state(0)
71
72
return html.div(
73
html.h1(f"Count: {count}", id="count"),
74
html.button(
75
{"onClick": lambda: set_count(count + 1), "id": "increment"},
76
"Increment"
77
)
78
)
79
80
await display.mount(ClickableButton)
81
82
# Test initial state
83
count_element = await display.page.locator("#count")
84
assert await count_element.text_content() == "Count: 0"
85
86
# Test interaction
87
button = await display.page.locator("#increment")
88
await button.click()
89
90
# Verify state update
91
assert await count_element.text_content() == "Count: 1"
92
```
93
94
### Polling Utilities
95
96
Utility for waiting on asynchronous conditions:
97
98
```python { .api }
99
async def poll(coroutine: Callable[[], Awaitable[T]], timeout: float = None) -> T: ...
100
```
101
102
**Parameters:**
103
- `coroutine`: Async function to poll until it succeeds
104
- `timeout`: Maximum time to wait (uses REACTPY_TESTING_DEFAULT_TIMEOUT if None)
105
106
**Returns:** Result of the coroutine when successful
107
108
**Usage Examples:**
109
110
```python
111
from reactpy.testing import poll
112
113
@pytest.mark.asyncio
114
async def test_async_state_update(display):
115
@component
116
def AsyncComponent():
117
data, set_data = use_state(None)
118
119
async def load_data():
120
# Simulate async data loading
121
await asyncio.sleep(0.1)
122
set_data("Loaded!")
123
124
use_effect(lambda: asyncio.create_task(load_data()), [])
125
126
return html.div(
127
html.p(data or "Loading...", id="status")
128
)
129
130
await display.mount(AsyncComponent)
131
132
# Poll until data is loaded
133
async def check_loaded():
134
status = await display.page.locator("#status")
135
text = await status.text_content()
136
assert text == "Loaded!"
137
return text
138
139
result = await poll(check_loaded, timeout=5.0)
140
assert result == "Loaded!"
141
```
142
143
### Log Testing Utilities
144
145
Assert logging behavior in components:
146
147
```python { .api }
148
def assert_reactpy_did_log(caplog, *patterns) -> None: ...
149
def assert_reactpy_did_not_log(caplog, *patterns) -> None: ...
150
```
151
152
**Parameters:**
153
- `caplog`: pytest caplog fixture
154
- `*patterns`: Log message patterns to match
155
156
**Usage Examples:**
157
158
```python
159
import logging
160
from reactpy.testing import assert_reactpy_did_log, assert_reactpy_did_not_log
161
162
def test_component_logging(caplog):
163
@component
164
def LoggingComponent():
165
logging.info("Component rendered")
166
return html.div("Content")
167
168
# Render component
169
layout = Layout(LoggingComponent)
170
layout.render()
171
172
# Assert logging occurred
173
assert_reactpy_did_log(caplog, "Component rendered")
174
assert_reactpy_did_not_log(caplog, "Error occurred")
175
```
176
177
### Static Event Handler
178
179
Static event handler for testing without browser interaction:
180
181
```python { .api }
182
class StaticEventHandler:
183
def __init__(self, function: Callable): ...
184
def __call__(self, event_data: dict) -> Any: ...
185
```
186
187
**Usage Examples:**
188
189
```python
190
from reactpy.testing import StaticEventHandler
191
192
def test_event_handler_logic():
193
clicked = False
194
195
def handle_click(event_data):
196
nonlocal clicked
197
clicked = True
198
199
handler = StaticEventHandler(handle_click)
200
201
# Simulate event
202
handler({"type": "click", "target": {"id": "button"}})
203
204
assert clicked is True
205
```
206
207
### Component Testing Patterns
208
209
Common patterns for testing ReactPy components:
210
211
```python
212
@pytest.mark.asyncio
213
async def test_form_submission(display):
214
submitted_data = None
215
216
@component
217
def ContactForm():
218
name, set_name = use_state("")
219
email, set_email = use_state("")
220
221
def handle_submit(event_data):
222
nonlocal submitted_data
223
submitted_data = {"name": name, "email": email}
224
225
return html.form(
226
{"onSubmit": handle_submit, "id": "contact-form"},
227
html.input({
228
"id": "name",
229
"value": name,
230
"onChange": lambda e: set_name(e["target"]["value"])
231
}),
232
html.input({
233
"id": "email",
234
"type": "email",
235
"value": email,
236
"onChange": lambda e: set_email(e["target"]["value"])
237
}),
238
html.button({"type": "submit"}, "Submit")
239
)
240
241
await display.mount(ContactForm)
242
243
# Fill form
244
await display.page.locator("#name").fill("John Doe")
245
await display.page.locator("#email").fill("john@example.com")
246
247
# Submit form
248
await display.page.locator("#contact-form").dispatch_event("submit")
249
250
# Wait for form processing
251
async def check_submission():
252
assert submitted_data is not None
253
assert submitted_data["name"] == "John Doe"
254
assert submitted_data["email"] == "john@example.com"
255
256
await poll(check_submission)
257
258
@pytest.mark.asyncio
259
async def test_conditional_rendering(display):
260
@component
261
def ConditionalComponent():
262
show_content, set_show_content = use_state(False)
263
264
return html.div(
265
html.button(
266
{"id": "toggle", "onClick": lambda: set_show_content(not show_content)},
267
"Toggle"
268
),
269
html.div(
270
{"id": "content", "style": {"display": "block" if show_content else "none"}},
271
"Hidden Content"
272
) if show_content else None
273
)
274
275
await display.mount(ConditionalComponent)
276
277
# Initially hidden
278
content = display.page.locator("#content")
279
assert await content.count() == 0
280
281
# Toggle visibility
282
await display.page.locator("#toggle").click()
283
284
# Now visible
285
await poll(lambda: content.count() == 1)
286
assert await content.text_content() == "Hidden Content"
287
288
@pytest.mark.asyncio
289
async def test_state_persistence(display):
290
@component
291
def CounterWithPersistence():
292
count, set_count = use_state(0)
293
294
# Persist to localStorage
295
use_effect(
296
lambda: display.page.evaluate(f"localStorage.setItem('count', {count})"),
297
[count]
298
)
299
300
return html.div(
301
html.span(f"Count: {count}", id="count"),
302
html.button(
303
{"id": "increment", "onClick": lambda: set_count(count + 1)},
304
"+"
305
)
306
)
307
308
await display.mount(CounterWithPersistence)
309
310
# Increment counter
311
await display.page.locator("#increment").click()
312
await display.page.locator("#increment").click()
313
314
# Check persistence
315
stored_count = await display.page.evaluate("localStorage.getItem('count')")
316
assert stored_count == "2"
317
318
def test_hook_behavior():
319
"""Test hooks outside of browser context"""
320
321
@component
322
def HookTestComponent():
323
# Test use_state
324
count, set_count = use_state(10)
325
assert count == 10
326
327
# Test use_ref
328
ref = use_ref("initial")
329
assert ref.current == "initial"
330
ref.current = "modified"
331
assert ref.current == "modified"
332
333
# Test use_memo
334
expensive_result = use_memo(
335
lambda: sum(range(100)),
336
[]
337
)
338
assert expensive_result == 4950
339
340
return html.div(f"Count: {count}")
341
342
# Create layout to test component
343
layout = Layout(HookTestComponent)
344
result = layout.render()
345
346
# Verify VDOM structure
347
assert result["body"]["root"]["tagName"] == "div"
348
assert "Count: 10" in str(result["body"]["root"]["children"])
349
```
350
351
### Test Configuration
352
353
Configure testing environment:
354
355
```python
356
import pytest
357
from reactpy import config
358
359
@pytest.fixture(autouse=True)
360
def setup_test_config():
361
# Set test-specific configuration
362
config.REACTPY_DEBUG_MODE = True
363
config.REACTPY_TESTING_DEFAULT_TIMEOUT = 10.0
364
365
yield
366
367
# Reset configuration after tests
368
config.REACTPY_DEBUG_MODE = False
369
```