123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530 |
- """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 = ""
- @rx.var
- def token(self) -> str:
- return self.get_token()
- 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.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()
- cookies = cookie_info_map(driver)
- assert cookies.pop("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",
- }
- assert cookies.pop("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",
- }
- assert cookies.pop("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",
- }
- assert cookies.pop("c6") == {
- "domain": "localhost",
- "httpOnly": False,
- "name": "c6",
- "path": "/",
- "sameSite": "Lax",
- "secure": False,
- "value": "c6%20value",
- }
- assert cookies.pop("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",
- }
- assert cookies.pop(
- "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",
- }
- # 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
- 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 == ""
|