File indexing completed on 2024-05-12 05:35:22

0001 #!/usr/bin/env python3
0002 
0003 # SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
0004 # SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
0005 # SPDX-License-Identifier: GPL-2.0-or-later
0006 
0007 import functools
0008 import os
0009 import pathlib
0010 import stat
0011 import subprocess
0012 import sys
0013 import time
0014 import unittest
0015 from typing import Final
0016 
0017 from appium import webdriver
0018 from appium.options.common.base import AppiumOptions
0019 from appium.webdriver.common.appiumby import AppiumBy
0020 from appium.webdriver.webelement import WebElement
0021 from gi.repository import Gio, GLib
0022 from selenium.webdriver.common.action_chains import ActionChains
0023 from selenium.webdriver.common.keys import Keys
0024 from selenium.webdriver.support import expected_conditions as EC
0025 from selenium.webdriver.support.ui import WebDriverWait
0026 
0027 CMAKE_BINARY_DIR: Final = os.environ.get("CMAKE_BINARY_DIR", os.path.join(pathlib.Path.home(), "kde/build/plasma-desktop/bin"))
0028 KACTIVITYMANAGERD_PATH: Final = os.environ.get("KACTIVITYMANAGERD_PATH", os.path.join(pathlib.Path.home(), "kde/usr/lib64/libexec/kactivitymanagerd"))
0029 KACTIVITYMANAGERD_SERVICE_NAME: Final = "org.kde.ActivityManager"
0030 KDE_VERSION: Final = 6
0031 
0032 
0033 def name_has_owner(session_bus: Gio.DBusConnection, name: str) -> bool:
0034     """
0035     Whether the given name is available on session bus
0036     """
0037     message: Gio.DBusMessage = Gio.DBusMessage.new_method_call("org.freedesktop.DBus", "/", "org.freedesktop.DBus", "NameHasOwner")
0038     message.set_body(GLib.Variant("(s)", [name]))
0039     reply, _ = session_bus.send_message_with_reply_sync(message, Gio.DBusSendMessageFlags.NONE, 1000)
0040     return reply and reply.get_signature() == 'b' and reply.get_body().get_child_value(0).get_boolean()
0041 
0042 
0043 def start_kactivitymanagerd() -> subprocess.Popen | None:
0044     session_bus: Gio.DBusConnection = Gio.bus_get_sync(Gio.BusType.SESSION)
0045     kactivitymanagerd = None
0046     if not name_has_owner(session_bus, KACTIVITYMANAGERD_SERVICE_NAME):
0047         kactivitymanagerd = subprocess.Popen([KACTIVITYMANAGERD_PATH], stdout=sys.stderr, stderr=sys.stderr)
0048         kactivitymanagerd_started: bool = False
0049         for _ in range(10):
0050             if name_has_owner(session_bus, KACTIVITYMANAGERD_SERVICE_NAME):
0051                 kactivitymanagerd_started = True
0052                 break
0053             print("waiting for kactivitymanagerd to appear on the DBus session")
0054             time.sleep(1)
0055         assert kactivitymanagerd_started
0056 
0057     return kactivitymanagerd
0058 
0059 
0060 def start_kded() -> subprocess.Popen | None:
0061     session_bus: Gio.DBusConnection = Gio.bus_get_sync(Gio.BusType.SESSION)
0062     kded = None
0063     if not name_has_owner(session_bus, f"org.kde.kded{KDE_VERSION}"):
0064         kded = subprocess.Popen([f"kded{KDE_VERSION}"], stdout=sys.stderr, stderr=sys.stderr)
0065         kded_started: bool = False
0066         for _ in range(10):
0067             if name_has_owner(session_bus, f"org.kde.kded{KDE_VERSION}"):
0068                 kded_started = True
0069                 break
0070             print(f"waiting for kded{KDE_VERSION} to appear on the dbus session")
0071             time.sleep(1)
0072         assert kded_started
0073 
0074     return kded
0075 
0076 
0077 def start_plasmashell() -> tuple:
0078     """
0079     Launches plashashell and returns the subprocess instances
0080     """
0081     kactivitymanagerd = start_kactivitymanagerd()
0082     kded = start_kded()
0083     plasmashell = subprocess.Popen(["plasmashell", "-p", "org.kde.plasma.desktop", "--no-respawn"], stdout=sys.stderr, stderr=sys.stderr)
0084 
0085     return (kactivitymanagerd, kded, plasmashell)
0086 
0087 
0088 class DesktopTest(unittest.TestCase):
0089     """
0090     Tests for the desktop package
0091     """
0092 
0093     driver: webdriver.Remote
0094     kactivitymanagerd: subprocess.Popen | None = None
0095     kded: subprocess.Popen | None = None
0096     plasmashell: subprocess.Popen | None = None
0097 
0098     @classmethod
0099     def setUpClass(cls) -> None:
0100         """
0101         Initializes the webdriver
0102         """
0103         cls.kactivitymanagerd, cls.kded, cls.plasmashell = start_plasmashell()
0104         options = AppiumOptions()
0105         options.set_capability("app", "Root")
0106         options.set_capability("timeouts", {'implicit': 30000})
0107         cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
0108 
0109     def tearDown(self) -> None:
0110         """
0111         Take screenshot when the current test fails
0112         """
0113         if not self._outcome.result.wasSuccessful():
0114             self.driver.get_screenshot_as_file(f"failed_test_shot_plasmashell_#{self.id()}.png")
0115 
0116     @classmethod
0117     def tearDownClass(cls) -> None:
0118         """
0119         Make sure to terminate the driver again, lest it dangles.
0120         """
0121         if cls.plasmashell is not None:
0122             subprocess.check_output(["kquitapp6", "plasmashell"], stderr=sys.stderr)
0123             assert cls.plasmashell.wait(30) == 0, cls.plasmashell.returncode
0124 
0125         if cls.kded:
0126             cls.kded.kill()
0127         if cls.kactivitymanagerd:
0128             cls.kactivitymanagerd.kill()
0129         cls.driver.quit()
0130 
0131     def _exit_edit_mode(self) -> None:
0132         """
0133         Finds the close button and clicks it
0134         """
0135         global_theme_button = self.driver.find_element(AppiumBy.NAME, "Choose Global Theme…")
0136         self.driver.find_element(AppiumBy.NAME, "Exit Edit Mode").click()
0137         WebDriverWait(self.driver, 30).until_not(lambda _: global_theme_button.is_displayed())
0138 
0139     def _open_containment_config_dialog(self) -> None:
0140         # Alt+D, S
0141         ActionChains(self.driver).key_down(Keys.ALT).send_keys("d").pause(0.5).send_keys("s").key_up(Keys.ALT).perform()
0142         WebDriverWait(self.driver, 30).until(EC.presence_of_element_located((AppiumBy.NAME, "Wallpaper type:")))
0143 
0144     def test_0_panel_ready(self) -> None:
0145         """
0146         Waits until the panel is ready
0147         """
0148         wait = WebDriverWait(self.driver, 30)
0149         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Application Launcher")))
0150 
0151     def test_1_containment_config_dialog_2_add_new_wallpaper(self) -> None:
0152         """
0153         Tests if the file dialog is opened successfully
0154         @see https://invent.kde.org/plasma/plasma-integration/-/merge_requests/117
0155         """
0156         self._open_containment_config_dialog()
0157         self.driver.find_element(AppiumBy.NAME, "Add…").click()
0158         wait = WebDriverWait(self.driver, 30)
0159         title_element: WebElement = wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Open Image")))
0160 
0161         ActionChains(self.driver).send_keys(Keys.ESCAPE).perform()
0162         wait.until_not(lambda _: title_element.is_displayed())
0163 
0164     def test_1_containment_config_dialog_3_other_sections(self) -> None:
0165         """
0166         Opens other sections successively and matches text to make sure there is no breaking QML error
0167         """
0168         self._open_containment_config_dialog()
0169         wait = WebDriverWait(self.driver, 30)
0170         mouseaction_element: WebElement = wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Mouse Actions")))
0171         location_element = wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Location")))
0172         icons_element = wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Icons")))
0173         filter_element = wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Filter")))
0174 
0175         mouseaction_element.click()
0176         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Add Action")))
0177 
0178         location_element.click()
0179         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Desktop folder")))
0180 
0181         icons_element.click()
0182         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Configure Preview Plugins…"))).click()
0183         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Preview Plugins")))
0184 
0185         filter_element.click()
0186         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "File name pattern:")))
0187 
0188         ActionChains(self.driver).send_keys(Keys.ESCAPE).perform()
0189         wait.until_not(lambda _: mouseaction_element.is_displayed())
0190 
0191     def test_3_open_panel_edit_mode(self) -> None:
0192         """
0193         Tests the edit mode toolbox can be loaded
0194         Consolidates https://invent.kde.org/frameworks/plasma-framework/-/commit/3bb099a427cacd44fef7668225d8094f952dd5b2
0195         """
0196         # Alt+D, E
0197         actions = ActionChains(self.driver)
0198         actions.key_down(Keys.ALT).send_keys("d").key_up(Keys.ALT).perform()
0199         actions.send_keys("e").perform()
0200 
0201         wait = WebDriverWait(self.driver, 30)
0202         self.driver.find_element(AppiumBy.NAME, "Configure Panel…").click()
0203         widget_button: WebElement = wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Add Widgets…")))
0204         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Add Spacer")))
0205 
0206         actions.send_keys(Keys.ESCAPE).perform()
0207         wait.until_not(lambda _: widget_button.is_displayed())
0208 
0209         self._exit_edit_mode()
0210 
0211     def test_5_bug477185_meta_number_shortcut(self) -> None:
0212         """
0213         Meta+1 should activate the first launcher item
0214         """
0215         # Prepare a desktop file
0216         os.makedirs(os.path.join(GLib.get_user_data_dir(), "applications"))
0217         desktopfile_path = os.path.join(GLib.get_user_data_dir(), "applications", "org.kde.testwindow.desktop")
0218         with open(desktopfile_path, "w", encoding="utf-8") as file_handler:
0219             file_handler.writelines([
0220                 "[Desktop Entry]\n",
0221                 "Type=Application\n",
0222                 "Icon=preferences-system\n",
0223                 "Name=Software Center\n",
0224                 f"Exec=python3 {os.path.join(os.getcwd(), 'resources', 'org.kde.testwindow.py')}\n",
0225             ])
0226             file_handler.flush()
0227         os.chmod(desktopfile_path, os.stat(desktopfile_path).st_mode | stat.S_IEXEC)
0228         self.addCleanup(functools.partial(os.remove, desktopfile_path))
0229 
0230         session_bus = Gio.bus_get_sync(Gio.BusType.SESSION)
0231 
0232         # Add a new launcher item
0233         message: Gio.DBusMessage = Gio.DBusMessage.new_method_call("org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell", "evaluateScript")
0234         message.set_body(GLib.Variant("(s)", [f"panels().forEach(containment => containment.widgets('org.kde.plasma.icontasks').forEach(widget => {{widget.currentConfigGroup = ['General'];widget.writeConfig('launchers', 'file://{desktopfile_path}');}}))"]))
0235         session_bus.send_message_with_reply_sync(message, Gio.DBusSendMessageFlags.NONE, 1000)
0236 
0237         wait = WebDriverWait(self.driver, 30)
0238         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Software Center")))
0239 
0240         # Activate the first launcher
0241         # ActionChains(self.driver).key_down(Keys.META).send_keys("1").key_up(Keys.META) # FIXME Meta modifier doesn't work
0242         message = Gio.DBusMessage.new_method_call("org.kde.kglobalaccel", "/component/plasmashell", "org.kde.kglobalaccel.Component", "invokeShortcut")
0243         message.set_body(GLib.Variant("(s)", ["activate task manager entry 1"]))
0244         session_bus.send_message_with_reply_sync(message, Gio.DBusSendMessageFlags.NONE, 1000)
0245         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Install Software")))
0246 
0247     def test_6_sentry_3516_load_layout(self) -> None:
0248         """
0249         ShellCorona::loadLookAndFeelDefaultLayout -> ShellCorona::unload() -> qDeleteAll(panelViews) -> QWindow::visibleChanged -> rectNotify() -> 💣
0250         @see https://bugreports.qt.io/browse/QTBUG-118841
0251         """
0252         kickoff_element = self.driver.find_element(AppiumBy.NAME, "Application Launcher")
0253         session_bus = Gio.bus_get_sync(Gio.BusType.SESSION)
0254         # LookAndFeelManager::save
0255         message: Gio.DBusMessage = Gio.DBusMessage.new_method_call("org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell", "loadLookAndFeelDefaultLayout")
0256         message.set_body(GLib.Variant("(s)", ["Breeze Dark"]))
0257         session_bus.send_message_with_reply_sync(message, Gio.DBusSendMessageFlags.NONE, 5000)
0258         self.assertFalse(kickoff_element.is_displayed())
0259         self.driver.find_element(AppiumBy.NAME, "Application Launcher")
0260 
0261     def test_7_bug_query_accent_color_binding_loop(self) -> None:
0262         """
0263         Don't use binding as usedInAccentColor may be disabled immediately after a query from kcm_colors.
0264 
0265         The call order is:
0266         1. desktop.usedInAccentColor: true -> wallpaperColors.active: true
0267         2. Kirigami.ImageColors.update()
0268         3. Kirigami.ImageColors emits paletteChanged()
0269         5. If binding is used, ShellCorona::colorChanged is emitted immediately in the same context after desktop.accentColor is updated
0270            -> desktop.usedInAccentColor: false -> desktop.accentColor: "transparent" (binding restoration).
0271         6. The second time querying the accent color, the QML engine will report binding loop detected in desktop.usedInAccentColor,
0272            and desktop.accentColor will return "transparent" directly before any accent color from the wallpaper is extracted.
0273         """
0274         session_bus = Gio.bus_get_sync(Gio.BusType.SESSION)
0275         # Send the same request twice to check binding loop
0276         for i in range(2):
0277             print(f"sending message {i}", file=sys.stderr)
0278             message: Gio.DBusMessage = Gio.DBusMessage.new_method_call("org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell", "color")
0279             reply, _ = session_bus.send_message_with_reply_sync(message, Gio.DBusSendMessageFlags.NONE, 1000)
0280             self.assertEqual(reply.get_signature(), "u")
0281             self.assertGreater(reply.get_body().get_child_value(0).get_uint32(), 0)
0282             print("done sending message", file=sys.stderr)
0283 
0284 
0285 if __name__ == '__main__':
0286     unittest.main()