test_call_script.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  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 pathlib import Path
  12. from typing import Dict, List, Optional, Union
  13. import reflex as rx
  14. inline_scripts = """
  15. let inline_counter = 0
  16. function inline1() {
  17. inline_counter += 1
  18. return "inline1"
  19. }
  20. function inline2() {
  21. inline_counter += 1
  22. console.log("inline2")
  23. }
  24. function inline3() {
  25. inline_counter += 1
  26. return {inline3: 42, a: [1, 2, 3], s: 'js', o: {a: 1, b: 2}}
  27. }
  28. async function inline4() {
  29. inline_counter += 1
  30. return "async inline4"
  31. }
  32. """
  33. external_scripts = inline_scripts.replace("inline", "external")
  34. class CallScriptState(rx.State):
  35. results: List[Optional[Union[str, Dict, List]]] = []
  36. inline_counter: int = 0
  37. external_counter: int = 0
  38. value: str = "Initial"
  39. last_result: str = ""
  40. def call_script_callback(self, result):
  41. self.results.append(result)
  42. def call_script_callback_other_arg(self, result, other_arg):
  43. self.results.append([other_arg, result])
  44. @rx.event
  45. def call_scripts_inline_yield(self):
  46. yield rx.call_script("inline1()")
  47. yield rx.call_script("inline2()")
  48. yield rx.call_script("inline3()")
  49. yield rx.call_script("inline4()")
  50. @rx.event
  51. def call_script_inline_return(self):
  52. return rx.call_script("inline2()")
  53. @rx.event
  54. def call_scripts_inline_yield_callback(self):
  55. yield rx.call_script(
  56. "inline1()", callback=CallScriptState.call_script_callback
  57. )
  58. yield rx.call_script(
  59. "inline2()", callback=CallScriptState.call_script_callback
  60. )
  61. yield rx.call_script(
  62. "inline3()", callback=CallScriptState.call_script_callback
  63. )
  64. yield rx.call_script(
  65. "inline4()", callback=CallScriptState.call_script_callback
  66. )
  67. @rx.event
  68. def call_script_inline_return_callback(self):
  69. return rx.call_script(
  70. "inline3()", callback=CallScriptState.call_script_callback
  71. )
  72. @rx.event
  73. def call_script_inline_return_lambda(self):
  74. return rx.call_script(
  75. "inline2()",
  76. callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
  77. result, "lambda"
  78. ),
  79. )
  80. @rx.event
  81. def get_inline_counter(self):
  82. return rx.call_script(
  83. "inline_counter",
  84. callback=CallScriptState.set_inline_counter, # type: ignore
  85. )
  86. @rx.event
  87. def call_scripts_external_yield(self):
  88. yield rx.call_script("external1()")
  89. yield rx.call_script("external2()")
  90. yield rx.call_script("external3()")
  91. yield rx.call_script("external4()")
  92. @rx.event
  93. def call_script_external_return(self):
  94. return rx.call_script("external2()")
  95. @rx.event
  96. def call_scripts_external_yield_callback(self):
  97. yield rx.call_script(
  98. "external1()", callback=CallScriptState.call_script_callback
  99. )
  100. yield rx.call_script(
  101. "external2()", callback=CallScriptState.call_script_callback
  102. )
  103. yield rx.call_script(
  104. "external3()", callback=CallScriptState.call_script_callback
  105. )
  106. yield rx.call_script(
  107. "external4()", callback=CallScriptState.call_script_callback
  108. )
  109. @rx.event
  110. def call_script_external_return_callback(self):
  111. return rx.call_script(
  112. "external3()", callback=CallScriptState.call_script_callback
  113. )
  114. @rx.event
  115. def call_script_external_return_lambda(self):
  116. return rx.call_script(
  117. "external2()",
  118. callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
  119. result, "lambda"
  120. ),
  121. )
  122. @rx.event
  123. def get_external_counter(self):
  124. return rx.call_script(
  125. "external_counter",
  126. callback=CallScriptState.set_external_counter, # type: ignore
  127. )
  128. @rx.event
  129. def call_with_var_f_string(self):
  130. return rx.call_script(
  131. f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}",
  132. callback=CallScriptState.set_last_result, # type: ignore
  133. )
  134. @rx.event
  135. def call_with_var_str_cast(self):
  136. return rx.call_script(
  137. f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}",
  138. callback=CallScriptState.set_last_result, # type: ignore
  139. )
  140. @rx.event
  141. def call_with_var_f_string_wrapped(self):
  142. return rx.call_script(
  143. rx.Var(f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}"),
  144. callback=CallScriptState.set_last_result, # type: ignore
  145. )
  146. @rx.event
  147. def call_with_var_str_cast_wrapped(self):
  148. return rx.call_script(
  149. rx.Var(
  150. f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}"
  151. ),
  152. callback=CallScriptState.set_last_result, # type: ignore
  153. )
  154. @rx.event
  155. def reset_(self):
  156. yield rx.call_script("inline_counter = 0; external_counter = 0")
  157. self.reset()
  158. app = rx.App(state=rx.State)
  159. Path("assets/external.js").write_text(external_scripts)
  160. @app.add_page
  161. def index():
  162. return rx.vstack(
  163. rx.input(
  164. value=CallScriptState.inline_counter.to(str), # type: ignore
  165. id="inline_counter",
  166. read_only=True,
  167. ),
  168. rx.input(
  169. value=CallScriptState.external_counter.to(str), # type: ignore
  170. id="external_counter",
  171. read_only=True,
  172. ),
  173. rx.text_area(
  174. value=CallScriptState.results.to_string(), # type: ignore
  175. id="results",
  176. read_only=True,
  177. ),
  178. rx.script(inline_scripts),
  179. rx.script(src="/external.js"),
  180. rx.button(
  181. "call_scripts_inline_yield",
  182. on_click=CallScriptState.call_scripts_inline_yield,
  183. id="inline_yield",
  184. ),
  185. rx.button(
  186. "call_script_inline_return",
  187. on_click=CallScriptState.call_script_inline_return,
  188. id="inline_return",
  189. ),
  190. rx.button(
  191. "call_scripts_inline_yield_callback",
  192. on_click=CallScriptState.call_scripts_inline_yield_callback,
  193. id="inline_yield_callback",
  194. ),
  195. rx.button(
  196. "call_script_inline_return_callback",
  197. on_click=CallScriptState.call_script_inline_return_callback,
  198. id="inline_return_callback",
  199. ),
  200. rx.button(
  201. "call_script_inline_return_lambda",
  202. on_click=CallScriptState.call_script_inline_return_lambda,
  203. id="inline_return_lambda",
  204. ),
  205. rx.button(
  206. "call_scripts_external_yield",
  207. on_click=CallScriptState.call_scripts_external_yield,
  208. id="external_yield",
  209. ),
  210. rx.button(
  211. "call_script_external_return",
  212. on_click=CallScriptState.call_script_external_return,
  213. id="external_return",
  214. ),
  215. rx.button(
  216. "call_scripts_external_yield_callback",
  217. on_click=CallScriptState.call_scripts_external_yield_callback,
  218. id="external_yield_callback",
  219. ),
  220. rx.button(
  221. "call_script_external_return_callback",
  222. on_click=CallScriptState.call_script_external_return_callback,
  223. id="external_return_callback",
  224. ),
  225. rx.button(
  226. "call_script_external_return_lambda",
  227. on_click=CallScriptState.call_script_external_return_lambda,
  228. id="external_return_lambda",
  229. ),
  230. rx.button(
  231. "Update Inline Counter",
  232. on_click=CallScriptState.get_inline_counter,
  233. id="update_inline_counter",
  234. ),
  235. rx.button(
  236. "Update External Counter",
  237. on_click=CallScriptState.get_external_counter,
  238. id="update_external_counter",
  239. ),
  240. rx.button(
  241. CallScriptState.value,
  242. on_click=rx.call_script(
  243. "'updated'",
  244. callback=CallScriptState.set_value, # type: ignore
  245. ),
  246. id="update_value",
  247. ),
  248. rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
  249. rx.input(
  250. value=CallScriptState.last_result,
  251. id="last_result",
  252. read_only=True,
  253. on_click=CallScriptState.set_last_result(""), # type: ignore
  254. ),
  255. rx.button(
  256. "call_with_var_f_string",
  257. on_click=CallScriptState.call_with_var_f_string,
  258. id="call_with_var_f_string",
  259. ),
  260. rx.button(
  261. "call_with_var_str_cast",
  262. on_click=CallScriptState.call_with_var_str_cast,
  263. id="call_with_var_str_cast",
  264. ),
  265. rx.button(
  266. "call_with_var_f_string_wrapped",
  267. on_click=CallScriptState.call_with_var_f_string_wrapped,
  268. id="call_with_var_f_string_wrapped",
  269. ),
  270. rx.button(
  271. "call_with_var_str_cast_wrapped",
  272. on_click=CallScriptState.call_with_var_str_cast_wrapped,
  273. id="call_with_var_str_cast_wrapped",
  274. ),
  275. rx.button(
  276. "call_with_var_f_string_inline",
  277. on_click=rx.call_script(
  278. f"{rx.Var('inline_counter')} + {CallScriptState.last_result}",
  279. callback=CallScriptState.set_last_result, # type: ignore
  280. ),
  281. id="call_with_var_f_string_inline",
  282. ),
  283. rx.button(
  284. "call_with_var_str_cast_inline",
  285. on_click=rx.call_script(
  286. f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}",
  287. callback=CallScriptState.set_last_result, # type: ignore
  288. ),
  289. id="call_with_var_str_cast_inline",
  290. ),
  291. rx.button(
  292. "call_with_var_f_string_wrapped_inline",
  293. on_click=rx.call_script(
  294. rx.Var(
  295. f"{rx.Var('inline_counter')} + {CallScriptState.last_result}"
  296. ),
  297. callback=CallScriptState.set_last_result, # type: ignore
  298. ),
  299. id="call_with_var_f_string_wrapped_inline",
  300. ),
  301. rx.button(
  302. "call_with_var_str_cast_wrapped_inline",
  303. on_click=rx.call_script(
  304. rx.Var(
  305. f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}"
  306. ),
  307. callback=CallScriptState.set_last_result, # type: ignore
  308. ),
  309. id="call_with_var_str_cast_wrapped_inline",
  310. ),
  311. )
  312. @pytest.fixture(scope="module")
  313. def call_script(tmp_path_factory) -> Generator[AppHarness, None, None]:
  314. """Start CallScript app at tmp_path via AppHarness.
  315. Args:
  316. tmp_path_factory: pytest tmp_path_factory fixture
  317. Yields:
  318. running AppHarness instance
  319. """
  320. with AppHarness.create(
  321. root=tmp_path_factory.mktemp("call_script"),
  322. app_source=CallScript,
  323. ) as harness:
  324. yield harness
  325. @pytest.fixture
  326. def driver(call_script: AppHarness) -> Generator[WebDriver, None, None]:
  327. """Get an instance of the browser open to the call_script app.
  328. Args:
  329. call_script: harness for CallScript app
  330. Yields:
  331. WebDriver instance.
  332. """
  333. assert call_script.app_instance is not None, "app is not running"
  334. driver = call_script.frontend()
  335. try:
  336. yield driver
  337. finally:
  338. driver.quit()
  339. def assert_token(driver: WebDriver) -> str:
  340. """Get the token associated with backend state.
  341. Args:
  342. driver: WebDriver instance.
  343. Returns:
  344. The token visible in the driver browser.
  345. """
  346. ss = SessionStorage(driver)
  347. assert AppHarness._poll_for(lambda: ss.get("token") is not None), "token not found"
  348. return ss.get("token")
  349. @pytest.mark.parametrize("script", ["inline", "external"])
  350. def test_call_script(
  351. call_script: AppHarness,
  352. driver: WebDriver,
  353. script: str,
  354. ):
  355. """Test calling javascript functions from python.
  356. Args:
  357. call_script: harness for CallScript app.
  358. driver: WebDriver instance.
  359. script: The type of script to test.
  360. """
  361. assert_token(driver)
  362. reset_button = driver.find_element(By.ID, "reset")
  363. update_counter_button = driver.find_element(By.ID, f"update_{script}_counter")
  364. counter = driver.find_element(By.ID, f"{script}_counter")
  365. results = driver.find_element(By.ID, "results")
  366. yield_button = driver.find_element(By.ID, f"{script}_yield")
  367. return_button = driver.find_element(By.ID, f"{script}_return")
  368. yield_callback_button = driver.find_element(By.ID, f"{script}_yield_callback")
  369. return_callback_button = driver.find_element(By.ID, f"{script}_return_callback")
  370. return_lambda_button = driver.find_element(By.ID, f"{script}_return_lambda")
  371. yield_button.click()
  372. update_counter_button.click()
  373. assert call_script.poll_for_value(counter, exp_not_equal="0") == "4"
  374. reset_button.click()
  375. assert call_script.poll_for_value(counter, exp_not_equal="4") == "0"
  376. return_button.click()
  377. update_counter_button.click()
  378. assert call_script.poll_for_value(counter, exp_not_equal="0") == "1"
  379. reset_button.click()
  380. assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"
  381. yield_callback_button.click()
  382. update_counter_button.click()
  383. assert call_script.poll_for_value(counter, exp_not_equal="0") == "4"
  384. assert (
  385. call_script.poll_for_value(results, exp_not_equal="[]")
  386. == '["%s1",null,{"%s3":42,"a":[1,2,3],"s":"js","o":{"a":1,"b":2}},"async %s4"]'
  387. % (
  388. script,
  389. script,
  390. script,
  391. )
  392. )
  393. reset_button.click()
  394. assert call_script.poll_for_value(counter, exp_not_equal="4") == "0"
  395. return_callback_button.click()
  396. update_counter_button.click()
  397. assert call_script.poll_for_value(counter, exp_not_equal="0") == "1"
  398. assert (
  399. call_script.poll_for_value(results, exp_not_equal="[]")
  400. == '[{"%s3":42,"a":[1,2,3],"s":"js","o":{"a":1,"b":2}}]' % script
  401. )
  402. reset_button.click()
  403. assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"
  404. return_lambda_button.click()
  405. update_counter_button.click()
  406. assert call_script.poll_for_value(counter, exp_not_equal="0") == "1"
  407. assert (
  408. call_script.poll_for_value(results, exp_not_equal="[]") == '[["lambda",null]]'
  409. )
  410. reset_button.click()
  411. assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"
  412. # Check that triggering script from event trigger calls callback
  413. update_value_button = driver.find_element(By.ID, "update_value")
  414. update_value_button.click()
  415. assert (
  416. call_script.poll_for_content(update_value_button, exp_not_equal="Initial")
  417. == "updated"
  418. )
  419. def test_call_script_w_var(
  420. call_script: AppHarness,
  421. driver: WebDriver,
  422. ):
  423. """Test evaluating javascript expressions containing Vars.
  424. Args:
  425. call_script: harness for CallScript app.
  426. driver: WebDriver instance.
  427. """
  428. assert_token(driver)
  429. last_result = driver.find_element(By.ID, "last_result")
  430. assert last_result.get_attribute("value") == ""
  431. inline_return_button = driver.find_element(By.ID, "inline_return")
  432. call_with_var_f_string_button = driver.find_element(By.ID, "call_with_var_f_string")
  433. call_with_var_str_cast_button = driver.find_element(By.ID, "call_with_var_str_cast")
  434. call_with_var_f_string_wrapped_button = driver.find_element(
  435. By.ID, "call_with_var_f_string_wrapped"
  436. )
  437. call_with_var_str_cast_wrapped_button = driver.find_element(
  438. By.ID, "call_with_var_str_cast_wrapped"
  439. )
  440. call_with_var_f_string_inline_button = driver.find_element(
  441. By.ID, "call_with_var_f_string_inline"
  442. )
  443. call_with_var_str_cast_inline_button = driver.find_element(
  444. By.ID, "call_with_var_str_cast_inline"
  445. )
  446. call_with_var_f_string_wrapped_inline_button = driver.find_element(
  447. By.ID, "call_with_var_f_string_wrapped_inline"
  448. )
  449. call_with_var_str_cast_wrapped_inline_button = driver.find_element(
  450. By.ID, "call_with_var_str_cast_wrapped_inline"
  451. )
  452. inline_return_button.click()
  453. call_with_var_f_string_button.click()
  454. assert call_script.poll_for_value(last_result, exp_not_equal="") == "1"
  455. inline_return_button.click()
  456. call_with_var_str_cast_button.click()
  457. assert call_script.poll_for_value(last_result, exp_not_equal="1") == "2"
  458. inline_return_button.click()
  459. call_with_var_f_string_wrapped_button.click()
  460. assert call_script.poll_for_value(last_result, exp_not_equal="2") == "3"
  461. inline_return_button.click()
  462. call_with_var_str_cast_wrapped_button.click()
  463. assert call_script.poll_for_value(last_result, exp_not_equal="3") == "4"
  464. inline_return_button.click()
  465. call_with_var_f_string_inline_button.click()
  466. assert call_script.poll_for_value(last_result, exp_not_equal="4") == "9"
  467. inline_return_button.click()
  468. call_with_var_str_cast_inline_button.click()
  469. assert call_script.poll_for_value(last_result, exp_not_equal="9") == "6"
  470. inline_return_button.click()
  471. call_with_var_f_string_wrapped_inline_button.click()
  472. assert call_script.poll_for_value(last_result, exp_not_equal="6") == "13"
  473. inline_return_button.click()
  474. call_with_var_str_cast_wrapped_inline_button.click()
  475. assert call_script.poll_for_value(last_result, exp_not_equal="13") == "8"