diff options
author | Florian Bruhin <me@the-compiler.org> | 2021-03-24 14:12:36 +0100 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2021-03-24 14:12:36 +0100 |
commit | 2f592f7ce6414e3c071eaa6df86bf34b6a213bdf (patch) | |
tree | e65f177e655544d756c52b880781d27b2016bfa5 | |
parent | fdc21e10dd2b8d11d3f2798b8a3a490ec67c9377 (diff) | |
parent | 41bcada133b7235db698a2354df27585438b6a4b (diff) | |
download | qutebrowser-2f592f7ce6414e3c071eaa6df86bf34b6a213bdf.tar.gz qutebrowser-2f592f7ce6414e3c071eaa6df86bf34b6a213bdf.zip |
Merge remote-tracking branch 'origin/pr/5457' into dev
-rw-r--r-- | .github/workflows/ci.yml | 10 | ||||
-rw-r--r-- | pytest.ini | 1 | ||||
-rw-r--r-- | qutebrowser/browser/webengine/notification.py | 248 | ||||
-rw-r--r-- | qutebrowser/browser/webengine/webenginetab.py | 6 | ||||
-rw-r--r-- | qutebrowser/config/configdata.yml | 17 | ||||
-rw-r--r-- | qutebrowser/qutebrowser.py | 4 | ||||
-rw-r--r-- | tests/end2end/conftest.py | 19 | ||||
-rw-r--r-- | tests/end2end/data/prompt/notifications.html | 17 | ||||
-rw-r--r-- | tests/end2end/features/notifications.feature | 50 | ||||
-rw-r--r-- | tests/end2end/features/test_notifications_bdd.py | 51 | ||||
-rw-r--r-- | tests/end2end/fixtures/notificationserver.py | 96 | ||||
-rw-r--r-- | tests/end2end/fixtures/quteprocess.py | 7 | ||||
-rw-r--r-- | tox.ini | 2 |
13 files changed, 514 insertions, 14 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccb29d100..3476f2abc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: python -m pip install -U pip python -m pip install -U -r misc/requirements/requirements-tox.txt - name: "Run ${{ matrix.testenv }}" - run: "tox -e ${{ matrix.testenv}} -- ${{ matrix.args }}" + run: "dbus-run-session -- tox -e ${{ matrix.testenv}} -- ${{ matrix.args }}" tests-docker: if: "!contains(github.event.head_commit.message, '[ci skip]')" @@ -90,7 +90,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up problem matchers run: "python scripts/dev/ci/problemmatchers.py py38 ${{ runner.temp }}" - - run: tox -e py + - run: dbus-run-session tox -e py tests: if: "!contains(github.event.head_commit.message, '[ci skip]')" @@ -164,7 +164,11 @@ jobs: python -m pip install -U pip python -m pip install -U -r misc/requirements/requirements-tox.txt - name: "Run ${{ matrix.testenv }}" - run: "tox -e ${{ matrix.testenv}} -- ${{ matrix.args }}" + run: "dbus-run-session -- tox -e ${{ matrix.testenv }} -- ${{ matrix.args }}" + if: "startsWith(matrix.os, 'ubuntu-')" + - name: "Run ${{ matrix.testenv }}" + run: "tox -e ${{ matrix.testenv }} -- ${{ matrix.args }}" + if: "!startsWith(matrix.os, 'ubuntu-')" - name: Analyze backtraces run: "bash scripts/dev/ci/backtrace.sh ${{ matrix.testenv }}" if: "failure()" diff --git a/pytest.ini b/pytest.ini index 7f4a58de3..4d8ae4bfe 100644 --- a/pytest.ini +++ b/pytest.ini @@ -30,6 +30,7 @@ markers = qtwebkit_skip: Tests not applicable with QtWebKit qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine + qtwebengine_py_5_15: Tests which require PyQtWebEngine 5.15. this: Used to mark tests during development no_invalid_lines: Don't fail on unparsable lines in end2end tests fake_os: Fake utils.is_* to a fake operating system diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py new file mode 100644 index 000000000..d3dc0af08 --- /dev/null +++ b/qutebrowser/browser/webengine/notification.py @@ -0,0 +1,248 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +"""Handles sending notifications over DBus.""" + +import typing + + +from PyQt5.QtGui import QImage +from PyQt5.QtCore import (QObject, QVariant, QMetaType, QByteArray, pyqtSlot, + PYQT_VERSION) +from PyQt5.QtDBus import (QDBusConnection, QDBusInterface, QDBus, + QDBusArgument, QDBusMessage) + +if typing.TYPE_CHECKING: + # putting these behind TYPE_CHECKING also means this module is importable + # on installs that don't have these + from PyQt5.QtWebEngineCore import QWebEngineNotification + from PyQt5.QtWebEngineWidgets import QWebEngineProfile + +from qutebrowser.browser.webengine import webenginesettings +from qutebrowser.config import config +from qutebrowser.misc import objects +from qutebrowser.utils import qtutils, log, utils + + +def init() -> None: + """Initialize the DBus notification presenter, if applicable. + + If the user doesn't want a notification presenter or it's not supported, + this method does nothing. + + Always succeeds, but might log an error. + """ + should_use_dbus = ( + qtutils.version_check("5.13") and + config.val.content.notification_presenter == "libnotify" and + # Don't even try to use DBus notifications on platforms that won't have + # it supported. + not utils.is_windows and + not utils.is_mac + ) + if not should_use_dbus: + return + + log.init.debug("Setting up DBus notification presenter...") + try: + testing = 'test-notification-service' in objects.debug_flags + presenter = DBusNotificationPresenter(testing) + for p in [webenginesettings.default_profile, + webenginesettings.private_profile]: + if not p: + continue + presenter.install(p) + except DBusException as e: + log.init.error( + "Failed to initialize DBus notification presenter: {}" + .format(e) + ) + + +class DBusException(Exception): + """Raised when something goes wrong with talking to DBus.""" + + +class DBusNotificationPresenter(QObject): + """Manages notifications that are sent over DBus.""" + + SERVICE = "org.freedesktop.Notifications" + # If you change the test service, make sure to also change the one in the + # test fixture. + TEST_SERVICE = "org.qutebrowser.TestNotifications" + PATH = "/org/freedesktop/Notifications" + INTERFACE = "org.freedesktop.Notifications" + + def __init__(self, test_service: bool = False): + super().__init__() + self._active_notifications = {} \ + # type: typing.Dict[int, QWebEngineNotification] + bus = QDBusConnection.sessionBus() + if not bus.isConnected(): + raise DBusException("Failed to connect to DBus session bus") + + service = self.TEST_SERVICE if test_service else self.SERVICE + + self.interface = QDBusInterface( + service, + self.PATH, + self.INTERFACE, + bus, + ) + + bus.connect( + service, + self.PATH, + self.INTERFACE, + "NotificationClosed", + self._handle_close + ) + + if not self.interface: + raise DBusException("Could not construct a DBus interface") + + # None means we don't know yet. + self._needs_escaping = None # type: typing.Optional[bool] + + def install(self, profile: "QWebEngineProfile") -> None: + """Sets the profile to use the manager as the presenter.""" + # WORKAROUND for + # https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042916.html + if PYQT_VERSION < 0x050F00: # PyQt 5.15 + # PyQtWebEngine unrefs the callback after it's called, for some + # reason. So we call setNotificationPresenter again to *increase* + # its refcount to prevent it from getting GC'd. Otherwise, random + # methods start getting called with the notification as `self`, or + # segfaults happen, or other badness. + def _present_and_reset(qt_notification: "QWebEngineNotification") \ + -> None: + profile.setNotificationPresenter(_present_and_reset) + self._present(qt_notification) + profile.setNotificationPresenter(_present_and_reset) + else: + profile.setNotificationPresenter(self._present) + + def _present(self, qt_notification: "QWebEngineNotification") -> None: + """Shows a notification over DBus. + + This should *not* be directly passed to setNotificationPresenter on + PyQtWebEngine < 5.15 because of a bug in the PyQtWebEngine bindings. + """ + # Deferring this check to the first presentation means we can tweak + # whether the test notification server supports body markup. + if self._needs_escaping is None: + self._needs_escaping = self._check_needs_escaping() + + # notification id 0 means 'assign us the ID'. We can't just pass 0 + # because it won't get sent as the right type. + zero = QVariant(0) + zero.convert(QVariant.UInt) + + actions_list = QDBusArgument([], QMetaType.QStringList) + + qt_notification.show() + hints = { + # Include the origin in case the user wants to do different things + # with different origin's notifications. + "x-qutebrowser-origin": qt_notification.origin().toDisplayString() + } # type: typing.Dict[str, typing.Any] + if not qt_notification.icon().isNull(): + hints["image-data"] = self._convert_image(qt_notification.icon()) + + reply = self.interface.call( + QDBus.BlockWithGui, + "Notify", + "qutebrowser", # application name + zero, # notification id + "qutebrowser", # icon + # Titles don't support markup, so no need to escape them. + qt_notification.title(), + self._format_body(qt_notification.message()), + actions_list, + hints, + -1, # timeout; -1 means 'use default' + ) + + if reply.signature() != "u": + raise DBusException( + "Got an unexpected reply {}; expected a single uint32" + .format(reply.arguments()) + ) + + notification_id = reply.arguments()[0] + self._active_notifications[notification_id] = qt_notification + log.webview.debug("Sent out notification {}".format(notification_id)) + + def _convert_image(self, qimage: QImage) -> QDBusArgument: + """Converts a QImage to the structure DBus expects.""" + # This is apparently what GTK-based notification daemons expect; tested + # it with dunst. Otherwise you get weird color schemes. + qimage.convertTo(QImage.Format_RGBA8888) + image_data = QDBusArgument() + image_data.beginStructure() + image_data.add(qimage.width()) + image_data.add(qimage.height()) + image_data.add(qimage.bytesPerLine()) + image_data.add(qimage.hasAlphaChannel()) + # RGBA_8888 always has 8 bits per color, 4 channels. + image_data.add(8) + image_data.add(4) + try: + size = qimage.sizeInBytes() + except TypeError: + # WORKAROUND for + # https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042919.html + # byteCount() is obsolete, but sizeInBytes() is only available with + # SIP >= 5.3.0. + size = qimage.byteCount() + bits = qimage.constBits().asstring(size) + image_data.add(QByteArray(bits)) + image_data.endStructure() + return image_data + + @pyqtSlot(QDBusMessage) + def _handle_close(self, message: QDBusMessage) -> None: + notification_id = message.arguments()[0] + if notification_id in self._active_notifications: + try: + self._active_notifications[notification_id].close() + except RuntimeError: + # WORKAROUND for + # https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html + pass + + def _check_needs_escaping(self) -> bool: + """Checks whether we need to escape body messages we send.""" + reply = self.interface.call( + QDBus.BlockWithGui, + "GetCapabilities", + ) + if reply.signature() != "as": + raise DBusException( + "Got an unexpected reply {} when checking capabilities" + .format(reply.arguments()) + ) + return "body-markup" in reply.arguments()[0] + + def _format_body(self, message: str) -> str: + if self._needs_escaping: + message = message.replace("&", "&") + message = message.replace("<", "<") + message = message.replace(">", ">") + return message diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 37562e751..5fda708bd 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -882,9 +882,9 @@ class _WebEnginePermissions(QObject): if on: timeout = config.val.content.fullscreen.overlay_timeout if timeout != 0: - notification = miscwidgets.FullscreenNotification(self._widget) - notification.set_timeout(timeout) - notification.show() + notif = miscwidgets.FullscreenNotification(self._widget) + notif.set_timeout(timeout) + notif.show() @pyqtSlot(QUrl, 'QWebEnginePage::Feature') def _on_feature_permission_requested(self, url, feature): diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 8b233e018..696a87d66 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -909,6 +909,23 @@ content.notifications: QtWebKit: true desc: Allow websites to show notifications. +content.notification_presenter: + default: libnotify + restart: true # no way to clear the presenter libnotify installs + type: + name: String + valid_values: + - qt: Use Qt's native notification presenter. + - libnotify: Use a libnotify-compatible presenter if one is present, + otherwise fall back to Qt. + backend: + QtWebEngine: Qt 5.13 + QtWebKit: false + desc: >- + What notification presenter to use for web notifications. + + Windows and macOS will always act as if this is set to `qt.` + content.pdfjs: default: false type: Bool diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 9e1fb91cd..d0819f832 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -179,11 +179,13 @@ def debug_flag_error(flag): wait-renderer-process: Wait for debugger in renderer process. avoid-chromium-init: Enable `--version` without initializing Chromium. werror: Turn Python warnings into errors. + test-notification-service: Use the testing libnotify service. """ valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history', 'no-scroll-filtering', 'log-requests', 'log-cookies', 'log-scroll-pos', 'log-sensitive-keys', 'stack', 'chromium', - 'wait-renderer-process', 'avoid-chromium-init', 'werror'] + 'wait-renderer-process', 'avoid-chromium-init', 'werror', + 'test-notification-service'] if flag in valid_flags: return flag diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index 205e2ece7..b903c96d9 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -34,6 +34,7 @@ from PyQt5.QtCore import PYQT_VERSION, QCoreApplication pytest.register_assert_rewrite('end2end.fixtures') +from end2end.fixtures.notificationserver import notification_server from end2end.fixtures.webserver import server, server_per_test, server2, ssl_server from end2end.fixtures.quteprocess import (quteproc_process, quteproc, quteproc_new) @@ -111,6 +112,7 @@ def _get_backend_tag(tag): 'qtwebengine_todo': pytest.mark.qtwebengine_todo, 'qtwebengine_skip': pytest.mark.qtwebengine_skip, 'qtwebengine_notifications': pytest.mark.qtwebengine_notifications, + 'qtwebengine_py_5_15': pytest.mark.qtwebengine_py_5_15, 'qtwebkit_skip': pytest.mark.qtwebkit_skip, } if not any(tag.startswith(t + ':') for t in pytest_marks): @@ -135,6 +137,14 @@ if not getattr(sys, 'frozen', False): return None +def _pyqt_webengine_at_least_5_15() -> bool: + try: + from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION + return PYQT_WEBENGINE_VERSION >= 0x050F00 + except ImportError: + return False + + def pytest_collection_modifyitems(config, items): """Apply @qtwebengine_* markers; skip unittests with QUTE_BDD_WEBENGINE.""" # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884 @@ -150,9 +160,9 @@ def pytest_collection_modifyitems(config, items): ('qtwebengine_skip', 'Skipped with QtWebEngine', pytest.mark.skipif, config.webengine), ('qtwebengine_notifications', - 'Skipped with QtWebEngine < 5.13', + 'Skipped unless QtWebEngine >= 5.13', pytest.mark.skipif, - config.webengine and not qtutils.version_check('5.13')), + not (config.webengine and qtutils.version_check('5.13'))), ('qtwebkit_skip', 'Skipped with QtWebKit', pytest.mark.skipif, not config.webengine), ('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif, @@ -170,7 +180,10 @@ def pytest_collection_modifyitems(config, items): 'Skipped on Windows', pytest.mark.skipif, utils.is_windows), - + # WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html + ('qtwebengine_py_5_15', 'Skipped with PyQtWebEngine < 5.15', + pytest.mark.skipif, + config.webengine and not _pyqt_webengine_at_least_5_15()), ] for item in items: diff --git a/tests/end2end/data/prompt/notifications.html b/tests/end2end/data/prompt/notifications.html index f96456ff2..a8867277d 100644 --- a/tests/end2end/data/prompt/notifications.html +++ b/tests/end2end/data/prompt/notifications.html @@ -31,9 +31,26 @@ console.log("[FAIL] notifications unavailable"); } } + + function show_notification() { + let notification = new Notification("notification title", { + body: "notification body" + }); + notification.onclick = function() { console.log("notification clicked"); }; + notification.onclose = function() { console.log("notification closed"); }; + notification.onshow = function() { console.log("notification shown"); }; + } + + function show_symbol_notification() { + let str = "<< && >>"; + let notification = new Notification(str, { body: str }); + notification.onshow = function() { console.log("notification shown"); }; + } </script> </head> <body> <input type="button" onclick="get_notification_permission()" value="Get notification permission" id="button"> + <input type="button" onclick="show_notification()" value="Show notification" id="show-button"> + <input type="button" onclick="show_symbol_notification()" value="Show notification with symbols" id="show-symbols-button"> </body> </html> diff --git a/tests/end2end/features/notifications.feature b/tests/end2end/features/notifications.feature new file mode 100644 index 000000000..5eb6769a3 --- /dev/null +++ b/tests/end2end/features/notifications.feature @@ -0,0 +1,50 @@ +# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et: + +Feature: Notifications + HTML5 notification API interaction + + Background: + Given I have a fresh instance + And I open data/prompt/notifications.html + And I set content.notifications to true + And I run :click-element id button + + @qtwebengine_notifications + Scenario: Notification is shown + When I run :click-element id show-button + Then the javascript message "notification shown" should be logged + And a notification with id 1 is presented + + @qtwebengine_notifications + Scenario: Notification containing escaped characters + Given the notification server supports body markup + When I run :click-element id show-symbols-button + Then the javascript message "notification shown" should be logged + And notification 1 has body "<< && >>" + And notification 1 has title "<< && >>" + + @qtwebengine_notifications + Scenario: Notification containing escaped characters with no body markup + Given the notification server doesn't support body markup + When I run :click-element id show-symbols-button + Then the javascript message "notification shown" should be logged + And notification 1 has body "<< && >>" + And notification 1 has title "<< && >>" + + # For these tests, we need to wait for the notification to be shown before + # we try to close it, otherwise we wind up in race-condition-ish + # situations. + + @qtwebengine_notifications @qtwebengine_py_5_15 + Scenario: User closes presented notification + When I run :click-element id show-button + And I wait for the javascript message "notification shown" + And I close the notification with id 1 + Then the javascript message "notification closed" should be logged + + @qtwebengine_notifications @qtwebengine_py_5_15 + Scenario: User closes some other application's notification + When I run :click-element id show-button + And I wait for the javascript message "notification shown" + And I close the notification with id 1234 + Then the javascript message "notification closed" should not be logged diff --git a/tests/end2end/features/test_notifications_bdd.py b/tests/end2end/features/test_notifications_bdd.py new file mode 100644 index 000000000..dc3fe16f4 --- /dev/null +++ b/tests/end2end/features/test_notifications_bdd.py @@ -0,0 +1,51 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016-2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +import pytest_bdd as bdd +bdd.scenarios('notifications.feature') + + +@bdd.given("the notification server supports body markup") +def supports_body_markup(notification_server): + notification_server.supports_body_markup = True + + +@bdd.given("the notification server doesn't support body markup") +def doesnt_support_body_markup(notification_server): + notification_server.supports_body_markup = False + + +@bdd.then(bdd.parsers.cfparse('a notification with id {id_:d} is presented')) +def notification_presented(notification_server, id_): + assert id_ in notification_server.messages + + +@bdd.then(bdd.parsers.cfparse('notification {id_:d} has body "{body}"')) +def notification_body(notification_server, id_, body): + assert notification_server.messages[id_]["body"] == body + + +@bdd.then(bdd.parsers.cfparse('notification {id_:d} has title "{title}"')) +def notification_title(notification_server, id_, title): + assert notification_server.messages[id_]["title"] == title + + +@bdd.when(bdd.parsers.cfparse('I close the notification with id {id_:d}')) +def close_notification(notification_server, id_): + notification_server.close(id_) diff --git a/tests/end2end/fixtures/notificationserver.py b/tests/end2end/fixtures/notificationserver.py new file mode 100644 index 000000000..122499d21 --- /dev/null +++ b/tests/end2end/fixtures/notificationserver.py @@ -0,0 +1,96 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +import typing + +from PyQt5.QtCore import QObject, QVariant, pyqtSlot +from PyQt5.QtDBus import QDBusConnection, QDBusArgument, QDBusMessage +import pytest + +from qutebrowser.utils import utils + + +class TestNotificationServer(QObject): + """A libnotify notification server used for testing.""" + + # These are the same as in DBusNotificationPresenter. We don't import that + # because it relies on Qt 5.13, and this fixture is *always* instantiated. + SERVICE = "org.freedesktop.Notifications" + TEST_SERVICE = "org.qutebrowser.TestNotifications" + PATH = "/org/freedesktop/Notifications" + INTERFACE = "org.freedesktop.Notifications" + + def __init__(self, service: str): + """Constructs a new server. + + This is safe even if there is no DBus daemon; we don't check whether + the connection is successful until register(). + """ + # Note that external users should call get() instead. + super().__init__() + self._service = service + # Trying to connect to the bus doesn't fail if there's no bus. + self._bus = QDBusConnection.sessionBus() + self._message_id = 0 + # A dict mapping notification IDs to currently-displayed notifications. + self.messages = {} # type: typing.Dict[int, QDBusMessage] + self.supports_body_markup = True + + def register(self) -> None: + assert self._bus.isConnected() + assert self._bus.registerService(self._service) + assert self._bus.registerObject(TestNotificationServer.PATH, + TestNotificationServer.INTERFACE, + self, + QDBusConnection.ExportAllSlots) + + def unregister(self) -> None: + self._bus.unregisterObject(TestNotificationServer.PATH) + assert self._bus.unregisterService(self._service) + + @pyqtSlot(QDBusMessage, result="uint") + def Notify(self, message: QDBusMessage) -> QDBusArgument: # pylint: disable=invalid-name + self._message_id += 1 + args = message.arguments() + self.messages[self._message_id] = { + "title": args[3], + "body": args[4] + } + return self._message_id + + @pyqtSlot(QDBusMessage, result="QStringList") + def GetCapabilities(self, message: QDBusMessage) -> typing.List[str]: # pylint: disable=invalid-name + if self.supports_body_markup: + return ["body-markup"] + else: + return [] + + def close(self, notification_id: int) -> None: + """Sends a close notification for the given ID.""" + message = QDBusMessage.createSignal( + TestNotificationServer.PATH, + TestNotificationServer.INTERFACE, + "NotificationClosed") + # the 2 here is the notification removal reason; it's effectively + # arbitrary + message.setArguments([_as_uint32(notification_id), _as_uint32(2)]) + if not self._bus.send(message): + raise IOError("Could not send close notification") + + +@pytest.fixture +def notification_server(qapp): + server = TestNotificationServer(TestNotificationServer.TEST_SERVICE) + if utils.is_windows or utils.is_mac: + yield server + else: + try: + server.register() + yield server + finally: + server.unregister() + + +def _as_uint32(x: int) -> QVariant: + variant = QVariant(x) + assert variant.convert(QVariant.UInt) + return variant diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 0fdc8dfcf..9b3c3cfb5 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -526,10 +526,11 @@ class QuteProc(testprocess.Process): args = ['--debug', '--no-err-windows', '--temp-basedir', '--json-logging', '--loglevel', 'vdebug', '--backend', backend, '--debug-flag', 'no-sql-history', - '--debug-flag', 'werror'] + '--debug-flag', 'werror', '--debug-flag', + 'test-notification-service'] if self.request.config.webengine: - args += testutils.seccomp_args(qt_flag=True) + args += testutils.sandbox_args(qt_flag=True) args.append('about:blank') return args @@ -1019,7 +1020,7 @@ def quteproc_process(qapp, server, request): @pytest.fixture -def quteproc(quteproc_process, server, request): +def quteproc(quteproc_process, server, request, notification_server): """Per-test qutebrowser fixture which uses the per-file process.""" request.node._quteproc_log = quteproc_process.captured_log quteproc_process.before_test() @@ -15,7 +15,7 @@ setenv = pyqt{,512,513,514,515,5150}: LINK_PYQT_SKIP=true pyqt{,512,513,514,515,5150}: QUTE_BDD_WEBENGINE=true cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= -passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS +passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS DBUS_SESSION_BUS_ADDRESS basepython = py: {env:PYTHON:python3} py3: {env:PYTHON:python3} |