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
@@ -62,6 +62,7 @@
|
||||
<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>
|
||||
<button id="another-test-button">I'm another button to be clicked</button>
|
||||
|
||||
<div id="element-append-tests"></div>
|
||||
<p class="collection"></p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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_current_target.py": "tests/test_current_target.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_web.py": "tests/test_web.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"
|
||||
},
|
||||
"js_modules": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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_current_target.py": "tests/test_current_target.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_web.py": "tests/test_web.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"
|
||||
},
|
||||
"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()
|
||||
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):
|
||||
# GIVEN an existing element on the page with a known empty text content
|
||||
div = web.page.find("#element_attribute_tests")[0]
|
||||
@@ -227,11 +278,15 @@ class TestCollection:
|
||||
assert el.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):
|
||||
called = False
|
||||
call_flag = asyncio.Event()
|
||||
|
||||
buttons_collection = web.page.find("button")
|
||||
buttons_collection = web.page["button"]
|
||||
|
||||
@when("click", buttons_collection)
|
||||
def on_click(event):
|
||||
@@ -249,6 +304,28 @@ class TestCollection:
|
||||
called = False
|
||||
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:
|
||||
|
||||
@@ -759,14 +836,13 @@ class TestElements:
|
||||
self._create_el_and_basic_asserts("iframe", properties=properties)
|
||||
|
||||
@upytest.skip(
|
||||
"Flakey on Pyodide in worker.",
|
||||
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
|
||||
"Flakey in worker.",
|
||||
skip_when=RUNNING_IN_WORKER,
|
||||
)
|
||||
async def test_img(self):
|
||||
"""
|
||||
This test contains a bespoke version of the _create_el_and_basic_asserts
|
||||
function so we can await asyncio.sleep if in a worker, so the DOM state
|
||||
is in sync with the worker before property based asserts can happen.
|
||||
This test, thanks to downloading an image from the internet, is flakey
|
||||
when run in a worker. It's skipped when running in a worker.
|
||||
"""
|
||||
properties = {
|
||||
"src": "https://picsum.photos/600/400",
|
||||
@@ -774,39 +850,7 @@ class TestElements:
|
||||
"width": 250,
|
||||
"height": 200,
|
||||
}
|
||||
|
||||
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)}"
|
||||
self._create_el_and_basic_asserts("img", properties=properties)
|
||||
|
||||
def test_input(self):
|
||||
# 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)
|
||||
Reference in New Issue
Block a user