File indexing completed on 2024-05-05 17:43:16

0001 #!/usr/bin/env python3
0002 
0003 # SPDX-FileCopyrightText: 2019 The GNOME Music developers
0004 # SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0005 # SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
0006 # SPDX-License-Identifier: GPL-2.0-or-later
0007 
0008 # pylint: disable=too-many-arguments
0009 
0010 import threading
0011 import unittest
0012 from os import getcwd, getpid, path
0013 from typing import Any, Final
0014 
0015 from appium import webdriver
0016 from appium.options.common.base import AppiumOptions
0017 from appium.webdriver.common.appiumby import AppiumBy
0018 from gi.repository import Gio, GLib
0019 from selenium.webdriver.support import expected_conditions as EC
0020 from selenium.webdriver.support.ui import WebDriverWait
0021 
0022 WIDGET_ID: Final = "org.kde.plasma.mediacontroller"
0023 
0024 
0025 class Mpris2:
0026     """
0027     MPRIS2 interface implemented in GDBus, since dbus-python does not support property
0028     """
0029 
0030     BASE_IFACE: Final = "org.mpris.MediaPlayer2"
0031     OBJECT_PATH: Final = "/org/mpris/MediaPlayer2"
0032     PLAYER_IFACE: Final = GLib.Variant('s', "org.mpris.MediaPlayer2.Player")
0033     APP_INTERFACE: Final = f"org.mpris.MediaPlayer2.appiumtest.instance{str(getpid())}"
0034 
0035     __connection: Gio.DBusConnection
0036 
0037     def __init__(self) -> None:
0038         self.metadata: dict[str, GLib.Variant] = {
0039             'mpris:trackid': GLib.Variant('o', "/appiumtest/1"),
0040             'xesam:url': GLib.Variant('s', "file://" + path.join(getcwd(), "resources/media1.mp3")),
0041             'mpris:length': GLib.Variant('x', 30 * 10e6),  # ms
0042             'xesam:title': GLib.Variant('s', "Konqi ❤️️ Katie"),
0043             'xesam:album': GLib.Variant('s', "The Best of Konqi"),
0044             'xesam:artist': GLib.Variant('as', ["KDE Community", "La communauté KDE"]),
0045             "mpris:artUrl": GLib.Variant('s', "file://" + path.join(getcwd(), "resources/mediacontrollertests_art1.png"))
0046         }
0047         self.base_properties: dict[str, GLib.Variant] = {
0048             "CanQuit": GLib.Variant('b', True),
0049             "Fullscreen": GLib.Variant('b', False),
0050             "CanSetFullscreen": GLib.Variant('b', False),
0051             "CanRaise": GLib.Variant('b', True),
0052             "HasTrackList": GLib.Variant('b', True),
0053             "Identity": GLib.Variant('s', 'AppiumTest'),
0054             "DesktopEntry": GLib.Variant('s', 'appiumtest'),
0055             "SupportedUriSchemes": GLib.Variant('as', [
0056                 'file',
0057             ]),
0058             "SupportedMimeTypes": GLib.Variant('as', [
0059                 'application/ogg',
0060             ]),
0061         }
0062         self.player_properties: dict[str, GLib.Variant] = {
0063             'PlaybackStatus': GLib.Variant('s', "Stopped"),
0064             'LoopStatus': GLib.Variant('s', "None"),
0065             'Rate': GLib.Variant('d', 1.0),
0066             'Shuffle': GLib.Variant('b', False),
0067             'Metadata': GLib.Variant('a{sv}', self.metadata),
0068             'Position': GLib.Variant('x', 0),
0069             'MinimumRate': GLib.Variant('d', 1.0),
0070             'MaximumRate': GLib.Variant('d', 1.0),
0071             'Volume': GLib.Variant('d', 1.0),
0072             'CanGoNext': GLib.Variant('b', True),
0073             'CanGoPrevious': GLib.Variant('b', True),
0074             'CanPlay': GLib.Variant('b', True),
0075             'CanPause': GLib.Variant('b', True),
0076             'CanSeek': GLib.Variant('b', True),
0077             'CanControl': GLib.Variant('b', True),
0078         }
0079 
0080         self.__owner_id: int = Gio.bus_own_name(Gio.BusType.SESSION, self.APP_INTERFACE, Gio.BusNameOwnerFlags.NONE, self.on_bus_acquired, self.on_name_acquired, self.on_name_lost)
0081         assert self.__owner_id > 0
0082 
0083     def quit(self) -> None:
0084         self.stop(self.__connection, self.OBJECT_PATH)
0085         Gio.bus_unown_name(self.__owner_id)
0086         print("Player exit")
0087 
0088     def on_bus_acquired(self, connection: Gio.DBusConnection, name: str, *args) -> None:
0089         """
0090         Interface is ready, now register objects.
0091         """
0092         self.__connection = connection
0093 
0094         properties_introspection_xml: str = '\n'.join(open("../dataengines/mpris2/org.freedesktop.DBus.Properties.xml", encoding="utf-8").readlines())
0095         introspection_data = Gio.DBusNodeInfo.new_for_xml(properties_introspection_xml)
0096         reg_id = connection.register_object(self.OBJECT_PATH, introspection_data.interfaces[0], self.properties_handle_method_call, None, None)
0097 
0098         player_introspection_xml: str = '\n'.join(open("../dataengines/mpris2/org.mpris.MediaPlayer2.Player.xml", encoding="utf-8").readlines())
0099 
0100         introspection_data = Gio.DBusNodeInfo.new_for_xml(player_introspection_xml)
0101         reg_id = connection.register_object(self.OBJECT_PATH, introspection_data.interfaces[0], self.player_handle_method_call, self.player_handle_get_property, self.player_handle_set_property)
0102         assert reg_id != 0
0103 
0104         interface_introspection_xml: str = '\n'.join(open("../dataengines/mpris2/org.mpris.MediaPlayer2.xml", encoding="utf-8").readlines())
0105         introspection_data = Gio.DBusNodeInfo.new_for_xml(interface_introspection_xml)
0106         reg_id = connection.register_object(self.OBJECT_PATH, introspection_data.interfaces[0], self.interface_handle_method_call, self.interface_handle_get_property, self.interface_handle_set_property)
0107         assert reg_id != 0
0108 
0109     def on_name_acquired(self, connection, name, *args) -> None:
0110         pass
0111 
0112     def on_name_lost(self, connection, name, *args) -> None:
0113         pass
0114 
0115     def properties_handle_method_call(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, method_name: str, parameters: GLib.Variant, invocation: Gio.DBusMethodInvocation) -> None:
0116         """
0117         Handles method calls for org.freedesktop.DBus.Properties
0118         """
0119         assert interface_name == "org.freedesktop.DBus.Properties", f"Wrong interface name {interface_name} from {sender}"
0120 
0121         if method_name == "Get":
0122             ret: Any = None
0123             interface: str = parameters[0]
0124             property_name: str = parameters[1]
0125             if interface == self.BASE_IFACE:
0126                 ret = self.base_properties[property_name]
0127             elif interface == self.PLAYER_IFACE.get_string():
0128                 ret = self.player_properties[property_name]
0129             else:
0130                 assert False, f"Unknown interface {interface}"
0131 
0132             print(f"Get: {interface} {property_name} {ret}")
0133             # https://bugzilla.gnome.org/show_bug.cgi?id=765603
0134             invocation.return_value(GLib.Variant('(v)', ret))
0135 
0136         elif method_name == "GetAll":
0137             interface: str = parameters[0]
0138             if interface == self.BASE_IFACE:
0139                 ret = GLib.Variant('a{sv}', self.base_properties)
0140             elif interface == self.PLAYER_IFACE.get_string():
0141                 ret = GLib.Variant('a{sv}', self.player_properties)
0142             else:
0143                 assert False, f"Unknown interface {interface}"
0144 
0145             print(f"GetAll: {interface} {ret}")
0146             invocation.return_value(GLib.Variant.new_tuple(ret))
0147 
0148         elif method_name == "Set":
0149             interface: str = parameters[0]
0150             property_name: str = parameters[1]
0151             value: Any = parameters[2]
0152 
0153             if interface != self.PLAYER_IFACE.get_string():
0154                 assert False, f"Wrong interface {interface}"
0155 
0156             if property_name == "Rate":
0157                 self.set_rate(value, connection, object_path)
0158             elif property_name == "LoopStatus":
0159                 self.set_repeat(value, connection, object_path)
0160             elif property_name == "Shuffle":
0161                 self.set_shuffle(value, connection, object_path)
0162             elif property_name == "Volume":
0163                 self.set_volume(value, connection, object_path)
0164             else:
0165                 assert False, f"Unknown property {property_name}"
0166 
0167             print(f"Set: {interface} {property_name} {value}")
0168 
0169         else:
0170             invocation.return_value(None)
0171 
0172     def set_playing(self, playing: bool, connection: Gio.DBusConnection, object_path: str) -> None:
0173         """
0174         Changes the playing state
0175         """
0176         self.player_properties["PlaybackStatus"] = GLib.Variant('s', "Playing" if playing else "Paused")
0177         changed_properties = GLib.Variant('a{sv}', {
0178             "PlaybackStatus": self.player_properties["PlaybackStatus"],
0179         })
0180         Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0181 
0182     def stop(self, connection: Gio.DBusConnection, object_path: str) -> None:
0183         """
0184         Changes the playing state
0185         """
0186         self.player_properties["PlaybackStatus"] = GLib.Variant('s', "Stopped")
0187         self.player_properties["Metadata"] = GLib.Variant('a{sv}', {})
0188         changed_properties = GLib.Variant('a{sv}', {
0189             "PlaybackStatus": self.player_properties["PlaybackStatus"],
0190             "Metadata": self.player_properties["Metadata"],
0191         })
0192         Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0193 
0194     def set_rate(self, rate: float, connection: Gio.DBusConnection, object_path: str) -> None:
0195         """
0196         Changes the shuffle state
0197         """
0198         assert isinstance(rate, float), f"argument is not a float but {type(rate)}"
0199         assert self.player_properties["MinimumRate"].get_double() <= rate <= self.player_properties["MaximumRate"].get_double(), f"Rate {rate} is out of bounds"
0200         self.player_properties["Rate"] = GLib.Variant('d', rate)
0201         changed_properties = GLib.Variant('a{sv}', {
0202             "Rate": self.player_properties["Rate"],
0203         })
0204         Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0205 
0206     def set_shuffle(self, shuffle: bool, connection: Gio.DBusConnection, object_path: str) -> None:
0207         """
0208         Changes the shuffle state
0209         """
0210         assert isinstance(shuffle, bool), f"argument is not a boolean but {type(shuffle)}"
0211         self.player_properties["Shuffle"] = GLib.Variant('b', shuffle)
0212         changed_properties = GLib.Variant('a{sv}', {
0213             "Shuffle": self.player_properties["Shuffle"],
0214         })
0215         Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0216 
0217     def set_repeat(self, repeat: str, connection: Gio.DBusConnection, object_path: str) -> None:
0218         """
0219         Changes the loop state
0220         """
0221         assert isinstance(repeat, str), f"argument is not a string but {type(repeat)}"
0222         self.player_properties["LoopStatus"] = GLib.Variant('s', repeat)
0223         changed_properties = GLib.Variant('a{sv}', {
0224             "LoopStatus": self.player_properties["LoopStatus"],
0225         })
0226         Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0227 
0228     def set_volume(self, volume: float, connection: Gio.DBusConnection, object_path: str) -> None:
0229         """
0230         Adjusts the volume
0231         """
0232         assert isinstance(volume, float) and 0 <= volume <= 1, f"Invalid volume {volume} of type {type(volume)}"
0233         self.player_properties["Volume"] = GLib.Variant('d', volume)
0234         changed_properties = GLib.Variant('a{sv}', {
0235             "Volume": self.player_properties["Volume"],
0236         })
0237         Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0238 
0239     def player_handle_method_call(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, method_name: str, parameters: GLib.Variant, invocation: Gio.DBusMethodInvocation) -> None:
0240         """
0241         Handles method calls for org.mpris.MediaPlayer2.Player
0242         """
0243         assert interface_name == "org.mpris.MediaPlayer2.Player", f"Wrong interface name {interface_name} from {sender}"
0244 
0245         print(f"player_handle_method_call method_name: {method_name}")
0246 
0247         if method_name == "Next":
0248             self.metadata = {
0249                 'mpris:trackid': GLib.Variant('o', "/appiumtest/2"),
0250                 'xesam:url': GLib.Variant('s', "file://" + path.join(getcwd(), "resources/media2.mp3")),
0251                 'mpris:length': GLib.Variant('x', 90 * 10e6),  # ms
0252                 'xesam:title': GLib.Variant('s', "Konqi's Favorite"),
0253                 'xesam:album': GLib.Variant('s', "The Best of Konqi"),
0254                 'xesam:artist': GLib.Variant('as', ["KDE Community", "KDE 社区"]),
0255                 "mpris:artUrl": GLib.Variant('s', "file://" + path.join(getcwd(), "resources/mediacontrollertests_art2.png"))
0256             }
0257             self.player_properties["Metadata"] = GLib.Variant('a{sv}', self.metadata)
0258             self.player_properties["CanGoPrevious"] = GLib.Variant('b', True)
0259             self.player_properties["CanGoNext"] = GLib.Variant('b', False)
0260             changed_properties = GLib.Variant('a{sv}', {
0261                 'Metadata': self.player_properties["Metadata"],
0262                 "CanGoPrevious": self.player_properties["CanGoPrevious"],
0263                 "CanGoNext": self.player_properties["CanGoNext"],
0264             })
0265             Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0266 
0267         elif method_name == "Previous":
0268             self.metadata = {
0269                 'mpris:trackid': GLib.Variant('o', "/appiumtest/2"),
0270                 'xesam:url': GLib.Variant('s', "file://" + path.join(getcwd(), "resources/media0.mp3")),
0271                 'mpris:length': GLib.Variant('x', 60 * 10e6),  # ms
0272                 'xesam:title': GLib.Variant('s', "Katie's Favorite"),
0273                 'xesam:album': GLib.Variant('s', "The Best of Konqi"),
0274                 'xesam:artist': GLib.Variant('as', ["KDE Community"]),
0275                 "mpris:artUrl": GLib.Variant('s', "file://" + path.join(getcwd(), "resources/mediacontrollertests_art0.png"))
0276             }
0277             self.player_properties["Metadata"] = GLib.Variant('a{sv}', self.metadata)
0278             self.player_properties["CanGoPrevious"] = GLib.Variant('b', False)
0279             self.player_properties["CanGoNext"] = GLib.Variant('b', True)
0280             changed_properties = GLib.Variant('a{sv}', {
0281                 'Metadata': self.player_properties["Metadata"],
0282                 "CanGoPrevious": self.player_properties["CanGoPrevious"],
0283                 "CanGoNext": self.player_properties["CanGoNext"],
0284             })
0285             Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0286 
0287         elif method_name == "Pause":
0288             self.set_playing(False, connection, object_path)
0289 
0290         elif method_name == "PlayPause":
0291             self.set_playing(self.player_properties["PlaybackStatus"] != "Playing", connection, object_path)
0292 
0293         elif method_name == "Stop":
0294             self.stop(connection, object_path)
0295 
0296         elif method_name == "Play":
0297             self.set_playing(True, connection, object_path)
0298 
0299         elif method_name == "Seek":
0300             offset: int = parameters[0]
0301             length: int = int(self.metadata["mpris:length"])
0302             position: int = int(self.player_properties["Position"])
0303             assert 0 <= position + offset <= length
0304             self.player_properties["Position"] = GLib.Variant('x', position + offset)
0305             changed_properties = GLib.Variant('a{sv}', {
0306                 'Position': self.player_properties["Position"],
0307             })
0308             Gio.DBusConnection.emit_signal(connection, None, object_path, self.PLAYER_IFACE.get_string(), "Seeked", GLib.Variant.new_tuple(self.player_properties["Position"]))
0309             Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0310 
0311         elif method_name == "SetPosition":
0312             assert parameters[0] == self.metadata["mpris:trackid"].get_string(), f"expected trackid: {parameters[0]}, actual trackid: {self.metadata['mpris:trackid'].get_string()}"
0313             self.player_properties["Position"] = GLib.Variant('x', parameters[1])
0314             changed_properties = GLib.Variant('a{sv}', {
0315                 'Position': self.player_properties["Position"],
0316             })
0317             Gio.DBusConnection.emit_signal(connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(self.PLAYER_IFACE, changed_properties, GLib.Variant('as', ())))
0318 
0319         elif method_name == "OpenUri":
0320             print("OpenUri")
0321 
0322         else:
0323             # In case the interface adds new methods, fail here for easier discovery
0324             assert False, f"{method_name} does not exist"
0325 
0326     def player_handle_get_property(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, value: Any):
0327         """
0328         Handles properties for org.mpris.MediaPlayer2.Player
0329         """
0330         assert interface_name == "org.mpris.MediaPlayer2.Player", f"Wrong interface name {interface_name} from {sender}"
0331         assert value in self.player_properties.keys(), f"{value} does not exist"
0332 
0333         return self.player_properties[value]
0334 
0335     def player_handle_set_property(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, key: str, value: Any) -> bool:
0336         """
0337         Handles properties for org.mpris.MediaPlayer2.Player
0338         """
0339         assert interface_name == "org.mpris.MediaPlayer2.Player", f"Wrong interface name {interface_name} from {sender}"
0340         assert key in self.player_properties.keys(), f"{key} does not exist"
0341 
0342         print(f"player_handle_set_property key: {key}, value: {value}")
0343 
0344         if key == "Rate":
0345             self.set_rate(value, connection, object_path)
0346         elif key == "LoopStatus":
0347             self.set_repeat(value, connection, object_path)
0348         elif key == "Shuffle":
0349             self.set_shuffle(value, connection, object_path)
0350         elif key == "Volume":
0351             self.set_volume(value, connection, object_path)
0352         else:
0353             assert False
0354 
0355         # What is the correct thing to return here on success?  It appears that
0356         # we need to return something other than None or what would be evaluated
0357         # to False for this call back to be successful.
0358         return True
0359 
0360     def interface_handle_method_call(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, method_name: str, parameters: GLib.Variant, invocation: Gio.DBusMethodInvocation) -> None:
0361         """
0362         Handles method calls for org.mpris.MediaPlayer2
0363         """
0364         assert interface_name == "org.mpris.MediaPlayer2", f"Wrong interface name {interface_name} from {sender}"
0365 
0366         if method_name == "Raise":
0367             print("Raise")
0368         elif method_name == "Quit":
0369             print("Quit")
0370         else:
0371             assert False, f"method f{method_name} does not exist"
0372 
0373     def interface_handle_get_property(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, value: Any):
0374         """
0375         Handles properties for org.mpris.MediaPlayer2
0376         """
0377         assert interface_name == "org.mpris.MediaPlayer2", f"Wrong interface name {interface_name} from {sender}"
0378         assert value in self.base_properties.keys(), f"{value} does not exist"
0379 
0380         return self.base_properties[value]
0381 
0382     def interface_handle_set_property(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, key: str, value: Any) -> bool:
0383         """
0384         Handles properties for org.mpris.MediaPlayer2
0385         """
0386         assert interface_name == "org.mpris.MediaPlayer2", f"Wrong interface name {interface_name} from {sender}"
0387         assert key in self.base_properties.keys(), f"{key} does not exist"
0388 
0389         return True
0390 
0391 
0392 class GlibMainloopThread(threading.Thread):
0393     """
0394     Runs Glib main loop in another thread
0395     """
0396 
0397     def __init__(self) -> None:
0398         # Set up DBus loop
0399         self.loop = GLib.MainLoop()
0400         self.timer = threading.Timer(30, self.loop.quit)
0401 
0402         # Create the thread
0403         super(GlibMainloopThread, self).__init__()
0404 
0405     def run(self) -> None:
0406         """
0407         Method to run the DBus main loop (on a thread)
0408         """
0409         self.timer.start()
0410         self.loop.run()
0411 
0412     def quit(self) -> None:
0413         self.timer.cancel()
0414         self.loop.quit()
0415 
0416 
0417 class MediaControllerTests(unittest.TestCase):
0418     """
0419     Tests for the media controller widget
0420     """
0421 
0422     driver: webdriver.Remote
0423     mpris_interface: Mpris2 | None
0424     loopThread: GlibMainloopThread
0425 
0426     @classmethod
0427     def setUpClass(cls) -> None:
0428         """
0429         Opens the widget and initialize the webdriver
0430         """
0431         options = AppiumOptions()
0432         options.set_capability("app", f"plasmawindowed -p org.kde.plasma.nano {WIDGET_ID}")
0433         options.set_capability("timeouts", {'implicit': 10000})
0434         cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
0435 
0436         cls.loopThread = GlibMainloopThread()
0437         cls.loopThread.start()
0438 
0439     @classmethod
0440     def tearDownClass(cls) -> None:
0441         cls.loopThread.quit()
0442 
0443     def setUp(self) -> None:
0444         self.mpris_interface = Mpris2()
0445 
0446     def tearDown(self) -> None:
0447         self.mpris_interface.quit()
0448         self.mpris_interface = None
0449         self.driver.find_element(by=AppiumBy.NAME, value="No media playing")
0450 
0451     def test_track(self) -> None:
0452         """
0453         Tests the widget can show track metadata
0454         """
0455         play_button = self.driver.find_element(by=AppiumBy.NAME, value="Play")
0456         previous_button = self.driver.find_element(by=AppiumBy.NAME, value="Previous Track")
0457         next_button = self.driver.find_element(by=AppiumBy.NAME, value="Next Track")
0458         shuffle_button = self.driver.find_element(by=AppiumBy.NAME, value="Shuffle")
0459         repeat_button = self.driver.find_element(by=AppiumBy.NAME, value="Repeat")
0460 
0461         # Match song title, artist and album
0462         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata["xesam:title"].get_string())  # Title
0463         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata["xesam:album"].get_string())  # Album
0464         self.driver.find_element(by=AppiumBy.NAME, value="0:00")  # Current position
0465         self.driver.find_element(by=AppiumBy.NAME, value="-5:00")  # Remaining time
0466         self.driver.find_element(by=AppiumBy.NAME, value=', '.join(self.mpris_interface.metadata["xesam:artist"].unpack()))  # Artists
0467 
0468         # Now click the play button
0469         wait: WebDriverWait = WebDriverWait(self.driver, 5)
0470         play_button.click()
0471         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Pause")))
0472         play_button.click()
0473         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Play")))
0474 
0475         # Now click the shuffle button
0476         shuffle_button.click()
0477         wait.until(lambda _: self.mpris_interface.player_properties["Shuffle"].get_boolean())
0478         # Click again to disable shuffle
0479         shuffle_button.click()
0480         wait.until(lambda _: not self.mpris_interface.player_properties["Shuffle"].get_boolean())
0481 
0482         # Now click the repeat button
0483         repeat_button.click()
0484         wait.until(lambda _: self.mpris_interface.player_properties["LoopStatus"].get_string() == "Playlist")
0485         # Click again to switch to Track mode
0486         repeat_button.click()
0487         wait.until(lambda _: self.mpris_interface.player_properties["LoopStatus"].get_string() == "Track")
0488         # Click again to disable repeat
0489         repeat_button.click()
0490         wait.until(lambda _: self.mpris_interface.player_properties["LoopStatus"].get_string() == "None")
0491 
0492         # Switch to the previous song, and match again
0493         previous_button.click()
0494         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Katie's Favorite")))
0495         self.assertFalse(previous_button.is_enabled())
0496         self.assertTrue(next_button.is_enabled())
0497         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata["xesam:title"].get_string())
0498         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata["xesam:album"].get_string())
0499         self.driver.find_element(by=AppiumBy.NAME, value="0:00")
0500         self.driver.find_element(by=AppiumBy.NAME, value="-10:00")
0501         self.driver.find_element(by=AppiumBy.NAME, value=', '.join(self.mpris_interface.metadata["xesam:artist"].unpack()))
0502 
0503         # Switch to the next song, and match again
0504         next_button.click()
0505         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Konqi's Favorite")))
0506         self.assertTrue(previous_button.is_enabled())
0507         self.assertFalse(next_button.is_enabled())
0508         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata["xesam:title"].get_string())
0509         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata["xesam:album"].get_string())
0510         self.driver.find_element(by=AppiumBy.NAME, value="0:00")
0511         self.driver.find_element(by=AppiumBy.NAME, value="-15:00")
0512         self.driver.find_element(by=AppiumBy.NAME, value=', '.join(self.mpris_interface.metadata["xesam:artist"].unpack()))
0513 
0514 
0515 if __name__ == '__main__':
0516     unittest.main()