Refactor repository. Fixes #2161 (#2192)

* Remove duplicate LICENSE.
* Remove un-userd pyscript.sw directory and its content.
* Remove ReadTheDocs settings (unused).
* Remove un-used pyproject.toml
* Remove now unused CHANGELOG. Changes now tracked via release notes on GitHub.
* Updated / cleaned release page template and associated GH actions.
* Update prettierignore to remove un-needed refs.
* Move troubleshooting into correct README.
* Add reason for the index.html
* Rename the "pyscript.core" directory to "core".
* Update PR template because CHANGELOG is no longer used.
* Codespell configuration in pyproject.toml.
* Update pyscript.core -> core in .githubignore
* Remove test-results/.last-run.json. This should be ignored by git.
* Pin nodejs version.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Nicholas Tollervey
2024-09-30 10:29:26 +01:00
committed by GitHub
parent abb1eb28fe
commit 9dad29ec17
230 changed files with 242 additions and 1679 deletions

View File

@@ -0,0 +1,7 @@
/*
A simple JavaScript module to test the integration with Python.
*/
export function hello() {
return "Hello from JavaScript!";
}

View File

@@ -0,0 +1,7 @@
/*
A simple JavaScript module to test the integration with Python on a worker.
*/
export function hello() {
return "Hello from JavaScript in a web worker!";
}

View File

@@ -0,0 +1,23 @@
const qs = new URLSearchParams(location.search);
const src = './main.py';
let config = './settings_mpy.json';
// terminal=0 to NOT have a terminal
const terminal = qs.has('terminal') ? qs.get('terminal') : 1;
// worker=1 to have a worker
const worker = qs.has('worker');
const interpreter = qs.get('type') || 'mpy';
if (interpreter === 'py') {
config = "./settings_py.json";
}
const script = document.createElement('script');
script.type = interpreter;
if (src) script.src = src;
if (config) script.setAttribute('config', config);
script.toggleAttribute('terminal', terminal);
script.toggleAttribute('worker', worker);
document.write(script.outerHTML);

View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pure Python PyScript tests</title>
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script src="./helper.js"></script>
<!-- script type="mpy" src="./main.py" config="./settings.json" terminal></script-->
<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_text" type="text" value="Content test_rr_input_text">
<input id="test_rr_input_button" type="button" value="Content test_rr_input_button">
<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>
<select id="test_select_element"></select>
<select id="test_select_element_w_options">
<option value="1">Option 1</option>
<option value="2" selected="selected">Option 2</option>
</select>
<select id="test_select_element_to_clear">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="4">Option 4</option>
</select>
<select id="test_select_element_to_remove">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
<option value="4">Option 4</option>
</select>
<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 id="element_attribute_tests"></div>
</div>
<div id="test-element-container"></div>
</body>
</html>

View File

@@ -0,0 +1,8 @@
import json
import upytest
from pyscript import web
result = await upytest.run("./tests", random=True)
output = web.div(json.dumps(result), id="result")
web.page.append(output)

View File

@@ -0,0 +1,26 @@
{
"files": {
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/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",
"./tests/test_document.py": "tests/test_document.py",
"./tests/test_fetch.py": "tests/test_fetch.py",
"./tests/test_ffi.py": "tests/test_ffi.py",
"./tests/test_js_modules.py": "tests/test_js_modules.py",
"./tests/test_storage.py": "tests/test_storage.py",
"./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_window.py": "tests/test_window.py"
},
"js_modules": {
"main": {
"./example_js_module.js": "greeting"
},
"worker": {
"./example_js_worker_module.js": "greeting_worker"
}
}
}

View File

@@ -0,0 +1,27 @@
{
"files": {
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/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",
"./tests/test_document.py": "tests/test_document.py",
"./tests/test_fetch.py": "tests/test_fetch.py",
"./tests/test_ffi.py": "tests/test_ffi.py",
"./tests/test_js_modules.py": "tests/test_js_modules.py",
"./tests/test_storage.py": "tests/test_storage.py",
"./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_window.py": "tests/test_window.py"
},
"js_modules": {
"main": {
"./example_js_module.js": "greeting"
},
"worker": {
"./example_js_worker_module.js": "greeting_worker"
}
},
"packages": ["Pillow" ]
}

