from time import sleep from appium.webdriver.common.appiumby import AppiumBy as By from selenium.common.exceptions import TimeoutException from helpers.platform.android.locators import ( content_desc_xpath, resource_id_xpath, text_view_xpath) from page_objects.base_screen import BaseScreen from page_objects.mixins.utils import retry_on_stale_element class SearchScreen(BaseScreen): first_search = True search_bar = content_desc_xpath('searchBar') artists_header = 'search.results.artistsheader' clear_button = resource_id_xpath('clearButton') clear_recent_results_btn = 'CLEAR RECENT RESULTS' no_recent_results_placeholder = text_view_xpath( 'Search for Songs, products, or any artist') no_results_placeholder = text_view_xpath( 'Please make sure your words are spelled correctly, ' 'or use fewer or different words.') no_results_state = resource_id_xpath('zeroStateMessage') artist_component = 'participantComponent' recent_searches_title = '//android.widget.TextView' \ '[contains(@text, "Recent Searches")]' _see_all_suffix = '/android.view.ViewGroup[@content-desc="SEE ALL"]' artists_see_all_btn = ( resource_id_xpath('participantList') + _see_all_suffix) songs_see_all_btn = resource_id_xpath('trackList') + _see_all_suffix products_see_all_btn = resource_id_xpath('productList') + _see_all_suffix artists_title = \ ("//android.widget.TextView[" "@text='Artists' and @resource-id='title']") songs_title = \ ("//android.widget.TextView[" "@text='Songs' and @resource-id='title']") products_title = \ ("//android.widget.TextView[" "@text='Products' and @resource-id='title']") artist_label = resource_id_xpath('artistLabel') artist_name = resource_id_xpath('participantName') song_cover = resource_id_xpath('coverArt') song_name = ('//android.view.ViewGroup[' 'starts-with(@resource-id, "topTracksItem")]' '//android.widget.TextView[1]') song_artist_label = resource_id_xpath('songArtistLabel') product_cover = resource_id_xpath('coverArt') product_name = resource_id_xpath('productName') product_artist = resource_id_xpath('artistName') def search(self, item, wait_for_results=True): def search_for_item(): if not self.first_search: self.scroll_and_assert( By.XPATH, self.clear_button, max_scrolls=10, small_scroll=True) self.click_with_retry(3, By.XPATH, self.clear_button) self.waiter(20, By.XPATH, self.search_bar).send_keys(item) self.first_search = False if wait_for_results: self.waiter(60, By.ACCESSIBILITY_ID, self.artists_header) try: search_for_item() except TimeoutException: self.driver.find_element(By.XPATH, self.clear_button).click() search_for_item() def assert_product(self, product_id): generic_locator = \ '//android.view.ViewGroup[starts-with(@content-desc, "product")]' specific_locator = self.get_product_id(product_id) try: self.waiter(10, By.XPATH, generic_locator) except TimeoutException: self.scroll_and_assert(By.XPATH, generic_locator) self.scroll_and_assert(By.XPATH, specific_locator, wd_timeout=20) def get_artist_id(self, artist, artist_id): return resource_id_xpath(f'{self.artist_component}' f'-{artist}-{artist_id}') def get_song_id(self, song, song_id): return \ f'//android.view.ViewGroup[@content-desc="topTracksItem{song_id}"]' def get_product_id(self, product_id): return f'//android.view.ViewGroup[@content-desc="product{product_id}"]' def assert_artist(self, artist, artist_id): artist_title_loc = \ (f'//android.view.ViewGroup[' f'starts-with(@content-desc, "{self.artist_component}")]') try: self.waiter(10, By.XPATH, artist_title_loc) except TimeoutException: self.scroll_and_assert(By.XPATH, artist_title_loc) id_string = self.get_artist_id(artist, artist_id) self.scroll_and_assert(By.XPATH, id_string) @retry_on_stale_element() def assert_song(self, song, song_id): id_string = self.get_song_id(song, song_id) self.scroll_and_assert(By.XPATH, id_string, max_scrolls=5) def assert_artist_in_recent_search(self, artist, artist_id): self.waiter(5, By.XPATH, self.recent_searches_title) id_string = self.get_artist_id(artist, artist_id) self.scroll_and_assert(By.XPATH, id_string) def assert_song_in_recent_search(self, song, song_id): self.waiter(5, By.XPATH, self.recent_searches_title) id_string = self.get_song_id(song, song_id) self.scroll_and_assert(By.XPATH, id_string) def assert_product_in_recent_search(self, product_id): self.waiter(5, By.XPATH, self.recent_searches_title) id_string = self.get_product_id(product_id) self.scroll_and_assert(By.XPATH, id_string) @retry_on_stale_element() def tap_product(self, product_id): id_string = resource_id_xpath(f'product{product_id}') elem = self.waiter(10, By.XPATH, id_string) self.tap_after_overlap_check(elem) @retry_on_stale_element() def tap_artist(self, artist, artist_id): id_string = self.get_artist_id(artist, artist_id) elem = self.waiter(10, By.XPATH, id_string) self.tap_after_overlap_check(elem) @retry_on_stale_element() def tap_song(self, song, song_id): id_string = self.get_song_id(song, song_id) self.driver.find_elements(By.XPATH, id_string)[0].click() def tap_see_all_button(self, section): sections = { 'Artists': self.artists_see_all_btn, 'Songs': self.songs_see_all_btn, 'Products': self.products_see_all_btn } self.scroll_and_assert(By.XPATH, sections[section]) section_elem = self.driver.find_element(By.XPATH, sections[section]) self.tap_after_overlap_check(section_elem) try: self.wait_for_element_invisibility(By.XPATH, sections[section]) except AssertionError: self.scroll_page() sleep(1) self.click_with_retry(3, By.XPATH, sections[section]) def clear_search_field(self): self.waiter(5, By.XPATH, self.clear_button) self.driver.find_element(By.XPATH, self.clear_button).click() def clear_recent_results(self): self.waiter(10, By.ACCESSIBILITY_ID, self.clear_recent_results_btn).click() def verify_no_recent_results_visible(self): try: self.waiter(10, By.XPATH, self.no_recent_results_placeholder) except TimeoutException: raise AssertionError('Recent results are still visible.') def verify_recent_search_on_line_android(self, line_num, item): xpath = \ (f'(//android.view.ViewGroup[@resource-id="recentSearchesList"]/*/' f'android.view.ViewGroup)[{line_num}]/android.widget.TextView[1]') elem = self.waiter(10, By.XPATH, xpath) assert elem.text == item, f"Expected '{item}', but got '{elem.text}'" def assert_no_matches_error_msg(self): self.waiter(10, By.XPATH, self.no_results_state) self.waiter(3, By.XPATH, self.no_results_placeholder) def verify_all_search_results_page(self, category, name): header_loc = text_view_xpath( f'{category.upper()} FOR "{name.upper()}"') self.waiter(10, By.XPATH, header_loc) def verify_all_artists_search_results_card(self): artists_label = ( self.waiter(10, By.XPATH, self.artist_label, multiple=True))[:6] artists_name = ( self.waiter(10, By.XPATH, self.artist_name, multiple=True))[:6] assert len(artists_label) == len(artists_name) def verify_all_songs_search_results_card(self): songs_cover = ( self.waiter(10, By.XPATH, self.song_cover, multiple=True)) songs_name = ( self.waiter(10, By.XPATH, self.song_name, multiple=True)) songs_artist_label = ( self.waiter(10, By.XPATH, self.song_artist_label, multiple=True)) assert len(songs_cover) == len(songs_name) == len(songs_artist_label) def verify_all_products_search_results_card(self): prod_cover = ( self.waiter(10, By.XPATH, self.product_cover, multiple=True))[:6] prod_name = ( self.waiter(10, By.XPATH, self.product_name, multiple=True))[:6] prod_artist = ( self.waiter(10, By.XPATH, self.product_artist, multiple=True))[:6] assert len(prod_cover) == len(prod_name) == len(prod_artist) def scroll_through_all_results(self, category): categories = { 'artist': (self.artists_title, self.artist_name), 'song': (self.songs_title, self.song_name), 'product': (self.products_title, self.product_name) } items_name = ( self.waiter(10, By.XPATH, categories[category][1], multiple=True)) if len(items_name) < 6: return else: self.scroll_page() self.wait_for_element_invisibility( By.XPATH, categories[category][0]) def _get_last_element(self, locator, timeout=10): """ Safely fetches the last visible element matching the locator. Re-fetches it individually to avoid stale element exceptions. """ elements = self.waiter(timeout, By.XPATH, locator, multiple=True) if not elements: raise Exception(f"No elements found for locator: {locator}") last_index = len(elements) last_element_xpath = f"({locator})[{last_index}]" return self.waiter(5, By.XPATH, last_element_xpath) def tap_last_artist_result_card_button(self, context): artists = self.waiter(20, By.XPATH, self.artist_name, multiple=True) # TODO: # Use the second-to-last product temporarily to avoid unreliable taps last_artist = artists[-2] context.artist_name = last_artist.text.strip() self.tap_after_overlap_check(last_artist) def tap_last_song_result_card_button(self, context): last_song = self._get_last_element(self.song_name) last_artist = self._get_last_element(self.song_artist_label) context.track_name_of_song = last_song.text.strip() context.track_artist_of_song = last_artist.text.strip() last_song.click() def tap_last_product_result_card_button(self, context): products = self.waiter(10, By.XPATH, self.product_name, multiple=True) # TODO: # Use the second-to-last product temporarily to avoid unreliable taps last_product = products[-2] context.product_name = last_product.text.strip() self.tap_after_overlap_check(last_product)