|
@@ -0,0 +1,341 @@
|
|
|
|
+"""Integration tests for client side storage."""
|
|
|
|
+from __future__ import annotations
|
|
|
|
+
|
|
|
|
+from typing import Generator
|
|
|
|
+
|
|
|
|
+import pytest
|
|
|
|
+from selenium.webdriver.common.by import By
|
|
|
|
+from selenium.webdriver.remote.webdriver import WebDriver
|
|
|
|
+
|
|
|
|
+from reflex.testing import AppHarness
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def CallScript():
|
|
|
|
+ """A test app for browser javascript integration."""
|
|
|
|
+ import reflex as rx
|
|
|
|
+
|
|
|
|
+ inline_scripts = """
|
|
|
|
+ let inline_counter = 0
|
|
|
|
+ function inline1() {
|
|
|
|
+ inline_counter += 1
|
|
|
|
+ return "inline1"
|
|
|
|
+ }
|
|
|
|
+ function inline2() {
|
|
|
|
+ inline_counter += 1
|
|
|
|
+ console.log("inline2")
|
|
|
|
+ }
|
|
|
|
+ function inline3() {
|
|
|
|
+ inline_counter += 1
|
|
|
|
+ return {inline3: 42, a: [1, 2, 3], s: 'js', o: {a: 1, b: 2}}
|
|
|
|
+ }
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ external_scripts = inline_scripts.replace("inline", "external")
|
|
|
|
+
|
|
|
|
+ class CallScriptState(rx.State):
|
|
|
|
+ results: list[str | dict | list | None] = []
|
|
|
|
+ inline_counter: int = 0
|
|
|
|
+ external_counter: int = 0
|
|
|
|
+
|
|
|
|
+ def call_script_callback(self, result):
|
|
|
|
+ self.results.append(result)
|
|
|
|
+
|
|
|
|
+ def call_script_callback_other_arg(self, result, other_arg):
|
|
|
|
+ self.results.append([other_arg, result])
|
|
|
|
+
|
|
|
|
+ def call_scripts_inline_yield(self):
|
|
|
|
+ yield rx.call_script("inline1()")
|
|
|
|
+ yield rx.call_script("inline2()")
|
|
|
|
+ yield rx.call_script("inline3()")
|
|
|
|
+
|
|
|
|
+ def call_script_inline_return(self):
|
|
|
|
+ return rx.call_script("inline2()")
|
|
|
|
+
|
|
|
|
+ def call_scripts_inline_yield_callback(self):
|
|
|
|
+ yield rx.call_script(
|
|
|
|
+ "inline1()", callback=CallScriptState.call_script_callback
|
|
|
|
+ )
|
|
|
|
+ yield rx.call_script(
|
|
|
|
+ "inline2()", callback=CallScriptState.call_script_callback
|
|
|
|
+ )
|
|
|
|
+ yield rx.call_script(
|
|
|
|
+ "inline3()", callback=CallScriptState.call_script_callback
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ def call_script_inline_return_callback(self):
|
|
|
|
+ return rx.call_script(
|
|
|
|
+ "inline3()", callback=CallScriptState.call_script_callback
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ def call_script_inline_return_lambda(self):
|
|
|
|
+ return rx.call_script(
|
|
|
|
+ "inline2()",
|
|
|
|
+ callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
|
|
|
|
+ result, "lambda"
|
|
|
|
+ ),
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ def get_inline_counter(self):
|
|
|
|
+ return rx.call_script(
|
|
|
|
+ "inline_counter",
|
|
|
|
+ callback=CallScriptState.set_inline_counter, # type: ignore
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ def call_scripts_external_yield(self):
|
|
|
|
+ yield rx.call_script("external1()")
|
|
|
|
+ yield rx.call_script("external2()")
|
|
|
|
+ yield rx.call_script("external3()")
|
|
|
|
+
|
|
|
|
+ def call_script_external_return(self):
|
|
|
|
+ return rx.call_script("external2()")
|
|
|
|
+
|
|
|
|
+ def call_scripts_external_yield_callback(self):
|
|
|
|
+ yield rx.call_script(
|
|
|
|
+ "external1()", callback=CallScriptState.call_script_callback
|
|
|
|
+ )
|
|
|
|
+ yield rx.call_script(
|
|
|
|
+ "external2()", callback=CallScriptState.call_script_callback
|
|
|
|
+ )
|
|
|
|
+ yield rx.call_script(
|
|
|
|
+ "external3()", callback=CallScriptState.call_script_callback
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ def call_script_external_return_callback(self):
|
|
|
|
+ return rx.call_script(
|
|
|
|
+ "external3()", callback=CallScriptState.call_script_callback
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ def call_script_external_return_lambda(self):
|
|
|
|
+ return rx.call_script(
|
|
|
|
+ "external2()",
|
|
|
|
+ callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
|
|
|
|
+ result, "lambda"
|
|
|
|
+ ),
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ def get_external_counter(self):
|
|
|
|
+ return rx.call_script(
|
|
|
|
+ "external_counter",
|
|
|
|
+ callback=CallScriptState.set_external_counter, # type: ignore
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ def reset_(self):
|
|
|
|
+ yield rx.call_script("inline_counter = 0; external_counter = 0")
|
|
|
|
+ self.reset()
|
|
|
|
+
|
|
|
|
+ app = rx.App(state=CallScriptState)
|
|
|
|
+ with open("assets/external.js", "w") as f:
|
|
|
|
+ f.write(external_scripts)
|
|
|
|
+
|
|
|
|
+ @app.add_page
|
|
|
|
+ def index():
|
|
|
|
+ return rx.vstack(
|
|
|
|
+ rx.input(
|
|
|
|
+ value=CallScriptState.router.session.client_token,
|
|
|
|
+ is_read_only=True,
|
|
|
|
+ id="token",
|
|
|
|
+ ),
|
|
|
|
+ rx.input(
|
|
|
|
+ value=CallScriptState.inline_counter.to(str), # type: ignore
|
|
|
|
+ id="inline_counter",
|
|
|
|
+ is_read_only=True,
|
|
|
|
+ ),
|
|
|
|
+ rx.input(
|
|
|
|
+ value=CallScriptState.external_counter.to(str), # type: ignore
|
|
|
|
+ id="external_counter",
|
|
|
|
+ is_read_only=True,
|
|
|
|
+ ),
|
|
|
|
+ rx.text_area(
|
|
|
|
+ value=CallScriptState.results.to_string(), # type: ignore
|
|
|
|
+ id="results",
|
|
|
|
+ is_read_only=True,
|
|
|
|
+ ),
|
|
|
|
+ rx.script(inline_scripts),
|
|
|
|
+ rx.script(src="/external.js"),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_scripts_inline_yield",
|
|
|
|
+ on_click=CallScriptState.call_scripts_inline_yield,
|
|
|
|
+ id="inline_yield",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_script_inline_return",
|
|
|
|
+ on_click=CallScriptState.call_script_inline_return,
|
|
|
|
+ id="inline_return",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_scripts_inline_yield_callback",
|
|
|
|
+ on_click=CallScriptState.call_scripts_inline_yield_callback,
|
|
|
|
+ id="inline_yield_callback",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_script_inline_return_callback",
|
|
|
|
+ on_click=CallScriptState.call_script_inline_return_callback,
|
|
|
|
+ id="inline_return_callback",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_script_inline_return_lambda",
|
|
|
|
+ on_click=CallScriptState.call_script_inline_return_lambda,
|
|
|
|
+ id="inline_return_lambda",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_scripts_external_yield",
|
|
|
|
+ on_click=CallScriptState.call_scripts_external_yield,
|
|
|
|
+ id="external_yield",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_script_external_return",
|
|
|
|
+ on_click=CallScriptState.call_script_external_return,
|
|
|
|
+ id="external_return",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_scripts_external_yield_callback",
|
|
|
|
+ on_click=CallScriptState.call_scripts_external_yield_callback,
|
|
|
|
+ id="external_yield_callback",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_script_external_return_callback",
|
|
|
|
+ on_click=CallScriptState.call_script_external_return_callback,
|
|
|
|
+ id="external_return_callback",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "call_script_external_return_lambda",
|
|
|
|
+ on_click=CallScriptState.call_script_external_return_lambda,
|
|
|
|
+ id="external_return_lambda",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "Update Inline Counter",
|
|
|
|
+ on_click=CallScriptState.get_inline_counter,
|
|
|
|
+ id="update_inline_counter",
|
|
|
|
+ ),
|
|
|
|
+ rx.button(
|
|
|
|
+ "Update External Counter",
|
|
|
|
+ on_click=CallScriptState.get_external_counter,
|
|
|
|
+ id="update_external_counter",
|
|
|
|
+ ),
|
|
|
|
+ rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ app.compile()
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+@pytest.fixture(scope="session")
|
|
|
|
+def call_script(tmp_path_factory) -> Generator[AppHarness, None, None]:
|
|
|
|
+ """Start CallScript app at tmp_path via AppHarness.
|
|
|
|
+
|
|
|
|
+ Args:
|
|
|
|
+ tmp_path_factory: pytest tmp_path_factory fixture
|
|
|
|
+
|
|
|
|
+ Yields:
|
|
|
|
+ running AppHarness instance
|
|
|
|
+ """
|
|
|
|
+ with AppHarness.create(
|
|
|
|
+ root=tmp_path_factory.mktemp("call_script"),
|
|
|
|
+ app_source=CallScript, # type: ignore
|
|
|
|
+ ) as harness:
|
|
|
|
+ yield harness
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+@pytest.fixture
|
|
|
|
+def driver(call_script: AppHarness) -> Generator[WebDriver, None, None]:
|
|
|
|
+ """Get an instance of the browser open to the call_script app.
|
|
|
|
+
|
|
|
|
+ Args:
|
|
|
|
+ call_script: harness for CallScript app
|
|
|
|
+
|
|
|
|
+ Yields:
|
|
|
|
+ WebDriver instance.
|
|
|
|
+ """
|
|
|
|
+ assert call_script.app_instance is not None, "app is not running"
|
|
|
|
+ driver = call_script.frontend()
|
|
|
|
+ try:
|
|
|
|
+ yield driver
|
|
|
|
+ finally:
|
|
|
|
+ driver.quit()
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def assert_token(call_script: AppHarness, driver: WebDriver) -> str:
|
|
|
|
+ """Get the token associated with backend state.
|
|
|
|
+
|
|
|
|
+ Args:
|
|
|
|
+ call_script: harness for CallScript app.
|
|
|
|
+ driver: WebDriver instance.
|
|
|
|
+
|
|
|
|
+ Returns:
|
|
|
|
+ The token visible in the driver browser.
|
|
|
|
+ """
|
|
|
|
+ assert call_script.app_instance is not None
|
|
|
|
+ token_input = driver.find_element(By.ID, "token")
|
|
|
|
+ assert token_input
|
|
|
|
+
|
|
|
|
+ # wait for the backend connection to send the token
|
|
|
|
+ token = call_script.poll_for_value(token_input)
|
|
|
|
+ assert token is not None
|
|
|
|
+
|
|
|
|
+ return token
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+@pytest.mark.parametrize("script", ["inline", "external"])
|
|
|
|
+def test_call_script(
|
|
|
|
+ call_script: AppHarness,
|
|
|
|
+ driver: WebDriver,
|
|
|
|
+ script: str,
|
|
|
|
+):
|
|
|
|
+ """Test calling javascript functions from python.
|
|
|
|
+
|
|
|
|
+ Args:
|
|
|
|
+ call_script: harness for CallScript app.
|
|
|
|
+ driver: WebDriver instance.
|
|
|
|
+ script: The type of script to test.
|
|
|
|
+ """
|
|
|
|
+ assert_token(call_script, driver)
|
|
|
|
+ reset_button = driver.find_element(By.ID, "reset")
|
|
|
|
+ update_counter_button = driver.find_element(By.ID, f"update_{script}_counter")
|
|
|
|
+ counter = driver.find_element(By.ID, f"{script}_counter")
|
|
|
|
+ results = driver.find_element(By.ID, "results")
|
|
|
|
+ yield_button = driver.find_element(By.ID, f"{script}_yield")
|
|
|
|
+ return_button = driver.find_element(By.ID, f"{script}_return")
|
|
|
|
+ yield_callback_button = driver.find_element(By.ID, f"{script}_yield_callback")
|
|
|
|
+ return_callback_button = driver.find_element(By.ID, f"{script}_return_callback")
|
|
|
|
+ return_lambda_button = driver.find_element(By.ID, f"{script}_return_lambda")
|
|
|
|
+
|
|
|
|
+ yield_button.click()
|
|
|
|
+ update_counter_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="0") == "3"
|
|
|
|
+ reset_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="3") == "0"
|
|
|
|
+ return_button.click()
|
|
|
|
+ update_counter_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="0") == "1"
|
|
|
|
+ reset_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"
|
|
|
|
+
|
|
|
|
+ yield_callback_button.click()
|
|
|
|
+ update_counter_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="0") == "3"
|
|
|
|
+ assert call_script.poll_for_value(
|
|
|
|
+ results, exp_not_equal="[]"
|
|
|
|
+ ) == '["%s1",null,{"%s3":42,"a":[1,2,3],"s":"js","o":{"a":1,"b":2}}]' % (
|
|
|
|
+ script,
|
|
|
|
+ script,
|
|
|
|
+ )
|
|
|
|
+ reset_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="3") == "0"
|
|
|
|
+
|
|
|
|
+ return_callback_button.click()
|
|
|
|
+ update_counter_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="0") == "1"
|
|
|
|
+ assert (
|
|
|
|
+ call_script.poll_for_value(results, exp_not_equal="[]")
|
|
|
|
+ == '[{"%s3":42,"a":[1,2,3],"s":"js","o":{"a":1,"b":2}}]' % script
|
|
|
|
+ )
|
|
|
|
+ reset_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"
|
|
|
|
+
|
|
|
|
+ return_lambda_button.click()
|
|
|
|
+ update_counter_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="0") == "1"
|
|
|
|
+ assert (
|
|
|
|
+ call_script.poll_for_value(results, exp_not_equal="[]") == '[["lambda",null]]'
|
|
|
|
+ )
|
|
|
|
+ reset_button.click()
|
|
|
|
+ assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"
|