Bootstrap python tests (#697)

* move current integration tests to the integration folder

* move pyscript.py into its own python folder

* change the path for python unit testing files

* change pyscript.py path

* Update Makefile

* remove echo

* replace conda run with pytest directly

* oops, add python test files I embarrassingly forgot to add

Co-authored-by: Peter W <34256109+pww217@users.noreply.github.com>
This commit is contained in:
Fabio Pliger
2022-08-18 16:59:07 -05:00
committed by GitHub
parent 8aba271a42
commit fa7a97ca30
15 changed files with 47 additions and 7 deletions

View File

View File

@@ -0,0 +1,51 @@
"""All data required for testing examples"""
import threading
from http.server import HTTPServer as SuperHTTPServer
from http.server import SimpleHTTPRequestHandler
import pytest
from .support import Logger
@pytest.fixture(scope="session")
def logger():
return Logger()
class HTTPServer(SuperHTTPServer):
"""
Class for wrapper to run SimpleHTTPServer on Thread.
Ctrl +Only Thread remains dead when terminated with C.
Keyboard Interrupt passes.
"""
def run(self):
try:
self.serve_forever()
except KeyboardInterrupt:
pass
finally:
self.server_close()
@pytest.fixture(scope="session")
def http_server(logger):
class MyHTTPRequestHandler(SimpleHTTPRequestHandler):
def log_message(self, fmt, *args):
logger.log("http_server", fmt % args, color="blue")
host, port = "127.0.0.1", 8080
base_url = f"http://{host}:{port}"
# serve_Run forever under thread
server = HTTPServer((host, port), MyHTTPRequestHandler)
thread = threading.Thread(None, server.run)
thread.start()
yield base_url # Transition to test here
# End thread
server.shutdown()
thread.join()

View File

@@ -0,0 +1,378 @@
import time
import py
import pytest
ROOT = py.path.local(__file__).dirpath("..", "..", "..")
BUILD = ROOT.join("pyscriptjs", "build")
@pytest.mark.usefixtures("init")
class PyScriptTest:
"""
Base class to write PyScript integration tests, based on playwright.
It provides a simple API to generate HTML files and load them in
playwright.
It also provides a Pythonic API on top of playwright for the most
common tasks; in particular:
- self.console collects all the JS console.* messages. Look at the doc
of ConsoleMessageCollection for more details.
- self.check_errors() checks that no JS errors have been thrown
- after each test, self.check_errors() is automatically run to ensure
that no JS error passes uncaught.
- self.wait_for_console waits until the specified message appears in the
console
- self.wait_for_pyscript waits until all the PyScript tags have been
evaluated
- self.pyscript_run is the main entry point for pyscript tests: it
creates an HTML page to run the specified snippet.
"""
@pytest.fixture()
def init(self, request, tmpdir, http_server, logger, page):
"""
Fixture to automatically initialize all the tests in this class and its
subclasses.
The magic is done by the decorator @pyest.mark.usefixtures("init"),
which tells pytest to automatically use this fixture for all the test
method of this class.
Using the standard pytest behavior, we can request more fixtures:
tmpdir, http_server and page; 'page' is a fixture provided by
pytest-playwright.
Then, we save these fixtures on the self and proceed with more
initialization. The end result is that the requested fixtures are
automatically made available as self.xxx in all methods.
"""
self.testname = request.function.__name__.replace("test_", "")
self.tmpdir = tmpdir
# create a symlink to BUILD inside tmpdir
tmpdir.join("build").mksymlinkto(BUILD)
self.tmpdir.chdir()
self.http_server = http_server
self.logger = logger
self.init_page(page)
#
# this extra print is useful when using pytest -s, else we start printing
# in the middle of the line
print()
#
# if you use pytest --headed you can see the browser page while
# playwright executes the tests. However, the page is closed very
# quickly as soon as the test finishes. If you want to pause the test
# to have time to inspect it manually, uncomment the next two
# lines. The lines after the 'yield' will be executed during the
# teardown, leaving the page open until you exit pdb.
## yield
## import pdb;pdb.set_trace()
def init_page(self, page):
self.page = page
self.console = ConsoleMessageCollection(self.logger)
self._page_errors = []
page.on("console", self.console.add_message)
page.on("pageerror", self._on_pageerror)
def teardown_method(self):
# we call check_errors on teardown: this means that if there are still
# non-cleared errors, the test will fail. If you expect errors in your
# page and they should not cause the test to fail, you should call
# self.check_errors() in the test itself.
self.check_errors()
def _on_pageerror(self, error):
self.logger.log("JS exception", error.stack, color="red")
self._page_errors.append(error)
def check_errors(self):
"""
Check whether JS errors were reported.
If it finds a single JS error, raise JsError.
If it finds multiple JS errors, raise JsMultipleErrors.
Upon return, all the errors are cleared, so a subsequent call to
check_errors will not raise, unless NEW JS errors have been reported
in the meantime.
"""
exc = None
if len(self._page_errors) == 1:
# if there is a single error, wrap it
exc = JsError(self._page_errors[0])
elif len(self._page_errors) >= 2:
exc = JsMultipleErrors(self._page_errors)
self._page_errors = []
if exc:
raise exc
def clear_errors(self):
"""
Clear all JS errors.
"""
self._page_errors = []
def writefile(self, filename, content):
"""
Very thin helper to write a file in the tmpdir
"""
f = self.tmpdir.join(filename)
f.write(content)
def goto(self, path):
self.logger.reset()
self.logger.log("page.goto", path, color="yellow")
url = f"{self.http_server}/{path}"
self.page.goto(url)
def wait_for_console(self, text, *, timeout=None, check_errors=True):
"""
Wait until the given message appear in the console.
Note: it must be the *exact* string as printed by e.g. console.log.
If you need more control on the predicate (e.g. if you want to match a
substring), use self.page.expect_console_message directly.
timeout is expressed in milliseconds. If it's None, it will use
playwright's own default value, which is 30 seconds).
If check_errors is True (the default), it also checks that no JS
errors were raised during the waiting.
"""
pred = lambda msg: msg.text == text
try:
with self.page.expect_console_message(pred, timeout=timeout):
pass
finally:
# raise JsError if there were any javascript exception. Note that
# this might happen also in case of a TimeoutError. In that case,
# the JsError will shadow the TimeoutError but this is correct,
# because it's very likely that the console message never appeared
# precisely because of the exception in JS.
if check_errors:
self.check_errors()
def wait_for_pyscript(self, *, timeout=None, check_errors=True):
"""
Wait until pyscript has been fully loaded.
Timeout is expressed in milliseconds. If it's None, it will use
playwright's own default value, which is 30 seconds).
If check_errors is True (the default), it also checks that no JS
errors were raised during the waiting.
"""
# this is printed by pyconfig.ts:PyodideRuntime.initialize
self.wait_for_console(
"===PyScript page fully initialized===",
timeout=timeout,
check_errors=check_errors,
)
def pyscript_run(self, snippet):
"""
Main entry point for pyscript tests.
snippet contains a fragment of HTML which will be put inside a full
HTML document. In particular, the <head> automatically contains the
correct <script> and <link> tags which are necessary to load pyscript
correctly.
This method does the following:
- write a full HTML file containing the snippet
- open a playwright page for it
- wait until pyscript has been fully loaded
"""
doc = f"""
<html>
<head>
<link rel="stylesheet" href="{self.http_server}/build/pyscript.css" />
<script defer src="{self.http_server}/build/pyscript.js"></script>
</head>
<body>
{snippet}
</body>
</html>
"""
filename = f"{self.testname}.html"
self.writefile(filename, doc)
self.goto(filename)
self.wait_for_pyscript()
# ============== Helpers and utility functions ==============
class JsError(Exception):
"""
Represent an exception which happened in JS.
It's a thin wrapper around playwright.sync_api.Error, with two important
differences:
1. it has a better name: if you see JsError in a traceback, it's
immediately obvious that it's a JS exception.
2. Show also the JS stacktrace by default, contrarily to
playwright.sync_api.Error
"""
def __init__(self, error):
super().__init__(self.format_playwright_error(error))
self.error = error
@staticmethod
def format_playwright_error(error):
# apparently, playwright Error.stack contains all the info that we
# want: exception name, message and stacktrace. The docs say that
# error.stack is optional, so fallback to the standard repr if it's
# unavailable.
return error.stack or str(error)
class JsMultipleErrors(Exception):
"""
This is raised in case we get multiple JS errors in the page
"""
def __init__(self, errors):
lines = ["Multiple JS errors found:"]
for err in errors:
lines.append(JsError.format_playwright_error(err))
msg = "\n".join(lines)
super().__init__(msg)
self.errors = errors
class ConsoleMessageCollection:
"""
Helper class to collect and expose ConsoleMessage in a Pythonic way.
Usage:
console.log.messages: list of ConsoleMessage with type=='log'
console.log.lines: list of strings
console.log.text: the whole text as single string
console.debug.* same as above, but with different types
console.info.*
console.error.*
console.warning.*
console.all.* same as above, but considering all messages, no filters
"""
class View:
"""
Filter console messages by the given msg_type
"""
def __init__(self, console, msg_type):
self.console = console
self.msg_type = msg_type
@property
def messages(self):
if self.msg_type is None:
return self.console._messages
else:
return [
msg for msg in self.console._messages if msg.type == self.msg_type
]
@property
def lines(self):
return [msg.text for msg in self.messages]
@property
def text(self):
return "\n".join(self.lines)
_COLORS = {
"error": "red",
"warning": "brown",
}
def __init__(self, logger):
self.logger = logger
self._messages = []
self.all = self.View(self, None)
self.log = self.View(self, "log")
self.debug = self.View(self, "debug")
self.info = self.View(self, "info")
self.error = self.View(self, "error")
self.warning = self.View(self, "warning")
def add_message(self, msg):
# log the message: pytest will capute the output and display the
# messages if the test fails.
category = f"console.{msg.type}"
color = self._COLORS.get(msg.type)
self.logger.log(category, msg.text, color=color)
self._messages.append(msg)
class Logger:
"""
Helper class to log messages to stdout.
Features:
- nice formatted category
- keep track of time passed since the last reset
- support colors
NOTE: the (lowercase) logger fixture is defined in conftest.py
"""
def __init__(self):
self.reset()
def reset(self):
self.start_time = time.time()
def log(self, category, text, *, color=None):
delta = time.time() - self.start_time
line = f"[{delta:6.2f} {category:15}] {text}"
if color:
line = Color.set(color, line)
print(line)
class Color:
"""
Helper method to print colored output using ANSI escape codes.
"""
black = "30"
darkred = "31"
darkgreen = "32"
brown = "33"
darkblue = "34"
purple = "35"
teal = "36"
lightgray = "37"
darkgray = "30;01"
red = "31;01"
green = "32;01"
yellow = "33;01"
blue = "34;01"
fuchsia = "35;01"
turquoise = "36;01"
white = "37;01"
@classmethod
def set(cls, color, string):
try:
color = getattr(cls, color)
except AttributeError:
pass
return f"\x1b[{color}m{string}\x1b[00m"

View File

@@ -0,0 +1,222 @@
import textwrap
import pytest
from playwright import sync_api
from .support import JsError, JsMultipleErrors, PyScriptTest
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_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_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")
with pytest.raises(JsError) as exc:
self.check_errors()
# check that the exception message contains the error message and the
# stack trace
msg = str(exc.value)
assert "Error: this is an error" in msg
assert f"at {self.http_server}/mytest.html" in msg
#
# after a call to check_errors, the errors are cleared
self.check_errors()
def test_check_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(JsMultipleErrors) as exc:
self.check_errors()
assert "error 1" in str(exc.value)
assert "error 2" in str(exc.value)
#
# check that errors are cleared
self.check_errors()
def test_clear_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_errors()
# self.check_errors does not raise, because the errors have been
# cleared
self.check_errors()
def test_wait_for_console(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 500ms 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_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(JsError) as exc:
self.wait_for_console("Page loaded!", timeout=200)
assert "this is an error" in str(exc.value)
assert isinstance(exc.value.__context__, sync_api.TimeoutError)
#
# if we use check_errors=False, the error are ignored, but we get the
# Timeout anyway
self.goto("mytest.html")
with pytest.raises(sync_api.TimeoutError):
self.wait_for_console("Page loaded!", timeout=200, check_errors=False)
# we still got a JsError, so we need to manually clear it, else the
# test fails at teardown
self.clear_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(JsError) as exc:
self.wait_for_console("Page loaded!", timeout=200)
assert "this is an error" in str(exc.value)
#
# with check_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_errors=False)
# clear the errors, else the test fails at teardown
self.clear_errors()

View File

@@ -0,0 +1,53 @@
import re
from .support import PyScriptTest
class TestBasic(PyScriptTest):
def test_pyscript_hello(self):
self.pyscript_run(
"""
<py-script>
print('hello pyscript')
</py-script>
"""
)
# this is a very ugly way of checking the content of the DOM. If we
# find ourselves to write a lot of code in this style, we will
# probably want to write a nicer API for it.
inner_html = self.page.locator("py-script").inner_html()
pattern = r'<div id="py-.*">hello pyscript</div>'
assert re.search(pattern, inner_html)
def test_execution_in_order(self):
"""
Check that they py-script tags are executed in the same order they are
defined
"""
# NOTE: this test relies on the fact that pyscript does not write
# anything to console.info. If we start writing to info in the future,
# we will probably need to tweak this test.
self.pyscript_run(
"""
<py-script>import js; js.console.info('one')</py-script>
<py-script>js.console.info('two')</py-script>
<py-script>js.console.info('three')</py-script>
<py-script>js.console.info('four')</py-script>
"""
)
assert self.console.info.lines == ["one", "two", "three", "four"]
def test_escaping_of_angle_brackets(self):
"""
Check that py-script tags escape angle brackets
"""
# NOTE: this test relies on the fact that pyscript does not write
# anything to console.info. If we start writing to info in the future,
# we will probably need to tweak this test.
self.pyscript_run(
"""
<py-script>import js; js.console.info(1<2, 1>2)</py-script>
<py-script>js.console.info("<div></div>")</py-script>
"""
)
assert self.console.info.lines == ["true false", "<div></div>"]

View File

@@ -0,0 +1,34 @@
import pytest
from .support import PyScriptTest
class TestPyButton(PyScriptTest):
@pytest.mark.xfail
def test_on_click(self):
"""
currently this test fails for a bad reason which is unrelated to
py-button. During the page loading, the following JS exception occur,
in base.ts:BaseEvalElement.evaluate
[JS exception ] TypeError: Cannot use 'in' operator to search for 'runPythonAsync' in undefined
at http://127.0.0.1:8080/build/pyscript.js:305:38
at Object.subscribe (http://127.0.0.1:8080/build/pyscript.js:46:13)
at PyButton.runAfterRuntimeInitialized (http://127.0.0.1:8080/build/pyscript.js:304:27)
at PyButton.connectedCallback (http://127.0.0.1:8080/build/pyscript.js:26856:18)
at http://127.0.0.1:8080/build/pyscript.js:27075:20
at http://127.0.0.1:8080/build/pyscript.js:27093:3
""" # noqa: E501
self.pyscript_run(
"""
<py-button label="my button">
import js
def on_click(evt):
js.console.info('clicked!')
</py-button>
"""
)
assert self.console.info.lines == []
self.page.locator("text=my button").click()
self.page.locator("text=my button").click()
assert self.console.info.lines == ["clicked!", "clicked!"]

View File

@@ -0,0 +1,203 @@
import math
import re
import time
import pytest
from .support import ROOT, PyScriptTest
MAX_TEST_TIME = 30 # Number of seconds allowed for checking a testing condition
TEST_TIME_INCREMENT = 0.25 # 1/4 second, the length of each iteration
TEST_ITERATIONS = math.ceil(
MAX_TEST_TIME / TEST_TIME_INCREMENT
) # 120 iters of 1/4 second
def wait_for_render(page, selector, pattern):
"""
Assert that rendering inserts data into the page as expected: search the
DOM from within the timing loop for a string that is not present in the
initial markup but should appear by way of rendering
"""
re_sub_content = re.compile(pattern)
py_rendered = False # Flag to be set to True when condition met
for _ in range(TEST_ITERATIONS):
content = page.inner_html(selector)
if re_sub_content.search(content):
py_rendered = True
break
time.sleep(TEST_TIME_INCREMENT)
assert py_rendered # nosec
@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)
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:
assert False, "Espresso time not found :("
def test_altair(self):
# XXX improve this test
self.goto("examples/altair.html")
self.wait_for_pyscript()
assert self.page.title() == "Altair"
wait_for_render(self.page, "*", '<canvas.*?class=\\"marks\\".*?>')
def test_bokeh(self):
# XXX improve this test
self.goto("examples/bokeh.html")
self.wait_for_pyscript()
assert self.page.title() == "Bokeh Example"
wait_for_render(self.page, "*", '<div.*class=\\"bk\\".*>')
def test_bokeh_interactive(self):
# XXX improve this test
self.goto("examples/bokeh_interactive.html")
self.wait_for_pyscript()
assert self.page.title() == "Bokeh Example"
wait_for_render(self.page, "*", '<div.*?class=\\"bk\\".*?>')
def test_d3(self):
# XXX improve this test
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.*?>")
def test_folium(self):
# XXX improve this test
self.goto("examples/folium.html")
self.wait_for_pyscript()
assert self.page.title() == "Folium"
wait_for_render(self.page, "*", "<iframe srcdoc=")
def test_matplotlib(self):
# XXX improve this test
self.goto("examples/matplotlib.html")
self.wait_for_pyscript()
assert self.page.title() == "Matplotlib"
wait_for_render(self.page, "*", "<img src=['\"]data:image")
def test_numpy_canvas_fractals(self):
# XXX improve this test
self.goto("examples/numpy_canvas_fractals.html")
self.wait_for_pyscript()
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)['\"].*?>"
)
def test_panel(self):
# XXX improve this test
self.goto("examples/panel.html")
self.wait_for_pyscript()
assert self.page.title() == "Panel Example"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
def test_panel_deckgl(self):
# XXX improve this test
self.goto("examples/panel_deckgl.html")
self.wait_for_pyscript()
assert self.page.title() == "PyScript/Panel DeckGL Demo"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
def test_panel_kmeans(self):
# XXX improve this test
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['\"].*?>")
@pytest.mark.xfail(reason="JsError: issue #677")
def test_panel_stream(self):
# XXX improve this test
self.goto("examples/panel_stream.html")
self.wait_for_pyscript()
assert self.page.title() == "PyScript/Panel Streaming Demo"
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
def test_repl(self):
# XXX improve this test
self.goto("examples/repl.html")
self.wait_for_pyscript()
assert self.page.title() == "REPL"
wait_for_render(self.page, "*", "<py-repl.*?>")
def test_repl2(self):
# XXX improve this test
self.goto("examples/repl2.html")
self.wait_for_pyscript()
assert self.page.title() == "Custom REPL Example"
wait_for_render(self.page, "*", "<py-repl.*?>")
def test_todo(self):
# XXX improve this test
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['\"].*?>")
@pytest.mark.xfail(reason="JsError, issue #673")
def test_todo_pylist(self):
# XXX improve this test
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['\"].*?>")
def test_toga_freedom(self):
# XXX improve this test
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+['\"].*?>")
@pytest.mark.xfail(reason="it never finishes loading, issue #678")
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.*?>")