Merge remote-tracking branch 'origin/pr/5457' into dev
This commit is contained in:
commit
2f592f7ce6
|
|
@ -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()"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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_)
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
2
tox.ini
2
tox.ini
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue