test_exception_handlers.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. """Integration tests for event exception handlers."""
  2. from __future__ import annotations
  3. import time
  4. from typing import Generator, Type
  5. import pytest
  6. from selenium.webdriver.common.by import By
  7. from selenium.webdriver.remote.webdriver import WebDriver
  8. from selenium.webdriver.support import expected_conditions as EC
  9. from selenium.webdriver.support.ui import WebDriverWait
  10. from reflex.testing import AppHarness, AppHarnessProd
  11. def TestApp():
  12. """A test app for event exception handler integration."""
  13. import reflex as rx
  14. class TestAppConfig(rx.Config):
  15. """Config for the TestApp app."""
  16. pass
  17. class TestAppState(rx.State):
  18. """State for the TestApp app."""
  19. react_error: bool = False
  20. def divide_by_number(self, number: int):
  21. """Divide by number and print the result.
  22. Args:
  23. number: number to divide by
  24. """
  25. print(1 / number)
  26. app = rx.App(state=rx.State)
  27. @app.add_page
  28. def index():
  29. return rx.vstack(
  30. rx.button(
  31. "induce_frontend_error",
  32. on_click=rx.call_script("induce_frontend_error()"),
  33. id="induce-frontend-error-btn",
  34. ),
  35. rx.button(
  36. "induce_backend_error",
  37. on_click=lambda: TestAppState.divide_by_number(0), # type: ignore
  38. id="induce-backend-error-btn",
  39. ),
  40. rx.button(
  41. "induce_react_error",
  42. on_click=TestAppState.set_react_error(True), # type: ignore
  43. id="induce-react-error-btn",
  44. ),
  45. rx.box(
  46. rx.cond(
  47. TestAppState.react_error,
  48. rx.Var.create({"invalid": "cannot have object as child"}),
  49. "",
  50. ),
  51. ),
  52. )
  53. @pytest.fixture(scope="module")
  54. def test_app(
  55. app_harness_env: Type[AppHarness], tmp_path_factory
  56. ) -> Generator[AppHarness, None, None]:
  57. """Start TestApp app at tmp_path via AppHarness.
  58. Args:
  59. app_harness_env: either AppHarness (dev) or AppHarnessProd (prod)
  60. tmp_path_factory: pytest tmp_path_factory fixture
  61. Yields:
  62. running AppHarness instance
  63. """
  64. with app_harness_env.create(
  65. root=tmp_path_factory.mktemp("test_app"),
  66. app_name=f"testapp_{app_harness_env.__name__.lower()}",
  67. app_source=TestApp,
  68. ) as harness:
  69. yield harness
  70. @pytest.fixture
  71. def driver(test_app: AppHarness) -> Generator[WebDriver, None, None]:
  72. """Get an instance of the browser open to the test_app app.
  73. Args:
  74. test_app: harness for TestApp app
  75. Yields:
  76. WebDriver instance.
  77. """
  78. assert test_app.app_instance is not None, "app is not running"
  79. driver = test_app.frontend()
  80. try:
  81. yield driver
  82. finally:
  83. driver.quit()
  84. def test_frontend_exception_handler_during_runtime(
  85. driver: WebDriver,
  86. capsys,
  87. ):
  88. """Test calling frontend exception handler during runtime.
  89. We send an event containing a call to a non-existent function in the frontend.
  90. This should trigger the default frontend exception handler.
  91. Args:
  92. driver: WebDriver instance.
  93. capsys: pytest fixture for capturing stdout and stderr.
  94. """
  95. reset_button = WebDriverWait(driver, 20).until(
  96. EC.element_to_be_clickable((By.ID, "induce-frontend-error-btn"))
  97. )
  98. reset_button.click()
  99. # Wait for the error to be logged
  100. time.sleep(2)
  101. captured_default_handler_output = capsys.readouterr()
  102. assert (
  103. "induce_frontend_error" in captured_default_handler_output.out
  104. and "ReferenceError" in captured_default_handler_output.out
  105. )
  106. def test_backend_exception_handler_during_runtime(
  107. driver: WebDriver,
  108. capsys,
  109. ):
  110. """Test calling backend exception handler during runtime.
  111. We invoke TestAppState.divide_by_zero to induce backend error.
  112. This should trigger the default backend exception handler.
  113. Args:
  114. driver: WebDriver instance.
  115. capsys: pytest fixture for capturing stdout and stderr.
  116. """
  117. reset_button = WebDriverWait(driver, 20).until(
  118. EC.element_to_be_clickable((By.ID, "induce-backend-error-btn"))
  119. )
  120. reset_button.click()
  121. # Wait for the error to be logged
  122. time.sleep(2)
  123. captured_default_handler_output = capsys.readouterr()
  124. assert (
  125. "divide_by_number" in captured_default_handler_output.out
  126. and "ZeroDivisionError" in captured_default_handler_output.out
  127. )
  128. def test_frontend_exception_handler_with_react(
  129. test_app: AppHarness,
  130. driver: WebDriver,
  131. capsys,
  132. ):
  133. """Test calling frontend exception handler during runtime.
  134. Render an object as a react child, which is invalid.
  135. Args:
  136. test_app: harness for TestApp app
  137. driver: WebDriver instance.
  138. capsys: pytest fixture for capturing stdout and stderr.
  139. """
  140. reset_button = WebDriverWait(driver, 20).until(
  141. EC.element_to_be_clickable((By.ID, "induce-react-error-btn"))
  142. )
  143. reset_button.click()
  144. # Wait for the error to be logged
  145. time.sleep(2)
  146. captured_default_handler_output = capsys.readouterr()
  147. if isinstance(test_app, AppHarnessProd):
  148. assert "Error: Minified React error #31" in captured_default_handler_output.out
  149. else:
  150. assert (
  151. "Error: Objects are not valid as a React child (found: object with keys \n{invalid})"
  152. in captured_default_handler_output.out
  153. )