Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/qmxgraph/_web_view.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Dynamic dependency that may mess freezing tools if not included
import json
from enum import auto
from enum import Enum
from typing import Any
Expand Down Expand Up @@ -132,7 +133,8 @@ def eval_js(self, script, *, timeout_ms: int = 10_000) -> Any:
"""
self._check_valid_eval_state()
with wait_callback_called(timeout_ms=timeout_ms) as callback:
self.page().runJavaScript(script, callback)
self.page().runJavaScript(_in_page_context(script), callback)
# Leaving the block guarantees the callback was called, as it raises on timeout.
assert callback.args is not None
Comment thread
nicoddemus marked this conversation as resolved.
return callback.args[0]

Expand All @@ -150,7 +152,7 @@ def eval_js_async(self, script: str) -> None:
:param script: A JavaScript statement.
"""
self._check_valid_eval_state()
self.page().runJavaScript(script)
self.page().runJavaScript(_in_page_context(script))

def _check_valid_eval_state(self) -> None:
"""Check the view is in a valid state to evaluate JS commands."""
Expand All @@ -167,3 +169,11 @@ def dragMoveEvent(self, event):

def dropEvent(self, event):
self.on_drop_event.emit(event)


def _in_page_context(script: str) -> str:
"""
Wrap a script so the page evaluates it as its own code (see `qmxgraphEval`), otherwise
its uncaught errors reach the error bridge without any details.
"""
return f"qmxgraphEval({json.dumps(script)})"
16 changes: 16 additions & 0 deletions src/qmxgraph/page/error_redirect.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,19 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {

bridge_error_handler.error_slot(msg, url, lineNo, columnNo);
};

/**
* Evaluates a script on behalf of Python, returning the value of its last statement.
*
* Chromium reports uncaught errors from scripts injected by `runJavaScript` as a bare
* "Script error." with no details. Evaluated from here, the code counts as the page's own,
* so `onerror` above receives its message and stack.
*
* @param {string} source The script to evaluate, in global scope.
* @returns {*} The value of the script's last statement.
*/
window.qmxgraphEval = function (source) {
"use strict";
/* jshint evil: true */
return (0, eval)(source);
};
40 changes: 31 additions & 9 deletions tests/test_qt_js_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,12 @@
from qmxgraph.widget import QmxGraph


# TODO(ASIM-6921): Remove once JS errors are no longer muted.
@pytest.mark.xfail(
reason="QtWebEngine 5.15.15 reports JS errors as a bare 'Script error.'", strict=True
)
def test_error_redirection(loaded_graph) -> None:
def test_error_redirection(loaded_graph: QmxGraph) -> None:
"""
It is possible to redirect errors in JS code to Python/Qt side.

:type loaded_graph: qmxgraph.widget.qmxgraph
The stack is only checked up to the error's position in the evaluated code, as the frames
after it belong to the evaluation machinery.
"""
error_redirection = loaded_graph.error_bridge

Expand All @@ -46,15 +43,40 @@ def test_error_redirection(loaded_graph) -> None:

assert cb.args is not None
msg, url, line, column = cb.args
expected = textwrap.dedent(
expected_start = textwrap.dedent(
"""\
Uncaught Error: test
stack:
Error: test
at <anonymous>:1:7"""
"""
)
assert (url, line, column) == ("qrc:/", 1, 1)
assert msg == expected
assert msg.startswith(expected_start)
assert "<anonymous>:1:7" in msg


@pytest.mark.parametrize(
"statement, expected_error",
[
("api.noSuchFunction(1)", "TypeError: api.noSuchFunction is not a function"),
("api.updateTable(", "SyntaxError: Unexpected end of input"),
],
)
def test_error_redirection_from_evaluated_code(
loaded_graph: QmxGraph, statement: str, expected_error: str
) -> None:
"""
Regression test for ASIM-6921: errors raised by the evaluated code itself, like calling a
function that doesn't exist or a syntax error, reached Python as a bare "Script error.",
as Chromium hides the details of errors from scripts injected by `runJavaScript`.
"""
with wait_signals_called(loaded_graph.error_bridge.on_error) as cb:
eval_js(loaded_graph, statement)

assert cb.args is not None
msg, url, line, column = cb.args
assert msg.startswith(f"Uncaught {expected_error}\nstack:\n")
assert (url, line) == ("qrc:/", 1)


def test_events_bridge_delayed_signals(graph, qtbot, mocker) -> None:
Expand Down
Loading