Skip to content

Commit 740f244

Browse files
committed
Merge remote-tracking branch 'origin/main' into masenf/default-color-mode
2 parents fe43c19 + a3be76f commit 740f244

35 files changed

+896
-246
lines changed

integration/test_client_storage.py

+56-11
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ class ClientSideSubState(ClientSideState):
4141
l3: str = rx.LocalStorage(name="l3")
4242
l4: str = rx.LocalStorage("l4 default")
4343

44+
# Sync'd local storage
45+
l5: str = rx.LocalStorage(sync=True)
46+
l6: str = rx.LocalStorage(sync=True, name="l6")
47+
48+
def set_l6(self, my_param: str):
49+
self.l6 = my_param
50+
4451
def set_var(self):
4552
setattr(self, self.state_var, self.input_value)
4653
self.state_var = self.input_value = ""
@@ -93,6 +100,8 @@ def index():
93100
rx.box(ClientSideSubState.l2, id="l2"),
94101
rx.box(ClientSideSubState.l3, id="l3"),
95102
rx.box(ClientSideSubState.l4, id="l4"),
103+
rx.box(ClientSideSubState.l5, id="l5"),
104+
rx.box(ClientSideSubState.l6, id="l6"),
96105
rx.box(ClientSideSubSubState.c1s, id="c1s"),
97106
rx.box(ClientSideSubSubState.l1s, id="l1s"),
98107
)
@@ -191,33 +200,44 @@ async def test_client_side_state(
191200
"""
192201
assert client_side.app_instance is not None
193202
assert client_side.frontend_url is not None
194-
token_input = driver.find_element(By.ID, "token")
195-
assert token_input
196203

197-
# wait for the backend connection to send the token
198-
token = client_side.poll_for_value(token_input)
199-
assert token is not None
204+
def poll_for_token():
205+
token_input = driver.find_element(By.ID, "token")
206+
assert token_input
200207

201-
# get a reference to the cookie manipulation form
202-
state_var_input = driver.find_element(By.ID, "state_var")
203-
input_value_input = driver.find_element(By.ID, "input_value")
204-
set_sub_state_button = driver.find_element(By.ID, "set_sub_state")
205-
set_sub_sub_state_button = driver.find_element(By.ID, "set_sub_sub_state")
208+
# wait for the backend connection to send the token
209+
token = client_side.poll_for_value(token_input)
210+
assert token is not None
211+
return token
206212

207213
def set_sub(var: str, value: str):
214+
# Get a reference to the cookie manipulation form.
215+
state_var_input = driver.find_element(By.ID, "state_var")
216+
input_value_input = driver.find_element(By.ID, "input_value")
217+
set_sub_state_button = driver.find_element(By.ID, "set_sub_state")
208218
AppHarness._poll_for(lambda: state_var_input.get_attribute("value") == "")
209219
AppHarness._poll_for(lambda: input_value_input.get_attribute("value") == "")
220+
221+
# Set the values.
210222
state_var_input.send_keys(var)
211223
input_value_input.send_keys(value)
212224
set_sub_state_button.click()
213225

214226
def set_sub_sub(var: str, value: str):
227+
# Get a reference to the cookie manipulation form.
228+
state_var_input = driver.find_element(By.ID, "state_var")
229+
input_value_input = driver.find_element(By.ID, "input_value")
230+
set_sub_sub_state_button = driver.find_element(By.ID, "set_sub_sub_state")
215231
AppHarness._poll_for(lambda: state_var_input.get_attribute("value") == "")
216232
AppHarness._poll_for(lambda: input_value_input.get_attribute("value") == "")
233+
234+
# Set the values.
217235
state_var_input.send_keys(var)
218236
input_value_input.send_keys(value)
219237
set_sub_sub_state_button.click()
220238

239+
token = poll_for_token()
240+
221241
# get a reference to all cookie and local storage elements
222242
c1 = driver.find_element(By.ID, "c1")
223243
c2 = driver.find_element(By.ID, "c2")
@@ -429,7 +449,7 @@ def set_sub_sub(var: str, value: str):
429449
assert l1s.text == "l1s value"
430450

431451
# reset the backend state to force refresh from client storage
432-
async with client_side.modify_state(token) as state:
452+
async with client_side.modify_state(f"{token}_state.client_side_state") as state:
433453
state.reset()
434454
driver.refresh()
435455

@@ -485,6 +505,31 @@ def set_sub_sub(var: str, value: str):
485505
"value": "c5%20value",
486506
}
487507

508+
# Open a new tab to check that sync'd local storage is working
509+
main_tab = driver.window_handles[0]
510+
driver.switch_to.new_window("window")
511+
driver.get(client_side.frontend_url)
512+
513+
# New tab should have a different state token.
514+
assert poll_for_token() != token
515+
516+
# Set values and check them in the new tab.
517+
set_sub("l5", "l5 value")
518+
set_sub("l6", "l6 value")
519+
l5 = driver.find_element(By.ID, "l5")
520+
l6 = driver.find_element(By.ID, "l6")
521+
assert l5.text == "l5 value"
522+
assert l6.text == "l6 value"
523+
524+
# Switch back to main window.
525+
driver.switch_to.window(main_tab)
526+
527+
# The values should have updated automatically.
528+
l5 = driver.find_element(By.ID, "l5")
529+
l6 = driver.find_element(By.ID, "l6")
530+
assert l5.text == "l5 value"
531+
assert l6.text == "l6 value"
532+
488533
# clear the cookie jar and local storage, ensure state reset to default
489534
driver.delete_all_cookies()
490535
local_storage.clear()

integration/test_dynamic_routes.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def dynamic_route(
8585
"""
8686
with app_harness_env.create(
8787
root=tmp_path_factory.mktemp(f"dynamic_route"),
88+
app_name=f"dynamicroute_{app_harness_env.__name__.lower()}",
8889
app_source=DynamicRoute, # type: ignore
8990
) as harness:
9091
yield harness
@@ -146,7 +147,7 @@ def poll_for_order(
146147

147148
async def _poll_for_order(exp_order: list[str]):
148149
async def _backend_state():
149-
return await dynamic_route.get_state(token)
150+
return await dynamic_route.get_state(f"{token}_state.dynamic_state")
150151

151152
async def _check():
152153
return (await _backend_state()).substates[
@@ -194,7 +195,9 @@ async def test_on_load_navigate(
194195
assert link
195196
assert page_id_input
196197

197-
assert dynamic_route.poll_for_value(page_id_input) == str(ix)
198+
assert dynamic_route.poll_for_value(
199+
page_id_input, exp_not_equal=str(ix - 1)
200+
) == str(ix)
198201
assert dynamic_route.poll_for_value(raw_path_input) == f"/page/{ix}/"
199202
await poll_for_order(exp_order)
200203

@@ -220,7 +223,9 @@ async def test_on_load_navigate(
220223
with poll_for_navigation(driver):
221224
driver.get(f"{driver.current_url}?foo=bar")
222225
await poll_for_order(exp_order)
223-
assert (await dynamic_route.get_state(token)).router.page.params["foo"] == "bar"
226+
assert (
227+
await dynamic_route.get_state(f"{token}_state.dynamic_state")
228+
).router.page.params["foo"] == "bar"
224229

225230
# hit a 404 and ensure we still hydrate
226231
exp_order += ["/404-no page id"]

integration/test_event_actions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def poll_for_order(
207207

208208
async def _poll_for_order(exp_order: list[str]):
209209
async def _backend_state():
210-
return await event_action.get_state(token)
210+
return await event_action.get_state(f"{token}_state.event_action_state")
211211

212212
async def _check():
213213
return (await _backend_state()).substates[

integration/test_event_chain.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def assert_token(event_chain: AppHarness, driver: WebDriver) -> str:
298298
token = event_chain.poll_for_value(token_input)
299299
assert token is not None
300300

301-
return token
301+
return f"{token}_state.state"
302302

303303

304304
@pytest.mark.parametrize(

integration/test_form_submit.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,11 @@ async def test_submit(driver, form_submit: AppHarness):
221221
submit_input.click()
222222

223223
async def get_form_data():
224-
return (await form_submit.get_state(token)).substates["form_state"].form_data
224+
return (
225+
(await form_submit.get_state(f"{token}_state.form_state"))
226+
.substates["form_state"]
227+
.form_data
228+
)
225229

226230
# wait for the form data to arrive at the backend
227231
form_data = await AppHarness._poll_for_async(get_form_data)

integration/test_input.py

+9-11
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
7676
token = fully_controlled_input.poll_for_value(token_input)
7777
assert token
7878

79+
async def get_state_text():
80+
state = await fully_controlled_input.get_state(f"{token}_state.state")
81+
return state.substates["state"].text
82+
7983
# find the input and wait for it to have the initial state value
8084
debounce_input = driver.find_element(By.ID, "debounce_input_input")
8185
value_input = driver.find_element(By.ID, "value_input")
@@ -95,16 +99,14 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
9599
debounce_input.send_keys("foo")
96100
time.sleep(0.5)
97101
assert debounce_input.get_attribute("value") == "ifoonitial"
98-
assert (await fully_controlled_input.get_state(token)).substates[
99-
"state"
100-
].text == "ifoonitial"
102+
assert await get_state_text() == "ifoonitial"
101103
assert fully_controlled_input.poll_for_value(value_input) == "ifoonitial"
102104
assert fully_controlled_input.poll_for_value(plain_value_input) == "ifoonitial"
103105

104106
# clear the input on the backend
105-
async with fully_controlled_input.modify_state(token) as state:
107+
async with fully_controlled_input.modify_state(f"{token}_state.state") as state:
106108
state.substates["state"].text = ""
107-
assert (await fully_controlled_input.get_state(token)).substates["state"].text == ""
109+
assert await get_state_text() == ""
108110
assert (
109111
fully_controlled_input.poll_for_value(
110112
debounce_input, exp_not_equal="ifoonitial"
@@ -116,9 +118,7 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
116118
debounce_input.send_keys("getting testing done")
117119
time.sleep(0.5)
118120
assert debounce_input.get_attribute("value") == "getting testing done"
119-
assert (await fully_controlled_input.get_state(token)).substates[
120-
"state"
121-
].text == "getting testing done"
121+
assert await get_state_text() == "getting testing done"
122122
assert fully_controlled_input.poll_for_value(value_input) == "getting testing done"
123123
assert (
124124
fully_controlled_input.poll_for_value(plain_value_input)
@@ -130,9 +130,7 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
130130
time.sleep(0.5)
131131
assert debounce_input.get_attribute("value") == "overwrite the state"
132132
assert on_change_input.get_attribute("value") == "overwrite the state"
133-
assert (await fully_controlled_input.get_state(token)).substates[
134-
"state"
135-
].text == "overwrite the state"
133+
assert await get_state_text() == "overwrite the state"
136134
assert fully_controlled_input.poll_for_value(value_input) == "overwrite the state"
137135
assert (
138136
fully_controlled_input.poll_for_value(plain_value_input)

integration/test_navigation.py

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Integration tests for links and related components."""
2+
from typing import Generator
3+
from urllib.parse import urlsplit
4+
5+
import pytest
6+
from selenium.webdriver.common.by import By
7+
8+
from reflex.testing import AppHarness
9+
10+
from .utils import poll_for_navigation
11+
12+
13+
def NavigationApp():
14+
"""Reflex app with links for navigation."""
15+
import reflex as rx
16+
17+
class State(rx.State):
18+
is_external: bool = True
19+
20+
app = rx.App()
21+
22+
@app.add_page
23+
def index():
24+
return rx.fragment(
25+
rx.link("Internal", href="/internal", id="internal"),
26+
rx.link(
27+
"External",
28+
href="/internal",
29+
is_external=State.is_external,
30+
id="external",
31+
),
32+
rx.link(
33+
"External Target", href="/internal", target="_blank", id="external2"
34+
),
35+
)
36+
37+
@rx.page(route="/internal")
38+
def internal():
39+
return rx.text("Internal")
40+
41+
42+
@pytest.fixture()
43+
def navigation_app(tmp_path) -> Generator[AppHarness, None, None]:
44+
"""Start NavigationApp app at tmp_path via AppHarness.
45+
46+
Args:
47+
tmp_path: pytest tmp_path fixture
48+
49+
Yields:
50+
running AppHarness instance
51+
"""
52+
with AppHarness.create(
53+
root=tmp_path,
54+
app_source=NavigationApp, # type: ignore
55+
) as harness:
56+
yield harness
57+
58+
59+
@pytest.mark.asyncio
60+
async def test_navigation_app(navigation_app: AppHarness):
61+
"""Type text after moving cursor. Update text on backend.
62+
63+
Args:
64+
navigation_app: harness for NavigationApp app
65+
"""
66+
assert navigation_app.app_instance is not None, "app is not running"
67+
driver = navigation_app.frontend()
68+
69+
internal_link = driver.find_element(By.ID, "internal")
70+
71+
with poll_for_navigation(driver):
72+
internal_link.click()
73+
assert urlsplit(driver.current_url).path == f"/internal/"
74+
with poll_for_navigation(driver):
75+
driver.back()
76+
77+
external_link = driver.find_element(By.ID, "external")
78+
external2_link = driver.find_element(By.ID, "external2")
79+
80+
external_link.click()
81+
# Expect a new tab to open
82+
assert AppHarness._poll_for(lambda: len(driver.window_handles) == 2)
83+
84+
# Switch back to the main tab
85+
driver.switch_to.window(driver.window_handles[0])
86+
87+
external2_link.click()
88+
# Expect another new tab to open
89+
assert AppHarness._poll_for(lambda: len(driver.window_handles) == 3)

integration/test_upload.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ async def test_upload_file(
171171
# wait for the backend connection to send the token
172172
token = upload_file.poll_for_value(token_input)
173173
assert token is not None
174+
substate_token = f"{token}_state.upload_state"
174175

175176
suffix = "_secondary" if secondary else ""
176177

@@ -191,7 +192,11 @@ async def test_upload_file(
191192

192193
# look up the backend state and assert on uploaded contents
193194
async def get_file_data():
194-
return (await upload_file.get_state(token)).substates["upload_state"]._file_data
195+
return (
196+
(await upload_file.get_state(substate_token))
197+
.substates["upload_state"]
198+
._file_data
199+
)
195200

196201
file_data = await AppHarness._poll_for_async(get_file_data)
197202
assert isinstance(file_data, dict)
@@ -201,7 +206,7 @@ async def get_file_data():
201206
selected_files = driver.find_element(By.ID, f"selected_files{suffix}")
202207
assert selected_files.text == exp_name
203208

204-
state = await upload_file.get_state(token)
209+
state = await upload_file.get_state(substate_token)
205210
if secondary:
206211
# only the secondary form tracks progress and chain events
207212
assert state.substates["upload_state"].event_order.count("upload_progress") == 1
@@ -223,6 +228,7 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
223228
# wait for the backend connection to send the token
224229
token = upload_file.poll_for_value(token_input)
225230
assert token is not None
231+
substate_token = f"{token}_state.upload_state"
226232

227233
upload_box = driver.find_element(By.XPATH, "//input[@type='file']")
228234
assert upload_box
@@ -250,7 +256,11 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
250256

251257
# look up the backend state and assert on uploaded contents
252258
async def get_file_data():
253-
return (await upload_file.get_state(token)).substates["upload_state"]._file_data
259+
return (
260+
(await upload_file.get_state(substate_token))
261+
.substates["upload_state"]
262+
._file_data
263+
)
254264

255265
file_data = await AppHarness._poll_for_async(get_file_data)
256266
assert isinstance(file_data, dict)
@@ -330,6 +340,7 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
330340
# wait for the backend connection to send the token
331341
token = upload_file.poll_for_value(token_input)
332342
assert token is not None
343+
substate_token = f"{token}_state.upload_state"
333344

334345
upload_box = driver.find_elements(By.XPATH, "//input[@type='file']")[1]
335346
upload_button = driver.find_element(By.ID, f"upload_button_secondary")
@@ -347,7 +358,7 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
347358
cancel_button.click()
348359

349360
# look up the backend state and assert on progress
350-
state = await upload_file.get_state(token)
361+
state = await upload_file.get_state(substate_token)
351362
assert state.substates["upload_state"].progress_dicts
352363
assert exp_name not in state.substates["upload_state"]._file_data
353364

0 commit comments

Comments
 (0)