test_computed_vars.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. """Test computed vars."""
  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 reflex.testing import DEFAULT_TIMEOUT, AppHarness, WebDriver
  8. def ComputedVars():
  9. """Test app for computed vars."""
  10. import reflex as rx
  11. class StateMixin(rx.State, mixin=True):
  12. pass
  13. class State(StateMixin, rx.State):
  14. count: int = 0
  15. # cached var with dep on count
  16. @rx.var(cache=True, interval=15)
  17. def count1(self) -> int:
  18. return self.count
  19. # cached backend var with dep on count
  20. @rx.var(cache=True, interval=15, backend=True)
  21. def count1_backend(self) -> int:
  22. return self.count
  23. # same as above but implicit backend with `_` prefix
  24. @rx.var(cache=True, interval=15)
  25. def _count1_backend(self) -> int:
  26. return self.count
  27. # explicit disabled auto_deps
  28. @rx.var(interval=15, cache=True, auto_deps=False)
  29. def count3(self) -> int:
  30. # this will not add deps, because auto_deps is False
  31. print(self.count1)
  32. return self.count
  33. # explicit dependency on count var
  34. @rx.var(cache=True, deps=["count"], auto_deps=False)
  35. def depends_on_count(self) -> int:
  36. return self.count
  37. # explicit dependency on count1 var
  38. @rx.var(cache=True, deps=[count1], auto_deps=False)
  39. def depends_on_count1(self) -> int:
  40. return self.count
  41. @rx.var(deps=[count3], auto_deps=False, cache=True)
  42. def depends_on_count3(self) -> int:
  43. return self.count
  44. # special floats should be properly decoded on the frontend
  45. @rx.var(cache=True, initial_value=[])
  46. def special_floats(self) -> list[float]:
  47. return [42.9, float("nan"), float("inf"), float("-inf")]
  48. @rx.event
  49. def increment(self):
  50. self.count += 1
  51. @rx.event
  52. def mark_dirty(self):
  53. self._mark_dirty()
  54. assert State.backend_vars == {}
  55. def index() -> rx.Component:
  56. return rx.center(
  57. rx.vstack(
  58. rx.input(
  59. id="token",
  60. value=State.router.session.client_token,
  61. is_read_only=True,
  62. ),
  63. rx.button("Increment", on_click=State.increment, id="increment"),
  64. rx.button("Do nothing", on_click=State.mark_dirty, id="mark_dirty"),
  65. rx.text("count:"),
  66. rx.text(State.count, id="count"),
  67. rx.text("count1:"),
  68. rx.text(State.count1, id="count1"),
  69. rx.text("count1_backend:"),
  70. rx.text(State.count1_backend, id="count1_backend"),
  71. rx.text("_count1_backend:"),
  72. rx.text(State._count1_backend, id="_count1_backend"),
  73. rx.text("count3:"),
  74. rx.text(State.count3, id="count3"),
  75. rx.text("depends_on_count:"),
  76. rx.text(
  77. State.depends_on_count,
  78. id="depends_on_count",
  79. ),
  80. rx.text("depends_on_count1:"),
  81. rx.text(
  82. State.depends_on_count1,
  83. id="depends_on_count1",
  84. ),
  85. rx.text("depends_on_count3:"),
  86. rx.text(
  87. State.depends_on_count3,
  88. id="depends_on_count3",
  89. ),
  90. rx.text("special_floats:"),
  91. rx.text(
  92. State.special_floats.join(", "),
  93. id="special_floats",
  94. ),
  95. ),
  96. )
  97. app = rx.App()
  98. app.add_page(index)
  99. @pytest.fixture(scope="module")
  100. def computed_vars(
  101. tmp_path_factory: pytest.TempPathFactory,
  102. ) -> Generator[AppHarness, None, None]:
  103. """Start ComputedVars app at tmp_path via AppHarness.
  104. Args:
  105. tmp_path_factory: pytest tmp_path_factory fixture
  106. Yields:
  107. running AppHarness instance
  108. """
  109. with AppHarness.create(
  110. root=tmp_path_factory.mktemp("computed_vars"),
  111. app_source=ComputedVars,
  112. ) as harness:
  113. yield harness
  114. @pytest.fixture
  115. def driver(computed_vars: AppHarness) -> Generator[WebDriver, None, None]:
  116. """Get an instance of the browser open to the computed_vars app.
  117. Args:
  118. computed_vars: harness for ComputedVars app
  119. Yields:
  120. WebDriver instance.
  121. """
  122. assert computed_vars.app_instance is not None, "app is not running"
  123. driver = computed_vars.frontend()
  124. try:
  125. yield driver
  126. finally:
  127. driver.quit()
  128. @pytest.fixture()
  129. def token(computed_vars: AppHarness, driver: WebDriver) -> str:
  130. """Get a function that returns the active token.
  131. Args:
  132. computed_vars: harness for ComputedVars app.
  133. driver: WebDriver instance.
  134. Returns:
  135. The token for the connected client
  136. """
  137. assert computed_vars.app_instance is not None
  138. token_input = driver.find_element(By.ID, "token")
  139. assert token_input
  140. # wait for the backend connection to send the token
  141. token = computed_vars.poll_for_value(token_input, timeout=DEFAULT_TIMEOUT * 2)
  142. assert token is not None
  143. return token
  144. @pytest.mark.asyncio
  145. async def test_computed_vars(
  146. computed_vars: AppHarness,
  147. driver: WebDriver,
  148. token: str,
  149. ):
  150. """Test that computed vars are working as expected.
  151. Args:
  152. computed_vars: harness for ComputedVars app.
  153. driver: WebDriver instance.
  154. token: The token for the connected client.
  155. """
  156. assert computed_vars.app_instance is not None
  157. state_name = computed_vars.get_state_name("_state")
  158. full_state_name = computed_vars.get_full_state_name(["_state"])
  159. token = f"{token}_{full_state_name}"
  160. state = (await computed_vars.get_state(token)).substates[state_name]
  161. assert state is not None
  162. assert state.count1_backend == 0
  163. assert state._count1_backend == 0
  164. # test that backend var is not rendered
  165. count1_backend = driver.find_element(By.ID, "count1_backend")
  166. assert count1_backend
  167. assert count1_backend.text == ""
  168. _count1_backend = driver.find_element(By.ID, "_count1_backend")
  169. assert _count1_backend
  170. assert _count1_backend.text == ""
  171. count = driver.find_element(By.ID, "count")
  172. assert count
  173. assert count.text == "0"
  174. count1 = driver.find_element(By.ID, "count1")
  175. assert count1
  176. assert count1.text == "0"
  177. count3 = driver.find_element(By.ID, "count3")
  178. assert count3
  179. assert count3.text == "0"
  180. depends_on_count = driver.find_element(By.ID, "depends_on_count")
  181. assert depends_on_count
  182. assert depends_on_count.text == "0"
  183. depends_on_count1 = driver.find_element(By.ID, "depends_on_count1")
  184. assert depends_on_count1
  185. assert depends_on_count1.text == "0"
  186. depends_on_count3 = driver.find_element(By.ID, "depends_on_count3")
  187. assert depends_on_count3
  188. assert depends_on_count3.text == "0"
  189. special_floats = driver.find_element(By.ID, "special_floats")
  190. assert special_floats
  191. assert special_floats.text == "42.9, NaN, Infinity, -Infinity"
  192. increment = driver.find_element(By.ID, "increment")
  193. assert increment.is_enabled()
  194. mark_dirty = driver.find_element(By.ID, "mark_dirty")
  195. assert mark_dirty.is_enabled()
  196. mark_dirty.click()
  197. increment.click()
  198. assert computed_vars.poll_for_content(count, timeout=2, exp_not_equal="0") == "1"
  199. assert computed_vars.poll_for_content(count1, timeout=2, exp_not_equal="0") == "1"
  200. assert (
  201. computed_vars.poll_for_content(depends_on_count, timeout=2, exp_not_equal="0")
  202. == "1"
  203. )
  204. state = (await computed_vars.get_state(token)).substates[state_name]
  205. assert state is not None
  206. assert state.count1_backend == 1
  207. assert count1_backend.text == ""
  208. assert state._count1_backend == 1
  209. assert _count1_backend.text == ""
  210. mark_dirty.click()
  211. with pytest.raises(TimeoutError):
  212. _ = computed_vars.poll_for_content(count3, timeout=5, exp_not_equal="0")
  213. time.sleep(10)
  214. assert count3.text == "0"
  215. assert depends_on_count3.text == "0"
  216. mark_dirty.click()
  217. assert computed_vars.poll_for_content(count3, timeout=2, exp_not_equal="0") == "1"
  218. assert depends_on_count3.text == "1"