Skip to content

Commit

Permalink
Feat: Add Session storage to store data on client storage (reflex-dev…
Browse files Browse the repository at this point in the history
  • Loading branch information
TG199 authored Jun 17, 2024
1 parent b78fa6f commit 2b2cdf9
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ repos:
always_run: true
language: system
description: 'Update pyi files as needed'
entry: python scripts/make_pyi.py
entry: python3 scripts/make_pyi.py
98 changes: 97 additions & 1 deletion integration/test_client_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ class ClientSideSubState(ClientSideState):
l5: str = rx.LocalStorage(sync=True)
l6: str = rx.LocalStorage(sync=True, name="l6")

# Session storage
s1: str = rx.SessionStorage()
s2: rx.SessionStorage = "s2 default" # type: ignore
s3: str = rx.SessionStorage(name="s3")

def set_l6(self, my_param: str):
self.l6 = my_param

Expand All @@ -56,6 +61,7 @@ def set_var(self):
class ClientSideSubSubState(ClientSideSubState):
c1s: str = rx.Cookie()
l1s: str = rx.LocalStorage()
s1s: str = rx.SessionStorage()

def set_var(self):
setattr(self, self.state_var, self.input_value)
Expand Down Expand Up @@ -103,8 +109,12 @@ def index():
rx.box(ClientSideSubState.l4, id="l4"),
rx.box(ClientSideSubState.l5, id="l5"),
rx.box(ClientSideSubState.l6, id="l6"),
rx.box(ClientSideSubState.s1, id="s1"),
rx.box(ClientSideSubState.s2, id="s2"),
rx.box(ClientSideSubState.s3, id="s3"),
rx.box(ClientSideSubSubState.c1s, id="c1s"),
rx.box(ClientSideSubSubState.l1s, id="l1s"),
rx.box(ClientSideSubSubState.s1s, id="s1s"),
)

