diff --git a/tests/uitests/data/keyfile/systray.ini b/tests/uitests/data/keyfile/systray.ini new file mode 100644 index 000000000..99c15100c --- /dev/null +++ b/tests/uitests/data/keyfile/systray.ini @@ -0,0 +1,15 @@ +[org/virt-manager/virt-manager] +system-tray=true + +manager-window-width=550 +manager-window-height=550 + +[org/virt-manager/virt-manager/connections] +uris=['test:///default', 'foo:///'] + +[org/virt-manager/virt-manager/stats] +update-interval=1 + +[org/virt-manager/virt-manager/vmlist-fields] +disk-usage=false +network-traffic=false diff --git a/tests/uitests/test_systray.py b/tests/uitests/test_systray.py new file mode 100644 index 000000000..7fe5dc9b0 --- /dev/null +++ b/tests/uitests/test_systray.py @@ -0,0 +1,124 @@ +# This work is licensed under the GNU GPLv2 or later. +# See the COPYING file in the top-level directory. + +import tests.utils +from tests.uitests import utils as uiutils + + +class Systray(uiutils.UITestCase): + """ + UI tests for virt-manager's systray using a fake testing backend + """ + + ############## + # Test cases # + ############## + + def testSystrayFake(self): + self.app.open( + keyfile="systray.ini", + extra_opts=["--test-options=fake-systray"], + window_name="Virtual Machine Manager") + + manager = self.app.topwin + systray = self.app.root.find("vmm-fake-systray", check_active=False) + uiutils.drag(manager, 1000, 1000) + + # Add a connection to trigger systray update + uri = tests.utils.URIs.kvm + manager.find("File", "menu").click() + manager.find("Add Connection...", "menu item").click() + win = self.app.root.find_fuzzy("Add Connection", "dialog") + win.combo_select("Hypervisor", "Custom URI") + win.find("uri-entry", "text").set_text(uri) + win.find("Connect", "push button").click() + + # Hide the manager + systray.click_title() + systray.click() + uiutils.check(lambda: not manager.showing) + uiutils.check(lambda: self.app.is_running()) + + systray.click(button=3) + menu = self.app.root.find("vmm-systray-menu") + + def _get_conn_action(connstr, actionstr): + if not menu.showing: + systray.click(button=3) + uiutils.check(lambda: menu.showing) + connmenu = menu.find(connstr, "menu") + connmenu.point() + return connmenu.find(actionstr, "menu") + + def _check_conn_action(connstr, actionstr): + item = _get_conn_action(connstr, actionstr) + uiutils.check(lambda: item.showing) + systray.click(button=3) + uiutils.check(lambda: not menu.showing) + + def _do_conn_action(connstr, actionstr): + item = _get_conn_action(connstr, actionstr) + item.click() + uiutils.check(lambda: not menu.showing) + + def _get_vm_action(connstr, vmname, action): + vmenu = _get_conn_action(connstr, vmname) + vmenu.point() + return vmenu.find(action, "menu") + + def _check_vm_action(connstr, vmname, action): + item = _get_vm_action(connstr, vmname, action) + uiutils.check(lambda: item.showing) + systray.click(button=3) + uiutils.check(lambda: not menu.showing) + + def _do_vm_action(connstr, vmname, action): + item = _get_vm_action(connstr, vmname, action) + item.click() + uiutils.check(lambda: not menu.showing) + + # Right click start a connection + _check_conn_action("QEMU/KVM", "Disconnect") + _do_conn_action("test default", "Connect") + _check_conn_action("test default", "Disconnect") + _do_conn_action("test testdriver", "Disconnect") + _check_conn_action("test testdriver", "Connect") + + # Trigger VM change + _do_vm_action("QEMU/KVM", "test-arm-kernel", "Pause") + _check_vm_action("QEMU/KVM", "test-arm-kernel", "Resume") + + # Reshow the manager + systray.click() + uiutils.check(lambda: manager.showing) + uiutils.check(lambda: self.app.is_running()) + + # Close from the menu + systray.click_title() + systray.click(button=3) + menu = self.app.root.find("vmm-systray-menu") + menu.find("Quit", "menu item").click() + + uiutils.check(lambda: not self.app.is_running()) + + def testSystrayToggle(self): + self.app.open( + keyfile="systray.ini", + extra_opts=["--test-options=fake-systray"], + window_name="Virtual Machine Manager") + + manager = self.app.topwin + systray = self.app.root.find("vmm-fake-systray", check_active=False) + manager.find("Edit", "menu").click() + manager.find("Preferences", "menu item").click() + prefs = self.app.root.find_fuzzy("Preferences", "frame") + + # Close the system tray + prefs.click_title() + prefs.find_fuzzy("Enable system tray", "check").click() + uiutils.check(lambda: not systray.showing) + + # Close the manager + manager.click_title() + manager.keyCombo("F4") + uiutils.check(lambda: not self.app.is_running()) diff --git a/virtManager/lib/testmock.py b/virtManager/lib/testmock.py index 928ad9a3f..9349100ea 100644 --- a/virtManager/lib/testmock.py +++ b/virtManager/lib/testmock.py @@ -112,6 +112,7 @@ class CLITestOptionsClass: * fake-console-resolution: Fake viewer console resolution response. Spice doesn't return values here when we are just testing against seabios in uitests, this fakes it to hit more code paths + * fake-systray: Enable the fake systray window """ def __init__(self, test_options_str): optset = set() @@ -149,6 +150,7 @@ class CLITestOptionsClass: self.fake_systemd_success = _get("fake-systemd-success") self.fake_vnc_username = _get("fake-vnc-username") self.fake_console_resolution = _get("fake-console-resolution") + self.fake_systray = _get("fake-systray") if optset: # pragma: no cover raise RuntimeError("Unknown --test-options keys: %s" % optset) diff --git a/virtManager/systray.py b/virtManager/systray.py index 7fa9f3fdc..6f1a37a60 100644 --- a/virtManager/systray.py +++ b/virtManager/systray.py @@ -19,7 +19,7 @@ from .connmanager import vmmConnectionManager try: # pylint: disable=ungrouped-imports from gi.repository import AppIndicator3 -except Exception: +except Exception: # pragma: no cover AppIndicator3 = None @@ -48,7 +48,7 @@ def _conn_disconnect_cb(src, uri): conn.close() -def _has_appindicator_dbus(): +def _has_appindicator_dbus(): # pragma: no cover try: bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) dbus = Gio.DBusProxy.new_sync(bus, 0, None, @@ -66,7 +66,7 @@ def _has_appindicator_dbus(): _USING_APPINDICATOR = False -if AppIndicator3: +if AppIndicator3: # pragma: no cover if not _has_appindicator_dbus(): log.debug("AppIndicator3 is available, but didn't " "find any dbus watcher.") @@ -90,7 +90,7 @@ class _Systray(object): raise NotImplementedError() -class _SystrayIndicator(_Systray): +class _SystrayIndicator(_Systray): # pragma: no cover """ UI backend for appindicator """ @@ -121,7 +121,7 @@ class _SystrayIndicator(_Systray): self._icon.set_status(AppIndicator3.IndicatorStatus.PASSIVE) -class _SystrayStatusIcon(_Systray): +class _SystrayStatusIcon(_Systray): # pragma: no cover """ UI backend for Gtk StatusIcon """ @@ -153,9 +153,45 @@ class _SystrayStatusIcon(_Systray): self._icon.set_visible(False) +class _SystrayWindow(_Systray): + """ + A mock systray implementation that shows its own top level window, + so we can test more of the infrastructure in our ui tests + """ + def __init__(self): + self._window = None + self._menu = None + self._init_ui() -def _cmp(a, b): - return ((a > b) - (a < b)) + def _init_ui(self): + image = Gtk.Image() + image.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.DIALOG) + + box = Gtk.EventBox() + box.add(image) + box.connect("button-press-event", self._popup_cb) + + self._window = Gtk.Window() + self._window.set_size_request(100, 100) + self._window.get_accessible().set_name("vmm-fake-systray") + self._window.add(box) + + def is_embedded(self): + return self._window.is_visible() + + def set_menu(self, menu): + self._menu = menu + + def _popup_cb(self, src, event): + if event.button == 1: + _toggle_manager() + else: + self._menu.popup_at_pointer(event) + + def show(self): + self._window.show_all() + def hide(self): + self._window.hide() class _TrayMainMenu(vmmGObject): @@ -183,6 +219,7 @@ class _TrayMainMenu(vmmGObject): Build the top level conn list menu when clicking the icon """ menu = Gtk.Menu() + menu.get_accessible().set_name("vmm-systray-menu") menu.add(Gtk.SeparatorMenuItem()) exit_item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_QUIT, None) @@ -362,7 +399,7 @@ class vmmSystray(vmmGObject): return cls._instance @staticmethod - def systray_disabled_message(): + def systray_disabled_message(): # pragma: no cover if "WAYLAND_DISPLAY" not in os.environ: return if _USING_APPINDICATOR: @@ -389,8 +426,9 @@ class vmmSystray(vmmGObject): def _cleanup(self): self._hide_systray() self._systray = None - self._mainmenu.cleanup() - self._mainmenu = None + if self._mainmenu: + self._mainmenu.cleanup() + self._mainmenu = None ########################### @@ -398,9 +436,6 @@ class vmmSystray(vmmGObject): ########################### def _init_mainmenu(self): - if self._mainmenu: - return - self._mainmenu = _TrayMainMenu() connmanager = vmmConnectionManager.get_instance() connmanager.connect("conn-added", self._conn_added_cb) @@ -410,9 +445,11 @@ class vmmSystray(vmmGObject): def _show_systray(self): if not self._systray: - if _USING_APPINDICATOR: + if self.config.CLITestOptions.fake_systray: + self._systray = _SystrayWindow() + elif _USING_APPINDICATOR: # pragma: no cover self._systray = _SystrayIndicator() - else: + else: # pragma: no cover self._systray = _SystrayStatusIcon() self._init_mainmenu() self._systray.set_menu(self._mainmenu.get_menu())