test_call_script.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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. from .utils import SessionStorage
  9. def CallScript():
  10. """A test app for browser javascript integration."""
  11. from typing import Dict, List, Optional, Union
  12. import reflex as rx
  13. inline_scripts = """
  14. let inline_counter = 0
  15. function inline1() {
  16. inline_counter += 1
  17. return "inline1"
  18. }
  19. function inline2() {
  20. inline_counter += 1
  21. console.log("inline2")
  22. }
  23. function inline3() {
  24. inline_counter += 1
  25. return {inline3: 42, a: [1, 2, 3], s: 'js', o: {a: 1, b: 2}}
  26. }
  27. async function inline4() {
  28. inline_counter += 1
  29. return "async inline4"
  30. }
  31. """
  32. external_scripts = inline_scripts.replace("inline", "external")
  33. class CallScriptState(rx.State):
  34. results: List[Optional[Union[str, Dict, List]]] = []
  35. inline_counter: int = 0
  36. external_counter: int = 0
  37. value: str = "Initial"
  38. def call_script_callback(self, result):
  39. self.results.append(result)
  40. def call_script_callback_other_arg(self, result, other_arg):
  41. self.results.append([other_arg, result])
  42. def call_scripts_inline_yield(self):
  43. yield rx.call_script("inline1()")
  44. yield rx.call_script("inline2()")
  45. yield rx.call_script("inline3()")
  46. yield rx.call_script("inline4()")
  47. def call_script_inline_return(self):
  48. return rx.call_script("inline2()")
  49. def call_scripts_inline_yield_callback(self):
  50. yield rx.call_script(
  51. "inline1()", callback=CallScriptState.call_script_callback
  52. )
  53. yield rx.call_script(
  54. "inline2()", callback=CallScriptState.call_script_callback
  55. )
  56. yield rx.call_script(
  57. "inline3()", callback=CallScriptState.call_script_callback
  58. )
  59. yield rx.call_script(
  60. "inline4()", callback=CallScriptState.call_script_callback
  61. )
  62. def call_script_inline_return_callback(self):
  63. return rx.call_script(
  64. "inline3()", callback=CallScriptState.call_script_callback
  65. )
  66. def call_script_inline_return_lambda(self):
  67. return rx.call_script(
  68. "inline2()",
  69. callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
  70. result, "lambda"
  71. ),
  72. )
  73. def get_inline_counter(self):
  74. return rx.call_script(
  75. "inline_counter",
  76. callback=CallScriptState.set_inline_counter, # type: ignore
  77. )
  78. def call_scripts_external_yield(self):
  79. yield rx.call_script("external1()")
  80. yield rx.call_script("external2()")
  81. yield rx.call_script("external3()")
  82. yield rx.call_script("external4()")
  83. def call_script_external_return(self):
  84. return rx.call_script("external2()")
  85. def call_scripts_external_yield_callback(self):
  86. yield rx.call_script(
  87. "external1()", callback=CallScriptState.call_script_callback
  88. )
  89. yield rx.call_script(
  90. "external2()", callback=CallScriptState.call_script_callback
  91. )
  92. yield rx.call_script(
  93. "external3()", callback=CallScriptState.call_script_callback
  94. )
  95. yield rx.call_script(
  96. "external4()", callback=CallScriptState.call_script_callback
  97. )
  98. def call_script_external_return_callback(self):
  99. return rx.call_script(
  100. "external3()", callback=CallScriptState.call_script_callback
  101. )
  102. def call_script_external_return_lambda(self):
  103. return rx.call_script(
  104. "external2()",
  105. callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
  106. result, "lambda"
  107. ),
  108. )
  109. def get_external_counter(self):
  110. return rx.call_script(
  111. "external_counter",
  112. callback=CallScriptState.set_external_counter, # type: ignore
  113. )
  114. def reset_(self):
  115. yield rx.call_script("inline_counter = 0; external_counter = 0")
  116. self.reset()
  117. app = rx.App(state=rx.State)
  118. with open("assets/external.js", "w") as f:
  119. f.write(external_scripts)
  120. @app.add_page
  121. def index():
  122. return rx.vstack(
  123. rx.input(
  124. value=CallScriptState.inline_counter.to(str), # type: ignore
  125. id="inline_counter",
  126. read_only=True,
  127. ),
  128. rx.input(
  129. value=CallScriptState.external_counter.to(str), # type: ignore
  130. id="external_counter",
  131. read_only=True,
  132. ),
  133. rx.text_area(
  134. value=CallScriptState.results.to_string(), # type: ignore
  135. id="results",
  136. read_only=True,
  137. ),
  138. rx.script(inline_scripts),
  139. rx.script(src="/external.js"),
  140. rx.button(
  141. "call_scripts_inline_yield",
  142. on_click=CallScriptState.call_scripts_inline_yield,
  143. id="inline_yield",
  144. ),
  145. rx.button(
  146. "call_script_inline_return",
  147. on_click=CallScriptState.call_script_inline_return,
  148. id="inline_return",
  149. ),
  150. rx.button(
  151. "call_scripts_inline_yield_callback",
  152. on_click=CallScriptState.call_scripts_inline_yield_callback,
  153. id="inline_yield_callback",
  154. ),
  155. rx.button(
  156. "call_script_inline_return_callback",
  157. on_click=CallScriptState.call_script_inline_return_callback,
  158. id="inline_return_callback",
  159. ),
  160. rx.button(
  161. "call_script_inline_return_lambda",
  162. on_click=CallScriptState.call_script_inline_return_lambda,
  163. id="inline_return_lambda",
  164. ),
  165. rx.button(
  166. "call_scripts_external_yield",
  167. on_click=CallScriptState.call_scripts_external_yield,
  168. id="external_yield",
  169. ),
  170. rx.button(
  171. "call_script_external_return",
  172. on_click=CallScriptState.call_script_external_return,
  173. id="external_return",
  174. ),
  175. rx.button(
  176. "call_scripts_external_yield_callback",
  177. on_click=CallScriptState.call_scripts_external_yield_callback,
  178. id="external_yield_callback",
  179. ),
  180. rx.button(
  181. "call_script_external_return_callback",
  182. on_click=CallScriptState.call_script_external_return_callback,
  183. id="external_return_callback",
  184. ),
  185. rx.button(
  186. "call_script_external_return_lambda",
  187. on_click=CallScriptState.call_script_external_return_lambda,
  188. id="external_return_lambda",
  189. ),
  190. rx.button(
  191. "Update Inline Counter",
  192. on_click=CallScriptState.get_inline_counter,
  193. id="update_inline_counter",
  194. ),
  195. rx.button(
  196. "Update External Counter",
  197. on_click=CallScriptState.get_external_counter,
  198. id="update_external_counter",
  199. ),
  200. rx.button(
  201. CallScriptState.value,
  202. on_click=rx.call_script(
  203. "'updated'",
  204. callback=CallScriptState.set_value, # type: ignore
  205. ),
  206. id="update_value",
  207. ),
  208. rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
  209. )
  210. @pytest.fixture(scope="module")
  211. def call_script(tmp_path_factory) -> Generator[AppHarness, None, None]:
  212. """Start CallScript app at tmp_path via AppHarness.
  213. Args:
  214. tmp_path_factory: pytest tmp_path_factory fixture
  215. Yields:
  216. running AppHarness instance
  217. """
  218. with AppHarness.create(
  219. root=tmp_path_factory.mktemp("call_script"),
  220. app_source=CallScript, # type: ignore
  221. ) as harness:
  222. yield harness
  223. @pytest.fixture
  224. def driver(call_script: AppHarness) -> Generator[WebDriver, None, None]:
  225. """Get an instance of the browser open to the call_script app.
  226. Args:
  227. call_script: harness for CallScript app
  228. Yields:
  229. WebDriver instance.
  230. """
  231. assert call_script.app_instance is not None, "app is not running"
  232. driver = call_script.frontend()
  233. try:
  234. yield driver
  235. finally:
  236. driver.quit()
  237. def assert_token(driver: WebDriver) -> str:
  238. """Get the token associated with backend state.
  239. Args:
  240. driver: WebDriver instance.
  241. Returns:
  242. The token visible in the driver browser.
  243. """
  244. ss = SessionStorage(driver)
  245. assert AppHarness._poll_for(lambda: ss.get("token") is not None), "token not found"
  246. return ss.get("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(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"
  310. # Check that triggering script from event trigger calls callback
  311. update_value_button = driver.find_element(By.ID, "update_value")
  312. update_value_button.click()
  313. assert (
  314. call_script.poll_for_content(update_value_button, exp_not_equal="Initial")
  315. == "updated"
  316. )