mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Refactor @when and add Event (#2239)
* Add two unit tests for illustrative purposes. * Radical simplification of @when, more tests and some minor refactoring. Handle ElementCollections, tests for ElementCollection, make serve for running tests locally. * Skip flakey Pyodide in worker test (it works 50/50 and appears to be a timing issue). * Ensure onFOO relates to an underlying FOO event in an Element. * Minor comment cleanup. * Add async test for Event listeners. * Handlers no longer require an event parameter. * Add tests for async handling via when. * Docstring cleanup. * Refactor onFOO to on_FOO. * Minor typo tidy ups. * Use correct check for MicroPython. --------- Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
This commit is contained in:
committed by
GitHub
parent
4ff02a24d1
commit
56c64cbee7
4
Makefile
4
Makefile
@@ -70,6 +70,10 @@ precommit-check:
|
|||||||
test:
|
test:
|
||||||
cd core && npm run test:integration
|
cd core && npm run test:integration
|
||||||
|
|
||||||
|
# Serve the repository with the correct headers.
|
||||||
|
serve:
|
||||||
|
npx mini-coi .
|
||||||
|
|
||||||
# Format the code.
|
# Format the code.
|
||||||
fmt: fmt-py
|
fmt: fmt-py
|
||||||
@echo "Format completed"
|
@echo "Format completed"
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
|
|||||||
to learn about our development process, reporting bugs and improvements,
|
to learn about our development process, reporting bugs and improvements,
|
||||||
creating issues and asking questions.
|
creating issues and asking questions.
|
||||||
|
|
||||||
Check out the [developing process](https://docs.pyscript.net/latest/developers/)
|
Check out the [development process](https://docs.pyscript.net/latest/developers/)
|
||||||
documentation for more information on how to setup your development environment.
|
documentation for more information on how to setup your development environment.
|
||||||
|
|
||||||
## Governance
|
## Governance
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -30,8 +30,6 @@
|
|||||||
# as it works transparently in both the main thread and worker cases.
|
# as it works transparently in both the main thread and worker cases.
|
||||||
|
|
||||||
from polyscript import lazy_py_modules as py_import
|
from polyscript import lazy_py_modules as py_import
|
||||||
from pyscript.display import HTML, display
|
|
||||||
from pyscript.fetch import fetch
|
|
||||||
from pyscript.magic_js import (
|
from pyscript.magic_js import (
|
||||||
RUNNING_IN_WORKER,
|
RUNNING_IN_WORKER,
|
||||||
PyWorker,
|
PyWorker,
|
||||||
@@ -43,19 +41,11 @@ from pyscript.magic_js import (
|
|||||||
sync,
|
sync,
|
||||||
window,
|
window,
|
||||||
)
|
)
|
||||||
|
from pyscript.display import HTML, display
|
||||||
|
from pyscript.fetch import fetch
|
||||||
from pyscript.storage import Storage, storage
|
from pyscript.storage import Storage, storage
|
||||||
from pyscript.websocket import WebSocket
|
from pyscript.websocket import WebSocket
|
||||||
|
from pyscript.events import when, Event
|
||||||
|
|
||||||
if not RUNNING_IN_WORKER:
|
if not RUNNING_IN_WORKER:
|
||||||
from pyscript.workers import create_named_worker, workers
|
from pyscript.workers import create_named_worker, workers
|
||||||
|
|
||||||
try:
|
|
||||||
from pyscript.event_handling import when
|
|
||||||
except:
|
|
||||||
# TODO: should we remove this? Or at the very least, we should capture
|
|
||||||
# the traceback otherwise it's very hard to debug
|
|
||||||
from pyscript.util import NotSupported
|
|
||||||
|
|
||||||
when = NotSupported(
|
|
||||||
"pyscript.when", "pyscript.when currently not available with this interpreter"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import inspect
|
|
||||||
|
|
||||||
try:
|
|
||||||
from pyodide.ffi.wrappers import add_event_listener
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
def add_event_listener(el, event_type, func):
|
|
||||||
el.addEventListener(event_type, func)
|
|
||||||
|
|
||||||
|
|
||||||
from pyscript.magic_js import document
|
|
||||||
|
|
||||||
|
|
||||||
def when(event_type=None, selector=None):
|
|
||||||
"""
|
|
||||||
Decorates a function and passes py-* events to the decorated function
|
|
||||||
The events might or not be an argument of the decorated function
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
|
|
||||||
from pyscript.web import Element, ElementCollection
|
|
||||||
|
|
||||||
if isinstance(selector, str):
|
|
||||||
elements = document.querySelectorAll(selector)
|
|
||||||
# TODO: This is a hack that will be removed when pyscript becomes a package
|
|
||||||
# and we can better manage the imports without circular dependencies
|
|
||||||
elif isinstance(selector, Element):
|
|
||||||
elements = [selector._dom_element]
|
|
||||||
elif isinstance(selector, ElementCollection):
|
|
||||||
elements = [el._dom_element for el in selector]
|
|
||||||
else:
|
|
||||||
if isinstance(selector, list):
|
|
||||||
elements = selector
|
|
||||||
else:
|
|
||||||
elements = [selector]
|
|
||||||
|
|
||||||
try:
|
|
||||||
sig = inspect.signature(func)
|
|
||||||
# Function doesn't receive events
|
|
||||||
if not sig.parameters:
|
|
||||||
|
|
||||||
# Function is async: must be awaited
|
|
||||||
if inspect.iscoroutinefunction(func):
|
|
||||||
|
|
||||||
async def wrapper(*args, **kwargs):
|
|
||||||
await func()
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
func()
|
|
||||||
|
|
||||||
else:
|
|
||||||
wrapper = func
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
# TODO: this is very ugly hack to get micropython working because inspect.signature
|
|
||||||
# doesn't exist, but we need to actually properly replace inspect.signature.
|
|
||||||
# It may be actually better to not try any magic for now and raise the error
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
try:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
except TypeError as e:
|
|
||||||
if "takes" in str(e) and "positional arguments" in str(e):
|
|
||||||
return func()
|
|
||||||
|
|
||||||
raise
|
|
||||||
|
|
||||||
for el in elements:
|
|
||||||
add_event_listener(el, event_type, wrapper)
|
|
||||||
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
166
core/src/stdlib/pyscript/events.py
Normal file
166
core/src/stdlib/pyscript/events.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
from pyscript.magic_js import document
|
||||||
|
from pyscript.ffi import create_proxy
|
||||||
|
from pyscript.util import is_awaitable
|
||||||
|
from pyscript import config
|
||||||
|
|
||||||
|
|
||||||
|
class Event:
|
||||||
|
"""
|
||||||
|
Represents something that may happen at some point in the future.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._listeners = []
|
||||||
|
|
||||||
|
def trigger(self, result):
|
||||||
|
"""
|
||||||
|
Trigger the event with a result to pass into the handlers.
|
||||||
|
"""
|
||||||
|
for listener in self._listeners:
|
||||||
|
if is_awaitable(listener):
|
||||||
|
# Use create task to avoid making this an async function.
|
||||||
|
asyncio.create_task(listener(result))
|
||||||
|
else:
|
||||||
|
listener(result)
|
||||||
|
|
||||||
|
def add_listener(self, listener):
|
||||||
|
"""
|
||||||
|
Add a callable/awaitable to listen to when this event is triggered.
|
||||||
|
"""
|
||||||
|
if is_awaitable(listener) or callable(listener):
|
||||||
|
if listener not in self._listeners:
|
||||||
|
self._listeners.append(listener)
|
||||||
|
else:
|
||||||
|
raise ValueError("Listener must be callable or awaitable.")
|
||||||
|
|
||||||
|
def remove_listener(self, *args):
|
||||||
|
"""
|
||||||
|
Clear the specified handler functions in *args. If no handlers
|
||||||
|
provided, clear all handlers.
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
for listener in args:
|
||||||
|
self._listeners.remove(listener)
|
||||||
|
else:
|
||||||
|
self._listeners = []
|
||||||
|
|
||||||
|
|
||||||
|
def when(target, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Add an event listener to the target element(s) for the specified event type.
|
||||||
|
|
||||||
|
The target can be a string representing the event type, or an Event object.
|
||||||
|
If the target is an Event object, the event listener will be added to that
|
||||||
|
object. If the target is a string, the event listener will be added to the
|
||||||
|
element(s) that match the (second) selector argument.
|
||||||
|
|
||||||
|
If a (third) handler argument is provided, it will be called when the event
|
||||||
|
is triggered; thus allowing this to be used as both a function and a
|
||||||
|
decorator.
|
||||||
|
"""
|
||||||
|
# If "when" is called as a function, try to grab the handler from the
|
||||||
|
# arguments. If there's no handler, this must be a decorator based call.
|
||||||
|
handler = None
|
||||||
|
if args and (callable(args[0]) or is_awaitable(args[0])):
|
||||||
|
handler = args[0]
|
||||||
|
elif callable(kwargs.get("handler")) or is_awaitable(kwargs.get("handler")):
|
||||||
|
handler = kwargs.pop("handler")
|
||||||
|
# If the target is a string, it is the "older" use of `when` where it
|
||||||
|
# represents the name of a DOM event.
|
||||||
|
if isinstance(target, str):
|
||||||
|
# Extract the selector from the arguments or keyword arguments.
|
||||||
|
selector = args[0] if args else kwargs.pop("selector")
|
||||||
|
if not selector:
|
||||||
|
raise ValueError("No selector provided.")
|
||||||
|
# Grab the DOM elements to which the target event will be attached.
|
||||||
|
from pyscript.web import Element, ElementCollection
|
||||||
|
|
||||||
|
if isinstance(selector, str):
|
||||||
|
elements = document.querySelectorAll(selector)
|
||||||
|
elif isinstance(selector, Element):
|
||||||
|
elements = [selector._dom_element]
|
||||||
|
elif isinstance(selector, ElementCollection):
|
||||||
|
elements = [el._dom_element for el in selector]
|
||||||
|
else:
|
||||||
|
elements = selector if isinstance(selector, list) else [selector]
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
if config["type"] == "mpy": # Is MicroPython?
|
||||||
|
if is_awaitable(func):
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
This is a very ugly hack to get micropython working because
|
||||||
|
`inspect.signature` doesn't exist. It may be actually better
|
||||||
|
to not try any magic for now and raise the error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
except TypeError as e:
|
||||||
|
if "takes" in str(e) and "positional arguments" in str(e):
|
||||||
|
return await func()
|
||||||
|
raise
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
This is a very ugly hack to get micropython working because
|
||||||
|
`inspect.signature` doesn't exist. It may be actually better
|
||||||
|
to not try any magic for now and raise the error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
except TypeError as e:
|
||||||
|
if "takes" in str(e) and "positional arguments" in str(e):
|
||||||
|
return func()
|
||||||
|
raise
|
||||||
|
|
||||||
|
else:
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
if sig.parameters:
|
||||||
|
if is_awaitable(func):
|
||||||
|
|
||||||
|
async def wrapper(event):
|
||||||
|
return await func(event)
|
||||||
|
|
||||||
|
else:
|
||||||
|
wrapper = func
|
||||||
|
else:
|
||||||
|
# Function doesn't receive events.
|
||||||
|
if is_awaitable(func):
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
return await func()
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func()
|
||||||
|
|
||||||
|
wrapper = wraps(func)(wrapper)
|
||||||
|
if isinstance(target, Event):
|
||||||
|
# The target is a single Event object.
|
||||||
|
target.add_listener(wrapper)
|
||||||
|
elif isinstance(target, list) and all(isinstance(t, Event) for t in target):
|
||||||
|
# The target is a list of Event objects.
|
||||||
|
for evt in target:
|
||||||
|
evt.add_listener(wrapper)
|
||||||
|
else:
|
||||||
|
# The target is a string representing an event type, and so a
|
||||||
|
# DOM element or collection of elements is found in "elements".
|
||||||
|
for el in elements:
|
||||||
|
el.addEventListener(target, create_proxy(wrapper))
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
# If "when" was called as a decorator, return the decorator function,
|
||||||
|
# otherwise just call the internal decorator function with the supplied
|
||||||
|
# handler.
|
||||||
|
return decorator(handler) if handler else decorator
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import js
|
import js
|
||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
def as_bytearray(buffer):
|
def as_bytearray(buffer):
|
||||||
|
"""
|
||||||
|
Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
|
||||||
|
MicroPython friendly manner.
|
||||||
|
"""
|
||||||
ui8a = js.Uint8Array.new(buffer)
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
size = ui8a.length
|
size = ui8a.length
|
||||||
ba = bytearray(size)
|
ba = bytearray(size)
|
||||||
@@ -31,3 +37,22 @@ class NotSupported:
|
|||||||
|
|
||||||
def __call__(self, *args):
|
def __call__(self, *args):
|
||||||
raise TypeError(self.error)
|
raise TypeError(self.error)
|
||||||
|
|
||||||
|
|
||||||
|
def is_awaitable(obj):
|
||||||
|
"""
|
||||||
|
Returns a boolean indication if the passed in obj is an awaitable
|
||||||
|
function. (MicroPython treats awaitables as generator functions, and if
|
||||||
|
the object is a closure containing an async function we need to work
|
||||||
|
carefully.)
|
||||||
|
"""
|
||||||
|
from pyscript import config
|
||||||
|
|
||||||
|
if config["type"] == "mpy": # Is MicroPython?
|
||||||
|
# MicroPython doesn't appear to have a way to determine if a closure is
|
||||||
|
# an async function except via the repr. This is a bit hacky.
|
||||||
|
if "<closure <generator>" in repr(obj):
|
||||||
|
return True
|
||||||
|
return inspect.isgeneratorfunction(obj)
|
||||||
|
|
||||||
|
return inspect.iscoroutinefunction(obj)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
# `when` is not used in this module. It is imported here save the user an additional
|
# `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`).
|
# import (i.e. they can get what they need from `pyscript.web`).
|
||||||
from pyscript import document, when # NOQA
|
from pyscript import document, when, Event # NOQA
|
||||||
|
from pyscript.ffi import create_proxy
|
||||||
|
|
||||||
|
|
||||||
def wrap_dom_element(dom_element):
|
def wrap_dom_element(dom_element):
|
||||||
@@ -68,6 +69,18 @@ class Element:
|
|||||||
type(self).get_tag_name()
|
type(self).get_tag_name()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# HTML on_events attached to the element become pyscript.Event instances.
|
||||||
|
self._on_events = {}
|
||||||
|
|
||||||
|
# Handle kwargs for handling named events with a default handler function.
|
||||||
|
properties = {}
|
||||||
|
for name, handler in kwargs.items():
|
||||||
|
if name.startswith("on_"):
|
||||||
|
ev = self.get_event(name) # Create the default Event instance.
|
||||||
|
ev.add_listener(handler)
|
||||||
|
else:
|
||||||
|
properties[name] = handler
|
||||||
|
|
||||||
# A set-like interface to the element's `classList`.
|
# A set-like interface to the element's `classList`.
|
||||||
self._classes = Classes(self)
|
self._classes = Classes(self)
|
||||||
|
|
||||||
@@ -75,7 +88,7 @@ class Element:
|
|||||||
self._style = Style(self)
|
self._style = Style(self)
|
||||||
|
|
||||||
# Set any specified classes, styles, and DOM properties.
|
# Set any specified classes, styles, and DOM properties.
|
||||||
self.update(classes=classes, style=style, **kwargs)
|
self.update(classes=classes, style=style, **properties)
|
||||||
|
|
||||||
def __eq__(self, obj):
|
def __eq__(self, obj):
|
||||||
"""Check for equality by comparing the underlying DOM element."""
|
"""Check for equality by comparing the underlying DOM element."""
|
||||||
@@ -93,13 +106,21 @@ class Element:
|
|||||||
return self.find(key)
|
return self.find(key)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
"""
|
||||||
|
Get an attribute from the element.
|
||||||
|
|
||||||
|
If the attribute is an event (e.g. "on_click"), we wrap it in an `Event`
|
||||||
|
instance and return that. Otherwise, we return the attribute from the
|
||||||
|
underlying DOM element.
|
||||||
|
"""
|
||||||
|
if name.startswith("on_"):
|
||||||
|
return self.get_event(name)
|
||||||
# This allows us to get attributes on the underlying DOM element that clash
|
# 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
|
# with Python keywords or built-ins (e.g. the output element has an
|
||||||
# attribute `for` which is a Python keyword, so you can access it on the
|
# attribute `for` which is a Python keyword, so you can access it on the
|
||||||
# Element instance via `for_`).
|
# Element instance via `for_`).
|
||||||
if name.endswith("_"):
|
if name.endswith("_"):
|
||||||
name = name[:-1]
|
name = name[:-1]
|
||||||
|
|
||||||
return getattr(self._dom_element, name)
|
return getattr(self._dom_element, name)
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
@@ -119,8 +140,33 @@ class Element:
|
|||||||
if name.endswith("_"):
|
if name.endswith("_"):
|
||||||
name = name[:-1]
|
name = name[:-1]
|
||||||
|
|
||||||
|
if name.startswith("on_"):
|
||||||
|
# Ensure on-events are cached in the _on_events dict if the
|
||||||
|
# user is setting them directly.
|
||||||
|
self._on_events[name] = value
|
||||||
|
|
||||||
setattr(self._dom_element, name, value)
|
setattr(self._dom_element, name, value)
|
||||||
|
|
||||||
|
def get_event(self, name):
|
||||||
|
"""
|
||||||
|
Get an `Event` instance for the specified event name.
|
||||||
|
"""
|
||||||
|
if not name.startswith("on_"):
|
||||||
|
raise ValueError("Event names must start with 'on_'.")
|
||||||
|
event_name = name[3:] # Remove the "on_" prefix.
|
||||||
|
if not hasattr(self._dom_element, event_name):
|
||||||
|
raise ValueError(f"Element has no '{event_name}' event.")
|
||||||
|
if name in self._on_events:
|
||||||
|
return self._on_events[name]
|
||||||
|
# Such an on-event exists in the DOM element, but we haven't yet
|
||||||
|
# wrapped it in an Event instance. Let's do that now. When the
|
||||||
|
# underlying DOM element's event is triggered, the Event instance
|
||||||
|
# will be triggered too.
|
||||||
|
ev = Event()
|
||||||
|
self._on_events[name] = ev
|
||||||
|
self._dom_element.addEventListener(event_name, create_proxy(ev.trigger))
|
||||||
|
return ev
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children(self):
|
def children(self):
|
||||||
"""Return the element's children as an `ElementCollection`."""
|
"""Return the element's children as an `ElementCollection`."""
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
<button id="a-test-button">I'm a button to be clicked</button>
|
<button id="a-test-button">I'm a button to be clicked</button>
|
||||||
<button>I'm another button you can click</button>
|
<button>I'm another button you can click</button>
|
||||||
<button id="a-third-button">2 is better than 3 :)</button>
|
<button id="a-third-button">2 is better than 3 :)</button>
|
||||||
|
<button id="another-test-button">I'm another button to be clicked</button>
|
||||||
|
|
||||||
<div id="element-append-tests"></div>
|
<div id="element-append-tests"></div>
|
||||||
<p class="collection"></p>
|
<p class="collection"></p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/upytest.py": "",
|
"https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
|
||||||
"./tests/test_config.py": "tests/test_config.py",
|
"./tests/test_config.py": "tests/test_config.py",
|
||||||
"./tests/test_current_target.py": "tests/test_current_target.py",
|
"./tests/test_current_target.py": "tests/test_current_target.py",
|
||||||
"./tests/test_display.py": "tests/test_display.py",
|
"./tests/test_display.py": "tests/test_display.py",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
|
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
|
||||||
"./tests/test_web.py": "tests/test_web.py",
|
"./tests/test_web.py": "tests/test_web.py",
|
||||||
"./tests/test_websocket.py": "tests/test_websocket.py",
|
"./tests/test_websocket.py": "tests/test_websocket.py",
|
||||||
"./tests/test_when.py": "tests/test_when.py",
|
"./tests/test_events.py": "tests/test_events.py",
|
||||||
"./tests/test_window.py": "tests/test_window.py"
|
"./tests/test_window.py": "tests/test_window.py"
|
||||||
},
|
},
|
||||||
"js_modules": {
|
"js_modules": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/upytest.py": "",
|
"https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
|
||||||
"./tests/test_config.py": "tests/test_config.py",
|
"./tests/test_config.py": "tests/test_config.py",
|
||||||
"./tests/test_current_target.py": "tests/test_current_target.py",
|
"./tests/test_current_target.py": "tests/test_current_target.py",
|
||||||
"./tests/test_display.py": "tests/test_display.py",
|
"./tests/test_display.py": "tests/test_display.py",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
|
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
|
||||||
"./tests/test_web.py": "tests/test_web.py",
|
"./tests/test_web.py": "tests/test_web.py",
|
||||||
"./tests/test_websocket.py": "tests/test_websocket.py",
|
"./tests/test_websocket.py": "tests/test_websocket.py",
|
||||||
"./tests/test_when.py": "tests/test_when.py",
|
"./tests/test_events.py": "tests/test_events.py",
|
||||||
"./tests/test_window.py": "tests/test_window.py"
|
"./tests/test_window.py": "tests/test_window.py"
|
||||||
},
|
},
|
||||||
"js_modules": {
|
"js_modules": {
|
||||||
|
|||||||
360
core/tests/python/tests/test_events.py
Normal file
360
core/tests/python/tests/test_events.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
"""
|
||||||
|
Tests for the when function and Event class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import upytest
|
||||||
|
from pyscript import RUNNING_IN_WORKER, web, Event, when
|
||||||
|
|
||||||
|
|
||||||
|
def get_container():
|
||||||
|
return web.page.find("#test-element-container")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
container = get_container()
|
||||||
|
container.innerHTML = ""
|
||||||
|
|
||||||
|
|
||||||
|
def teardown():
|
||||||
|
container = get_container()
|
||||||
|
container.innerHTML = ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_add_listener():
|
||||||
|
"""
|
||||||
|
Adding a listener to an event should add it to the list of listeners. It
|
||||||
|
should only be added once.
|
||||||
|
"""
|
||||||
|
event = Event()
|
||||||
|
listener = lambda x: x
|
||||||
|
event.add_listener(listener)
|
||||||
|
event.add_listener(listener)
|
||||||
|
assert len(event._listeners) == 1 # Only one item added.
|
||||||
|
assert listener in event._listeners # The item is the expected listener.
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_remove_listener():
|
||||||
|
"""
|
||||||
|
Removing a listener from an event should remove it from the list of
|
||||||
|
listeners.
|
||||||
|
"""
|
||||||
|
event = Event()
|
||||||
|
listener1 = lambda x: x
|
||||||
|
listener2 = lambda x: x
|
||||||
|
event.add_listener(listener1)
|
||||||
|
event.add_listener(listener2)
|
||||||
|
assert len(event._listeners) == 2 # Two listeners added.
|
||||||
|
assert listener1 in event._listeners # The first listener is in the list.
|
||||||
|
assert listener2 in event._listeners # The second listener is in the list.
|
||||||
|
event.remove_listener(listener1)
|
||||||
|
assert len(event._listeners) == 1 # Only one item remains.
|
||||||
|
assert listener2 in event._listeners # The second listener is in the list.
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_remove_all_listeners():
|
||||||
|
"""
|
||||||
|
Removing all listeners from an event should clear the list of listeners.
|
||||||
|
"""
|
||||||
|
event = Event()
|
||||||
|
listener1 = lambda x: x
|
||||||
|
listener2 = lambda x: x
|
||||||
|
event.add_listener(listener1)
|
||||||
|
event.add_listener(listener2)
|
||||||
|
assert len(event._listeners) == 2 # Two listeners added.
|
||||||
|
event.remove_listener()
|
||||||
|
assert len(event._listeners) == 0 # No listeners remain.
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_trigger():
|
||||||
|
"""
|
||||||
|
Triggering an event should call all of the listeners with the provided
|
||||||
|
arguments.
|
||||||
|
"""
|
||||||
|
event = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
def listener(x):
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert x == "ok"
|
||||||
|
|
||||||
|
event.add_listener(listener)
|
||||||
|
assert counter == 0 # The listener has not been triggered yet.
|
||||||
|
event.trigger("ok")
|
||||||
|
assert counter == 1 # The listener has been triggered with the expected result.
|
||||||
|
|
||||||
|
|
||||||
|
async def test_event_trigger_with_awaitable():
|
||||||
|
"""
|
||||||
|
Triggering an event with an awaitable listener should call the listener
|
||||||
|
with the provided arguments.
|
||||||
|
"""
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
event = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
async def listener(x):
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert x == "ok"
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
event.add_listener(listener)
|
||||||
|
assert counter == 0 # The listener has not been triggered yet.
|
||||||
|
event.trigger("ok")
|
||||||
|
await call_flag.wait()
|
||||||
|
assert counter == 1 # The listener has been triggered with the expected result.
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_with_event():
|
||||||
|
"""
|
||||||
|
When the decorated function takes a single parameter,
|
||||||
|
it should be passed the event object.
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo(evt):
|
||||||
|
nonlocal called
|
||||||
|
called = evt
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called.target.id == "foo_id"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_without_event():
|
||||||
|
"""
|
||||||
|
When the decorated function takes no parameters (not including 'self'),
|
||||||
|
it should be called without the event object.
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@web.when("click", selector="#foo_id")
|
||||||
|
def foo():
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_with_event_as_async_handler():
|
||||||
|
"""
|
||||||
|
When the decorated function takes a single parameter,
|
||||||
|
it should be passed the event object. Async version.
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
async def foo(evt):
|
||||||
|
nonlocal called
|
||||||
|
called = evt
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called.target.id == "foo_id"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_without_event_as_async_handler():
|
||||||
|
"""
|
||||||
|
When the decorated function takes no parameters (not including 'self'),
|
||||||
|
it should be called without the event object. Async version.
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@web.when("click", selector="#foo_id")
|
||||||
|
async def foo():
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_two_when_decorators():
|
||||||
|
"""
|
||||||
|
When decorating a function twice, both should function
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called1 = False
|
||||||
|
called2 = False
|
||||||
|
call_flag1 = asyncio.Event()
|
||||||
|
call_flag2 = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo1(evt):
|
||||||
|
nonlocal called1
|
||||||
|
called1 = True
|
||||||
|
call_flag1.set()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo2(evt):
|
||||||
|
nonlocal called2
|
||||||
|
called2 = True
|
||||||
|
call_flag2.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag1.wait()
|
||||||
|
await call_flag2.wait()
|
||||||
|
assert called1
|
||||||
|
assert called2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_multiple_elements():
|
||||||
|
"""
|
||||||
|
The @when decorator's selector should successfully select multiple
|
||||||
|
DOM elements
|
||||||
|
"""
|
||||||
|
btn1 = web.button(
|
||||||
|
"foo_button1",
|
||||||
|
id="foo_id1",
|
||||||
|
classes=[
|
||||||
|
"foo_class",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
btn2 = web.button(
|
||||||
|
"foo_button2",
|
||||||
|
id="foo_id2",
|
||||||
|
classes=[
|
||||||
|
"foo_class",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn1)
|
||||||
|
container.append(btn2)
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
call_flag1 = asyncio.Event()
|
||||||
|
call_flag2 = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector=".foo_class")
|
||||||
|
def foo(evt):
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
if evt.target.id == "foo_id1":
|
||||||
|
call_flag1.set()
|
||||||
|
else:
|
||||||
|
call_flag2.set()
|
||||||
|
|
||||||
|
assert counter == 0, counter
|
||||||
|
btn1.click()
|
||||||
|
await call_flag1.wait()
|
||||||
|
assert counter == 1, counter
|
||||||
|
btn2.click()
|
||||||
|
await call_flag2.wait()
|
||||||
|
assert counter == 2, counter
|
||||||
|
|
||||||
|
|
||||||
|
@upytest.skip(
|
||||||
|
"Only works in Pyodide on main thread",
|
||||||
|
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
||||||
|
)
|
||||||
|
def test_when_decorator_invalid_selector():
|
||||||
|
"""
|
||||||
|
When the selector parameter of @when is invalid, it should raise an error.
|
||||||
|
"""
|
||||||
|
if upytest.is_micropython:
|
||||||
|
from jsffi import JsException
|
||||||
|
else:
|
||||||
|
from pyodide.ffi import JsException
|
||||||
|
|
||||||
|
with upytest.raises(JsException) as e:
|
||||||
|
|
||||||
|
@when("click", selector="#.bad")
|
||||||
|
def foo(evt): ...
|
||||||
|
|
||||||
|
assert "'#.bad' is not a valid selector" in str(e.exception), str(e.exception)
|
||||||
|
|
||||||
|
|
||||||
|
def test_when_decorates_an_event():
|
||||||
|
"""
|
||||||
|
When the @when decorator is used on a function to handle an Event instance,
|
||||||
|
the function should be called when the Event object is triggered.
|
||||||
|
"""
|
||||||
|
|
||||||
|
whenable = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
# When as a decorator.
|
||||||
|
@when(whenable)
|
||||||
|
def handler(result):
|
||||||
|
"""
|
||||||
|
A function that should be called when the whenable object is triggered.
|
||||||
|
|
||||||
|
The result generated by the whenable object should be passed to the
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert result == "ok"
|
||||||
|
|
||||||
|
# The function should not be called until the whenable object is triggered.
|
||||||
|
assert counter == 0
|
||||||
|
# Trigger the whenable object.
|
||||||
|
whenable.trigger("ok")
|
||||||
|
# The function should have been called when the whenable object was
|
||||||
|
# triggered.
|
||||||
|
assert counter == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_when_called_with_an_event_and_handler():
|
||||||
|
"""
|
||||||
|
The when function should be able to be called with an Event object,
|
||||||
|
and a handler function.
|
||||||
|
"""
|
||||||
|
whenable = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
def handler(result):
|
||||||
|
"""
|
||||||
|
A function that should be called when the whenable object is triggered.
|
||||||
|
|
||||||
|
The result generated by the whenable object should be passed to the
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert result == "ok"
|
||||||
|
|
||||||
|
# When as a function.
|
||||||
|
when(whenable, handler)
|
||||||
|
|
||||||
|
# The function should not be called until the whenable object is triggered.
|
||||||
|
assert counter == 0
|
||||||
|
# Trigger the whenable object.
|
||||||
|
whenable.trigger("ok")
|
||||||
|
# The function should have been called when the whenable object was
|
||||||
|
# triggered.
|
||||||
|
assert counter == 1
|
||||||
48
core/tests/python/tests/test_util.py
Normal file
48
core/tests/python/tests/test_util.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import upytest
|
||||||
|
import js
|
||||||
|
from pyscript import util
|
||||||
|
|
||||||
|
|
||||||
|
def test_as_bytearray():
|
||||||
|
"""
|
||||||
|
Test the as_bytearray function correctly converts a JavaScript ArrayBuffer
|
||||||
|
to a Python bytearray.
|
||||||
|
"""
|
||||||
|
msg = b"Hello, world!"
|
||||||
|
buffer = js.ArrayBuffer.new(len(msg))
|
||||||
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
|
for b in msg:
|
||||||
|
ui8a[i] = b
|
||||||
|
ba = util.as_bytearray(buffer)
|
||||||
|
assert isinstance(ba, bytearray)
|
||||||
|
assert ba == msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_supported():
|
||||||
|
"""
|
||||||
|
Test the NotSupported class raises an exception when trying to access
|
||||||
|
attributes or call the object.
|
||||||
|
"""
|
||||||
|
ns = util.NotSupported("test", "This is not supported.")
|
||||||
|
with upytest.raises(AttributeError) as e:
|
||||||
|
ns.test
|
||||||
|
assert str(e.exception) == "This is not supported.", str(e.exception)
|
||||||
|
with upytest.raises(AttributeError) as e:
|
||||||
|
ns.test = 1
|
||||||
|
assert str(e.exception) == "This is not supported.", str(e.exception)
|
||||||
|
with upytest.raises(TypeError) as e:
|
||||||
|
ns()
|
||||||
|
assert str(e.exception) == "This is not supported.", str(e.exception)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_awaitable():
|
||||||
|
"""
|
||||||
|
Test the is_awaitable function correctly identifies an asynchronous
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def async_func():
|
||||||
|
yield
|
||||||
|
|
||||||
|
assert util.is_awaitable(async_func)
|
||||||
|
assert not util.is_awaitable(lambda: None)
|
||||||
@@ -164,6 +164,57 @@ class TestElement:
|
|||||||
await call_flag.wait()
|
await call_flag.wait()
|
||||||
assert called
|
assert called
|
||||||
|
|
||||||
|
async def test_when_decorator_on_event(self):
|
||||||
|
called = False
|
||||||
|
|
||||||
|
another_button = web.page.find("#another-test-button")[0]
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
assert another_button.on_click is not None
|
||||||
|
assert isinstance(another_button.on_click, web.Event)
|
||||||
|
|
||||||
|
@when(another_button.on_click)
|
||||||
|
def on_click(event):
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
# Now let's simulate a click on the button (using the low level JS API)
|
||||||
|
# so we don't risk dom getting in the way
|
||||||
|
assert not called
|
||||||
|
another_button._dom_element.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called
|
||||||
|
|
||||||
|
async def test_on_event_with_default_handler(self):
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
def handler(event):
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
b = web.button("Click me", on_click=handler)
|
||||||
|
|
||||||
|
# Now let's simulate a click on the button (using the low level JS API)
|
||||||
|
# so we don't risk dom getting in the way
|
||||||
|
assert not called
|
||||||
|
b._dom_element.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called
|
||||||
|
|
||||||
|
def test_on_event_must_be_actual_event(self):
|
||||||
|
"""
|
||||||
|
Any on_FOO event must relate to an actual FOO event on the element.
|
||||||
|
"""
|
||||||
|
b = web.button("Click me")
|
||||||
|
# Non-existent event causes a ValueError
|
||||||
|
with upytest.raises(ValueError):
|
||||||
|
b.on_chicken
|
||||||
|
# Buttons have an underlying "click" event so this will work.
|
||||||
|
assert b.on_click
|
||||||
|
|
||||||
def test_inner_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 = web.page.find("#element_attribute_tests")[0]
|
div = web.page.find("#element_attribute_tests")[0]
|
||||||
@@ -227,11 +278,15 @@ class TestCollection:
|
|||||||
assert el.style["background-color"] != "red"
|
assert el.style["background-color"] != "red"
|
||||||
assert elements[i].style["background-color"] != "red"
|
assert elements[i].style["background-color"] != "red"
|
||||||
|
|
||||||
|
@upytest.skip(
|
||||||
|
"Flakey in Pyodide on Worker",
|
||||||
|
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
|
||||||
|
)
|
||||||
async def test_when_decorator(self):
|
async def test_when_decorator(self):
|
||||||
called = False
|
called = False
|
||||||
call_flag = asyncio.Event()
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
buttons_collection = web.page.find("button")
|
buttons_collection = web.page["button"]
|
||||||
|
|
||||||
@when("click", buttons_collection)
|
@when("click", buttons_collection)
|
||||||
def on_click(event):
|
def on_click(event):
|
||||||
@@ -249,6 +304,28 @@ class TestCollection:
|
|||||||
called = False
|
called = False
|
||||||
call_flag.clear()
|
call_flag.clear()
|
||||||
|
|
||||||
|
async def test_when_decorator_on_event(self):
|
||||||
|
call_counter = 0
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
buttons_collection = web.page.find("button")
|
||||||
|
number_of_clicks = len(buttons_collection)
|
||||||
|
|
||||||
|
@when(buttons_collection.on_click)
|
||||||
|
def on_click(event):
|
||||||
|
nonlocal call_counter
|
||||||
|
call_counter += 1
|
||||||
|
if call_counter == number_of_clicks:
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
# Now let's simulate a click on the button (using the low level JS API)
|
||||||
|
# so we don't risk dom getting in the way
|
||||||
|
assert call_counter == 0
|
||||||
|
for button in buttons_collection:
|
||||||
|
button._dom_element.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert call_counter == number_of_clicks
|
||||||
|
|
||||||
|
|
||||||
class TestCreation:
|
class TestCreation:
|
||||||
|
|
||||||
@@ -759,14 +836,13 @@ class TestElements:
|
|||||||
self._create_el_and_basic_asserts("iframe", properties=properties)
|
self._create_el_and_basic_asserts("iframe", properties=properties)
|
||||||
|
|
||||||
@upytest.skip(
|
@upytest.skip(
|
||||||
"Flakey on Pyodide in worker.",
|
"Flakey in worker.",
|
||||||
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
|
skip_when=RUNNING_IN_WORKER,
|
||||||
)
|
)
|
||||||
async def test_img(self):
|
async def test_img(self):
|
||||||
"""
|
"""
|
||||||
This test contains a bespoke version of the _create_el_and_basic_asserts
|
This test, thanks to downloading an image from the internet, is flakey
|
||||||
function so we can await asyncio.sleep if in a worker, so the DOM state
|
when run in a worker. It's skipped when running in a worker.
|
||||||
is in sync with the worker before property based asserts can happen.
|
|
||||||
"""
|
"""
|
||||||
properties = {
|
properties = {
|
||||||
"src": "https://picsum.photos/600/400",
|
"src": "https://picsum.photos/600/400",
|
||||||
@@ -774,39 +850,7 @@ class TestElements:
|
|||||||
"width": 250,
|
"width": 250,
|
||||||
"height": 200,
|
"height": 200,
|
||||||
}
|
}
|
||||||
|
self._create_el_and_basic_asserts("img", properties=properties)
|
||||||
def parse_value(v):
|
|
||||||
if isinstance(v, bool):
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
return f"{v}"
|
|
||||||
|
|
||||||
args = []
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
if properties:
|
|
||||||
kwargs = {k: parse_value(v) for k, v in properties.items()}
|
|
||||||
|
|
||||||
# Let's make sure the target div to contain the element is empty.
|
|
||||||
container = web.page["#test-element-container"][0]
|
|
||||||
container.innerHTML = ""
|
|
||||||
assert container.innerHTML == "", container.innerHTML
|
|
||||||
# Let's create the element
|
|
||||||
try:
|
|
||||||
klass = getattr(web, "img")
|
|
||||||
el = klass(*args, **kwargs)
|
|
||||||
container.append(el)
|
|
||||||
except Exception as e:
|
|
||||||
assert False, f"Failed to create element img: {e}"
|
|
||||||
|
|
||||||
if RUNNING_IN_WORKER:
|
|
||||||
# Needed to sync the DOM with the worker.
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
# Check the img element was created correctly and all its properties
|
|
||||||
# were set correctly.
|
|
||||||
for k, v in properties.items():
|
|
||||||
assert v == getattr(el, k), f"{k} should be {v} but is {getattr(el, k)}"
|
|
||||||
|
|
||||||
def test_input(self):
|
def test_input(self):
|
||||||
# TODO: we need multiple input tests
|
# TODO: we need multiple input tests
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
"""
|
|
||||||
Tests for the pyscript.when decorator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import upytest
|
|
||||||
from pyscript import RUNNING_IN_WORKER, web
|
|
||||||
|
|
||||||
|
|
||||||
def get_container():
|
|
||||||
return web.page.find("#test-element-container")[0]
|
|
||||||
|
|
||||||
|
|
||||||
def setup():
|
|
||||||
container = get_container()
|
|
||||||
container.innerHTML = ""
|
|
||||||
|
|
||||||
|
|
||||||
def teardown():
|
|
||||||
container = get_container()
|
|
||||||
container.innerHTML = ""
|
|
||||||
|
|
||||||
|
|
||||||
async def test_when_decorator_with_event():
|
|
||||||
"""
|
|
||||||
When the decorated function takes a single parameter,
|
|
||||||
it should be passed the event object
|
|
||||||
"""
|
|
||||||
btn = web.button("foo_button", id="foo_id")
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn)
|
|
||||||
|
|
||||||
called = False
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
def foo(evt):
|
|
||||||
nonlocal called
|
|
||||||
called = evt
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
btn.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert called.target.id == "foo_id"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_when_decorator_without_event():
|
|
||||||
"""
|
|
||||||
When the decorated function takes no parameters (not including 'self'),
|
|
||||||
it should be called without the event object
|
|
||||||
"""
|
|
||||||
btn = web.button("foo_button", id="foo_id")
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn)
|
|
||||||
|
|
||||||
called = False
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
def foo():
|
|
||||||
nonlocal called
|
|
||||||
called = True
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
btn.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert called
|
|
||||||
|
|
||||||
|
|
||||||
async def test_two_when_decorators():
|
|
||||||
"""
|
|
||||||
When decorating a function twice, both should function
|
|
||||||
"""
|
|
||||||
btn = web.button("foo_button", id="foo_id")
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn)
|
|
||||||
|
|
||||||
called1 = False
|
|
||||||
called2 = False
|
|
||||||
call_flag1 = asyncio.Event()
|
|
||||||
call_flag2 = asyncio.Event()
|
|
||||||
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
def foo1(evt):
|
|
||||||
nonlocal called1
|
|
||||||
called1 = True
|
|
||||||
call_flag1.set()
|
|
||||||
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
def foo2(evt):
|
|
||||||
nonlocal called2
|
|
||||||
called2 = True
|
|
||||||
call_flag2.set()
|
|
||||||
|
|
||||||
btn.click()
|
|
||||||
await call_flag1.wait()
|
|
||||||
await call_flag2.wait()
|
|
||||||
assert called1
|
|
||||||
assert called2
|
|
||||||
|
|
||||||
|
|
||||||
async def test_two_when_decorators_same_element():
|
|
||||||
"""
|
|
||||||
When decorating a function twice *on the same DOM element*, both should
|
|
||||||
function
|
|
||||||
"""
|
|
||||||
btn = web.button("foo_button", id="foo_id")
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn)
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
def foo(evt):
|
|
||||||
nonlocal counter
|
|
||||||
counter += 1
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
assert counter == 0, counter
|
|
||||||
btn.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert counter == 2, counter
|
|
||||||
|
|
||||||
|
|
||||||
async def test_when_decorator_multiple_elements():
|
|
||||||
"""
|
|
||||||
The @when decorator's selector should successfully select multiple
|
|
||||||
DOM elements
|
|
||||||
"""
|
|
||||||
btn1 = web.button(
|
|
||||||
"foo_button1",
|
|
||||||
id="foo_id1",
|
|
||||||
classes=[
|
|
||||||
"foo_class",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
btn2 = web.button(
|
|
||||||
"foo_button2",
|
|
||||||
id="foo_id2",
|
|
||||||
classes=[
|
|
||||||
"foo_class",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn1)
|
|
||||||
container.append(btn2)
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
call_flag1 = asyncio.Event()
|
|
||||||
call_flag2 = asyncio.Event()
|
|
||||||
|
|
||||||
@web.when("click", selector=".foo_class")
|
|
||||||
def foo(evt):
|
|
||||||
nonlocal counter
|
|
||||||
counter += 1
|
|
||||||
if evt.target.id == "foo_id1":
|
|
||||||
call_flag1.set()
|
|
||||||
else:
|
|
||||||
call_flag2.set()
|
|
||||||
|
|
||||||
assert counter == 0, counter
|
|
||||||
btn1.click()
|
|
||||||
await call_flag1.wait()
|
|
||||||
assert counter == 1, counter
|
|
||||||
btn2.click()
|
|
||||||
await call_flag2.wait()
|
|
||||||
assert counter == 2, counter
|
|
||||||
|
|
||||||
|
|
||||||
async def test_when_decorator_duplicate_selectors():
|
|
||||||
"""
|
|
||||||
When is not idempotent, so it should be possible to add multiple
|
|
||||||
@when decorators with the same selector.
|
|
||||||
"""
|
|
||||||
btn = web.button("foo_button", id="foo_id")
|
|
||||||
container = get_container()
|
|
||||||
container.append(btn)
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
call_flag = asyncio.Event()
|
|
||||||
|
|
||||||
@web.when("click", selector="#foo_id")
|
|
||||||
@web.when("click", selector="#foo_id") # duplicate
|
|
||||||
def foo1(evt):
|
|
||||||
nonlocal counter
|
|
||||||
counter += 1
|
|
||||||
call_flag.set()
|
|
||||||
|
|
||||||
assert counter == 0, counter
|
|
||||||
btn.click()
|
|
||||||
await call_flag.wait()
|
|
||||||
assert counter == 2, counter
|
|
||||||
|
|
||||||
|
|
||||||
@upytest.skip(
|
|
||||||
"Only works in Pyodide on main thread",
|
|
||||||
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
|
||||||
)
|
|
||||||
def test_when_decorator_invalid_selector():
|
|
||||||
"""
|
|
||||||
When the selector parameter of @when is invalid, it should raise an error.
|
|
||||||
"""
|
|
||||||
if upytest.is_micropython:
|
|
||||||
from jsffi import JsException
|
|
||||||
else:
|
|
||||||
from pyodide.ffi import JsException
|
|
||||||
|
|
||||||
with upytest.raises(JsException) as e:
|
|
||||||
|
|
||||||
@web.when("click", selector="#.bad")
|
|
||||||
def foo(evt): ...
|
|
||||||
|
|
||||||
assert "'#.bad' is not a valid selector" in str(e.exception), str(e.exception)
|
|
||||||
2
core/types/stdlib/pyscript.d.ts
vendored
2
core/types/stdlib/pyscript.d.ts
vendored
@@ -2,7 +2,7 @@ declare namespace _default {
|
|||||||
let pyscript: {
|
let pyscript: {
|
||||||
"__init__.py": string;
|
"__init__.py": string;
|
||||||
"display.py": string;
|
"display.py": string;
|
||||||
"event_handling.py": string;
|
"events.py": string;
|
||||||
"fetch.py": string;
|
"fetch.py": string;
|
||||||
"ffi.py": string;
|
"ffi.py": string;
|
||||||
"flatted.py": string;
|
"flatted.py": string;
|
||||||
|
|||||||
Reference in New Issue
Block a user