Pydom add better support for select element (#1887)

* add tests for select options

* add classes to support select and options management

* fix add methond and implement clear on options

* fix optionsproxy.add

* fix select.add method

* add test adding a second option to select

* add tests around adding options in multiple flavors

* add test to add an option by passing the option it wants to be added before

* complete test around adding options

* add select to add test on remove

* add tests and support for selected item

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
This commit is contained in:
Fabio Pliger
2023-12-06 17:56:14 -06:00
committed by GitHub
parent f18ec3d20a
commit c0d45d368b
3 changed files with 259 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ class BaseElement:
self._js = js_element
self._parent = None
self.style = StyleProxy(self)
self._proxies = {}
def __eq__(self, obj):
"""Check if the element is the same as the other element by comparing
@@ -129,6 +130,18 @@ class Element(BaseElement):
def id(self, value):
self._js.id = value
@property
def options(self):
if "options" in self._proxies:
return self._proxies["options"]
if not self._js.tagName.lower() in {"select", "datalist", "optgroup"}:
raise AttributeError(
f"Element {self._js.tagName} has no options attribute."
)
self._proxies["options"] = OptionsProxy(self)
return self._proxies["options"]
@property
def value(self):
return self._js.value
@@ -145,6 +158,22 @@ class Element(BaseElement):
)
self._js.value = value
@property
def selected(self):
return self._js.selected
@selected.setter
def selected(self, value):
# in order to avoid confusion to the user, we don't allow setting the
# value of elements that don't have a value attribute
if not hasattr(self._js, "selected"):
raise AttributeError(
f"Element {self._js.tagName} has no value attribute. If you want to "
"force a value attribute, set it directly using the `_js.value = <value>` "
"javascript API attribute instead."
)
self._js.selected = value
def clone(self, new_id=None):
clone = Element(self._js.cloneNode(True))
clone.id = new_id
@@ -176,6 +205,77 @@ class Element(BaseElement):
self._js.scrollIntoView()
class OptionsProxy:
"""This class represents the options of a select element. It
allows to access to add and remove options by using the `add` and `remove` methods.
"""
def __init__(self, element: Element) -> None:
self._element = element
if self._element._js.tagName.lower() != "select":
raise AttributeError(
f"Element {self._element._js.tagName} has no options attribute."
)
def add(
self,
value: Any = None,
html: str = None,
text: str = None,
before: Element | int = None,
**kws,
) -> None:
"""Add a new option to the select element"""
# create the option element and set the attributes
option = document.createElement("option")
if value is not None:
kws["value"] = value
if html is not None:
option.innerHTML = html
if text is not None:
kws["text"] = text
for key, value in kws.items():
option.setAttribute(key, value)
if before:
if isinstance(before, Element):
before = before._js
self._element._js.add(option, before)
def remove(self, item: int) -> None:
"""Remove the option at the specified index"""
self._element._js.remove(item)
def clear(self) -> None:
"""Remove all the options"""
for i in range(len(self)):
self.remove(0)
@property
def options(self):
"""Return the list of options"""
return [Element(opt) for opt in self._element._js.options]
@property
def selected(self):
"""Return the selected option"""
return self.options[self._element._js.selectedIndex]
def __iter__(self):
yield from self.options
def __len__(self):
return len(self.options)
def __repr__(self):
return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
def __getitem__(self, key):
return self.options[key]
class StyleProxy(dict):
def __init__(self, element: Element) -> None:
self._element = element

View File

@@ -70,6 +70,24 @@
<input id="test_rr_input_password" type="password" value="Content test_rr_input_password">
</form>
<select id="test_select_element"></select>
<select id="test_select_element_w_options">
<option value="1">Option 1</option>
<option value="2" selected="selected">Option 2</option>
</select>
<select id="test_select_element_to_clear">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="4">Option 4</option>
</select>
<select id="test_select_element_to_remove">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
<option value="4">Option 4</option>
</select>
<div id="element-creation-test"></div>
<button id="a-test-button">I'm a button to be clicked</button>

View File

@@ -292,3 +292,144 @@ class TestInput:
result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError):
result.value = "some value"
def test_element_without_collection(self):
result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError):
result.value = "some value"
class TestSelect:
def test_select_options_iter(self):
select = pydom[f"#test_select_element_w_options"][0]
for i, option in enumerate(select.options, 1):
assert option.value == f"{i}"
assert option.html == f"Option {i}"
def test_select_options_len(self):
select = pydom[f"#test_select_element_w_options"][0]
assert len(select.options) == 2
def test_select_options_clear(self):
select = pydom[f"#test_select_element_to_clear"][0]
assert len(select.options) == 3
select.options.clear()
assert len(select.options) == 0
def test_select_element_add(self):
# GIVEN the existing select element with no options
select = pydom[f"#test_select_element"][0]
# EXPECT the select element to have no options
assert len(select.options) == 0
# WHEN we add an option
select.options.add(value="1", html="Option 1")
# EXPECT the select element to have 1 option matching the attributes
# we passed in
assert len(select.options) == 1
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
# WHEN we add another option (blank this time)
select.options.add()
# EXPECT the select element to have 2 options
assert len(select.options) == 2
# EXPECT the last option to have an empty value and html
assert select.options[1].value == ""
assert select.options[1].html == ""
# WHEN we add another option (this time adding it in between the other 2
# options by using an integer index)
select.options.add(value="2", html="Option 2", before=1)
# EXPECT the select element to have 3 options
assert len(select.options) == 3
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[2].value == ""
assert select.options[2].html == ""
# WHEN we add another option (this time adding it in between the other 2
# options but using the option itself)
select.options.add(
value="3", html="Option 3", before=select.options[2], selected=True
)
# EXPECT the select element to have 3 options
assert len(select.options) == 4
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].selected == select.options[0]._js.selected == False
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[2].value == "3"
assert select.options[2].html == "Option 3"
assert select.options[2].selected == select.options[2]._js.selected == True
assert select.options[3].value == ""
assert select.options[3].html == ""
# WHEN we add another option (this time adding it in between the other 2
# options but using the JS element of the option itself)
select.options.add(value="2a", html="Option 2a", before=select.options[2]._js)
# EXPECT the select element to have 3 options
assert len(select.options) == 5
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[2].value == "2a"
assert select.options[2].html == "Option 2a"
assert select.options[3].value == "3"
assert select.options[3].html == "Option 3"
assert select.options[4].value == ""
assert select.options[4].html == ""
def test_select_options_remove(self):
# GIVEN the existing select element with 3 options
select = pydom[f"#test_select_element_to_remove"][0]
# EXPECT the select element to have 3 options
assert len(select.options) == 4
# EXPECT the options to have the values originally set
assert select.options[0].value == "1"
assert select.options[1].value == "2"
assert select.options[2].value == "3"
assert select.options[3].value == "4"
# WHEN we remove the second option (index starts at 0)
select.options.remove(1)
# EXPECT the select element to have 2 options
assert len(select.options) == 3
# EXPECT the options to have the values originally set but the second
assert select.options[0].value == "1"
assert select.options[1].value == "3"
assert select.options[2].value == "4"
def test_select_get_selected_option(self):
# GIVEN the existing select element with one selected option
select = pydom[f"#test_select_element_w_options"][0]
# WHEN we get the selected option
selected_option = select.options.selected
# EXPECT the selected option to be correct
assert selected_option.value == "2"
assert selected_option.html == "Option 2"
assert selected_option.selected == selected_option._js.selected == True