Skip to content

Commit b3eabbb

Browse files
Add prepend_body with noscript default (#1343)
Co-authored-by: Archmonger <16909269+Archmonger@users.noreply.github.com>
1 parent 633df5b commit b3eabbb

6 files changed

Lines changed: 177 additions & 4 deletions

File tree

src/build_scripts/install_deps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def find_deps(data):
3030

3131

3232
def install_deps():
33-
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
33+
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
3434
with open(pyproject_path, "rb") as f:
3535
pyproject_data = toml.load(f)
3636
find_deps(pyproject_data)

src/reactpy/executors/asgi/pyscript.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from reactpy.executors.utils import vdom_head_to_html
2121
from reactpy.types import ReactPyConfig, VdomDict
22+
from reactpy.utils import reactpy_to_string
2223

2324

2425
class ReactPyCsr(ReactPy):
@@ -32,6 +33,7 @@ def __init__(
3233
initial: str | VdomDict = "",
3334
http_headers: dict[str, str] | None = None,
3435
html_head: VdomDict | None = None,
36+
prepend_body: VdomDict | None = ..., # type: ignore[assignment]
3537
html_lang: str = "en",
3638
**settings: Unpack[ReactPyConfig],
3739
) -> None:
@@ -59,6 +61,9 @@ def __init__(
5961
commonly used to render a loading animation.
6062
http_headers: Additional headers to include in the HTTP response for the base HTML document.
6163
html_head: Additional head elements to include in the HTML response.
64+
prepend_body: Content rendered at the start of the ``<body>`` element.
65+
A ``VdomDict`` constructed via ``html.*`` functions, or ``None`` to omit.
66+
Defaults to ``html.noscript("Enable JavaScript to view this site.")``.
6267
html_lang: The language of the HTML document.
6368
settings:
6469
Global ReactPy configuration settings that affect behavior and performance. Most settings
@@ -78,6 +83,10 @@ def __init__(
7883
self.extra_headers = http_headers or {}
7984
self.dispatcher_pattern = re.compile(f"^{self.dispatcher_path}?")
8085
self.html_head = html_head or html.head()
86+
if prepend_body is not ...:
87+
self.prepend_body = prepend_body
88+
else:
89+
self.prepend_body = html.noscript("Enable JavaScript to view this site.")
8190
self.html_lang = html_lang
8291

8392
def match_dispatch_path(self, scope: AsgiWebsocketScope) -> bool: # nocov
@@ -97,6 +106,10 @@ class ReactPyPyscriptApp(ReactPyApp):
97106
def render_index_html(self) -> None:
98107
"""Process the index.html and store the results in this class."""
99108
head_content = vdom_head_to_html(self.parent.html_head)
109+
if not self.parent.prepend_body or self.parent.prepend_body == ...:
110+
prepend_body = ""
111+
else:
112+
prepend_body = reactpy_to_string(self.parent.prepend_body)
100113
pyscript_setup = pyscript_setup_html(
101114
extra_py=self.parent.extra_py,
102115
extra_js=self.parent.extra_js,
@@ -114,6 +127,7 @@ def render_index_html(self) -> None:
114127
f'<html lang="{self.parent.html_lang}">'
115128
f"{head_content}"
116129
"<body>"
130+
f"{prepend_body}"
117131
f"{pyscript_component}"
118132
"</body>"
119133
"</html>"

src/reactpy/executors/asgi/standalone.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@
2424
AsgiWebsocketScope,
2525
)
2626
from reactpy.executors.pyscript.utils import pyscript_setup_html
27-
from reactpy.executors.utils import server_side_component_html, vdom_head_to_html
27+
from reactpy.executors.utils import (
28+
server_side_component_html,
29+
vdom_head_to_html,
30+
)
2831
from reactpy.types import (
2932
PyScriptOptions,
3033
ReactPyConfig,
3134
RootComponentConstructor,
3235
VdomDict,
3336
)
34-
from reactpy.utils import import_dotted_path, string_to_reactpy
37+
from reactpy.utils import import_dotted_path, reactpy_to_string, string_to_reactpy
3538

3639
_logger = getLogger(__name__)
3740

@@ -45,6 +48,7 @@ def __init__(
4548
*,
4649
http_headers: dict[str, str] | None = None,
4750
html_head: VdomDict | None = None,
51+
prepend_body: VdomDict | None = ..., # type: ignore[assignment]
4852
html_lang: str = "en",
4953
pyscript_setup: bool = False,
5054
pyscript_options: PyScriptOptions | None = None,
@@ -56,6 +60,9 @@ def __init__(
5660
root_component: The root component to render. This app is typically a single page application.
5761
http_headers: Additional headers to include in the HTTP response for the base HTML document.
5862
html_head: Additional head elements to include in the HTML response.
63+
prepend_body: Content rendered at the start of the ``<body>`` element.
64+
A ``VdomDict`` constructed via ``html.*`` functions, or ``None`` to omit.
65+
Defaults to ``html.noscript("Enable JavaScript to view this site.")``.
5966
html_lang: The language of the HTML document.
6067
pyscript_setup: Whether to automatically load PyScript within your HTML head.
6168
pyscript_options: Options to configure PyScript behavior.
@@ -66,6 +73,10 @@ def __init__(
6673
self.extra_headers = http_headers or {}
6774
self.dispatcher_pattern = re.compile(f"^{self.dispatcher_path}?")
6875
self.html_head = html_head or html.head()
76+
if prepend_body is not ...:
77+
self.prepend_body = prepend_body
78+
else:
79+
self.prepend_body = html.noscript("Enable JavaScript to view this site.")
6980
self.html_lang = html_lang
7081

7182
if pyscript_setup:
@@ -229,11 +240,16 @@ async def __call__(
229240

230241
def render_index_html(self) -> None:
231242
"""Process the index.html and store the results in this class."""
243+
if not self.parent.prepend_body or self.parent.prepend_body == ...:
244+
prepend_body = ""
245+
else:
246+
prepend_body = reactpy_to_string(self.parent.prepend_body)
232247
self._index_html = (
233248
"<!doctype html>"
234249
f'<html lang="{self.parent.html_lang}">'
235250
f"{vdom_head_to_html(self.parent.html_head)}"
236251
"<body>"
252+
f"{prepend_body}"
237253
f"{server_side_component_html(element_id='app', class_='', component_path='')}"
238254
"</body>"
239255
"</html>"

tests/test_asgi/test_pyscript.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
# ruff: noqa: S701
2+
import asyncio
23
from pathlib import Path
34

45
import pytest
56
from jinja2 import Environment as JinjaEnvironment
67
from jinja2 import FileSystemLoader as JinjaFileSystemLoader
8+
from requests import request
79
from starlette.applications import Starlette
810
from starlette.routing import Route
911
from starlette.templating import Jinja2Templates
1012

13+
from reactpy import config as _config
1114
from reactpy import html
1215
from reactpy.executors.asgi.pyscript import ReactPyCsr
1316
from reactpy.testing import BackendFixture, DisplayFixture
1417

18+
REACTPY_TESTS_DEFAULT_TIMEOUT = _config.REACTPY_TESTS_DEFAULT_TIMEOUT
19+
1520

1621
@pytest.fixture(scope="module")
1722
async def display(browser):
@@ -102,6 +107,66 @@ def test_bad_file_path():
102107
ReactPyCsr()
103108

104109

110+
async def test_customized_noscript_vdom():
111+
app = ReactPyCsr(
112+
Path(__file__).parent / "pyscript_components" / "root.py",
113+
prepend_body=html.noscript(
114+
html.p({"id": "noscript-message"}, "Please enable JavaScript.")
115+
),
116+
)
117+
118+
async with BackendFixture(app) as server:
119+
url = f"http://{server.host}:{server.port}"
120+
response = await asyncio.to_thread(
121+
request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current
122+
)
123+
assert response.status_code == 200
124+
assert (
125+
'<noscript><p id="noscript-message">Please enable JavaScript.</p></noscript>'
126+
in response.text
127+
)
128+
129+
async with BackendFixture(app) as server:
130+
url = f"http://{server.host}:{server.port}"
131+
response = await asyncio.to_thread(
132+
request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current
133+
)
134+
assert response.status_code == 200
135+
assert (
136+
'<noscript><p id="noscript-message">Please enable JavaScript.</p></noscript>'
137+
in response.text
138+
)
139+
140+
141+
async def test_prepend_body_default_is_noscript():
142+
app = ReactPyCsr(Path(__file__).parent / "pyscript_components" / "root.py")
143+
144+
async with BackendFixture(app) as server:
145+
url = f"http://{server.host}:{server.port}"
146+
response = await asyncio.to_thread(
147+
request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current
148+
)
149+
assert response.status_code == 200
150+
assert (
151+
"<noscript>Enable JavaScript to view this site.</noscript>" in response.text
152+
)
153+
154+
155+
async def test_prepend_body_disabled():
156+
app = ReactPyCsr(
157+
Path(__file__).parent / "pyscript_components" / "root.py",
158+
prepend_body=None,
159+
)
160+
161+
async with BackendFixture(app) as server:
162+
url = f"http://{server.host}:{server.port}"
163+
response = await asyncio.to_thread(
164+
request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current
165+
)
166+
assert response.status_code == 200
167+
assert "<noscript>" not in response.text
168+
169+
105170
async def test_jinja_template_tag(jinja_display: DisplayFixture):
106171
await jinja_display.goto("/")
107172

tests/test_asgi/test_standalone.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,64 @@ def sample():
193193
assert (await new_display.page.title()) == custom_title
194194

195195

196+
async def test_prepend_body_vdom():
197+
@reactpy.component
198+
def sample():
199+
return html.h1("Hello World")
200+
201+
app = ReactPy(
202+
sample,
203+
prepend_body=html.noscript(
204+
html.p({"id": "noscript-message"}, "Please enable JavaScript.")
205+
),
206+
)
207+
208+
async with BackendFixture(app) as server:
209+
url = f"http://{server.host}:{server.port}"
210+
response = await asyncio.to_thread(
211+
request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current
212+
)
213+
assert response.status_code == 200
214+
assert (
215+
'<noscript><p id="noscript-message">Please enable JavaScript.</p></noscript>'
216+
in response.text
217+
)
218+
219+
220+
async def test_prepend_body_default_is_noscript():
221+
@reactpy.component
222+
def sample():
223+
return html.h1("Hello World")
224+
225+
app = ReactPy(sample)
226+
227+
async with BackendFixture(app) as server:
228+
url = f"http://{server.host}:{server.port}"
229+
response = await asyncio.to_thread(
230+
request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current
231+
)
232+
assert response.status_code == 200
233+
assert (
234+
"<noscript>Enable JavaScript to view this site.</noscript>" in response.text
235+
)
236+
237+
238+
async def test_prepend_body_disabled():
239+
@reactpy.component
240+
def sample():
241+
return html.h1("Hello World")
242+
243+
app = ReactPy(sample, prepend_body=None)
244+
245+
async with BackendFixture(app) as server:
246+
url = f"http://{server.host}:{server.port}"
247+
response = await asyncio.to_thread(
248+
request, "GET", url, timeout=REACTPY_TESTS_DEFAULT_TIMEOUT.current
249+
)
250+
assert response.status_code == 200
251+
assert "<noscript>" not in response.text
252+
253+
196254
async def test_head_request():
197255
@reactpy.component
198256
def sample():

tests/test_asgi/test_utils.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from reactpy import config
3+
from reactpy import config, html
44
from reactpy.executors import utils
55

66

@@ -9,6 +9,26 @@ def test_invalid_vdom_head():
99
utils.vdom_head_to_html({"tagName": "invalid"})
1010

1111

12+
def test_prepend_body_content_as_vdom_dict():
13+
from reactpy.utils import reactpy_to_string
14+
15+
assert (
16+
reactpy_to_string(
17+
html.div(html.p({"id": "noscript-message"}, "Please enable JavaScript."))
18+
)
19+
== '<div><p id="noscript-message">Please enable JavaScript.</p></div>'
20+
)
21+
22+
23+
def test_prepend_body_content_as_noscript():
24+
from reactpy.utils import reactpy_to_string
25+
26+
assert (
27+
reactpy_to_string(html.noscript(html.p("Enable JavaScript to view this site.")))
28+
== "<noscript><p>Enable JavaScript to view this site.</p></noscript>"
29+
)
30+
31+
1232
def test_process_settings():
1333
utils.process_settings({"async_rendering": False})
1434
assert config.REACTPY_ASYNC_RENDERING.current is False

0 commit comments

Comments
 (0)