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()