Skip to content

Commit 1dd48c0

Browse files
committed
Merge branch 'main' into release/reflex-0.8.0
2 parents 7e428a5 + d9d6fb8 commit 1dd48c0

26 files changed

+126
-120
lines changed

reflex/.templates/jinja/web/pages/_app.js.jinja2

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{% from "web/pages/macros.js.jinja2" import renderHooks %}
33

44
{% block early_imports %}
5-
import '$/styles/__reflex_global_styles.css'
5+
import reflexGlobalStyles from '$/styles/__reflex_global_styles.css?url';
66
{% endblock %}
77

88
{% block declaration %}
@@ -20,6 +20,10 @@ import * as {{library_alias}} from "{{library_path}}";
2020
{% endblock %}
2121

2222
{% block export %}
23+
export const links = () => [
24+
{ rel: 'stylesheet', href: reflexGlobalStyles, type: 'text/css' }
25+
];
26+
2327
function AppWrap({children}) {
2428
{{ renderHooks(hooks) }}
2529

reflex/constants/installer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def DEPENDENCIES(cls) -> dict[str, str]:
130130
"@react-router/node": cls._react_router_version,
131131
"serve": "14.2.4",
132132
"react": cls._react_version,
133+
"react-helmet": "6.1.0",
133134
"react-dom": cls._react_version,
134135
"isbot": "5.1.26",
135136
"socket.io-client": "4.8.1",

reflex/testing.py

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from collections.abc import AsyncIterator, Callable, Coroutine, Sequence
2323
from http.server import SimpleHTTPRequestHandler
2424
from pathlib import Path
25-
from typing import TYPE_CHECKING, Any, TypeVar
25+
from typing import TYPE_CHECKING, Any, Literal, TypeVar
2626

2727
import psutil
2828
import uvicorn
@@ -526,7 +526,7 @@ def _poll_for(
526526
target: Callable[[], T],
527527
timeout: TimeoutType = None,
528528
step: TimeoutType = None,
529-
) -> T | bool:
529+
) -> T | Literal[False]:
530530
"""Generic polling logic.
531531
532532
Args:
@@ -544,9 +544,10 @@ def _poll_for(
544544
step = POLL_INTERVAL
545545
deadline = time.time() + timeout
546546
while time.time() < deadline:
547-
success = target()
548-
if success:
549-
return success
547+
with contextlib.suppress(Exception):
548+
success = target()
549+
if success:
550+
return success
550551
time.sleep(step)
551552
return False
552553

@@ -850,35 +851,55 @@ def poll_for_clients(self, timeout: TimeoutType = None) -> dict[str, BaseState]:
850851
return state_manager.states
851852

852853
@staticmethod
853-
def poll_for_result(
854-
f: Callable[[], T],
855-
exception: type[Exception] = Exception,
856-
max_attempts: int = 5,
857-
seconds_between_attempts: int = 1,
854+
def poll_for_or_raise_timeout(
855+
target: Callable[[], T],
856+
timeout: TimeoutType = None,
857+
step: TimeoutType = None,
858858
) -> T:
859-
"""Poll for a result from a function.
859+
"""Poll target callable for a truthy return value.
860+
861+
Like `_poll_for`, but raises a `TimeoutError` if the target does not
862+
return a truthy value within the timeout.
860863
861864
Args:
862-
f: function to call
863-
exception: exception to catch
864-
max_attempts: maximum number of attempts
865-
seconds_between_attempts: seconds to wait between
865+
target: callable that returns truthy if polling condition is met.
866+
timeout: max polling time
867+
step: interval between checking target()
866868
867869
Returns:
868-
Result of the function
870+
return value of target() if truthy within timeout
869871
870872
Raises:
871-
AssertionError: if the function does not return a value
873+
TimeoutError: when target does not return a truthy value within timeout
872874
"""
873-
attempts = 0
874-
while attempts < max_attempts:
875-
try:
876-
return f()
877-
except exception: # noqa: PERF203
878-
attempts += 1
879-
time.sleep(seconds_between_attempts)
880-
msg = "Function did not return a value"
881-
raise AssertionError(msg)
875+
result = AppHarness._poll_for(
876+
target=target,
877+
timeout=timeout,
878+
step=step,
879+
)
880+
if result is False:
881+
msg = "Target did not return a truthy value while polling."
882+
raise TimeoutError(msg)
883+
return result
884+
885+
@staticmethod
886+
def expect(
887+
target: Callable[[], T],
888+
timeout: TimeoutType = None,
889+
step: TimeoutType = None,
890+
):
891+
"""Expect a target callable to return a truthy value within the timeout.
892+
893+
Args:
894+
target: callable that returns truthy if polling condition is met.
895+
timeout: max polling time
896+
step: interval between checking target()
897+
"""
898+
AppHarness.poll_for_or_raise_timeout(
899+
target=target,
900+
timeout=timeout,
901+
step=step,
902+
)
882903