View File

@@ -0,0 +1,20 @@
"""
Tests for the pyscript.config dictionary.
"""
from pyscript import config, document, fetch
from upytest import is_micropython
async def test_config_reads_expected_settings_correctly():
"""
The config dictionary should read expected settings for this test suite.
Just grab the raw JSON for the settings and compare it to the config
dictionary.
"""
settings = "/settings_mpy.json" if is_micropython else "/settings_py.json"
url = document.location.href.rsplit("/", 1)[0] + settings
raw_config = await fetch(url).json()
for key, value in raw_config.items():
assert config[key] == value, f"Expected {key} to be {value}, got {config[key]}"

View File

@@ -0,0 +1,22 @@
"""
Ensure the pyscript.current_target function returns the expected target
element's id.
"""
from pyscript import RUNNING_IN_WORKER, current_target
from upytest import is_micropython
def test_current_target():
"""
The current_target function should return the expected target element's id.
"""
expected = "py-0"
if is_micropython:
if RUNNING_IN_WORKER:
expected = "mpy-w0-target"
else:
expected = "mpy-0"
elif RUNNING_IN_WORKER:
expected = "py-w0-target"
assert current_target() == expected, f"Expected {expected} got {current_target()}"

View File

@@ -0,0 +1,289 @@
"""
Tests for the display function in PyScript.
"""
import asyncio
import upytest
from pyscript import HTML, RUNNING_IN_WORKER, display, py_import, web
async def get_display_container():
"""
Get the element that contains the output of the display function.
"""
if RUNNING_IN_WORKER:
# Needed to ensure the DOM has time to catch up with the display calls
# made in the worker thread.
await asyncio.sleep(0.01)
py_display = web.page.find("script-py")
if len(py_display) == 1:
return py_display[0]
mpy_display = web.page.find("script-mpy")
if len(mpy_display) == 1:
return mpy_display[0]
return None
async def setup():
"""
Setup function for the test_display.py module. Remove all references to the
display output in the DOM so we always start from a clean state.
"""
container = await get_display_container()
if container:
container.replaceChildren()
target_container = web.page.find("#test-element-container")[0]
target_container.innerHTML = ""
async def teardown():
"""
Like setup.
"""
container = await get_display_container()
if container:
container.replaceChildren()
target_container = web.page.find("#test-element-container")[0]
target_container.innerHTML = ""
async def test_simple_display():
"""
Test the display function with a simple string.
"""
display("Hello, world")
container = await get_display_container()
assert len(container.children) == 1, "Expected one child in the display container."
assert (
container.children[0].tagName == "DIV"
), "Expected a div element in the display container."
assert container.children[0].innerHTML == "Hello, world"
async def test_consecutive_display():
"""
Display order should be preserved.
"""
display("hello 1")
display("hello 2")
container = await get_display_container()
assert (
len(container.children) == 2
), "Expected two children in the display container."
assert container.children[0].innerHTML == "hello 1"
assert container.children[1].innerHTML == "hello 2"
def test_target_parameter():
"""
The output from display is placed in the target element.
"""
display("hello world", target="test-element-container")
target = web.page.find("#test-element-container")[0]
assert target.innerText == "hello world"
def test_target_parameter_with_hash():
"""
The target parameter can have a hash in front of it.
"""
display("hello world", target="#test-element-container")
target = web.page.find("#test-element-container")[0]
assert target.innerText == "hello world"
def test_non_existing_id_target_raises_value_error():
"""
If the target parameter is set to a non-existing element, a ValueError should be raised.
"""
with upytest.raises(ValueError):
display("hello world", target="non-existing")
def test_empty_string_target_raises_value_error():
"""
If the target parameter is an empty string, a ValueError should be raised.
"""
with upytest.raises(ValueError) as exc:
display("hello world", target="")
assert str(exc.exception) == "Cannot have an empty target"
def test_non_string_target_values_raise_typerror():
"""
The target parameter must be a string.
"""
with upytest.raises(TypeError) as exc:
display("hello world", target=True)
assert str(exc.exception) == "target must be str or None, not bool"
with upytest.raises(TypeError) as exc:
display("hello world", target=123)
assert str(exc.exception) == "target must be str or None, not int"
async def test_tag_target_attribute():
"""
The order and arrangement of the display calls (including targets) should be preserved.
"""
display("item 1")
display("item 2", target="test-element-container")
display("item 3")
container = await get_display_container()
assert (
len(container.children) == 2
), "Expected two children in the display container."
assert container.children[0].innerHTML == "item 1"
assert container.children[1].innerHTML == "item 3"
target = web.page.find("#test-element-container")[0]
assert target.innerText == "item 2"
async def test_multiple_display_calls_same_tag():
"""
Multiple display calls in the same script tag should be displayed in order.
"""
display("item 1")
display("item 2")
container = await get_display_container()
assert (
len(container.children) == 2
), "Expected two children in the display container."
assert container.children[0].innerHTML == "item 1"
assert container.children[1].innerHTML == "item 2"
async def test_append_true():
"""
Explicit append flag as true should append to the expected container element.
"""
display("item 1", append=True)
display("item 2", append=True)
container = await get_display_container()
assert (
len(container.children) == 2
), "Expected two children in the display container."
assert container.children[0].innerHTML == "item 1"
assert container.children[1].innerHTML == "item 2"
async def test_append_false():
"""
Explicit append flag as false should replace the expected container element.
"""
display("item 1", append=False)
display("item 2", append=False)
container = await get_display_container()
assert container.innerText == "item 2"
async def test_display_multiple_values():
"""
Display multiple values in the same call.
"""
display("hello", "world")
container = await get_display_container()
assert container.innerText == "hello\nworld", container.innerText
async def test_display_multiple_append_false():
display("hello", "world", append=False)
container = await get_display_container()
assert container.innerText == "world"
def test_display_multiple_append_false_with_target():
"""
TODO: this is a display.py issue to fix when append=False is used
do not use the first element, just clean up and then append
remove the # display comment once that's done
"""
class Circle:
r = 0
def _repr_svg_(self):
return (
f'<svg height="{self.r*2}" width="{self.r*2}">'
f'<circle cx="{self.r}" cy="{self.r}" r="{self.r}" fill="red"></circle></svg>'
)
circle = Circle()
circle.r += 5
display(circle, circle, target="test-element-container", append=False)
target = web.page.find("#test-element-container")[0]
assert target.innerHTML == circle._repr_svg_()
async def test_display_list_dict_tuple():
"""
Display a list, dictionary, and tuple with the expected __repr__.
NOTE: MicroPython doesn't (yet) have ordered dicts. Hence the rather odd
check that the dictionary is displayed as a string.
"""
l = ["A", 1, "!"]
d = {"B": 2, "List": l}
t = ("C", 3, "!")
display(l, d, t)
container = await get_display_container()
l2, d2, t2 = container.innerText.split("\n")
assert l == eval(l2)
assert d == eval(d2)
assert t == eval(t2)
async def test_display_should_escape():
display("<p>hello world</p>")
container = await get_display_container()
assert container[0].innerHTML == "&lt;p&gt;hello world&lt;/p&gt;"
assert container.innerText == "<p>hello world</p>"
async def test_display_HTML():
display(HTML("<p>hello world</p>"))
container = await get_display_container()
assert container[0].innerHTML == "<p>hello world</p>"
assert container.innerText == "hello world"
@upytest.skip(
"Pyodide main thread only",
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
)
async def test_image_display():
"""
Check an image is displayed correctly.
"""
mpl = await py_import("matplotlib")
import matplotlib.pyplot as plt
xpoints = [3, 6, 9]
ypoints = [1, 2, 3]
plt.plot(xpoints, ypoints)
display(plt)
container = await get_display_container()
img = container.find("img")[0]
img_src = img.getAttribute("src").replace(
"data:image/png;charset=utf-8;base64,", ""
)
assert len(img_src) > 0
@upytest.skip(
"Pyodide main thread only",
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
)
async def test_image_renders_correctly():
"""
This is just a sanity check to make sure that images are rendered
in a reasonable way.
"""
from PIL import Image
img = Image.new("RGB", (4, 4), color=(0, 0, 0))
display(img, target="test-element-container", append=False)
target = web.page.find("#test-element-container")[0]
img = target.find("img")[0]
assert img.src.startswith("data:image/png;charset=utf-8;base64")

