or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

backend.mdcomponents.mdconfiguration.mdevents.mdhooks.mdhtml-elements.mdindex.mdsvg-elements.mdtesting.mdvdom.mdweb-modules.mdwidgets.md

testing.mddocs/

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

```