test_event_chain.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. """Ensure that Event Chains are properly queued and handled between frontend and backend."""
  2. import time
  3. from typing import Generator
  4. import pytest
  5. from selenium.webdriver.common.by import By
  6. from reflex.testing import AppHarness
  7. MANY_EVENTS = 50
  8. def EventChain():
  9. """App with chained event handlers."""
  10. import reflex as rx
  11. # repeated here since the outer global isn't exported into the App module
  12. MANY_EVENTS = 50
  13. class State(rx.State):
  14. event_order: list[str] = []
  15. @rx.var
  16. def token(self) -> str:
  17. return self.get_token()
  18. def event_no_args(self):
  19. self.event_order.append("event_no_args")
  20. def event_arg(self, arg):
  21. self.event_order.append(f"event_arg:{arg}")
  22. def event_nested_1(self):
  23. self.event_order.append("event_nested_1")
  24. yield State.event_nested_2
  25. yield State.event_arg("nested_1") # type: ignore
  26. def event_nested_2(self):
  27. self.event_order.append("event_nested_2")
  28. yield State.event_nested_3
  29. yield rx.console_log("event_nested_2")
  30. yield State.event_arg("nested_2") # type: ignore
  31. def event_nested_3(self):
  32. self.event_order.append("event_nested_3")
  33. yield State.event_no_args
  34. yield State.event_arg("nested_3") # type: ignore
  35. def on_load_return_chain(self):
  36. self.event_order.append("on_load_return_chain")
  37. return [State.event_arg(1), State.event_arg(2), State.event_arg(3)] # type: ignore
  38. def on_load_yield_chain(self):
  39. self.event_order.append("on_load_yield_chain")
  40. yield State.event_arg(4) # type: ignore
  41. yield State.event_arg(5) # type: ignore
  42. yield State.event_arg(6) # type: ignore
  43. def click_return_event(self):
  44. self.event_order.append("click_return_event")
  45. return State.event_no_args
  46. def click_return_events(self):
  47. self.event_order.append("click_return_events")
  48. return [
  49. State.event_arg(7), # type: ignore
  50. rx.console_log("click_return_events"),
  51. State.event_arg(8), # type: ignore
  52. State.event_arg(9), # type: ignore
  53. ]
  54. def click_yield_chain(self):
  55. self.event_order.append("click_yield_chain:0")
  56. yield State.event_arg(10) # type: ignore
  57. self.event_order.append("click_yield_chain:1")
  58. yield rx.console_log("click_yield_chain")
  59. yield State.event_arg(11) # type: ignore
  60. self.event_order.append("click_yield_chain:2")
  61. yield State.event_arg(12) # type: ignore
  62. self.event_order.append("click_yield_chain:3")
  63. def click_yield_many_events(self):
  64. self.event_order.append("click_yield_many_events")
  65. for ix in range(MANY_EVENTS):
  66. yield State.event_arg(ix) # type: ignore
  67. yield rx.console_log(f"many_events_{ix}")
  68. self.event_order.append("click_yield_many_events_done")
  69. def click_yield_nested(self):
  70. self.event_order.append("click_yield_nested")
  71. yield State.event_nested_1
  72. yield State.event_arg("yield_nested") # type: ignore
  73. def redirect_return_chain(self):
  74. self.event_order.append("redirect_return_chain")
  75. yield rx.redirect("/on-load-return-chain")
  76. def redirect_yield_chain(self):
  77. self.event_order.append("redirect_yield_chain")
  78. yield rx.redirect("/on-load-yield-chain")
  79. app = rx.App(state=State)
  80. @app.add_page
  81. def index():
  82. return rx.fragment(
  83. rx.input(value=State.token, readonly=True, id="token"),
  84. rx.button(
  85. "Return Event",
  86. id="return_event",
  87. on_click=State.click_return_event,
  88. ),
  89. rx.button(
  90. "Return Events",
  91. id="return_events",
  92. on_click=State.click_return_events,
  93. ),
  94. rx.button(
  95. "Yield Chain",
  96. id="yield_chain",
  97. on_click=State.click_yield_chain,
  98. ),
  99. rx.button(
  100. "Yield Many events",
  101. id="yield_many_events",
  102. on_click=State.click_yield_many_events,
  103. ),
  104. rx.button(
  105. "Yield Nested",
  106. id="yield_nested",
  107. on_click=State.click_yield_nested,
  108. ),
  109. rx.button(
  110. "Redirect Yield Chain",
  111. id="redirect_yield_chain",
  112. on_click=State.redirect_yield_chain,
  113. ),
  114. rx.button(
  115. "Redirect Return Chain",
  116. id="redirect_return_chain",
  117. on_click=State.redirect_return_chain,
  118. ),
  119. )
  120. def on_load_return_chain():
  121. return rx.fragment(
  122. rx.text("return"),
  123. rx.input(value=State.token, readonly=True, id="token"),
  124. )
  125. def on_load_yield_chain():
  126. return rx.fragment(
  127. rx.text("yield"),
  128. rx.input(value=State.token, readonly=True, id="token"),
  129. )
  130. app.add_page(on_load_return_chain, on_load=State.on_load_return_chain) # type: ignore
  131. app.add_page(on_load_yield_chain, on_load=State.on_load_yield_chain) # type: ignore
  132. app.compile()
  133. @pytest.fixture(scope="session")
  134. def event_chain(tmp_path_factory) -> Generator[AppHarness, None, None]:
  135. """Start EventChain app at tmp_path via AppHarness.
  136. Args:
  137. tmp_path_factory: pytest tmp_path_factory fixture
  138. Yields:
  139. running AppHarness instance
  140. """
  141. with AppHarness.create(
  142. root=tmp_path_factory.mktemp("event_chain"),
  143. app_source=EventChain, # type: ignore
  144. ) as harness:
  145. yield harness
  146. @pytest.fixture
  147. def driver(event_chain: AppHarness):
  148. """Get an instance of the browser open to the event_chain app.
  149. Args:
  150. event_chain: harness for EventChain app
  151. Yields:
  152. WebDriver instance.
  153. """
  154. assert event_chain.app_instance is not None, "app is not running"
  155. driver = event_chain.frontend()
  156. try:
  157. assert event_chain.poll_for_clients()
  158. yield driver
  159. finally:
  160. driver.quit()
  161. @pytest.mark.parametrize(
  162. ("button_id", "exp_event_order"),
  163. [
  164. ("return_event", ["click_return_event", "event_no_args"]),
  165. (
  166. "return_events",
  167. ["click_return_events", "event_arg:7", "event_arg:8", "event_arg:9"],
  168. ),
  169. (
  170. "yield_chain",
  171. [
  172. "click_yield_chain:0",
  173. "click_yield_chain:1",
  174. "click_yield_chain:2",
  175. "click_yield_chain:3",
  176. "event_arg:10",
  177. "event_arg:11",
  178. "event_arg:12",
  179. ],
  180. ),
  181. (
  182. "yield_many_events",
  183. [
  184. "click_yield_many_events",
  185. "click_yield_many_events_done",
  186. *[f"event_arg:{ix}" for ix in range(MANY_EVENTS)],
  187. ],
  188. ),
  189. (
  190. "yield_nested",
  191. [
  192. "click_yield_nested",
  193. "event_nested_1",
  194. "event_arg:yield_nested",
  195. "event_nested_2",
  196. "event_arg:nested_1",
  197. "event_nested_3",
  198. "event_arg:nested_2",
  199. "event_no_args",
  200. "event_arg:nested_3",
  201. ],
  202. ),
  203. (
  204. "redirect_return_chain",
  205. [
  206. "redirect_return_chain",
  207. "on_load_return_chain",
  208. "event_arg:1",
  209. "event_arg:2",
  210. "event_arg:3",
  211. ],
  212. ),
  213. (
  214. "redirect_yield_chain",
  215. [
  216. "redirect_yield_chain",
  217. "on_load_yield_chain",
  218. "event_arg:4",
  219. "event_arg:5",
  220. "event_arg:6",
  221. ],
  222. ),
  223. ],
  224. )
  225. def test_event_chain_click(event_chain, driver, button_id, exp_event_order):
  226. """Click the button, assert that the events are handled in the correct order.
  227. Args:
  228. event_chain: AppHarness for the event_chain app
  229. driver: selenium WebDriver open to the app
  230. button_id: the ID of the button to click
  231. exp_event_order: the expected events recorded in the State
  232. """
  233. token_input = driver.find_element(By.ID, "token")
  234. btn = driver.find_element(By.ID, button_id)
  235. assert token_input
  236. assert btn
  237. token = event_chain.poll_for_value(token_input)
  238. btn.click()
  239. if "redirect" in button_id:
  240. # wait a bit longer if we're redirecting
  241. time.sleep(1)
  242. if "many_events" in button_id:
  243. # wait a bit longer if we have loads of events
  244. time.sleep(1)
  245. time.sleep(0.5)
  246. backend_state = event_chain.app_instance.state_manager.states[token]
  247. assert backend_state.event_order == exp_event_order
  248. @pytest.mark.parametrize(
  249. ("uri", "exp_event_order"),
  250. [
  251. (
  252. "/on-load-return-chain",
  253. [
  254. "on_load_return_chain",
  255. "event_arg:1",
  256. "event_arg:2",
  257. "event_arg:3",
  258. ],
  259. ),
  260. (
  261. "/on-load-yield-chain",
  262. [
  263. "on_load_yield_chain",
  264. "event_arg:4",
  265. "event_arg:5",
  266. "event_arg:6",
  267. ],
  268. ),
  269. ],
  270. )
  271. def test_event_chain_on_load(event_chain, driver, uri, exp_event_order):
  272. """Load the URI, assert that the events are handled in the correct order.
  273. Args:
  274. event_chain: AppHarness for the event_chain app
  275. driver: selenium WebDriver open to the app
  276. uri: the page to load
  277. exp_event_order: the expected events recorded in the State
  278. """
  279. driver.get(event_chain.frontend_url + uri)
  280. token_input = driver.find_element(By.ID, "token")
  281. assert token_input
  282. token = event_chain.poll_for_value(token_input)
  283. time.sleep(0.5)
  284. backend_state = event_chain.app_instance.state_manager.states[token]
  285. assert backend_state.event_order == exp_event_order