app = rx.App(state=rx.State)
Expand Down Expand Up @@ -162,6 +172,21 @@ def local_storage(driver: WebDriver) -> Generator[utils.LocalStorage, None, None
ls.clear()


@pytest.fixture()
def session_storage(driver: WebDriver) -> Generator[utils.SessionStorage, None, None]:
"""Get an instance of the session storage helper.
Args:
driver: WebDriver instance.
Yields:
Session storage helper.
"""
ss = utils.SessionStorage(driver)
yield ss
ss.clear()


@pytest.fixture(autouse=True)
def delete_all_cookies(driver: WebDriver) -> Generator[None, None, None]:
"""Delete all cookies after each test.
Expand Down Expand Up @@ -190,14 +215,18 @@ def cookie_info_map(driver: WebDriver) -> dict[str, dict[str, str]]:

@pytest.mark.asyncio
async def test_client_side_state(
client_side: AppHarness, driver: WebDriver, local_storage: utils.LocalStorage
client_side: AppHarness,
driver: WebDriver,
local_storage: utils.LocalStorage,
session_storage: utils.SessionStorage,
):
"""Test client side state.
Args:
client_side: harness for ClientSide app.
driver: WebDriver instance.
local_storage: Local storage helper.
session_storage: Session storage helper.
"""
assert client_side.app_instance is not None
assert client_side.frontend_url is not None
Expand Down Expand Up @@ -251,8 +280,12 @@ def set_sub_sub(var: str, value: str):
l2 = driver.find_element(By.ID, "l2")
l3 = driver.find_element(By.ID, "l3")
l4 = driver.find_element(By.ID, "l4")
s1 = driver.find_element(By.ID, "s1")
s2 = driver.find_element(By.ID, "s2")
s3 = driver.find_element(By.ID, "s3")
c1s = driver.find_element(By.ID, "c1s")
l1s = driver.find_element(By.ID, "l1s")
s1s = driver.find_element(By.ID, "s1s")

# assert on defaults where present
assert c1.text == ""
Expand All @@ -266,8 +299,12 @@ def set_sub_sub(var: str, value: str):
assert l2.text == "l2 default"
assert l3.text == ""
assert l4.text == "l4 default"
assert s1.text == ""
assert s2.text == "s2 default"
assert s3.text == ""
assert c1s.text == ""
assert l1s.text == ""
assert s1s.text == ""

# no cookies should be set yet!
assert not driver.get_cookies()
Expand All @@ -287,8 +324,12 @@ def set_sub_sub(var: str, value: str):
set_sub("l2", "l2 value")
set_sub("l3", "l3 value")
set_sub("l4", "l4 value")
set_sub("s1", "s1 value")
set_sub("s2", "s2 value")
set_sub("s3", "s3 value")
set_sub_sub("c1s", "c1s value")
set_sub_sub("l1s", "l1s value")
set_sub_sub("s1s", "s1s value")

exp_cookies = {
"state.client_side_state.client_side_sub_state.c1": {
Expand Down Expand Up @@ -405,6 +446,25 @@ def set_sub_sub(var: str, value: str):
)
assert not local_storage_items

session_storage_items = session_storage.items()
session_storage_items.pop("token", None)
assert (
session_storage_items.pop("state.client_side_state.client_side_sub_state.s1")
== "s1 value"
)
assert (
session_storage_items.pop("state.client_side_state.client_side_sub_state.s2")
== "s2 value"
)
assert session_storage_items.pop("s3") == "s3 value"
assert (
session_storage_items.pop(
"state.client_side_state.client_side_sub_state.client_side_sub_sub_state.s1s"
)
== "s1s value"
)
assert not session_storage_items

assert c1.text == "c1 value"
assert c2.text == "c2 value"
assert c3.text == "c3 value"
Expand All @@ -416,8 +476,12 @@ def set_sub_sub(var: str, value: str):
assert l2.text == "l2 value"
assert l3.text == "l3 value"
assert l4.text == "l4 value"
assert s1.text == "s1 value"
assert s2.text == "s2 value"
assert s3.text == "s3 value"
assert c1s.text == "c1s value"
assert l1s.text == "l1s value"
assert s1s.text == "s1s value"

# navigate to the /foo route
with utils.poll_for_navigation(driver):
Expand All @@ -435,8 +499,12 @@ def set_sub_sub(var: str, value: str):
l2 = driver.find_element(By.ID, "l2")
l3 = driver.find_element(By.ID, "l3")
l4 = driver.find_element(By.ID, "l4")
s1 = driver.find_element(By.ID, "s1")
s2 = driver.find_element(By.ID, "s2")
s3 = driver.find_element(By.ID, "s3")
c1s = driver.find_element(By.ID, "c1s")
l1s = driver.find_element(By.ID, "l1s")
s1s = driver.find_element(By.ID, "s1s")

assert c1.text == "c1 value"
assert c2.text == "c2 value"
Expand All @@ -449,8 +517,12 @@ def set_sub_sub(var: str, value: str):
assert l2.text == "l2 value"
assert l3.text == "l3 value"
assert l4.text == "l4 value"
assert s1.text == "s1 value"
assert s2.text == "s2 value"
assert s3.text == "s3 value"
assert c1s.text == "c1s value"
assert l1s.text == "l1s value"
assert s1s.text == "s1s value"

# reset the backend state to force refresh from client storage
async with client_side.modify_state(f"{token}_state.client_side_state") as state:
Expand All @@ -475,8 +547,12 @@ def set_sub_sub(var: str, value: str):
l2 = driver.find_element(By.ID, "l2")
l3 = driver.find_element(By.ID, "l3")
l4 = driver.find_element(By.ID, "l4")
s1 = driver.find_element(By.ID, "s1")
s2 = driver.find_element(By.ID, "s2")
s3 = driver.find_element(By.ID, "s3")
c1s = driver.find_element(By.ID, "c1s")
l1s = driver.find_element(By.ID, "l1s")
s1s = driver.find_element(By.ID, "s1s")

assert c1.text == "c1 value"
assert c2.text == "c2 value"
Expand All @@ -489,8 +565,12 @@ def set_sub_sub(var: str, value: str):
assert l2.text == "l2 value"
assert l3.text == "l3 value"
assert l4.text == "l4 value"
assert s1.text == "s1 value"
assert s2.text == "s2 value"
assert s3.text == "s3 value"
assert c1s.text == "c1s value"
assert l1s.text == "l1s value"
assert s1s.text == "s1s value"

# make sure c5 cookie shows up on the `/foo` route
AppHarness._poll_for(
Expand Down Expand Up @@ -525,6 +605,15 @@ def set_sub_sub(var: str, value: str):
assert AppHarness._poll_for(lambda: l6.text == "l6 value")
assert l5.text == "l5 value"

# Set session storage values in the new tab
set_sub("s1", "other tab s1")
s1 = driver.find_element(By.ID, "s1")
s2 = driver.find_element(By.ID, "s2")
s3 = driver.find_element(By.ID, "s3")
assert AppHarness._poll_for(lambda: s1.text == "other tab s1")
assert s2.text == "s2 default"
assert s3.text == ""

# Switch back to main window.
driver.switch_to.window(main_tab)

Expand All @@ -534,6 +623,13 @@ def set_sub_sub(var: str, value: str):
assert AppHarness._poll_for(lambda: l6.text == "l6 value")
assert l5.text == "l5 value"

s1 = driver.find_element(By.ID, "s1")
s2 = driver.find_element(By.ID, "s2")
s3 = driver.find_element(By.ID, "s3")
assert AppHarness._poll_for(lambda: s1.text == "s1 value")
assert s2.text == "s2 value"
assert s3.text == "s3 value"

# clear the cookie jar and local storage, ensure state reset to default
driver.delete_all_cookies()
local_storage.clear()
Expand Down
33 changes: 32 additions & 1 deletion reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,18 @@ export const applyEvent = async (event, socket) => {
return false;
}

if (event.name == "_clear_session_storage") {
sessionStorage.clear();
queueEvents(initialEvents(), socket);
return false;
}

if (event.name == "_remove_session_storage") {
sessionStorage.removeItem(event.payload.key);
queueEvents(initialEvents(), socket);
return false;
}

if (event.name == "_set_clipboard") {
const content = event.payload.content;
navigator.clipboard.writeText(content);
Expand Down Expand Up @@ -538,7 +550,18 @@ export const hydrateClientStorage = (client_storage) => {
}
}
}
if (client_storage.cookies || client_storage.local_storage) {
if (client_storage.session_storage && typeof window != "undefined") {
for (const state_key in client_storage.session_storage) {
const session_options = client_storage.session_storage[state_key];
const session_storage_value = sessionStorage.getItem(
session_options.name || state_key
);
if (session_storage_value != null) {
client_storage_values[state_key] = session_storage_value;
}
}
}
if (client_storage.cookies || client_storage.local_storage || client_storage.session_storage) {
return client_storage_values;
}
return {};
Expand Down Expand Up @@ -578,7 +601,15 @@ const applyClientStorageDelta = (client_storage, delta) => {
) {
const options = client_storage.local_storage[state_key];
localStorage.setItem(options.name || state_key, delta[substate][key]);
} else if(
client_storage.session_storage &&
state_key in client_storage.session_storage &&
typeof window !== "undefined"
) {
const session_options = client_storage.session_storage[state_key];
sessionStorage.setItem(session_options.name || state_key, delta[substate][key]);
}

}
}
};
Expand Down
3 changes: 3 additions & 0 deletions reflex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,14 @@
"background",
"call_script",
"clear_local_storage",
"clear_session_storage",
"console_log",
"download",
"prevent_default",
"redirect",
"remove_cookie",
"remove_local_storage",
"remove_session_storage",
"set_clipboard",
"set_focus",
"scroll_to",
Expand All @@ -307,6 +309,7 @@
"var",
"Cookie",
"LocalStorage",
"SessionStorage",
"ComponentState",
"State",
],
Expand Down
3 changes: 3 additions & 0 deletions reflex/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,14 @@ from .event import EventHandler as EventHandler
from .event import background as background
from .event import call_script as call_script
from .event import clear_local_storage as clear_local_storage
from .event import clear_session_storage as clear_session_storage
from .event import console_log as console_log
from .event import download as download
from .event import prevent_default as prevent_default
from .event import redirect as redirect
from .event import remove_cookie as remove_cookie
from .event import remove_local_storage as remove_local_storage
from .event import remove_session_storage as remove_session_storage
from .event import set_clipboard as set_clipboard
from .event import set_focus as set_focus
from .event import scroll_to as scroll_to
Expand All @@ -177,6 +179,7 @@ from .model import Model as Model
from .state import var as var
from .state import Cookie as Cookie
from .state import LocalStorage as LocalStorage
from .state import SessionStorage as SessionStorage
from .state import ComponentState as ComponentState
from .state import State as State
from .style import Style as Style
Expand Down
Loading

0 comments on commit 2b2cdf9

Please sign in to comment.