883904

884905
class SimpleHTTPRequestHandlerCustomErrors(SimpleHTTPRequestHandler):

tests/integration/test_background_task.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,9 @@ def token(background_task: AppHarness, driver: WebDriver) -> str:
242242
"""
243243
assert background_task.app_instance is not None
244244

245-
token_input = background_task.poll_for_result(
245+
token_input = AppHarness.poll_for_or_raise_timeout(
246246
lambda: driver.find_element(By.ID, "token")
247247
)
248-
assert token_input
249248

250249
# wait for the backend connection to send the token
251250
token = background_task.poll_for_value(token_input, timeout=DEFAULT_TIMEOUT * 2)
@@ -305,10 +304,10 @@ def test_background_task(
305304
increment_button.click()
306305
yield_increment_button.click()
307306
blocking_pause_button.click()
308-
assert background_task._poll_for(lambda: counter.text == "620", timeout=40)
309-
assert background_task._poll_for(lambda: counter_async_cv.text == "620", timeout=40)
307+
AppHarness.expect(lambda: counter.text == "620", timeout=40)
308+
AppHarness.expect(lambda: counter_async_cv.text == "620", timeout=40)
310309
# all tasks should have exited and cleaned up
311-
assert background_task._poll_for(
310+
AppHarness.expect(
312311
lambda: not background_task.app_instance._background_tasks # pyright: ignore [reportOptionalMemberAccess]
313312
)
314313

@@ -333,13 +332,13 @@ def test_nested_async_with_self(
333332

334333
# get a reference to the counter
335334
counter = driver.find_element(By.ID, "counter")
336-
assert background_task._poll_for(lambda: counter.text == "0", timeout=5)
335+
AppHarness.expect(lambda: counter.text == "0", timeout=5)
337336

338337
nested_async_with_self_button.click()
339-
assert background_task._poll_for(lambda: counter.text == "1", timeout=5)
338+
AppHarness.expect(lambda: counter.text == "1", timeout=5)
340339

341340
increment_button.click()
342-
assert background_task._poll_for(lambda: counter.text == "2", timeout=5)
341+
AppHarness.expect(lambda: counter.text == "2", timeout=5)
343342

344343

345344
def test_get_state(
@@ -362,13 +361,13 @@ def test_get_state(
362361

363362
# get a reference to the counter
364363
counter = driver.find_element(By.ID, "counter")
365-
assert background_task._poll_for(lambda: counter.text == "0", timeout=5)
364+
AppHarness.expect(lambda: counter.text == "0", timeout=5)
366365

367366
other_state_button.click()
368-
assert background_task._poll_for(lambda: counter.text == "12", timeout=5)
367+
AppHarness.expect(lambda: counter.text == "12", timeout=5)
369368

370369
increment_button.click()
371-
assert background_task._poll_for(lambda: counter.text == "13", timeout=5)
370+
AppHarness.expect(lambda: counter.text == "13", timeout=5)
372371

373372

374373
def test_yield_in_async_with_self(
@@ -392,7 +391,7 @@ def test_yield_in_async_with_self(
392391

393392
# get a reference to the counter
394393
counter = driver.find_element(By.ID, "counter")
395-
assert background_task._poll_for(lambda: counter.text == "0", timeout=5)
394+
AppHarness.expect(lambda: counter.text == "0", timeout=5)
396395

397396
yield_in_async_with_self_button.click()
398-
assert background_task._poll_for(lambda: counter.text == "2", timeout=5)
397+
AppHarness.expect(lambda: counter.text == "2", timeout=5)

tests/integration/test_client_storage.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,9 @@ async def test_client_side_state(
252252
assert client_side.frontend_url is not None
253253

254254
def poll_for_token():
255-
token_input = client_side.poll_for_result(
255+
token_input = AppHarness.poll_for_or_raise_timeout(
256256
lambda: driver.find_element(By.ID, "token")
257257
)
258-
assert token_input
259258

260259
# wait for the backend connection to send the token
261260
token = client_side.poll_for_value(token_input)
@@ -267,8 +266,8 @@ def set_sub(var: str, value: str):
267266
state_var_input = driver.find_element(By.ID, "state_var")
268267
input_value_input = driver.find_element(By.ID, "input_value")
269268
set_sub_state_button = driver.find_element(By.ID, "set_sub_state")
270-
AppHarness._poll_for(lambda: state_var_input.get_attribute("value") == "")
271-
AppHarness._poll_for(lambda: input_value_input.get_attribute("value") == "")
269+
AppHarness.expect(lambda: state_var_input.get_attribute("value") == "")
270+
AppHarness.expect(lambda: input_value_input.get_attribute("value") == "")
272271

273272
# Set the values.
274273
state_var_input.send_keys(var)
@@ -280,8 +279,8 @@ def set_sub_sub(var: str, value: str):
280279
state_var_input = driver.find_element(By.ID, "state_var")
281280
input_value_input = driver.find_element(By.ID, "input_value")
282281
set_sub_sub_state_button = driver.find_element(By.ID, "set_sub_sub_state")
283-
AppHarness._poll_for(lambda: state_var_input.get_attribute("value") == "")
284-
AppHarness._poll_for(lambda: input_value_input.get_attribute("value") == "")
282+
AppHarness.expect(lambda: state_var_input.get_attribute("value") == "")
283+
AppHarness.expect(lambda: input_value_input.get_attribute("value") == "")
285284

286285
# Set the values.
287286
state_var_input.send_keys(var)
@@ -418,7 +417,7 @@ def set_sub_sub(var: str, value: str):
418417
"value": "c1s%20value",
419418
},
420419
}
421-
AppHarness._poll_for(
420+
AppHarness.expect(
422421
lambda: all(cookie_key in cookie_info_map(driver) for cookie_key in exp_cookies)
423422
)
424423
cookies = cookie_info_map(driver)
@@ -429,7 +428,7 @@ def set_sub_sub(var: str, value: str):
429428

430429
# Test cookie with expiry by itself to avoid timing flakiness
431430
set_sub("c3", "c3 value")
432-
AppHarness._poll_for(
431+
AppHarness.expect(
433432
lambda: f"{sub_state_name}.c3" + FIELD_MARKER in cookie_info_map(driver)
434433
)
435434
c3_cookie = cookie_info_map(driver)[f"{sub_state_name}.c3" + FIELD_MARKER]
@@ -499,7 +498,7 @@ def set_sub_sub(var: str, value: str):
499498
driver.get(client_side.frontend_url.removesuffix("/") + "/foo/")
500499

501500
# get new references to all cookie and local storage elements
502-
c1 = client_side.poll_for_result(lambda: driver.find_element(By.ID, "c1"))
501+
c1 = AppHarness.poll_for_or_raise_timeout(lambda: driver.find_element(By.ID, "c1"))
503502
c2 = driver.find_element(By.ID, "c2")
504503
c3 = driver.find_element(By.ID, "c3")
505504
c4 = driver.find_element(By.ID, "c4")
@@ -541,7 +540,7 @@ def set_sub_sub(var: str, value: str):
541540
driver.refresh()
542541

543542
# wait for the backend connection to send the token (again)
544-
token_input = client_side.poll_for_result(
543+
token_input = AppHarness.poll_for_or_raise_timeout(
545544
lambda: driver.find_element(By.ID, "token")
546545
)
547546
assert token_input
@@ -586,7 +585,7 @@ def set_sub_sub(var: str, value: str):
586585
assert s1s.text == "s1s value"
587586

588587
# make sure c5 cookie shows up on the `/foo` route
589-
AppHarness._poll_for(
588+
AppHarness.expect(
590589
lambda: f"{sub_state_name}.c5" + FIELD_MARKER in cookie_info_map(driver)
591590
)
592591
assert cookie_info_map(driver)[f"{sub_state_name}.c5" + FIELD_MARKER] == {
@@ -612,15 +611,15 @@ def set_sub_sub(var: str, value: str):
612611
set_sub("l6", "l6 value")
613612
l5 = driver.find_element(By.ID, "l5")
614613
l6 = driver.find_element(By.ID, "l6")
615-
assert AppHarness._poll_for(lambda: l6.text == "l6 value")
614+
AppHarness.expect(lambda: l6.text == "l6 value")
616615
assert l5.text == "l5 value"
617616

618617
# Set session storage values in the new tab
619618
set_sub("s1", "other tab s1")
620619
s1 = driver.find_element(By.ID, "s1")
621620
s2 = driver.find_element(By.ID, "s2")
622621
s3 = driver.find_element(By.ID, "s3")
623-
assert AppHarness._poll_for(lambda: s1.text == "other tab s1")
622+
AppHarness.expect(lambda: s1.text == "other tab s1")
624623
assert s2.text == "s2 default"
625624
assert s3.text == ""
626625

@@ -630,13 +629,13 @@ def set_sub_sub(var: str, value: str):
630629
# The values should have updated automatically.
631630
l5 = driver.find_element(By.ID, "l5")
632631
l6 = driver.find_element(By.ID, "l6")
633-
assert AppHarness._poll_for(lambda: l6.text == "l6 value")
632+
AppHarness.expect(lambda: l6.text == "l6 value")
634633
assert l5.text == "l5 value"
635634

636635
s1 = driver.find_element(By.ID, "s1")
637636
s2 = driver.find_element(By.ID, "s2")
638637
s3 = driver.find_element(By.ID, "s3")
639-
assert AppHarness._poll_for(lambda: s1.text == "s1 value")
638+
AppHarness.expect(lambda: s1.text == "s1 value")
640639
assert s2.text == "s2 value"
641640
assert s3.text == "s3 value"
642641

@@ -748,10 +747,9 @@ async def poll_for_c1_set():
748747
driver.refresh()
749748

750749
# wait for the backend connection to send the token (again)
751-
token_input = client_side.poll_for_result(
750+
token_input = AppHarness.poll_for_or_raise_timeout(
752751
lambda: driver.find_element(By.ID, "token")
753752
)
754-
assert token_input
755753
token = client_side.poll_for_value(token_input)
756754
assert token is not None
757755

tests/integration/test_computed_vars.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,9 @@ def token(computed_vars: AppHarness, driver: WebDriver) -> str:
172172
The token for the connected client
173173
"""
174174
assert computed_vars.app_instance is not None
175-
token_input = computed_vars.poll_for_result(
175+
token_input = AppHarness.poll_for_or_raise_timeout(
176176
lambda: driver.find_element(By.ID, "token")
177177
)
178-
assert token_input
179178

180179
# wait for the backend connection to send the token
181180
token = computed_vars.poll_for_value(token_input, timeout=DEFAULT_TIMEOUT * 2)

tests/integration/test_connection_banner.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ async def test_connection_banner(connection_banner: AppHarness):
148148
driver = connection_banner.frontend()
149149

150150
_assert_token(connection_banner, driver)
151-
assert connection_banner._poll_for(lambda: not has_error_modal(driver))
151+
AppHarness.expect(lambda: not has_error_modal(driver))
152152

153153
delay_button = driver.find_element(By.ID, "delay")
154154
increment_button = driver.find_element(By.ID, "increment")
@@ -170,7 +170,7 @@ async def test_connection_banner(connection_banner: AppHarness):
170170
connection_banner.backend_thread.join()
171171

172172
# Error modal should now be displayed
173-
assert connection_banner._poll_for(lambda: has_error_modal(driver))
173+
AppHarness.expect(lambda: has_error_modal(driver))
174174

175175
# Increment the counter with backend down
176176
increment_button.click()
@@ -183,7 +183,7 @@ async def test_connection_banner(connection_banner: AppHarness):
183183
await connection_banner._reset_backend_state_manager()
184184

185185
# Banner should be gone now
186-
assert connection_banner._poll_for(lambda: not has_error_modal(driver))
186+
AppHarness.expect(lambda: not has_error_modal(driver))
187187

188188
# Count should have incremented after coming back up
189189
assert connection_banner.poll_for_value(counter_element, exp_not_equal="1") == "2"
@@ -206,17 +206,17 @@ async def test_cloud_banner(
206206
driver.add_cookie({"name": "backend-enabled", "value": "truly"})
207207
driver.refresh()
208208
_assert_token(connection_banner, driver)
209-
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
209+
AppHarness.expect(lambda: not has_cloud_banner(driver))
210210

211211
driver.add_cookie({"name": "backend-enabled", "value": "false"})
212212
driver.refresh()
213213
if simulate_compile_context == constants.CompileContext.DEPLOY:
214-
assert connection_banner._poll_for(lambda: has_cloud_banner(driver))
214+
AppHarness.expect(lambda: has_cloud_banner(driver))
215215
else:
216216
_assert_token(connection_banner, driver)
217-
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
217+
AppHarness.expect(lambda: not has_cloud_banner(driver))
218218

219219
driver.delete_cookie("backend-enabled")
220220
driver.refresh()
221221
_assert_token(connection_banner, driver)
222-
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
222+
AppHarness.expect(lambda: not has_cloud_banner(driver))

0 commit comments

Comments
 (0)