The all-new, pyscript.web (ignore the branch name :) ) (#2129)

* Minor cleanups: move all Element classes to bottom of module.

* Commenting.

* Commenting.

* Commenting.

* Group dunder methods.

* Don't cache the element's parent.

* Remove style type check until we decide whether or not to add for classes too.

* Add ability to register/unregister element classes.

* Implement __iter__ for container elements.

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

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

* Minor renaming to make it clear when we have an Element instance vs an actual DOM element.

* remove duplication: added Element.get_tag_name

* Commenting.

* Allow Element.append to 1) use *args, 2) accept iterables

* Remove iterable check - inteferes with js proxies.

* Don't use *args, so it quacks more like a list ;)

* Element.append take 2 :)

* Remove unused code.

* Move to web.py with a page object!

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

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

* Added 'page.title' too :)

* Add __getitem__ as a shortcut for page.find

* Add Element.__getitem__ to be consistent

* Make __getitem__ consistent for Page, Element and ElementCollection.

* Docstringing.

* Docstringing.

* Docstringing/commenting.

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

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

* fix select.add (revert InnerHTML->html)

* Commenting.

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

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

* Hand-edit some of the AI :)

* Rename ElementCollection.children -> ElementCollection.elements

* Remove unnecessary guard.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Martin
2024-08-01 03:36:57 -06:00
committed by GitHub
parent d47fb58ede
commit 999897df12
6 changed files with 496 additions and 453 deletions

View File

@@ -20,7 +20,7 @@ def when(event_type=None, selector=None):
def decorator(func):
from pyscript.web.elements import Element, ElementCollection
from pyscript.web import Element, ElementCollection
if isinstance(selector, str):
elements = document.querySelectorAll(selector)

View File

