test_input.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. """Integration tests for text input and related components."""
  2. from collections.abc import Generator
  3. import pytest
  4. from selenium.webdriver.common.by import By
  5. from selenium.webdriver.common.keys import Keys
  6. from reflex.testing import AppHarness
  7. def FullyControlledInput():
  8. """App using a fully controlled input with implicit debounce wrapper."""
  9. import reflex as rx
  10. class State(rx.State):
  11. text: str = "initial"
  12. @rx.event
  13. def set_text(self, text: str):
  14. self.text = text
  15. app = rx.App()
  16. @app.add_page
  17. def index():
  18. return rx.fragment(
  19. rx.input(
  20. value=State.router.session.client_token, is_read_only=True, id="token"
  21. ),
  22. rx.input(
  23. id="debounce_input_input",
  24. on_change=State.set_text,
  25. value=State.text,
  26. ),
  27. rx.input(value=State.text, id="value_input", is_read_only=True),
  28. rx.input(on_change=State.set_text, id="on_change_input"),
  29. rx.el.input(
  30. value=State.text,
  31. id="plain_value_input",
  32. disabled=True,
  33. _disabled={"width": "42px"},
  34. ),
  35. rx.input(default_value="default", id="default_input"),
  36. rx.el.input(
  37. type="text", default_value="default plain", id="plain_default_input"
  38. ),
  39. rx.checkbox(default_checked=True, id="default_checkbox"),
  40. rx.el.input(
  41. type="checkbox", default_checked=True, id="plain_default_checkbox"
  42. ),
  43. rx.button(
  44. "CLEAR", on_click=rx.set_value("on_change_input", ""), id="clear"
  45. ),
  46. )
  47. @pytest.fixture()
  48. def fully_controlled_input(tmp_path) -> Generator[AppHarness, None, None]:
  49. """Start FullyControlledInput app at tmp_path via AppHarness.
  50. Args:
  51. tmp_path: pytest tmp_path fixture
  52. Yields:
  53. running AppHarness instance
  54. """
  55. with AppHarness.create(
  56. root=tmp_path,
  57. app_source=FullyControlledInput,
  58. ) as harness:
  59. yield harness
  60. @pytest.mark.asyncio
  61. async def test_fully_controlled_input(fully_controlled_input: AppHarness):
  62. """Type text after moving cursor. Update text on backend.
  63. Args:
  64. fully_controlled_input: harness for FullyControlledInput app
  65. """
  66. assert fully_controlled_input.app_instance is not None, "app is not running"
  67. driver = fully_controlled_input.frontend()
  68. # get a reference to the connected client
  69. token_input = driver.find_element(By.ID, "token")
  70. assert token_input
  71. # wait for the backend connection to send the token
  72. token = fully_controlled_input.poll_for_value(token_input)
  73. assert token
  74. state_name = fully_controlled_input.get_state_name("_state")
  75. full_state_name = fully_controlled_input.get_full_state_name(["_state"])
  76. async def get_state_text():
  77. state = await fully_controlled_input.get_state(f"{token}_{full_state_name}")
  78. return state.substates[state_name].text
  79. # ensure defaults are set correctly
  80. assert (
  81. fully_controlled_input.poll_for_value(
  82. driver.find_element(By.ID, "default_input")
  83. )
  84. == "default"
  85. )
  86. assert (
  87. fully_controlled_input.poll_for_value(
  88. driver.find_element(By.ID, "plain_default_input")
  89. )
  90. == "default plain"
  91. )
  92. assert (
  93. fully_controlled_input.poll_for_value(
  94. driver.find_element(By.ID, "default_checkbox")
  95. )
  96. == "on"
  97. )
  98. assert (
  99. fully_controlled_input.poll_for_value(
  100. driver.find_element(By.ID, "plain_default_checkbox")
  101. )
  102. == "on"
  103. )
  104. # find the input and wait for it to have the initial state value
  105. debounce_input = driver.find_element(By.ID, "debounce_input_input")
  106. value_input = driver.find_element(By.ID, "value_input")
  107. on_change_input = driver.find_element(By.ID, "on_change_input")
  108. plain_value_input = driver.find_element(By.ID, "plain_value_input")
  109. clear_button = driver.find_element(By.ID, "clear")
  110. assert fully_controlled_input.poll_for_value(debounce_input) == "initial"
  111. assert fully_controlled_input.poll_for_value(value_input) == "initial"
  112. assert fully_controlled_input.poll_for_value(plain_value_input) == "initial"
  113. assert plain_value_input.value_of_css_property("width") == "42px"
  114. # move cursor to home, then to the right and type characters
  115. debounce_input.send_keys(*([Keys.ARROW_LEFT] * len("initial")), Keys.ARROW_RIGHT)
  116. debounce_input.send_keys("foo")
  117. assert AppHarness._poll_for(
  118. lambda: fully_controlled_input.poll_for_value(value_input) == "ifoonitial"
  119. )
  120. assert debounce_input.get_attribute("value") == "ifoonitial"
  121. assert await get_state_text() == "ifoonitial"
  122. assert fully_controlled_input.poll_for_value(plain_value_input) == "ifoonitial"
  123. # clear the input on the backend
  124. async with fully_controlled_input.modify_state(
  125. f"{token}_{full_state_name}"
  126. ) as state:
  127. state.substates[state_name].text = ""
  128. assert await get_state_text() == ""
  129. assert (
  130. fully_controlled_input.poll_for_value(
  131. debounce_input, exp_not_equal="ifoonitial"
  132. )
  133. == ""
  134. )
  135. # type more characters
  136. debounce_input.send_keys("getting testing done")
  137. assert AppHarness._poll_for(
  138. lambda: fully_controlled_input.poll_for_value(value_input)
  139. == "getting testing done"
  140. )
  141. assert debounce_input.get_attribute("value") == "getting testing done"
  142. assert await get_state_text() == "getting testing done"
  143. assert (
  144. fully_controlled_input.poll_for_value(plain_value_input)
  145. == "getting testing done"
  146. )
  147. # type into the on_change input
  148. on_change_input.send_keys("overwrite the state")
  149. assert AppHarness._poll_for(
  150. lambda: fully_controlled_input.poll_for_value(value_input)
  151. == "overwrite the state"
  152. )
  153. assert debounce_input.get_attribute("value") == "overwrite the state"
  154. assert on_change_input.get_attribute("value") == "overwrite the state"
  155. assert await get_state_text() == "overwrite the state"
  156. assert (
  157. fully_controlled_input.poll_for_value(plain_value_input)
  158. == "overwrite the state"
  159. )
  160. clear_button.click()
  161. assert AppHarness._poll_for(lambda: on_change_input.get_attribute("value") == "")
  162. # potential bug: clearing the on_change field doesn't itself trigger on_change
  163. # assert backend_state.text == "" #noqa: ERA001
  164. # assert debounce_input.get_attribute("value") == "" #noqa: ERA001
  165. # assert value_input.get_attribute("value") == "" #noqa: ERA001