mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Override __getattr__ and __setattr__ on ElementCollection. (#2116)
* Override __getattr__ and __setattr__ on ElementCollection. * fix: bug when using a string to query an ElementCollection. * Use Element.find when indexing ElementCollection with a string. * For consistency also have a find method on ElementCollection. * ElementCollection.find now returns a collection of collections :) * fix tests: for textContent * Revert to extend for ElementCollection.find :) * Make element_from_dom a classmethod Element.from_dom * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * rename: Element.from_dom -> Element.from_dom_element * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * PyCharm warning sweep. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Workaround for mp not allowing setting via __dict__ --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,18 +1,21 @@
|
|||||||
from pyscript import document
|
from pyscript import document
|
||||||
from pyscript.web.elements import ElementCollection, element_from_dom
|
from pyscript.web.elements import Element, ElementCollection
|
||||||
|
|
||||||
|
|
||||||
class DOM:
|
class DOM:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.body = element_from_dom(document.body)
|
self.body = Element.from_dom_element(document.body)
|
||||||
self.head = element_from_dom(document.head)
|
self.head = Element.from_dom_element(document.head)
|
||||||
|
|
||||||
def __getitem__(self, selector):
|
def __getitem__(self, selector):
|
||||||
return self.find(selector)
|
return self.find(selector)
|
||||||
|
|
||||||
def find(self, selector):
|
def find(self, selector):
|
||||||
return ElementCollection(
|
return ElementCollection(
|
||||||
[element_from_dom(el) for el in document.querySelectorAll(selector)]
|
[
|
||||||
|
Element.from_dom_element(dom_element)
|
||||||
|
for dom_element in document.querySelectorAll(selector)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# noinspection PyPep8Naming
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ def getmembers_static(cls):
|
|||||||
|
|
||||||
|
|
||||||
class DOMProperty:
|
class DOMProperty:
|
||||||
"""A descriptor representing a DOM property on an Element`.
|
"""A descriptor representing a DOM property on an `Element` instance.
|
||||||
|
|
||||||
This maps a property on an `Element` instance, to the property with the specified
|
This maps a property on an `Element` instance, to the property with the specified
|
||||||
name on the element's underlying DOM element.
|
name on the element's underlying DOM element.
|
||||||
@@ -53,41 +54,11 @@ class DOMProperty:
|
|||||||
setattr(obj._dom_element, self.name, value)
|
setattr(obj._dom_element, self.name, value)
|
||||||
|
|
||||||
|
|
||||||
def element_from_dom(dom_element):
|
|
||||||
"""Create an instance of the appropriate subclass of `Element` for a DOM element.
|
|
||||||
|
|
||||||
If the DOM element was created via an `Element` (i.e. by us) it will have a data
|
|
||||||
attribute named `data-pyscript-type` that contains the name of the subclass
|
|
||||||
that created it. If the `data-pyscript-type` attribute *is* present we look up the
|
|
||||||
subclass by name and create an instance of that. Otherwise, we make a 'best-guess'
|
|
||||||
and look up the `Element` subclass by the DOM element's tag name (this is NOT
|
|
||||||
fool-proof as many subclasses might use a `<div>`, but close enough for jazz).
|
|
||||||
"""
|
|
||||||
|
|
||||||
# We use "getAttribute" here instead of `js_element.dataset.pyscriptType` as the
|
|
||||||
# latter throws an `AttributeError` if the value isn't set. This way we just get
|
|
||||||
# `None` which seems cleaner.
|
|
||||||
cls_name = dom_element.getAttribute("data-pyscript-type")
|
|
||||||
if cls_name:
|
|
||||||
cls = ELEMENT_CLASSES_BY_NAME.get(cls_name.lower())
|
|
||||||
|
|
||||||
else:
|
|
||||||
cls = ELEMENT_CLASSES_BY_TAG.get(dom_element.tagName.lower())
|
|
||||||
|
|
||||||
# For any unknown elements (custom tags etc.) create an instance of the 'Element'
|
|
||||||
# class.
|
|
||||||
if not cls:
|
|
||||||
cls = Element
|
|
||||||
|
|
||||||
return cls(dom_element=dom_element)
|
|
||||||
|
|
||||||
|
|
||||||
class Element:
|
class Element:
|
||||||
tag = "div"
|
tag = "div"
|
||||||
|
|
||||||
# GLOBAL ATTRIBUTES.
|
|
||||||
# These are attribute that all elements have (this list is a subset of the official
|
# These are attribute that all elements have (this list is a subset of the official
|
||||||
# one). We are trying to capture the most used ones.
|
# one - we are just trying to capture the most used ones).
|
||||||
accesskey = DOMProperty("accesskey")
|
accesskey = DOMProperty("accesskey")
|
||||||
autofocus = DOMProperty("autofocus")
|
autofocus = DOMProperty("autofocus")
|
||||||
autocapitalize = DOMProperty("autocapitalize")
|
autocapitalize = DOMProperty("autocapitalize")
|
||||||
@@ -105,11 +76,43 @@ class Element:
|
|||||||
slot = DOMProperty("slot")
|
slot = DOMProperty("slot")
|
||||||
spellcheck = DOMProperty("spellcheck")
|
spellcheck = DOMProperty("spellcheck")
|
||||||
tabindex = DOMProperty("tabindex")
|
tabindex = DOMProperty("tabindex")
|
||||||
text = DOMProperty("textContent")
|
tagName = DOMProperty("tagName")
|
||||||
|
textContent = DOMProperty("textContent")
|
||||||
title = DOMProperty("title")
|
title = DOMProperty("title")
|
||||||
translate = DOMProperty("translate")
|
translate = DOMProperty("translate")
|
||||||
virtualkeyboardpolicy = DOMProperty("virtualkeyboardpolicy")
|
virtualkeyboardpolicy = DOMProperty("virtualkeyboardpolicy")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dom_element(cls, dom_element):
|
||||||
|
"""Create an instance of the appropriate subclass of `Element` for a DOM
|
||||||
|
element.
|
||||||
|
|
||||||
|
If the DOM element was created via an `Element` (i.e. by us) it will have a data
|
||||||
|
attribute named `data-pyscript-type` that contains the name of the subclass
|
||||||
|
that created it. Hence, if the `data-pyscript-type` attribute *is* present we
|
||||||
|
look up the subclass by name and create an instance of that. Otherwise, we make
|
||||||
|
a 'best-guess' and look up the `Element` subclass by the DOM element's tag name
|
||||||
|
(this is NOT fool-proof as many subclasses might use a `<div>`, but close enough
|
||||||
|
for jazz).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We use "getAttribute" here instead of `js_element.dataset.pyscriptType` as the
|
||||||
|
# latter throws an `AttributeError` if the value isn't set. This way we just get
|
||||||
|
# `None` which seems cleaner.
|
||||||
|
cls_name = dom_element.getAttribute("data-pyscript-type")
|
||||||
|
if cls_name:
|
||||||
|
element_cls = ELEMENT_CLASSES_BY_NAME.get(cls_name.lower())
|
||||||
|
|
||||||
|
else:
|
||||||
|
element_cls = ELEMENT_CLASSES_BY_TAG.get(dom_element.tagName.lower())
|
||||||
|
|
||||||
|
# For any unknown elements (custom tags etc.) create an instance of this
|
||||||
|
# class ('Element').
|
||||||
|
if not element_cls:
|
||||||
|
element_cls = cls
|
||||||
|
|
||||||
|
return element_cls(dom_element=dom_element)
|
||||||
|
|
||||||
def __init__(self, dom_element=None, classes=None, style=None, **kwargs):
|
def __init__(self, dom_element=None, classes=None, style=None, **kwargs):
|
||||||
"""Create a new, or wrap an existing DOM element.
|
"""Create a new, or wrap an existing DOM element.
|
||||||
|
|
||||||
@@ -122,7 +125,7 @@ class Element:
|
|||||||
#
|
#
|
||||||
# Using the `dataset` attribute is how you programmatically add `data-xxx`
|
# Using the `dataset` attribute is how you programmatically add `data-xxx`
|
||||||
# attributes to a DOM element. In this case it will set an attribute that
|
# attributes to a DOM element. In this case it will set an attribute that
|
||||||
# appears in (say) the devtools as `data-pyscript-type`.
|
# appears in the DOM as `data-pyscript-type`.
|
||||||
self._dom_element.dataset.pyscriptType = type(self).__name__
|
self._dom_element.dataset.pyscriptType = type(self).__name__
|
||||||
|
|
||||||
self._parent = None
|
self._parent = None
|
||||||
@@ -138,24 +141,24 @@ class Element:
|
|||||||
if classes:
|
if classes:
|
||||||
self.classes.add(classes)
|
self.classes.add(classes)
|
||||||
|
|
||||||
# Set any specified styles.
|
|
||||||
if isinstance(style, dict):
|
if isinstance(style, dict):
|
||||||
self.style.set(**style)
|
self.style.set(**style)
|
||||||
|
|
||||||
elif style is not None:
|
elif style is not None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Style should be a dictionary, received {style} (type {type(style)}) instead."
|
f"Style should be a dictionary, received {style} "
|
||||||
|
f"(type {type(style)}) instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
self._set_dom_properties(**kwargs)
|
self._set_dom_properties(**kwargs)
|
||||||
|
|
||||||
def _set_dom_properties(self, **kwargs):
|
def _set_dom_properties(self, **kwargs):
|
||||||
"""Set all the properties (of type DOMProperty) provided in input as properties
|
"""Set the specified DOM properties.
|
||||||
of the class instance.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
**kwargs: The properties to set
|
**kwargs: The properties to set
|
||||||
"""
|
"""
|
||||||
|
# Harvest all DOM properties from the instance's class.
|
||||||
dom_properties = {
|
dom_properties = {
|
||||||
attribute_name: attribute_value
|
attribute_name: attribute_value
|
||||||
for attribute_name, attribute_value in getmembers_static(self.__class__)
|
for attribute_name, attribute_value in getmembers_static(self.__class__)
|
||||||
@@ -179,7 +182,7 @@ class Element:
|
|||||||
@property
|
@property
|
||||||
def children(self):
|
def children(self):
|
||||||
return ElementCollection(
|
return ElementCollection(
|
||||||
[element_from_dom(el) for el in self._dom_element.children]
|
[Element.from_dom_element(el) for el in self._dom_element.children]
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -192,7 +195,7 @@ class Element:
|
|||||||
return self._parent
|
return self._parent
|
||||||
|
|
||||||
if self._dom_element.parentElement:
|
if self._dom_element.parentElement:
|
||||||
self._parent = element_from_dom(self._dom_element.parentElement)
|
self._parent = Element.from_dom_element(self._dom_element.parentElement)
|
||||||
|
|
||||||
return self._parent
|
return self._parent
|
||||||
|
|
||||||
@@ -228,12 +231,13 @@ class Element:
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Nope! This is not an element or a NodeList.
|
# Nope! This is not an element or a NodeList.
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f'Element "{child}" is a proxy object, but not a valid element or a NodeList.'
|
f'Element "{child}" is a proxy object, "'
|
||||||
|
f"but not a valid element or a NodeList."
|
||||||
)
|
)
|
||||||
|
|
||||||
def clone(self, clone_id=None):
|
def clone(self, clone_id=None):
|
||||||
"""Make a clone of the element (clones the underlying DOM object too)."""
|
"""Make a clone of the element (clones the underlying DOM object too)."""
|
||||||
clone = element_from_dom(self._dom_element.cloneNode(True))
|
clone = Element.from_dom_element(self._dom_element.cloneNode(True))
|
||||||
clone.id = clone_id
|
clone.id = clone_id
|
||||||
return clone
|
return clone
|
||||||
|
|
||||||
@@ -249,8 +253,8 @@ class Element:
|
|||||||
"""
|
"""
|
||||||
return ElementCollection(
|
return ElementCollection(
|
||||||
[
|
[
|
||||||
element_from_dom(el)
|
Element.from_dom_element(dom_element)
|
||||||
for el in self._dom_element.querySelectorAll(selector)
|
for dom_element in self._dom_element.querySelectorAll(selector)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -322,13 +326,13 @@ class Classes:
|
|||||||
self.remove(old_class)
|
self.remove(old_class)
|
||||||
self.add(new_class)
|
self.add(new_class)
|
||||||
|
|
||||||
def toggle(self, class_name):
|
def toggle(self, *class_names):
|
||||||
|
for class_name in class_names:
|
||||||
if class_name in self:
|
if class_name in self:
|
||||||
self.remove(class_name)
|
self.remove(class_name)
|
||||||
return False
|
|
||||||
|
|
||||||
|
else:
|
||||||
self.add(class_name)
|
self.add(class_name)
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class HasOptions:
|
class HasOptions:
|
||||||
@@ -365,7 +369,7 @@ class Options:
|
|||||||
**kws,
|
**kws,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a new option to the select element"""
|
"""Add a new option to the select element"""
|
||||||
# create the option element and set the attributes
|
|
||||||
option = document.createElement("option")
|
option = document.createElement("option")
|
||||||
if value is not None:
|
if value is not None:
|
||||||
kws["value"] = value
|
kws["value"] = value
|
||||||
@@ -395,7 +399,9 @@ class Options:
|
|||||||
@property
|
@property
|
||||||
def options(self):
|
def options(self):
|
||||||
"""Return the list of options"""
|
"""Return the list of options"""
|
||||||
return [element_from_dom(opt) for opt in self._element._dom_element.options]
|
return [
|
||||||
|
Element.from_dom_element(opt) for opt in self._element._dom_element.options
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selected(self):
|
def selected(self):
|
||||||
@@ -449,6 +455,8 @@ class Style:
|
|||||||
|
|
||||||
|
|
||||||
class ContainerElement(Element):
|
class ContainerElement(Element):
|
||||||
|
"""Base class for elements that can contain other elements."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *args, children=None, dom_element=None, style=None, classes=None, **kwargs
|
self, *args, children=None, dom_element=None, style=None, classes=None, **kwargs
|
||||||
):
|
):
|
||||||
@@ -1434,7 +1442,7 @@ class video(ContainerElement):
|
|||||||
|
|
||||||
# If 'to' is a string, then assume it is a query selector.
|
# If 'to' is a string, then assume it is a query selector.
|
||||||
elif isinstance(to, str):
|
elif isinstance(to, str):
|
||||||
nodelist = document.querySelectorAll(to)
|
nodelist = document.querySelectorAll(to) # NOQA
|
||||||
if nodelist.length == 0:
|
if nodelist.length == 0:
|
||||||
raise TypeError("No element with selector {to} to snap to.")
|
raise TypeError("No element with selector {to} to snap to.")
|
||||||
|
|
||||||
@@ -1464,7 +1472,7 @@ class grid(ContainerElement):
|
|||||||
self.style["grid-template-columns"] = layout
|
self.style["grid-template-columns"] = layout
|
||||||
|
|
||||||
# TODO: This should be a property
|
# TODO: This should be a property
|
||||||
if not gap is None:
|
if gap is not None:
|
||||||
self.style["gap"] = gap
|
self.style["gap"] = gap
|
||||||
|
|
||||||
|
|
||||||
@@ -1513,9 +1521,9 @@ class ClassesCollection:
|
|||||||
for element in self._collection:
|
for element in self._collection:
|
||||||
element.classes.replace(old_class, new_class)
|
element.classes.replace(old_class, new_class)
|
||||||
|
|
||||||
def toggle(self, class_name):
|
def toggle(self, *class_names):
|
||||||
for element in self._collection:
|
for element in self._collection:
|
||||||
element.classes.toggle(class_name)
|
element.classes.toggle(*class_names)
|
||||||
|
|
||||||
def _all_class_names(self):
|
def _all_class_names(self):
|
||||||
all_class_names = set()
|
all_class_names = set()
|
||||||
@@ -1554,37 +1562,8 @@ class ElementCollection:
|
|||||||
self._classes = ClassesCollection(self)
|
self._classes = ClassesCollection(self)
|
||||||
self._style = StyleCollection(self)
|
self._style = StyleCollection(self)
|
||||||
|
|
||||||
@property
|
|
||||||
def children(self):
|
|
||||||
return self._elements
|
|
||||||
|
|
||||||
@property
|
|
||||||
def classes(self):
|
|
||||||
return self._classes
|
|
||||||
|
|
||||||
@property
|
|
||||||
def style(self):
|
|
||||||
return self._style
|
|
||||||
|
|
||||||
@property
|
|
||||||
def innerHTML(self):
|
|
||||||
return self._get_attribute("innerHTML")
|
|
||||||
|
|
||||||
@innerHTML.setter
|
|
||||||
def innerHTML(self, value):
|
|
||||||
self._set_attribute("innerHTML", value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return self._get_attribute("value")
|
|
||||||
|
|
||||||
@value.setter
|
|
||||||
def value(self, value):
|
|
||||||
self._set_attribute("value", value)
|
|
||||||
|
|
||||||
def __eq__(self, obj):
|
def __eq__(self, obj):
|
||||||
"""Check if the element is the same as the other element by comparing
|
"""Check for equality by comparing the underlying DOM elements."""
|
||||||
the underlying DOM element"""
|
|
||||||
return isinstance(obj, ElementCollection) and obj._elements == self._elements
|
return isinstance(obj, ElementCollection) and obj._elements == self._elements
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
@@ -1598,8 +1577,7 @@ class ElementCollection:
|
|||||||
return ElementCollection(self._elements[key])
|
return ElementCollection(self._elements[key])
|
||||||
|
|
||||||
# If it's anything else (basically a string) we use it as a query selector.
|
# If it's anything else (basically a string) we use it as a query selector.
|
||||||
elements = self._elements.querySelectorAll(key)
|
return self.find(key)
|
||||||
return ElementCollection([element_from_dom(el) for el in elements])
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
yield from self._elements
|
yield from self._elements
|
||||||
@@ -1608,7 +1586,43 @@ class ElementCollection:
|
|||||||
return len(self._elements)
|
return len(self._elements)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}"
|
return (
|
||||||
|
f"{self.__class__.__name__} (length: {len(self._elements)}) "
|
||||||
|
f"{self._elements}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
return self._get_attribute(item)
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
# This class overrides `__setattr__` to delegate "public" attributes to the
|
||||||
|
# elements in the collection, but we don't use the usual Python pattern where we
|
||||||
|
# set attributes on the collection itself via `self.__dict__` as it is not yet
|
||||||
|
# supported in our build of MicroPython. Instead, we handle it here.
|
||||||
|
if key.startswith("_"):
|
||||||
|
super().__setattr__(key, value)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._set_attribute(key, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self):
|
||||||
|
return self._elements
|
||||||
|
|
||||||
|
@property
|
||||||
|
def classes(self):
|
||||||
|
return self._classes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def style(self):
|
||||||
|
return self._style
|
||||||
|
|
||||||
|
def find(self, selector):
|
||||||
|
elements = []
|
||||||
|
for element in self._elements:
|
||||||
|
elements.extend(element.find(selector))
|
||||||
|
|
||||||
|
return ElementCollection(elements)
|
||||||
|
|
||||||
def _get_attribute(self, attr, index=None):
|
def _get_attribute(self, attr, index=None):
|
||||||
if index is None:
|
if index is None:
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="py" src="./run_tests.py" config="./tests.toml"></script>
|
<script type="py" src="/test/pyscript_dom/run_tests.py" config="/test/pyscript_dom/tests.toml"></script>
|
||||||
|
|
||||||
<h1>pyscript.dom Tests</h1>
|
<h1>pyscript.dom Tests</h1>
|
||||||
<p>You can pass test parameters to this test suite by passing them as query params on the url.
|
<p>You can pass test parameters to this test suite by passing them as query params on the url.
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class TestElement:
|
|||||||
|
|
||||||
assert called
|
assert called
|
||||||
|
|
||||||
def test_html_attribute(self):
|
def test_inner_html_attribute(self):
|
||||||
# GIVEN an existing element on the page with a known empty text content
|
# GIVEN an existing element on the page with a known empty text content
|
||||||
div = dom.find("#element_attribute_tests")[0]
|
div = dom.find("#element_attribute_tests")[0]
|
||||||
|
|
||||||
@@ -163,14 +163,14 @@ class TestElement:
|
|||||||
# EXPECT the element html and underlying JS Element innerHTML property
|
# EXPECT the element html and underlying JS Element innerHTML property
|
||||||
# to match what we expect and what
|
# to match what we expect and what
|
||||||
assert div.innerHTML == div._dom_element.innerHTML == "<b>New Content</b>"
|
assert div.innerHTML == div._dom_element.innerHTML == "<b>New Content</b>"
|
||||||
assert div.text == div._dom_element.textContent == "New Content"
|
assert div.textContent == div._dom_element.textContent == "New Content"
|
||||||
|
|
||||||
def test_text_attribute(self):
|
def test_text_attribute(self):
|
||||||
# GIVEN an existing element on the page with a known empty text content
|
# GIVEN an existing element on the page with a known empty text content
|
||||||
div = dom.find("#element_attribute_tests")[0]
|
div = dom.find("#element_attribute_tests")[0]
|
||||||
|
|
||||||
# WHEN we set the html attribute
|
# WHEN we set the html attribute
|
||||||
div.text = "<b>New Content</b>"
|
div.textContent = "<b>New Content</b>"
|
||||||
|
|
||||||
# EXPECT the element html and underlying JS Element innerHTML property
|
# EXPECT the element html and underlying JS Element innerHTML property
|
||||||
# to match what we expect and what
|
# to match what we expect and what
|
||||||
@@ -179,7 +179,7 @@ class TestElement:
|
|||||||
== div._dom_element.innerHTML
|
== div._dom_element.innerHTML
|
||||||
== "<b>New Content</b>"
|
== "<b>New Content</b>"
|
||||||
)
|
)
|
||||||
assert div.text == div._dom_element.textContent == "<b>New Content</b>"
|
assert div.textContent == div._dom_element.textContent == "<b>New Content</b>"
|
||||||
|
|
||||||
|
|
||||||
class TestCollection:
|
class TestCollection:
|
||||||
|
|||||||
Reference in New Issue
Block a user