From 608e6cb41a6488abdb61041a8ad2351d92046414 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Mon, 4 May 2020 00:10:39 -0700 Subject: [PATCH] progress towards #143 --- idom/client/__init__.py | 15 ++--- idom/core/events.py | 17 ++--- idom/widgets/input.py | 32 +++++----- idom/widgets/utils.py | 28 ++++++++- setup.cfg | 4 +- tests/driver_utils.py | 7 +-- tests/test_core/test_events.py | 14 +++++ tests/test_widgets.py | 110 ++++++++++++++++++++++++++++++--- 8 files changed, 180 insertions(+), 47 deletions(-) diff --git a/idom/client/__init__.py b/idom/client/__init__.py index 24cf2e65e..d390f796b 100644 --- a/idom/client/__init__.py +++ b/idom/client/__init__.py @@ -14,17 +14,6 @@ WEB_MODULES = CLIENT_DIR / "web_modules" -def core_module(name: str) -> str: - path = f"../{CORE_MODULES.name}/{name}.js" - if not core_module_exists(name): - raise ValueError(f"Module '{path}' does not exist.") - return path - - -def core_module_exists(name: str) -> bool: - return _find_module_os_path(CORE_MODULES, name) is not None - - def web_module(name: str) -> str: path = f"../{WEB_MODULES.name}/{name}.js" if not web_module_exists(name): @@ -32,6 +21,10 @@ def web_module(name: str) -> str: return path +def web_module_path(name: str) -> Optional[Path]: + return _find_module_os_path(WEB_MODULES, name) + + def web_module_exists(name: str) -> bool: return _find_module_os_path(WEB_MODULES, name) is not None diff --git a/idom/core/events.py b/idom/core/events.py index b76440d64..495d13262 100644 --- a/idom/core/events.py +++ b/idom/core/events.py @@ -129,7 +129,7 @@ def __iter__(self) -> Iterator[str]: def __getitem__(self, key: str) -> "EventHandler": return self._handlers[key] - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover return repr(self._handlers) @@ -193,11 +193,6 @@ def remove(self, function: EventHandlerFunction) -> None: """ self._handlers.remove(function) - async def __call__(self, data: List[Any]) -> Any: - """Trigger all callbacks in the event handler.""" - for handler in self._handlers: - await handler(*data) - def serialize(self) -> Dict[str, Any]: """Serialize the event handler.""" return { @@ -206,5 +201,13 @@ def serialize(self) -> Dict[str, Any]: "stopPropagation": self._stop_propogation, } - def __repr__(self) -> str: + async def __call__(self, data: List[Any]) -> Any: + """Trigger all callbacks in the event handler.""" + for handler in self._handlers: + await handler(*data) + + def __contains__(self, function: Any) -> bool: + return function in self._handlers + + def __repr__(self) -> str: # pragma: no cover return f"{type(self).__name__}({self.serialize()})" diff --git a/idom/widgets/input.py b/idom/widgets/input.py index 186af6d6d..ca089db2e 100644 --- a/idom/widgets/input.py +++ b/idom/widgets/input.py @@ -33,10 +33,10 @@ class Input(Generic[_InputType], AbstractElement): """ __slots__ = ( + "_type", "_value", "_cast", "_display_value", - "_label", "_ignore_empty", "_events", "_attributes", @@ -48,26 +48,23 @@ def __init__( value: _InputType = "", # type: ignore attributes: Optional[Dict[str, Any]] = None, cast: Callable[[str], _InputType] = _pass_through, - label: Optional[str] = None, ignore_empty: bool = True, ) -> None: super().__init__() + self._type = type self._value = value self._display_value = str(value) self._cast = cast - self._label = label self._ignore_empty = ignore_empty self._events = Events() self._attributes = attributes or {} - self._attributes["type"] = type self_ref = ref(self) @self._events.on("change") async def on_change(event: Dict[str, Any]) -> None: self_deref = self_ref() if self_deref is not None: - value = self_deref._cast(event["value"]) - self_deref.update(value) + self_deref._set_str_value(event["value"]) @property def value(self) -> _InputType: @@ -86,22 +83,27 @@ def attributes(self) -> Dict[str, Any]: def update(self, value: _InputType) -> None: """Update the current value of the input.""" self._set_value(value) - super().update() async def render(self) -> VdomDict: input_element = html.input( - self.attributes, {"value": self._display_value}, event_handlers=self.events, + self.attributes, + {"type": self._type, "value": self._display_value}, + event_handlers=self.events, ) - if self._label is not None: - return html.label([self._label, input_element]) - else: - return input_element + return input_element + + def _set_str_value(self, value: str) -> None: + self._display_value = value + super().update() + print(value) + if not value and self._ignore_empty: + return + self._value = self._cast(value) def _set_value(self, value: _InputType) -> None: self._display_value = str(value) - if self._ignore_empty and not value: - return self._value = value + super().update() - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover return f"{type(self).__name__}({self.value!r})" diff --git a/idom/widgets/utils.py b/idom/widgets/utils.py index 695e7c5d4..c44a1c1f0 100644 --- a/idom/widgets/utils.py +++ b/idom/widgets/utils.py @@ -25,7 +25,7 @@ class Module: An :class:`Import` element for the newly defined module. """ - __slots__ = "_module" + __slots__ = ("_module", "_name", "_installed") def __init__( self, @@ -34,28 +34,52 @@ def __init__( source: Optional[IO] = None, replace: bool = False, ) -> None: + self._installed = False if install and source: raise ValueError("Both 'install' and 'source' were given.") elif (install or source) and not replace and client.web_module_exists(name): self._module = client.web_module(name) + self._installed = True + self._name = name elif source is not None: client.define_web_module(name, source.read()) self._module = client.web_module(name) + self._installed = True + self._name = name elif isinstance(install, str): client.install({install: name}) self._module = client.web_module(name) + self._installed = True + self._name = name elif install is True: client.install({name: name}) self._module = client.web_module(name) + self._installed = True + self._name = name else: self._module = name + @property + def name(self) -> str: + if not self._installed: + raise ValueError("Module is not installed locally") + return self._name + + @property + def url(self) -> str: + return self._module + def Import(self, name: str, *args, **kwargs) -> "Import": return Import(self._module, name, *args, **kwargs) def delete(self) -> None: + if not self._installed: + raise ValueError("Module is not installed locally") client.delete_web_module(self._module) + def __repr__(self) -> str: # pragma: no cover + return f"{type(self).__name__}({self._module!r})" + class Import: """Import a react module @@ -87,7 +111,7 @@ def __init__( def __call__(self, *args: Any, **kwargs: Any,) -> VdomDict: return self._constructor(import_source=self._import_source, *args, **kwargs) - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover items = ", ".join(f"{k}={v!r}" for k, v in self._import_source.items()) return f"{type(self).__name__}({items})" diff --git a/setup.cfg b/setup.cfg index 2648eaee4..5155d4333 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,9 +27,11 @@ exclude = idom/client/node_modules/* testpaths = tests xfail_strict = True addopts = --cov=idom +markers = + slow: marks tests as slow (deselect with '-m "not slow"') [coverage:report] -fail_under = 93 +fail_under = 95 show_missing = True skip_covered = True sort = Miss diff --git a/tests/driver_utils.py b/tests/driver_utils.py index 1f05737b4..67f8da3a1 100644 --- a/tests/driver_utils.py +++ b/tests/driver_utils.py @@ -2,7 +2,6 @@ from selenium.webdriver.remote.webelement import WebElement -def send_keys(element: WebElement, *values: Any) -> None: - for keys in values: - for char in keys: - element.send_keys(char) +def send_keys(element: WebElement, keys: Any) -> None: + for char in keys: + element.send_keys(char) diff --git a/tests/test_core/test_events.py b/tests/test_core/test_events.py index 413decd1c..c71a00bfe 100644 --- a/tests/test_core/test_events.py +++ b/tests/test_core/test_events.py @@ -15,6 +15,9 @@ async def key_press_handler(): assert isinstance(events["onClick"], EventHandler) assert isinstance(events["onKeyPress"], EventHandler) + assert "onClick" in events + assert "onKeyPress" in events + assert len(events) == 2 def test_event_handler_serialization(): @@ -51,3 +54,14 @@ async def callback_2(event): await event_handler([{}]) assert calls == [1, 2] + + +def test_remove_event_handlers(): + def my_callback(event): + ... + + events = EventHandler() + events.add(my_callback) + assert my_callback in events + events.remove(my_callback) + assert my_callback not in events diff --git a/tests/test_widgets.py b/tests/test_widgets.py index f9efdd5be..ac877f5f7 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -1,6 +1,11 @@ -import idom - +import time +from io import StringIO from queue import Queue +from threading import Event + +import pytest +import idom +from selenium.webdriver.common.keys import Keys from .driver_utils import send_keys @@ -35,6 +40,7 @@ async def on_click(event): button.click() driver.find_element_by_id("complete") + # we care what happens in the final delete when there's no value assert clicked.get() @@ -135,10 +141,10 @@ async def outer_click_is_not_triggered(): inner.click() -def test_input(driver, driver_wait, display, driver_get): +def test_input(driver, driver_wait, display): inp = idom.Input("text", "initial-value", {"id": "inp"}) - display(lambda: inp) + display(inp) client_inp = driver.find_element_by_id("inp") assert client_inp.get_attribute("value") == "initial-value" @@ -152,13 +158,103 @@ def test_input(driver, driver_wait, display, driver_get): driver_wait.until(lambda dvr: client_inp.get_attribute("value") == "new-value-2") -def test_image(driver, driver_wait, display): +def test_input_server_side_update(driver, driver_wait, display): + @idom.element + async def UpdateImmediately(self): + inp = idom.Input("text", "initial-value", {"id": "inp"}) + inp.update("new-value") + return inp + + display(UpdateImmediately) + + client_inp = driver.find_element_by_id("inp") + driver_wait.until(lambda drv: client_inp.get_attribute("value") == "new-value") + + +def test_input_cast_and_ignore_empty(driver, driver_wait, display): + # ignore empty since that's an invalid float + change_occured = Event() + + inp = idom.Input("number", 1, {"id": "inp"}, cast=float, ignore_empty=True) + + @inp.events.on("change") + async def on_change(event): + change_occured.set() + + display(inp) + + client_inp = driver.find_element_by_id("inp") + assert client_inp.get_attribute("value") == "1" + + send_keys(client_inp, Keys.BACKSPACE) + time.sleep(0.1) # waiting and deleting again seems to decrease flakiness + send_keys(client_inp, Keys.BACKSPACE) + + assert change_occured.wait(timeout=3.0) + assert client_inp.get_attribute("value") == "" + # change ignored server side + assert inp.value == 1 + + send_keys(client_inp, "2") + driver_wait.until(lambda drv: inp.value == 2) + + +def test_image_from_string(driver, driver_wait, display): src = """ """ - img = idom.Image("svg", src, {"id": "a-circle"}) + img = idom.Image("svg", src, {"id": "a-circle-1"}) display(img) - client_img = driver.find_element_by_id("a-circle") + client_img = driver.find_element_by_id("a-circle-1") + assert img.base64_source in client_img.get_attribute("src") + + img2 = idom.Image("svg", attributes={"id": "a-circle-2"}) + img2.io.write(src) + display(img2) + client_img = driver.find_element_by_id("a-circle-2") assert img.base64_source in client_img.get_attribute("src") + + +def test_image_from_bytes(driver, driver_wait, display): + src = b""" + + + + """ + img = idom.Image("svg", src, {"id": "a-circle-1"}) + display(img) + client_img = driver.find_element_by_id("a-circle-1") + assert img.base64_source in client_img.get_attribute("src") + + img2 = idom.Image("svg", attributes={"id": "a-circle-2"}) + img2.io.write(src) + display(img2) + client_img = driver.find_element_by_id("a-circle-2") + assert img.base64_source in client_img.get_attribute("src") + + +def test_module_cannot_have_source_and_install(): + with pytest.raises(ValueError, match=r"Both .* were given."): + idom.Module("something", install="something", source=StringIO()) + + +def test_module_deleteion(): + # also test install + jquery = idom.Module("jquery", install="jquery@3.5.0") + assert idom.client.web_module_exists(jquery.name) + with idom.client.web_module_path(jquery.name).open() as f: + assert "jQuery JavaScript Library v3.5.0" in f.read() + jquery.delete() + assert not idom.client.web_module_exists(jquery.name) + + +def test_module_from_url(): + url = "https://code.jquery.com/jquery-3.5.0.js" + jquery = idom.Module(url) + assert jquery.url == url + with pytest.raises(ValueError, match="Module is not installed locally"): + jquery.name + with pytest.raises(ValueError, match="Module is not installed locally"): + jquery.delete()