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()