Skip to content

Commit a8c19b8

Browse files
committed
tests for middleware
1 parent a058403 commit a8c19b8

File tree

6 files changed

+146
-23
lines changed

6 files changed

+146
-23
lines changed

Diff for: src/reactpy/asgi/middleware.py

+14-11
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ def __init__(
4747
must be valid to Python's import system.
4848
settings: Global ReactPy configuration settings that affect behavior and performance.
4949
"""
50+
# Validate the configuration
51+
if "path_prefix" in settings:
52+
reason = check_path(settings["path_prefix"])
53+
if reason:
54+
raise ValueError(
55+
f'Invalid `path_prefix` of "{settings["path_prefix"]}". {reason}'
56+
)
57+
if "web_modules_dir" in settings and not settings["web_modules_dir"].exists():
58+
raise ValueError(
59+
f'Web modules directory "{settings["web_modules_dir"]}" does not exist.'
60+
)
61+
5062
# Process global settings
5163
process_settings(settings)
5264

@@ -69,15 +81,6 @@ def __init__(
6981
self.web_modules_dir = config.REACTPY_WEB_MODULES_DIR.current
7082
self.static_dir = Path(__file__).parent.parent / "static"
7183

72-
# Validate the configuration
73-
reason = check_path(self.path_prefix)
74-
if reason:
75-
raise ValueError(f"Invalid `path_prefix`. {reason}")
76-
if not self.web_modules_dir.exists():
77-
raise ValueError(
78-
f"Web modules directory {self.web_modules_dir} does not exist."
79-
)
80-
8184
# Initialize the sub-applications
8285
self.component_dispatch_app = ComponentDispatchApp(parent=self)
8386
self.static_file_app = StaticFileApp(parent=self)
@@ -162,7 +165,7 @@ async def run_dispatcher(
162165
# Determine component to serve by analyzing the URL and/or class parameters.
163166
if self.parent.multiple_root_components:
164167
url_match = re.match(self.parent.dispatcher_pattern, scope["path"])
165-
if not url_match:
168+
if not url_match: # pragma: no cover
166169
raise RuntimeError("Could not find component in URL path.")
167170
dotted_path = url_match["dotted_path"]
168171
if dotted_path not in self.parent.root_components:
@@ -172,7 +175,7 @@ async def run_dispatcher(
172175
component = self.parent.root_components[dotted_path]
173176
elif self.parent.root_component:
174177
component = self.parent.root_component
175-
else:
178+
else: # pragma: no cover
176179
raise RuntimeError("No root component provided.")
177180

178181
# Create a connection object by analyzing the websocket's query string.

Diff for: src/reactpy/asgi/utils.py

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
def import_dotted_path(dotted_path: str) -> Any:
1818
"""Imports a dotted path and returns the callable."""
19+
if "." not in dotted_path:
20+
raise ValueError(f"{dotted_path!r} is not a valid dotted path.")
21+
1922
module_name, component_name = dotted_path.rsplit(".", 1)
2023

2124
try:

Diff for: src/reactpy/testing/backend.py

+22-11
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import uvicorn
1212
from asgiref import typing as asgi_types
1313

14-
from reactpy.asgi.standalone import ReactPy
14+
from reactpy.asgi.standalone import ReactPy, ReactPyMiddleware
1515
from reactpy.config import REACTPY_TESTS_DEFAULT_TIMEOUT
1616
from reactpy.core.component import component
1717
from reactpy.core.hooks import use_callback, use_effect, use_state
@@ -21,7 +21,7 @@
2121
list_logged_exceptions,
2222
)
2323
from reactpy.testing.utils import find_available_port
24-
from reactpy.types import ComponentConstructor
24+
from reactpy.types import ComponentConstructor, ReactPyConfig
2525
from reactpy.utils import Ref
2626

2727

@@ -37,7 +37,7 @@ class BackendFixture:
3737
server.mount(MyComponent)
3838
"""
3939

40-
_records: list[logging.LogRecord]
40+
log_records: list[logging.LogRecord]
4141
_server_future: asyncio.Task[Any]
4242
_exit_stack = AsyncExitStack()
4343

@@ -47,25 +47,33 @@ def __init__(
4747
host: str = "127.0.0.1",
4848
port: int | None = None,
4949
timeout: float | None = None,
50+
reactpy_config: ReactPyConfig | None = None,
5051
) -> None:
5152
self.host = host
5253
self.port = port or find_available_port(host)
53-
self.mount, self._root_component = _hotswap()
54+
self.mount = mount_to_hotswap
5455
self.timeout = (
5556
REACTPY_TESTS_DEFAULT_TIMEOUT.current if timeout is None else timeout
5657
)
57-
self._app = app or ReactPy(self._root_component)
58+
if isinstance(app, (ReactPyMiddleware, ReactPy)):
59+
self._app = app
60+
elif app:
61+
self._app = ReactPyMiddleware(
62+
app,
63+
root_components=["reactpy.testing.backend.root_hotswap_component"],
64+
**(reactpy_config or {}),
65+
)
66+
else:
67+
self._app = ReactPy(
68+
root_hotswap_component,
69+
**(reactpy_config or {}),
70+
)
5871
self.webserver = uvicorn.Server(
5972
uvicorn.Config(
6073
app=self._app, host=self.host, port=self.port, loop="asyncio"
6174
)
6275
)
6376

64-
@property
65-
def log_records(self) -> list[logging.LogRecord]:
66-
"""A list of captured log records"""
67-
return self._records
68-
6977
def url(self, path: str = "", query: Any | None = None) -> str:
7078
"""Return a URL string pointing to the host and point of the server
7179
@@ -108,7 +116,7 @@ def list_logged_exceptions(
108116

109117
async def __aenter__(self) -> BackendFixture:
110118
self._exit_stack = AsyncExitStack()
111-
self._records = self._exit_stack.enter_context(capture_reactpy_logs())
119+
self.log_records = self._exit_stack.enter_context(capture_reactpy_logs())
112120

113121
# Wait for the server to start
114122
Thread(target=self.webserver.run, daemon=True).start()
@@ -215,3 +223,6 @@ def swap(constructor: Callable[[], Any] | None) -> None:
215223
constructor_ref.current = constructor or (lambda: None)
216224

217225
return swap, HotSwap
226+
227+
228+
mount_to_hotswap, root_hotswap_component = _hotswap()

Diff for: tests/templates/index.html

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head></head>
5+
6+
<body>
7+
<div id="app"></div>
8+
{% component "reactpy.testing.backend.root_hotswap_component" %}
9+
</body>
10+
11+
</html>

Diff for: tests/test_asgi/test_middleware.py

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
from jinja2 import Environment as JinjaEnvironment
5+
from jinja2 import FileSystemLoader as JinjaFileSystemLoader
6+
from starlette.applications import Starlette
7+
from starlette.routing import Route
8+
from starlette.templating import Jinja2Templates
9+
10+
import reactpy
11+
from reactpy.asgi.middleware import ReactPyMiddleware
12+
from reactpy.testing import BackendFixture, DisplayFixture
13+
14+
15+
@pytest.fixture()
16+
async def display(page):
17+
templates = Jinja2Templates(
18+
env=JinjaEnvironment(
19+
loader=JinjaFileSystemLoader("tests/templates"),
20+
extensions=["reactpy.jinja.ReactPyTemplateTag"],
21+
)
22+
)
23+
24+
async def homepage(request):
25+
return templates.TemplateResponse(request, "index.html")
26+
27+
app = Starlette(routes=[Route("/", homepage)])
28+
29+
async with BackendFixture(app) as server:
30+
async with DisplayFixture(backend=server, driver=page) as new_display:
31+
yield new_display
32+
33+
34+
def test_invalid_path_prefix():
35+
with pytest.raises(ValueError, match="Invalid `path_prefix`*"):
36+
37+
async def app(scope, receive, send):
38+
pass
39+
40+
reactpy.ReactPyMiddleware(app, root_components=["abc"], path_prefix="invalid")
41+
42+
43+
def test_invalid_web_modules_dir():
44+
with pytest.raises(
45+
ValueError, match='Web modules directory "invalid" does not exist.'
46+
):
47+
48+
async def app(scope, receive, send):
49+
pass
50+
51+
reactpy.ReactPyMiddleware(
52+
app, root_components=["abc"], web_modules_dir=Path("invalid")
53+
)
54+
55+
56+
async def test_unregistered_root_component():
57+
templates = Jinja2Templates(
58+
env=JinjaEnvironment(
59+
loader=JinjaFileSystemLoader("tests/templates"),
60+
extensions=["reactpy.jinja.ReactPyTemplateTag"],
61+
)
62+
)
63+
64+
async def homepage(request):
65+
return templates.TemplateResponse(request, "index.html")
66+
67+
@reactpy.component
68+
def Stub():
69+
return reactpy.html.p("Hello")
70+
71+
app = Starlette(routes=[Route("/", homepage)])
72+
app = ReactPyMiddleware(app, root_components=["tests.sample.SampleApp"])
73+
74+
async with BackendFixture(app) as server:
75+
async with DisplayFixture(backend=server) as new_display:
76+
await new_display.show(Stub)
77+
assert (
78+
"Attempting to use an unregistered root component"
79+
in server.log_records[-1].message
80+
)
81+
82+
83+
async def test_display_simple_hello_world(display: DisplayFixture):
84+
@reactpy.component
85+
def Hello():
86+
return reactpy.html.p({"id": "hello"}, ["Hello World"])
87+
88+
await display.show(Hello)
89+
90+
await display.page.wait_for_selector("#hello")
91+
92+
# test that we can reconnect successfully
93+
await display.page.reload()
94+
95+
await display.page.wait_for_selector("#hello")

Diff for: tests/test_asgi/test_standalone.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414
@pytest.fixture()
15-
async def display(page, request):
15+
async def display(page):
1616
async with BackendFixture() as server:
1717
async with DisplayFixture(backend=server, driver=page) as display:
1818
yield display

0 commit comments

Comments
 (0)