
* test_client_storage: remove race conditions for cookie assignment Poll for default timeout for cookies to appear in the controlled browser. * Remove use of deprecated get_token and get_sid in core Both reflex.app and reflex.state were still using deprecated methods, which were throwing unsolvable warnings for end users. * Remove deprecated router functions from integration tests Mostly removing custom "token" var and replacing with router.session.client_token. Also replacing `get_query_params` and `get_current_page` usage as well. * fix upload tests Cannot pass substate as main app state, since it blocks us from accessing "inherited vars" * state: do NOT reset `router` to default When calling `.reset` to reset state vars, do NOT reset the router data, as that could mess up internal event processing.
539 lines
18 KiB
Python
539 lines
18 KiB
Python
"""Integration tests for client side storage."""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
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
|
|
|
|
from . import utils
|
|
|
|
|
|
def ClientSide():
|
|
"""App for testing client-side state."""
|
|
import reflex as rx
|
|
|
|
class ClientSideState(rx.State):
|
|
state_var: str = ""
|
|
input_value: str = ""
|
|
|
|
class ClientSideSubState(ClientSideState):
|
|
# cookies with default settings
|
|
c1: str = rx.Cookie()
|
|
c2: rx.Cookie = "c2 default" # type: ignore
|
|
|
|
# cookies with custom settings
|
|
c3: str = rx.Cookie(max_age=2) # expires after 2 second
|
|
c4: rx.Cookie = rx.Cookie(same_site="strict")
|
|
c5: str = rx.Cookie(path="/foo/") # only accessible on `/foo/`
|
|
c6: str = rx.Cookie(name="c6")
|
|
c7: str = rx.Cookie("c7 default")
|
|
|
|
# local storage with default settings
|
|
l1: str = rx.LocalStorage()
|
|
l2: rx.LocalStorage = "l2 default" # type: ignore
|
|
|
|
# local storage with custom settings
|
|
l3: str = rx.LocalStorage(name="l3")
|
|
l4: str = rx.LocalStorage("l4 default")
|
|
|
|
def set_var(self):
|
|
setattr(self, self.state_var, self.input_value)
|
|
self.state_var = self.input_value = ""
|
|
|
|
class ClientSideSubSubState(ClientSideSubState):
|
|
c1s: str = rx.Cookie()
|
|
l1s: str = rx.LocalStorage()
|
|
|
|
def set_var(self):
|
|
setattr(self, self.state_var, self.input_value)
|
|
self.state_var = self.input_value = ""
|
|
|
|
def index():
|
|
return rx.fragment(
|
|
rx.input(
|
|
value=ClientSideState.router.session.client_token,
|
|
is_read_only=True,
|
|
id="token",
|
|
),
|
|
rx.input(
|
|
placeholder="state var",
|
|
value=ClientSideState.state_var,
|
|
on_change=ClientSideState.set_state_var, # type: ignore
|
|
id="state_var",
|
|
),
|
|
rx.input(
|
|
placeholder="input value",
|
|
value=ClientSideState.input_value,
|
|
on_change=ClientSideState.set_input_value, # type: ignore
|
|
id="input_value",
|
|
),
|
|
rx.button(
|
|
"Set ClientSideSubState",
|
|
on_click=ClientSideSubState.set_var,
|
|
id="set_sub_state",
|
|
),
|
|
rx.button(
|
|
"Set ClientSideSubSubState",
|
|
on_click=ClientSideSubSubState.set_var,
|
|
id="set_sub_sub_state",
|
|
),
|
|
rx.box(ClientSideSubState.c1, id="c1"),
|
|
rx.box(ClientSideSubState.c2, id="c2"),
|
|
rx.box(ClientSideSubState.c3, id="c3"),
|
|
rx.box(ClientSideSubState.c4, id="c4"),
|
|
rx.box(ClientSideSubState.c5, id="c5"),
|
|
rx.box(ClientSideSubState.c6, id="c6"),
|
|
rx.box(ClientSideSubState.c7, id="c7"),
|
|
rx.box(ClientSideSubState.l1, id="l1"),
|
|
rx.box(ClientSideSubState.l2, id="l2"),
|
|
rx.box(ClientSideSubState.l3, id="l3"),
|
|
rx.box(ClientSideSubState.l4, id="l4"),
|
|
rx.box(ClientSideSubSubState.c1s, id="c1s"),
|
|
rx.box(ClientSideSubSubState.l1s, id="l1s"),
|
|
)
|
|
|
|
app = rx.App(state=ClientSideState)
|
|
app.add_page(index)
|
|
app.add_page(index, route="/foo")
|
|
app.compile()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def client_side(tmp_path_factory) -> Generator[AppHarness, None, None]:
|
|
"""Start ClientSide 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("client_side"),
|
|
app_source=ClientSide, # type: ignore
|
|
) as harness:
|
|
yield harness
|
|
|
|
|
|
@pytest.fixture
|
|
def driver(client_side: AppHarness) -> Generator[WebDriver, None, None]:
|
|
"""Get an instance of the browser open to the client_side app.
|
|
|
|
Args:
|
|
client_side: harness for ClientSide app
|
|
|
|
Yields:
|
|
WebDriver instance.
|
|
"""
|
|
assert client_side.app_instance is not None, "app is not running"
|
|
driver = client_side.frontend()
|
|
try:
|
|
yield driver
|
|
finally:
|
|
driver.quit()
|
|
|
|
|
|
@pytest.fixture()
|
|
def local_storage(driver: WebDriver) -> Generator[utils.LocalStorage, None, None]:
|
|
"""Get an instance of the local storage helper.
|
|
|
|
Args:
|
|
driver: WebDriver instance.
|
|
|
|
Yields:
|
|
Local storage helper.
|
|
"""
|
|
ls = utils.LocalStorage(driver)
|
|
yield ls
|
|
ls.clear()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def delete_all_cookies(driver: WebDriver) -> Generator[None, None, None]:
|
|
"""Delete all cookies after each test.
|
|
|
|
Args:
|
|
driver: WebDriver instance.
|
|
|
|
Yields:
|
|
None
|
|
"""
|
|
yield
|
|
driver.delete_all_cookies()
|
|
|
|
|
|
def cookie_info_map(driver: WebDriver) -> dict[str, dict[str, str]]:
|
|
"""Get a map of cookie names to cookie info.
|
|
|
|
Args:
|
|
driver: WebDriver instance.
|
|
|
|
Returns:
|
|
A map of cookie names to cookie info.
|
|
"""
|
|
return {cookie_info["name"]: cookie_info for cookie_info in driver.get_cookies()}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_client_side_state(
|
|
client_side: AppHarness, driver: WebDriver, local_storage: utils.LocalStorage
|
|
):
|
|
"""Test client side state.
|
|
|
|
Args:
|
|
client_side: harness for ClientSide app.
|
|
driver: WebDriver instance.
|
|
local_storage: Local storage helper.
|
|
"""
|
|
assert client_side.app_instance is not None
|
|
assert client_side.frontend_url is not None
|
|
token_input = driver.find_element(By.ID, "token")
|
|
assert token_input
|
|
|
|
# wait for the backend connection to send the token
|
|
token = client_side.poll_for_value(token_input)
|
|
assert token is not None
|
|
|
|
# get a reference to the cookie manipulation form
|
|
state_var_input = driver.find_element(By.ID, "state_var")
|
|
input_value_input = driver.find_element(By.ID, "input_value")
|
|
set_sub_state_button = driver.find_element(By.ID, "set_sub_state")
|
|
set_sub_sub_state_button = driver.find_element(By.ID, "set_sub_sub_state")
|
|
|
|
# get a reference to all cookie and local storage elements
|
|
c1 = driver.find_element(By.ID, "c1")
|
|
c2 = driver.find_element(By.ID, "c2")
|
|
c3 = driver.find_element(By.ID, "c3")
|
|
c4 = driver.find_element(By.ID, "c4")
|
|
c5 = driver.find_element(By.ID, "c5")
|
|
c6 = driver.find_element(By.ID, "c6")
|
|
c7 = driver.find_element(By.ID, "c7")
|
|
l1 = driver.find_element(By.ID, "l1")
|
|
l2 = driver.find_element(By.ID, "l2")
|
|
l3 = driver.find_element(By.ID, "l3")
|
|
l4 = driver.find_element(By.ID, "l4")
|
|
c1s = driver.find_element(By.ID, "c1s")
|
|
l1s = driver.find_element(By.ID, "l1s")
|
|
|
|
# assert on defaults where present
|
|
assert c1.text == ""
|
|
assert c2.text == "c2 default"
|
|
assert c3.text == ""
|
|
assert c4.text == ""
|
|
assert c5.text == ""
|
|
assert c6.text == ""
|
|
assert c7.text == "c7 default"
|
|
assert l1.text == ""
|
|
assert l2.text == "l2 default"
|
|
assert l3.text == ""
|
|
assert l4.text == "l4 default"
|
|
assert c1s.text == ""
|
|
assert l1s.text == ""
|
|
|
|
# no cookies should be set yet!
|
|
assert not driver.get_cookies()
|
|
local_storage_items = local_storage.items()
|
|
local_storage_items.pop("chakra-ui-color-mode", None)
|
|
assert not local_storage_items
|
|
|
|
# set some cookies and local storage values
|
|
state_var_input.send_keys("c1")
|
|
input_value_input.send_keys("c1 value")
|
|
set_sub_state_button.click()
|
|
state_var_input.send_keys("c2")
|
|
input_value_input.send_keys("c2 value")
|
|
set_sub_state_button.click()
|
|
state_var_input.send_keys("c4")
|
|
input_value_input.send_keys("c4 value")
|
|
set_sub_state_button.click()
|
|
state_var_input.send_keys("c5")
|
|
input_value_input.send_keys("c5 value")
|
|
set_sub_state_button.click()
|
|
state_var_input.send_keys("c6")
|
|
input_value_input.send_keys("c6 throwaway value")
|
|
set_sub_state_button.click()
|
|
state_var_input.send_keys("c6")
|
|
input_value_input.send_keys("c6 value")
|
|
set_sub_state_button.click()
|
|
state_var_input.send_keys("c7")
|
|
input_value_input.send_keys("c7 value")
|
|
set_sub_state_button.click()
|
|
|
|
state_var_input.send_keys("l1")
|
|
input_value_input.send_keys("l1 value")
|
|
set_sub_state_button.click()
|
|
state_var_input.send_keys("l2")
|
|
input_value_input.send_keys("l2 value")
|
|
set_sub_state_button.click()
|
|
state_var_input.send_keys("l3")
|
|
input_value_input.send_keys("l3 value")
|
|
set_sub_state_button.click()
|
|
state_var_input.send_keys("l4")
|
|
input_value_input.send_keys("l4 value")
|
|
set_sub_state_button.click()
|
|
|
|
state_var_input.send_keys("c1s")
|
|
input_value_input.send_keys("c1s value")
|
|
set_sub_sub_state_button.click()
|
|
state_var_input.send_keys("l1s")
|
|
input_value_input.send_keys("l1s value")
|
|
set_sub_sub_state_button.click()
|
|
|
|
exp_cookies = {
|
|
"client_side_state.client_side_sub_state.c1": {
|
|
"domain": "localhost",
|
|
"httpOnly": False,
|
|
"name": "client_side_state.client_side_sub_state.c1",
|
|
"path": "/",
|
|
"sameSite": "Lax",
|
|
"secure": False,
|
|
"value": "c1%20value",
|
|
},
|
|
"client_side_state.client_side_sub_state.c2": {
|
|
"domain": "localhost",
|
|
"httpOnly": False,
|
|
"name": "client_side_state.client_side_sub_state.c2",
|
|
"path": "/",
|
|
"sameSite": "Lax",
|
|
"secure": False,
|
|
"value": "c2%20value",
|
|
},
|
|
"client_side_state.client_side_sub_state.c4": {
|
|
"domain": "localhost",
|
|
"httpOnly": False,
|
|
"name": "client_side_state.client_side_sub_state.c4",
|
|
"path": "/",
|
|
"sameSite": "Strict",
|
|
"secure": False,
|
|
"value": "c4%20value",
|
|
},
|
|
"c6": {
|
|
"domain": "localhost",
|
|
"httpOnly": False,
|
|
"name": "c6",
|
|
"path": "/",
|
|
"sameSite": "Lax",
|
|
"secure": False,
|
|
"value": "c6%20value",
|
|
},
|
|
"client_side_state.client_side_sub_state.c7": {
|
|
"domain": "localhost",
|
|
"httpOnly": False,
|
|
"name": "client_side_state.client_side_sub_state.c7",
|
|
"path": "/",
|
|
"sameSite": "Lax",
|
|
"secure": False,
|
|
"value": "c7%20value",
|
|
},
|
|
"client_side_state.client_side_sub_state.client_side_sub_sub_state.c1s": {
|
|
"domain": "localhost",
|
|
"httpOnly": False,
|
|
"name": "client_side_state.client_side_sub_state.client_side_sub_sub_state.c1s",
|
|
"path": "/",
|
|
"sameSite": "Lax",
|
|
"secure": False,
|
|
"value": "c1s%20value",
|
|
},
|
|
}
|
|
AppHarness._poll_for(
|
|
lambda: all(cookie_key in cookie_info_map(driver) for cookie_key in exp_cookies)
|
|
)
|
|
cookies = cookie_info_map(driver)
|
|
for exp_cookie_key, exp_cookie_data in exp_cookies.items():
|
|
assert cookies.pop(exp_cookie_key) == exp_cookie_data
|
|
# assert all cookies have been popped for this page
|
|
assert not cookies
|
|
|
|
# Test cookie with expiry by itself to avoid timing flakiness
|
|
state_var_input.send_keys("c3")
|
|
input_value_input.send_keys("c3 value")
|
|
set_sub_state_button.click()
|
|
AppHarness._poll_for(
|
|
lambda: "client_side_state.client_side_sub_state.c3" in cookie_info_map(driver)
|
|
)
|
|
c3_cookie = cookie_info_map(driver)["client_side_state.client_side_sub_state.c3"]
|
|
assert c3_cookie.pop("expiry") is not None
|
|
assert c3_cookie == {
|
|
"domain": "localhost",
|
|
"httpOnly": False,
|
|
"name": "client_side_state.client_side_sub_state.c3",
|
|
"path": "/",
|
|
"sameSite": "Lax",
|
|
"secure": False,
|
|
"value": "c3%20value",
|
|
}
|
|
time.sleep(2) # wait for c3 to expire
|
|
assert "client_side_state.client_side_sub_state.c3" not in cookie_info_map(driver)
|
|
|
|
local_storage_items = local_storage.items()
|
|
local_storage_items.pop("chakra-ui-color-mode", None)
|
|
assert (
|
|
local_storage_items.pop("client_side_state.client_side_sub_state.l1")
|
|
== "l1 value"
|
|
)
|
|
assert (
|
|
local_storage_items.pop("client_side_state.client_side_sub_state.l2")
|
|
== "l2 value"
|
|
)
|
|
assert local_storage_items.pop("l3") == "l3 value"
|
|
assert (
|
|
local_storage_items.pop("client_side_state.client_side_sub_state.l4")
|
|
== "l4 value"
|
|
)
|
|
assert (
|
|
local_storage_items.pop(
|
|
"client_side_state.client_side_sub_state.client_side_sub_sub_state.l1s"
|
|
)
|
|
== "l1s value"
|
|
)
|
|
assert not local_storage_items
|
|
|
|
assert c1.text == "c1 value"
|
|
assert c2.text == "c2 value"
|
|
assert c3.text == "c3 value"
|
|
assert c4.text == "c4 value"
|
|
assert c5.text == "c5 value"
|
|
assert c6.text == "c6 value"
|
|
assert c7.text == "c7 value"
|
|
assert l1.text == "l1 value"
|
|
assert l2.text == "l2 value"
|
|
assert l3.text == "l3 value"
|
|
assert l4.text == "l4 value"
|
|
assert c1s.text == "c1s value"
|
|
assert l1s.text == "l1s value"
|
|
|
|
# navigate to the /foo route
|
|
with utils.poll_for_navigation(driver):
|
|
driver.get(client_side.frontend_url + "/foo")
|
|
|
|
# get new references to all cookie and local storage elements
|
|
c1 = driver.find_element(By.ID, "c1")
|
|
c2 = driver.find_element(By.ID, "c2")
|
|
c3 = driver.find_element(By.ID, "c3")
|
|
c4 = driver.find_element(By.ID, "c4")
|
|
c5 = driver.find_element(By.ID, "c5")
|
|
c6 = driver.find_element(By.ID, "c6")
|
|
c7 = driver.find_element(By.ID, "c7")
|
|
l1 = driver.find_element(By.ID, "l1")
|
|
l2 = driver.find_element(By.ID, "l2")
|
|
l3 = driver.find_element(By.ID, "l3")
|
|
l4 = driver.find_element(By.ID, "l4")
|
|
c1s = driver.find_element(By.ID, "c1s")
|
|
l1s = driver.find_element(By.ID, "l1s")
|
|
|
|
assert c1.text == "c1 value"
|
|
assert c2.text == "c2 value"
|
|
assert c3.text == "" # cookie expired so value removed from state
|
|
assert c4.text == "c4 value"
|
|
assert c5.text == "c5 value"
|
|
assert c6.text == "c6 value"
|
|
assert c7.text == "c7 value"
|
|
assert l1.text == "l1 value"
|
|
assert l2.text == "l2 value"
|
|
assert l3.text == "l3 value"
|
|
assert l4.text == "l4 value"
|
|
assert c1s.text == "c1s value"
|
|
assert l1s.text == "l1s value"
|
|
|
|
# reset the backend state to force refresh from client storage
|
|
async with client_side.modify_state(token) as state:
|
|
state.reset()
|
|
driver.refresh()
|
|
|
|
# wait for the backend connection to send the token (again)
|
|
token_input = driver.find_element(By.ID, "token")
|
|
assert token_input
|
|
token = client_side.poll_for_value(token_input)
|
|
assert token is not None
|
|
|
|
# get new references to all cookie and local storage elements (again)
|
|
c1 = driver.find_element(By.ID, "c1")
|
|
c2 = driver.find_element(By.ID, "c2")
|
|
c3 = driver.find_element(By.ID, "c3")
|
|
c4 = driver.find_element(By.ID, "c4")
|
|
c5 = driver.find_element(By.ID, "c5")
|
|
c6 = driver.find_element(By.ID, "c6")
|
|
c7 = driver.find_element(By.ID, "c7")
|
|
l1 = driver.find_element(By.ID, "l1")
|
|
l2 = driver.find_element(By.ID, "l2")
|
|
l3 = driver.find_element(By.ID, "l3")
|
|
l4 = driver.find_element(By.ID, "l4")
|
|
c1s = driver.find_element(By.ID, "c1s")
|
|
l1s = driver.find_element(By.ID, "l1s")
|
|
|
|
assert c1.text == "c1 value"
|
|
assert c2.text == "c2 value"
|
|
assert c3.text == "" # temporary cookie expired after reset state!
|
|
assert c4.text == "c4 value"
|
|
assert c5.text == "c5 value"
|
|
assert c6.text == "c6 value"
|
|
assert c7.text == "c7 value"
|
|
assert l1.text == "l1 value"
|
|
assert l2.text == "l2 value"
|
|
assert l3.text == "l3 value"
|
|
assert l4.text == "l4 value"
|
|
assert c1s.text == "c1s value"
|
|
assert l1s.text == "l1s value"
|
|
|
|
# make sure c5 cookie shows up on the `/foo` route
|
|
AppHarness._poll_for(
|
|
lambda: "client_side_state.client_side_sub_state.c5" in cookie_info_map(driver)
|
|
)
|
|
assert cookie_info_map(driver)["client_side_state.client_side_sub_state.c5"] == {
|
|
"domain": "localhost",
|
|
"httpOnly": False,
|
|
"name": "client_side_state.client_side_sub_state.c5",
|
|
"path": "/foo/",
|
|
"sameSite": "Lax",
|
|
"secure": False,
|
|
"value": "c5%20value",
|
|
}
|
|
|
|
# clear the cookie jar and local storage, ensure state reset to default
|
|
driver.delete_all_cookies()
|
|
local_storage.clear()
|
|
|
|
# refresh the page to trigger re-hydrate
|
|
driver.refresh()
|
|
|
|
# wait for the backend connection to send the token (again)
|
|
token_input = driver.find_element(By.ID, "token")
|
|
assert token_input
|
|
token = client_side.poll_for_value(token_input)
|
|
assert token is not None
|
|
|
|
# all values should be back to their defaults
|
|
c1 = driver.find_element(By.ID, "c1")
|
|
c2 = driver.find_element(By.ID, "c2")
|
|
c3 = driver.find_element(By.ID, "c3")
|
|
c4 = driver.find_element(By.ID, "c4")
|
|
c5 = driver.find_element(By.ID, "c5")
|
|
c6 = driver.find_element(By.ID, "c6")
|
|
c7 = driver.find_element(By.ID, "c7")
|
|
l1 = driver.find_element(By.ID, "l1")
|
|
l2 = driver.find_element(By.ID, "l2")
|
|
l3 = driver.find_element(By.ID, "l3")
|
|
l4 = driver.find_element(By.ID, "l4")
|
|
c1s = driver.find_element(By.ID, "c1s")
|
|
l1s = driver.find_element(By.ID, "l1s")
|
|
|
|
# assert on defaults where present
|
|
assert c1.text == ""
|
|
assert c2.text == "c2 default"
|
|
assert c3.text == ""
|
|
assert c4.text == ""
|
|
assert c5.text == ""
|
|
assert c6.text == ""
|
|
assert c7.text == "c7 default"
|
|
assert l1.text == ""
|
|
assert l2.text == "l2 default"
|
|
assert l3.text == ""
|
|
assert l4.text == "l4 default"
|
|
assert c1s.text == ""
|
|
assert l1s.text == ""
|