test_call_script.py 19 KB

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