Cleanup pyscript web elements (#2094)

* change pydom example to use new pyscript.web namespace

* change tests to use new pyscript.web namespace

* create new pyscript.web package and move pydom to pyscript.web.dom

* add __init__ to pyscript.web and expose the dom instance instead of the pyscript.web.dom module

* move elements from pyweb.ui to pyscript.web and temp fix pydom import

* moved of elements file completed

* moved media from pyweb to pyscript.web

* RIP pyweb

* move JSProperty from pyscript.web.dom to pyscript.web.elements

* move element classes from pyscript.web.dom to pyscript.web.elements

* first round of fixes while running tests

* fix test typo

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

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

* restore right type type returned for Element.parent. ALL TESTS PASS LOCALLY NOW

* lint

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

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

* clean up dom.py from dead commented code and osbolete comments

* bugfix: dom shouldn't return None when it can't find any element for a specific selector so it now returns an empty collection

* additional cleanup in tests

* lint

* initial cleaning up of unused modules

* change element.append to not consider types anymore and add tests for appending elements.Element or a JsProxy object

* add Element.append tests for append JS elements directly and appending nodeList as well

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

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

* Tag and create the correct subclass of Element.

* Move: Element.snap -> video.snap

* Move: Element.download and draw to canvas.download and draw.

* Minor cleanups.

* Commenting.

* Allow css classes to be passed to Element constructor.

* Commenting.

* Typo fix.

* Make html, id and text JSProperties.

* Commenting.

* Remove unnecessary selected attribute on BaseElement.

* Extract: BaseElement.from_js -> element_from_js

* Pass *args and **kwargs to element_from_js and remove BaseElement.create

* Move value attribute to specific Element subclasses.

* fix: wrapping of existing js elements.

* Add body and head elements so parent and children work everywhere.

* Revert order of HasOptions mixin for the select element.

* Comment out tests that are no longer relevant (see comment).

* Use correct super args in mixin.

* Have to use element_from_js when returning options from OptionsProxy.

* rename: StyleProxy -> Style, OptionsProxy -> Options and added Classes.

* Remove cached_property.

* Remove list-y methods from Classes collection.

* Allow explicit children or *args for containers.

* controversial: fix tests to use find rather than dom

* Add html element so (say) body.parent does what is expected.

* Collapse Element class hierarchy.

* rename: js_element -> dom_element

* rename: element_from_js -> element_from_dom

* replace: JS with DOM

* rename: _js -> _dom_element

* fix dom tests.

* Complete element list with void elements derived from Element.

* Added attributes to the newly added Element subclasses.

* remove dom module, replace with instance.

Also, remove media :)

* fix: typo in test for 'b' element.

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

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

* Remove dom and media modules.

* fix up ts definitions.

* Added missing import (used in content property).

* Added TODO :)

* wip: Add ClassesCollection

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

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

* Attempt to ask black to leave class list alone.

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

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

* Add classes attribute to ElementCollection

* wip: work on classes collection

* Extract code to get set of all class names in ClassesCollection.

* Update elements.py

* Polishing.

* Make children return an ElementCollection

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

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

* wip: Add the ability to set multiple properties.

* Add __getitem__ back to the dom object.

* Put validation when setting DOM properties back in.

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

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

* All tests green.

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

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

* Remove unnecessary comment.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Martin <martin.chilvers@gmail.com>
This commit is contained in:
Fabio Pliger
2024-07-03 17:21:23 -04:00
committed by GitHub
parent 67d47511d5
commit f8f7ba89c1
14 changed files with 1270 additions and 2614 deletions

View File

