mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-21 19:25:35 -05:00
Make sure that tests fail in case there is an unhandled Python error (#1456)
Before this PR, the following test passed:
def test_pyscript_hello(self):
self.pyscript_run(
"""
<script type="py">
raise Exception("hello")
</script>
""")
What happens is that we intercept the Python exception and display a nice banner on the DOM, but the test itself passes. This is error prone: if we have Python exceptions on the page, the test should fail by default, and we should have a way to silence it in case those exceptions are expected.
This PR treats Python errors as we treat JS errors: unhandled exceptions cause the test to fail, but you can silence them by calling self.check_py_errors(), exactly as you can call self.check_js_errors().
This commit is contained in:
@@ -208,6 +208,7 @@ class PyScriptTest:
|
||||
|
||||
self.console = ConsoleMessageCollection(self.logger)
|
||||
self._js_errors = []
|
||||
self._py_errors = []
|
||||
page.on("console", self._on_console)
|
||||
page.on("pageerror", self._on_pageerror)
|
||||
|
||||
@@ -243,17 +244,26 @@ class PyScriptTest:
|
||||
# page and they should not cause the test to fail, you should call
|
||||
# self.check_js_errors() in the test itself.
|
||||
self.check_js_errors()
|
||||
self.check_py_errors()
|
||||
|
||||
def _on_console(self, msg):
|
||||
if msg.type == "error" and "Traceback (most recent call last)" in msg.text:
|
||||
# this is a Python traceback, let's record it as a py_error
|
||||
self._py_errors.append(msg.text)
|
||||
self.console.add_message(msg.type, msg.text)
|
||||
|
||||
def _on_pageerror(self, error):
|
||||
self.console.add_message("js_error", error.stack)
|
||||
self._js_errors.append(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.
|
||||
error_msg = error.stack or str(error)
|
||||
self.console.add_message("js_error", error_msg)
|
||||
self._js_errors.append(error_msg)
|
||||
|
||||
def check_js_errors(self, *expected_messages):
|
||||
def _check_page_errors(self, kind, expected_messages):
|
||||
"""
|
||||
Check whether JS errors were reported.
|
||||
Check whether the page raised any 'JS' or 'Python' error.
|
||||
|
||||
expected_messages is a list of strings of errors that you expect they
|
||||
were raised in the page. They are checked using a simple 'in' check,
|
||||
@@ -261,40 +271,62 @@ class PyScriptTest:
|
||||
if expected_message in actual_error_message:
|
||||
...
|
||||
|
||||
If an error was expected but not found, it raises
|
||||
DidNotRaiseJsError().
|
||||
If an error was expected but not found, it raises PageErrorsDidNotRaise.
|
||||
|
||||
If there are MORE errors other than the expected ones, it raises JsErrors.
|
||||
If there are MORE errors other than the expected ones, it raises PageErrors.
|
||||
|
||||
Upon return, all the errors are cleared, so a subsequent call to
|
||||
check_js_errors will not raise, unless NEW JS errors have been reported
|
||||
check_{js,py}_errors will not raise, unless NEW errors have been reported
|
||||
in the meantime.
|
||||
"""
|
||||
assert kind in ("JS", "Python")
|
||||
if kind == "JS":
|
||||
actual_errors = self._js_errors[:]
|
||||
else:
|
||||
actual_errors = self._py_errors[:]
|
||||
expected_messages = list(expected_messages)
|
||||
js_errors = self._js_errors[:]
|
||||
|
||||
for i, msg in enumerate(expected_messages):
|
||||
for j, error in enumerate(js_errors):
|
||||
if msg is not None and error is not None and msg in error.message:
|
||||
for j, error in enumerate(actual_errors):
|
||||
if msg is not None and error is not None and msg in error:
|
||||
# we matched one expected message with an error, remove both
|
||||
expected_messages[i] = None
|
||||
js_errors[j] = None
|
||||
actual_errors[j] = None
|
||||
|
||||
# if everything is find, now expected_messages and js_errors contains
|
||||
# if everything is find, now expected_messages and actual_errors contains
|
||||
# only Nones. If they contain non-None elements, it means that we
|
||||
# either have messages which are expected-but-not-found or errors
|
||||
# which are found-but-not-expected.
|
||||
expected_messages = [msg for msg in expected_messages if msg is not None]
|
||||
js_errors = [err for err in js_errors if err is not None]
|
||||
self.clear_js_errors()
|
||||
# either have messages which are expected-but-not-found or
|
||||
# found-but-not-expected.
|
||||
not_found = [msg for msg in expected_messages if msg is not None]
|
||||
unexpected = [err for err in actual_errors if err is not None]
|
||||
|
||||
if expected_messages:
|
||||
if kind == "JS":
|
||||
self.clear_js_errors()
|
||||
else:
|
||||
self.clear_py_errors()
|
||||
|
||||
if not_found:
|
||||
# expected-but-not-found
|
||||
raise JsErrorsDidNotRaise(expected_messages, js_errors)
|
||||
|
||||
if js_errors:
|
||||
raise PageErrorsDidNotRaise(kind, not_found, unexpected)
|
||||
if unexpected:
|
||||
# found-but-not-expected
|
||||
raise JsErrors(js_errors)
|
||||
raise PageErrors(kind, unexpected)
|
||||
|
||||
def check_js_errors(self, *expected_messages):
|
||||
"""
|
||||
Check whether JS errors were reported.
|
||||
|
||||
See the docstring for _check_page_errors for more details.
|
||||
"""
|
||||
self._check_page_errors("JS", expected_messages)
|
||||
|
||||
def check_py_errors(self, *expected_messages):
|
||||
"""
|
||||
Check whether Python errors were reported.
|
||||
|
||||
See the docstring for _check_page_errors for more details.
|
||||
"""
|
||||
self._check_page_errors("Python", expected_messages)
|
||||
|
||||
def clear_js_errors(self):
|
||||
"""
|
||||
@@ -302,6 +334,9 @@ class PyScriptTest:
|
||||
"""
|
||||
self._js_errors = []
|
||||
|
||||
def clear_py_errors(self):
|
||||
self._py_errors = []
|
||||
|
||||
def writefile(self, filename, content):
|
||||
"""
|
||||
Very thin helper to write a file in the tmpdir
|
||||
@@ -614,54 +649,37 @@ def wait_for_render(page, selector, pattern):
|
||||
assert py_rendered # nosec
|
||||
|
||||
|
||||
class JsErrors(Exception):
|
||||
class PageErrors(Exception):
|
||||
"""
|
||||
Represent one or more exceptions 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
|
||||
Represent one or more exceptions which happened in JS or Python.
|
||||
"""
|
||||
|
||||
def __init__(self, errors):
|
||||
def __init__(self, kind, errors):
|
||||
assert kind in ("JS", "Python")
|
||||
n = len(errors)
|
||||
assert n != 0
|
||||
lines = [f"JS errors found: {n}"]
|
||||
for err in errors:
|
||||
lines.append(self.format_playwright_error(err))
|
||||
lines = [f"{kind} errors found: {n}"]
|
||||
lines += errors
|
||||
msg = "\n".join(lines)
|
||||
super().__init__(msg)
|
||||
self.errors = errors
|
||||
|
||||
@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 JsErrorsDidNotRaise(Exception):
|
||||
class PageErrorsDidNotRaise(Exception):
|
||||
"""
|
||||
Exception raised by check_js_errors when the expected JS error messages
|
||||
are not found.
|
||||
Exception raised by check_{js,py}_errors when the expected JS or Python
|
||||
error messages are not found.
|
||||
"""
|
||||
|
||||
def __init__(self, expected_messages, errors):
|
||||
lines = ["The following JS errors were expected but could not be found:"]
|
||||
def __init__(self, kind, expected_messages, errors):
|
||||
assert kind in ("JS", "Python")
|
||||
lines = [f"The following {kind} errors were expected but could not be found:"]
|
||||
for msg in expected_messages:
|
||||
lines.append(" - " + msg)
|
||||
if errors:
|
||||
lines.append("---")
|
||||
lines.append("The following JS errors were raised but not expected:")
|
||||
for err in errors:
|
||||
lines.append(JsErrors.format_playwright_error(err))
|
||||
lines.append(f"The following {kind} errors were raised but not expected:")
|
||||
lines += errors
|
||||
msg = "\n".join(lines)
|
||||
super().__init__(msg)
|
||||
self.expected_messages = expected_messages
|
||||
|
||||
Reference in New Issue
Block a user