diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 3d5ce30bd..fc9c6edc7 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -925,6 +925,46 @@ def _backend() -> str: raise utils.Unreachable(objects.backend) +def _webengine_extensions() -> Sequence[str]: + """Get a list of WebExtensions enabled in QtWebEngine.""" + lines: list[str] = [] + if ( + objects.backend == usertypes.Backend.QtWebEngine + and "avoid-chromium-init" not in objects.debug_flags + and machinery.IS_QT6 # mypy; TODO early return once Qt 5 is dropped + ): + from qutebrowser.qt.webenginecore import QWebEngineProfile + profile = QWebEngineProfile.defaultProfile() + assert profile is not None # mypy + + try: + ext_manager = profile.extensionManager() + except AttributeError: + # Added in QtWebEngine 6.10 + return [] + assert ext_manager is not None # mypy + + lines.append("WebExtensions:") + if not ext_manager.extensions(): + lines[0] += " none" + + for info in ext_manager.extensions(): + tags = [ + ("[x]" if info.isEnabled() else "[ ]") + " enabled", + ("[x]" if info.isLoaded() else "[ ]") + " loaded", + ("[x]" if info.isInstalled() else "[ ]") + " installed", + ] + lines.append(f" {info.name()} ({info.id()})") + lines.append(f" {' '.join(tags)}") + lines.append(f" {info.path()}") + url = info.actionPopupUrl() + if url.isValid(): + lines.append(f" {url.toDisplayString()}") + lines.append("") + + return lines + + def _uptime() -> datetime.timedelta: time_delta = datetime.datetime.now() - objects.qapp.launch_time # Round off microseconds @@ -974,6 +1014,8 @@ def version_info() -> str: if QSslSocket.supportsSsl() else 'no'), ] + lines += _webengine_extensions() + if objects.qapp: style = objects.qapp.style() assert style is not None diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index a6db93bb2..45a445e6d 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -23,7 +23,8 @@ import pytest_mock import hypothesis import hypothesis.strategies from qutebrowser.qt import machinery -from qutebrowser.qt.core import PYQT_VERSION_STR +from qutebrowser.qt.core import PYQT_VERSION_STR, QUrl +from qutebrowser.qt.webenginecore import QWebEngineProfile import qutebrowser from qutebrowser.config import config, websettings @@ -1153,13 +1154,7 @@ class TestChromiumVersion: def test_prefers_saved_user_agent(self, monkeypatch, patch_no_api): webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87')) - - class FakeProfile: - def defaultProfile(self): - raise AssertionError("Should not be called") - - monkeypatch.setattr(webenginesettings, 'QWebEngineProfile', FakeProfile()) - + monkeypatch.setattr(QWebEngineProfile, "defaultProfile", lambda: 1/0) version.qtwebengine_versions() def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub): @@ -1280,6 +1275,62 @@ class TestChromiumVersion: assert versions.webengine == override +class FakeExtensionInfo: + def __init__( + self, + name: str, + *, + enabled: bool = False, + installed: bool = False, + loaded: bool = False, + action_popup_url: QUrl = QUrl(), + ) -> None: + self._name = name + self.enabled = enabled + self.installed = installed + self.loaded = loaded + self.action_popup_url = action_popup_url + + def isEnabled(self) -> bool: + return self.enabled + + def isInstalled(self) -> bool: + return self.installed + + def isLoaded(self) -> bool: + return self.loaded + + def name(self) -> str: + return self._name + + def actionPopupUrl(self) -> QUrl: + return self.action_popup_url + + def path(self) -> str: + return f"{self._name}-path" + + def id(self) -> str: + return f"{self._name}-id" + + +class FakeExtensionManager: + + def __init__(self, extensions: list[FakeExtensionInfo]) -> None: + self._extensions = extensions + + def extensions(self) -> list[FakeExtensionInfo]: + return self._extensions + + +class FakeExtensionProfile: + + def __init__(self, ext_manager: FakeExtensionManager) -> None: + self._ext_manager = ext_manager + + def extensionManager(self) -> FakeExtensionManager: + return self._ext_manager + + @dataclasses.dataclass class VersionParams: @@ -1373,6 +1424,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub): 'python_path': 'EXECUTABLE PATH', 'uptime': "1:23:45", 'autoconfig_loaded': "yes" if params.autoconfig_loaded else "no", + 'webextensions': "", # overridden below if QtWebEngine is used } patches['qtwebengine_versions'] = ( @@ -1395,6 +1447,20 @@ def test_version_info(params, stubs, monkeypatch, config_stub): substitutions['backend'] = 'new QtWebKit (WebKit WEBKIT VERSION)' else: monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False) + monkeypatch.setattr( + QWebEngineProfile, + "defaultProfile", + lambda: FakeExtensionProfile( + FakeExtensionManager([FakeExtensionInfo("ext1")]) + ), + ) + substitutions['webextensions'] = ( + "\n" + "WebExtensions:\n" + " ext1 (ext1-id)\n" + " [ ] enabled [ ] loaded [ ] installed\n" + " ext1-path\n" + ) patches['objects.backend'] = usertypes.Backend.QtWebEngine substitutions['backend'] = 'QtWebEngine 1.2.3\n (source: faked)' @@ -1434,7 +1500,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub): pdf.js: PDFJS VERSION sqlite: SQLITE VERSION QtNetwork SSL: {ssl} - {style}{platform_plugin}{opengl} + {webextensions}{style}{platform_plugin}{opengl} Platform: PLATFORM, ARCHITECTURE{linuxdist} Frozen: {frozen} Imported from {import_path} @@ -1519,6 +1585,92 @@ class TestOpenGLInfo: assert str(info) == 'OpenGL ES' +class TestWebEngineExtensions: + + def test_qtwebkit(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(version.objects, "backend", usertypes.Backend.QtWebKit) + monkeypatch.setattr(QWebEngineProfile, "defaultProfile", lambda: 1/0) + assert not version._webengine_extensions() + + def test_avoid_chromium_init(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(version.objects, "backend", usertypes.Backend.QtWebEngine) + monkeypatch.setattr(objects, "debug_flags", {"avoid-chromium-init"}) + monkeypatch.setattr(QWebEngineProfile, "defaultProfile", lambda: 1/0) + assert not version._webengine_extensions() + + def test_no_extension_manager(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(QWebEngineProfile, "defaultProfile", object) + assert not version._webengine_extensions() + + @pytest.mark.parametrize( + "extensions, expected", + [ + pytest.param([], ["WebExtensions: none"], id="empty"), + pytest.param( + [FakeExtensionInfo("ext1")], + [ + "WebExtensions:", + " ext1 (ext1-id)", + " [ ] enabled [ ] loaded [ ] installed", + " ext1-path", + "", + ], + id="single", + ), + pytest.param( + [ + FakeExtensionInfo("ext1", enabled=True), + FakeExtensionInfo( + "ext2", enabled=True, loaded=True, installed=True + ), + ], + [ + "WebExtensions:", + " ext1 (ext1-id)", + " [x] enabled [ ] loaded [ ] installed", + " ext1-path", + "", + " ext2 (ext2-id)", + " [x] enabled [x] loaded [x] installed", + " ext2-path", + "", + ], + id="multiple", + ), + pytest.param( + [ + FakeExtensionInfo( + "ext", action_popup_url=QUrl("chrome-extension://ext") + ) + ], + [ + "WebExtensions:", + " ext (ext-id)", + " [ ] enabled [ ] loaded [ ] installed", + " ext-path", + " chrome-extension://ext", + "", + ], + id="with-url", + ), + ], + ) + def test_extensions( + self, + monkeypatch: pytest.MonkeyPatch, + extensions: list[FakeExtensionInfo], + expected: list[str], + ) -> None: + monkeypatch.setattr( + QWebEngineProfile, + "defaultProfile", + lambda: FakeExtensionProfile( + FakeExtensionManager(extensions) + ), + ) + assert version._webengine_extensions() == expected + + @pytest.fixture def pbclient(stubs): http_stub = stubs.HTTPPostStub()