test_call_script.py 12 KB

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