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:
Fabio Pliger
2023-09-15 14:09:07 -07:00
committed by GitHub
parent ed6de66c08
commit f77241e977
28 changed files with 24 additions and 5 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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 == []

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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()

View File

@@ -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",
]

View File

@@ -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)

View File

@@ -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"

View File

@@ -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

View File

@@ -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)"

View File

@@ -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"]

View File

@@ -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 &amp; b')
</script>
"""
)
text_content = self.page.locator("script-py").text_content()
assert "a &amp; b" == text_content
def test_quot(self):
self.pyscript_run(
r"""
<script type="py">
from pyscript import display
display('a &quot; b')
</script>
"""
)
text_content = self.page.locator("script-py").text_content()
assert "a &quot; b" == text_content
def test_lt_gt(self):
self.pyscript_run(
r"""
<script type="py">
from pyscript import display
display('< &lt; &gt; >')
</script>
"""
)
text_content = self.page.locator("script-py").text_content()
assert "< &lt; &gt; >" == 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."

View File

@@ -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"

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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.'
)

View File

@@ -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()

View File

@@ -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)"