From 516bf17a48000f3d281a071b56192ef99bcd8e6e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 8 Oct 2026 16:54:30 -0300 Subject: [PATCH] [ASIM-6921] Keep the details of errors raised by evaluated JS code Errors raised by the code Python evaluates in the graph page, like calling a JS function that doesn't exist or a syntax error, reached the error bridge as a bare "Script error." with no message, location or stack: Chromium hides the details of uncaught errors from scripts injected by `runJavaScript`. Errors raised inside the page's own code were not affected, and all QtWebEngine 5.15 builds in use behave the same. Have a function defined by the page evaluate the scripts instead, so Chromium treats them as the page's own code and the error handler gets the full details, and drop the xfail from the test that caught it. Co-Authored-By: Claude Opus 5.5 --- src/qmxgraph/_web_view.py | 14 ++++++++-- src/qmxgraph/page/error_redirect.js | 16 ++++++++++++ tests/test_qt_js_integration.py | 40 ++++++++++++++++++++++------- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/qmxgraph/_web_view.py b/src/qmxgraph/_web_view.py index 6aa779b..25b353a 100644 --- a/src/qmxgraph/_web_view.py +++ b/src/qmxgraph/_web_view.py @@ -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 @@ -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 return callback.args[0] @@ -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.""" @@ -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)})" diff --git a/src/qmxgraph/page/error_redirect.js b/src/qmxgraph/page/error_redirect.js index 2344dd9..c751023 100644 --- a/src/qmxgraph/page/error_redirect.js +++ b/src/qmxgraph/page/error_redirect.js @@ -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); +}; diff --git a/tests/test_qt_js_integration.py b/tests/test_qt_js_integration.py index 91a02f6..986c685 100644 --- a/tests/test_qt_js_integration.py +++ b/tests/test_qt_js_integration.py @@ -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 @@ -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 :1:7""" + """ ) assert (url, line, column) == ("qrc:/", 1, 1) - assert msg == expected + assert msg.startswith(expected_start) + assert ":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: