test_call_script.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. """Integration tests for client side storage."""
  2. from __future__ import annotations
  3. from typing import Generator
  4. import pytest
  5. from selenium.webdriver.common.by import By
  6. from selenium.webdriver.remote.webdriver import WebDriver
  7. from reflex.testing import AppHarness
  8. def CallScript():
  9. """A test app for browser javascript integration."""
  10. import reflex as rx
  11. inline_scripts = """
  12. let inline_counter = 0
  13. function inline1() {
  14. inline_counter += 1
  15. return "inline1"
  16. }
  17. function inline2() {
  18. inline_counter += 1
  19. console.log("inline2")
  20. }
  21. function inline3() {
  22. inline_counter += 1
  23. return {inline3: 42, a: [1, 2, 3], s: 'js', o: {a: 1, b: 2}}
  24. }
  25. """
  26. external_scripts = inline_scripts.replace("inline", "external")
  27. class CallScriptState(rx.State):
  28. results: list[str | dict | list | None] = []
  29. inline_counter: int = 0
  30. external_counter: int = 0
  31. def call_script_callback(self, result):
  32. self.results.append(result)
  33. def call_script_callback_other_arg(self, result, other_arg):
  34. self.results.append([other_arg, result])
  35. def call_scripts_inline_yield(self):
  36. yield rx.call_script("inline1()")
  37. yield rx.call_script("inline2()")
  38. yield rx.call_script("inline3()")
  39. def call_script_inline_return(self):
  40. return rx.call_script("inline2()")
  41. def call_scripts_inline_yield_callback(self):
  42. yield rx.call_script(
  43. "inline1()", callback=CallScriptState.call_script_callback
  44. )
  45. yield rx.call_script(
  46. "inline2()", callback=CallScriptState.call_script_callback
  47. )
  48. yield rx.call_script(
  49. "inline3()", callback=CallScriptState.call_script_callback
  50. )
  51. def call_script_inline_return_callback(self):
  52. return rx.call_script(
  53. "inline3()", callback=CallScriptState.call_script_callback
  54. )
  55. def call_script_inline_return_lambda(self):
  56. return rx.call_script(
  57. "inline2()",
  58. callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
  59. result, "lambda"
  60. ),
  61. )
  62. def get_inline_counter(self):
  63. return rx.call_script(
  64. "inline_counter",
  65. callback=CallScriptState.set_inline_counter, # type: ignore
  66. )
  67. def call_scripts_external_yield(self):
  68. yield rx.call_script("external1()")
  69. yield rx.call_script("external2()")
  70. yield rx.call_script("external3()")
  71. def call_script_external_return(self):
  72. return rx.call_script("external2()")
  73. def call_scripts_external_yield_callback(self):
  74. yield rx.call_script(
  75. "external1()", callback=CallScriptState.call_script_callback
  76. )
  77. yield rx.call_script(
  78. "external2()", callback=CallScriptState.call_script_callback
  79. )
  80. yield rx.call_script(
  81. "external3()", callback=CallScriptState.call_script_callback
  82. )
  83. def call_script_external_return_callback(self):
  84. return rx.call_script(
  85. "external3()", callback=CallScriptState.call_script_callback
  86. )
  87. def call_script_external_return_lambda(self):
  88. return rx.call_script(
  89. "external2()",
  90. callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
  91. result, "lambda"
  92. ),
  93. )
  94. def get_external_counter(self):
  95. return rx.call_script(
  96. "external_counter",
  97. callback=CallScriptState.set_external_counter, # type: ignore
  98. )
  99. def reset_(self):
  100. yield rx.call_script("inline_counter = 0; external_counter = 0")
  101. self.reset()
  102. app = rx.App(state=CallScriptState)
  103. with open("assets/external.js", "w") as f:
  104. f.write(external_scripts)
  105. @app.add_page
  106. def index():
  107. return rx.vstack(
  108. rx.input(
  109. value=CallScriptState.router.session.client_token,
  110. is_read_only=True,
  111. id="token",
  112. ),
  113. rx.input(
  114. value=CallScriptState.inline_counter.to(str), # type: ignore
  115. id="inline_counter",
  116. is_read_only=True,
  117. ),
  118. rx.input(
  119. value=CallScriptState.external_counter.to(str), # type: ignore
  120. id="external_counter",
  121. is_read_only=True,
  122. ),
  123. rx.text_area(
  124. value=CallScriptState.results.to_string(), # type: ignore
  125. id="results",
  126. is_read_only=True,
  127. ),
  128. rx.script(inline_scripts),
  129. rx.script(src="/external.js"),
  130. rx.button(
  131. "call_scripts_inline_yield",
  132. on_click=CallScriptState.call_scripts_inline_yield,
  133. id="inline_yield",
  134. ),
  135. rx.button(
  136. "call_script_inline_return",
  137. on_click=CallScriptState.call_script_inline_return,
  138. id="inline_return",
  139. ),
  140. rx.button(
  141. "call_scripts_inline_yield_callback",
  142. on_click=CallScriptState.call_scripts_inline_yield_callback,
  143. id="inline_yield_callback",
  144. ),
  145. rx.button(
  146. "call_script_inline_return_callback",
  147. on_click=CallScriptState.call_script_inline_return_callback,
  148. id="inline_return_callback",
  149. ),
  150. rx.button(
  151. "call_script_inline_return_lambda",
  152. on_click=CallScriptState.call_script_inline_return_lambda,
  153. id="inline_return_lambda",
  154. ),
  155. rx.button(
  156. "call_scripts_external_yield",
  157. on_click=CallScriptState.call_scripts_external_yield,
  158. id="external_yield",
  159. ),
  160. rx.button(
  161. "call_script_external_return",
  162. on_click=CallScriptState.call_script_external_return,
  163. id="external_return",
  164. ),
  165. rx.button(
  166. "call_scripts_external_yield_callback",
  167. on_click=CallScriptState.call_scripts_external_yield_callback,
  168. id="external_yield_callback",
  169. ),
  170. rx.button(
  171. "call_script_external_return_callback",
  172. on_click=CallScriptState.call_script_external_return_callback,
  173. id="external_return_callback",
  174. ),
  175. rx.button(
  176. "call_script_external_return_lambda",
  177. on_click=CallScriptState.call_script_external_return_lambda,
  178. id="external_return_lambda",
  179. ),
  180. rx.button(
  181. "Update Inline Counter",
  182. on_click=CallScriptState.get_inline_counter,
  183. id="update_inline_counter",
  184. ),
  185. rx.button(
  186. "Update External Counter",
  187. on_click=CallScriptState.get_external_counter,
  188. id="update_external_counter",
  189. ),
  190. rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
  191. )
  192. app.compile()
  193. @pytest.fixture(scope="session")
  194. def call_script(tmp_path_factory) -> Generator[AppHarness, None, None]:
  195. """Start CallScript app at tmp_path via AppHarness.
  196. Args:
  197. tmp_path_factory: pytest tmp_path_factory fixture
  198. Yields:
  199. running AppHarness instance
  200. """
  201. with AppHarness.create(
  202. root=tmp_path_factory.mktemp("call_script"),
  203. app_source=CallScript, # type: ignore
  204. ) as harness:
  205. yield harness
  206. @pytest.fixture
  207. def driver(call_script: AppHarness) -> Generator[WebDriver, None, None]:
  208. """Get an instance of the browser open to the call_script app.
  209. Args:
  210. call_script: harness for CallScript app
  211. Yields:
  212. WebDriver instance.
  213. """
  214. assert call_script.app_instance is not None, "app is not running"
  215. driver = call_script.frontend()
  216. try:
  217. yield driver
  218. finally:
  219. driver.quit()
  220. def assert_token(call_script: AppHarness, driver: WebDriver) -> str:
  221. """Get the token associated with backend state.
  222. Args:
  223. call_script: harness for CallScript app.
  224. driver: WebDriver instance.
  225. Returns:
  226. The token visible in the driver browser.
  227. """
  228. assert call_script.app_instance is not None
  229. token_input = driver.find_element(By.ID, "token")
  230. assert token_input
  231. # wait for the backend connection to send the token
  232. token = call_script.poll_for_value(token_input)
  233. assert token is not None
  234. return token
  235. @pytest.mark.parametrize("script", ["inline", "external"])
  236. def test_call_script(
  237. call_script: AppHarness,
  238. driver: WebDriver,
  239. script: str,
  240. ):
  241. """Test calling javascript functions from python.
  242. Args:
  243. call_script: harness for CallScript app.
  244. driver: WebDriver instance.
  245. script: The type of script to test.
  246. """
  247. assert_token(call_script, driver)
  248. reset_button = driver.find_element(By.ID, "reset")
  249. update_counter_button = driver.find_element(By.ID, f"update_{script}_counter")
  250. counter = driver.find_element(By.ID, f"{script}_counter")
  251. results = driver.find_element(By.ID, "results")
  252. yield_button = driver.find_element(By.ID, f"{script}_yield")
  253. return_button = driver.find_element(By.ID, f"{script}_return")
  254. yield_callback_button = driver.find_element(By.ID, f"{script}_yield_callback")
  255. return_callback_button = driver.find_element(By.ID, f"{script}_return_callback")
  256. return_lambda_button = driver.find_element(By.ID, f"{script}_return_lambda")
  257. yield_button.click()
  258. update_counter_button.click()
  259. assert call_script.poll_for_value(counter, exp_not_equal="0") == "3"
  260. reset_button.click()
  261. assert call_script.poll_for_value(counter, exp_not_equal="3") == "0"
  262. return_button.click()
  263. update_counter_button.click()
  264. assert call_script.poll_for_value(counter, exp_not_equal="0") == "1"
  265. reset_button.click()
  266. assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"
  267. yield_callback_button.click()
  268. update_counter_button.click()
  269. assert call_script.poll_for_value(counter, exp_not_equal="0") == "3"
  270. assert call_script.poll_for_value(
  271. results, exp_not_equal="[]"
  272. ) == '["%s1",null,{"%s3":42,"a":[1,2,3],"s":"js","o":{"a":1,"b":2}}]' % (
  273. script,
  274. script,
  275. )
  276. reset_button.click()
  277. assert call_script.poll_for_value(counter, exp_not_equal="3") == "0"
  278. return_callback_button.click()
  279. update_counter_button.click()
  280. assert call_script.poll_for_value(counter, exp_not_equal="0") == "1"
  281. assert (
  282. call_script.poll_for_value(results, exp_not_equal="[]")
  283. == '[{"%s3":42,"a":[1,2,3],"s":"js","o":{"a":1,"b":2}}]' % script
  284. )
  285. reset_button.click()
  286. assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"
  287. return_lambda_button.click()
  288. update_counter_button.click()
  289. assert call_script.poll_for_value(counter, exp_not_equal="0") == "1"
  290. assert (
  291. call_script.poll_for_value(results, exp_not_equal="[]") == '[["lambda",null]]'
  292. )
  293. reset_button.click()
  294. assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"