mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-22 11:45:28 -05:00
next integration tests (#1712)
* move integration tests pyscriptjs/tests/integration ->pyscript.core/tests/integration * add information in regards to how to run integration tests to README * fix fake server build paths * fix paths to build and run tests. The change of path before integration tests is a glitch maybe due to pytest cache? * remove test files created by mistake * update readme with latest changes --------- Co-authored-by: Fabio Pliger <fpliger@anaconda.com>
This commit is contained in:
@@ -1,184 +0,0 @@
|
||||
import shutil
|
||||
import threading
|
||||
from http.server import HTTPServer as SuperHTTPServer
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import Logger
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
"""
|
||||
If we pass --clear-http-cache, we don't enter the main pytest logic, but
|
||||
use our custom main instead
|
||||
"""
|
||||
|
||||
def mymain(config, session):
|
||||
print()
|
||||
print("-" * 20, "SmartRouter HTTP cache", "-" * 20)
|
||||
# unfortunately pytest-cache doesn't offer a public API to selectively
|
||||
# clear the cache, so we need to peek its internal. The good news is
|
||||
# that pytest-cache is very old, stable and robust, so it's likely
|
||||
# that this won't break anytime soon.
|
||||
cache = config.cache
|
||||
base = cache._cachedir.joinpath(cache._CACHE_PREFIX_VALUES, "pyscript")
|
||||
if not base.exists():
|
||||
print("No cache found, nothing to do")
|
||||
return 0
|
||||
#
|
||||
print("Requests found in the cache:")
|
||||
for f in base.rglob("*"):
|
||||
if f.is_file():
|
||||
# requests are saved in dirs named pyscript/http:/foo/bar, let's turn
|
||||
# them into a proper url
|
||||
url = str(f.relative_to(base))
|
||||
url = url.replace(":/", "://")
|
||||
print(" ", url)
|
||||
shutil.rmtree(base)
|
||||
print("Cache cleared")
|
||||
return 0
|
||||
|
||||
if config.option.clear_http_cache:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, mymain)
|
||||
return None
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""
|
||||
THIS IS A WORKAROUND FOR A pytest QUIRK!
|
||||
|
||||
At the moment of writing this conftest defines two new options, --dev and
|
||||
--no-fake-server, but because of how pytest works, they are available only
|
||||
if this is the "root conftest" for the test session.
|
||||
|
||||
This means that if you are in the pyscriptjs directory:
|
||||
|
||||
$ py.test # does NOT work
|
||||
$ py.test tests/integration/ # works
|
||||
|
||||
This happens because there is also test py-unit directory, so in the first
|
||||
case the "root conftest" would be tests/conftest.py (which doesn't exist)
|
||||
instead of this.
|
||||
|
||||
There are various workarounds, but for now we can just detect it and
|
||||
inform the user.
|
||||
|
||||
Related StackOverflow answer: https://stackoverflow.com/a/51733980
|
||||
"""
|
||||
if not hasattr(config.option, "dev"):
|
||||
msg = """
|
||||
Running a bare "pytest" command from the pyscriptjs directory
|
||||
is not supported. Please use one of the following commands:
|
||||
- pytest tests/integration
|
||||
- pytest tests/py-unit
|
||||
- pytest tests/*
|
||||
- cd tests/integration; pytest
|
||||
"""
|
||||
pytest.fail(msg)
|
||||
else:
|
||||
if config.option.dev:
|
||||
config.option.headed = True
|
||||
config.option.no_fake_server = True
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def logger():
|
||||
return Logger()
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--no-fake-server",
|
||||
action="store_true",
|
||||
help="Use a real HTTP server instead of http://fakeserver",
|
||||
)
|
||||
parser.addoption(
|
||||
"--dev",
|
||||
action="store_true",
|
||||
help="Automatically open a devtools panel. Implies --headed and --no-fake-server",
|
||||
)
|
||||
parser.addoption(
|
||||
"--clear-http-cache",
|
||||
action="store_true",
|
||||
help="Clear the cache of HTTP requests for SmartRouter",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def browser_type_launch_args(request):
|
||||
"""
|
||||
Override the browser_type_launch_args defined by pytest-playwright to
|
||||
support --devtools.
|
||||
|
||||
NOTE: this has been tested with pytest-playwright==0.3.0. It might break
|
||||
with newer versions of it.
|
||||
"""
|
||||
# this calls the "original" fixture defined by pytest_playwright.py
|
||||
launch_options = request.getfixturevalue("browser_type_launch_args")
|
||||
if request.config.option.dev:
|
||||
launch_options["devtools"] = True
|
||||
return launch_options
|
||||
|
||||
|
||||
class DevServer(SuperHTTPServer):
|
||||
"""
|
||||
Class for wrapper to run SimpleHTTPServer on Thread.
|
||||
Ctrl +Only Thread remains dead when terminated with C.
|
||||
Keyboard Interrupt passes.
|
||||
"""
|
||||
|
||||
def __init__(self, base_url, *args, **kwargs):
|
||||
self.base_url = base_url
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.server_close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def dev_server(logger):
|
||||
class MyHTTPRequestHandler(SimpleHTTPRequestHandler):
|
||||
enable_cors_headers = True
|
||||
|
||||
@classmethod
|
||||
def my_headers(cls):
|
||||
if cls.enable_cors_headers:
|
||||
return {
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
}
|
||||
return {}
|
||||
|
||||
def end_headers(self):
|
||||
self.send_my_headers()
|
||||
SimpleHTTPRequestHandler.end_headers(self)
|
||||
|
||||
def send_my_headers(self):
|
||||
for k, v in self.my_headers().items():
|
||||
self.send_header(k, v)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
logger.log("http_server", fmt % args, color="blue")
|
||||
|
||||
host, port = "localhost", 8080
|
||||
base_url = f"http://{host}:{port}"
|
||||
|
||||
# serve_Run forever under thread
|
||||
server = DevServer(base_url, (host, port), MyHTTPRequestHandler)
|
||||
|
||||
thread = threading.Thread(None, server.run)
|
||||
thread.start()
|
||||
|
||||
yield server # Transition to test here
|
||||
|
||||
# End thread
|
||||
server.shutdown()
|
||||
thread.join()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,495 +0,0 @@
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import (
|
||||
PageErrors,
|
||||
PageErrorsDidNotRaise,
|
||||
PyScriptTest,
|
||||
with_execution_thread,
|
||||
)
|
||||
|
||||
|
||||
@with_execution_thread(None)
|
||||
class TestSupport(PyScriptTest):
|
||||
"""
|
||||
These are NOT tests about PyScript.
|
||||
|
||||
They test the PyScriptTest class, i.e. we want to ensure that all the
|
||||
testing machinery that we have works correctly.
|
||||
"""
|
||||
|
||||
def test_basic(self):
|
||||
"""
|
||||
Very basic test, just to check that we can write, serve and read a simple
|
||||
HTML (no pyscript yet)
|
||||
"""
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello world</h1>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
content = self.page.content()
|
||||
assert "<h1>Hello world</h1>" in content
|
||||
|
||||
def test_await_with_run_js(self):
|
||||
self.run_js(
|
||||
"""
|
||||
function resolveAfter200MilliSeconds(x) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(x);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
const x = await resolveAfter200MilliSeconds(10);
|
||||
console.log(x);
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-1] == "10"
|
||||
|
||||
def test_console(self):
|
||||
"""
|
||||
Test that we capture console.log messages correctly.
|
||||
"""
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
console.log("my log 1");
|
||||
console.debug("my debug");
|
||||
console.info("my info");
|
||||
console.error("my error");
|
||||
console.warn("my warning");
|
||||
console.log("my log 2");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
assert len(self.console.all.messages) == 6
|
||||
assert self.console.all.lines == [
|
||||
"my log 1",
|
||||
"my debug",
|
||||
"my info",
|
||||
"my error",
|
||||
"my warning",
|
||||
"my log 2",
|
||||
]
|
||||
|
||||
# fmt: off
|
||||
assert self.console.all.text == textwrap.dedent("""
|
||||
my log 1
|
||||
my debug
|
||||
my info
|
||||
my error
|
||||
my warning
|
||||
my log 2
|
||||
""").strip()
|
||||
# fmt: on
|
||||
|
||||
assert self.console.log.lines == ["my log 1", "my log 2"]
|
||||
assert self.console.debug.lines == ["my debug"]
|
||||
|
||||
def test_check_js_errors_simple(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.check_js_errors()
|
||||
# check that the exception message contains the error message and the
|
||||
# stack trace
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
f"""
|
||||
JS errors found: 1
|
||||
Error: this is an error
|
||||
at {self.http_server_addr}/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
#
|
||||
# after a call to check_js_errors, the errors are cleared
|
||||
self.check_js_errors()
|
||||
#
|
||||
# JS exceptions are also available in self.console.js_error
|
||||
assert self.console.js_error.lines[0].startswith("Error: this is an error")
|
||||
|
||||
def test_check_js_errors_expected(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
self.check_js_errors("this is an error")
|
||||
|
||||
def test_check_js_errors_expected_but_didnt_raise(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error 2');</script>
|
||||
<script>throw new Error('this is an error 4');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrorsDidNotRaise) as exc:
|
||||
self.check_js_errors(
|
||||
"this is an error 1",
|
||||
"this is an error 2",
|
||||
"this is an error 3",
|
||||
"this is an error 4",
|
||||
)
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
"""
|
||||
The following JS errors were expected but could not be found:
|
||||
- this is an error 1
|
||||
- this is an error 3
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
|
||||
def test_check_js_errors_multiple(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('error 1');</script>
|
||||
<script>throw new Error('error 2');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.check_js_errors()
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
"""
|
||||
JS errors found: 2
|
||||
Error: error 1
|
||||
at https://fake_server/mytest.html:.*
|
||||
Error: error 2
|
||||
at https://fake_server/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
#
|
||||
# check that errors are cleared
|
||||
self.check_js_errors()
|
||||
|
||||
def test_check_js_errors_some_expected_but_others_not(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('expected 1');</script>
|
||||
<script>throw new Error('NOT expected 2');</script>
|
||||
<script>throw new Error('expected 3');</script>
|
||||
<script>throw new Error('NOT expected 4');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.check_js_errors("expected 1", "expected 3")
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
"""
|
||||
JS errors found: 2
|
||||
Error: NOT expected 2
|
||||
at https://fake_server/mytest.html:.*
|
||||
Error: NOT expected 4
|
||||
at https://fake_server/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
|
||||
def test_check_js_errors_expected_not_found_but_other_errors(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('error 1');</script>
|
||||
<script>throw new Error('error 2');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrorsDidNotRaise) as exc:
|
||||
self.check_js_errors("this is not going to be found")
|
||||
#
|
||||
msg = str(exc.value)
|
||||
expected = textwrap.dedent(
|
||||
"""
|
||||
The following JS errors were expected but could not be found:
|
||||
- this is not going to be found
|
||||
---
|
||||
The following JS errors were raised but not expected:
|
||||
Error: error 1
|
||||
at https://fake_server/mytest.html:.*
|
||||
Error: error 2
|
||||
at https://fake_server/mytest.html:.*
|
||||
"""
|
||||
).strip()
|
||||
assert re.search(expected, msg)
|
||||
|
||||
def test_clear_js_errors(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
self.clear_js_errors()
|
||||
# self.check_js_errors does not raise, because the errors have been
|
||||
# cleared
|
||||
self.check_js_errors()
|
||||
|
||||
def test_wait_for_console_simple(self):
|
||||
"""
|
||||
Test that self.wait_for_console actually waits.
|
||||
If it's buggy, the test will try to read self.console.log BEFORE the
|
||||
log has been written and it will fail.
|
||||
"""
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
console.log('Page loaded!');
|
||||
}, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
# we use a timeout of 200ms to give plenty of time to the page to
|
||||
# actually run the setTimeout callback
|
||||
self.wait_for_console("Page loaded!", timeout=200)
|
||||
assert self.console.log.lines[-1] == "Page loaded!"
|
||||
|
||||
def test_wait_for_console_timeout(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(TimeoutError):
|
||||
self.wait_for_console("This text will never be printed", timeout=200)
|
||||
|
||||
def test_wait_for_console_dont_wait_if_already_emitted(self):
|
||||
"""
|
||||
If the text is already on the console, wait_for_console() should return
|
||||
immediately without waiting.
|
||||
"""
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
console.log('Hello world')
|
||||
console.log('Page loaded!');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
self.wait_for_console("Page loaded!", timeout=200)
|
||||
assert self.console.log.lines[-2] == "Hello world"
|
||||
assert self.console.log.lines[-1] == "Page loaded!"
|
||||
# the following call should return immediately without waiting
|
||||
self.wait_for_console("Hello world", timeout=1)
|
||||
|
||||
def test_wait_for_console_exception_1(self):
|
||||
"""
|
||||
Test that if a JS exception is raised while waiting for the console, we
|
||||
report the exception and not the timeout.
|
||||
|
||||
There are two main cases:
|
||||
1. there is an exception and the console message does not appear
|
||||
2. there is an exception but the console message appears anyway
|
||||
|
||||
This test checks for case 1. Case 2 is tested by
|
||||
test_wait_for_console_exception_2
|
||||
"""
|
||||
# case 1: there is an exception and the console message does not appear
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>throw new Error('this is an error');</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
# "Page loaded!" will never appear, of course.
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.wait_for_console("Page loaded!", timeout=200)
|
||||
assert "this is an error" in str(exc.value)
|
||||
assert isinstance(exc.value.__context__, TimeoutError)
|
||||
#
|
||||
# if we use check_js_errors=False, the error are ignored, but we get the
|
||||
# Timeout anyway
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(TimeoutError):
|
||||
self.wait_for_console("Page loaded!", timeout=200, check_js_errors=False)
|
||||
# we still got a PageErrors, so we need to manually clear it, else the
|
||||
# test fails at teardown
|
||||
self.clear_js_errors()
|
||||
|
||||
def test_wait_for_console_exception_2(self):
|
||||
"""
|
||||
See the description in test_wait_for_console_exception_1.
|
||||
"""
|
||||
# case 2: there is an exception, but the console message appears
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
console.log('Page loaded!');
|
||||
}, 100);
|
||||
throw new Error('this is an error');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(PageErrors) as exc:
|
||||
self.wait_for_console("Page loaded!", timeout=200)
|
||||
assert "this is an error" in str(exc.value)
|
||||
#
|
||||
# with check_js_errors=False, the Error is ignored and the
|
||||
# wait_for_console succeeds
|
||||
self.goto("mytest.html")
|
||||
self.wait_for_console("Page loaded!", timeout=200, check_js_errors=False)
|
||||
# clear the errors, else the test fails at teardown
|
||||
self.clear_js_errors()
|
||||
|
||||
def test_wait_for_console_match_substring(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
console.log('Foo Bar Baz');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
with pytest.raises(TimeoutError):
|
||||
self.wait_for_console("Bar", timeout=200)
|
||||
#
|
||||
self.wait_for_console("Bar", timeout=200, match_substring=True)
|
||||
assert self.console.log.lines[-1] == "Foo Bar Baz"
|
||||
|
||||
def test_iter_locator(self):
|
||||
doc = """
|
||||
<html>
|
||||
<body>
|
||||
<div>foo</div>
|
||||
<div>bar</div>
|
||||
<div>baz</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
self.goto("mytest.html")
|
||||
divs = self.page.locator("div")
|
||||
assert divs.count() == 3
|
||||
texts = [el.inner_text() for el in self.iter_locator(divs)]
|
||||
assert texts == ["foo", "bar", "baz"]
|
||||
|
||||
def test_smartrouter_cache(self):
|
||||
if self.router is None:
|
||||
pytest.skip("Cannot test SmartRouter with --dev")
|
||||
|
||||
# this is not an image but who cares, I just want the browser to make
|
||||
# an HTTP request
|
||||
URL = "https://raw.githubusercontent.com/pyscript/pyscript/main/README.md"
|
||||
doc = f"""
|
||||
<html>
|
||||
<body>
|
||||
<img src="{URL}">
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("mytest.html", doc)
|
||||
#
|
||||
self.router.clear_cache(URL)
|
||||
self.goto("mytest.html")
|
||||
assert self.router.requests == [
|
||||
(200, "fake_server", "https://fake_server/mytest.html"),
|
||||
(200, "NETWORK", URL),
|
||||
]
|
||||
#
|
||||
# let's visit the page again, now it should be cached
|
||||
self.goto("mytest.html")
|
||||
assert self.router.requests == [
|
||||
# 1st visit
|
||||
(200, "fake_server", "https://fake_server/mytest.html"),
|
||||
(200, "NETWORK", URL),
|
||||
# 2nd visit
|
||||
(200, "fake_server", "https://fake_server/mytest.html"),
|
||||
(200, "CACHED", URL),
|
||||
]
|
||||
|
||||
def test_404(self):
|
||||
"""
|
||||
Test that we capture a 404 in loading a page that does not exist.
|
||||
"""
|
||||
self.goto("this_url_does_not_exist.html")
|
||||
assert [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
] == self.console.all.lines
|
||||
|
||||
def test__pyscript_format_inject_execution_thread(self):
|
||||
"""
|
||||
This is slightly different than other tests: it doesn't use playwright, it
|
||||
just tests that our own internal helper works
|
||||
"""
|
||||
doc = self._pyscript_format("<b>Hello</b>", execution_thread="main")
|
||||
cfg = self._parse_py_config(doc)
|
||||
assert cfg == {"execution_thread": "main"}
|
||||
|
||||
def test__pyscript_format_modify_existing_py_config(self):
|
||||
src = """
|
||||
<py-config>
|
||||
hello = 42
|
||||
</py-config>
|
||||
"""
|
||||
doc = self._pyscript_format(src, execution_thread="main")
|
||||
cfg = self._parse_py_config(doc)
|
||||
assert cfg == {"execution_thread": "main", "hello": 42}
|
||||
@@ -1,353 +0,0 @@
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
class TestBasic(PyScriptTest):
|
||||
def test_pyscript_hello(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py">
|
||||
import js
|
||||
js.console.log('hello pyscript')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines == ["hello pyscript"]
|
||||
|
||||
def test_execution_thread(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<!-- we don't really need anything here, we just want to check that
|
||||
pyscript does not bootstrap -->
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
assert self.execution_thread in ("main", "worker")
|
||||
if self.execution_thread == "main":
|
||||
pass
|
||||
elif self.execution_thread == "worker":
|
||||
pass
|
||||
assert self.console.log.lines == []
|
||||
|
||||
# TODO: if there's no py-script there are surely no plugins neither
|
||||
# this test must be discussed or rewritten to make sense now
|
||||
@pytest.mark.skip(
|
||||
reason="FIXME: No banner and should also add a WARNING about CORS"
|
||||
)
|
||||
def test_no_cors_headers(self):
|
||||
self.disable_cors_headers()
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<!-- we don't really need anything here, we just want to check that
|
||||
pyscript starts -->
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
assert self.headers == {}
|
||||
if self.execution_thread == "worker":
|
||||
expected_alert_banner_msg = (
|
||||
'(PY1000): When execution_thread is "worker", the site must be cross origin '
|
||||
"isolated, but crossOriginIsolated is false. To be cross origin isolated, "
|
||||
"the server must use https and also serve with the following headers: "
|
||||
'{"Cross-Origin-Embedder-Policy":"require-corp",'
|
||||
'"Cross-Origin-Opener-Policy":"same-origin"}. '
|
||||
"The problem may be that one or both of these are missing."
|
||||
)
|
||||
alert_banner = self.page.wait_for_selector(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
else:
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_print(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
print('hello pyscript')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello pyscript"
|
||||
|
||||
def test_python_exception(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
print('hello pyscript')
|
||||
raise Exception('this is an error')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert "hello pyscript" in self.console.log.lines
|
||||
self.check_py_errors("Exception: this is an error")
|
||||
#
|
||||
# check that we sent the traceback to the console
|
||||
tb_lines = self.console.error.lines[-1].splitlines()
|
||||
assert tb_lines[0] == "PythonError: Traceback (most recent call last):"
|
||||
#
|
||||
# check that we show the traceback in the page. Note that here we
|
||||
# display the "raw" python traceback, without the "[pyexec] Python
|
||||
# exception:" line (which is useful in the console, but not for the
|
||||
# user)
|
||||
banner = self.page.locator(".py-error")
|
||||
tb_lines = banner.inner_text().splitlines()
|
||||
assert tb_lines[0] == "Traceback (most recent call last):"
|
||||
assert tb_lines[-1] == "Exception: this is an error"
|
||||
|
||||
def test_python_exception_in_event_handler(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button py-click="onclick">Click me</button>
|
||||
<py-script>
|
||||
def onclick(event):
|
||||
raise Exception("this is an error inside handler")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.page.locator("button").click()
|
||||
self.wait_for_console(
|
||||
"Exception: this is an error inside handler", match_substring=True
|
||||
)
|
||||
|
||||
self.check_py_errors("Exception: this is an error inside handler")
|
||||
|
||||
## error in console
|
||||
tb_lines = self.console.error.lines[-1].splitlines()
|
||||
assert tb_lines[0] == "PythonError: Traceback (most recent call last):"
|
||||
|
||||
## error in DOM
|
||||
tb_lines = self.page.locator(".py-error").inner_text().splitlines()
|
||||
assert tb_lines[0] == "Traceback (most recent call last):"
|
||||
assert tb_lines[-1] == "Exception: this is an error inside handler"
|
||||
|
||||
def test_execution_in_order(self):
|
||||
"""
|
||||
Check that they py-script tags are executed in the same order they are
|
||||
defined
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>import js; js.console.log('one')</py-script>
|
||||
<py-script>js.console.log('two')</py-script>
|
||||
<py-script>js.console.log('three')</py-script>
|
||||
<py-script>js.console.log('four')</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-4:] == [
|
||||
"one",
|
||||
"two",
|
||||
"three",
|
||||
"four",
|
||||
]
|
||||
|
||||
def test_escaping_of_angle_brackets(self):
|
||||
"""
|
||||
Check that py-script tags escape angle brackets
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>import js; js.console.log(1<2, 1>2)</py-script>
|
||||
<py-script>js.console.log("<div></div>")</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-2:] == ["true false", "<div></div>"]
|
||||
|
||||
@pytest.mark.skip(reason="FIX TEST: Works on CHROME")
|
||||
def test_packages(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
packages = ["asciitree"]
|
||||
</py-config>
|
||||
<py-script>
|
||||
import js
|
||||
import asciitree
|
||||
js.console.log('hello', asciitree.__name__)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-3:] == [
|
||||
"Loading asciitree", # printed by pyodide
|
||||
"Loaded asciitree", # printed by pyodide
|
||||
"hello asciitree", # printed by us
|
||||
]
|
||||
|
||||
# TODO: if there's no py-script there are surely no plugins neither
|
||||
# this test must be discussed or rewritten to make sense now
|
||||
@pytest.mark.skip("FIXME: No banner")
|
||||
def test_non_existent_package(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
packages = ["i-dont-exist"]
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
"(PY1001): Unable to install package(s) 'i-dont-exist'. "
|
||||
"Unable to find package in PyPI. Please make sure you have "
|
||||
"entered a correct package name."
|
||||
)
|
||||
|
||||
alert_banner = self.page.wait_for_selector(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
self.check_py_errors("Can't fetch metadata for 'i-dont-exist'")
|
||||
|
||||
# TODO: if there's no py-script there are surely no plugins neither
|
||||
# this test must be discussed or rewritten to make sense now
|
||||
@pytest.mark.skip("FIXME: No banner")
|
||||
def test_no_python_wheel(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
packages = ["opsdroid"]
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
"(PY1001): Unable to install package(s) 'opsdroid'. "
|
||||
"Reason: Can't find a pure Python 3 Wheel for package(s) 'opsdroid'"
|
||||
)
|
||||
|
||||
alert_banner = self.page.wait_for_selector(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
self.check_py_errors("Can't find a pure Python 3 wheel for 'opsdroid'")
|
||||
|
||||
def test_dynamically_add_py_script_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script>
|
||||
function addPyScriptTag(event) {
|
||||
let tag = document.createElement('py-script');
|
||||
tag.innerHTML = "print('hello world')";
|
||||
document.body.appendChild(tag);
|
||||
}
|
||||
addPyScriptTag()
|
||||
</script>
|
||||
""",
|
||||
timeout=20000,
|
||||
)
|
||||
self.page.locator("py-script")
|
||||
|
||||
assert self.console.log.lines[-1] == "hello world"
|
||||
|
||||
def test_py_script_src_attribute(self):
|
||||
self.writefile("foo.py", "print('hello from foo')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script src="foo.py"></py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello from foo"
|
||||
|
||||
def test_py_script_src_not_found(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script src="foo.py"></py-script>
|
||||
""",
|
||||
check_js_errors=False,
|
||||
)
|
||||
assert "Failed to load resource" in self.console.error.lines[0]
|
||||
|
||||
# TODO: we need to be sure errors make sense from both main and worker worlds
|
||||
# expected_msg = "(PY0404): Fetching from URL foo.py failed with error 404"
|
||||
# assert any((expected_msg in line) for line in self.console.js_error.lines)
|
||||
# assert self.assert_banner_message(expected_msg)
|
||||
|
||||
# pyscript_tag = self.page.locator("py-script")
|
||||
# assert pyscript_tag.inner_html() == ""
|
||||
|
||||
# self.check_js_errors(expected_msg)
|
||||
|
||||
# TODO: ... and we shouldn't: it's a module and we better don't leak in global
|
||||
@pytest.mark.skip("DIFFERENT BEHAVIOUR: we don't expose pyscript on window")
|
||||
def test_js_version(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.add_script_tag(content="console.log(pyscript.version)")
|
||||
|
||||
assert (
|
||||
re.match(r"\d{4}\.\d{2}\.\d+(\.[a-zA-Z0-9]+)?", self.console.log.lines[-1])
|
||||
is not None
|
||||
)
|
||||
|
||||
# TODO: ... and we shouldn't: it's a module and we better don't leak in global
|
||||
@pytest.mark.skip("DIFFERENT BEHAVIOUR: we don't expose pyscript on window")
|
||||
def test_python_version(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
import js
|
||||
js.console.log(pyscript.__version__)
|
||||
js.console.log(str(pyscript.version_info))
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert (
|
||||
re.match(r"\d{4}\.\d{2}\.\d+(\.[a-zA-Z0-9]+)?", self.console.log.lines[-2])
|
||||
is not None
|
||||
)
|
||||
assert (
|
||||
re.match(
|
||||
r"version_info\(year=\d{4}, month=\d{2}, "
|
||||
r"minor=\d+, releaselevel='([a-zA-Z0-9]+)?'\)",
|
||||
self.console.log.lines[-1],
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
||||
def test_assert_no_banners(self):
|
||||
"""
|
||||
Test that the DOM doesn't contain error/warning banners
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
import sys
|
||||
print("hello world", file=sys.stderr)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.page.locator(".py-error").inner_text() == "hello world"
|
||||
|
||||
def test_getPySrc_returns_source_code(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>print("hello world!")</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
pyscript_tag = self.page.locator("py-script")
|
||||
assert pyscript_tag.inner_html() == ""
|
||||
assert pyscript_tag.evaluate("node => node.srcCode") == 'print("hello world!")'
|
||||
|
||||
@pytest.mark.skip(reason="FIX TEST: works in chrome!")
|
||||
def test_py_attribute_without_id(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button py-click="myfunc">Click me</button>
|
||||
<py-script>
|
||||
def myfunc(event):
|
||||
print("hello world!")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
btn = self.page.wait_for_selector("button")
|
||||
btn.click()
|
||||
self.wait_for_console("hello world!")
|
||||
assert self.console.log.lines[-1] == "hello world!"
|
||||
assert self.console.error.lines == []
|
||||
@@ -1,452 +0,0 @@
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from .support import (
|
||||
PyScriptTest,
|
||||
filter_inner_text,
|
||||
filter_page_content,
|
||||
wait_for_render,
|
||||
)
|
||||
|
||||
DISPLAY_OUTPUT_ID_PATTERN = r'[id^="py-"]'
|
||||
|
||||
|
||||
class TestDisplay(PyScriptTest):
|
||||
def test_simple_display(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
print('ciao')
|
||||
from pyscript import display
|
||||
display("hello world")
|
||||
</py-script>
|
||||
""",
|
||||
timeout=20000,
|
||||
)
|
||||
node_list = self.page.query_selector_all(DISPLAY_OUTPUT_ID_PATTERN)
|
||||
pattern = r"<div>hello world</div>"
|
||||
assert node_list[0].inner_html() == pattern
|
||||
assert len(node_list) == 1
|
||||
|
||||
def test_consecutive_display(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
display('hello 1')
|
||||
</py-script>
|
||||
<p>hello 2</p>
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
display('hello 3')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("body")
|
||||
lines = inner_text.splitlines()
|
||||
|
||||
lines = [line for line in filter_page_content(lines)] # remove empty lines
|
||||
assert lines == ["hello 1", "hello 2", "hello 3"]
|
||||
|
||||
def test_target_attribute(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
display('hello world', target="mydiv")
|
||||
</py-script>
|
||||
<div id="mydiv"></div>
|
||||
"""
|
||||
)
|
||||
mydiv = self.page.locator("#mydiv")
|
||||
assert mydiv.inner_text() == "hello world"
|
||||
|
||||
def test_consecutive_display_target(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script id="first">
|
||||
from pyscript import display
|
||||
display('hello 1')
|
||||
</py-script>
|
||||
<p>hello in between 1 and 2</p>
|
||||
<py-script id="second">
|
||||
from pyscript import display
|
||||
display('hello 2', target="second")
|
||||
</py-script>
|
||||
<py-script id="third">
|
||||
from pyscript import display
|
||||
display('hello 3')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("body")
|
||||
lines = inner_text.splitlines()
|
||||
lines = [line for line in filter_page_content(lines)] # remove empty lines
|
||||
assert lines == ["hello 1", "hello in between 1 and 2", "hello 2", "hello 3"]
|
||||
|
||||
def test_multiple_display_calls_same_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
display('hello')
|
||||
display('world')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
tag = self.page.locator("py-script")
|
||||
lines = tag.inner_text().splitlines()
|
||||
assert lines == ["hello", "world"]
|
||||
|
||||
def test_implicit_target_from_a_different_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script id="py1">
|
||||
from pyscript import display
|
||||
def say_hello():
|
||||
display('hello')
|
||||
</py-script>
|
||||
|
||||
<py-script id="py2">
|
||||
from pyscript import display
|
||||
say_hello()
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
py1 = self.page.locator("#py1")
|
||||
py2 = self.page.locator("#py2")
|
||||
assert py1.inner_text() == ""
|
||||
assert py2.inner_text() == "hello"
|
||||
|
||||
def test_no_explicit_target(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
def display_hello(error):
|
||||
display('hello world')
|
||||
</py-script>
|
||||
<button id="my-button" py-click="display_hello">Click me</button>
|
||||
"""
|
||||
)
|
||||
self.page.locator("button").click()
|
||||
|
||||
text = self.page.locator("py-script").text_content()
|
||||
assert "hello world" in text
|
||||
|
||||
def test_explicit_target_pyscript_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
def display_hello():
|
||||
display('hello', target='second-pyscript-tag')
|
||||
</py-script>
|
||||
<py-script id="second-pyscript-tag">
|
||||
display_hello()
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
text = self.page.locator("id=second-pyscript-tag").inner_text()
|
||||
assert text == "hello"
|
||||
|
||||
def test_explicit_target_on_button_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
def display_hello(error):
|
||||
display('hello', target='my-button')
|
||||
</py-script>
|
||||
<button id="my-button" py-click="display_hello">Click me</button>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=Click me").click()
|
||||
text = self.page.locator("id=my-button").inner_text()
|
||||
assert "hello" in text
|
||||
|
||||
def test_explicit_different_target_from_call(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script id="first-pyscript-tag">
|
||||
from pyscript import display
|
||||
def display_hello():
|
||||
display('hello', target='second-pyscript-tag')
|
||||
</py-script>
|
||||
<py-script id="second-pyscript-tag">
|
||||
print('nothing to see here')
|
||||
</py-script>
|
||||
<py-script>
|
||||
display_hello()
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
text = self.page.locator("id=second-pyscript-tag").all_inner_texts()
|
||||
assert "hello" in text
|
||||
|
||||
def test_append_true(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
display('hello world', append=True)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
node_list = self.page.query_selector_all(DISPLAY_OUTPUT_ID_PATTERN)
|
||||
pattern = r"<div>hello world</div>"
|
||||
|
||||
assert node_list[0].inner_html() == pattern
|
||||
assert len(node_list) == 1
|
||||
|
||||
def test_append_false(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
display('hello world', append=False)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
inner_html = self.page.content()
|
||||
pattern = r'<py-script id="py-.*">hello world</py-script>'
|
||||
assert re.search(pattern, inner_html)
|
||||
|
||||
def test_display_multiple_values(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
hello = 'hello'
|
||||
world = 'world'
|
||||
display(hello, world)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("html")
|
||||
assert inner_text == "hello\nworld"
|
||||
|
||||
def test_display_multiple_append_false(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
display('hello', append=False)
|
||||
display('world', append=False)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
inner_html = self.page.content()
|
||||
pattern = r'<py-script id="py-.*">world</py-script>'
|
||||
assert re.search(pattern, inner_html)
|
||||
|
||||
# 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
|
||||
def test_display_multiple_append_false_with_target(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="circle-div"></div>
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
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" /></svg>'
|
||||
)
|
||||
|
||||
circle = Circle()
|
||||
|
||||
circle.r += 5
|
||||
# display(circle, target="circle-div", append=False)
|
||||
circle.r += 5
|
||||
display(circle, target="circle-div", append=False)
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
innerhtml = self.page.locator("id=circle-div").inner_html()
|
||||
assert (
|
||||
innerhtml
|
||||
== '<svg height="20" width="20"><circle cx="10" cy="10" r="10" fill="red"></circle></svg>' # noqa: E501
|
||||
)
|
||||
assert self.console.error.lines == []
|
||||
|
||||
def test_display_list_dict_tuple(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
l = ['A', 1, '!']
|
||||
d = {'B': 2, 'List': l}
|
||||
t = ('C', 3, '!')
|
||||
display(l, d, t)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("html")
|
||||
filtered_inner_text = filter_inner_text(inner_text)
|
||||
print(filtered_inner_text)
|
||||
assert (
|
||||
filtered_inner_text
|
||||
== "['A', 1, '!']\n{'B': 2, 'List': ['A', 1, '!']}\n('C', 3, '!')"
|
||||
)
|
||||
|
||||
def test_display_should_escape(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
display("<p>hello world</p>")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
# out = self.page.locator("py-script > div")
|
||||
node_list = self.page.query_selector_all(DISPLAY_OUTPUT_ID_PATTERN)
|
||||
node_list[0]
|
||||
# assert out.inner_html() == html.escape("<p>hello world</p>")
|
||||
# assert out.inner_text() == "<p>hello world</p>"
|
||||
|
||||
@pytest.mark.skip("FIXME: HTML has been removed from pyscript")
|
||||
def test_display_HTML(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display, HTML
|
||||
display(HTML("<p>hello world</p>"))
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
# out = self.page.locator("py-script > div")
|
||||
node_list = self.page.query_selector_all(DISPLAY_OUTPUT_ID_PATTERN)
|
||||
node_list[0]
|
||||
# assert out.inner_html() == "<p>hello world</p>"
|
||||
# assert out.inner_text() == "hello world"
|
||||
|
||||
@pytest.mark.skip(
|
||||
"FIX TEST: Works correctly in Chrome, but fails in TEST with the error:\n\n"
|
||||
"It's likely that the Test framework injections in config are causing"
|
||||
"this error."
|
||||
)
|
||||
def test_image_display(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config> packages = ["matplotlib"] </py-config>
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
import matplotlib.pyplot as plt
|
||||
xpoints = [3, 6, 9]
|
||||
ypoints = [1, 2, 3]
|
||||
plt.plot(xpoints, ypoints)
|
||||
display(plt)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
wait_for_render(self.page, "*", "<img src=['\"]data:image")
|
||||
test = self.page.wait_for_selector("img")
|
||||
img_src = test.get_attribute("src").replace(
|
||||
"data:image/png;charset=utf-8;base64,", ""
|
||||
)
|
||||
img_data = np.asarray(Image.open(io.BytesIO(base64.b64decode(img_src))))
|
||||
with Image.open(
|
||||
os.path.join(os.path.dirname(__file__), "test_assets", "line_plot.png"),
|
||||
) as image:
|
||||
ref_data = np.asarray(image)
|
||||
|
||||
deviation = np.mean(np.abs(img_data - ref_data))
|
||||
assert deviation == 0.0
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_empty_HTML_and_console_output(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
import js
|
||||
print('print from python')
|
||||
js.console.log('print from js')
|
||||
js.console.error('error from js');
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
inner_html = self.page.content()
|
||||
assert re.search("", inner_html)
|
||||
console_text = self.console.all.lines
|
||||
assert "print from python" in console_text
|
||||
assert "print from js" in console_text
|
||||
assert "error from js" in console_text
|
||||
|
||||
def test_text_HTML_and_console_output(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
import js
|
||||
display('this goes to the DOM')
|
||||
print('print from python')
|
||||
js.console.log('print from js')
|
||||
js.console.error('error from js');
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("py-script")
|
||||
assert inner_text == "this goes to the DOM"
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"print from python",
|
||||
"print from js",
|
||||
]
|
||||
print(self.console.error.lines)
|
||||
assert self.console.error.lines[-1] == "error from js"
|
||||
|
||||
def test_console_line_break(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
print('1print\\n2print')
|
||||
print('1console\\n2console')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
console_text = self.console.all.lines
|
||||
assert console_text.index("1print") == (console_text.index("2print") - 1)
|
||||
assert console_text.index("1console") == (console_text.index("2console") - 1)
|
||||
|
||||
@pytest.mark.skip(
|
||||
"FIX TEST: Works correctly in Chrome, but fails in TEST with the error:\n\n"
|
||||
"It's likely that the Test framework injections in config are causing"
|
||||
"this error."
|
||||
)
|
||||
def test_image_renders_correctly(self):
|
||||
"""This is just a sanity check to make sure that images are rendered correctly."""
|
||||
buffer = io.BytesIO()
|
||||
img = Image.new("RGB", (4, 4), color=(0, 0, 0))
|
||||
img.save(buffer, format="PNG")
|
||||
|
||||
b64_img = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
expected_img_src = f"data:image/png;charset=utf-8;base64,{b64_img}"
|
||||
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
packages = ["pillow"]
|
||||
</py-config>
|
||||
|
||||
<div id="img-target" />
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
from PIL import Image
|
||||
img = Image.new("RGB", (4, 4), color=(0, 0, 0))
|
||||
display(img, target='img-target', append=False)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
rendered_img_src = self.page.locator("img").get_attribute("src")
|
||||
assert rendered_img_src == expected_img_src
|
||||
@@ -1,303 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
class TestElement(PyScriptTest):
|
||||
"""Test the Element api"""
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_id(self):
|
||||
"""Test the element id"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="foo"></div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
print(el.id)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "foo"
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert "foo" in py_terminal.inner_text()
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_value(self):
|
||||
"""Test the element value"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<input id="foo" value="bar">
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
print(el.value)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "bar"
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert "bar" in py_terminal.inner_text()
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_innerHtml(self):
|
||||
"""Test the element innerHtml"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="foo"><b>bar</b></div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
print(el.innerHtml)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "<b>bar</b>"
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert "bar" in py_terminal.inner_text()
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_write_no_append(self):
|
||||
"""Test the element write"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="foo"></div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.write("Hello!")
|
||||
el.write("World!")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
div = self.page.wait_for_selector("#foo")
|
||||
assert "World!" in div.inner_text()
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_write_append(self):
|
||||
"""Test the element write"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="foo"></div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.write("Hello!")
|
||||
el.write("World!", append=True)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
parent_div = self.page.wait_for_selector("#foo")
|
||||
|
||||
assert "Hello!" in parent_div.inner_text()
|
||||
# confirm that the second write was appended
|
||||
assert "Hello!<div>World!</div>" in parent_div.inner_html()
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_clear_div(self):
|
||||
"""Test the element clear"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="foo">Hello!</div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.clear()
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
div = self.page.locator("#foo")
|
||||
assert div.inner_text() == ""
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_clear_input(self):
|
||||
"""Test the element clear"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<input id="foo" value="bar">
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.clear()
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
input = self.page.wait_for_selector("#foo")
|
||||
assert input.input_value() == ""
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_select(self):
|
||||
"""Test the element select"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<select id="foo">
|
||||
<option value="bar">Bar</option>
|
||||
</select>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
js.console.log(el.select("option").value)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "bar"
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_select_content(self):
|
||||
"""Test the element select"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<template id="foo">
|
||||
<div>Bar</div>
|
||||
</template>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
js.console.log(el.select("div", from_content=True).innerHtml)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "Bar"
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_clone_no_id(self):
|
||||
"""Test the element clone"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="foo">Hello!</div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.clone()
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
divs = self.page.locator("#foo")
|
||||
assert divs.count() == 2
|
||||
assert divs.first.inner_text() == "Hello!"
|
||||
assert divs.last.inner_text() == "Hello!"
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_clone_with_id(self):
|
||||
"""Test the element clone"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="foo">Hello!</div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.clone(new_id="bar")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
divs = self.page.locator("#foo")
|
||||
assert divs.count() == 1
|
||||
assert divs.inner_text() == "Hello!"
|
||||
|
||||
clone = self.page.locator("#bar")
|
||||
assert clone.inner_text() == "Hello!"
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_clone_to_other_element(self):
|
||||
"""Test the element clone"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="container">
|
||||
<div id="bond">
|
||||
Bond
|
||||
</div>
|
||||
<div id="james">
|
||||
James
|
||||
</div>
|
||||
</div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
|
||||
bond_div = Element("bond")
|
||||
james_div = Element("james")
|
||||
|
||||
bond_div.clone(new_id="bond-2", to=james_div)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
bond_divs = self.page.locator("#bond")
|
||||
james_divs = self.page.locator("#james")
|
||||
bond_2_divs = self.page.locator("#bond-2")
|
||||
|
||||
assert bond_divs.count() == 1
|
||||
assert james_divs.count() == 1
|
||||
assert bond_2_divs.count() == 1
|
||||
|
||||
container_div = self.page.locator("#container")
|
||||
# Make sure that the clones are rendered in the right order
|
||||
assert container_div.inner_text() == "Bond\nJames\nBond"
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_remove_single_class(self):
|
||||
"""Test the element remove_class"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="foo" class="bar baz"></div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.remove_class("bar")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
div = self.page.locator("#foo")
|
||||
assert div.get_attribute("class") == "baz"
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_remove_multiple_classes(self):
|
||||
"""Test the element remove_class"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="foo" class="foo bar baz"></div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.remove_class(["foo", "baz", "bar"])
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
div = self.page.locator("#foo")
|
||||
assert div.get_attribute("class") == ""
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_add_single_class(self):
|
||||
"""Test the element add_class"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<style> .red { color: red; } </style>
|
||||
<div id="foo">Hi!</div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.add_class("red")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
div = self.page.locator("#foo")
|
||||
assert div.get_attribute("class") == "red"
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_element_add_multiple_class(self):
|
||||
"""Test the element add_class"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<style> .red { color: red; } .bold { font-weight: bold; } </style>
|
||||
<div id="foo">Hi!</div>
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
el = Element("foo")
|
||||
el.add_class(["red", "bold"])
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
div = self.page.locator("#foo")
|
||||
assert div.get_attribute("class") == "red bold"
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,197 +0,0 @@
|
||||
from .support import PyScriptTest, filter_inner_text
|
||||
|
||||
|
||||
class TestAsync(PyScriptTest):
|
||||
# ensure_future() and create_task() should behave similarly;
|
||||
# we'll use the same source code to test both
|
||||
coroutine_script = """
|
||||
<py-script>
|
||||
import js
|
||||
import asyncio
|
||||
js.console.log("first")
|
||||
async def main():
|
||||
await asyncio.sleep(1)
|
||||
js.console.log("third")
|
||||
asyncio.{func}(main())
|
||||
js.console.log("second")
|
||||
</py-script>
|
||||
"""
|
||||
|
||||
def test_asyncio_ensure_future(self):
|
||||
self.pyscript_run(self.coroutine_script.format(func="ensure_future"))
|
||||
self.wait_for_console("third")
|
||||
assert self.console.log.lines[-3:] == ["first", "second", "third"]
|
||||
|
||||
def test_asyncio_create_task(self):
|
||||
self.pyscript_run(self.coroutine_script.format(func="create_task"))
|
||||
self.wait_for_console("third")
|
||||
assert self.console.log.lines[-3:] == ["first", "second", "third"]
|
||||
|
||||
def test_asyncio_gather(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script id="pys">
|
||||
import asyncio
|
||||
import js
|
||||
from pyodide.ffi import to_js
|
||||
|
||||
async def coro(delay):
|
||||
await asyncio.sleep(delay)
|
||||
return(delay)
|
||||
|
||||
async def get_results():
|
||||
results = await asyncio.gather(*[coro(d) for d in range(3,0,-1)])
|
||||
js.console.log(str(results)) #Compare to string representation, not Proxy
|
||||
js.console.log("DONE")
|
||||
|
||||
asyncio.ensure_future(get_results())
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.wait_for_console("DONE")
|
||||
assert self.console.log.lines[-2:] == ["[3, 2, 1]", "DONE"]
|
||||
|
||||
def test_multiple_async(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
import js
|
||||
import asyncio
|
||||
async def a_func():
|
||||
for i in range(3):
|
||||
js.console.log('A', i)
|
||||
await asyncio.sleep(0.1)
|
||||
asyncio.ensure_future(a_func())
|
||||
</py-script>
|
||||
|
||||
<py-script>
|
||||
import js
|
||||
import asyncio
|
||||
async def b_func():
|
||||
for i in range(3):
|
||||
js.console.log('B', i)
|
||||
await asyncio.sleep(0.1)
|
||||
js.console.log('b func done')
|
||||
asyncio.ensure_future(b_func())
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.wait_for_console("b func done")
|
||||
assert self.console.log.lines == [
|
||||
"A 0",
|
||||
"B 0",
|
||||
"A 1",
|
||||
"B 1",
|
||||
"A 2",
|
||||
"B 2",
|
||||
"b func done",
|
||||
]
|
||||
|
||||
def test_multiple_async_multiple_display_targeted(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script id='pyA'>
|
||||
from pyscript import display
|
||||
import js
|
||||
import asyncio
|
||||
|
||||
async def a_func():
|
||||
for i in range(2):
|
||||
display(f'A{i}', target='pyA', append=True)
|
||||
await asyncio.sleep(0.1)
|
||||
asyncio.ensure_future(a_func())
|
||||
|
||||
</py-script>
|
||||
<py-script id='pyB'>
|
||||
from pyscript import display
|
||||
import js
|
||||
import asyncio
|
||||
|
||||
async def a_func():
|
||||
for i in range(2):
|
||||
display(f'B{i}', target='pyB', append=True)
|
||||
await asyncio.sleep(0.1)
|
||||
js.console.log("B DONE")
|
||||
|
||||
asyncio.ensure_future(a_func())
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.wait_for_console("B DONE")
|
||||
inner_text = self.page.inner_text("html")
|
||||
assert "A0\nA1\nB0\nB1" in filter_inner_text(inner_text)
|
||||
|
||||
def test_async_display_untargeted(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
from pyscript import display
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def a_func():
|
||||
display('A')
|
||||
await asyncio.sleep(1)
|
||||
js.console.log("DONE")
|
||||
|
||||
asyncio.ensure_future(a_func())
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.wait_for_console("DONE")
|
||||
assert self.page.locator("py-script").inner_text() == "A"
|
||||
|
||||
def test_sync_and_async_order(self):
|
||||
"""
|
||||
The order of execution is defined as follows:
|
||||
1. first, we execute all the py-script tag in order
|
||||
2. then, we start all the tasks which were scheduled with create_task
|
||||
|
||||
Note that tasks are started *AFTER* all py-script tags have been
|
||||
executed. That's why the console.log() inside mytask1 and mytask2 are
|
||||
executed after e.g. js.console.log("6").
|
||||
"""
|
||||
src = """
|
||||
<py-script>
|
||||
import js
|
||||
js.console.log("1")
|
||||
</py-script>
|
||||
|
||||
<py-script>
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def mytask1():
|
||||
js.console.log("7")
|
||||
await asyncio.sleep(0)
|
||||
js.console.log("9")
|
||||
|
||||
js.console.log("2")
|
||||
asyncio.create_task(mytask1())
|
||||
js.console.log("3")
|
||||
</py-script>
|
||||
|
||||
<py-script>
|
||||
import js
|
||||
js.console.log("4")
|
||||
</py-script>
|
||||
|
||||
<py-script>
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def mytask2():
|
||||
js.console.log("8")
|
||||
await asyncio.sleep(0)
|
||||
js.console.log("10")
|
||||
js.console.log("DONE")
|
||||
|
||||
js.console.log("5")
|
||||
asyncio.create_task(mytask2())
|
||||
js.console.log("6")
|
||||
</py-script>
|
||||
"""
|
||||
self.pyscript_run(src, wait_for_pyscript=False)
|
||||
self.wait_for_console("DONE")
|
||||
lines = self.console.log.lines[-11:]
|
||||
assert lines == ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "DONE"]
|
||||
@@ -1,193 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
pytest.skip(
|
||||
reason="FIXME: @when decorator missing from pyscript", allow_module_level=True
|
||||
)
|
||||
|
||||
|
||||
class TestEventHandler(PyScriptTest):
|
||||
def test_when_decorator_with_event(self):
|
||||
"""When the decorated function takes a single parameter,
|
||||
it should be passed the event object
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
def foo(evt):
|
||||
print(f"I've clicked {evt.target} with id {evt.target.id}")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
console_text = self.console.all.lines
|
||||
self.wait_for_console("I've clicked [object HTMLButtonElement] with id foo_id")
|
||||
assert "I've clicked [object HTMLButtonElement] with id foo_id" in console_text
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_when_decorator_without_event(self):
|
||||
"""When the decorated function takes no parameters (not including 'self'),
|
||||
it should be called without the event object
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
def foo():
|
||||
print("The button was clicked")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
self.wait_for_console("The button was clicked")
|
||||
assert "The button was clicked" in self.console.log.lines
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_multiple_when_decorators_with_event(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<button id="bar_id">bar_button</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
def foo(evt):
|
||||
print(f"I've clicked {evt.target} with id {evt.target.id}")
|
||||
@when("click", selector="#bar_id")
|
||||
def foo(evt):
|
||||
print(f"I've clicked {evt.target} with id {evt.target.id}")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
console_text = self.console.all.lines
|
||||
self.wait_for_console("I've clicked [object HTMLButtonElement] with id foo_id")
|
||||
assert "I've clicked [object HTMLButtonElement] with id foo_id" in console_text
|
||||
|
||||
self.page.locator("text=bar_button").click()
|
||||
console_text = self.console.all.lines
|
||||
self.wait_for_console("I've clicked [object HTMLButtonElement] with id bar_id")
|
||||
assert "I've clicked [object HTMLButtonElement] with id bar_id" in console_text
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_two_when_decorators(self):
|
||||
"""When decorating a function twice, both should function"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<button class="bar_class">bar_button</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
@when("mouseover", selector=".bar_class")
|
||||
def foo(evt):
|
||||
print(f"An event of type {evt.type} happened")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=bar_button").hover()
|
||||
self.page.locator("text=foo_button").click()
|
||||
self.wait_for_console("An event of type click happened")
|
||||
assert "An event of type mouseover happened" in self.console.log.lines
|
||||
assert "An event of type click happened" in self.console.log.lines
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_two_when_decorators_same_element(self):
|
||||
"""When decorating a function twice *on the same DOM element*, both should function"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
@when("mouseover", selector="#foo_id")
|
||||
def foo(evt):
|
||||
print(f"An event of type {evt.type} happened")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").hover()
|
||||
self.page.locator("text=foo_button").click()
|
||||
self.wait_for_console("An event of type click happened")
|
||||
assert "An event of type mouseover happened" in self.console.log.lines
|
||||
assert "An event of type click happened" in self.console.log.lines
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_when_decorator_multiple_elements(self):
|
||||
"""The @when decorator's selector should successfully select multiple
|
||||
DOM elements
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button class="bar_class">button1</button>
|
||||
<button class="bar_class">button2</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector=".bar_class")
|
||||
def foo(evt):
|
||||
print(f"{evt.target.innerText} was clicked")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=button1").click()
|
||||
self.page.locator("text=button2").click()
|
||||
self.wait_for_console("button2 was clicked")
|
||||
assert "button1 was clicked" in self.console.log.lines
|
||||
assert "button2 was clicked" in self.console.log.lines
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_when_decorator_duplicate_selectors(self):
|
||||
""" """
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector="#foo_id")
|
||||
@when("click", selector="#foo_id")
|
||||
def foo(evt):
|
||||
print(f"I've clicked {evt.target} with id {evt.target.id}")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
console_text = self.console.all.lines
|
||||
self.wait_for_console("I've clicked [object HTMLButtonElement] with id foo_id")
|
||||
assert (
|
||||
console_text.count("I've clicked [object HTMLButtonElement] with id foo_id")
|
||||
== 2
|
||||
)
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_when_decorator_invalid_selector(self):
|
||||
"""When the selector parameter of @when is invalid, it should show an error"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="foo_id">foo_button</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector="#.bad")
|
||||
def foo(evt):
|
||||
...
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("text=foo_button").click()
|
||||
msg = "Failed to execute 'querySelectorAll' on 'Document': '#.bad' is not a valid selector."
|
||||
error = self.page.wait_for_selector(".py-error")
|
||||
banner_text = error.inner_text()
|
||||
|
||||
if msg not in banner_text:
|
||||
raise AssertionError(
|
||||
f"Expected message '{msg}' does not "
|
||||
f"match banner text '{banner_text}'"
|
||||
)
|
||||
|
||||
assert msg in self.console.error.lines[-1]
|
||||
self.check_py_errors(msg)
|
||||
@@ -1,66 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="See PR #938")
|
||||
class TestImportmap(PyScriptTest):
|
||||
def test_importmap(self):
|
||||
src = """
|
||||
export function say_hello(who) {
|
||||
console.log("hello from", who);
|
||||
}
|
||||
"""
|
||||
self.writefile("mymod.js", src)
|
||||
#
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"mymod": "/mymod.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import { say_hello } from "mymod";
|
||||
say_hello("JS");
|
||||
</script>
|
||||
|
||||
<py-script>
|
||||
import mymod
|
||||
mymod.say_hello("Python")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines == [
|
||||
"hello from JS",
|
||||
"hello from Python",
|
||||
]
|
||||
|
||||
def test_invalid_json(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="importmap">
|
||||
this is not valid JSON
|
||||
</script>
|
||||
|
||||
<py-script>
|
||||
print("hello world")
|
||||
</py-script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
# this error is raised by the browser itself, when *it* tries to parse
|
||||
# the import map
|
||||
self.check_js_errors("Failed to parse import map")
|
||||
|
||||
self.wait_for_pyscript()
|
||||
assert self.console.log.lines == [
|
||||
"hello world",
|
||||
]
|
||||
# this warning is shown by pyscript, when *we* try to parse the import
|
||||
# map
|
||||
banner = self.page.locator(".py-warning")
|
||||
assert "Failed to parse import map" in banner.inner_text()
|
||||
@@ -1,98 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
pytest.skip(
|
||||
reason="FIXME: pyscript API changed doesn't expose pyscript to window anymore",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
class TestInterpreterAccess(PyScriptTest):
|
||||
"""Test accessing Python objects from JS via pyscript.interpreter"""
|
||||
|
||||
def test_interpreter_python_access(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
x = 1
|
||||
def py_func():
|
||||
return 2
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.run_js(
|
||||
"""
|
||||
const x = await pyscript.interpreter.globals.get('x');
|
||||
const py_func = await pyscript.interpreter.globals.get('py_func');
|
||||
const py_func_res = await py_func();
|
||||
console.log(`x is ${x}`);
|
||||
console.log(`py_func() returns ${py_func_res}`);
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"x is 1",
|
||||
"py_func() returns 2",
|
||||
]
|
||||
|
||||
def test_interpreter_script_execution(self):
|
||||
"""Test running Python code from js via pyscript.interpreter"""
|
||||
self.pyscript_run("")
|
||||
|
||||
self.run_js(
|
||||
"""
|
||||
const interface = pyscript.interpreter._remote.interface;
|
||||
await interface.runPython('print("Interpreter Ran This")');
|
||||
"""
|
||||
)
|
||||
|
||||
expected_message = "Interpreter Ran This"
|
||||
assert self.console.log.lines[-1] == expected_message
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert py_terminal.text_content() == expected_message
|
||||
|
||||
def test_backward_compatibility_runtime_script_execution(self):
|
||||
"""Test running Python code from js via pyscript.runtime"""
|
||||
self.pyscript_run("")
|
||||
|
||||
self.run_js(
|
||||
"""
|
||||
const interface = pyscript.runtime._remote.interpreter;
|
||||
await interface.runPython('print("Interpreter Ran This")');
|
||||
"""
|
||||
)
|
||||
|
||||
expected_message = "Interpreter Ran This"
|
||||
assert self.console.log.lines[-1] == expected_message
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert py_terminal.text_content() == expected_message
|
||||
|
||||
def test_backward_compatibility_runtime_python_access(self):
|
||||
"""Test accessing Python objects from JS via pyscript.runtime"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
x = 1
|
||||
def py_func():
|
||||
return 2
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.run_js(
|
||||
"""
|
||||
const x = await pyscript.interpreter.globals.get('x');
|
||||
const py_func = await pyscript.interpreter.globals.get('py_func');
|
||||
const py_func_res = await py_func();
|
||||
console.log(`x is ${x}`);
|
||||
console.log(`py_func() returns ${py_func_res}`);
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"x is 1",
|
||||
"py_func() returns 2",
|
||||
]
|
||||
@@ -1,419 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(
|
||||
reason="FIX LATER: pyscript NEXT doesn't support plugins yet",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
# Source code of a simple plugin that creates a Custom Element for testing purposes
|
||||
CE_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
plugin = Plugin('py-upper')
|
||||
|
||||
console.log("py_upper Plugin loaded")
|
||||
|
||||
@plugin.register_custom_element('py-up')
|
||||
class Upper:
|
||||
def __init__(self, element):
|
||||
self.element = element
|
||||
|
||||
def connect(self):
|
||||
console.log("Upper plugin connected")
|
||||
return self.element.originalInnerHTML.upper()
|
||||
"""
|
||||
|
||||
# Source of a plugin hooks into the PyScript App lifecycle events
|
||||
HOOKS_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
class TestLogger(Plugin):
|
||||
def configure(self, config):
|
||||
console.log('configure called')
|
||||
|
||||
def beforeLaunch(self, config):
|
||||
console.log('beforeLaunch called')
|
||||
|
||||
def afterSetup(self, config):
|
||||
console.log('afterSetup called')
|
||||
|
||||
def afterStartup(self, config):
|
||||
console.log('afterStartup called')
|
||||
|
||||
def beforePyScriptExec(self, interpreter, src, pyScriptTag):
|
||||
console.log(f'beforePyScriptExec called')
|
||||
console.log(f'before_src:{src}')
|
||||
|
||||
def afterPyScriptExec(self, interpreter, src, pyScriptTag, result):
|
||||
console.log(f'afterPyScriptExec called')
|
||||
console.log(f'after_src:{src}')
|
||||
|
||||
def onUserError(self, config):
|
||||
console.log('onUserError called')
|
||||
|
||||
|
||||
plugin = TestLogger()
|
||||
"""
|
||||
|
||||
# Source of script that defines a plugin with only beforePyScriptExec and
|
||||
# afterPyScriptExec methods
|
||||
PYSCRIPT_HOOKS_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
class ExecTestLogger(Plugin):
|
||||
|
||||
async def beforePyScriptExec(self, interpreter, src, pyScriptTag):
|
||||
console.log(f'beforePyScriptExec called')
|
||||
console.log(f'before_src:{src}')
|
||||
|
||||
async def afterPyScriptExec(self, interpreter, src, pyScriptTag, result):
|
||||
console.log(f'afterPyScriptExec called')
|
||||
console.log(f'after_src:{src}')
|
||||
console.log(f'result:{result}')
|
||||
|
||||
|
||||
plugin = ExecTestLogger()
|
||||
"""
|
||||
|
||||
# Source of script that defines a plugin with only beforePyScriptExec and
|
||||
# afterPyScriptExec methods
|
||||
PYREPL_HOOKS_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
console.warn("This is in pyrepl hooks file")
|
||||
|
||||
class PyReplTestLogger(Plugin):
|
||||
|
||||
def beforePyReplExec(self, interpreter, src, outEl, pyReplTag):
|
||||
console.log(f'beforePyReplExec called')
|
||||
console.log(f'before_src:{src}')
|
||||
|
||||
def afterPyReplExec(self, interpreter, src, outEl, pyReplTag, result):
|
||||
console.log(f'afterPyReplExec called')
|
||||
console.log(f'after_src:{src}')
|
||||
console.log(f'result:{result}')
|
||||
|
||||
|
||||
plugin = PyReplTestLogger()
|
||||
"""
|
||||
|
||||
# Source of a script that doesn't call define a `plugin` attribute
|
||||
NO_PLUGIN_CODE = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
class TestLogger(Plugin):
|
||||
pass
|
||||
"""
|
||||
|
||||
# Source code of a simple plugin that creates a Custom Element for testing purposes
|
||||
CODE_CE_PLUGIN_BAD_RETURNS = """
|
||||
from pyscript import Plugin
|
||||
from js import console
|
||||
|
||||
plugin = Plugin('py-broken')
|
||||
|
||||
@plugin.register_custom_element('py-up')
|
||||
class Upper:
|
||||
def __init__(self, element):
|
||||
self.element = element
|
||||
|
||||
def connect(self):
|
||||
# Just returning something... anything other than a string should be ignore
|
||||
return Plugin
|
||||
"""
|
||||
HTML_TEMPLATE_WITH_TAG = """
|
||||
<py-config>
|
||||
plugins = [
|
||||
"./{plugin_name}.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
<{tagname}>
|
||||
{html}
|
||||
</{tagname}>
|
||||
"""
|
||||
HTML_TEMPLATE_NO_TAG = """
|
||||
<py-config>
|
||||
plugins = [
|
||||
"./{plugin_name}.py"
|
||||
]
|
||||
</py-config>
|
||||
"""
|
||||
|
||||
|
||||
def prepare_test(
|
||||
plugin_name, code, tagname="", html="", template=HTML_TEMPLATE_WITH_TAG
|
||||
):
|
||||
"""
|
||||
Prepares the test by writing a new plugin file named `plugin_name`.py, with `code` as its
|
||||
content and run `pyscript_run` on `template` formatted with the above inputs to create the
|
||||
page HTML code.
|
||||
|
||||
For example:
|
||||
|
||||
>> @prepare_test('py-upper', CE_PLUGIN_CODE, tagname='py-up', html="Hello World")
|
||||
>> def my_foo(...):
|
||||
>> ...
|
||||
|
||||
will:
|
||||
|
||||
* write a new `py-upper.py` file to the FS
|
||||
* the contents of `py-upper.py` is equal to CE_PLUGIN_CODE
|
||||
* call self.pyscript_run with the following string:
|
||||
'''
|
||||
<py-config>
|
||||
plugins = [
|
||||
"./py-upper.py"
|
||||
]
|
||||
</py-config>
|
||||
|
||||
<py-up>
|
||||
{html}
|
||||
</py-up>
|
||||
'''
|
||||
* call `my_foo` just like a normal decorator would
|
||||
|
||||
"""
|
||||
|
||||
def dec(f):
|
||||
def _inner(self, *args, **kws):
|
||||
self.writefile(f"{plugin_name}.py", code)
|
||||
page_html = template.format(
|
||||
plugin_name=plugin_name, tagname=tagname, html=html
|
||||
)
|
||||
self.pyscript_run(page_html)
|
||||
return f(self, *args, **kws)
|
||||
|
||||
return _inner
|
||||
|
||||
return dec
|
||||
|
||||
|
||||
class TestPlugin(PyScriptTest):
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test("py-upper", CE_PLUGIN_CODE, tagname="py-up", html="Hello World")
|
||||
def test_py_plugin_inline(self):
|
||||
"""Test that a regular plugin that returns new HTML content from connected works"""
|
||||
# GIVEN a plugin that returns the all caps version of the tag innerHTML and logs text
|
||||
# during it's execution/hooks
|
||||
|
||||
# EXPECT the plugin logs to be present in the console logs
|
||||
log_lines = self.console.log.lines
|
||||
for log_line in ["py_upper Plugin loaded", "Upper plugin connected"]:
|
||||
assert log_line in log_lines
|
||||
|
||||
# EXPECT the inner text of the Plugin CustomElement to be all caps
|
||||
rendered_text = self.page.locator("py-up").inner_text()
|
||||
assert rendered_text == "HELLO WORLD"
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test("hooks_logger", HOOKS_PLUGIN_CODE, template=HTML_TEMPLATE_NO_TAG)
|
||||
def test_execution_hooks(self):
|
||||
"""Test that a Plugin that hooks into the PyScript App events, gets called
|
||||
for each one of them"""
|
||||
# GIVEN a plugin that logs specific strings for each app execution event
|
||||
hooks_available = ["afterSetup", "afterStartup"]
|
||||
hooks_unavailable = [
|
||||
"configure",
|
||||
"beforeLaunch",
|
||||
"beforePyScriptExec",
|
||||
"afterPyScriptExec",
|
||||
"beforePyReplExec",
|
||||
"afterPyReplExec",
|
||||
]
|
||||
|
||||
# EXPECT it to log the correct logs for the events it intercepts
|
||||
log_lines = self.console.log.lines
|
||||
num_calls = {
|
||||
method: log_lines.count(f"{method} called") for method in hooks_available
|
||||
}
|
||||
expected_calls = {method: 1 for method in hooks_available}
|
||||
assert num_calls == expected_calls
|
||||
|
||||
# EXPECT it to NOT be called (hence not log anything) the events that happen
|
||||
# before it's ready, hence is not called
|
||||
unavailable_called = {
|
||||
method: f"{method} called" in log_lines for method in hooks_unavailable
|
||||
}
|
||||
assert unavailable_called == {method: False for method in hooks_unavailable}
|
||||
|
||||
# TODO: It'd be actually better to check that the events get called in order
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test(
|
||||
"exec_test_logger",
|
||||
PYSCRIPT_HOOKS_PLUGIN_CODE,
|
||||
template=HTML_TEMPLATE_NO_TAG + "\n<py-script id='pyid'>x=2; x</py-script>",
|
||||
)
|
||||
def test_pyscript_exec_hooks(self):
|
||||
"""Test that the beforePyScriptExec and afterPyScriptExec hooks work as intended"""
|
||||
assert self.page.locator("py-script") is not None
|
||||
|
||||
log_lines: list[str] = self.console.log.lines
|
||||
|
||||
assert "beforePyScriptExec called" in log_lines
|
||||
assert "afterPyScriptExec called" in log_lines
|
||||
|
||||
# These could be made better with a utility function that found log lines
|
||||
# that match a filter function, or start with something
|
||||
assert "before_src:x=2; x" in log_lines
|
||||
assert "after_src:x=2; x" in log_lines
|
||||
assert "result:2" in log_lines
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test(
|
||||
"pyrepl_test_logger",
|
||||
PYREPL_HOOKS_PLUGIN_CODE,
|
||||
template=HTML_TEMPLATE_NO_TAG + "\n<py-repl id='pyid'>x=2; x</py-repl>",
|
||||
)
|
||||
def test_pyrepl_exec_hooks(self):
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
# allow afterPyReplExec to also finish before the test finishes
|
||||
self.wait_for_console("result:2")
|
||||
|
||||
log_lines: list[str] = self.console.log.lines
|
||||
|
||||
assert "beforePyReplExec called" in log_lines
|
||||
assert "afterPyReplExec called" in log_lines
|
||||
|
||||
# These could be made better with a utility function that found log lines
|
||||
# that match a filter function, or start with something
|
||||
assert "before_src:x=2; x" in log_lines
|
||||
assert "after_src:x=2; x" in log_lines
|
||||
assert "result:2" in log_lines
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
@prepare_test("no_plugin", NO_PLUGIN_CODE)
|
||||
def test_no_plugin_attribute_error(self):
|
||||
"""
|
||||
Test a plugin that do not add the `plugin` attribute to its module
|
||||
"""
|
||||
# GIVEN a Plugin NO `plugin` attribute in it's module
|
||||
error_msg = (
|
||||
"[pyscript/main] Cannot find plugin on Python module no_plugin! Python plugins "
|
||||
'modules must contain a "plugin" attribute. For more information check the '
|
||||
"plugins documentation."
|
||||
)
|
||||
# EXPECT an error for the missing attribute
|
||||
assert error_msg in self.console.error.lines
|
||||
|
||||
@skip_worker("FIXME: relative paths")
|
||||
def test_fetch_python_plugin(self):
|
||||
"""
|
||||
Test that we can fetch a plugin from a remote URL. Note we need to use
|
||||
the 'raw' URL for the plugin, otherwise the request will be rejected
|
||||
by cors policy.
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/python/hello-world.py"
|
||||
]
|
||||
|
||||
</py-config>
|
||||
<py-hello-world></py-hello-world>
|
||||
"""
|
||||
)
|
||||
|
||||
hello_element = self.page.locator("py-hello-world")
|
||||
assert hello_element.inner_html() == '<div id="hello">Hello World!</div>'
|
||||
|
||||
def test_fetch_js_plugin(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/js/hello-world.js"
|
||||
]
|
||||
</py-config>
|
||||
"""
|
||||
)
|
||||
|
||||
hello_element = self.page.locator("py-hello-world")
|
||||
assert hello_element.inner_html() == "<h1>Hello, world!</h1>"
|
||||
|
||||
def test_fetch_js_plugin_bare(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/js/hello-world-base.js"
|
||||
]
|
||||
</py-config>
|
||||
"""
|
||||
)
|
||||
|
||||
hello_element = self.page.locator("py-hello-world")
|
||||
assert hello_element.inner_html() == "<h1>Hello, world!</h1>"
|
||||
|
||||
def test_fetch_plugin_no_file_extension(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://non-existent.blah/hello-world"
|
||||
]
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_msg = (
|
||||
"(PY2000): Unable to load plugin from "
|
||||
"'https://non-existent.blah/hello-world'. Plugins "
|
||||
"need to contain a file extension and be either a "
|
||||
"python or javascript file."
|
||||
)
|
||||
|
||||
assert self.assert_banner_message(expected_msg)
|
||||
|
||||
def test_fetch_js_plugin_non_existent(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"http://non-existent.example.com/hello-world.js"
|
||||
]
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_msg = (
|
||||
"(PY0001): Fetching from URL "
|
||||
"http://non-existent.example.com/hello-world.js failed "
|
||||
"with error 'Failed to fetch'. Are your filename and "
|
||||
"path correct?"
|
||||
)
|
||||
|
||||
assert self.assert_banner_message(expected_msg)
|
||||
|
||||
def test_fetch_js_no_export(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
plugins = [
|
||||
"https://raw.githubusercontent.com/FabioRosado/pyscript-plugins/main/js/hello-world-no-export.js"
|
||||
]
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected_message = (
|
||||
"(PY2001): Unable to load plugin from "
|
||||
"'https://raw.githubusercontent.com/FabioRosado/pyscript-plugins"
|
||||
"/main/js/hello-world-no-export.js'. "
|
||||
"Plugins need to contain a default export."
|
||||
)
|
||||
|
||||
assert self.assert_banner_message(expected_message)
|
||||
@@ -1,294 +0,0 @@
|
||||
import os
|
||||
import tarfile
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from .support import PyScriptTest, with_execution_thread
|
||||
|
||||
PYODIDE_VERSION = "0.23.4"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pyodide_tar(request):
|
||||
"""
|
||||
Fixture which returns a local copy of pyodide. It uses pytest-cache to
|
||||
avoid re-downloading it between runs.
|
||||
"""
|
||||
URL = (
|
||||
f"https://github.com/pyodide/pyodide/releases/download/{PYODIDE_VERSION}/"
|
||||
f"pyodide-core-{PYODIDE_VERSION}.tar.bz2"
|
||||
)
|
||||
tar_name = Path(URL).name
|
||||
|
||||
val = request.config.cache.get(tar_name, None)
|
||||
if val is None:
|
||||
response = requests.get(URL, stream=True)
|
||||
TMP_DIR = tempfile.mkdtemp()
|
||||
TMP_TAR_LOCATION = os.path.join(TMP_DIR, tar_name)
|
||||
with open(TMP_TAR_LOCATION, "wb") as f:
|
||||
f.write(response.raw.read())
|
||||
val = TMP_TAR_LOCATION
|
||||
request.config.cache.set(tar_name, val)
|
||||
return val
|
||||
|
||||
|
||||
def unzip(location, extract_to="."):
|
||||
file = tarfile.open(name=location, mode="r:bz2")
|
||||
file.extractall(path=extract_to)
|
||||
|
||||
|
||||
# Disable the main/worker dual testing, for two reasons:
|
||||
#
|
||||
# 1. the <py-config> logic happens before we start the worker, so there is
|
||||
# no point in running these tests twice
|
||||
#
|
||||
# 2. the logic to inject execution_thread into <py-config> works only with
|
||||
# plain <py-config> tags, but here we want to test all weird combinations
|
||||
# of config
|
||||
@with_execution_thread(None)
|
||||
class TestConfig(PyScriptTest):
|
||||
def test_py_config_inline(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
name = "foobar"
|
||||
</py-config>
|
||||
|
||||
<py-script async>
|
||||
from pyscript import window, document
|
||||
promise = await document.currentScript._pyodide.promise
|
||||
window.console.log("config name:", promise.config.name)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "config name: foobar"
|
||||
|
||||
def test_py_config_external(self):
|
||||
pyconfig_toml = """
|
||||
name = "app with external config"
|
||||
"""
|
||||
self.writefile("pyconfig.toml", pyconfig_toml)
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config src="pyconfig.toml"></py-config>
|
||||
|
||||
<py-script async>
|
||||
from pyscript import window, document
|
||||
promise = await document.currentScript._pyodide.promise
|
||||
window.console.log("config name:", promise.config.name)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "config name: app with external config"
|
||||
|
||||
# The default pyodide version is newer than
|
||||
# the one we are loading below (after downloading locally)
|
||||
# which is 0.22.0
|
||||
|
||||
# The test checks if loading a different interpreter is possible
|
||||
# and that too from a locally downloaded file without needing
|
||||
# the use of explicit `indexURL` calculation.
|
||||
def test_interpreter_config(self, pyodide_tar):
|
||||
unzip(pyodide_tar, extract_to=self.tmpdir)
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config type="json">
|
||||
{
|
||||
"interpreters": [{
|
||||
"src": "/pyodide/pyodide.js",
|
||||
"name": "my-own-pyodide",
|
||||
"lang": "python"
|
||||
}]
|
||||
}
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
import sys, js
|
||||
pyodide_version = sys.modules["pyodide"].__version__
|
||||
js.console.log("version", pyodide_version)
|
||||
</py-script>
|
||||
""",
|
||||
)
|
||||
|
||||
assert self.console.log.lines[-1] == f"version {PYODIDE_VERSION}"
|
||||
|
||||
@pytest.mark.skip("FIXME: We need to restore the banner.")
|
||||
def test_invalid_json_config(self):
|
||||
# we need wait_for_pyscript=False because we bail out very soon,
|
||||
# before being able to write 'PyScript page fully initialized'
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config type="json">
|
||||
[[
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
banner = self.page.wait_for_selector(".py-error")
|
||||
assert "SyntaxError: Unexpected end of JSON input" in self.console.error.text
|
||||
expected = (
|
||||
"(PY1000): The config supplied: [[ is an invalid JSON and cannot be "
|
||||
"parsed: SyntaxError: Unexpected end of JSON input"
|
||||
)
|
||||
assert banner.inner_text() == expected
|
||||
|
||||
@pytest.mark.skip("FIXME: We need to restore the banner.")
|
||||
def test_invalid_toml_config(self):
|
||||
# we need wait_for_pyscript=False because we bail out very soon,
|
||||
# before being able to write 'PyScript page fully initialized'
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
banner = self.page.wait_for_selector(".py-error")
|
||||
assert "SyntaxError: Expected DoubleQuote" in self.console.error.text
|
||||
expected = (
|
||||
"(PY1000): The config supplied: [[ is an invalid TOML and cannot be parsed: "
|
||||
"SyntaxError: Expected DoubleQuote, Whitespace, or [a-z], [A-Z], "
|
||||
'[0-9], "-", "_" but "\\n" found.'
|
||||
)
|
||||
assert banner.inner_text() == expected
|
||||
|
||||
@pytest.mark.skip("FIXME: We need to restore the banner.")
|
||||
def test_multiple_py_config(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
name = "foobar"
|
||||
</py-config>
|
||||
|
||||
<py-config>
|
||||
this is ignored and won't even be parsed
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
import js
|
||||
config = js.pyscript_get_config()
|
||||
js.console.log("config name:", config.name)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
banner = self.page.wait_for_selector(".py-warning")
|
||||
expected = (
|
||||
"Multiple <py-config> tags detected. Only the first "
|
||||
"is going to be parsed, all the others will be ignored"
|
||||
)
|
||||
assert banner.text_content() == expected
|
||||
|
||||
@pytest.mark.skip("FIXME: We need to restore the banner.")
|
||||
def test_no_interpreter(self):
|
||||
snippet = """
|
||||
<py-config type="json">
|
||||
{
|
||||
"interpreters": []
|
||||
}
|
||||
</py-config>
|
||||
"""
|
||||
self.pyscript_run(snippet, wait_for_pyscript=False)
|
||||
div = self.page.wait_for_selector(".py-error")
|
||||
assert (
|
||||
div.text_content() == "(PY1000): Fatal error: config.interpreter is empty"
|
||||
)
|
||||
|
||||
@pytest.mark.skip("FIXME: We need to restore the banner.")
|
||||
def test_multiple_interpreter(self):
|
||||
snippet = """
|
||||
<py-config type="json">
|
||||
{
|
||||
"interpreters": [
|
||||
{
|
||||
"src": "https://cdn.jsdelivr.net/pyodide/v0.23.2/full/pyodide.js",
|
||||
"name": "pyodide-0.23.2",
|
||||
"lang": "python"
|
||||
},
|
||||
{
|
||||
"src": "http://...",
|
||||
"name": "this will be ignored",
|
||||
"lang": "this as well"
|
||||
}
|
||||
]
|
||||
}
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
import js
|
||||
js.console.log("hello world");
|
||||
</py-script>
|
||||
"""
|
||||
self.pyscript_run(snippet)
|
||||
banner = self.page.wait_for_selector(".py-warning")
|
||||
expected = (
|
||||
"Multiple interpreters are not supported yet.Only the first will be used"
|
||||
)
|
||||
assert banner.text_content() == expected
|
||||
assert self.console.log.lines[-1] == "hello world"
|
||||
|
||||
def test_paths(self):
|
||||
self.writefile("a.py", "x = 'hello from A'")
|
||||
self.writefile("b.py", "x = 'hello from B'")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[fetch]]
|
||||
files = ["./a.py", "./b.py"]
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
import js
|
||||
import a, b
|
||||
js.console.log(a.x)
|
||||
js.console.log(b.x)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-2:] == [
|
||||
"hello from A",
|
||||
"hello from B",
|
||||
]
|
||||
|
||||
@pytest.mark.skip("FIXME: We need to restore the banner.")
|
||||
def test_paths_that_do_not_exist(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[fetch]]
|
||||
files = ["./f.py"]
|
||||
</py-config>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
|
||||
expected = "(PY0404): Fetching from URL ./f.py failed with " "error 404"
|
||||
|
||||
inner_html = self.page.locator(".py-error").inner_html()
|
||||
|
||||
assert expected in inner_html
|
||||
assert expected in self.console.error.lines[-1]
|
||||
|
||||
def test_paths_from_packages(self):
|
||||
self.writefile("utils/__init__.py", "")
|
||||
self.writefile("utils/a.py", "x = 'hello from A'")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[fetch]]
|
||||
from = "utils"
|
||||
to_folder = "pkg"
|
||||
files = ["__init__.py", "a.py"]
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
import js
|
||||
from pkg.a import x
|
||||
js.console.log(x)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello from A"
|
||||
@@ -1,663 +0,0 @@
|
||||
import platform
|
||||
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(
|
||||
reason="FIX LATER: pyscript NEXT doesn't support the REPL yet",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
class TestPyRepl(PyScriptTest):
|
||||
def _replace(self, py_repl, newcode):
|
||||
"""
|
||||
Clear the editor and write new code in it.
|
||||
WARNING: this assumes that the textbox has already the focus
|
||||
"""
|
||||
# clear the editor, write new code
|
||||
if "macOS" in platform.platform():
|
||||
self.page.keyboard.press("Meta+A")
|
||||
else:
|
||||
self.page.keyboard.press("Control+A")
|
||||
|
||||
self.page.keyboard.press("Backspace")
|
||||
self.page.keyboard.type(newcode)
|
||||
|
||||
def test_repl_loads(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl></py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.query_selector("py-repl .py-repl-box")
|
||||
assert py_repl
|
||||
|
||||
def test_execute_preloaded_source(self):
|
||||
"""
|
||||
Unfortunately it tests two things at once, but it's impossible to write a
|
||||
smaller test. I think this is the most basic test that we can write.
|
||||
|
||||
We test that:
|
||||
1. the source code that we put in the tag is loaded inside the editor
|
||||
2. clicking the button executes it
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
print('hello from py-repl')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
src = py_repl.locator("div.cm-content").inner_text()
|
||||
assert "print('hello from py-repl')" in src
|
||||
py_repl.locator("button").click()
|
||||
self.page.wait_for_selector("py-terminal")
|
||||
assert self.console.log.lines[-1] == "hello from py-repl"
|
||||
|
||||
def test_execute_code_typed_by_the_user(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl></py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.type('print("hello")')
|
||||
py_repl.locator("button").click()
|
||||
self.page.wait_for_selector("py-terminal")
|
||||
assert self.console.log.lines[-1] == "hello"
|
||||
|
||||
def test_execute_on_shift_enter(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
print("hello world")
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
self.page.wait_for_selector("py-repl .py-repl-run-button")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("py-terminal")
|
||||
|
||||
assert self.console.log.lines[-1] == "hello world"
|
||||
|
||||
# Shift-enter should not add a newline to the editor
|
||||
assert self.page.locator(".cm-line").count() == 1
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_display(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
display('hello world')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "hello world"
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_show_last_expression(self):
|
||||
"""
|
||||
Test that we display() the value of the last expression, as you would
|
||||
expect by a REPL
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
42
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "42"
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_show_last_expression_with_output(self):
|
||||
"""
|
||||
Test that we display() the value of the last expression, as you would
|
||||
expect by a REPL
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="repl-target"></div>
|
||||
<py-repl output="repl-target">
|
||||
42
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
out_div = py_repl.locator("div.py-repl-output")
|
||||
assert out_div.all_inner_texts()[0] == ""
|
||||
|
||||
out_div = self.page.wait_for_selector("#repl-target")
|
||||
assert out_div.inner_text() == "42"
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_run_clears_previous_output(self):
|
||||
"""
|
||||
Check that we clear the previous output of the cell before executing it
|
||||
again
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
display('hello world')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "hello world"
|
||||
# clear the editor, write new code, execute
|
||||
self._replace(py_repl, "display('another output')")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
# test runner can be too fast, the line below should wait for output to change
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "another output"
|
||||
|
||||
def test_python_exception(self):
|
||||
"""
|
||||
See also test01_basic::test_python_exception, since it's very similar
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
raise Exception('this is an error')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
self.page.wait_for_selector(".py-error")
|
||||
#
|
||||
# check that we sent the traceback to the console
|
||||
tb_lines = self.console.error.lines[-1].splitlines()
|
||||
assert tb_lines[0] == "[pyexec] Python exception:"
|
||||
assert tb_lines[1] == "Traceback (most recent call last):"
|
||||
assert tb_lines[-1] == "Exception: this is an error"
|
||||
#
|
||||
# check that we show the traceback in the page
|
||||
err_pre = py_repl.locator("div.py-repl-output > pre.py-error")
|
||||
tb_lines = err_pre.inner_text().splitlines()
|
||||
assert tb_lines[0] == "Traceback (most recent call last):"
|
||||
assert tb_lines[-1] == "Exception: this is an error"
|
||||
#
|
||||
self.check_py_errors("this is an error")
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_multiple_repls(self):
|
||||
"""
|
||||
Multiple repls showing in the correct order in the page
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl data-testid=="first"> display("first") </py-repl>
|
||||
<py-repl data-testid=="second"> display("second") </py-repl>
|
||||
"""
|
||||
)
|
||||
first_py_repl = self.page.get_by_text("first")
|
||||
first_py_repl.click()
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert self.page.inner_text("#py-internal-0-repl-output") == "first"
|
||||
|
||||
second_py_repl = self.page.get_by_text("second")
|
||||
second_py_repl.click()
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-1-repl-output")
|
||||
assert self.page.inner_text("#py-internal-1-repl-output") == "second"
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_python_exception_after_previous_output(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
display('hello world')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "hello world"
|
||||
#
|
||||
# clear the editor, write new code, execute
|
||||
self._replace(py_repl, "0/0")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
# test runner can be too fast, the line below should wait for output to change
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert "hello world" not in out_div.inner_text()
|
||||
assert "ZeroDivisionError" in out_div.inner_text()
|
||||
#
|
||||
self.check_py_errors("ZeroDivisionError")
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_hide_previous_error_after_successful_run(self):
|
||||
"""
|
||||
this tests the fact that a new error div should be created once there's an
|
||||
error but also that it should disappear automatically once the error
|
||||
is fixed
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl>
|
||||
raise Exception('this is an error')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert "this is an error" in out_div.inner_text()
|
||||
#
|
||||
self._replace(py_repl, "display('hello')")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
# test runner can be too fast, the line below should wait for output to change
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "hello"
|
||||
#
|
||||
self.check_py_errors("this is an error")
|
||||
|
||||
def test_output_attribute_does_not_exist(self):
|
||||
"""
|
||||
If we try to use an attribute which doesn't exist, we display an error
|
||||
instead
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl output="I-dont-exist">
|
||||
print('I will not be executed')
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
banner = self.page.wait_for_selector(".py-warning")
|
||||
|
||||
banner_content = banner.inner_text()
|
||||
expected = (
|
||||
'output = "I-dont-exist" does not match the id of any element on the page.'
|
||||
)
|
||||
assert banner_content == expected
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_auto_generate(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl auto-generate="true">
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repls = self.page.locator("py-repl")
|
||||
outputs = py_repls.locator("div.py-repl-output")
|
||||
assert py_repls.count() == 1
|
||||
assert outputs.count() == 1
|
||||
#
|
||||
# evaluate the py-repl, and wait for the newly generated one
|
||||
self.page.keyboard.type("'hello'")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.locator('py-repl[exec-id="1"]').wait_for()
|
||||
assert py_repls.count() == 2
|
||||
assert outputs.count() == 2
|
||||
#
|
||||
# now we type something else: the new py-repl should have the focus
|
||||
self.page.keyboard.type("'world'")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.locator('py-repl[exec-id="2"]').wait_for()
|
||||
assert py_repls.count() == 3
|
||||
assert outputs.count() == 3
|
||||
#
|
||||
# check that the code and the outputs are in order
|
||||
out_texts = [el.inner_text() for el in self.iter_locator(outputs)]
|
||||
assert out_texts == ["hello", "world", ""]
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_multiple_repls_mixed_display_order(self):
|
||||
"""
|
||||
Displaying several outputs that don't obey the order in which the original
|
||||
repl displays were created using the auto_generate attr
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl auto-generate="true" data-testid=="first"> display("root first") </py-repl>
|
||||
<py-repl auto-generate="true" data-testid=="second"> display("root second") </py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
second_py_repl = self.page.get_by_text("root second")
|
||||
second_py_repl.click()
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-1-repl-output")
|
||||
self.page.keyboard.type("display('second children')")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-1-1-repl-output")
|
||||
|
||||
first_py_repl = self.page.get_by_text("root first")
|
||||
first_py_repl.click()
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
self.page.keyboard.type("display('first children')")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
self.page.wait_for_selector("#py-internal-0-1-repl-output")
|
||||
|
||||
assert self.page.inner_text("#py-internal-1-1-repl-output") == "second children"
|
||||
assert self.page.inner_text("#py-internal-0-1-repl-output") == "first children"
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_repl_output_attribute(self):
|
||||
# Test that output attribute sends stdout to the element
|
||||
# with the given ID, but not display()
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="repl-target"></div>
|
||||
<py-repl output="repl-target">
|
||||
print('print from py-repl')
|
||||
display('display from py-repl')
|
||||
</py-repl>
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
target = self.page.wait_for_selector("#repl-target")
|
||||
assert "print from py-repl" in target.inner_text()
|
||||
|
||||
out_div = self.page.wait_for_selector("#py-internal-0-repl-output")
|
||||
assert out_div.inner_text() == "display from py-repl"
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_repl_output_display_async(self):
|
||||
# py-repls running async code are not expected to
|
||||
# send display to element element
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="repl-target"></div>
|
||||
<py-script>
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def print_it():
|
||||
await asyncio.sleep(1)
|
||||
print('print from py-repl')
|
||||
|
||||
|
||||
async def display_it():
|
||||
display('display from py-repl')
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def done():
|
||||
await asyncio.sleep(3)
|
||||
js.console.log("DONE")
|
||||
</py-script>
|
||||
|
||||
<py-repl output="repl-target">
|
||||
asyncio.ensure_future(print_it());
|
||||
asyncio.ensure_future(display_it());
|
||||
asyncio.ensure_future(done());
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
self.wait_for_console("DONE")
|
||||
|
||||
assert self.page.locator("#repl-target").text_content() == ""
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_repl_stdio_dynamic_tags(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<py-repl output="first">
|
||||
import js
|
||||
|
||||
print("first.")
|
||||
|
||||
# Using string, since no clean way to write to the
|
||||
# code contents of the CodeMirror in a PyRepl
|
||||
newTag = '<py-repl id="second-repl" output="second">print("second.")</py-repl>'
|
||||
js.document.body.innerHTML += newTag
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
assert self.page.wait_for_selector("#first").inner_text() == "first.\n"
|
||||
|
||||
second_repl = self.page.locator("py-repl#second-repl")
|
||||
second_repl.locator("button").click()
|
||||
assert self.page.wait_for_selector("#second").inner_text() == "second.\n"
|
||||
|
||||
def test_repl_output_id_errors(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl output="not-on-page">
|
||||
print("bad.")
|
||||
print("bad.")
|
||||
</py-repl>
|
||||
|
||||
<py-repl output="not-on-page">
|
||||
print("bad.")
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repls = self.page.query_selector_all("py-repl")
|
||||
for repl in py_repls:
|
||||
repl.query_selector_all("button")[0].click()
|
||||
|
||||
banner = self.page.wait_for_selector(".py-warning")
|
||||
|
||||
banner_content = banner.inner_text()
|
||||
expected = (
|
||||
'output = "not-on-page" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
assert banner_content == expected
|
||||
|
||||
def test_repl_stderr_id_errors(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl stderr="not-on-page">
|
||||
import sys
|
||||
print("bad.", file=sys.stderr)
|
||||
print("bad.", file=sys.stderr)
|
||||
</py-repl>
|
||||
|
||||
<py-repl stderr="not-on-page">
|
||||
print("bad.", file=sys.stderr)
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
py_repls = self.page.query_selector_all("py-repl")
|
||||
for repl in py_repls:
|
||||
repl.query_selector_all("button")[0].click()
|
||||
|
||||
banner = self.page.wait_for_selector(".py-warning")
|
||||
|
||||
banner_content = banner.inner_text()
|
||||
expected = (
|
||||
'stderr = "not-on-page" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
assert banner_content == expected
|
||||
|
||||
def test_repl_output_stderr(self):
|
||||
# Test that stderr works, and routes to the same location as stdout
|
||||
# Also, repls with the stderr attribute route to an additional location
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="stdout-div"></div>
|
||||
<div id="stderr-div"></div>
|
||||
<py-repl output="stdout-div" stderr="stderr-div">
|
||||
import sys
|
||||
print("one.", file=sys.stderr)
|
||||
print("two.")
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
assert self.page.wait_for_selector("#stdout-div").inner_text() == "one.\ntwo.\n"
|
||||
assert self.page.wait_for_selector("#stderr-div").inner_text() == "one.\n"
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_repl_output_attribute_change(self):
|
||||
# If the user changes the 'output' attribute of a <py-repl> tag mid-execution,
|
||||
# Output should no longer go to the selected div and a warning should appear
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<!-- There is no tag with id "third" -->
|
||||
<py-repl id="repl-tag" output="first">
|
||||
print("one.")
|
||||
|
||||
# Change the 'output' attribute of this tag
|
||||
import js
|
||||
this_tag = js.document.getElementById("repl-tag")
|
||||
|
||||
this_tag.setAttribute("output", "second")
|
||||
print("two.")
|
||||
|
||||
this_tag.setAttribute("output", "third")
|
||||
print("three.")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
assert self.page.wait_for_selector("#first").inner_text() == "one.\n"
|
||||
assert self.page.wait_for_selector("#second").inner_text() == "two.\n"
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
'output = "third" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
alert_banner = self.page.wait_for_selector(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_repl_output_element_id_change(self):
|
||||
# If the user changes the ID of the targeted DOM element mid-execution,
|
||||
# Output should no longer go to the selected element and a warning should appear
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<!-- There is no tag with id "third" -->
|
||||
<py-repl id="pyscript-tag" output="first">
|
||||
print("one.")
|
||||
|
||||
# Change the ID of the targeted DIV to something else
|
||||
import js
|
||||
target_tag = js.document.getElementById("first")
|
||||
|
||||
# should fail and show banner
|
||||
target_tag.setAttribute("id", "second")
|
||||
print("two.")
|
||||
|
||||
# But changing both the 'output' attribute and the id of the target
|
||||
# should work
|
||||
target_tag.setAttribute("id", "third")
|
||||
js.document.getElementById("pyscript-tag").setAttribute("output", "third")
|
||||
print("three.")
|
||||
</py-repl>
|
||||
"""
|
||||
)
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
py_repl.locator("button").click()
|
||||
|
||||
# Note the ID of the div has changed by the time of this assert
|
||||
assert self.page.wait_for_selector("#third").inner_text() == "one.\nthree.\n"
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
'output = "first" does not match the id of any element on the page.'
|
||||
)
|
||||
alert_banner = self.page.wait_for_selector(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
|
||||
def test_repl_load_content_from_src(self):
|
||||
self.writefile("loadReplSrc1.py", "print('1')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl id="py-repl1" output="replOutput1" src="./loadReplSrc1.py"></py-repl>
|
||||
<div id="replOutput1"></div>
|
||||
"""
|
||||
)
|
||||
successMsg = "[py-repl] loading code from ./loadReplSrc1.py to repl...success"
|
||||
assert self.console.info.lines[-1] == successMsg
|
||||
|
||||
py_repl = self.page.locator("py-repl")
|
||||
code = py_repl.locator("div.cm-content").inner_text()
|
||||
assert "print('1')" in code
|
||||
|
||||
@skip_worker("TIMEOUT")
|
||||
def test_repl_src_change(self):
|
||||
self.writefile("loadReplSrc2.py", "2")
|
||||
self.writefile("loadReplSrc3.py", "print('3')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl id="py-repl2" output="replOutput2" src="./loadReplSrc2.py"></py-repl>
|
||||
<div id="replOutput2"></div>
|
||||
|
||||
<py-repl id="py-repl3" output="replOutput3">
|
||||
import js
|
||||
target_tag = js.document.getElementById("py-repl2")
|
||||
target_tag.setAttribute("src", "./loadReplSrc3.py")
|
||||
</py-repl>
|
||||
<div id="replOutput3"></div>
|
||||
"""
|
||||
)
|
||||
|
||||
successMsg1 = "[py-repl] loading code from ./loadReplSrc2.py to repl...success"
|
||||
assert self.console.info.lines[-1] == successMsg1
|
||||
|
||||
py_repl3 = self.page.locator("py-repl#py-repl3")
|
||||
py_repl3.locator("button").click()
|
||||
py_repl2 = self.page.locator("py-repl#py-repl2")
|
||||
py_repl2.locator("button").click()
|
||||
self.page.wait_for_selector("py-terminal")
|
||||
assert self.console.log.lines[-1] == "3"
|
||||
|
||||
successMsg2 = "[py-repl] loading code from ./loadReplSrc3.py to repl...success"
|
||||
assert self.console.info.lines[-1] == successMsg2
|
||||
|
||||
def test_repl_src_path_that_do_not_exist(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-repl id="py-repl4" output="replOutput4" src="./loadReplSrc4.py"></py-repl>
|
||||
<div id="replOutput4"></div>
|
||||
"""
|
||||
)
|
||||
errorMsg = (
|
||||
"(PY0404): Fetching from URL ./loadReplSrc4.py "
|
||||
"failed with error 404 (Not Found). "
|
||||
"Are your filename and path correct?"
|
||||
)
|
||||
assert self.console.error.lines[-1] == errorMsg
|
||||
@@ -1,270 +0,0 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(
|
||||
reason="FIX LATER: pyscript NEXT doesn't support the Terminal yet",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
class TestPyTerminal(PyScriptTest):
|
||||
def test_py_terminal(self):
|
||||
"""
|
||||
1. <py-terminal> should redirect stdout and stderr to the DOM
|
||||
|
||||
2. they also go to the console as usual
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-terminal></py-terminal>
|
||||
|
||||
<py-script>
|
||||
import sys
|
||||
print('hello world')
|
||||
print('this goes to stderr', file=sys.stderr)
|
||||
print('this goes to stdout')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
term_lines = term.inner_text().splitlines()
|
||||
assert term_lines == [
|
||||
"hello world",
|
||||
"this goes to stderr",
|
||||
"this goes to stdout",
|
||||
]
|
||||
assert self.console.log.lines[-3:] == [
|
||||
"hello world",
|
||||
"this goes to stderr",
|
||||
"this goes to stdout",
|
||||
]
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_two_terminals(self):
|
||||
"""
|
||||
Multiple <py-terminal>s can cohexist.
|
||||
A <py-terminal> receives only output from the moment it is added to
|
||||
the DOM.
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-terminal id="term1"></py-terminal>
|
||||
|
||||
<py-script>
|
||||
import js
|
||||
print('one')
|
||||
term2 = js.document.createElement('py-terminal')
|
||||
term2.id = 'term2'
|
||||
js.document.body.append(term2)
|
||||
|
||||
print('two')
|
||||
print('three')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
term1 = self.page.locator("#term1")
|
||||
term2 = self.page.locator("#term2")
|
||||
term1_lines = term1.inner_text().splitlines()
|
||||
term2_lines = term2.inner_text().splitlines()
|
||||
assert term1_lines == ["one", "two", "three"]
|
||||
assert term2_lines == ["two", "three"]
|
||||
|
||||
def test_auto_attribute(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-terminal auto></py-terminal>
|
||||
|
||||
<button id="my-button" py-click="print('hello world')">Click me</button>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
expect(term).to_be_hidden()
|
||||
self.page.locator("button").click()
|
||||
expect(term).to_be_visible()
|
||||
assert term.inner_text() == "hello world\n"
|
||||
|
||||
def test_config_auto(self):
|
||||
"""
|
||||
config.terminal == "auto" is the default: a <py-terminal auto> is
|
||||
automatically added to the page
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="my-button" py-click="print('hello world')">Click me</button>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
expect(term).to_be_hidden()
|
||||
assert "No <py-terminal> found, adding one" in self.console.info.text
|
||||
#
|
||||
self.page.locator("button").click()
|
||||
expect(term).to_be_visible()
|
||||
assert term.inner_text() == "hello world\n"
|
||||
|
||||
def test_config_true(self):
|
||||
"""
|
||||
If we set config.terminal == true, a <py-terminal> is automatically added
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
terminal = true
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
print('hello world')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
expect(term).to_be_visible()
|
||||
assert term.inner_text() == "hello world\n"
|
||||
|
||||
def test_config_false(self):
|
||||
"""
|
||||
If we set config.terminal == false, no <py-terminal> is added
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
terminal = false
|
||||
</py-config>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
assert term.count() == 0
|
||||
|
||||
def test_config_docked(self):
|
||||
"""
|
||||
config.docked == "docked" is also the default: a <py-terminal auto docked> is
|
||||
automatically added to the page
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="my-button" py-click="print('hello world')">Click me</button>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
self.page.locator("button").click()
|
||||
expect(term).to_be_visible()
|
||||
assert term.get_attribute("docked") == ""
|
||||
|
||||
def test_xterm_function(self):
|
||||
"""Test a few basic behaviors of the xtermjs terminal.
|
||||
|
||||
This test isn't meant to capture all of the behaviors of an xtermjs terminal;
|
||||
rather, it confirms with a few basic formatting sequences that (1) the xtermjs
|
||||
terminal is functioning/loaded correctly and (2) that output toward that terminal
|
||||
isn't being escaped in a way that prevents it reacting to escape seqeunces. The
|
||||
main goal is preventing regressions.
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
xterm = true
|
||||
</py-config>
|
||||
<py-script>
|
||||
print("\x1b[33mYellow\x1b[0m")
|
||||
print("\x1b[4mUnderline\x1b[24m")
|
||||
print("\x1b[1mBold\x1b[22m")
|
||||
print("\x1b[3mItalic\x1b[23m")
|
||||
print("done")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
# Wait for "done" to actually appear in the xterm; may be delayed,
|
||||
# since xtermjs processes its input buffer in chunks
|
||||
last_line = self.page.get_by_text("done")
|
||||
last_line.wait_for()
|
||||
|
||||
# Yes, this is not ideal. However, per http://xtermjs.org/docs/guides/hooks/
|
||||
# "It is not possible to conclude, whether or when a certain chunk of data
|
||||
# will finally appear on the screen," which is what we'd really like to know.
|
||||
# By waiting for the "done" test to appear above, we get close, however it is
|
||||
# possible for the text to appear and not be 'processed' (i.e.) formatted. This
|
||||
# small delay should avoid that.
|
||||
time.sleep(1)
|
||||
|
||||
rows = self.page.locator(".xterm-rows")
|
||||
|
||||
# The following use locator.evaluate() and getComputedStyle to get
|
||||
# the computed CSS values; this tests that the lines are rendering
|
||||
# properly in a better way than just testing whether they
|
||||
# get the right css classes from xtermjs
|
||||
|
||||
# First line should be yellow
|
||||
first_line = rows.locator("div").nth(0)
|
||||
first_char = first_line.locator("span").nth(0)
|
||||
color = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('color')"
|
||||
)
|
||||
assert color == "rgb(196, 160, 0)"
|
||||
|
||||
# Second line should be underlined
|
||||
second_line = rows.locator("div").nth(1)
|
||||
first_char = second_line.locator("span").nth(0)
|
||||
text_decoration = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('text-decoration')"
|
||||
)
|
||||
assert "underline" in text_decoration
|
||||
|
||||
# We'll make sure the 'bold' font weight is more than the
|
||||
# default font weight without specifying a specific value
|
||||
baseline_font_weight = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('font-weight')"
|
||||
)
|
||||
|
||||
# Third line should be bold
|
||||
third_line = rows.locator("div").nth(2)
|
||||
first_char = third_line.locator("span").nth(0)
|
||||
font_weight = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('font-weight')"
|
||||
)
|
||||
assert int(font_weight) > int(baseline_font_weight)
|
||||
|
||||
# Fourth line should be italic
|
||||
fourth_line = rows.locator("div").nth(3)
|
||||
first_char = fourth_line.locator("span").nth(0)
|
||||
font_style = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('font-style')"
|
||||
)
|
||||
assert font_style == "italic"
|
||||
|
||||
def test_xterm_multiple(self):
|
||||
"""Test whether multiple x-terms on the page all function"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
xterm = true
|
||||
</py-config>
|
||||
<py-script>
|
||||
print("\x1b[33mYellow\x1b[0m")
|
||||
print("done")
|
||||
</py-script>
|
||||
<py-terminal id="a"></py-terminal>
|
||||
<py-terminal id="b" data-testid="b"></py-terminal>
|
||||
"""
|
||||
)
|
||||
|
||||
# Wait for "done" to actually appear in the xterm; may be delayed,
|
||||
# since xtermjs processes its input buffer in chunks
|
||||
last_line = self.page.get_by_test_id("b").get_by_text("done")
|
||||
last_line.wait_for()
|
||||
|
||||
# Yes, this is not ideal. See note in `test_xterm_function`
|
||||
time.sleep(1)
|
||||
|
||||
rows = self.page.locator("#a .xterm-rows")
|
||||
|
||||
# First line should be yellow
|
||||
first_line = rows.locator("div").nth(0)
|
||||
first_char = first_line.locator("span").nth(0)
|
||||
color = first_char.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('color')"
|
||||
)
|
||||
assert color == "rgb(196, 160, 0)"
|
||||
@@ -1,64 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
class TestPyScriptRuntimeAttributes(PyScriptTest):
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_injected_html_with_py_event(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<div id="py-button-container"></div>
|
||||
<py-script>
|
||||
import js
|
||||
|
||||
py_button = Element("py-button-container")
|
||||
py_button.element.innerHTML = '<button py-click="print_hello()"></button>'
|
||||
|
||||
def print_hello():
|
||||
js.console.log("hello pyscript")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("button").click()
|
||||
assert self.console.log.lines == ["hello pyscript"]
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_added_py_event(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<button id="py-button"></button>
|
||||
<py-script>
|
||||
import js
|
||||
|
||||
py_button = Element("py-button")
|
||||
py_button.element.setAttribute("py-click", "print_hello()")
|
||||
|
||||
def print_hello():
|
||||
js.console.log("hello pyscript")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("button").click()
|
||||
assert self.console.log.lines == ["hello pyscript"]
|
||||
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_added_then_removed_py_event(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<button id="py-button">live content</button>
|
||||
<py-script>
|
||||
import js
|
||||
|
||||
py_button = Element("py-button")
|
||||
py_button.element.setAttribute("py-click", "print_hello()")
|
||||
|
||||
def print_hello():
|
||||
js.console.log("hello pyscript")
|
||||
py_button.element.removeAttribute("py-click")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.locator("button").click()
|
||||
self.page.locator("button").click()
|
||||
assert self.console.log.lines == ["hello pyscript"]
|
||||
@@ -1,121 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
class TestScriptTypePyScript(PyScriptTest):
|
||||
def test_display_line_break(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('hello\nworld')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text_content = self.page.locator("script-py").text_content()
|
||||
assert "hello\nworld" == text_content
|
||||
|
||||
def test_amp(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('a & b')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text_content = self.page.locator("script-py").text_content()
|
||||
assert "a & b" == text_content
|
||||
|
||||
def test_quot(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('a " b')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text_content = self.page.locator("script-py").text_content()
|
||||
assert "a " b" == text_content
|
||||
|
||||
def test_lt_gt(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script type="py">
|
||||
from pyscript import display
|
||||
display('< < > >')
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text_content = self.page.locator("script-py").text_content()
|
||||
assert "< < > >" == text_content
|
||||
|
||||
def test_dynamically_add_script_type_py_tag(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script>
|
||||
function addPyScriptTag() {
|
||||
let tag = document.createElement('script');
|
||||
tag.type = 'py';
|
||||
tag.textContent = "print('hello world')";
|
||||
document.body.appendChild(tag);
|
||||
}
|
||||
addPyScriptTag();
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
# please note the test here was on timeout
|
||||
# incapable of finding a <button> after the script
|
||||
self.page.locator("script-py")
|
||||
|
||||
assert self.console.log.lines[-1] == "hello world"
|
||||
|
||||
def test_script_type_py_src_attribute(self):
|
||||
self.writefile("foo.py", "print('hello from foo')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" src="foo.py"></script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello from foo"
|
||||
|
||||
@pytest.mark.skip("FIXME: test failure is unrelated")
|
||||
def test_script_type_py_worker_attribute(self):
|
||||
self.writefile("foo.py", "print('hello from foo')")
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<script type="py" worker="foo.py"></script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "hello from foo"
|
||||
|
||||
@pytest.mark.skip("FIXME: output attribute is not implemented")
|
||||
def test_script_type_py_output_attribute(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<script type="py" output="first">
|
||||
print("<p>Hello</p>")
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
text = self.page.locator("#first").text_content()
|
||||
assert "<p>Hello</p>" in text
|
||||
|
||||
@pytest.mark.skip("FIXME: stderr attribute is not implemented")
|
||||
def test_script_type_py_stderr_attribute(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="stdout-div"></div>
|
||||
<div id="stderr-div"></div>
|
||||
<py-script output="stdout-div" stderr="stderr-div">
|
||||
import sys
|
||||
print("one.", file=sys.stderr)
|
||||
print("two.")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.page.locator("#stdout-div").text_content() == "one.two."
|
||||
assert self.page.locator("#stderr-div").text_content() == "one."
|
||||
@@ -1,33 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
class TestShadowRoot(PyScriptTest):
|
||||
# @skip_worker("FIXME: js.document")
|
||||
@pytest.mark.skip("FIXME: Element interface is gone. Replace with PyDom")
|
||||
def test_reachable_shadow_root(self):
|
||||
self.pyscript_run(
|
||||
r"""
|
||||
<script>
|
||||
// reason to wait for py-script is that it's the entry point for
|
||||
// all patches and the MutationObserver, otherwise being this a synchronous
|
||||
// script the constructor gets instantly invoked at the node before
|
||||
// py-script gets a chance to initialize itself.
|
||||
customElements.whenDefined('py-script').then(() => {
|
||||
customElements.define('s-r', class extends HTMLElement {
|
||||
constructor() {
|
||||
super().attachShadow({mode: 'closed'}).innerHTML =
|
||||
'<div id="shadowed">OK</div>';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<s-r></s-r>
|
||||
<py-script>
|
||||
import js
|
||||
js.console.log(Element("shadowed").innerHtml)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
assert self.console.log.lines[-1] == "OK"
|
||||
@@ -1,124 +0,0 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(
|
||||
reason="DECIDE: Should we remove the splashscreen?", allow_module_level=True
|
||||
)
|
||||
|
||||
|
||||
class TestSplashscreen(PyScriptTest):
|
||||
def test_autoshow_and_autoclose(self):
|
||||
"""
|
||||
By default, we show the splashscreen and we close it when the loading is
|
||||
complete.
|
||||
|
||||
XXX: this test is a bit fragile: now it works reliably because the
|
||||
startup is so slow that when we do expect(div).to_be_visible(), the
|
||||
splashscreen is still there. But in theory, if the startup become very
|
||||
fast, it could happen that by the time we arrive in python lang, it
|
||||
has already been removed.
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
print('hello pyscript')
|
||||
</py-script>
|
||||
""",
|
||||
wait_for_pyscript=False,
|
||||
)
|
||||
div = self.page.locator("py-splashscreen > div")
|
||||
expect(div).to_be_visible()
|
||||
expect(div).to_contain_text("Python startup...")
|
||||
assert "Python startup..." in self.console.info.text
|
||||
#
|
||||
# now we wait for the startup to complete
|
||||
self.wait_for_pyscript()
|
||||
#
|
||||
# and now the splashscreen should have been removed
|
||||
expect(div).to_be_hidden()
|
||||
assert self.page.locator("py-locator").count() == 0
|
||||
|
||||
assert "hello pyscript" in self.console.log.lines
|
||||
|
||||
def test_autoclose_false(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[splashscreen]
|
||||
autoclose = false
|
||||
</py-config>
|
||||
<py-script>
|
||||
print('hello pyscript')
|
||||
</py-script>
|
||||
""",
|
||||
)
|
||||
div = self.page.locator("py-splashscreen > div")
|
||||
expect(div).to_be_visible()
|
||||
expect(div).to_contain_text("Python startup...")
|
||||
expect(div).to_contain_text("Startup complete")
|
||||
assert "hello pyscript" in self.console.log.lines
|
||||
|
||||
def test_autoclose_loader_deprecated(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
autoclose_loader = false
|
||||
</py-config>
|
||||
<py-script>
|
||||
print('hello pyscript')
|
||||
</py-script>
|
||||
""",
|
||||
)
|
||||
warning = self.page.locator(".py-warning")
|
||||
inner_text = warning.inner_html()
|
||||
assert "The setting autoclose_loader is deprecated" in inner_text
|
||||
|
||||
div = self.page.locator("py-splashscreen > div")
|
||||
expect(div).to_be_visible()
|
||||
expect(div).to_contain_text("Python startup...")
|
||||
expect(div).to_contain_text("Startup complete")
|
||||
assert "hello pyscript" in self.console.log.lines
|
||||
|
||||
def test_splashscreen_disabled_option(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[splashscreen]
|
||||
enabled = false
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
def test():
|
||||
print("Hello pyscript!")
|
||||
test()
|
||||
</py-script>
|
||||
""",
|
||||
)
|
||||
assert self.page.locator("py-splashscreen").count() == 0
|
||||
assert self.console.log.lines[-1] == "Hello pyscript!"
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert py_terminal.inner_text() == "Hello pyscript!\n"
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_splashscreen_custom_message(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[splashscreen]
|
||||
autoclose = false
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
from js import document
|
||||
|
||||
splashscreen = document.querySelector("py-splashscreen")
|
||||
splashscreen.log("Hello, world!")
|
||||
</py-script>
|
||||
""",
|
||||
)
|
||||
|
||||
splashscreen = self.page.locator("py-splashscreen")
|
||||
assert splashscreen.count() == 1
|
||||
assert "Hello, world!" in splashscreen.inner_text()
|
||||
@@ -1,370 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(reason="FIXME: entire stdio should be reviewed", allow_module_level=True)
|
||||
|
||||
|
||||
class TestOutputHandling(PyScriptTest):
|
||||
# Source of a script to test the TargetedStdio functionality
|
||||
|
||||
def test_targeted_stdio_solo(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
terminal = true
|
||||
</py-config>
|
||||
<py-terminal></py-terminal>
|
||||
<div id="container">
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<div id="third"></div>
|
||||
</div>
|
||||
<py-script output="first">print("first 1.")</py-script>
|
||||
<py-script output="second">print("second.")</py-script>
|
||||
<py-script output="third">print("third.")</py-script>
|
||||
<py-script output="first">print("first 2.")</py-script>
|
||||
<py-script>print("no output.")</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
# Check that page has desired parent/child structure, and that
|
||||
# Output divs are correctly located
|
||||
assert (container := self.page.locator("#container")).count() > 0
|
||||
assert (first_div := container.locator("#first")).count() > 0
|
||||
assert (second_div := container.locator("#second")).count() > 0
|
||||
assert (third_div := container.locator("#third")).count() > 0
|
||||
|
||||
# Check that output ends up in proper div
|
||||
assert first_div.text_content() == "first 1.first 2."
|
||||
assert second_div.text_content() == "second."
|
||||
assert third_div.text_content() == "third."
|
||||
|
||||
# Check that tag with no otuput attribute doesn't end up in container at all
|
||||
assert container.get_by_text("no output.").count() == 0
|
||||
|
||||
# Check that all output ends up in py-terminal
|
||||
assert (
|
||||
self.page.locator("py-terminal").text_content()
|
||||
== "first 1.second.third.first 2.no output."
|
||||
)
|
||||
|
||||
# Check that all output ends up in the dev console, in order
|
||||
last_index = -1
|
||||
for line in ["first 1.", "second.", "third.", "first 2.", "no output."]:
|
||||
assert (line_index := self.console.log.lines.index(line)) > -1
|
||||
assert line_index > last_index
|
||||
last_index = line_index
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_stdio_escape(self):
|
||||
# Test that text that looks like HTML tags is properly escaped in stdio
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<py-script output="first">
|
||||
print("<p>Hello</p>")
|
||||
print('<img src="https://example.net">')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
text = self.page.locator("#first").text_content()
|
||||
|
||||
assert "<p>Hello</p>" in text
|
||||
assert '<img src="https://example.net">' in text
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_targeted_stdio_linebreaks(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<py-script output="first">
|
||||
print("one.")
|
||||
print("two.")
|
||||
print("three.")
|
||||
</py-script>
|
||||
|
||||
<div id="second"></div>
|
||||
<py-script output="second">
|
||||
print("one.\\ntwo.\\nthree.")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
# check line breaks at end of each input
|
||||
assert self.page.locator("#first").inner_html() == "one.<br>two.<br>three.<br>"
|
||||
|
||||
# new lines are converted to line breaks
|
||||
assert self.page.locator("#second").inner_html() == "one.<br>two.<br>three.<br>"
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_targeted_stdio_async(self):
|
||||
# Test the behavior of stdio capture in async contexts
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def coro(value, delay):
|
||||
print(value)
|
||||
await asyncio.sleep(delay)
|
||||
js.console.log(f"DONE {value}")
|
||||
</py-script>
|
||||
|
||||
<div id="first"></div>
|
||||
<py-script>
|
||||
asyncio.ensure_future(coro("first", 1))
|
||||
</py-script>
|
||||
|
||||
<div id="second"></div>
|
||||
<py-script output="second">
|
||||
asyncio.ensure_future(coro("second", 1))
|
||||
</py-script>
|
||||
|
||||
<div id="third"></div>
|
||||
<py-script output="third">
|
||||
asyncio.ensure_future(coro("third", 0))
|
||||
</py-script>
|
||||
|
||||
<py-script output="third">
|
||||
asyncio.ensure_future(coro("DONE", 3))
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
self.wait_for_console("DONE DONE")
|
||||
|
||||
# py-script tags without output parameter should not send
|
||||
# stdout to element
|
||||
assert self.page.locator("#first").text_content() == ""
|
||||
|
||||
# py-script tags with output parameter not expected to send
|
||||
# std to element in coroutine
|
||||
assert self.page.locator("#second").text_content() == ""
|
||||
assert self.page.locator("#third").text_content() == ""
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_targeted_stdio_interleaved(self):
|
||||
# Test that synchronous writes to stdout are placed correctly, even
|
||||
# While interleaved with scheduling coroutines in the same tag
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="good"></div>
|
||||
<div id="bad"></div>
|
||||
<py-script output="good">
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def coro_bad(value, delay):
|
||||
print(value)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
print("one.")
|
||||
asyncio.ensure_future(coro_bad("badone.", 0.1))
|
||||
print("two.")
|
||||
asyncio.ensure_future(coro_bad("badtwo.", 0.2))
|
||||
print("three.")
|
||||
asyncio.ensure_future(coro_bad("badthree.", 0))
|
||||
asyncio.ensure_future(coro_bad("DONE", 1))
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
# Three prints should appear from synchronous writes
|
||||
assert self.page.locator("#good").text_content() == "one.two.three."
|
||||
|
||||
# Check that all output ends up in the dev console, in order
|
||||
last_index = -1
|
||||
for line in ["one.", "two.", "three.", "badthree.", "badone.", "badtwo."]:
|
||||
assert (line_index := self.console.log.lines.index(line)) > -1
|
||||
assert line_index > last_index
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_targeted_stdio_dynamic_tags(self):
|
||||
# Test that creating py-script tags via Python still leaves
|
||||
# stdio targets working
|
||||
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<py-script output="first">
|
||||
print("first.")
|
||||
|
||||
import js
|
||||
tag = js.document.createElement("py-script")
|
||||
tag.innerText = "print('second.')"
|
||||
tag.setAttribute("output", "second")
|
||||
js.document.body.appendChild(tag)
|
||||
|
||||
print("first.")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
# Ensure second tag was added to page
|
||||
assert (second_div := self.page.locator("#second")).count() > 0
|
||||
|
||||
# Ensure output when to correct locations
|
||||
assert self.page.locator("#first").text_content() == "first.first."
|
||||
assert second_div.text_content() == "second."
|
||||
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_stdio_stdout_id_errors(self):
|
||||
# Test that using an ID not present on the page as the Output
|
||||
# Attribute creates exactly 1 warning banner per missing id
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script output="not-on-page">
|
||||
print("bad.")
|
||||
</py-script>
|
||||
|
||||
<div id="on-page"></div>
|
||||
<py-script>
|
||||
print("good.")
|
||||
</py-script>
|
||||
|
||||
<py-script output="not-on-page">
|
||||
print("bad.")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
banner = self.page.query_selector_all(".py-warning")
|
||||
assert len(banner) == 1
|
||||
banner_content = banner[0].inner_text()
|
||||
expected = (
|
||||
'output = "not-on-page" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
assert banner_content == expected
|
||||
|
||||
def test_stdio_stderr_id_errors(self):
|
||||
# Test that using an ID not present on the page as the stderr
|
||||
# attribute creates exactly 1 warning banner per missing id
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script stderr="not-on-page">
|
||||
import sys
|
||||
print("bad.", file=sys.stderr)
|
||||
</py-script>
|
||||
|
||||
<div id="on-page"></div>
|
||||
<py-script>
|
||||
print("good.", file=sys.stderr)
|
||||
</py-script>
|
||||
|
||||
<py-script stderr="not-on-page">
|
||||
print("bad.", file=sys.stderr)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
banner = self.page.query_selector_all(".py-warning")
|
||||
assert len(banner) == 1
|
||||
banner_content = banner[0].inner_text()
|
||||
expected = (
|
||||
'stderr = "not-on-page" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
assert banner_content == expected
|
||||
|
||||
def test_stdio_stderr(self):
|
||||
# Test that stderr works, and routes to the same location as stdout
|
||||
# Also, script tags with the stderr attribute route to an additional location
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="stdout-div"></div>
|
||||
<div id="stderr-div"></div>
|
||||
<py-script output="stdout-div" stderr="stderr-div">
|
||||
import sys
|
||||
print("one.", file=sys.stderr)
|
||||
print("two.")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.page.locator("#stdout-div").text_content() == "one.two."
|
||||
assert self.page.locator("#stderr-div").text_content() == "one."
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_stdio_output_attribute_change(self):
|
||||
# If the user changes the 'output' attribute of a <py-script> tag mid-execution,
|
||||
# Output should no longer go to the selected div and a warning should appear
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<!-- There is no tag with id "third" -->
|
||||
<py-script id="pyscript-tag" output="first">
|
||||
print("one.")
|
||||
|
||||
# Change the 'output' attribute of this tag
|
||||
import js
|
||||
this_tag = js.document.getElementById("pyscript-tag")
|
||||
|
||||
this_tag.setAttribute("output", "second")
|
||||
print("two.")
|
||||
|
||||
this_tag.setAttribute("output", "third")
|
||||
print("three.")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
assert self.page.locator("#first").text_content() == "one."
|
||||
assert self.page.locator("#second").text_content() == "two."
|
||||
expected_alert_banner_msg = (
|
||||
'output = "third" does not match the id of any element on the page.'
|
||||
)
|
||||
|
||||
alert_banner = self.page.locator(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_stdio_target_element_id_change(self):
|
||||
# If the user changes the ID of the targeted DOM element mid-execution,
|
||||
# Output should no longer go to the selected element and a warning should appear
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="first"></div>
|
||||
<div id="second"></div>
|
||||
<!-- There is no tag with id "third" -->
|
||||
<py-script id="pyscript-tag" output="first">
|
||||
print("one.")
|
||||
|
||||
# Change the ID of the targeted DIV to something else
|
||||
import js
|
||||
target_tag = js.document.getElementById("first")
|
||||
|
||||
# should fail and show banner
|
||||
target_tag.setAttribute("id", "second")
|
||||
print("two.")
|
||||
|
||||
# But changing both the 'output' attribute and the id of the target
|
||||
# should work
|
||||
target_tag.setAttribute("id", "third")
|
||||
js.document.getElementById("pyscript-tag").setAttribute("output", "third")
|
||||
print("three.")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
# Note the ID of the div has changed by the time of this assert
|
||||
assert self.page.locator("#third").text_content() == "one.three."
|
||||
|
||||
expected_alert_banner_msg = (
|
||||
'output = "first" does not match the id of any element on the page.'
|
||||
)
|
||||
alert_banner = self.page.locator(".alert-banner")
|
||||
assert expected_alert_banner_msg in alert_banner.inner_text()
|
||||
@@ -1,47 +0,0 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
pytest.skip(
|
||||
reason="FIX TESTS: These tests should reflect new PyScript and remove/change css ",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
class TestStyle(PyScriptTest):
|
||||
def test_pyscript_not_defined(self):
|
||||
"""Test raw elements that are not defined for display:none"""
|
||||
doc = """
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="build/pyscript.css" />
|
||||
</head>
|
||||
<body>
|
||||
<py-config>hello</py-config>
|
||||
<py-script>hello</py-script>
|
||||
<py-repl>hello</py-repl>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.writefile("test-not-defined-css.html", doc)
|
||||
self.goto("test-not-defined-css.html")
|
||||
expect(self.page.locator("py-config")).to_be_hidden()
|
||||
expect(self.page.locator("py-script")).to_be_hidden()
|
||||
expect(self.page.locator("py-repl")).to_be_hidden()
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_pyscript_defined(self):
|
||||
"""Test elements have visibility that should"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
name = "foo"
|
||||
</py-config>
|
||||
<py-script>display("hello")</py-script>
|
||||
<py-repl>display("hello")</py-repl>
|
||||
"""
|
||||
)
|
||||
expect(self.page.locator("py-config")).to_be_hidden()
|
||||
expect(self.page.locator("py-script")).to_be_visible()
|
||||
expect(self.page.locator("py-repl")).to_be_visible()
|
||||
@@ -1,32 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
pytest.skip(reason="FIXME: Restore the banner", allow_module_level=True)
|
||||
|
||||
|
||||
class TestWarningsAndBanners(PyScriptTest):
|
||||
# Test the behavior of generated warning banners
|
||||
|
||||
def test_create_singular_warning(self):
|
||||
# Use a script tag with an invalid output attribute to generate a warning, but only one
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script output="foo">
|
||||
print("one.")
|
||||
print("two.")
|
||||
</py-script>
|
||||
<py-script output="foo">
|
||||
print("three.")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
loc = self.page.locator(".alert-banner")
|
||||
|
||||
# Only one banner should appear
|
||||
assert loc.count() == 1
|
||||
assert (
|
||||
loc.text_content()
|
||||
== 'output = "foo" does not match the id of any element on the page.'
|
||||
)
|
||||
@@ -1,419 +0,0 @@
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from .support import ROOT, PyScriptTest, wait_for_render, with_execution_thread
|
||||
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="SKIPPING EXAMPLES: these should be moved elsewhere and updated"
|
||||
)
|
||||
@with_execution_thread(None)
|
||||
@pytest.mark.usefixtures("chdir")
|
||||
class TestExamples(PyScriptTest):
|
||||
"""
|
||||
Each example requires the same three tests:
|
||||
|
||||
- Test that the initial markup loads properly (currently done by
|
||||
testing the <title> tag's content)
|
||||
- Testing that pyscript is loading properly
|
||||
- Testing that the page contains appropriate content after rendering
|
||||
"""
|
||||
|
||||
@pytest.fixture()
|
||||
def chdir(self):
|
||||
# make sure that the http server serves from the right directory
|
||||
ROOT.join("pyscriptjs").chdir()
|
||||
|
||||
def test_hello_world(self):
|
||||
self.goto("examples/hello_world.html")
|
||||
self.wait_for_pyscript()
|
||||
assert self.page.title() == "PyScript Hello World"
|
||||
content = self.page.content()
|
||||
pattern = "\\d+/\\d+/\\d+, \\d+:\\d+:\\d+" # e.g. 08/09/2022 15:57:32
|
||||
assert re.search(pattern, content)
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_simple_clock(self):
|
||||
self.goto("examples/simple_clock.html")
|
||||
self.wait_for_pyscript()
|
||||
assert self.page.title() == "Simple Clock Demo"
|
||||
pattern = r"\d{2}/\d{2}/\d{4}, \d{2}:\d{2}:\d{2}"
|
||||
# run for 5 seconds to be sure that we see the page with "It's
|
||||
# espresso time!"
|
||||
for _ in range(5):
|
||||
content = self.page.inner_html("#outputDiv2")
|
||||
if re.match(pattern, content) and int(content[-1]) in (0, 4, 8):
|
||||
assert self.page.inner_html("#outputDiv3") == "It's espresso time!"
|
||||
break
|
||||
else:
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise AssertionError("Espresso time not found :(")
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_altair(self):
|
||||
self.goto("examples/altair.html")
|
||||
self.wait_for_pyscript(timeout=90 * 1000)
|
||||
assert self.page.title() == "Altair"
|
||||
wait_for_render(self.page, "*", '<canvas.*?class=\\"marks\\".*?>')
|
||||
save_as_png_link = self.page.locator("text=Save as PNG")
|
||||
see_source_link = self.page.locator("text=View Source")
|
||||
# These shouldn't be visible since we didn't click the menu
|
||||
assert not save_as_png_link.is_visible()
|
||||
assert not see_source_link.is_visible()
|
||||
|
||||
self.page.locator("summary").click()
|
||||
|
||||
# Let's confirm that the links are visible now after clicking the menu
|
||||
assert save_as_png_link.is_visible()
|
||||
assert see_source_link.is_visible()
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_antigravity(self):
|
||||
self.goto("examples/antigravity.html")
|
||||
self.wait_for_pyscript()
|
||||
assert self.page.title() == "Antigravity"
|
||||
|
||||
# confirm that svg added to page
|
||||
wait_for_render(self.page, "*", '<svg.*id="svg8".*>')
|
||||
|
||||
# Get svg layer of flying character
|
||||
char = self.page.wait_for_selector("#python")
|
||||
assert char is not None
|
||||
|
||||
# check that character moves in negative-y direction over time
|
||||
ycoord_pattern = r"translate\(-?\d*\.\d*,\s(?P<ycoord>-?[\d.]+)\)"
|
||||
starting_y_coord = float(
|
||||
re.match(ycoord_pattern, char.get_attribute("transform")).group("ycoord")
|
||||
)
|
||||
time.sleep(2)
|
||||
later_y_coord = float(
|
||||
re.match(ycoord_pattern, char.get_attribute("transform")).group("ycoord")
|
||||
)
|
||||
assert later_y_coord < starting_y_coord
|
||||
self.check_tutor_generated_code(modules_to_check=["antigravity.py"])
|
||||
|
||||
def test_bokeh(self):
|
||||
# XXX improve this test
|
||||
self.goto("examples/bokeh.html")
|
||||
self.wait_for_pyscript(timeout=90 * 1000)
|
||||
assert self.page.title() == "Bokeh Example"
|
||||
wait_for_render(self.page, "*", '<div.*?class="bk.*".*?>')
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_bokeh_interactive(self):
|
||||
# XXX improve this test
|
||||
self.goto("examples/bokeh_interactive.html")
|
||||
self.wait_for_pyscript(timeout=90 * 1000)
|
||||
assert self.page.title() == "Bokeh Example"
|
||||
wait_for_render(self.page, "*", '<div.*?class=\\"bk\\".*?>')
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
@pytest.mark.skip("flaky, see issue 759")
|
||||
def test_d3(self):
|
||||
self.goto("examples/d3.html")
|
||||
self.wait_for_pyscript()
|
||||
assert (
|
||||
self.page.title() == "d3: JavaScript & PyScript visualizations side-by-side"
|
||||
)
|
||||
wait_for_render(self.page, "*", "<svg.*?>")
|
||||
assert "PyScript version" in self.page.content()
|
||||
pyscript_chart = self.page.wait_for_selector("#py")
|
||||
|
||||
# Let's simply assert that the text of the chart is as expected which
|
||||
# means that the chart rendered successfully and with the right text
|
||||
assert "🍊21\n🍇13\n🍏8\n🍌5\n🍐3\n🍋2\n🍎1\n🍉1" in pyscript_chart.inner_text()
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["d3.py"])
|
||||
|
||||
def test_folium(self):
|
||||
self.goto("examples/folium.html")
|
||||
self.wait_for_pyscript(timeout=90 * 1000)
|
||||
assert self.page.title() == "Folium"
|
||||
wait_for_render(self.page, "*", "<iframe srcdoc=")
|
||||
|
||||
# We need to look into the iframe first
|
||||
iframe = self.page.frame_locator("iframe")
|
||||
|
||||
# Just checking that legend was rendered correctly
|
||||
legend = iframe.locator("#legend")
|
||||
assert "Unemployment Rate (%)" in legend.inner_html()
|
||||
|
||||
# Let's check that the zoom buttons are rendered and clickable
|
||||
# Note: if element is not clickable it will timeout
|
||||
zoom_in = iframe.locator("[aria-label='Zoom in']")
|
||||
assert "+" in zoom_in.inner_text()
|
||||
zoom_in.click()
|
||||
zoom_out = iframe.locator("[aria-label='Zoom out']")
|
||||
assert "−" in zoom_out.inner_text()
|
||||
zoom_out.click()
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_markdown_plugin(self):
|
||||
# Given the example page with:
|
||||
# * <title>PyMarkdown</title>
|
||||
# * <py-md>#Hello world!</py-md>
|
||||
self.goto("examples/markdown-plugin.html")
|
||||
self.wait_for_pyscript()
|
||||
# ASSERT title is rendered correctly
|
||||
assert self.page.title() == "PyMarkdown"
|
||||
# ASSERT markdown is rendered to the corresponding HTML tag
|
||||
wait_for_render(self.page, "*", "<h1>Hello world!</h1>")
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_matplotlib(self):
|
||||
self.goto("examples/matplotlib.html")
|
||||
self.wait_for_pyscript(timeout=90 * 1000)
|
||||
assert self.page.title() == "Matplotlib"
|
||||
wait_for_render(self.page, "*", "<img src=['\"]data:image")
|
||||
# The image is being rended using base64, lets fetch its source
|
||||
# and replace everything but the actual base64 string.
|
||||
# Note: The first image on the page is the logo, so we are lookin
|
||||
# at the mpl-1 div which is rendered once the image is in the page
|
||||
# if this test fails, confirm that the div has the right id using
|
||||
# the --dev flag when running the tests
|
||||
test = self.page.wait_for_selector("#mpl >> img")
|
||||
img_src = test.get_attribute("src").replace(
|
||||
"data:image/png;charset=utf-8;base64,", ""
|
||||
)
|
||||
# Finally, let's get the np array from the previous data
|
||||
img_data = np.asarray(Image.open(io.BytesIO(base64.b64decode(img_src))))
|
||||
with Image.open(
|
||||
os.path.join(os.path.dirname(__file__), "test_assets", "tripcolor.png"),
|
||||
) as image:
|
||||
ref_data = np.asarray(image)
|
||||
# Now that we have both images data as a numpy array
|
||||
# let's confirm that they are the same
|
||||
deviation = np.mean(np.abs(img_data - ref_data))
|
||||
assert deviation == 0.0
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_numpy_canvas_fractals(self):
|
||||
self.goto("examples/numpy_canvas_fractals.html")
|
||||
self.wait_for_pyscript(timeout=90 * 1000)
|
||||
assert (
|
||||
self.page.title()
|
||||
== "Visualization of Mandelbrot, Julia and Newton sets with NumPy and HTML5 canvas"
|
||||
)
|
||||
wait_for_render(
|
||||
self.page, "*", "<div.*?id=['\"](mandelbrot|julia|newton)['\"].*?>"
|
||||
)
|
||||
|
||||
# Assert that we get the title and canvas for each element
|
||||
mandelbrot = self.page.wait_for_selector("#mandelbrot")
|
||||
assert "Mandelbrot set" in mandelbrot.inner_text()
|
||||
assert "<canvas" in mandelbrot.inner_html()
|
||||
|
||||
julia = self.page.wait_for_selector("#julia")
|
||||
assert "Julia set" in julia.inner_text()
|
||||
assert "<canvas" in julia.inner_html()
|
||||
|
||||
newton = self.page.wait_for_selector("#newton")
|
||||
assert "Newton set" in newton.inner_text()
|
||||
assert "<canvas" in newton.inner_html()
|
||||
|
||||
# Confirm that all fieldsets are rendered correctly
|
||||
poly = newton.wait_for_selector("#poly")
|
||||
assert poly.input_value() == "z**3 - 2*z + 2"
|
||||
|
||||
coef = newton.wait_for_selector("#coef")
|
||||
assert coef.input_value() == "1"
|
||||
|
||||
# Let's now change some x/y values to confirm that they
|
||||
# are editable (is it the best way to test this?)
|
||||
x0 = newton.wait_for_selector("#x0")
|
||||
y0 = newton.wait_for_selector("#y0")
|
||||
|
||||
x0.fill("50")
|
||||
assert x0.input_value() == "50"
|
||||
y0.fill("-25")
|
||||
assert y0.input_value() == "-25"
|
||||
|
||||
# This was the first computation with the default values
|
||||
assert self.console.log.lines[-2] == "Computing Newton set ..."
|
||||
# Confirm that changing the input values, triggered a new computation
|
||||
assert self.console.log.lines[-1] == "Computing Newton set ..."
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_panel(self):
|
||||
self.goto("examples/panel.html")
|
||||
self.wait_for_pyscript(timeout=90 * 1000)
|
||||
assert self.page.title() == "Panel Example"
|
||||
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
|
||||
slider_title = self.page.wait_for_selector(".bk-slider-title")
|
||||
assert slider_title.inner_text() == "Amplitude: 0"
|
||||
|
||||
slider_result = self.page.wait_for_selector(".bk-clearfix")
|
||||
assert slider_result.inner_text() == "Amplitude is: 0"
|
||||
|
||||
amplitude_bar = self.page.wait_for_selector(".noUi-connects")
|
||||
amplitude_bar.click()
|
||||
|
||||
# Let's confirm that slider title changed
|
||||
assert slider_title.inner_text() == "Amplitude: 5"
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_panel_deckgl(self):
|
||||
# XXX improve this test
|
||||
self.goto("examples/panel_deckgl.html")
|
||||
self.wait_for_pyscript(timeout=90 * 1000)
|
||||
assert self.page.title() == "PyScript/Panel DeckGL Demo"
|
||||
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_panel_kmeans(self):
|
||||
# XXX improve this test>>>>>>> main
|
||||
self.goto("examples/panel_kmeans.html")
|
||||
self.wait_for_pyscript(timeout=120 * 1000)
|
||||
assert self.page.title() == "Pyscript/Panel KMeans Demo"
|
||||
wait_for_render(
|
||||
self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>", timeout_seconds=60 * 2
|
||||
)
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_panel_stream(self):
|
||||
# XXX improve this test
|
||||
self.goto("examples/panel_stream.html")
|
||||
self.wait_for_pyscript(timeout=3 * 60 * 1000)
|
||||
assert self.page.title() == "PyScript/Panel Streaming Demo"
|
||||
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_repl(self):
|
||||
self.goto("examples/repl.html")
|
||||
self.wait_for_pyscript()
|
||||
assert self.page.title() == "REPL"
|
||||
self.page.wait_for_selector("py-repl")
|
||||
|
||||
self.page.locator("py-repl").type("display('Hello, World!')")
|
||||
self.page.wait_for_selector(".py-repl-run-button").click()
|
||||
self.page.wait_for_selector("#my-repl-repl-output")
|
||||
assert (
|
||||
self.page.locator("#my-repl-repl-output").text_content() == "Hello, World!"
|
||||
)
|
||||
|
||||
# Confirm that using the second repl still works properly
|
||||
self.page.locator("#my-repl-1").type("display(2*2)")
|
||||
self.page.keyboard.press("Shift+Enter")
|
||||
my_repl_1 = self.page.wait_for_selector("#my-repl-1-repl-output")
|
||||
assert my_repl_1.inner_text() == "4"
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["antigravity.py"])
|
||||
|
||||
def test_repl2(self):
|
||||
self.goto("examples/repl2.html")
|
||||
self.wait_for_pyscript(timeout=1.5 * 60 * 1000)
|
||||
assert self.page.title() == "Custom REPL Example"
|
||||
wait_for_render(self.page, "*", "<py-repl.*?>")
|
||||
# confirm we can import utils and run one command
|
||||
self.page.locator("py-repl").type("import utils\ndisplay(utils.now())")
|
||||
self.page.wait_for_selector("py-repl .py-repl-run-button").click()
|
||||
# Make sure the output is in the page
|
||||
self.page.wait_for_selector("#my-repl-1")
|
||||
# utils.now returns current date time
|
||||
content = self.page.content()
|
||||
pattern = "\\d+/\\d+/\\d+, \\d+:\\d+:\\d+" # e.g. 08/09/2022 15:57:32
|
||||
assert re.search(pattern, content)
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["antigravity.py"])
|
||||
|
||||
def test_todo(self):
|
||||
self.goto("examples/todo.html")
|
||||
self.wait_for_pyscript()
|
||||
assert self.page.title() == "Todo App"
|
||||
wait_for_render(self.page, "*", "<input.*?id=['\"]new-task-content['\"].*?>")
|
||||
todo_input = self.page.locator("input")
|
||||
submit_task_button = self.page.locator("button")
|
||||
|
||||
todo_input.type("Fold laundry")
|
||||
submit_task_button.click()
|
||||
|
||||
first_task = self.page.locator("#task-0")
|
||||
assert "Fold laundry" in first_task.inner_text()
|
||||
|
||||
task_checkbox = first_task.locator("input")
|
||||
# Confirm that the new task isn't checked
|
||||
assert not task_checkbox.is_checked()
|
||||
|
||||
# Let's mark it as done now
|
||||
task_checkbox.check()
|
||||
|
||||
# Basic check that the task has the line-through class
|
||||
assert (
|
||||
'<p class="m-0 inline line-through">Fold laundry</p>'
|
||||
in first_task.inner_html()
|
||||
)
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["./utils.py", "./todo.py"])
|
||||
|
||||
def test_todo_pylist(self):
|
||||
self.goto("examples/todo-pylist.html")
|
||||
self.wait_for_pyscript()
|
||||
assert self.page.title() == "Todo App"
|
||||
wait_for_render(self.page, "*", "<input.*?id=['\"]new-task-content['\"].*?>")
|
||||
todo_input = self.page.locator("input")
|
||||
submit_task_button = self.page.locator("button#new-task-btn")
|
||||
|
||||
todo_input.type("Fold laundry")
|
||||
submit_task_button.click()
|
||||
|
||||
first_task = self.page.locator("div#myList-c-0")
|
||||
assert "Fold laundry" in first_task.inner_text()
|
||||
|
||||
task_checkbox = first_task.locator("input")
|
||||
# Confirm that the new task isn't checked
|
||||
assert not task_checkbox.is_checked()
|
||||
|
||||
# Let's mark it as done now
|
||||
task_checkbox.check()
|
||||
|
||||
# Basic check that the task has the line-through class
|
||||
assert "line-through" in first_task.get_attribute("class")
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["utils.py"])
|
||||
|
||||
@pytest.mark.xfail(reason="To be moved to collective and updated, see issue #686")
|
||||
def test_toga_freedom(self):
|
||||
self.goto("examples/toga/freedom.html")
|
||||
self.wait_for_pyscript()
|
||||
assert self.page.title() in ["Loading...", "Freedom Units"]
|
||||
wait_for_render(self.page, "*", "<(main|div).*?id=['\"]toga_\\d+['\"].*?>")
|
||||
|
||||
page_content = self.page.content()
|
||||
|
||||
assert "Fahrenheit" in page_content
|
||||
assert "Celsius" in page_content
|
||||
|
||||
self.page.locator("#toga_f_input").fill("105")
|
||||
self.page.locator("button#toga_calculate").click()
|
||||
result = self.page.locator("#toga_c_input")
|
||||
assert "40.555" in result.input_value()
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_webgl_raycaster_index(self):
|
||||
# XXX improve this test
|
||||
self.goto("examples/webgl/raycaster/index.html")
|
||||
self.wait_for_pyscript()
|
||||
assert self.page.title() == "Raycaster"
|
||||
wait_for_render(self.page, "*", "<canvas.*?>")
|
||||
self.assert_no_banners()
|
||||
@@ -1,305 +0,0 @@
|
||||
import re
|
||||
import pytest
|
||||
|
||||
from .support import PyScriptTest, skip_worker
|
||||
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="SKIPPING Docs: these should be reviewed ALL TOGETHER as we fix docs"
|
||||
)
|
||||
class TestDocsSnippets(PyScriptTest):
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_tutorials_py_click(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button
|
||||
py-click="current_time()"
|
||||
id="get-time" class="py-button">
|
||||
Get current time
|
||||
</button>
|
||||
<p id="current-time"></p>
|
||||
|
||||
<py-script>
|
||||
from pyscript import Element
|
||||
import datetime
|
||||
|
||||
def current_time():
|
||||
now = datetime.datetime.now()
|
||||
|
||||
# Get paragraph element by id
|
||||
paragraph = Element("current-time")
|
||||
|
||||
# Add current time to the paragraph element
|
||||
paragraph.write(now.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
btn = self.page.wait_for_selector("#get-time")
|
||||
btn.click()
|
||||
|
||||
current_time = self.page.wait_for_selector("#current-time")
|
||||
|
||||
pattern = "\\d+-\\d+-\\d+\\s\\d+:\\d+:\\d+" # e.g. 08-09-2022 15:57:32
|
||||
assert re.search(pattern, current_time.inner_text())
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_tutorials_requests(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
packages = ["requests", "pyodide-http"]
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
import requests
|
||||
import pyodide_http
|
||||
|
||||
# Patch the Requests library so it works with Pyscript
|
||||
pyodide_http.patch_all()
|
||||
|
||||
# Make a request to the JSON Placeholder API
|
||||
response = requests.get("https://jsonplaceholder.typicode.com/todos")
|
||||
print(response.json())
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
# Just a small check to confirm that the response was received
|
||||
assert "userId" in py_terminal.inner_text()
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_tutorials_py_config_fetch(self):
|
||||
# flake8: noqa
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[fetch]]
|
||||
from = "https://pyscript.net/examples/"
|
||||
files = ["utils.py"]
|
||||
[[fetch]]
|
||||
from = "https://gist.githubusercontent.com/FabioRosado/faba0b7f6ad4438b07c9ac567c73b864/raw/37603b76dc7ef7997bf36781ea0116150f727f44/"
|
||||
files = ["todo.py"]
|
||||
</py-config>
|
||||
<py-script>
|
||||
from todo import add_task, add_task_event
|
||||
</py-script>
|
||||
<section>
|
||||
<div class="text-center w-full mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-800 uppercase tracking-tight">
|
||||
To Do List
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<input id="new-task-content" class="py-input" type="text">
|
||||
<button id="new-task-btn" class="py-button" type="submit" py-click="add_task()">
|
||||
Add task
|
||||
</button>
|
||||
</div>
|
||||
<div id="list-tasks-container" class="flex flex-col-reverse mt-4"></div>
|
||||
<template id="task-template">
|
||||
<section class="task py-li-element">
|
||||
<label for="flex items-center p-2 ">
|
||||
<input class="mr-2" type="checkbox">
|
||||
<p class="m-0 inline"></p>
|
||||
</label>
|
||||
</section>
|
||||
</template
|
||||
"""
|
||||
)
|
||||
|
||||
todo_input = self.page.locator("input")
|
||||
submit_task_button = self.page.locator("button")
|
||||
|
||||
todo_input.type("Fold laundry")
|
||||
submit_task_button.click()
|
||||
|
||||
first_task = self.page.locator("#task-0")
|
||||
assert "Fold laundry" in first_task.inner_text()
|
||||
|
||||
task_checkbox = first_task.locator("input")
|
||||
# Confirm that the new task isn't checked
|
||||
assert not task_checkbox.is_checked()
|
||||
|
||||
# Let's mark it as done now
|
||||
task_checkbox.check()
|
||||
|
||||
# Basic check that the task has the line-through class
|
||||
assert (
|
||||
'<p class="m-0 inline line-through">Fold laundry</p>'
|
||||
in first_task.inner_html()
|
||||
)
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_tutorials_py_config_interpreter(self):
|
||||
"""Load a previous version of Pyodide"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
[[interpreters]]
|
||||
src = "https://cdn.jsdelivr.net/pyodide/v0.23.0/full/pyodide.js"
|
||||
name = "pyodide-0.23.0"
|
||||
lang = "python"
|
||||
</py-config>
|
||||
<py-script>
|
||||
import pyodide
|
||||
print(pyodide.__version__)
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert "0.23.0" in py_terminal.inner_text()
|
||||
self.assert_no_banners()
|
||||
|
||||
@skip_worker("FIXME: display()")
|
||||
def test_tutorials_writing_to_page(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="manual-write"></div>
|
||||
<button py-click="write_to_page()" id="manual">Say Hello</button>
|
||||
<div id="display-write"></div>
|
||||
<button py-click="display_to_div()" id="display">Say Things!</button>
|
||||
<div>
|
||||
<py-terminal>
|
||||
</div>
|
||||
<button py-click="print_to_page()" id="print">Print Things!</button>
|
||||
|
||||
<py-script>
|
||||
def write_to_page():
|
||||
manual_div = Element("manual-write")
|
||||
manual_div.element.innerHTML = "<p><b>Hello World</b></p>"
|
||||
|
||||
def display_to_div():
|
||||
display("I display things!", target="display-write")
|
||||
|
||||
def print_to_page():
|
||||
print("I print things!")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
btn_manual = self.page.wait_for_selector("#manual")
|
||||
btn_display = self.page.wait_for_selector("#display")
|
||||
btn_print = self.page.wait_for_selector("#print")
|
||||
|
||||
btn_manual.click()
|
||||
manual_write_div = self.page.wait_for_selector("#manual-write")
|
||||
assert "<p><b>Hello World</b></p>" in manual_write_div.inner_html()
|
||||
|
||||
btn_display.click()
|
||||
display_write_div = self.page.wait_for_selector("#display-write")
|
||||
assert "I display things!" in display_write_div.inner_text()
|
||||
|
||||
btn_print.click()
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
assert "I print things!" in py_terminal.inner_text()
|
||||
self.assert_no_banners()
|
||||
|
||||
def test_guides_asyncio(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
for i in range(3):
|
||||
print(i)
|
||||
|
||||
asyncio.ensure_future(main())
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
py_terminal = self.page.wait_for_selector("py-terminal")
|
||||
|
||||
assert "0\n1\n2\n" in py_terminal.inner_text()
|
||||
|
||||
@skip_worker("FIXME: js.document")
|
||||
def test_reference_pyterminal_xterm(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
xterm = true
|
||||
</py-config>
|
||||
<py-script>
|
||||
print("HELLO!")
|
||||
import js
|
||||
import asyncio
|
||||
|
||||
async def adjust_term_size(columns, rows):
|
||||
xterm = await js.document.querySelector('py-terminal').xtermReady
|
||||
xterm.resize(columns, rows)
|
||||
print("test-done")
|
||||
|
||||
asyncio.ensure_future(adjust_term_size(40, 10))
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.get_by_text("test-done").wait_for()
|
||||
|
||||
py_terminal = self.page.locator("py-terminal")
|
||||
print(dir(py_terminal))
|
||||
print(type(py_terminal))
|
||||
assert py_terminal.evaluate("el => el.xterm.cols") == 40
|
||||
assert py_terminal.evaluate("el => el.xterm.rows") == 10
|
||||
|
||||
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||
def test_reference_when_simple(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="my_btn">Click Me to Say Hi</button>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
@when("click", selector="#my_btn")
|
||||
def say_hello():
|
||||
print(f"Hello, world!")
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
self.page.get_by_text("Click Me to Say Hi").click()
|
||||
self.wait_for_console("Hello, world!")
|
||||
assert ("Hello, world!") in self.console.log.lines
|
||||
|
||||
@skip_worker(reason="FIXME: js.document (@when decorator)")
|
||||
def test_reference_when_complex(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<div id="container">
|
||||
<button>First</button>
|
||||
<button>Second</button>
|
||||
<button>Third</button>
|
||||
</div>
|
||||
<py-script>
|
||||
from pyscript import when
|
||||
import js
|
||||
|
||||
@when("click", selector="#container button")
|
||||
def highlight(evt):
|
||||
#Set the clicked button's background to green
|
||||
evt.target.style.backgroundColor = 'green'
|
||||
|
||||
#Set the background of all buttons to red
|
||||
other_buttons = (button for button in js.document.querySelectorAll('button') if button != evt.target)
|
||||
for button in other_buttons:
|
||||
button.style.backgroundColor = 'red'
|
||||
|
||||
print("set") # Test Only
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
|
||||
def getBackgroundColor(locator):
|
||||
return locator.evaluate(
|
||||
"(element) => getComputedStyle(element).getPropertyValue('background-color')"
|
||||
)
|
||||
|
||||
first_button = self.page.get_by_text("First")
|
||||
assert getBackgroundColor(first_button) == "rgb(239, 239, 239)"
|
||||
|
||||
first_button.click()
|
||||
self.wait_for_console("set")
|
||||
|
||||
assert getBackgroundColor(first_button) == "rgb(0, 128, 0)"
|
||||
assert getBackgroundColor(self.page.get_by_text("Second")) == "rgb(255, 0, 0)"
|
||||
assert getBackgroundColor(self.page.get_by_text("Third")) == "rgb(255, 0, 0)"
|
||||
Reference in New Issue
Block a user