mirror of
https://github.com/pyscript/pyscript.git
synced 2026-02-12 22:00:56 -05:00
[NEXT] Pydom (#1681)
* add pyweb * build * add test file * fix pydom example code * remove old reference to js * temporarily comment out query functions on BaseElement while rearranging code to reuse the same underlying logic accross PyDom and other elements * add temp TODO comment to content as it breaks with template elements * update pydom example to define code on external file * fix name type while renaming document -> dom * add real pydom test files * add classes to dom scope * __len__ to ElementCollection * fix some of the old tests * rename test from test_query_by_class to test_getitem_by_class * change test for read and write multiple elements * add find method to BaseElement * fix remaining tests * add Collection Tests * add equality to Collection * add test for collection style manipulation * fix getter for style property and rename style related attribute from pop to remove * add single element creation test * remove append on BaseElement and add body and head to dom * add test_create_element_child to verify child creation * add children getter property to Element * remove old code * remove more old code, change style attribute from visibility to visible and now default getters on collection to return a list with the value of an attribute for every element in the collection * remove more old code and add possibility to customize test flags via url * add support to pass Js and pydom.Element elements to when decorator * remove methods related to input type of elements until we have a better design for it * rename _element to _js * add test_when decorator with a ElementCollection input * when decorator now supporte pydom.ElementCollection as input * update pyscript.js * remove useless variable from when decorator * remove base.py from pyweb * add nodes for append collection test and add better feedback on successes vs failure * add tests and fix code for support of append Element and ElementCollection * manage access to content attribute when tagname is template * fix comment --------- Co-authored-by: Fabio Pliger <fpliger@anaconda.com>
This commit is contained in:
@@ -11,7 +11,23 @@ def when(event_type=None, selector=None):
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
elements = document.querySelectorAll(selector)
|
||||
if isinstance(selector, str):
|
||||
elements = document.querySelectorAll(selector)
|
||||
else:
|
||||
# TODO: This is a hack that will be removed when pyscript becomes a package
|
||||
# and we can better manage the imports without circular dependencies
|
||||
from pyweb import pydom
|
||||
|
||||
if isinstance(selector, pydom.Element):
|
||||
elements = [selector._js]
|
||||
elif isinstance(selector, pydom.ElementCollection):
|
||||
elements = [el._js for el in selector]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid selector: {selector}. Selector must"
|
||||
" be a string, a pydom.Element or a pydom.ElementCollection."
|
||||
)
|
||||
|
||||
sig = inspect.signature(func)
|
||||
# Function doesn't receive events
|
||||
if not sig.parameters:
|
||||
|
||||
File diff suppressed because one or more lines are too long
314
pyscript.core/src/stdlib/pyweb/pydom.py
Normal file
314
pyscript.core/src/stdlib/pyweb/pydom.py
Normal file
@@ -0,0 +1,314 @@
|
||||
import sys
|
||||
import warnings
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
from pyodide.ffi import JsProxy
|
||||
from pyscript import display, document, window
|
||||
|
||||
# from pyscript import when as _when
|
||||
|
||||
alert = window.alert
|
||||
|
||||
|
||||
class BaseElement:
|
||||
def __init__(self, js_element):
|
||||
self._js = js_element
|
||||
self._parent = None
|
||||
self.style = StyleProxy(self)
|
||||
|
||||
def __eq__(self, obj):
|
||||
"""Check if the element is the same as the other element by comparing
|
||||
the underlying JS element"""
|
||||
return isinstance(obj, BaseElement) and obj._js == self._js
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
if self._parent:
|
||||
return self._parent
|
||||
|
||||
if self._js.parentElement:
|
||||
self._parent = self.__class__(self._js.parentElement)
|
||||
|
||||
return self._parent
|
||||
|
||||
@property
|
||||
def __class(self):
|
||||
return self.__class__ if self.__class__ != PyDom else Element
|
||||
|
||||
def create(self, type_, is_child=True, classes=None, html=None, label=None):
|
||||
js_el = document.createElement(type_)
|
||||
element = self.__class(js_el)
|
||||
|
||||
if classes:
|
||||
for class_ in classes:
|
||||
element.add_class(class_)
|
||||
|
||||
if html is not None:
|
||||
element.html = html
|
||||
|
||||
if label is not None:
|
||||
element.label = label
|
||||
|
||||
if is_child:
|
||||
self.append(element)
|
||||
|
||||
return element
|
||||
|
||||
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
|
||||
"""
|
||||
elements = self._js.querySelectorAll(selector)
|
||||
if not elements:
|
||||
return None
|
||||
return ElementCollection([Element(el) for el in elements])
|
||||
|
||||
|
||||
class Element(BaseElement):
|
||||
@property
|
||||
def children(self):
|
||||
return [self.__class__(el) for el in self._js.children]
|
||||
|
||||
def append(self, child):
|
||||
# TODO: this is Pyodide specific for now!!!!!!
|
||||
# if we get passed a JSProxy Element directly we just map it to the
|
||||
# higher level Python element
|
||||
if isinstance(child, JsProxy):
|
||||
return self.append(Element(child))
|
||||
|
||||
elif isinstance(child, Element):
|
||||
self._js.appendChild(child._js)
|
||||
|
||||
return child
|
||||
|
||||
elif isinstance(child, ElementCollection):
|
||||
for el in child:
|
||||
self.append(el)
|
||||
|
||||
# -------- Pythonic Interface to Element -------- #
|
||||
@property
|
||||
def html(self):
|
||||
return self._js.innerHTML
|
||||
|
||||
@html.setter
|
||||
def html(self, value):
|
||||
self._js.innerHTML = value
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
# TODO: This breaks with with standard template elements. Define how to best
|
||||
# handle this specifica use case. Just not support for now?
|
||||
if self._js.tagName == "TEMPLATE":
|
||||
warnings.warn(
|
||||
"Content attribute not supported for template elements.", stacklevel=2
|
||||
)
|
||||
return None
|
||||
return self._js.innerHTML
|
||||
|
||||
@content.setter
|
||||
def content(self, value):
|
||||
# TODO: (same comment as above)
|
||||
if self._js.tagName == "TEMPLATE":
|
||||
warnings.warn(
|
||||
"Content attribute not supported for template elements.", stacklevel=2
|
||||
)
|
||||
return
|
||||
|
||||
display(value, target=self.id)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._js.id
|
||||
|
||||
@id.setter
|
||||
def id(self, value):
|
||||
self._js.id = value
|
||||
|
||||
def clone(self, new_id=None):
|
||||
clone = Element(self._js.cloneNode(True))
|
||||
clone.id = new_id
|
||||
|
||||
return clone
|
||||
|
||||
def remove_class(self, classname):
|
||||
classList = self._js.classList
|
||||
if isinstance(classname, list):
|
||||
classList.remove(*classname)
|
||||
else:
|
||||
classList.remove(classname)
|
||||
return self
|
||||
|
||||
def add_class(self, classname):
|
||||
classList = self._js.classList
|
||||
if isinstance(classname, list):
|
||||
classList.add(*classname)
|
||||
else:
|
||||
self._js.classList.add(classname)
|
||||
return self
|
||||
|
||||
@property
|
||||
def classes(self):
|
||||
classes = self._js.classList.values()
|
||||
return [x for x in classes]
|
||||
|
||||
def show_me(self):
|
||||
self._js.scrollIntoView()
|
||||
|
||||
def when(self, event, handler):
|
||||
document.when(event, selector=self)(handler)
|
||||
|
||||
|
||||
class StyleProxy(dict):
|
||||
def __init__(self, element: Element) -> None:
|
||||
self._element = element
|
||||
|
||||
@cached_property
|
||||
def _style(self):
|
||||
return self._element._js.style
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._style.getPropertyValue(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._style.setProperty(key, value)
|
||||
|
||||
def remove(self, key):
|
||||
self._style.removeProperty(key)
|
||||
|
||||
def set(self, **kws):
|
||||
for k, v in kws.items():
|
||||
self._element._js.style.setProperty(k, v)
|
||||
|
||||
# CSS Properties
|
||||
# Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2
|
||||
# Following prperties automatically generated from the above reference using
|
||||
# tools/codegen_css_proxy.py
|
||||
@property
|
||||
def visible(self):
|
||||
return self._element._js.style.visibility
|
||||
|
||||
@visible.setter
|
||||
def visible(self, value):
|
||||
self._element._js.style.visibility = value
|
||||
|
||||
|
||||
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 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.style = StyleCollection(self)
|
||||
|
||||
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 selector
|
||||
# TODO: Write tests!
|
||||
elements = self._element.querySelectorAll(key)
|
||||
return ElementCollection([Element(el) for el in elements])
|
||||
|
||||
def __len__(self):
|
||||
return len(self._elements)
|
||||
|
||||
def __eq__(self, obj):
|
||||
"""Check if the element is the same as the other element by comparing
|
||||
the underlying JS element"""
|
||||
return isinstance(obj, ElementCollection) and obj._elements == self._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)
|
||||
|
||||
@property
|
||||
def html(self):
|
||||
return self._get_attribute("html")
|
||||
|
||||
@html.setter
|
||||
def html(self, value):
|
||||
self._set_attribute("html", value)
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self._elements
|
||||
|
||||
def __iter__(self):
|
||||
yield from self._elements
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}"
|
||||
|
||||
|
||||
class DomScope:
|
||||
def __getattr__(self, __name: str) -> Any:
|
||||
element = document[f"#{__name}"]
|
||||
if element:
|
||||
return element[0]
|
||||
|
||||
|
||||
class PyDom(BaseElement):
|
||||
# Add objects we want to expose to the DOM namespace since this class instance is being
|
||||
# remapped as "the module" itself
|
||||
BaseElement = BaseElement
|
||||
Element = Element
|
||||
ElementCollection = ElementCollection
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(document)
|
||||
self.ids = DomScope()
|
||||
self.body = Element(document.body)
|
||||
self.head = Element(document.head)
|
||||
|
||||
def create(self, type_, parent=None, classes=None, html=None):
|
||||
return super().create(type_, is_child=False)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, int):
|
||||
indices = range(*key.indices(len(self.list)))
|
||||
return [self.list[i] for i in indices]
|
||||
|
||||
elements = self._js.querySelectorAll(key)
|
||||
if not elements:
|
||||
return None
|
||||
return ElementCollection([Element(el) for el in elements])
|
||||
|
||||
|
||||
dom = PyDom()
|
||||
|
||||
sys.modules[__name__] = dom
|
||||
19
pyscript.core/test/pydom.html
Normal file
19
pyscript.core/test/pydom.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Next Plugin</title>
|
||||
<link rel="stylesheet" href="../dist/core.css">
|
||||
<script type="module" src="../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="pydom.py"></script>
|
||||
|
||||
<button id="just-a-button">Click For Time</button>
|
||||
<button id="color-button">Click For Color</button>
|
||||
<button id="color-reset-button">Reset Color</button>
|
||||
|
||||
<div id="result"></div>
|
||||
</body>
|
||||
</html>
|
||||
26
pyscript.core/test/pydom.py
Normal file
26
pyscript.core/test/pydom.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import random
|
||||
from pyscript import display
|
||||
from pyweb import pydom
|
||||
from pyweb.base import when
|
||||
from datetime import datetime as dt
|
||||
|
||||
|
||||
@when("click", "#just-a-button")
|
||||
def on_click(event):
|
||||
print(f"Hello from Python! {dt.now()}")
|
||||
display(f"Hello from Python! {dt.now()}", append=False, target="result")
|
||||
|
||||
|
||||
@when("click", "#color-button")
|
||||
def on_color_click(event):
|
||||
print("1")
|
||||
btn = pydom["#result"]
|
||||
print("2")
|
||||
btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}"
|
||||
|
||||
|
||||
def reset_color():
|
||||
pydom["#result"].style["background-color"] = "white"
|
||||
|
||||
|
||||
# btn_reset = pydom["#color-reset-button"][0].when('click', reset_color)
|
||||
111
pyscript.core/test/pyscript_dom/index.html
Normal file
111
pyscript.core/test/pyscript_dom/index.html
Normal file
@@ -0,0 +1,111 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>PyperCard PyTest Suite</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css?family=Roboto:100,400");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*:before, *:after {
|
||||
box-sizing: inherit;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace;
|
||||
font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; line-height: 20px;
|
||||
}
|
||||
|
||||
h1 { font-size: 24px; font-weight: 700; line-height: 26.4px; }
|
||||
h2 { font-size: 14px; font-weight: 700; line-height: 15.4px; }
|
||||
|
||||
#tests-terminal{
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="run_tests.py" config="tests.toml"></script>
|
||||
|
||||
<h1>pyscript.dom Tests</h1>
|
||||
<p>You can pass test parameters to this test suite by passing them as query params on the url.
|
||||
For instance, to pass "-v -s --pdb" to pytest, you would use the following url:
|
||||
<label style="color: blue">?-v&-s&--pdb</label>
|
||||
</p>
|
||||
<div id="tests-terminal"></div>
|
||||
|
||||
<template id="test_card_with_element_template">
|
||||
<p>This is a test. {foo}</p>
|
||||
</template>
|
||||
|
||||
<div id="test_id_selector" style="visibility: hidden;">You found test_id_selector</div>
|
||||
<div id="test_class_selector" class="a-test-class" style="visibility: hidden;">You found test_class_selector</div>
|
||||
<div id="test_selector_w_children" class="a-test-class" style="visibility: hidden;">
|
||||
<div id="test_selector_w_children_child_1" class="a-test-class" style="visibility: hidden;">Child 1</div>
|
||||
<div id="test_selector_w_children_child_2" style="visibility: hidden;">Child 2</div>
|
||||
</div>
|
||||
|
||||
<div id="div-no-classes"></div>
|
||||
|
||||
<div style="visibility: hidden;">
|
||||
<h2>Test Read and Write</h2>
|
||||
<div id="test_rr_div">Content test_rr_div</div>
|
||||
<h3 id="test_rr_h3">Content test_rr_h3</h3>
|
||||
|
||||
<div id="multi-elem-div" class="multi-elems">Content multi-elem-div</div>
|
||||
<p id="multi-elem-p" class="multi-elems">Content multi-elem-p</p>
|
||||
<h2 id="multi-elem-h2" class="multi-elems">Content multi-elem-h2</h2>
|
||||
|
||||
<form>
|
||||
<input id="test_rr_input_txt" type="text" value="Content test_rr_input_txt">
|
||||
<input id="test_rr_input_btn" type="button" value="Content test_rr_input_btn">
|
||||
<input id="test_rr_input_email" type="email" value="Content test_rr_input_email">
|
||||
<input id="test_rr_input_password" type="password" value="Content test_rr_input_password">
|
||||
</form>
|
||||
|
||||
<div id="element-creation-test"></div>
|
||||
|
||||
<button id="a-test-button">I'm a button to be clicked</button>
|
||||
<button>I'm another button you can click</button>
|
||||
<button id="a-third-button">2 is better than 3 :)</button>
|
||||
|
||||
<div id="element-append-tests"></div>
|
||||
<p class="collection"></p>
|
||||
<div class="collection"></div>
|
||||
<h3 class="collection"></h3>
|
||||
</div>
|
||||
|
||||
|
||||
<script defer>
|
||||
console.log("remapping console.log")
|
||||
const terminalDiv = document.getElementById("tests-terminal");
|
||||
const log = console.log.bind(console)
|
||||
let testsStarted = false;
|
||||
console.log = (...args) => {
|
||||
log("---IN---");
|
||||
let txt = args.join(" ");
|
||||
let token = "<br>";
|
||||
if (txt.endsWith("FAILED"))
|
||||
token = " ❌<br>";
|
||||
else if (txt.endsWith("PASSED"))
|
||||
token = " ✅<br>";
|
||||
if (testsStarted)
|
||||
terminalDiv.innerHTML += args.join(" ") + token;
|
||||
|
||||
log(...args)
|
||||
|
||||
// if we got the flag that tests are starting, then we can start logging
|
||||
if (args.join(" ") == "tests starting")
|
||||
testsStarted = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
7
pyscript.core/test/pyscript_dom/run_tests.py
Normal file
7
pyscript.core/test/pyscript_dom/run_tests.py
Normal file
@@ -0,0 +1,7 @@
|
||||
print("tests starting")
|
||||
import pytest
|
||||
from pyscript import window
|
||||
|
||||
args = window.location.search.replace("?", "").split("&")
|
||||
|
||||
pytest.main(args)
|
||||
8
pyscript.core/test/pyscript_dom/tests.toml
Normal file
8
pyscript.core/test/pyscript_dom/tests.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
packages = [
|
||||
"pytest"
|
||||
]
|
||||
|
||||
[[fetch]]
|
||||
from = "tests/"
|
||||
files = ["__init__.py", "conftest.py", "test_dom.py"]
|
||||
to_folder = "tests"
|
||||
0
pyscript.core/test/pyscript_dom/tests/__init__.py
Normal file
0
pyscript.core/test/pyscript_dom/tests/__init__.py
Normal file
15
pyscript.core/test/pyscript_dom/tests/conftest.py
Normal file
15
pyscript.core/test/pyscript_dom/tests/conftest.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import pytest
|
||||
from js import document, localStorage
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def before_tests():
|
||||
"""
|
||||
Ensure browser storage is always reset to empty. Remove the app
|
||||
placeholder. Reset the page title.
|
||||
"""
|
||||
localStorage.clear()
|
||||
# app_placeholder = document.querySelector("pyper-app")
|
||||
# if app_placeholder:
|
||||
# app_placeholder.remove()
|
||||
document.querySelector("title").innerText = "Web API PyTest Suite"
|
||||
246
pyscript.core/test/pyscript_dom/tests/test_dom.py
Normal file
246
pyscript.core/test/pyscript_dom/tests/test_dom.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import pytest
|
||||
from pyscript import document, when
|
||||
from unittest import mock
|
||||
|
||||
from pyweb import pydom
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_getitem_by_id():
|
||||
# GIVEN an existing element on the page with a known text content
|
||||
id_ = "test_id_selector"
|
||||
txt = "You found test_id_selector"
|
||||
selector = f"#{id_}"
|
||||
# EXPECT the element to be found by id
|
||||
result = pydom[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
|
||||
# EXPECT the results to be of the right types
|
||||
assert isinstance(div, pydom.BaseElement)
|
||||
assert isinstance(result, pydom.ElementCollection)
|
||||
|
||||
|
||||
def test_getitem_by_class():
|
||||
ids = [
|
||||
"test_class_selector",
|
||||
"test_selector_w_children",
|
||||
"test_selector_w_children_child_1",
|
||||
]
|
||||
expected_class = "a-test-class"
|
||||
result = pydom[f".{expected_class}"]
|
||||
div = result[0]
|
||||
|
||||
# EXPECT to find exact number of elements with the class in the page (== 3)
|
||||
assert len(result) == 3
|
||||
|
||||
# EXPECT that all element ids are in the expected list
|
||||
assert [el.id for el in result] == ids
|
||||
|
||||
|
||||
def test_read_n_write_collection_elements():
|
||||
elements = pydom[".multi-elems"]
|
||||
|
||||
for element in elements:
|
||||
assert element.html == f"Content {element.id.replace('#', '')}"
|
||||
|
||||
new_content = "New Content"
|
||||
elements.html = new_content
|
||||
for element in elements:
|
||||
assert element.html == 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]
|
||||
|
||||
# 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 div attributes to be == to how they are configured in the page
|
||||
assert div.html == "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]
|
||||
|
||||
# EXPECT them to be equal
|
||||
assert div == div2
|
||||
# EXPECT them to be different objects
|
||||
assert div is not div2
|
||||
|
||||
# EXPECT their value to always be equal
|
||||
assert div.html == div2.html
|
||||
div.html = "some value"
|
||||
|
||||
assert div.html == div2.html == "some value"
|
||||
|
||||
def test_append_element(self):
|
||||
id_ = "element-append-tests"
|
||||
div = pydom[f"#{id_}"][0]
|
||||
len_children_before = len(div.children)
|
||||
new_el = div.create("p")
|
||||
div.append(new_el)
|
||||
assert len(div.children) == len_children_before + 1
|
||||
assert div.children[-1] == new_el
|
||||
|
||||
def test_append_js_element(self):
|
||||
id_ = "element-append-tests"
|
||||
div = pydom[f"#{id_}"][0]
|
||||
len_children_before = len(div.children)
|
||||
new_el = div.create("p")
|
||||
div.append(new_el._js)
|
||||
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]
|
||||
len_children_before = len(div.children)
|
||||
collection = pydom[".collection"]
|
||||
div.append(collection)
|
||||
assert len(div.children) == len_children_before + len(collection)
|
||||
|
||||
for i in range(len(collection)):
|
||||
assert div.children[-1 - i] == collection[-1 - i]
|
||||
|
||||
def test_read_classes(self):
|
||||
id_ = "test_class_selector"
|
||||
expected_class = "a-test-class"
|
||||
div = pydom[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]
|
||||
assert not div.classes
|
||||
div.add_class(classname)
|
||||
same_div = pydom[f"#{id_}"][0]
|
||||
assert div.classes == [classname] == same_div.classes
|
||||
div.remove_class(classname)
|
||||
assert div.classes == [] == same_div.classes
|
||||
|
||||
def test_when_decorator(self):
|
||||
called = False
|
||||
|
||||
just_a_button = pydom["#a-test-button"][0]
|
||||
|
||||
@when("click", just_a_button)
|
||||
def on_click(event):
|
||||
nonlocal called
|
||||
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
|
||||
assert not called
|
||||
just_a_button._js.click()
|
||||
|
||||
assert called
|
||||
|
||||
|
||||
class TestCollection:
|
||||
def test_iter_eq_children(self):
|
||||
elements = pydom[".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"]
|
||||
assert elements[0]
|
||||
_slice = elements[:2]
|
||||
assert len(_slice) == 2
|
||||
for i, el in enumerate(_slice):
|
||||
assert el == elements[i]
|
||||
assert elements[:] == elements
|
||||
|
||||
def test_style_rule(self):
|
||||
selector = ".multi-elems"
|
||||
elements = pydom[selector]
|
||||
for el in elements:
|
||||
assert el.style["background-color"] != "red"
|
||||
|
||||
elements.style["background-color"] = "red"
|
||||
|
||||
for i, el in enumerate(pydom[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]):
|
||||
assert el.style["background-color"] != "red"
|
||||
assert elements[i].style["background-color"] != "red"
|
||||
|
||||
def test_when_decorator(self):
|
||||
called = False
|
||||
|
||||
buttons_collection = pydom["button"]
|
||||
|
||||
@when("click", buttons_collection)
|
||||
def on_click(event):
|
||||
nonlocal called
|
||||
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
|
||||
assert not called
|
||||
for button in buttons_collection:
|
||||
button._js.click()
|
||||
assert called
|
||||
called = False
|
||||
|
||||
|
||||
class TestCreation:
|
||||
def test_create_document_element(self):
|
||||
new_el = pydom.create("div")
|
||||
new_el.id = "new_el_id"
|
||||
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
|
||||
pydom.body.append(new_el)
|
||||
|
||||
assert pydom["#new_el_id"][0].parent == pydom.body
|
||||
|
||||
def test_create_element_child(self):
|
||||
selector = "#element-creation-test"
|
||||
parent_div = pydom[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!"
|
||||
)
|
||||
|
||||
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
|
||||
3
pyscript.core/types/stdlib/pyscript.d.ts
vendored
3
pyscript.core/types/stdlib/pyscript.d.ts
vendored
@@ -5,5 +5,8 @@ declare const _default: {
|
||||
"event_handling.py": string;
|
||||
};
|
||||
"pyscript.py": string;
|
||||
pyweb: {
|
||||
"pydom.py": string;
|
||||
};
|
||||
};
|
||||
export default _default;
|
||||
|
||||
Reference in New Issue
Block a user