test_client_storage.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. """Integration tests for client side storage."""
  2. from __future__ import annotations
  3. import time
  4. from typing import Generator
  5. import pytest
  6. from selenium.webdriver.common.by import By
  7. from selenium.webdriver.remote.webdriver import WebDriver
  8. from reflex.testing import AppHarness
  9. from . import utils
  10. def ClientSide():
  11. """App for testing client-side state."""
  12. import reflex as rx
  13. class ClientSideState(rx.State):
  14. state_var: str = ""
  15. input_value: str = ""
  16. @rx.var
  17. def token(self) -> str:
  18. return self.get_token()
  19. class ClientSideSubState(ClientSideState):
  20. # cookies with default settings
  21. c1: str = rx.Cookie()
  22. c2: rx.Cookie = "c2 default" # type: ignore
  23. # cookies with custom settings
  24. c3: str = rx.Cookie(max_age=2) # expires after 2 second
  25. c4: rx.Cookie = rx.Cookie(same_site="strict")
  26. c5: str = rx.Cookie(path="/foo/") # only accessible on `/foo/`
  27. c6: str = rx.Cookie(name="c6")
  28. c7: str = rx.Cookie("c7 default")
  29. # local storage with default settings
  30. l1: str = rx.LocalStorage()
  31. l2: rx.LocalStorage = "l2 default" # type: ignore
  32. # local storage with custom settings
  33. l3: str = rx.LocalStorage(name="l3")
  34. l4: str = rx.LocalStorage("l4 default")
  35. def set_var(self):
  36. setattr(self, self.state_var, self.input_value)
  37. self.state_var = self.input_value = ""
  38. class ClientSideSubSubState(ClientSideSubState):
  39. c1s: str = rx.Cookie()
  40. l1s: str = rx.LocalStorage()
  41. def set_var(self):
  42. setattr(self, self.state_var, self.input_value)
  43. self.state_var = self.input_value = ""
  44. def index():
  45. return rx.fragment(
  46. rx.input(value=ClientSideState.token, is_read_only=True, id="token"),
  47. rx.input(
  48. placeholder="state var",
  49. value=ClientSideState.state_var,
  50. on_change=ClientSideState.set_state_var, # type: ignore
  51. id="state_var",
  52. ),
  53. rx.input(
  54. placeholder="input value",
  55. value=ClientSideState.input_value,
  56. on_change=ClientSideState.set_input_value, # type: ignore
  57. id="input_value",
  58. ),
  59. rx.button(
  60. "Set ClientSideSubState",
  61. on_click=ClientSideSubState.set_var,
  62. id="set_sub_state",
  63. ),
  64. rx.button(
  65. "Set ClientSideSubSubState",
  66. on_click=ClientSideSubSubState.set_var,
  67. id="set_sub_sub_state",
  68. ),
  69. rx.box(ClientSideSubState.c1, id="c1"),
  70. rx.box(ClientSideSubState.c2, id="c2"),
  71. rx.box(ClientSideSubState.c3, id="c3"),
  72. rx.box(ClientSideSubState.c4, id="c4"),
  73. rx.box(ClientSideSubState.c5, id="c5"),
  74. rx.box(ClientSideSubState.c6, id="c6"),
  75. rx.box(ClientSideSubState.c7, id="c7"),
  76. rx.box(ClientSideSubState.l1, id="l1"),
  77. rx.box(ClientSideSubState.l2, id="l2"),
  78. rx.box(ClientSideSubState.l3, id="l3"),
  79. rx.box(ClientSideSubState.l4, id="l4"),
  80. rx.box(ClientSideSubSubState.c1s, id="c1s"),
  81. rx.box(ClientSideSubSubState.l1s, id="l1s"),
  82. )
  83. app = rx.App(state=ClientSideState)
  84. app.add_page(index)
  85. app.add_page(index, route="/foo")
  86. app.compile()
  87. @pytest.fixture(scope="session")
  88. def client_side(tmp_path_factory) -> Generator[AppHarness, None, None]:
  89. """Start ClientSide app at tmp_path via AppHarness.
  90. Args:
  91. tmp_path_factory: pytest tmp_path_factory fixture
  92. Yields:
  93. running AppHarness instance
  94. """
  95. with AppHarness.create(
  96. root=tmp_path_factory.mktemp("client_side"),
  97. app_source=ClientSide, # type: ignore
  98. ) as harness:
  99. yield harness
  100. @pytest.fixture
  101. def driver(client_side: AppHarness) -> Generator[WebDriver, None, None]:
  102. """Get an instance of the browser open to the client_side app.
  103. Args:
  104. client_side: harness for ClientSide app
  105. Yields:
  106. WebDriver instance.
  107. """
  108. assert client_side.app_instance is not None, "app is not running"
  109. driver = client_side.frontend()
  110. try:
  111. yield driver
  112. finally:
  113. driver.quit()
  114. @pytest.fixture()
  115. def local_storage(driver: WebDriver) -> Generator[utils.LocalStorage, None, None]:
  116. """Get an instance of the local storage helper.
  117. Args:
  118. driver: WebDriver instance.
  119. Yields:
  120. Local storage helper.
  121. """
  122. ls = utils.LocalStorage(driver)
  123. yield ls
  124. ls.clear()
  125. @pytest.fixture(autouse=True)
  126. def delete_all_cookies(driver: WebDriver) -> Generator[None, None, None]:
  127. """Delete all cookies after each test.
  128. Args:
  129. driver: WebDriver instance.
  130. Yields:
  131. None
  132. """
  133. yield
  134. driver.delete_all_cookies()
  135. def cookie_info_map(driver: WebDriver) -> dict[str, dict[str, str]]:
  136. """Get a map of cookie names to cookie info.
  137. Args:
  138. driver: WebDriver instance.
  139. Returns:
  140. A map of cookie names to cookie info.
  141. """
  142. return {cookie_info["name"]: cookie_info for cookie_info in driver.get_cookies()}
  143. @pytest.mark.asyncio
  144. async def test_client_side_state(
  145. client_side: AppHarness, driver: WebDriver, local_storage: utils.LocalStorage
  146. ):
  147. """Test client side state.
  148. Args:
  149. client_side: harness for ClientSide app.
  150. driver: WebDriver instance.
  151. local_storage: Local storage helper.
  152. """
  153. assert client_side.app_instance is not None
  154. assert client_side.frontend_url is not None
  155. token_input = driver.find_element(By.ID, "token")
  156. assert token_input
  157. # wait for the backend connection to send the token
  158. token = client_side.poll_for_value(token_input)
  159. assert token is not None
  160. # get a reference to the cookie manipulation form
  161. state_var_input = driver.find_element(By.ID, "state_var")
  162. input_value_input = driver.find_element(By.ID, "input_value")
  163. set_sub_state_button = driver.find_element(By.ID, "set_sub_state")
  164. set_sub_sub_state_button = driver.find_element(By.ID, "set_sub_sub_state")
  165. # get a reference to all cookie and local storage elements
  166. c1 = driver.find_element(By.ID, "c1")
  167. c2 = driver.find_element(By.ID, "c2")
  168. c3 = driver.find_element(By.ID, "c3")
  169. c4 = driver.find_element(By.ID, "c4")
  170. c5 = driver.find_element(By.ID, "c5")
  171. c6 = driver.find_element(By.ID, "c6")
  172. c7 = driver.find_element(By.ID, "c7")
  173. l1 = driver.find_element(By.ID, "l1")
  174. l2 = driver.find_element(By.ID, "l2")
  175. l3 = driver.find_element(By.ID, "l3")
  176. l4 = driver.find_element(By.ID, "l4")
  177. c1s = driver.find_element(By.ID, "c1s")
  178. l1s = driver.find_element(By.ID, "l1s")
  179. # assert on defaults where present
  180. assert c1.text == ""
  181. assert c2.text == "c2 default"
  182. assert c3.text == ""
  183. assert c4.text == ""
  184. assert c5.text == ""
  185. assert c6.text == ""
  186. assert c7.text == "c7 default"
  187. assert l1.text == ""
  188. assert l2.text == "l2 default"
  189. assert l3.text == ""
  190. assert l4.text == "l4 default"
  191. assert c1s.text == ""
  192. assert l1s.text == ""
  193. # no cookies should be set yet!
  194. assert not driver.get_cookies()
  195. local_storage_items = local_storage.items()
  196. local_storage_items.pop("chakra-ui-color-mode", None)
  197. assert not local_storage_items
  198. # set some cookies and local storage values
  199. state_var_input.send_keys("c1")
  200. input_value_input.send_keys("c1 value")
  201. set_sub_state_button.click()
  202. state_var_input.send_keys("c2")
  203. input_value_input.send_keys("c2 value")
  204. set_sub_state_button.click()
  205. state_var_input.send_keys("c4")
  206. input_value_input.send_keys("c4 value")
  207. set_sub_state_button.click()
  208. state_var_input.send_keys("c5")
  209. input_value_input.send_keys("c5 value")
  210. set_sub_state_button.click()
  211. state_var_input.send_keys("c6")
  212. input_value_input.send_keys("c6 throwaway value")
  213. set_sub_state_button.click()
  214. state_var_input.send_keys("c6")
  215. input_value_input.send_keys("c6 value")
  216. set_sub_state_button.click()
  217. state_var_input.send_keys("c7")
  218. input_value_input.send_keys("c7 value")
  219. set_sub_state_button.click()
  220. state_var_input.send_keys("l1")
  221. input_value_input.send_keys("l1 value")
  222. set_sub_state_button.click()
  223. state_var_input.send_keys("l2")
  224. input_value_input.send_keys("l2 value")
  225. set_sub_state_button.click()
  226. state_var_input.send_keys("l3")
  227. input_value_input.send_keys("l3 value")
  228. set_sub_state_button.click()
  229. state_var_input.send_keys("l4")
  230. input_value_input.send_keys("l4 value")
  231. set_sub_state_button.click()
  232. state_var_input.send_keys("c1s")
  233. input_value_input.send_keys("c1s value")
  234. set_sub_sub_state_button.click()
  235. state_var_input.send_keys("l1s")
  236. input_value_input.send_keys("l1s value")
  237. set_sub_sub_state_button.click()
  238. cookies = cookie_info_map(driver)
  239. assert cookies.pop("client_side_state.client_side_sub_state.c1") == {
  240. "domain": "localhost",
  241. "httpOnly": False,
  242. "name": "client_side_state.client_side_sub_state.c1",
  243. "path": "/",
  244. "sameSite": "Lax",
  245. "secure": False,
  246. "value": "c1%20value",
  247. }
  248. assert cookies.pop("client_side_state.client_side_sub_state.c2") == {
  249. "domain": "localhost",
  250. "httpOnly": False,
  251. "name": "client_side_state.client_side_sub_state.c2",
  252. "path": "/",
  253. "sameSite": "Lax",
  254. "secure": False,
  255. "value": "c2%20value",
  256. }
  257. assert cookies.pop("client_side_state.client_side_sub_state.c4") == {
  258. "domain": "localhost",
  259. "httpOnly": False,
  260. "name": "client_side_state.client_side_sub_state.c4",
  261. "path": "/",
  262. "sameSite": "Strict",
  263. "secure": False,
  264. "value": "c4%20value",
  265. }
  266. assert cookies.pop("c6") == {
  267. "domain": "localhost",
  268. "httpOnly": False,
  269. "name": "c6",
  270. "path": "/",
  271. "sameSite": "Lax",
  272. "secure": False,
  273. "value": "c6%20value",
  274. }
  275. assert cookies.pop("client_side_state.client_side_sub_state.c7") == {
  276. "domain": "localhost",
  277. "httpOnly": False,
  278. "name": "client_side_state.client_side_sub_state.c7",
  279. "path": "/",
  280. "sameSite": "Lax",
  281. "secure": False,
  282. "value": "c7%20value",
  283. }
  284. assert cookies.pop(
  285. "client_side_state.client_side_sub_state.client_side_sub_sub_state.c1s"
  286. ) == {
  287. "domain": "localhost",
  288. "httpOnly": False,
  289. "name": "client_side_state.client_side_sub_state.client_side_sub_sub_state.c1s",
  290. "path": "/",
  291. "sameSite": "Lax",
  292. "secure": False,
  293. "value": "c1s%20value",
  294. }
  295. # assert all cookies have been popped for this page
  296. assert not cookies
  297. # Test cookie with expiry by itself to avoid timing flakiness
  298. state_var_input.send_keys("c3")
  299. input_value_input.send_keys("c3 value")
  300. set_sub_state_button.click()
  301. AppHarness._poll_for(
  302. lambda: "client_side_state.client_side_sub_state.c3" in cookie_info_map(driver)
  303. )
  304. c3_cookie = cookie_info_map(driver)["client_side_state.client_side_sub_state.c3"]
  305. assert c3_cookie.pop("expiry") is not None
  306. assert c3_cookie == {
  307. "domain": "localhost",
  308. "httpOnly": False,
  309. "name": "client_side_state.client_side_sub_state.c3",
  310. "path": "/",
  311. "sameSite": "Lax",
  312. "secure": False,
  313. "value": "c3%20value",
  314. }
  315. time.sleep(2) # wait for c3 to expire
  316. assert "client_side_state.client_side_sub_state.c3" not in cookie_info_map(driver)
  317. local_storage_items = local_storage.items()
  318. local_storage_items.pop("chakra-ui-color-mode", None)
  319. assert (
  320. local_storage_items.pop("client_side_state.client_side_sub_state.l1")
  321. == "l1 value"
  322. )
  323. assert (
  324. local_storage_items.pop("client_side_state.client_side_sub_state.l2")
  325. == "l2 value"
  326. )
  327. assert local_storage_items.pop("l3") == "l3 value"
  328. assert (
  329. local_storage_items.pop("client_side_state.client_side_sub_state.l4")
  330. == "l4 value"
  331. )
  332. assert (
  333. local_storage_items.pop(
  334. "client_side_state.client_side_sub_state.client_side_sub_sub_state.l1s"
  335. )
  336. == "l1s value"
  337. )
  338. assert not local_storage_items
  339. assert c1.text == "c1 value"
  340. assert c2.text == "c2 value"
  341. assert c3.text == "c3 value"
  342. assert c4.text == "c4 value"
  343. assert c5.text == "c5 value"
  344. assert c6.text == "c6 value"
  345. assert c7.text == "c7 value"
  346. assert l1.text == "l1 value"
  347. assert l2.text == "l2 value"
  348. assert l3.text == "l3 value"
  349. assert l4.text == "l4 value"
  350. assert c1s.text == "c1s value"
  351. assert l1s.text == "l1s value"
  352. # navigate to the /foo route
  353. with utils.poll_for_navigation(driver):
  354. driver.get(client_side.frontend_url + "/foo")
  355. # get new references to all cookie and local storage elements
  356. c1 = driver.find_element(By.ID, "c1")
  357. c2 = driver.find_element(By.ID, "c2")
  358. c3 = driver.find_element(By.ID, "c3")
  359. c4 = driver.find_element(By.ID, "c4")
  360. c5 = driver.find_element(By.ID, "c5")
  361. c6 = driver.find_element(By.ID, "c6")
  362. c7 = driver.find_element(By.ID, "c7")
  363. l1 = driver.find_element(By.ID, "l1")
  364. l2 = driver.find_element(By.ID, "l2")
  365. l3 = driver.find_element(By.ID, "l3")
  366. l4 = driver.find_element(By.ID, "l4")
  367. c1s = driver.find_element(By.ID, "c1s")
  368. l1s = driver.find_element(By.ID, "l1s")
  369. assert c1.text == "c1 value"
  370. assert c2.text == "c2 value"
  371. assert c3.text == "" # cookie expired so value removed from state
  372. assert c4.text == "c4 value"
  373. assert c5.text == "c5 value"
  374. assert c6.text == "c6 value"
  375. assert c7.text == "c7 value"
  376. assert l1.text == "l1 value"
  377. assert l2.text == "l2 value"
  378. assert l3.text == "l3 value"
  379. assert l4.text == "l4 value"
  380. assert c1s.text == "c1s value"
  381. assert l1s.text == "l1s value"
  382. # reset the backend state to force refresh from client storage
  383. async with client_side.modify_state(token) as state:
  384. state.reset()
  385. driver.refresh()
  386. # wait for the backend connection to send the token (again)
  387. token_input = driver.find_element(By.ID, "token")
  388. assert token_input
  389. token = client_side.poll_for_value(token_input)
  390. assert token is not None
  391. # get new references to all cookie and local storage elements (again)
  392. c1 = driver.find_element(By.ID, "c1")
  393. c2 = driver.find_element(By.ID, "c2")
  394. c3 = driver.find_element(By.ID, "c3")
  395. c4 = driver.find_element(By.ID, "c4")
  396. c5 = driver.find_element(By.ID, "c5")
  397. c6 = driver.find_element(By.ID, "c6")
  398. c7 = driver.find_element(By.ID, "c7")
  399. l1 = driver.find_element(By.ID, "l1")
  400. l2 = driver.find_element(By.ID, "l2")
  401. l3 = driver.find_element(By.ID, "l3")
  402. l4 = driver.find_element(By.ID, "l4")
  403. c1s = driver.find_element(By.ID, "c1s")
  404. l1s = driver.find_element(By.ID, "l1s")
  405. assert c1.text == "c1 value"
  406. assert c2.text == "c2 value"
  407. assert c3.text == "" # temporary cookie expired after reset state!
  408. assert c4.text == "c4 value"
  409. assert c5.text == "c5 value"
  410. assert c6.text == "c6 value"
  411. assert c7.text == "c7 value"
  412. assert l1.text == "l1 value"
  413. assert l2.text == "l2 value"
  414. assert l3.text == "l3 value"
  415. assert l4.text == "l4 value"
  416. assert c1s.text == "c1s value"
  417. assert l1s.text == "l1s value"
  418. # make sure c5 cookie shows up on the `/foo` route
  419. assert cookie_info_map(driver)["client_side_state.client_side_sub_state.c5"] == {
  420. "domain": "localhost",
  421. "httpOnly": False,
  422. "name": "client_side_state.client_side_sub_state.c5",
  423. "path": "/foo/",
  424. "sameSite": "Lax",
  425. "secure": False,
  426. "value": "c5%20value",
  427. }
  428. # clear the cookie jar and local storage, ensure state reset to default
  429. driver.delete_all_cookies()
  430. local_storage.clear()
  431. # refresh the page to trigger re-hydrate
  432. driver.refresh()
  433. # wait for the backend connection to send the token (again)
  434. token_input = driver.find_element(By.ID, "token")
  435. assert token_input
  436. token = client_side.poll_for_value(token_input)
  437. assert token is not None
  438. # all values should be back to their defaults
  439. c1 = driver.find_element(By.ID, "c1")
  440. c2 = driver.find_element(By.ID, "c2")
  441. c3 = driver.find_element(By.ID, "c3")
  442. c4 = driver.find_element(By.ID, "c4")
  443. c5 = driver.find_element(By.ID, "c5")
  444. c6 = driver.find_element(By.ID, "c6")
  445. c7 = driver.find_element(By.ID, "c7")
  446. l1 = driver.find_element(By.ID, "l1")
  447. l2 = driver.find_element(By.ID, "l2")
  448. l3 = driver.find_element(By.ID, "l3")
  449. l4 = driver.find_element(By.ID, "l4")
  450. c1s = driver.find_element(By.ID, "c1s")
  451. l1s = driver.find_element(By.ID, "l1s")
  452. # assert on defaults where present
  453. assert c1.text == ""
  454. assert c2.text == "c2 default"
  455. assert c3.text == ""
  456. assert c4.text == ""
  457. assert c5.text == ""
  458. assert c6.text == ""
  459. assert c7.text == "c7 default"
  460. assert l1.text == ""
  461. assert l2.text == "l2 default"
  462. assert l3.text == ""
  463. assert l4.text == "l4 default"
  464. assert c1s.text == ""
  465. assert l1s.text == ""