File indexing completed on 2024-05-12 09:39:31

0001 #!/usr/bin/env python3
0002 
0003 # SPDX-FileCopyrightText: 2022 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 # pylint: disable=too-many-arguments
0008 
0009 import json
0010 import subprocess
0011 import unittest
0012 from os import getcwd, path
0013 from tempfile import NamedTemporaryFile
0014 from time import sleep
0015 from typing import Final
0016 
0017 import gi
0018 from appium import webdriver
0019 from appium.options.common.base import AppiumOptions
0020 from appium.webdriver.common.appiumby import AppiumBy
0021 from selenium.webdriver.common.actions.action_builder import ActionBuilder
0022 from selenium.webdriver.common.actions.interaction import POINTER_TOUCH
0023 from selenium.webdriver.common.actions.pointer_input import PointerInput
0024 from selenium.webdriver.remote.webelement import WebElement
0025 from selenium.webdriver.support import expected_conditions as EC
0026 from selenium.webdriver.support.ui import WebDriverWait
0027 from utils.GLibMainLoopThread import GLibMainLoopThread
0028 from utils.mediaplayer import (InvalidMpris2, Mpris2, read_base_properties, read_player_metadata, read_player_properties)
0029 
0030 gi.require_version('Gtk', '4.0')
0031 from gi.repository import Gtk, Gio, GLib
0032 
0033 WIDGET_ID: Final = "org.kde.plasma.mediacontroller"
0034 
0035 
0036 class MediaControllerTests(unittest.TestCase):
0037     """
0038     Tests for the media controller widget
0039     """
0040 
0041     driver: webdriver.Remote
0042     loop_thread: GLibMainLoopThread
0043     mpris_interface: Mpris2 | None
0044     player_b: subprocess.Popen | None = None
0045     player_browser: subprocess.Popen | None = None
0046     player_plasma_browser_integration: subprocess.Popen | None = None
0047 
0048     @classmethod
0049     def setUpClass(cls) -> None:
0050         """
0051         Opens the widget and initialize the webdriver
0052         """
0053         cls.loop_thread = GLibMainLoopThread()
0054         cls.loop_thread.start()
0055 
0056         options = AppiumOptions()
0057         options.set_capability("app", f"plasmawindowed -p org.kde.plasma.nano {WIDGET_ID}")
0058         options.set_capability("environ", {
0059             "QT_FATAL_WARNINGS": "1",
0060             "QT_LOGGING_RULES": "qt.accessibility.atspi.warning=false;kf.plasma.core.warning=false;kf.windowsystem.warning=false;kf.kirigami.platform.warning=false",
0061         })
0062         options.set_capability("timeouts", {'implicit': 10000})
0063         cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
0064 
0065     def setUp(self) -> None:
0066         json_path: str = path.join(getcwd(), "resources/player_a.json")
0067         with open(json_path, "r", encoding="utf-8") as f:
0068             json_dict: dict[str, list | dict] = json.load(f)
0069         metadata: list[dict[str, GLib.Variant]] = read_player_metadata(json_dict)
0070         base_properties: dict[str, GLib.Variant] = read_base_properties(json_dict)
0071         current_index: int = 1
0072         player_properties: dict[str, GLib.Variant] = read_player_properties(json_dict, metadata[current_index])
0073 
0074         self.mpris_interface = Mpris2(metadata, base_properties, player_properties, current_index)
0075         assert self.mpris_interface.registered_event.wait(10)
0076 
0077     def tearDown(self) -> None:
0078         if not self._outcome.result.wasSuccessful():
0079             self.driver.get_screenshot_as_file(f"failed_test_shot_{WIDGET_ID}_#{self.id()}.png")
0080         if self.mpris_interface is not None:
0081             self.mpris_interface.quit()
0082             self.mpris_interface = None
0083         WebDriverWait(self.driver, 5, 0.2).until(EC.presence_of_element_located((AppiumBy.NAME, "No media playing")))
0084 
0085     @classmethod
0086     def tearDownClass(cls) -> None:
0087         """
0088         Make sure to terminate the driver again, lest it dangles.
0089         """
0090         cls.loop_thread.quit()
0091         cls.driver.quit()
0092 
0093     def test_track(self) -> None:
0094         """
0095         Tests the widget can show track metadata
0096         """
0097         assert self.mpris_interface
0098         play_button = self.driver.find_element(by=AppiumBy.NAME, value="Play")
0099         previous_button = self.driver.find_element(by=AppiumBy.NAME, value="Previous Track")
0100         next_button = self.driver.find_element(by=AppiumBy.NAME, value="Next Track")
0101         shuffle_button = self.driver.find_element(by=AppiumBy.NAME, value="Shuffle")
0102         repeat_button = self.driver.find_element(by=AppiumBy.NAME, value="Repeat")
0103 
0104         # Match song title, artist and album
0105         wait: WebDriverWait = WebDriverWait(self.driver, 5)
0106         wait.until(EC.presence_of_element_located((AppiumBy.NAME, self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())))  # Title
0107         wait.until(EC.presence_of_element_located((AppiumBy.NAME, self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:album"].get_string())))  # Album
0108         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "0:00")))  # Current position
0109         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "-5:00")))  # Remaining time
0110         wait.until(EC.presence_of_element_located((AppiumBy.NAME, ', '.join(self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:artist"].unpack()))))  # Artists
0111 
0112         # Now click the play button
0113         play_button.click()
0114         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Pause")))
0115         play_button.click()
0116         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Play")))
0117 
0118         # Now click the shuffle button
0119         shuffle_button.click()
0120         wait.until(lambda _: self.mpris_interface.player_properties["Shuffle"].get_boolean())
0121         # Click again to disable shuffle
0122         shuffle_button.click()
0123         wait.until(lambda _: not self.mpris_interface.player_properties["Shuffle"].get_boolean())
0124 
0125         # Now click the repeat button
0126         repeat_button.click()
0127         wait.until(lambda _: self.mpris_interface.player_properties["LoopStatus"].get_string() == "Playlist")
0128         # Click again to switch to Track mode
0129         repeat_button.click()
0130         wait.until(lambda _: self.mpris_interface.player_properties["LoopStatus"].get_string() == "Track")
0131         # Click again to disable repeat
0132         repeat_button.click()
0133         wait.until(lambda _: self.mpris_interface.player_properties["LoopStatus"].get_string() == "None")
0134 
0135         # Switch to the previous song, and match again
0136         previous_button.click()
0137         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Katie's Favorite")))
0138         self.assertFalse(previous_button.is_enabled())
0139         self.assertTrue(next_button.is_enabled())
0140         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())
0141         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:album"].get_string())
0142         self.driver.find_element(by=AppiumBy.NAME, value="0:00")
0143         self.driver.find_element(by=AppiumBy.NAME, value="-10:00")
0144         self.driver.find_element(by=AppiumBy.NAME, value=', '.join(self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:artist"].unpack()))
0145 
0146         # Switch to the next song (need to click twice), and match again
0147         next_button.click()
0148         next_button.click()
0149         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Konqi's Favorite")))
0150         self.assertTrue(previous_button.is_enabled())
0151         self.assertFalse(next_button.is_enabled())
0152         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())
0153         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:album"].get_string())
0154         self.driver.find_element(by=AppiumBy.NAME, value="0:00")
0155         self.driver.find_element(by=AppiumBy.NAME, value="-15:00")
0156         self.driver.find_element(by=AppiumBy.NAME, value=', '.join(self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:artist"].unpack()))
0157 
0158     def test_touch_gestures(self) -> None:
0159         """
0160         Tests touch gestures like swipe up/down/left/right to adjust volume/progress
0161         @see https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/2438
0162         """
0163         assert self.mpris_interface
0164         wait: WebDriverWait = WebDriverWait(self.driver, 5)
0165         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "0:00")))  # Current position
0166         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "-5:00")))  # Remaining time
0167 
0168         # Center point of the screen
0169         geometry = Gtk.Window().get_display().get_monitors()[0].get_geometry()
0170         center_pos_x: int = int(geometry.width / 2)
0171         center_pos_y: int = int(geometry.height / 2)
0172         self.assertGreater(center_pos_x, 1)
0173         self.assertGreater(center_pos_y, 1)
0174 
0175         # Touch the window, and wait a moment to make sure the widget is ready
0176         input_source = PointerInput(POINTER_TOUCH, "finger")
0177         action = ActionBuilder(self.driver, mouse=input_source, duration=500)
0178         action.pointer_action.move_to_location(center_pos_x, center_pos_y).click().pause(1)
0179         action.perform()
0180 
0181         # Swipe right -> Position++
0182         input_source = PointerInput(POINTER_TOUCH, "finger")
0183         action = ActionBuilder(self.driver, mouse=input_source, duration=500)
0184         action.pointer_action.move_to_location(center_pos_x, center_pos_y).pointer_down().move_to_location(center_pos_x * 2, center_pos_y).pointer_up()
0185         action.perform()
0186         self.mpris_interface.connection.flush_sync(None)
0187         wait.until(lambda _: self.mpris_interface.player_properties["Position"].get_int64() > 0)
0188 
0189         # Swipe left -> Position--
0190         old_position: int = self.mpris_interface.player_properties["Position"].get_int64()
0191         input_source = PointerInput(POINTER_TOUCH, "finger")
0192         action = ActionBuilder(self.driver, mouse=input_source, duration=500)
0193         action.pointer_action.move_to_location(center_pos_x, center_pos_y).pointer_down().move_to_location(0, center_pos_y).pointer_up().pause(1)
0194         action.perform()
0195         self.mpris_interface.connection.flush_sync(None)
0196         wait.until(lambda _: self.mpris_interface.player_properties["Position"].get_int64() < old_position)
0197 
0198         # Swipe down: Volume--
0199         input_source = PointerInput(POINTER_TOUCH, "finger")
0200         action = ActionBuilder(self.driver, mouse=input_source, duration=500)
0201         action.pointer_action.move_to_location(center_pos_x, center_pos_y).pointer_down().move_to_location(center_pos_x, center_pos_y * 2).pointer_up().pause(1)
0202         action.perform()
0203         self.mpris_interface.connection.flush_sync(None)
0204         wait.until(lambda _: self.mpris_interface.player_properties["Volume"].get_double() < 1.0)
0205 
0206         # Swipe up: Volume++
0207         old_volume: float = self.mpris_interface.player_properties["Volume"].get_double()
0208         input_source = PointerInput(POINTER_TOUCH, "finger")
0209         action = ActionBuilder(self.driver, mouse=input_source, duration=500)
0210         action.pointer_action.move_to_location(center_pos_x, center_pos_y).pointer_down().move_to_location(center_pos_x, 0).pointer_up().pause(1)
0211         action.perform()
0212         self.mpris_interface.connection.flush_sync(None)
0213         wait.until(lambda _: self.mpris_interface.player_properties["Volume"].get_double() > old_volume)
0214 
0215         # Swipe down and then swipe right, only volume should change
0216         old_volume = self.mpris_interface.player_properties["Volume"].get_double()
0217         old_position = self.mpris_interface.player_properties["Position"].get_int64()
0218         input_source = PointerInput(POINTER_TOUCH, "finger")
0219         action = ActionBuilder(self.driver, mouse=input_source, duration=500)
0220         action.pointer_action.move_to_location(center_pos_x, center_pos_y).pointer_down().move_to_location(center_pos_x, center_pos_y * 2).pause(0.5).move_to_location(center_pos_x * 2, center_pos_y * 2).pointer_up().pause(1)
0221         action.perform()
0222         self.mpris_interface.connection.flush_sync(None)
0223         wait.until(lambda _: self.mpris_interface.player_properties["Volume"].get_double() < old_volume)
0224         self.assertEqual(old_position, self.mpris_interface.player_properties["Position"].get_int64())
0225 
0226         # Swipe right and then swipe up, only position should change
0227         old_volume = self.mpris_interface.player_properties["Volume"].get_double()
0228         old_position = self.mpris_interface.player_properties["Position"].get_int64()
0229         input_source = PointerInput(POINTER_TOUCH, "finger")
0230         action = ActionBuilder(self.driver, mouse=input_source, duration=500)
0231         action.pointer_action.move_to_location(center_pos_x, center_pos_y).pointer_down().move_to_location(center_pos_x * 2, center_pos_y).pause(0.5).move_to_location(center_pos_x * 2, 0).pointer_up().pause(1)
0232         action.perform()
0233         self.mpris_interface.connection.flush_sync(None)
0234         wait.until(lambda _: self.mpris_interface.player_properties["Position"].get_int64() > old_position)
0235         self.assertAlmostEqual(old_volume, self.mpris_interface.player_properties["Volume"].get_double())
0236 
0237     def _cleanup_multiplexer(self) -> None:
0238         if self.player_b:
0239             self.player_b.kill()
0240             self.player_b.wait()
0241             self.player_b = None
0242 
0243     def test_multiplexer(self) -> None:
0244         """
0245         The multiplexer should be hidden when there is only 1 player, and shows information from the active/playing player if there is one
0246         """
0247         self.addCleanup(self._cleanup_multiplexer)
0248 
0249         # Wait until the first player is ready
0250         wait: WebDriverWait = WebDriverWait(self.driver, 3)
0251         wait.until(EC.presence_of_element_located((AppiumBy.NAME, self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())))
0252 
0253         # Start Player B, Total 2 players
0254         player_b_json_path: str = path.join(getcwd(), "resources/player_b.json")
0255         self.player_b = subprocess.Popen(("python3", path.join(getcwd(), "utils/mediaplayer.py"), player_b_json_path))
0256         player_selector: WebElement = wait.until(EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "playerSelector")))
0257 
0258         # Find player tabs based on "Identity"
0259         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "AppiumTest")))
0260 
0261         # Make sure the current index does not change after a new player appears
0262         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())  # Title
0263 
0264         # Switch to Player B
0265         self.driver.find_element(by=AppiumBy.NAME, value="Audacious").click()
0266         with open(player_b_json_path, "r", encoding="utf-8") as f:
0267             player_b_metadata = read_player_metadata(json.load(f))
0268         wait.until(EC.presence_of_element_located((AppiumBy.NAME, player_b_metadata[0]["xesam:title"].get_string())))
0269         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Play")))
0270         wait.until(EC.presence_of_element_located((AppiumBy.NAME, player_b_metadata[0]["xesam:album"].get_string())))
0271         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "0:00")))
0272         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "-15:00")))
0273         wait.until(EC.presence_of_element_located((AppiumBy.NAME, ', '.join(player_b_metadata[0]["xesam:artist"].unpack()))))
0274         wait.until_not(EC.element_to_be_clickable((AppiumBy.NAME, "Next Track")))
0275         wait.until_not(EC.element_to_be_clickable((AppiumBy.NAME, "Previous Track")))
0276 
0277         # Switch to Multiplexer
0278         # A Paused, B Paused -> A (first added)
0279         self.driver.find_element(by=AppiumBy.NAME, value="Choose player automatically").click()
0280         wait.until(EC.presence_of_element_located((AppiumBy.NAME, self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())))
0281         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Play")))
0282         wait.until(EC.element_to_be_clickable((AppiumBy.NAME, "Next Track")))
0283         wait.until(EC.element_to_be_clickable((AppiumBy.NAME, "Previous Track")))
0284 
0285         # A Paused, B Playing -> B
0286         # Doc: https://lazka.github.io/pgi-docs/Gio-2.0/classes/DBusConnection.html
0287         session_bus: Gio.DBusConnection = Gio.bus_get_sync(Gio.BusType.SESSION)
0288         session_bus.call(f"org.mpris.MediaPlayer2.appiumtest.instance{str(self.player_b.pid)}", Mpris2.OBJECT_PATH, Mpris2.PLAYER_IFACE.get_string(), "Play", None, None, Gio.DBusSendMessageFlags.NONE, 1000)
0289         wait.until(EC.presence_of_element_located((AppiumBy.NAME, player_b_metadata[0]["xesam:title"].get_string())))
0290         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Pause")))
0291         wait.until_not(EC.element_to_be_clickable((AppiumBy.NAME, "Next Track")))
0292         wait.until_not(EC.element_to_be_clickable((AppiumBy.NAME, "Previous Track")))
0293 
0294         # Pause B -> Still B
0295         session_bus.call(f"org.mpris.MediaPlayer2.appiumtest.instance{str(self.player_b.pid)}", Mpris2.OBJECT_PATH, Mpris2.PLAYER_IFACE.get_string(), "Pause", None, None, Gio.DBusSendMessageFlags.NONE, 1000)
0296         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Play")))
0297         wait.until(EC.presence_of_element_located((AppiumBy.NAME, player_b_metadata[0]["xesam:title"].get_string())))
0298 
0299         # A Playing, B Paused -> A
0300         session_bus.call(self.mpris_interface.APP_INTERFACE, Mpris2.OBJECT_PATH, Mpris2.PLAYER_IFACE.get_string(), "Play", None, None, Gio.DBusSendMessageFlags.NONE, 1000)
0301         wait.until(EC.presence_of_element_located((AppiumBy.NAME, self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())))
0302         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Pause")))
0303         wait.until(EC.element_to_be_clickable((AppiumBy.NAME, "Next Track")))
0304         wait.until(EC.element_to_be_clickable((AppiumBy.NAME, "Previous Track")))
0305 
0306         # A Playing, B Playing -> Still A
0307         session_bus.call(f"org.mpris.MediaPlayer2.appiumtest.instance{str(self.player_b.pid)}", Mpris2.OBJECT_PATH, Mpris2.PLAYER_IFACE.get_string(), "Play", None, None, Gio.DBusSendMessageFlags.NONE, 1000)
0308         sleep(1)
0309         self.driver.find_element(by=AppiumBy.NAME, value=self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())  # Title
0310 
0311         # A Paused, B Playing -> B
0312         session_bus.call(self.mpris_interface.APP_INTERFACE, Mpris2.OBJECT_PATH, Mpris2.PLAYER_IFACE.get_string(), "Pause", None, None, Gio.DBusSendMessageFlags.NONE, 1000)
0313         wait.until(EC.presence_of_element_located((AppiumBy.NAME, player_b_metadata[0]["xesam:title"].get_string())))
0314         wait.until_not(EC.element_to_be_clickable((AppiumBy.NAME, "Next Track")))
0315         wait.until_not(EC.element_to_be_clickable((AppiumBy.NAME, "Previous Track")))
0316 
0317         # Close B -> A
0318         self.player_b.terminate()
0319         self.player_b.wait(10)
0320         self.player_b = None
0321         wait.until(EC.presence_of_element_located((AppiumBy.NAME, self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())))
0322         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Play")))
0323         wait.until(EC.element_to_be_clickable((AppiumBy.NAME, "Next Track")))
0324         wait.until(EC.element_to_be_clickable((AppiumBy.NAME, "Previous Track")))
0325         self.assertFalse(player_selector.is_displayed())  # Tabbar is hidden again
0326 
0327     def _cleanup_filter_plasma_browser_integration(self) -> None:
0328         """
0329         A cleanup function to be called after the test is completed
0330         """
0331         if self.player_browser:
0332             self.player_browser.terminate()
0333             self.player_browser.wait(10)
0334             self.player_browser = None
0335         if self.player_plasma_browser_integration:
0336             self.player_plasma_browser_integration.terminate()
0337             self.player_plasma_browser_integration.wait(10)
0338             self.player_plasma_browser_integration = None
0339 
0340     def test_filter_plasma_browser_integration(self) -> None:
0341         """
0342         When Plasma Browser Integration is installed, the widget should only show the player from p-b-i, and hide the player from the browser.
0343         """
0344         self.addCleanup(self._cleanup_filter_plasma_browser_integration)
0345 
0346         # Make sure the active player is not the browser so the bug can be tested
0347         wait = WebDriverWait(self.driver, 3)
0348         wait.until(EC.presence_of_element_located((AppiumBy.NAME, self.mpris_interface.metadata[self.mpris_interface.current_index]["xesam:title"].get_string())))  # Title
0349 
0350         player_browser_json_path: str = path.join(getcwd(), "resources/player_browser.json")
0351         with open(player_browser_json_path, "r", encoding="utf-8") as f:
0352             browser_json_data = json.load(f)
0353         self.player_browser = subprocess.Popen(("python3", path.join(getcwd(), "utils/mediaplayer.py"), player_browser_json_path))
0354         wait.until(EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "playerSelector")))
0355         browser_tab: WebElement = wait.until(EC.presence_of_element_located((AppiumBy.NAME, browser_json_data["base_properties"]["Identity"])))
0356         browser_tab.click()
0357         wait.until(EC.presence_of_element_located((AppiumBy.NAME, browser_json_data["metadata"][0]["xesam:title"])))
0358         wait.until(EC.presence_of_element_located((AppiumBy.NAME, browser_json_data["metadata"][0]["xesam:album"])))
0359         self.assertFalse(self.driver.find_element(by=AppiumBy.NAME, value="Next Track").is_enabled())
0360 
0361         with open(path.join(getcwd(), "resources/player_plasma_browser_integration.json"), "r", encoding="utf-8") as f:
0362             pbi_json_data = json.load(f)
0363         pbi_json_data["metadata"][0]["kde:pid"] = self.player_browser.pid  # Simulate Plasma Browser Integration
0364         with NamedTemporaryFile("w", encoding="utf-8", suffix=".json", delete=False) as temp_file:
0365             json.dump(pbi_json_data, temp_file)
0366             temp_file.flush()
0367 
0368             self.player_plasma_browser_integration = subprocess.Popen(("python3", path.join(getcwd(), "utils/mediaplayer.py"), temp_file.name))
0369             wait.until(EC.presence_of_element_located((AppiumBy.NAME, pbi_json_data["base_properties"]["Identity"]))).click()
0370             wait.until(EC.presence_of_element_located((AppiumBy.NAME, pbi_json_data["metadata"][0]["xesam:title"])))
0371             wait.until(EC.presence_of_element_located((AppiumBy.NAME, pbi_json_data["metadata"][0]["xesam:album"])))
0372             wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Play")))
0373             wait.until(EC.element_to_be_clickable((AppiumBy.NAME, "Next Track")))
0374             self.assertFalse(browser_tab.is_displayed())
0375 
0376         # When a browser starts playing a video
0377         # 1. It registers the browser MPRIS instance with PlaybackStatus: Stopped/Paused, and Mpris2FilterProxyModel has it but the active player can be others
0378         # 2. p-b-i also registers its MPRIS instance, and Mpris2FilterProxyModel filters out the Chromium MPRIS instance, so the browser MPRIS instance in Mpris2FilterProxyModel becomes invalid
0379         # 3. PlaybackStatus changes to Playing, and the container of the browser MPRIS instance emits playbackStatusChanged() signal. However, the signal should be ignored by Multiplexer (disconnect in Multiplexer::onRowsAboutToBeRemoved)
0380         session_bus: Gio.DBusConnection = Gio.bus_get_sync(Gio.BusType.SESSION)
0381         session_bus.call(f"org.mpris.MediaPlayer2.appiumtest.instance{str(self.player_browser.pid)}", Mpris2.OBJECT_PATH, Mpris2.PLAYER_IFACE.get_string(), "Play", None, None, Gio.DBusSendMessageFlags.NONE, 1000)
0382         session_bus.call(f"org.mpris.MediaPlayer2.appiumtest.instance{str(self.player_plasma_browser_integration.pid)}", Mpris2.OBJECT_PATH, Mpris2.PLAYER_IFACE.get_string(), "Play", None, None, Gio.DBusSendMessageFlags.NONE, 1000)
0383         wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Pause")))  # Confirm the backend does not crash
0384 
0385         self._cleanup_filter_plasma_browser_integration()
0386 
0387     def test_bug477144_invalid_player(self) -> None:
0388         """
0389         Do not crash when a player is invalid or its DBus interface returns any errors on initialization
0390         @see https://bugs.kde.org/show_bug.cgi?id=477144
0391         """
0392         if self.mpris_interface is not None:
0393             self.mpris_interface.quit()
0394 
0395         placeholder_element: WebElement = self.driver.find_element(AppiumBy.NAME, "No media playing")
0396         self.mpris_interface = InvalidMpris2()
0397         self.assertTrue(self.mpris_interface.registered_event.wait(10))
0398         self.assertTrue(placeholder_element.is_displayed())
0399 
0400     def test_bug477335_decode_xesam_url(self) -> None:
0401         """
0402         Make sure "xesam_url" is decoded before using it in other places like album name
0403         """
0404         if self.mpris_interface is not None:
0405             self.mpris_interface.quit()
0406 
0407         player_with_encoded_url_json_path: str = path.join(getcwd(), "resources/player_with_encoded_url.json")
0408         with subprocess.Popen(("python3", path.join(getcwd(), "utils/mediaplayer.py"), player_with_encoded_url_json_path)) as player_with_encoded_url:
0409             wait = WebDriverWait(self.driver, 3)
0410             wait.until(EC.presence_of_element_located((AppiumBy.NAME, "Flash Funk")))
0411             wait.until(EC.presence_of_element_located((AppiumBy.NAME, "League of Legends")))  # Album name deduced from folder name
0412             # Overflow check, 2160000000 (microsecond) > INT_MAX (2147483647)
0413             wait.until(EC.presence_of_element_located((AppiumBy.NAME, "-36:00")))
0414             player_with_encoded_url.terminate()
0415             player_with_encoded_url.wait(10)
0416 
0417 
0418 if __name__ == '__main__':
0419     unittest.main()