
* [REF-2265] ComponentState: scaffold for copying State per Component instance Define a base ComponentState which can be used to easily create copies of the given State definition (Vars and EventHandlers) that are tied to a particular instance of a Component (returned by get_component) * Define `State` field on `Component` for typing compatibility. This is an Optional field of Type[State] and is populated by ComponentState. * Add integration/test_component_state.py Create two independent counters and increment them separately * Add unit test for ComponentState
108 lines
3.0 KiB
Python
108 lines
3.0 KiB
Python
"""Test that per-component state scaffold works and operates independently."""
|
|
from typing import Generator
|
|
|
|
import pytest
|
|
from selenium.webdriver.common.by import By
|
|
|
|
from reflex.testing import AppHarness
|
|
|
|
from . import utils
|
|
|
|
|
|
def ComponentStateApp():
|
|
"""App using per component state."""
|
|
import reflex as rx
|
|
|
|
class MultiCounter(rx.ComponentState):
|
|
count: int = 0
|
|
|
|
def increment(self):
|
|
self.count += 1
|
|
|
|
@classmethod
|
|
def get_component(cls, *children, **props):
|
|
return rx.vstack(
|
|
*children,
|
|
rx.heading(cls.count, id=f"count-{props.get('id', 'default')}"),
|
|
rx.button(
|
|
"Increment",
|
|
on_click=cls.increment,
|
|
id=f"button-{props.get('id', 'default')}",
|
|
),
|
|
**props,
|
|
)
|
|
|
|
app = rx.App(state=rx.State) # noqa
|
|
|
|
@rx.page()
|
|
def index():
|
|
mc_a = MultiCounter.create(id="a")
|
|
mc_b = MultiCounter.create(id="b")
|
|
assert mc_a.State != mc_b.State
|
|
return rx.vstack(
|
|
mc_a,
|
|
mc_b,
|
|
rx.button(
|
|
"Inc A",
|
|
on_click=mc_a.State.increment, # type: ignore
|
|
id="inc-a",
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def component_state_app(tmp_path) -> Generator[AppHarness, None, None]:
|
|
"""Start ComponentStateApp app at tmp_path via AppHarness.
|
|
|
|
Args:
|
|
tmp_path: pytest tmp_path fixture
|
|
|
|
Yields:
|
|
running AppHarness instance
|
|
"""
|
|
with AppHarness.create(
|
|
root=tmp_path,
|
|
app_source=ComponentStateApp, # type: ignore
|
|
) as harness:
|
|
yield harness
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_component_state_app(component_state_app: AppHarness):
|
|
"""Increment counters independently.
|
|
|
|
Args:
|
|
component_state_app: harness for ComponentStateApp app
|
|
"""
|
|
assert component_state_app.app_instance is not None, "app is not running"
|
|
driver = component_state_app.frontend()
|
|
|
|
ss = utils.SessionStorage(driver)
|
|
token = AppHarness._poll_for(lambda: ss.get("token") is not None)
|
|
assert token is not None
|
|
|
|
count_a = driver.find_element(By.ID, "count-a")
|
|
count_b = driver.find_element(By.ID, "count-b")
|
|
button_a = driver.find_element(By.ID, "button-a")
|
|
button_b = driver.find_element(By.ID, "button-b")
|
|
button_inc_a = driver.find_element(By.ID, "inc-a")
|
|
|
|
assert count_a.text == "0"
|
|
|
|
button_a.click()
|
|
assert component_state_app.poll_for_content(count_a, exp_not_equal="0") == "1"
|
|
|
|
button_a.click()
|
|
assert component_state_app.poll_for_content(count_a, exp_not_equal="1") == "2"
|
|
|
|
button_inc_a.click()
|
|
assert component_state_app.poll_for_content(count_a, exp_not_equal="2") == "3"
|
|
|
|
assert count_b.text == "0"
|
|
|
|
button_b.click()
|
|
assert component_state_app.poll_for_content(count_b, exp_not_equal="0") == "1"
|
|
|
|
button_b.click()
|
|
assert component_state_app.poll_for_content(count_b, exp_not_equal="1") == "2"
|