test_event_actions.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. """Ensure stopPropagation and preventDefault work as expected."""
  2. from __future__ import annotations
  3. import asyncio
  4. from typing import Callable, Coroutine, Generator
  5. import pytest
  6. from selenium.webdriver.common.by import By
  7. from reflex.testing import AppHarness, WebDriver
  8. def TestEventAction():
  9. """App for testing event_actions."""
  10. from typing import List, Optional
  11. import reflex as rx
  12. class EventActionState(rx.State):
  13. order: List[str]
  14. def on_click(self, ev):
  15. self.order.append(f"on_click:{ev}")
  16. def on_click2(self):
  17. self.order.append("on_click2")
  18. class EventFiringComponent(rx.Component):
  19. """A component that fires onClick event without passing DOM event."""
  20. tag = "EventFiringComponent"
  21. def _get_custom_code(self) -> Optional[str]:
  22. return """
  23. function EventFiringComponent(props) {
  24. return (
  25. <div id={props.id} onClick={(e) => props.onClick("foo")}>
  26. Event Firing Component
  27. </div>
  28. )
  29. }"""
  30. def get_event_triggers(self):
  31. return {"on_click": lambda: []}
  32. def index():
  33. return rx.vstack(
  34. rx.chakra.input(
  35. value=EventActionState.router.session.client_token,
  36. is_read_only=True,
  37. id="token",
  38. ),
  39. rx.button("No events", id="btn-no-events"),
  40. rx.button(
  41. "Stop Prop Only",
  42. id="btn-stop-prop-only",
  43. on_click=rx.stop_propagation, # type: ignore
  44. ),
  45. rx.button(
  46. "Click event",
  47. on_click=EventActionState.on_click("no_event_actions"), # type: ignore
  48. id="btn-click-event",
  49. ),
  50. rx.button(
  51. "Click stop propagation",
  52. on_click=EventActionState.on_click("stop_propagation").stop_propagation, # type: ignore
  53. id="btn-click-stop-propagation",
  54. ),
  55. rx.button(
  56. "Click stop propagation2",
  57. on_click=EventActionState.on_click2.stop_propagation,
  58. id="btn-click-stop-propagation2",
  59. ),
  60. rx.button(
  61. "Click event 2",
  62. on_click=EventActionState.on_click2,
  63. id="btn-click-event2",
  64. ),
  65. rx.link(
  66. "Link",
  67. href="#",
  68. on_click=EventActionState.on_click("link_no_event_actions"), # type: ignore
  69. id="link",
  70. ),
  71. rx.link(
  72. "Link Stop Propagation",
  73. href="#",
  74. on_click=EventActionState.on_click( # type: ignore
  75. "link_stop_propagation"
  76. ).stop_propagation,
  77. id="link-stop-propagation",
  78. ),
  79. rx.link(
  80. "Link Prevent Default Only",
  81. href="/invalid",
  82. on_click=rx.prevent_default, # type: ignore
  83. id="link-prevent-default-only",
  84. ),
  85. rx.link(
  86. "Link Prevent Default",
  87. href="/invalid",
  88. on_click=EventActionState.on_click( # type: ignore
  89. "link_prevent_default"
  90. ).prevent_default,
  91. id="link-prevent-default",
  92. ),
  93. rx.link(
  94. "Link Both",
  95. href="/invalid",
  96. on_click=EventActionState.on_click( # type: ignore
  97. "link_both"
  98. ).stop_propagation.prevent_default,
  99. id="link-stop-propagation-prevent-default",
  100. ),
  101. EventFiringComponent.create(
  102. id="custom-stop-propagation",
  103. on_click=EventActionState.on_click( # type: ignore
  104. "custom-stop-propagation"
  105. ).stop_propagation,
  106. ),
  107. EventFiringComponent.create(
  108. id="custom-prevent-default",
  109. on_click=EventActionState.on_click( # type: ignore
  110. "custom-prevent-default"
  111. ).prevent_default,
  112. ),
  113. rx.chakra.list(
  114. rx.foreach(
  115. EventActionState.order, # type: ignore
  116. rx.chakra.list_item,
  117. ),
  118. ),
  119. on_click=EventActionState.on_click("outer"), # type: ignore
  120. )
  121. app = rx.App(state=rx.State)
  122. app.add_page(index)
  123. @pytest.fixture(scope="session")
  124. def event_action(tmp_path_factory) -> Generator[AppHarness, None, None]:
  125. """Start TestEventAction app at tmp_path via AppHarness.
  126. Args:
  127. tmp_path_factory: pytest tmp_path_factory fixture
  128. Yields:
  129. running AppHarness instance
  130. """
  131. with AppHarness.create(
  132. root=tmp_path_factory.mktemp(f"event_action"),
  133. app_source=TestEventAction, # type: ignore
  134. ) as harness:
  135. yield harness
  136. @pytest.fixture
  137. def driver(event_action: AppHarness) -> Generator[WebDriver, None, None]:
  138. """Get an instance of the browser open to the event_action app.
  139. Args:
  140. event_action: harness for TestEventAction app
  141. Yields:
  142. WebDriver instance.
  143. """
  144. assert event_action.app_instance is not None, "app is not running"
  145. driver = event_action.frontend()
  146. try:
  147. yield driver
  148. finally:
  149. driver.quit()
  150. @pytest.fixture()
  151. def token(event_action: AppHarness, driver: WebDriver) -> str:
  152. """Get the token associated with backend state.
  153. Args:
  154. event_action: harness for TestEventAction app.
  155. driver: WebDriver instance.
  156. Returns:
  157. The token visible in the driver browser.
  158. """
  159. assert event_action.app_instance is not None
  160. token_input = driver.find_element(By.ID, "token")
  161. assert token_input
  162. # wait for the backend connection to send the token
  163. token = event_action.poll_for_value(token_input)
  164. assert token is not None
  165. return token
  166. @pytest.fixture()
  167. def poll_for_order(
  168. event_action: AppHarness, token: str
  169. ) -> Callable[[list[str]], Coroutine[None, None, None]]:
  170. """Poll for the order list to match the expected order.
  171. Args:
  172. event_action: harness for TestEventAction app.
  173. token: The token visible in the driver browser.
  174. Returns:
  175. An async function that polls for the order list to match the expected order.
  176. """
  177. async def _poll_for_order(exp_order: list[str]):
  178. async def _backend_state():
  179. return await event_action.get_state(f"{token}_state.event_action_state")
  180. async def _check():
  181. return (await _backend_state()).substates[
  182. "event_action_state"
  183. ].order == exp_order
  184. await AppHarness._poll_for_async(_check)
  185. assert (await _backend_state()).substates[
  186. "event_action_state"
  187. ].order == exp_order
  188. return _poll_for_order
  189. @pytest.mark.parametrize(
  190. ("element_id", "exp_order"),
  191. [
  192. ("btn-no-events", ["on_click:outer"]),
  193. ("btn-stop-prop-only", []),
  194. ("btn-click-event", ["on_click:no_event_actions", "on_click:outer"]),
  195. ("btn-click-stop-propagation", ["on_click:stop_propagation"]),
  196. ("btn-click-stop-propagation2", ["on_click2"]),
  197. ("btn-click-event2", ["on_click2", "on_click:outer"]),
  198. ("link", ["on_click:link_no_event_actions", "on_click:outer"]),
  199. ("link-stop-propagation", ["on_click:link_stop_propagation"]),
  200. ("link-prevent-default", ["on_click:link_prevent_default", "on_click:outer"]),
  201. ("link-prevent-default-only", ["on_click:outer"]),
  202. ("link-stop-propagation-prevent-default", ["on_click:link_both"]),
  203. (
  204. "custom-stop-propagation",
  205. ["on_click:custom-stop-propagation", "on_click:outer"],
  206. ),
  207. (
  208. "custom-prevent-default",
  209. ["on_click:custom-prevent-default", "on_click:outer"],
  210. ),
  211. ],
  212. )
  213. @pytest.mark.usefixtures("token")
  214. @pytest.mark.asyncio
  215. async def test_event_actions(
  216. driver: WebDriver,
  217. poll_for_order: Callable[[list[str]], Coroutine[None, None, None]],
  218. element_id: str,
  219. exp_order: list[str],
  220. ):
  221. """Click links and buttons and assert on fired events.
  222. Args:
  223. driver: WebDriver instance.
  224. poll_for_order: function that polls for the order list to match the expected order.
  225. element_id: The id of the element to click.
  226. exp_order: The expected order of events.
  227. """
  228. el = driver.find_element(By.ID, element_id)
  229. assert el
  230. prev_url = driver.current_url
  231. el.click()
  232. if "on_click:outer" not in exp_order:
  233. # really make sure the outer event is not fired
  234. await asyncio.sleep(0.5)
  235. await poll_for_order(exp_order)
  236. if element_id.startswith("link") and "prevent-default" not in element_id:
  237. assert driver.current_url != prev_url
  238. else:
  239. assert driver.current_url == prev_url