View File

@@ -0,0 +1,17 @@
"""
Sanity check for the pyscript.document object.
"""
from pyscript import document
def test_document():
"""
The document object should be available and we can change its attributes
(in this case, the title).
"""
title = document.title
assert title
document.title = "A new title"
assert document.title == "A new title"
document.title = title

View File

@@ -0,0 +1,83 @@
"""
Ensure the pyscript.test function behaves as expected.
"""
from pyscript import fetch
async def test_fetch_json():
"""
The fetch function should return the expected JSON response.
"""
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
assert response.ok
data = await response.json()
assert data["userId"] == 1
assert data["id"] == 1
assert data["title"] == "delectus aut autem"
assert data["completed"] is False
async def test_fetch_text():
"""
The fetch function should return the expected text response.
"""
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
assert response.ok
text = await response.text()
assert "delectus aut autem" in text
assert "completed" in text
assert "false" in text
assert "1" in text
async def test_fetch_bytearray():
"""
The fetch function should return the expected bytearray response.
"""
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
assert response.ok
data = await response.bytearray()
assert b"delectus aut autem" in data
assert b"completed" in data
assert b"false" in data
assert b"1" in data
async def test_fetch_array_buffer():
"""
The fetch function should return the expected array buffer response.
"""
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
assert response.ok
data = await response.arrayBuffer()
bytes_ = bytes(data)
assert b"delectus aut autem" in bytes_
assert b"completed" in bytes_
assert b"false" in bytes_
assert b"1" in bytes_
async def test_fetch_ok():
"""
The fetch function should return a response with ok set to True for an
existing URL.
"""
response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
assert response.ok
assert response.status == 200
data = await response.json()
assert data["userId"] == 1
assert data["id"] == 1
assert data["title"] == "delectus aut autem"
assert data["completed"] is False
async def test_fetch_not_ok():
"""
The fetch function should return a response with ok set to False for a
non-existent URL.
"""
response = await fetch("https://jsonplaceholder.typicode.com/todos/1000")
assert not response.ok
assert response.status == 404

