test_call_script.py 12 KB

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