diff --git a/dzgui/api/shortcuts.py b/dzgui/api/shortcuts.py index 144bb44..2ecb076 100644 --- a/dzgui/api/shortcuts.py +++ b/dzgui/api/shortcuts.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Any from dzgui.api.steam import find_user_id_32 -from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, GTK_APP_ID, IMAGES_PATH +from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH from dzgui.util.dirs import copy_dzgui_to_xdg_data, make_parents from dzgui.util.strings import unknown @@ -158,7 +158,7 @@ class Shortcuts: NEW_ENTRY["DevkitGameID"] = "" NEW_ENTRY["DevkitOverrideAppID"] = 0 NEW_ENTRY["LastPlayTime"] = 0 - NEW_ENTRY["FlatpakAppID"] = GTK_APP_ID + NEW_ENTRY["FlatpakAppID"] = "" NEW_ENTRY["sortas"] = "" NEW_ENTRY["tags"] = {} diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 7253d1c..2580bd0 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -13,6 +13,7 @@ from warnings import deprecated from dzgui.api.acf import ACF from dzgui.init.prereqs import has_any_client +from dzgui.init.proc import has_cmd from dzgui.const.constants import ( APPID_DAYZ, APPID_DAYZ_EXP, @@ -22,10 +23,12 @@ from dzgui.const.constants import ( DEBIAN_STEAM_PATH, DEFAULT_STEAM_PATH, DOWNLOADS_PATH, + FLATPAK_CMD, FLATPAK_STEAM_PATH, LIBRARYFOLDERS_PATH, - UBUNTU_STEAM_PATH, REQUEST_TIMEOUT, + STEAM_CMD, + UBUNTU_STEAM_PATH, VDF_PATH, ) from dzgui.const.endpoints import ( @@ -82,6 +85,10 @@ def get_steam_paths() -> list[tuple[Path, str]]: } for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH: + if path is FLATPAK_PATH and has_cmd(FLATPAK_CMD) is False: + continue + if path is not FLATPAK_PATH and has_cmd(STEAM_CMD) is False: + continue if path.joinpath(VDF_PATH).is_file(): paths.append((path, human[path])) return paths @@ -198,6 +205,7 @@ def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int: """Loads to the menu screen with the selected mods; used for AFK/pre-joining""" concat = concat_mods(mods) client_args = concat_bash_args(client) + logger.debug(f"Setting client arguments to: {client_args}") params = [ "-applaunch", str(appid), diff --git a/dzgui/app_init.py b/dzgui/app_init.py index 26df94d..dbd01ea 100644 --- a/dzgui/app_init.py +++ b/dzgui/app_init.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, STEAM_ICON, ICONS_PATH +from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, FLATPAK_ROOT_PATH, ICONS_PATH from dzgui.const.enum import Preferences from dzgui.config.query import lookup from dzgui.config.userprefs import UserPrefs @@ -18,7 +18,7 @@ from dzgui.init.migrate import ( migrate_cols_file, ) from dzgui.init.flock import lock_acquire -from dzgui.init.prereqs import has_any_client +from dzgui.init.prereqs import has_flatpak_client, has_steam_client from dzgui.strings import boot from dzgui.util.deck import is_steam_deck, is_game_mode @@ -142,7 +142,7 @@ def load_gui(version: str, is_debug: bool) -> None: migrate_cols_file(XDG.columns) copy_state_files(xdg_paths["XDG_STATE_HOME"]) # TODO: add logging inside wizard - wizard = SetupWizard(_is_steam_deck, XDG) + wizard = SetupWizard(_is_steam_deck, XDG, xdg_paths) if not wizard.is_setup_complete(): return @@ -155,7 +155,11 @@ def load_gui(version: str, is_debug: bool) -> None: # TODO: sudo escalation dialog # count = get_map_count() # TODO: move into module - if has_any_client() is False: + if FLATPAK_ROOT_PATH in lookup(XDG.config, Preferences.DEFAULT): + has_client = has_flatpak_client() + else: + has_client = has_steam_client() + if has_client is False: EarlyAlertDialog(init.requires_steam) bootwin = BootWindow(XDG, version) diff --git a/dzgui/config/xdg.py b/dzgui/config/xdg.py index 47f747a..1d7f5b3 100644 --- a/dzgui/config/xdg.py +++ b/dzgui/config/xdg.py @@ -3,7 +3,13 @@ import os from pathlib import Path from dataclasses import dataclass -from dzgui.const.constants import APP_NAME_LOWER, DEBUG_LOG, SYSTEM_LOG +from dzgui.const.constants import ( + APP_NAME_LOWER, + DEBUG_LOG, + FLATPAK_ROOT_PATH, + SYSTEM_LOG, +) +from dzgui.init.proc import is_sandboxed @dataclass @@ -53,10 +59,12 @@ def get_xdg_paths() -> dict: "XDG_DATA_HOME": home / ".local" / "share", "XDG_CACHE_HOME": home / ".cache", } - resolved_paths = {} for path in xdg_paths: real_path = os.environ.get(path) + if real_path is not None and is_sandboxed(): + search_string = FLATPAK_ROOT_PATH + "/" + real_path = real_path.replace(search_string, "") if real_path is not None and is_writeable(real_path): new_path = Path(real_path) else: diff --git a/dzgui/init/proc.py b/dzgui/init/proc.py index 3d76516..0c1a2a0 100644 --- a/dzgui/init/proc.py +++ b/dzgui/init/proc.py @@ -1,7 +1,8 @@ -import psutil -import subprocess -import shutil import logging +import os +import psutil +import shutil +import subprocess from dzgui.const.constants import ( APP_NAME, @@ -26,7 +27,13 @@ def is_steam_running(cmd: str) -> bool: raise TypeError("Not a valid Steam client selection") +def is_sandboxed() -> bool: + return bool(os.environ.get("FLATPAK_ID")) + + def is_flatpak_steam_running() -> bool: + if is_sandboxed(): + return True if has_cmd(FLATPAK_CMD) is False: return False proc = subprocess.check_output( diff --git a/dzgui/strings/wizard.py b/dzgui/strings/wizard.py index 9f499b0..de83762 100644 --- a/dzgui/strings/wizard.py +++ b/dzgui/strings/wizard.py @@ -3,6 +3,8 @@ title_intro = "Welcome" blurb_intro = """ This wizard will help you set up some common config options before launching the application. """ +error_sandbox = "Setup cannot be run from inside Flatpak sandbox.\nPlease launch the application directly." +welcome = "Welcome to {0}!" ### SteamPathPage heading_steam_path = "Steam path" @@ -17,7 +19,7 @@ desc_flatpak_path = ( desc_ubuntu_path = "This is the default Steam path on Ubuntu-based systems." desc_debian_path = "This is the default Steam path on Debian-based systems." no_valid_paths = ( - "No valid Steam paths found on system. Please install Steam to continue." + "No valid Steam path or Steam command found on system. Please install Steam to continue." ) button_scan = "Scan for Steam" @@ -80,6 +82,17 @@ checkbox_start_menu = ( ) checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.") +### Flatpak page +heading_flatpak = "Flatpak sandbox overrides" +blurb_flatpak = ( + "You chose to add a shortcut directly to Flatpak Steam. " + "To launch DZGUI from Flatpak's sandbox, you must grant " + "permission to read from and write to the following locations." +) +tree_column = "Paths" +overrides_applied = "Overrides applied." +button_flatpak = "Grant permissions" + ### Completion page heading_completion = "Setup complete" blurb_completion = "Configuration completed successfully. Click 'Close' to close this dialog and launch DZGUI." diff --git a/dzgui/util/bash.py b/dzgui/util/bash.py index 9c2a73d..d612b94 100644 --- a/dzgui/util/bash.py +++ b/dzgui/util/bash.py @@ -1,5 +1,12 @@ import shlex +from dzgui.const.constants import STEAM_CMD +from dzgui.init.proc import is_sandboxed + def concat_bash_args(command: str) -> list[str]: + # NOTE: NSGs spawned from sandbox cannot access host application commands, + # so fallback to Steam internal URIs + if is_sandboxed(): + return [STEAM_CMD] return shlex.split(command) diff --git a/dzgui/util/flatpak.py b/dzgui/util/flatpak.py new file mode 100644 index 0000000..a432970 --- /dev/null +++ b/dzgui/util/flatpak.py @@ -0,0 +1,31 @@ +import logging +import subprocess + +from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID + +logger = logging.getLogger(APP_NAME) + + +def apply_overrides(xdg: dict[str, str]) -> None: + try: + # NOTE: D-Bus registration required for sandbox + args = [ + "flatpak", + "--user", + "override", + f"--own-name={GTK_APP_ID}", + FLATPAK_APPID, + ] + subprocess.run([*args], capture_output=True) + + for path in xdg.values(): + args = [ + "flatpak", + "--user", + "override", + f"--filesystem={path}", + FLATPAK_APPID, + ] + subprocess.run([*args], capture_output=True) + except Exception as e: + logger.critical(e) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 30a7fc6..91c41ea 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -297,7 +297,7 @@ class Options: init = Init( is_steam_running="Is Steam running? DZGUI must be run on top of Steam.", is_dayz_running="Is DayZ already running? DZGUI cannot launch DayZ if another process is using it.", - requires_steam="DZGUI requires that Steam or Flatpak Steam be installed on the system.", + requires_steam="Your chosen client (Steam/Flatpak Steam) must be installed on the system.", ) mod_panel = ModPanelStrings( diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index b3e54f4..1c12d40 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -12,22 +12,24 @@ from dzgui.const.constants import ( APP_NAME, APP_NAME_LOWER, HERO_PATH, - FLATPAK_STEAM_PATH, + FLATPAK_ROOT_PATH, LEGACY_CONFIG_PATH, ) from dzgui.const.boilerplate import config_boilerplate from dzgui.const.endpoints import STEAM_API_SETUP from dzgui.config import freedesktop -from dzgui.init.proc import path_to_steam_cmd +from dzgui.init.proc import is_sandboxed, path_to_steam_cmd from dzgui.init.migrate import migrate_legacy_conf from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.strings import wizard from dzgui.util._json import write_json +from dzgui.util.flatpak import apply_overrides from dzgui.util.open_links import open_link_by_url from dzgui.util.css import add_class, load_css from dzgui.views.components.buttons import WebButton from dzgui.views.components.entry import APIEntry from dzgui.views.mixins.colorscheme import ColorAwareApp +from dzgui.views.trees.tree_simple import SimpleTree import gi @@ -45,7 +47,8 @@ class PageNum(Enum): STEAM_API = 4 USER_PREFS = 5 SHORTCUTS = 6 - FINAL = 7 + FLATPAK = 7 + FINAL = 8 class OptionalPageMixin: @@ -144,7 +147,7 @@ class NotificationFrame(Gtk.Frame): margin_start=50, margin_end=50, ) - wrapped = textwrap.fill(label, width=80) + wrapped = textwrap.fill(label, width=60) self.label = Gtk.Label( label=wrapped, justify=Gtk.Justification.CENTER, @@ -158,7 +161,7 @@ class NotificationFrame(Gtk.Frame): add_class(self, "error-frame") def set_text(self, text: str) -> None: - wrapped = textwrap.fill(text, width=80) + wrapped = textwrap.fill(text, width=60) self.label.set_markup(wrapped) @@ -243,12 +246,23 @@ class IntroductionPage(EnumeratedWizardPage): def __init__(self) -> None: super().__init__( enum=PageNum.INTRO, - heading=f"Welcome to {APP_NAME}!", + heading=wizard.welcome.format(APP_NAME), description=wizard.blurb_intro, ) self.page_type = Gtk.AssistantPageType.INTRO self.set_title(wizard.title_intro) + self.error_box = NotificationFrame(wizard.error_sandbox, error=True) + self.add_start(self.error_box) + + self.connect("map", self._on_map) + + def _on_map(self, page: "EnumeratedWizardPage") -> None: + self.error_box.hide() + if is_sandboxed(): + self.error_box.show() + EMITTER.emit("step_pending") + class Heading(Gtk.Label): def __init__(self, label: str): @@ -405,6 +419,42 @@ class PreferencesPage(EnumeratedWizardPage): EMITTER.emit("step_complete") +class FlatpakOverridePage(EnumeratedWizardPage): + def __init__(self, xdg_paths: dict[str, str]) -> None: + super().__init__( + enum=PageNum.FLATPAK, + heading=wizard.heading_flatpak, + description=wizard.blurb_flatpak, + ) + self.xdg_paths = xdg_paths + self.page_type = Gtk.AssistantPageType.INTRO + + store = Gtk.ListStore(str) + for path in xdg_paths.values(): + store.append((str(path),)) + tree = SimpleTree(store, wizard.tree_column) + tree.set_halign(Gtk.Align.CENTER) + + self.authorize_button = Gtk.Button( + label=wizard.button_flatpak, halign=Gtk.Align.CENTER + ) + self.authorize_button.connect("clicked", self._on_authorize) + + self.notice = NotificationFrame(wizard.overrides_applied) + self.connect("map", self._on_map) + + for el in tree, self.authorize_button, self.notice: + self.add_start(el) + + def _on_map(self, page: "EnumeratedWizardPage") -> None: + self.notice.hide() + + def _on_authorize(self, button: Gtk.Button) -> None: + apply_overrides(self.xdg_paths) + self.notice.show() + EMITTER.emit("step_complete") + + class CompletionPage(EnumeratedWizardPage): def __init__(self) -> None: super().__init__( @@ -418,7 +468,7 @@ class CompletionPage(EnumeratedWizardPage): class Assistant(Gtk.Assistant): - def __init__(self, is_deck: bool, XDG: "Xdg"): + def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]): super().__init__() if is_deck: # NOTE: deemed to be "safe" dimensions that exclude taskbar size @@ -437,6 +487,7 @@ class Assistant(Gtk.Assistant): self.page_api = SteamValidationPage() self.page_prefs = PreferencesPage() self.page_shortcuts = ShortcutCreationPage(XDG.shortcut) + self.page_flatpak = FlatpakOverridePage(xdg_paths) self.page_completion = CompletionPage() self.set_forward_page_func(self._advance_page) @@ -456,6 +507,7 @@ class Assistant(Gtk.Assistant): self.page_api, self.page_prefs, self.page_shortcuts, + self.page_flatpak, self.page_completion, ): # NOTE: skip config migration page if no legacy config file @@ -476,7 +528,10 @@ class Assistant(Gtk.Assistant): def write_config(self, config: dict[str, Any]) -> None: write_json(config, self.config_path) - def get_final_page(self) -> int: + def get_completion_page(self) -> int: + return self.pages[self.page_completion] + + def get_shortcut_page(self) -> int: return self.pages[self.page_shortcuts] def _advance_page(self, index: int) -> int: @@ -489,7 +544,7 @@ class Assistant(Gtk.Assistant): sp = page.migrated_conf["default_steam_path"] self.page_shortcuts.set_steam_path(sp) self.config_values = self.page_migration.migrated_conf - return self.get_final_page() + return self.get_shortcut_page() case SteamPathPage(): path = page.get_path_from_radio() self.config_values["default_steam_path"] = path @@ -506,10 +561,22 @@ class Assistant(Gtk.Assistant): ) case ShortcutCreationPage(): page.create_shortcuts() + if not self.needs_flatpak_override(): + return self.get_completion_page() + case FlatpakOverridePage(): + pass case _: raise AttributeError("Trying to advance a non-canonical page") return index + 1 + def needs_flatpak_override(self) -> bool: + if ( + self.page_shortcuts.is_steam_checkbox_active() + and FLATPAK_ROOT_PATH in self.config_values["default_steam_path"] + ): + return True + return False + def is_setup_complete(self) -> bool: return self.setup_complete @@ -625,12 +692,6 @@ class ShortcutCreationPage(OptionalPageMixin, EnumeratedWizardPage): # type: ig self.add_start(self.checks_area) self.show_all() self.connect("map", self._on_map) - self.connect("map", self._toggle_checkbox) - - def _toggle_checkbox(self, page: "EnumeratedWizardPage") -> None: - if FLATPAK_STEAM_PATH in str(self.steam_path): - self.steam_checkbox.set_active(False) - self.steam_checkbox.set_sensitive(False) def _on_start_menu_toggled(self, button: Gtk.CheckButton) -> None: state = button.get_active() @@ -641,6 +702,9 @@ class ShortcutCreationPage(OptionalPageMixin, EnumeratedWizardPage): # type: ig def set_steam_path(self, path: Path) -> None: self.steam_path = path + def is_steam_checkbox_active(self) -> bool: + return self.steam_checkbox.get_active() + def create_shortcuts(self) -> None: # NOTE: best-effort, permissive even on failure (page is already marked as complete) if self.steam_checkbox.get_active(): @@ -709,10 +773,10 @@ class SteamPathPage(EnumeratedWizardPage): class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore - def __init__(self, is_deck: bool, XDG: "Xdg") -> None: + def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None: super().__init__() GLib.set_prgname(APP_NAME) - self.win = Window(is_deck, XDG) + self.win = Window(is_deck, XDG, xdg_paths) Gtk.main() def is_setup_complete(self) -> int: @@ -720,9 +784,9 @@ class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore class Window(Gtk.Window): - def __init__(self, is_deck: bool, XDG: "Xdg") -> None: + def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None: super().__init__(title=APP_NAME, icon_name=APP_NAME) - self.assistant = Assistant(is_deck, XDG) + self.assistant = Assistant(is_deck, XDG, xdg_paths) class Emitter(GObject.GObject): diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 64c5d51..ebce129 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -266,7 +266,6 @@ class FlatpakToggleField(ToggleField): def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None: cmd = FLATPAK_RUN_CMD if self.radio1.get_active() else FLATPAK_SANDBOX - print(context) self.controller.update_config(context, cmd) diff --git a/dzgui/views/trees/tree_simple.py b/dzgui/views/trees/tree_simple.py new file mode 100644 index 0000000..be96668 --- /dev/null +++ b/dzgui/views/trees/tree_simple.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING, Union + +if TYPE_CHECKING: + from dzgui.model.model_factory import FastInsertListStore + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk # noqa + + +class SimpleTree(Gtk.ScrolledWindow): + """Simple scrollable 1-column tree""" + + def __init__( + self, model: Union[Gtk.ListStore, "FastInsertListStore"], title: str + ) -> None: + super().__init__( + overlay_scrolling=False, + propagate_natural_width=True, + propagate_natural_height=True, + ) + + self.tree = Gtk.TreeView(model=model, enable_search=False) + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(title, renderer, text=0) + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + column.set_resizable(False) + column.set_sort_column_id(0) + self.tree.append_column(column) + + self.add(self.tree) + + def get_tree(self) -> Gtk.TreeView: + return self.tree diff --git a/pyproject.toml b/pyproject.toml index 1d52faa..b7a9729 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,10 +110,11 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } } markers = [ "apitest: checks remote endpoints", "config: config file keys/values", - "realconfig: depends on actual generated local config file", + "flatpak: depends on local flatpak installation", "mods: tests mod metadata/link creation", "pefile: validate PE files", "post_install: requires a completed installation", + "realconfig: depends on actual generated local config file", "redact: log redaction mechanisms", "slow: long-running tests", "webtest: depends on remote endpoint", diff --git a/tests/test_flatpak_override.py b/tests/test_flatpak_override.py new file mode 100644 index 0000000..d34307d --- /dev/null +++ b/tests/test_flatpak_override.py @@ -0,0 +1,43 @@ +import configparser +import pytest + +from dzgui.const.constants import FLATPAK_APPID, GTK_APP_ID +from dzgui.config.xdg import get_xdg_paths +from dzgui.util.flatpak import apply_overrides + +PATHS_KEY = "Context" +PATHS_SUBKEY = "filesystems" +BUS_KEY = "Session Bus Policy" + +pytestmark = pytest.mark.flatpak + + +@pytest.fixture +def xdg_paths() -> dict[str, str]: + return get_xdg_paths() + + +@pytest.fixture +def override(xdg_paths) -> None: + apply_overrides(xdg_paths) + + +@pytest.fixture +def config(xdg_paths) -> configparser.ConfigParser: + conf_path = xdg_paths["XDG_DATA_HOME"].parent.joinpath( + f"flatpak/overrides/{FLATPAK_APPID}" + ) + config = configparser.ConfigParser() + config.read(conf_path) + return config + + +def test_session_bus(override, config) -> None: + bus = config[BUS_KEY][GTK_APP_ID] + assert bus == "own" + + +def test_paths(override, xdg_paths, config) -> None: + expect = xdg_paths + paths = config[PATHS_KEY][PATHS_SUBKEY].split(";") + assert all(str(path) in paths for path in expect.values()) diff --git a/tests/test_flatpak_toggle.py b/tests/test_flatpak_toggle.py new file mode 100644 index 0000000..eec56e7 --- /dev/null +++ b/tests/test_flatpak_toggle.py @@ -0,0 +1,30 @@ +import pytest + +from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX +from dzgui.const.enum import Preferences +from dzgui.views.pages.options import FlatpakToggleField + + +class MockController: + def __init__(self) -> None: + self.command: str + + def update_config(self, context: Preferences, cmd: str) -> None: + self.command = cmd + + def get_command(self) -> str: + return self.command + + +@pytest.mark.parametrize("index, expect", [(0, FLATPAK_RUN_CMD), (1, FLATPAK_SANDBOX)]) +def test_toggle(index: int, expect: str) -> None: + controller = MockController() + toggle = FlatpakToggleField(controller) + match index: + case 0: + toggle.radio2.set_active(True) + toggle.radio1.set_active(True) + case 1: + toggle.radio1.set_active(True) + toggle.radio2.set_active(True) + assert controller.get_command() == expect diff --git a/tests/test_html_escaping.py b/tests/test_html_escaping.py index 471bb1a..904e5b2 100644 --- a/tests/test_html_escaping.py +++ b/tests/test_html_escaping.py @@ -1,3 +1,5 @@ +import pytest + from dzgui.api.servers import Details from dzgui.views.dialogs.servers import ServerDetailsDialog @@ -21,6 +23,7 @@ class MockController: return None +@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_html_escaping() -> None: name = "Test server" desc = " Game & mods" @@ -31,5 +34,7 @@ def test_html_escaping() -> None: controller = MockController() dialog = ServerDetailsDialog(controller, details) dialog.destroy() + while Gtk.events_pending(): + Gtk.main_iteration() assert dialog.description.get_text() == desc diff --git a/tests/test_process_detection.py b/tests/test_process_detection.py index 23f0956..8b25c9b 100644 --- a/tests/test_process_detection.py +++ b/tests/test_process_detection.py @@ -1,3 +1,4 @@ +import pytest import unittest from unittest.mock import patch @@ -9,6 +10,8 @@ from dzgui.const.constants import ( ) from dzgui.init import proc +pytestmark = pytest.mark.flatpak + class TestSteamDetection(unittest.TestCase): def test_flatpak_detection_with_multiple_running_apps(self):