View File

@@ -0,0 +1,40 @@
"""
Exercise (as much as is possible) the pyscript.ffi namespace.
"""
import upytest
from pyscript import ffi
def test_create_proxy():
"""
The create_proxy function should return a proxy object that is callable.
"""
def func():
return 42
proxy = ffi.create_proxy(func)
assert proxy() == 42
if upytest.is_micropython:
from jsffi import JsProxy
else:
from pyodide.ffi import JsProxy
assert isinstance(proxy, JsProxy)
def test_to_js():
"""
The to_js function should convert a Python object to a JavaScript object.
In this instance, a Python dict should be converted to a JavaScript object
represented by a JsProxy object.
"""
obj = {"a": 1, "b": 2}
js_obj = ffi.to_js(obj)
assert js_obj.a == 1
assert js_obj.b == 2
if upytest.is_micropython:
from jsffi import JsProxy
else:
from pyodide.ffi import JsProxy
assert isinstance(js_obj, JsProxy)

View File

@@ -0,0 +1,52 @@
"""
Ensure referenced JavaScript modules are available via the pyscript.js_modules
object.
"""
import upytest
from pyscript import RUNNING_IN_WORKER
@upytest.skip("Main thread only.", skip_when=RUNNING_IN_WORKER)
def test_js_module_is_available_on_main():
"""
The "hello" function in the example_js_module.js file is available via the
js_modules object while running in the main thread. See the settings.json
file for the configuration that makes this possible.
"""
from pyscript.js_modules import greeting
assert greeting.hello() == "Hello from JavaScript!"
@upytest.skip("Worker only.", skip_when=not RUNNING_IN_WORKER)
def test_js_module_is_available_on_worker():
"""
The "hello" function in the example_js_module.js file is available via the
js_modules object while running in a worker. See the settings.json file for
the configuration that makes this possible.
"""
from pyscript.js_modules import greeting
assert greeting.hello() == "Hello from JavaScript!"
@upytest.skip("Worker only.", skip_when=not RUNNING_IN_WORKER)
def test_js_module_is_available_on_worker():
"""
The "hello" function in the example_js_worker_module.js file is available
via the js_modules object while running in a worker.
"""
from pyscript.js_modules import greeting_worker
assert greeting_worker.hello() == "Hello from JavaScript in a web worker!"
@upytest.skip("Main thread only.", skip_when=RUNNING_IN_WORKER)
def test_js_worker_module_is_not_available_on_main():
"""
The "hello" function in the example_js_worker_module.js file is not
available via the js_modules object while running in the main thread.
"""
with upytest.raises(ImportError):
from pyscript.js_modules import greeting_worker