@@ -1,23 +1,13 @@
from unittest import mock
import pytest
from pyscript import document, when
from pyweb import pydom
from pyscript.web import dom
from pyscript.web import elements as el
from pyscript.web.elements import ElementCollection
class TestDocument:
def test__element(self):
assert pydom._js == document
def test_no_parent(self):
assert pydom.parent is None
def test_create_element(self):
new_el = pydom.create("div")
assert isinstance(new_el, pydom.BaseElement)
assert new_el._js.tagName == "DIV"
# EXPECT the new element to be associated with the document
assert new_el.parent == None
assert dom.body._dom_element == document.body
assert dom.head._dom_element == document.head
def test_getitem_by_id():
@@ -26,14 +16,14 @@ def test_getitem_by_id():
txt = "You found test_id_selector"
selector = f"#{id_}"
# EXPECT the element to be found by id
result = pydom[selector]
result = dom.find(selector)
div = result[0]
# EXPECT the element text value to match what we expect and what
# the JS document.querySelector API would return
assert document.querySelector(selector).innerHTML == div.html == txt
assert document.querySelector(selector).innerHTML == div.innerHTML == txt
# EXPECT the results to be of the right types
assert isinstance(div, pydom.BaseElement)
assert isinstance(result, pydom.ElementCollection)
assert isinstance(div, el.Element)
assert isinstance(result, ElementCollection)
def test_getitem_by_class():
@@ -43,7 +33,7 @@ def test_getitem_by_class():
"test_selector_w_children_child_1",
]
expected_class = "a-test-class"
result = pydom[f".{expected_class}"]
result = dom.find(f".{expected_class}")
div = result[0]
# EXPECT to find exact number of elements with the class in the page (== 3)
@@ -54,40 +44,40 @@ def test_getitem_by_class():
def test_read_n_write_collection_elements():
elements = pydom[".multi-elems"]
elements = dom.find(".multi-elems")
for element in elements:
assert element.html == f"Content {element.id.replace('#', '')}"
assert element.innerHTML == f"Content {element.id.replace('#', '')}"
new_content = "New Content"
elements.html = new_content
elements.innerHTML = new_content
for element in elements:
assert element.html == new_content
assert element.innerHTML == new_content
class TestElement:
def test_query(self):
# GIVEN an existing element on the page, with at least 1 child element
id_ = "test_selector_w_children"
parent_div = pydom[f"#{id_}"][0]
parent_div = dom.find(f"#{id_}")[0]
# EXPECT it to be able to query for the first child element
div = parent_div.find("div")[0]
# EXPECT the new element to be associated with the parent
assert div.parent == parent_div
# EXPECT the new element to be a BaseElement
assert isinstance(div, pydom.BaseElement)
# EXPECT the new element to be a el.Element
assert isinstance(div, el.Element)
# EXPECT the div attributes to be == to how they are configured in the page
assert div.html == "Child 1"
assert div.innerHTML == "Child 1"
assert div.id == "test_selector_w_children_child_1"
def test_equality(self):
# GIVEN 2 different Elements pointing to the same underlying element
id_ = "test_id_selector"
selector = f"#{id_}"
div = pydom[selector][0]
div2 = pydom[selector][0]
div = dom.find(selector)[0]
div2 = dom.find(selector)[0]
# EXPECT them to be equal
assert div == div2
@@ -95,34 +85,34 @@ class TestElement:
assert div is not div2
# EXPECT their value to always be equal
assert div.html == div2.html
div.html = "some value"
assert div.innerHTML == div2.innerHTML
div.innerHTML = "some value"
assert div.html == div2.html == "some value"
assert div.innerHTML == div2.innerHTML == "some value"
def test_append_element(self):
id_ = "element-append-tests"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
len_children_before = len(div.children)
new_el = div.create("p")
new_el = el.p("new element")
div.append(new_el)
assert len(div.children) == len_children_before + 1
assert div.children[-1] == new_el
def test_append_js_element(self):
def test_append_dom_element_element(self):
id_ = "element-append-tests"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
len_children_before = len(div.children)
new_el = div.create("p")
div.append(new_el._js)
new_el = el.p("new element")
div.append(new_el._dom_element)
assert len(div.children) == len_children_before + 1
assert div.children[-1] == new_el
def test_append_collection(self):
id_ = "element-append-tests"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
len_children_before = len(div.children)
collection = pydom[".collection"]
collection = dom.find(".collection")
div.append(collection)
assert len(div.children) == len_children_before + len(collection)
@@ -132,24 +122,24 @@ class TestElement:
def test_read_classes(self):
id_ = "test_class_selector"
expected_class = "a-test-class"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
assert div.classes == [expected_class]
def test_add_remove_class(self):
id_ = "div-no-classes"
classname = "tester-class"
div = pydom[f"#{id_}"][0]
div = dom.find(f"#{id_}")[0]
assert not div.classes
div.add_class(classname)
same_div = pydom[f"#{id_}"][0]
div.classes.add(classname)
same_div = dom.find(f"#{id_}")[0]
assert div.classes == [classname] == same_div.classes
div.remove_class(classname)
div.classes.remove(classname)
assert div.classes == [] == same_div.classes
def test_when_decorator(self):
called = False
just_a_button = pydom["#a-test-button"][0]
just_a_button = dom.find("#a-test-button")[0]
@when("click", just_a_button)
def on_click(event):
@@ -157,45 +147,49 @@ class TestElement:
called = True
# Now let's simulate a click on the button (using the low level JS API)
# so we don't risk pydom getting in the way
# so we don't risk dom getting in the way
assert not called
just_a_button._js.click()
just_a_button._dom_element.click()
assert called
def test_html_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = pydom["#element_attribute_tests"][0]
div = dom.find("#element_attribute_tests")[0]
# WHEN we set the html attribute
div.html = "<b>New Content</b>"
div.innerHTML = "<b>New Content</b>"
# EXPECT the element html and underlying JS Element innerHTML property
# to match what we expect and what
assert div.html == div._js.innerHTML == "<b>New Content</b>"
assert div.text == div._js.textContent == "New Content"
assert div.innerHTML == div._dom_element.innerHTML == "<b>New Content</b>"
assert div.text == div._dom_element.textContent == "New Content"
def test_text_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = pydom["#element_attribute_tests"][0]
div = dom.find("#element_attribute_tests")[0]
# WHEN we set the html attribute
div.text = "<b>New Content</b>"
# EXPECT the element html and underlying JS Element innerHTML property
# to match what we expect and what
assert div.html == div._js.innerHTML == "&lt;b&gt;New Content&lt;/b&gt;"
assert div.text == div._js.textContent == "<b>New Content</b>"
assert (
div.innerHTML
== div._dom_element.innerHTML
== "&lt;b&gt;New Content&lt;/b&gt;"
)
assert div.text == div._dom_element.textContent == "<b>New Content</b>"
class TestCollection:
def test_iter_eq_children(self):
elements = pydom[".multi-elems"]
elements = dom.find(".multi-elems")
assert [el for el in elements] == [el for el in elements.children]
assert len(elements) == 3
def test_slices(self):
elements = pydom[".multi-elems"]
elements = dom.find(".multi-elems")
assert elements[0]
_slice = elements[:2]
assert len(_slice) == 2
@@ -205,26 +199,26 @@ class TestCollection:
def test_style_rule(self):
selector = ".multi-elems"
elements = pydom[selector]
elements = dom.find(selector)
for el in elements:
assert el.style["background-color"] != "red"
elements.style["background-color"] = "red"
for i, el in enumerate(pydom[selector]):
for i, el in enumerate(dom.find(selector)):
assert elements[i].style["background-color"] == "red"
assert el.style["background-color"] == "red"
elements.style.remove("background-color")
for i, el in enumerate(pydom[selector]):
for i, el in enumerate(dom.find(selector)):
assert el.style["background-color"] != "red"
assert elements[i].style["background-color"] != "red"
def test_when_decorator(self):
called = False
buttons_collection = pydom["button"]
buttons_collection = dom.find("button")
@when("click", buttons_collection)
def on_click(event):
@@ -232,42 +226,44 @@ class TestCollection:
called = True
# Now let's simulate a click on the button (using the low level JS API)
# so we don't risk pydom getting in the way
# so we don't risk dom getting in the way
assert not called
for button in buttons_collection:
button._js.click()
button._dom_element.click()
assert called
called = False
class TestCreation:
def test_create_document_element(self):
new_el = pydom.create("div")
# TODO: This test should probably be removed since it's testing the elements module
new_el = el.div("new element")
new_el.id = "new_el_id"
assert isinstance(new_el, pydom.BaseElement)
assert new_el._js.tagName == "DIV"
assert isinstance(new_el, el.Element)
assert new_el._dom_element.tagName == "DIV"
# EXPECT the new element to be associated with the document
assert new_el.parent == None
pydom.body.append(new_el)
dom.body.append(new_el)
assert pydom["#new_el_id"][0].parent == pydom.body
assert dom.find("#new_el_id")[0].parent == dom.body
def test_create_element_child(self):
selector = "#element-creation-test"
parent_div = pydom[selector][0]
parent_div = dom.find(selector)[0]
# Creating an element from another element automatically creates that element
# as a child of the original element
new_el = parent_div.create(
"p", classes=["code-description"], html="Ciao PyScripters!"
new_el = el.p(
"a div", classes=["code-description"], innerHTML="Ciao PyScripters!"
)
parent_div.append(new_el)
assert isinstance(new_el, el.Element)
assert new_el._dom_element.tagName == "P"
assert isinstance(new_el, pydom.BaseElement)
assert new_el._js.tagName == "P"
# EXPECT the new element to be associated with the document
assert new_el.parent == parent_div
assert pydom[selector][0].children[0] == new_el
assert dom.find(selector)[0].children[0] == new_el
class TestInput:
@@ -281,10 +277,10 @@ class TestInput:
def test_value(self):
for id_ in self.input_ids:
expected_type = id_.split("_")[-1]
result = pydom[f"#{id_}"]
result = dom.find(f"#{id_}")
input_el = result[0]
assert input_el._js.type == expected_type
assert input_el.value == f"Content {id_}" == input_el._js.value
assert input_el._dom_element.type == expected_type
assert input_el.value == f"Content {id_}" == input_el._dom_element.value
# Check that we can set the value
new_value = f"New Value {expected_type}"
@@ -299,7 +295,7 @@ class TestInput:
def test_set_value_collection(self):
for id_ in self.input_ids:
input_el = pydom[f"#{id_}"]
input_el = dom.find(f"#{id_}")
assert input_el.value[0] == f"Content {id_}" == input_el[0].value
@@ -307,36 +303,35 @@ class TestInput:
input_el.value = new_value
assert input_el.value[0] == new_value == input_el[0].value
def test_element_without_value(self):
result = pydom[f"#tests-terminal"][0]
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"
def test_element_without_collection(self):
result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError):
result.value = "some value"
# TODO: We only attach attributes to the classes that have them now which means we
# would have to have some other way to help users if using attributes that aren't
# actually on the class. Maybe a job for __setattr__?
#
# def test_element_without_value(self):
# result = dom.find(f"#tests-terminal"][0]
# with pytest.raises(AttributeError):
# result.value = "some value"
#
# def test_element_without_value_via_collection(self):
# result = dom.find(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]
select = dom.find(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}"
assert option.innerHTML == f"Option {i}"
def test_select_options_len(self):
select = pydom[f"#test_select_element_w_options"][0]
select = dom.find(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]
select = dom.find(f"#test_select_element_to_clear")[0]
assert len(select.options) == 3
select.options.clear()
@@ -345,7 +340,7 @@ class TestSelect:
def test_select_element_add(self):
# GIVEN the existing select element with no options
select = pydom[f"#test_select_element"][0]
select = dom.find(f"#test_select_element")[0]
# EXPECT the select element to have no options
assert len(select.options) == 0
@@ -357,7 +352,7 @@ class TestSelect:
# we passed in
assert len(select.options) == 1
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].innerHTML == "Option 1"
# WHEN we add another option (blank this time)
select.options.add("")
@@ -367,7 +362,7 @@ class TestSelect:
# EXPECT the last option to have an empty value and html
assert select.options[1].value == ""
assert select.options[1].html == ""
assert select.options[1].innerHTML == ""
# WHEN we add another option (this time adding it in between the other 2
# options by using an integer index)
@@ -378,11 +373,11 @@ class TestSelect:
# 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].innerHTML == "Option 1"
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[1].innerHTML == "Option 2"
assert select.options[2].value == ""
assert select.options[2].html == ""
assert select.options[2].innerHTML == ""
# WHEN we add another option (this time adding it in between the other 2
# options but using the option itself)
@@ -395,38 +390,48 @@ class TestSelect:
# 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[0].innerHTML == "Option 1"
assert (
select.options[0].selected
== select.options[0]._dom_element.selected
== False
)
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[1].innerHTML == "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[2].innerHTML == "Option 3"
assert (
select.options[2].selected
== select.options[2]._dom_element.selected
== True
)
assert select.options[3].value == ""
assert select.options[3].html == ""
assert select.options[3].innerHTML == ""
# 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)
select.options.add(
value="2a", html="Option 2a", before=select.options[2]._dom_element
)
# 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[0].innerHTML == "Option 1"
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[1].innerHTML == "Option 2"
assert select.options[2].value == "2a"
assert select.options[2].html == "Option 2a"
assert select.options[2].innerHTML == "Option 2a"
assert select.options[3].value == "3"
assert select.options[3].html == "Option 3"
assert select.options[3].innerHTML == "Option 3"
assert select.options[4].value == ""
assert select.options[4].html == ""
assert select.options[4].innerHTML == ""
def test_select_options_remove(self):
# GIVEN the existing select element with 3 options
select = pydom[f"#test_select_element_to_remove"][0]
select = dom.find(f"#test_select_element_to_remove")[0]
# EXPECT the select element to have 3 options
assert len(select.options) == 4
@@ -448,12 +453,12 @@ class TestSelect:
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]
select = dom.find(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
assert selected_option.innerHTML == "Option 2"
assert selected_option.selected == selected_option._dom_element.selected == True