test_connection_banner.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. """Test case for displaying the connection banner when the websocket drops."""
  2. from collections.abc import Generator
  3. import pytest
  4. from selenium.common.exceptions import NoSuchElementException
  5. from selenium.webdriver.common.by import By
  6. from reflex import constants
  7. from reflex.config import environment
  8. from reflex.testing import AppHarness, WebDriver
  9. from .utils import SessionStorage
  10. def ConnectionBanner():
  11. """App with a connection banner."""
  12. import asyncio
  13. import reflex as rx
  14. class State(rx.State):
  15. foo: int = 0
  16. @rx.event
  17. async def delay(self):
  18. await asyncio.sleep(5)
  19. def index():
  20. return rx.vstack(
  21. rx.text("Hello World"),
  22. rx.input(value=State.foo, read_only=True, id="counter"),
  23. rx.button(
  24. "Increment",
  25. id="increment",
  26. on_click=State.set_foo(State.foo + 1), # pyright: ignore [reportAttributeAccessIssue]
  27. ),
  28. rx.button("Delay", id="delay", on_click=State.delay),
  29. )
  30. app = rx.App()
  31. app.add_page(index)
  32. @pytest.fixture(
  33. params=[constants.CompileContext.RUN, constants.CompileContext.DEPLOY],
  34. ids=["compile_context_run", "compile_context_deploy"],
  35. )
  36. def simulate_compile_context(request) -> constants.CompileContext:
  37. """Fixture to simulate reflex cloud deployment.
  38. Args:
  39. request: pytest request fixture.
  40. Returns:
  41. The context to run the app with.
  42. """
  43. return request.param
  44. @pytest.fixture()
  45. def connection_banner(
  46. tmp_path,
  47. simulate_compile_context: constants.CompileContext,
  48. ) -> Generator[AppHarness, None, None]:
  49. """Start ConnectionBanner app at tmp_path via AppHarness.
  50. Args:
  51. tmp_path: pytest tmp_path fixture
  52. simulate_compile_context: Which context to run the app with.
  53. Yields:
  54. running AppHarness instance
  55. """
  56. environment.REFLEX_COMPILE_CONTEXT.set(simulate_compile_context)
  57. with AppHarness.create(
  58. root=tmp_path,
  59. app_source=ConnectionBanner,
  60. app_name=(
  61. "connection_banner_reflex_cloud"
  62. if simulate_compile_context == constants.CompileContext.DEPLOY
  63. else "connection_banner"
  64. ),
  65. ) as harness:
  66. yield harness
  67. CONNECTION_ERROR_XPATH = "//*[ contains(text(), 'Cannot connect to server') ]"
  68. def has_error_modal(driver: WebDriver) -> bool:
  69. """Check if the connection error modal is displayed.
  70. Args:
  71. driver: Selenium webdriver instance.
  72. Returns:
  73. True if the modal is displayed, False otherwise.
  74. """
  75. try:
  76. driver.find_element(By.XPATH, CONNECTION_ERROR_XPATH)
  77. except NoSuchElementException:
  78. return False
  79. else:
  80. return True
  81. def has_cloud_banner(driver: WebDriver) -> bool:
  82. """Check if the cloud banner is displayed.
  83. Args:
  84. driver: Selenium webdriver instance.
  85. Returns:
  86. True if the banner is displayed, False otherwise.
  87. """
  88. try:
  89. driver.find_element(By.XPATH, "//*[ contains(text(), 'This app is paused') ]")
  90. except NoSuchElementException:
  91. return False
  92. else:
  93. return True
  94. def _assert_token(connection_banner, driver):
  95. """Poll for backend to be up.
  96. Args:
  97. connection_banner: AppHarness instance.
  98. driver: Selenium webdriver instance.
  99. """
  100. ss = SessionStorage(driver)
  101. assert connection_banner._poll_for(lambda: ss.get("token") is not None), (
  102. "token not found"
  103. )
  104. @pytest.mark.asyncio
  105. async def test_connection_banner(connection_banner: AppHarness):
  106. """Test that the connection banner is displayed when the websocket drops.
  107. Args:
  108. connection_banner: AppHarness instance.
  109. """
  110. assert connection_banner.app_instance is not None
  111. assert connection_banner.backend is not None
  112. driver = connection_banner.frontend()
  113. _assert_token(connection_banner, driver)
  114. assert connection_banner._poll_for(lambda: not has_error_modal(driver))
  115. delay_button = driver.find_element(By.ID, "delay")
  116. increment_button = driver.find_element(By.ID, "increment")
  117. counter_element = driver.find_element(By.ID, "counter")
  118. # Increment the counter
  119. increment_button.click()
  120. assert connection_banner.poll_for_value(counter_element, exp_not_equal="0") == "1"
  121. # Start an long event before killing the backend, to mark event_processing=true
  122. delay_button.click()
  123. # Get the backend port
  124. backend_port = connection_banner._poll_for_servers().getsockname()[1]
  125. # Kill the backend
  126. connection_banner.backend.should_exit = True
  127. if connection_banner.backend_thread is not None:
  128. connection_banner.backend_thread.join()
  129. # Error modal should now be displayed
  130. assert connection_banner._poll_for(lambda: has_error_modal(driver))
  131. # Increment the counter with backend down
  132. increment_button.click()
  133. assert connection_banner.poll_for_value(counter_element, exp_not_equal="0") == "1"
  134. # Bring the backend back up
  135. connection_banner._start_backend(port=backend_port)
  136. # Create a new StateManager to avoid async loop affinity issues w/ redis
  137. await connection_banner._reset_backend_state_manager()
  138. # Banner should be gone now
  139. assert connection_banner._poll_for(lambda: not has_error_modal(driver))
  140. # Count should have incremented after coming back up
  141. assert connection_banner.poll_for_value(counter_element, exp_not_equal="1") == "2"
  142. @pytest.mark.asyncio
  143. async def test_cloud_banner(
  144. connection_banner: AppHarness, simulate_compile_context: constants.CompileContext
  145. ):
  146. """Test that the connection banner is displayed when the websocket drops.
  147. Args:
  148. connection_banner: AppHarness instance.
  149. simulate_compile_context: Which context to set for the app.
  150. """
  151. assert connection_banner.app_instance is not None
  152. assert connection_banner.backend is not None
  153. driver = connection_banner.frontend()
  154. driver.add_cookie({"name": "backend-enabled", "value": "truly"})
  155. driver.refresh()
  156. _assert_token(connection_banner, driver)
  157. assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
  158. driver.add_cookie({"name": "backend-enabled", "value": "false"})
  159. driver.refresh()
  160. if simulate_compile_context == constants.CompileContext.DEPLOY:
  161. assert connection_banner._poll_for(lambda: has_cloud_banner(driver))
  162. else:
  163. _assert_token(connection_banner, driver)
  164. assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
  165. driver.delete_cookie("backend-enabled")
  166. driver.refresh()
  167. _assert_token(connection_banner, driver)
  168. assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))