@@ -1,36 +1,60 @@
try:
from typing import Any
"""Lightweight interface to the DOM and HTML elements."""
except ImportError:
Any = "Any"
try:
import warnings
except ImportError:
# TODO: For now it probably means we are in MicroPython. We should figure
# out the "right" way to handle this. For now we just ignore the warning
# and logging to console
class warnings:
@staticmethod
def warn(*args, **kwargs):
print("WARNING: ", *args, **kwargs)
# `when` is not used in this module. It is imported here save the user an additional
# import (i.e. they can get what they need from `pyscript.web`).
from pyscript import document, when # NOQA
from pyscript import document
def wrap_dom_element(dom_element):
"""Wrap an existing DOM element in an instance of a subclass of `Element`.
This is just a convenience function to avoid having to import the `Element` class
and use its class method.
"""
return Element.wrap_dom_element(dom_element)
class Element:
# A lookup table to get an `Element` subclass by tag name. Used when wrapping an
# existing DOM element.
element_classes_by_tag_name = {}
@classmethod
def from_dom_element(cls, dom_element):
"""Create an instance of a subclass of `Element` for a DOM element."""
def get_tag_name(cls):
"""Return the HTML tag name for the class.
element_cls = ELEMENT_CLASSES_BY_TAG_NAME.get(dom_element.tagName.lower())
For classes that have a trailing underscore (because they clash with a Python
keyword or built-in), we remove it to get the tag name. e.g. for the `input_`
class, the tag name is `input`.
# For any unknown elements (custom tags etc.) create an instance of this
# class ('Element').
if not element_cls:
element_cls = cls
"""
return cls.__name__.replace("_", "")
@classmethod
def register_element_classes(cls, element_classes):
"""Register an iterable of element classes."""
for element_class in element_classes:
tag_name = element_class.get_tag_name()
cls.element_classes_by_tag_name[tag_name] = element_class
@classmethod
def unregister_element_classes(cls, element_classes):
"""Unregister an iterable of element classes."""
for element_class in element_classes:
tag_name = element_class.get_tag_name()
cls.element_classes_by_tag_name.pop(tag_name, None)
@classmethod
def wrap_dom_element(cls, dom_element):
"""Wrap an existing DOM element in an instance of a subclass of `Element`.
We look up the `Element` subclass by the DOM element's tag name. For any unknown
elements (custom tags etc.) use *this* class (`Element`).
"""
element_cls = cls.element_classes_by_tag_name.get(
dom_element.tagName.lower(), cls
)
return element_cls(dom_element=dom_element)
@@ -41,16 +65,33 @@ class Element:
Otherwise, we are being called to *wrap* an existing DOM element.
"""
self._dom_element = dom_element or document.createElement(
type(self).__name__.replace("_", "")
type(self).get_tag_name()
)
self._parent = None
# A set-like interface to the element's `classList`.
self._classes = Classes(self)
# A dict-like interface to the element's `style` attribute.
self._style = Style(self)
# Set any specified classes, styles, and DOM properties.
self.update(classes=classes, style=style, **kwargs)
def __eq__(self, obj):
"""Check for equality by comparing the underlying DOM element."""
return isinstance(obj, Element) and obj._dom_element == self._dom_element
def __getitem__(self, key):
"""Get an item within the element's children.
If `key` is an integer or a slice we use it to index/slice the element's
children. Otherwise, we use `key` as a query selector.
"""
if isinstance(key, int) or isinstance(key, slice):
return self.children[key]
return self.find(key)
def __getattr__(self, name):
# This allows us to get attributes on the underlying DOM element that clash
# with Python keywords or built-ins (e.g. the output element has an
@@ -80,119 +121,102 @@ class Element:
setattr(self._dom_element, name, value)
@property
def children(self):
"""Return the element's children as an `ElementCollection`."""
return ElementCollection.wrap_dom_elements(self._dom_element.children)
@property
def classes(self):
"""Return the element's `classList` as a `Classes` instance."""
return self._classes
@property
def parent(self):
"""Return the element's `parent `Element`."""
if self._dom_element.parentElement is None:
return None
return Element.wrap_dom_element(self._dom_element.parentElement)
@property
def style(self):
"""Return the element's `style` attribute as a `Style` instance."""
return self._style
def append(self, *items):
"""Append the specified items to the element."""
for item in items:
if isinstance(item, Element):
self._dom_element.appendChild(item._dom_element)
elif isinstance(item, ElementCollection):
for element in item:
self._dom_element.appendChild(element._dom_element)
# We check for list/tuple here and NOT for any iterable as it will match
# a JS Nodelist which is handled explicitly below.
# NodeList.
elif isinstance(item, list) or isinstance(item, tuple):
for child in item:
self.append(child)
else:
# In this case we know it's not an Element or an ElementCollection, so
# we guess that it's either a DOM element or NodeList returned via the
# ffi.
try:
# First, we try to see if it's an element by accessing the 'tagName'
# attribute.
item.tagName
self._dom_element.appendChild(item)
except AttributeError:
try:
# Ok, it's not an element, so let's see if it's a NodeList by
# accessing the 'length' attribute.
item.length
for element_ in item:
self._dom_element.appendChild(element_)
except AttributeError:
# Nope! This is not an element or a NodeList.
raise TypeError(
f'Element "{item}" is a proxy object, "'
f"but not a valid element or a NodeList."
)
def clone(self, clone_id=None):
"""Make a clone of the element (clones the underlying DOM object too)."""
clone = Element.wrap_dom_element(self._dom_element.cloneNode(True))
clone.id = clone_id
return clone
def find(self, selector):
"""Find all elements that match the specified selector.
Return the results as a (possibly empty) `ElementCollection`.
"""
return ElementCollection.wrap_dom_elements(
self._dom_element.querySelectorAll(selector)
)
def show_me(self):
"""Convenience method for 'element.scrollIntoView()'."""
self._dom_element.scrollIntoView()
def update(self, classes=None, style=None, **kwargs):
"""Update the element with the specified classes, styles, and DOM properties."""
if classes:
self.classes.add(classes)
if isinstance(style, dict):
if style:
self.style.set(**style)
elif style is not None:
raise ValueError(
f"Style should be a dictionary, received {style} "
f"(type {type(style)}) instead."
)
self._set_dom_properties(**kwargs)
def _set_dom_properties(self, **kwargs):
"""Set the specified DOM properties.
Args:
**kwargs: The properties to set
"""
for name, value in kwargs.items():
setattr(self, name, value)
def __eq__(self, obj):
"""Check for equality by comparing the underlying DOM element."""
return isinstance(obj, Element) and obj._dom_element == self._dom_element
@property
def children(self):
return ElementCollection(
[Element.from_dom_element(el) for el in self._dom_element.children]
)
@property
def classes(self):
return self._classes
@property
def parent(self):
if self._parent:
return self._parent
if self._dom_element.parentElement:
self._parent = Element.from_dom_element(self._dom_element.parentElement)
return self._parent
@property
def style(self):
return self._style
def append(self, child):
if isinstance(child, Element):
self._dom_element.appendChild(child._dom_element)
elif isinstance(child, ElementCollection):
for el in child:
self._dom_element.appendChild(el._dom_element)
else:
# In this case we know it's not an Element or an ElementCollection, so we
# guess that it's either a DOM element or NodeList returned via the ffi.
try:
# First, we try to see if it's an element by accessing the 'tagName'
# attribute.
child.tagName
self._dom_element.appendChild(child)
except AttributeError:
try:
# Ok, it's not an element, so let's see if it's a NodeList by
# accessing the 'length' attribute.
child.length
for element_ in child:
self._dom_element.appendChild(element_)
except AttributeError:
# Nope! This is not an element or a NodeList.
raise TypeError(
f'Element "{child}" is a proxy object, "'
f"but not a valid element or a NodeList."
)
def clone(self, clone_id=None):
"""Make a clone of the element (clones the underlying DOM object too)."""
clone = Element.from_dom_element(self._dom_element.cloneNode(True))
clone.id = clone_id
return clone
def find(self, selector):
"""Return an ElementCollection representing all the child elements that
match the specified selector.
Args:
selector (str): A string containing a selector expression
Returns:
ElementCollection: A collection of elements matching the selector
"""
return ElementCollection(
[
Element.from_dom_element(dom_element)
for dom_element in self._dom_element.querySelectorAll(selector)
]
)
def show_me(self):
"""Scroll the element into view."""
self._dom_element.scrollIntoView()
class Classes:
"""A set-like interface to an element's `classList`."""
@@ -233,6 +257,7 @@ class Classes:
return " ".join(self._class_list)
def add(self, *class_names):
"""Add one or more classes to the element."""
for class_name in class_names:
if isinstance(class_name, list):
for item in class_name:
@@ -242,9 +267,11 @@ class Classes:
self._class_list.add(class_name)
def contains(self, class_name):
"""Check if the element has the specified class."""
return class_name in self
def remove(self, *class_names):
"""Remove one or more classes from the element."""
for class_name in class_names:
if isinstance(class_name, list):
for item in class_name:
@@ -254,10 +281,12 @@ class Classes:
self._class_list.remove(class_name)
def replace(self, old_class, new_class):
"""Replace one of the element's classes with another."""
self.remove(old_class)
self.add(new_class)
def toggle(self, *class_names):
"""Toggle one or more of the element's classes."""
for class_name in class_names:
if class_name in self:
self.remove(class_name)
@@ -274,6 +303,7 @@ class HasOptions:
@property
def options(self):
"""Return the element's options as an `Options"""
if not hasattr(self, "_options"):
self._options = Options(self)
@@ -281,63 +311,17 @@ class HasOptions:
class Options:
"""This class represents the <option>s of a <datalist>, <optgroup> or <select>
element.
"""This class represents the <option>s of a <datalist>, <optgroup> or <select>.
It allows to access to add and remove <option>s by using the `add` and `remove`
methods.
It allows access to add and remove <option>s by using the `add`, `remove` and
`clear` methods.
"""
def __init__(self, element: Element) -> None:
def __init__(self, element):
self._element = element
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"""
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._dom_element
self._element._dom_element.add(option, before)
def remove(self, item: int) -> None:
"""Remove the option at the specified index"""
self._element._dom_element.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.from_dom_element(opt) for opt in self._element._dom_element.options
]
@property
def selected(self):
"""Return the selected option"""
return self.options[self._element._dom_element.selectedIndex]
def __getitem__(self, key):
return self.options[key]
def __iter__(self):
yield from self.options
@@ -348,14 +332,49 @@ class Options:
def __repr__(self):
return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
def __getitem__(self, key):
return self.options[key]
@property
def options(self):
"""Return the list of options."""
return [Element.wrap_dom_element(o) for o in self._element._dom_element.options]
@property
def selected(self):
"""Return the selected option."""
return self.options[self._element._dom_element.selectedIndex]
def add(self, value=None, html=None, text=None, before=None, **kwargs):
"""Add a new option to the element"""
if value is not None:
kwargs["value"] = value
if html is not None:
kwargs["innerHTML"] = html
if text is not None:
kwargs["text"] = text
new_option = option(**kwargs)
if before:
if isinstance(before, Element):
before = before._dom_element
self._element._dom_element.add(new_option._dom_element, before)
def clear(self):
"""Remove all options."""
while len(self) > 0:
self.remove(0)
def remove(self, index):
"""Remove the option at the specified index."""
self._element._dom_element.remove(index)
class Style:
"""A dict-like interface to an element's css style."""
"""A dict-like interface to an element's `style` attribute."""
def __init__(self, element: Element) -> None:
def __init__(self, element: Element):
self._element = element
self._style = self._element._dom_element.style
@@ -366,9 +385,11 @@ class Style:
self._style.setProperty(key, value)
def remove(self, key):
"""Remove a CSS property from the element."""
self._style.removeProperty(key)
def set(self, **kwargs):
"""Set one or more CSS properties on the element."""
for key, value in kwargs.items():
self._element._dom_element.style.setProperty(key, value)
@@ -402,10 +423,188 @@ class ContainerElement(Element):
else:
self.innerHTML += child
def __iter__(self):
yield from self.children
# Classes for every element type. If the element type (e.g. "input") clashes with
class ClassesCollection:
"""A set-like interface to the classes of the elements in a collection."""
def __init__(self, collection):
self._collection = collection
def __contains__(self, class_name):
for element in self._collection:
if class_name in element.classes:
return True
return False
def __eq__(self, other):
return (
isinstance(other, ClassesCollection)
and self._collection == other._collection
)
def __iter__(self):
for class_name in self._all_class_names():
yield class_name
def __len__(self):
return len(self._all_class_names())
def __repr__(self):
return f"ClassesCollection({repr(self._collection)})"
def __str__(self):
return " ".join(self._all_class_names())
def add(self, *class_names):
"""Add one or more classes to the elements in the collection."""
for element in self._collection:
element.classes.add(*class_names)
def contains(self, class_name):
"""Check if any element in the collection has the specified class."""
return class_name in self
def remove(self, *class_names):
"""Remove one or more classes from the elements in the collection."""
for element in self._collection:
element.classes.remove(*class_names)
def replace(self, old_class, new_class):
"""Replace one of the classes in the elements in the collection with another."""
for element in self._collection:
element.classes.replace(old_class, new_class)
def toggle(self, *class_names):
"""Toggle one or more classes on the elements in the collection."""
for element in self._collection:
element.classes.toggle(*class_names)
def _all_class_names(self):
all_class_names = set()
for element in self._collection:
for class_name in element.classes:
all_class_names.add(class_name)
return all_class_names
class StyleCollection:
"""A dict-like interface to the styles of the elements in a collection."""
def __init__(self, collection):
self._collection = collection
def __getitem__(self, key):
return [element.style[key] for element in self._collection._elements]
def __setitem__(self, key, value):
for element in self._collection._elements:
element.style[key] = value
def __repr__(self):
return f"StyleCollection({repr(self._collection)})"
def remove(self, key):
"""Remove a CSS property from the elements in the collection."""
for element in self._collection._elements:
element.style.remove(key)
class ElementCollection:
@classmethod
def wrap_dom_elements(cls, dom_elements):
"""Wrap an iterable of dom_elements in an `ElementCollection`."""
return cls(
[Element.wrap_dom_element(dom_element) for dom_element in dom_elements]
)
def __init__(self, elements: [Element]):
self._elements = elements
self._classes = ClassesCollection(self)
self._style = StyleCollection(self)
def __eq__(self, obj):
"""Check for equality by comparing the underlying DOM elements."""
return isinstance(obj, ElementCollection) and obj._elements == self._elements
def __getitem__(self, key):
"""Get an item in the collection.
If `key` is an integer or a slice we use it to index/slice the collection.
Otherwise, we use `key` as a query selector.
"""
if isinstance(key, int):
return self._elements[key]
elif isinstance(key, slice):
return ElementCollection(self._elements[key])
return self.find(key)
def __iter__(self):
yield from self._elements
def __len__(self):
return len(self._elements)
def __repr__(self):
return (
f"{self.__class__.__name__} (length: {len(self._elements)}) "
f"{self._elements}"
)
def __getattr__(self, name):
return [getattr(element, name) for element in self._elements]
def __setattr__(self, name, 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 that is not
# yet supported in our build of MicroPython. Instead, we handle it here by
# using super for all "private" attributes (those starting with an underscore).
if name.startswith("_"):
super().__setattr__(name, value)
else:
for element in self._elements:
setattr(element, name, value)
@property
def classes(self):
"""Return the classes of the elements in the collection as a `ClassesCollection`."""
return self._classes
@property
def elements(self):
"""Return the elements in the collection as a list."""
return self._elements
@property
def style(self):
""""""
return self._style
def find(self, selector):
"""Find all elements that match the specified selector.
Return the results as a (possibly empty) `ElementCollection`.
"""
elements = []
for element in self._elements:
elements.extend(element.find(selector))
return ElementCollection(elements)
# Classes for every HTML element. If the element tag name (e.g. "input") clashes with
# either a Python keyword or common symbol, then we suffix the class name with an "_"
# (e.g. "input_").
# (e.g. the class for the "input" element is "input_").
class a(ContainerElement):
@@ -463,7 +662,7 @@ class button(ContainerElement):
class canvas(ContainerElement):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas"""
def download(self, filename: str = "snapped.png") -> None:
def download(self, filename: str = "snapped.png"):
"""Download the current element with the filename provided in input.
Inputs:
@@ -905,167 +1104,6 @@ class wbr(Element):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr"""
class ClassesCollection:
def __init__(self, collection: "ElementCollection") -> None:
self._collection = collection
def __contains__(self, class_name):
for element in self._collection:
if class_name in element.classes:
return True
return False
def __eq__(self, other):
return (
isinstance(other, ClassesCollection)
and self._collection == other._collection
)
def __iter__(self):
for class_name in self._all_class_names():
yield class_name
def __len__(self):
return len(self._all_class_names())
def __repr__(self):
return f"ClassesCollection({repr(self._collection)})"
def __str__(self):
return " ".join(self._all_class_names())
def add(self, *class_names):
for element in self._collection:
element.classes.add(*class_names)
def contains(self, class_name):
return class_name in self
def remove(self, *class_names):
for element in self._collection:
element.classes.remove(*class_names)
def replace(self, old_class, new_class):
for element in self._collection:
element.classes.replace(old_class, new_class)
def toggle(self, *class_names):
for element in self._collection:
element.classes.toggle(*class_names)
def _all_class_names(self):
all_class_names = set()
for element in self._collection:
for class_name in element.classes:
all_class_names.add(class_name)
return all_class_names
class StyleCollection:
def __init__(self, collection: "ElementCollection") -> None:
self._collection = collection
def __get__(self, obj, objtype=None):
return obj._get_attribute("style")
def __getitem__(self, key):
return self._collection._get_attribute("style")[key]
def __setitem__(self, key, value):
for element in self._collection._elements:
element.style[key] = value
def __repr__(self):
return f"StyleCollection({repr(self._collection)})"
def remove(self, key):
for element in self._collection._elements:
element.style.remove(key)
class ElementCollection:
def __init__(self, elements: [Element]) -> None:
self._elements = elements
self._classes = ClassesCollection(self)
self._style = StyleCollection(self)
def __eq__(self, obj):
"""Check for equality by comparing the underlying DOM elements."""
return isinstance(obj, ElementCollection) and obj._elements == self._elements
def __getitem__(self, key):
# If it's an integer we use it to access the elements in the collection
if isinstance(key, int):
return self._elements[key]
# If it's a slice we use it to support slice operations over the elements
# in the collection
elif isinstance(key, slice):
return ElementCollection(self._elements[key])
# If it's anything else (basically a string) we use it as a query selector.
return self.find(key)
def __iter__(self):
yield from self._elements
def __len__(self):
return len(self._elements)
def __repr__(self):
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 that is not
# yet supported in our build of MicroPython. Instead, we handle it here by
# using super for all "private" attributes (those starting with an underscore).
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):
if index is None:
return [getattr(el, attr) for el in self._elements]
# As JQuery, when getting an attr, only return it for the first element
return getattr(self._elements[index], attr)
def _set_attribute(self, attr, value):
for el in self._elements:
setattr(el, attr, value)
# fmt: off
ELEMENT_CLASSES = [
a, abbr, address, area, article, aside, audio,
@@ -1092,7 +1130,47 @@ ELEMENT_CLASSES = [
# fmt: on
# Lookup table to get an element class by its tag name.
ELEMENT_CLASSES_BY_TAG_NAME = {
cls.__name__.replace("_", ""): cls for cls in ELEMENT_CLASSES
}
# Register all the default (aka "built-in") Element classes.
Element.register_element_classes(ELEMENT_CLASSES)
class Page:
"""Represents the whole page."""
def __init__(self):
self.html = Element.wrap_dom_element(document.documentElement)
self.body = Element.wrap_dom_element(document.body)
self.head = Element.wrap_dom_element(document.head)
def __getitem__(self, selector):
"""Get an item on the page.
We don't index/slice the page like we do with `Element` and `ElementCollection`
as it is a bit muddier what the ideal behavior should be. Instead, we simply
use this as a convenience method to `find` elements on the page.
"""
return self.find(selector)
@property
def title(self):
"""Return the page title."""
return document.title
@title.setter
def title(self, value):
"""Set the page title."""
document.title = value
def append(self, *items):
"""Shortcut for `page.body.append`."""
self.body.append(*items)
def find(self, selector): # NOQA
"""Find all elements that match the specified selector.
Return the results as a (possibly empty) `ElementCollection`.
"""
return ElementCollection.wrap_dom_elements(document.querySelectorAll(selector))
page = Page()

View File

@@ -1,22 +0,0 @@
from pyscript import document
from pyscript.web.elements import Element, ElementCollection
class DOM:
def __init__(self):
self.body = Element.from_dom_element(document.body)
self.head = Element.from_dom_element(document.head)
def __getitem__(self, selector):
return self.find(selector)
def find(self, selector):
return ElementCollection(
[
Element.from_dom_element(dom_element)
for dom_element in document.querySelectorAll(selector)
]
)
dom = DOM()

View File

@@ -1,13 +1,11 @@
from pyscript import document, when
from pyscript.web import dom
from pyscript.web import elements as el
from pyscript.web.elements import ElementCollection
from pyscript.web import Element, ElementCollection, div, p, page
class TestDocument:
def test__element(self):
assert dom.body._dom_element == document.body
assert dom.head._dom_element == document.head
assert page.body._dom_element == document.body
assert page.head._dom_element == document.head
def test_getitem_by_id():
@@ -16,13 +14,13 @@ def test_getitem_by_id():
txt = "You found test_id_selector"
selector = f"#{id_}"
# EXPECT the element to be found by id
result = dom.find(selector)
result = page.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.innerHTML == txt
# EXPECT the results to be of the right types
assert isinstance(div, el.Element)
assert isinstance(div, Element)
assert isinstance(result, ElementCollection)
@@ -33,8 +31,7 @@ def test_getitem_by_class():
"test_selector_w_children_child_1",
]
expected_class = "a-test-class"
result = dom.find(f".{expected_class}")
div = result[0]
result = page.find(f".{expected_class}")
# EXPECT to find exact number of elements with the class in the page (== 3)
assert len(result) == 3
@@ -44,7 +41,7 @@ def test_getitem_by_class():
def test_read_n_write_collection_elements():
elements = dom.find(".multi-elems")
elements = page.find(".multi-elems")
for element in elements:
assert element.innerHTML == f"Content {element.id.replace('#', '')}"
@@ -59,15 +56,15 @@ 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 = dom.find(f"#{id_}")[0]
parent_div = page.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 el.Element
assert isinstance(div, el.Element)
# EXPECT the new element to be an Element
assert isinstance(div, Element)
# EXPECT the div attributes to be == to how they are configured in the page
assert div.innerHTML == "Child 1"
assert div.id == "test_selector_w_children_child_1"
@@ -76,8 +73,8 @@ class TestElement:
# GIVEN 2 different Elements pointing to the same underlying element
id_ = "test_id_selector"
selector = f"#{id_}"
div = dom.find(selector)[0]
div2 = dom.find(selector)[0]
div = page.find(selector)[0]
div2 = page.find(selector)[0]
# EXPECT them to be equal
assert div == div2
@@ -92,27 +89,27 @@ class TestElement:
def test_append_element(self):
id_ = "element-append-tests"
div = dom.find(f"#{id_}")[0]
div = page.find(f"#{id_}")[0]
len_children_before = len(div.children)
new_el = el.p("new element")
new_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_dom_element_element(self):
id_ = "element-append-tests"
div = dom.find(f"#{id_}")[0]
div = page.find(f"#{id_}")[0]
len_children_before = len(div.children)
new_el = el.p("new element")
new_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 = dom.find(f"#{id_}")[0]
div = page.find(f"#{id_}")[0]
len_children_before = len(div.children)
collection = dom.find(".collection")
collection = page.find(".collection")
div.append(collection)
assert len(div.children) == len_children_before + len(collection)
@@ -122,16 +119,16 @@ class TestElement:
def test_read_classes(self):
id_ = "test_class_selector"
expected_class = "a-test-class"
div = dom.find(f"#{id_}")[0]
div = page.find(f"#{id_}")[0]
assert div.classes == [expected_class]
def test_add_remove_class(self):
id_ = "div-no-classes"
classname = "tester-class"
div = dom.find(f"#{id_}")[0]
div = page.find(f"#{id_}")[0]
assert not div.classes
div.classes.add(classname)
same_div = dom.find(f"#{id_}")[0]
same_div = page.find(f"#{id_}")[0]
assert div.classes == [classname] == same_div.classes
div.classes.remove(classname)
assert div.classes == [] == same_div.classes
@@ -139,7 +136,7 @@ class TestElement:
def test_when_decorator(self):
called = False
just_a_button = dom.find("#a-test-button")[0]
just_a_button = page.find("#a-test-button")[0]
@when("click", just_a_button)
def on_click(event):
@@ -155,7 +152,7 @@ class TestElement:
def test_inner_html_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = dom.find("#element_attribute_tests")[0]
div = page.find("#element_attribute_tests")[0]
# WHEN we set the html attribute
div.innerHTML = "<b>New Content</b>"
@@ -167,7 +164,7 @@ class TestElement:
def test_text_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = dom.find("#element_attribute_tests")[0]
div = page.find("#element_attribute_tests")[0]
# WHEN we set the html attribute
div.textContent = "<b>New Content</b>"
@@ -184,12 +181,12 @@ class TestElement:
class TestCollection:
def test_iter_eq_children(self):
elements = dom.find(".multi-elems")
assert [el for el in elements] == [el for el in elements.children]
elements = page.find(".multi-elems")
assert [el for el in elements] == [el for el in elements.elements]
assert len(elements) == 3
def test_slices(self):
elements = dom.find(".multi-elems")
elements = page.find(".multi-elems")
assert elements[0]
_slice = elements[:2]
assert len(_slice) == 2
@@ -199,26 +196,26 @@ class TestCollection:
def test_style_rule(self):
selector = ".multi-elems"
elements = dom.find(selector)
elements = page.find(selector)
for el in elements:
assert el.style["background-color"] != "red"
elements.style["background-color"] = "red"
for i, el in enumerate(dom.find(selector)):
for i, el in enumerate(page.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(dom.find(selector)):
for i, el in enumerate(page.find(selector)):
assert el.style["background-color"] != "red"
assert elements[i].style["background-color"] != "red"
def test_when_decorator(self):
called = False
buttons_collection = dom.find("button")
buttons_collection = page.find("button")
@when("click", buttons_collection)
def on_click(event):
@@ -236,34 +233,33 @@ class TestCollection:
class TestCreation:
def test_create_document_element(self):
# TODO: This test should probably be removed since it's testing the elements module
new_el = el.div("new element")
# TODO: This test should probably be removed since it's testing the elements
# module.
new_el = div("new element")
new_el.id = "new_el_id"
assert isinstance(new_el, el.Element)
assert isinstance(new_el, Element)
assert new_el._dom_element.tagName == "DIV"
# EXPECT the new element to be associated with the document
assert new_el.parent == None
dom.body.append(new_el)
assert new_el.parent is None
page.body.append(new_el)
assert dom.find("#new_el_id")[0].parent == dom.body
assert page.find("#new_el_id")[0].parent == page.body
def test_create_element_child(self):
selector = "#element-creation-test"
parent_div = dom.find(selector)[0]
parent_div = page.find(selector)[0]
# Creating an element from another element automatically creates that element
# as a child of the original element
new_el = el.p(
"a div", classes=["code-description"], innerHTML="Ciao PyScripters!"
)
new_el = p("a div", classes=["code-description"], innerHTML="Ciao PyScripters!")
parent_div.append(new_el)
assert isinstance(new_el, el.Element)
assert isinstance(new_el, Element)
assert new_el._dom_element.tagName == "P"
# EXPECT the new element to be associated with the document
assert new_el.parent == parent_div
assert dom.find(selector)[0].children[0] == new_el
assert page.find(selector)[0].children[0] == new_el
class TestInput:
@@ -277,7 +273,7 @@ class TestInput:
def test_value(self):
for id_ in self.input_ids:
expected_type = id_.split("_")[-1]
result = dom.find(f"#{id_}")
result = page.find(f"#{id_}")
input_el = result[0]
assert input_el._dom_element.type == expected_type
assert input_el.value == f"Content {id_}" == input_el._dom_element.value
@@ -295,7 +291,7 @@ class TestInput:
def test_set_value_collection(self):
for id_ in self.input_ids:
input_el = dom.find(f"#{id_}")
input_el = page.find(f"#{id_}")
assert input_el.value[0] == f"Content {id_}" == input_el[0].value
@@ -308,30 +304,30 @@ class TestInput:
# actually on the class. Maybe a job for __setattr__?
#
# def test_element_without_value(self):
# result = dom.find(f"#tests-terminal"][0]
# result = page.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"]
# result = page.find(f"#tests-terminal"]
# with pytest.raises(AttributeError):
# result.value = "some value"
class TestSelect:
def test_select_options_iter(self):
select = dom.find(f"#test_select_element_w_options")[0]
select = page.find(f"#test_select_element_w_options")[0]
for i, option in enumerate(select.options, 1):
assert option.value == f"{i}"
assert option.innerHTML == f"Option {i}"
def test_select_options_len(self):
select = dom.find(f"#test_select_element_w_options")[0]
select = page.find(f"#test_select_element_w_options")[0]
assert len(select.options) == 2
def test_select_options_clear(self):
select = dom.find(f"#test_select_element_to_clear")[0]
select = page.find(f"#test_select_element_to_clear")[0]
assert len(select.options) == 3
select.options.clear()
@@ -340,7 +336,7 @@ class TestSelect:
def test_select_element_add(self):
# GIVEN the existing select element with no options
select = dom.find(f"#test_select_element")[0]
select = page.find(f"#test_select_element")[0]
# EXPECT the select element to have no options
assert len(select.options) == 0
@@ -431,7 +427,7 @@ class TestSelect:
def test_select_options_remove(self):
# GIVEN the existing select element with 3 options
select = dom.find(f"#test_select_element_to_remove")[0]
select = page.find(f"#test_select_element_to_remove")[0]
# EXPECT the select element to have 3 options
assert len(select.options) == 4
@@ -453,7 +449,7 @@ class TestSelect:
def test_select_get_selected_option(self):
# GIVEN the existing select element with one selected option
select = dom.find(f"#test_select_element_w_options")[0]
select = page.find(f"#test_select_element_w_options")[0]
# WHEN we get the selected option
selected_option = select.options.selected

View File

@@ -101,11 +101,9 @@ class TestElements(PyScriptTest):
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript.web import dom
from pyscript.web.elements import {el_type}
from pyscript.web import page, {el_type}
el = {el_type}({attributes})
dom.body.append(el)
page.body.append(el)
</script>
"""
self.pyscript_run(code_)
@@ -620,13 +618,12 @@ class TestElements(PyScriptTest):
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript.web import dom
from pyscript.web.elements import div, p
from pyscript.web import page, div, p
el = div("{div_text_content}")
child = p('{p_text_content}')
el.append(child)
dom.body.append(el)
page.body.append(el)
</script>
"""
self.pyscript_run(code_)
@@ -664,14 +661,13 @@ class TestElements(PyScriptTest):
from pyscript import when
<script type="{interpreter}">
from pyscript import document
from pyscript.web import dom
from pyscript.web.elements import div, p
from pyscript.web import page, div, p
el = div("{div_text_content}")
child = document.createElement('P')
child.textContent = '{p_text_content}'
el.append(child)
dom.body.append(el)
page.body.append(el)
</script>
"""
self.pyscript_run(code_)
@@ -709,15 +705,14 @@ class TestElements(PyScriptTest):
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript.web import dom
from pyscript.web.elements import div, p, ElementCollection
from pyscript.web import page, div, p, ElementCollection
el = div("{div_text_content}")
child1 = p('{p_text_content}')
child2 = p('{p2_text_content}', id='child2')
collection = ElementCollection([child1, child2])
el.append(collection)
dom.body.append(el)
page.body.append(el)
</script>
"""
self.pyscript_run(code_)
@@ -765,20 +760,19 @@ class TestElements(PyScriptTest):
from pyscript import when
<script type="{interpreter}">
from pyscript import document
from pyscript.web import dom
from pyscript.web.elements import div, p, ElementCollection
from pyscript.web import page, div, p, ElementCollection
el = div("{div_text_content}")
child1 = p('{p_text_content}')
child2 = p('{p2_text_content}', id='child2')
dom.body.append(child1)
dom.body.append(child2)
page.body.append(child1)
page.body.append(child2)
nodes = document.querySelectorAll('p')
el.append(nodes)
dom.body.append(el)
page.body.append(el)
</script>
"""
self.pyscript_run(code_)

View File

@@ -9,10 +9,7 @@ declare namespace _default {
"magic_js.py": string;
"storage.py": string;
"util.py": string;
web: {
"__init__.py": string;
"elements.py": string;
};
"web.py": string;
"websocket.py": string;
"workers.py": string;
};