test_exception_handlers.py 5.5 KB

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