View File

@@ -0,0 +1,27 @@
"""
Ensure the pyscript.RUNNING_IN_WORKER flag is set correctly (a sanity check).
"""
import upytest
from pyscript import RUNNING_IN_WORKER, document
# In the test suite, running in a worker is flagged by the presence of the
# "worker" query string. We do this to avoid using RUNNING_IN_WORKER to skip
# tests that check RUNNING_IN_WORKER.
in_worker = "worker" in document.location.search.lower()
@upytest.skip("Main thread only.", skip_when=in_worker)
def test_running_in_main():
"""
The flag should be False.
"""
assert RUNNING_IN_WORKER is False
@upytest.skip("Worker only.", skip_when=not in_worker)
def test_running_in_worker():
"""
The flag should be True.
"""
assert RUNNING_IN_WORKER is True

View File

@@ -0,0 +1,89 @@
"""
Ensure the pyscript.storage object behaves as a Python dict.
"""
from pyscript import Storage, storage
test_store = None
async def setup():
global test_store
if test_store is None:
test_store = await storage("test_store")
test_store.clear()
await test_store.sync()
async def teardown():
if test_store:
test_store.clear()
await test_store.sync()
async def test_storage_as_dict():
"""
The storage object should behave as a Python dict.
"""
# Assign
test_store["a"] = 1
# Retrieve
assert test_store["a"] == 1
assert "a" in test_store
assert len(test_store) == 1
# Iterate
for k, v in test_store.items():
assert k == "a"
assert v == 1
# Remove
del test_store["a"]
assert "a" not in test_store
assert len(test_store) == 0
async def test_storage_types():
"""
The storage object should support different types of values.
"""
test_store["boolean"] = False
test_store["integer"] = 42
test_store["float"] = 3.14
test_store["string"] = "hello"
test_store["none"] = None
test_store["list"] = [1, 2, 3]
test_store["dict"] = {"a": 1, "b": 2}
test_store["tuple"] = (1, 2, 3)
test_store["bytearray"] = bytearray(b"hello")
test_store["memoryview"] = memoryview(b"hello")
await test_store.sync()
assert test_store["boolean"] is False
assert isinstance(test_store["boolean"], bool)
assert test_store["integer"] == 42
assert isinstance(test_store["integer"], int)
assert test_store["float"] == 3.14
assert isinstance(test_store["float"], float)
assert test_store["string"] == "hello"
assert isinstance(test_store["string"], str)
assert test_store["none"] is None
assert isinstance(test_store["none"], type(None))
assert test_store["list"] == [1, 2, 3]
assert isinstance(test_store["list"], list)
assert test_store["dict"] == {"a": 1, "b": 2}
assert isinstance(test_store["dict"], dict)
assert test_store["tuple"] == (1, 2, 3)
assert isinstance(test_store["tuple"], tuple)
assert test_store["bytearray"] == bytearray(b"hello")
assert isinstance(test_store["bytearray"], bytearray)
assert test_store["memoryview"] == memoryview(b"hello")
assert isinstance(test_store["memoryview"], memoryview)
async def test_storage_clear():
"""
The clear method should remove all items from the storage object.
"""
test_store["a"] = 1
test_store["b"] = 2
assert len(test_store) == 2
test_store.clear()
assert len(test_store) == 0

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
"""
Exercise the pyscript.Websocket class.
"""
import asyncio
from pyscript import WebSocket
async def test_websocket_with_attributes():
"""
Event handlers assigned via object attributes.
The Websocket class should be able to connect to a websocket server and
send and receive messages.
Use of echo.websocket.org means:
1) When connecting it responds with a "Request served by" message.
2) When sending a message it echos it back.
"""
connected_flag = False
closed_flag = False
messages = []
ready_to_test = asyncio.Event()
def on_open(event):
nonlocal connected_flag
connected_flag = True
ws.send("Hello, world!") # A message to echo.
def on_message(event):
messages.append(event.data)
if len(messages) == 2: # We're done.
ws.close()
def on_close(event):
nonlocal closed_flag
closed_flag = True
ready_to_test.set() # Finished!
ws = WebSocket(url="wss://echo.websocket.org")
ws.onopen = on_open
ws.onmessage = on_message
ws.onclose = on_close
# Wait for everything to be finished.
await ready_to_test.wait()
assert connected_flag is True
assert len(messages) == 2
assert "request served by" in messages[0].lower()
assert messages[1] == "Hello, world!"
assert closed_flag is True
async def test_websocket_with_init():
"""
Event handlers assigned via __init__ arguments.
The Websocket class should be able to connect to a websocket server and
send and receive messages.
Use of echo.websocket.org means:
1) When connecting it responds with a "Request served by" message.
2) When sending a message it echos it back.
"""
connected_flag = False
closed_flag = False
messages = []
ready_to_test = asyncio.Event()
def on_open(event):
nonlocal connected_flag
connected_flag = True
ws.send("Hello, world!") # A message to echo.
def on_message(event):
messages.append(event.data)
if len(messages) == 2: # We're done.
ws.close()
def on_close(event):
nonlocal closed_flag
closed_flag = True
ready_to_test.set() # Finished!
ws = WebSocket(
url="wss://echo.websocket.org",
onopen=on_open,
onmessage=on_message,
onclose=on_close,
)
# Wait for everything to be finished.
await ready_to_test.wait()
assert connected_flag is True
assert len(messages) == 2
assert "request served by" in messages[0].lower()
assert messages[1] == "Hello, world!"
assert closed_flag is True

View File

@@ -0,0 +1,216 @@
"""
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)

View File

@@ -0,0 +1,25 @@
"""
Ensure the pyscript.window object refers to the main thread's window object.
"""
import upytest
from pyscript import RUNNING_IN_WORKER, window
@upytest.skip("Main thread only.", skip_when=RUNNING_IN_WORKER)
def test_window_in_main_thread():
"""
The window object should refer to the main thread's window object.
"""
# The window will have a document.
assert window.document
@upytest.skip("Worker only.", skip_when=not RUNNING_IN_WORKER)
def test_window_in_worker():
"""
The window object should refer to the worker's self object, even though
this code is running in a web worker.
"""
# The window will have a document.
assert window.document