import re from time import sleep from datetime import datetime from appium.webdriver.common.appiumby import AppiumBy as By from selenium.common.exceptions import TimeoutException from ..base_screen import BaseScreen from page_objects.ios_pom.sound_recording_screen import SoundRecordingScreen from page_objects.ios_pom.product_screen import ProductScreen from page_objects.mixins.playlists_mixin import PlaylistsMixin from ..mixins.utils import parse_custom_format, retry_on_assertion, \ retry_on_stale_element from helpers.platform.ios.locators import ios_other_by_name from helpers.test_data import localization from helpers.appium.keyboard import hide_keyboard class HomeScreen(BaseScreen, PlaylistsMixin): profile_button = 'profileButton' playlist_element = ios_other_by_name('cardPlaylist') playlist_metrics = (ios_other_by_name('playlistCard') + '/' 'XCUIElementTypeOther[`name == "metrics"`]') songs_tab = ios_other_by_name('Songs', index=4) products_tab = ios_other_by_name('Products', index=2) artists_tab = ios_other_by_name('Artists', index=2) playlists_tab = ios_other_by_name('Playlists', index=2) labels_tab = ios_other_by_name('Labels', index=2) product_element = ('**/XCUIElementTypeOther[' '`name BEGINSWITH "product"`]') artist_element = ('**/XCUIElementTypeOther[' '`name BEGINSWITH "participantComponent"`]') top_track_item = ('**/XCUIElementTypeOther[' '`name BEGINSWITH "topTracksItem"`]') product_item = recent_product = \ '**/XCUIElementTypeOther[`name BEGINSWITH "product"`]' sound_recording_in_playlist_item = ios_other_by_name('songTile') catalog_tab = 'myCatalogTab' my_favorites_tab = 'starredTab' search_tab = 'searchTab' account_tab = 'accountTab' charts_tab = 'chartsTab' track_id = 'track-DED831000613' filter_icon = 'filterToggle' no_songs_msg_title = 'No songs' no_products_msg_title = 'No products' no_artists_msg_title = 'No Artists' no_items_msg_body = 'You can adjust the filters to broaden your search' no_items_reset_btn = 'reset' search_bar = '//*[@name="searchBar"]' apply_btn = 'APPLY' times_scrolled = 0 sorting_btn = 'sortButton' def open_playlists_tab(self): self.waiter(10, By.IOS_CLASS_CHAIN, self.playlists_tab).click() def select_item(self, item_type, number, index_offset=0): items_map = { 'track': self.top_track_item, 'product': self.product_item, 'recording': self.sound_recording_in_playlist_item } item_locator = items_map[item_type] number = int(number) + index_offset effective_index = number if item_type == 'product': effective_index += 3 item_full_loc = f'{item_locator}[{effective_index}]' self.scroll_and_assert(By.IOS_CLASS_CHAIN, item_full_loc, wd_timeout=5) self.click_with_retry(10, By.IOS_CLASS_CHAIN, item_full_loc) def open_songs_tab(self): fallback_locator = ios_other_by_name('Songs', index=2) try: self.waiter(10, By.IOS_CLASS_CHAIN, self.songs_tab).click() except TimeoutException: self.waiter(3, By.IOS_CLASS_CHAIN, fallback_locator).click() def open_products_tab(self): self.waiter(10, By.IOS_CLASS_CHAIN, self.products_tab).click() def open_artists_tab(self): self.waiter(10, By.IOS_CLASS_CHAIN, self.artists_tab).click() def open_labels_tab(self): self.waiter(10, By.IOS_CLASS_CHAIN, self.labels_tab).click() def open_search_tab(self, context): hide_keyboard(context.driver) self.waiter(10, By.ID, self.search_tab).click() def open_my_favorites_tab(self): self.waiter(10, By.ID, self.my_favorites_tab).click() def open_chart_digest_screen(self): self.waiter(10, By.ID, self.charts_tab).click() def go_home(self): self.waiter(30, By.ID, self.catalog_tab).click() return self def profile(self): self.waiter(10, By.ID, self.profile_button).click() return self def assert_top_tracks(self): assert self.waiter(20, By.IOS_CLASS_CHAIN, self.top_track_item) def assert_songs_with_only_artist(self, artist): for i in range(3): all_tracks = self.waiter(20, By.IOS_CLASS_CHAIN, self.top_track_item, multiple=True) all_tracks[i].click() self.waiter(20, By.IOS_CLASS_CHAIN, ios_other_by_name(artist)) self.press_back_button() def assert_multiple_product_labels_correctness(self, label): self.open_products_tab() product_screen = ProductScreen(self.driver) for num in range(3): # Without a sleep command, Appium clicks don't affect the UI sleep(1) self.select_item('product', num + 1) product_screen.scroll_header("left") header_info = product_screen.header_info() assert header_info["Label"] == label self.press_back_button() def assert_multiple_playlist_labels_correctness(self, label): self.waiter(10, By.ACCESSIBILITY_ID, self.playlisting_screen).click() sound_recording_screen = SoundRecordingScreen(self.driver) for num in range(3): # Without a sleep command, Appium clicks don't affect the UI sleep(1) self.select_item('recording', num + 1) sound_recording_screen.scroll_header("left") header_info = sound_recording_screen.header_info() assert header_info["Label"] == label self.press_back_button() def assert_multiple_track_labels_correctness(self, label): sound_recording_screen = SoundRecordingScreen(self.driver) for num in range(2): # Without a sleep command, Appium clicks don't affect the UI sleep(1) self.select_item('track', num + 1) sound_recording_screen.scroll_header("left") header_info = sound_recording_screen.header_info() assert header_info["Label"] == label self.press_back_button() @staticmethod def extract_streams(label_text, stream_type): pattern = rf"{stream_type} Streams\s(-|\d+(\.\d+)?[kKmM]?)" match = re.search(pattern, label_text) if match: if match.group(1) != '-': return match.group(1) else: return '0' else: raise ValueError( f"{stream_type} Streams value not found in label text: " f"{label_text}") @staticmethod def extract_added_date(label_text): """ Extracts the 'Added' date in the format 'DD MMM YYYY' from the provided label text. """ pattern = r"Added: (\d{1,2} [A-Za-z]{3} \d{4})" match = re.search(pattern, label_text) if match: return match.group(1) else: raise ValueError( f"Added date not found in label text: {label_text}") def change_sorting_option(self, sorting_option, period=None): self.click_with_retry(10, By.ACCESSIBILITY_ID, self.sorting_btn) sorting_option_btns = \ {'7d Streams': 'streamsSort7Days', 'Most Recent': 'addedSort', 'Followers': 'followersSort', 'Release Date': 'release_date'} streams_option_btns = \ {'1': 'streams_1_day', '7': 'streams_7_days', '28': 'streams_28_days', 'All': 'streams_all_time'} if sorting_option in sorting_option_btns: sleep(3) # A pause is needed to prevent premature clicks self.waiter(10, By.ACCESSIBILITY_ID, sorting_option_btns[sorting_option]).click() elif sorting_option == 'Streams': sleep(3) elems = self.waiter(10, By.IOS_PREDICATE, 'name CONTAINS "Streams"', multiple=True) elems[-1].click() self.click_with_retry(10, By.ACCESSIBILITY_ID, streams_option_btns[period]) else: raise ValueError(f'No such sorting option: {sorting_option}') def __fetch_sound_recording_labels(self): elements = self.waiter( 20, By.IOS_CLASS_CHAIN, self.sound_recording_in_playlist_item, multiple=True) return [e.get_attribute("label") for e in elements] @retry_on_stale_element() def assert_displayed_sorting_option(self, sorting_option): sorting_btn = self.waiter(10, By.ACCESSIBILITY_ID, self.sorting_btn) assert sorting_option == sorting_btn.get_attribute("label") def assert_playlists_sorted_by_relative_time(self): added_dates = \ [self.extract_added_date(e) for e in self.__fetch_sound_recording_labels()] datetime_objects = \ [datetime.strptime(date, '%d %b %Y') for date in added_dates] assert datetime_objects == sorted(datetime_objects, reverse=True), \ "Playlists are not sorted by added date in descending order." def assert_playlists_sorted_by_streams(self, stream_type): stream_values = [ self.extract_streams(e, stream_type) for e in self.__fetch_sound_recording_labels() ] formatted_values = [parse_custom_format(e) for e in stream_values] assert formatted_values == sorted(formatted_values, reverse=True), \ (f"Playlists are not sorted by {stream_type} " f"Streams in descending order.") def assert_playlists_sorted_by_followers(self): playlist_metrics = self.waiter( 30, By.IOS_CLASS_CHAIN, self.playlist_metrics, multiple=True) followers = \ [parse_custom_format(e.text) for e in playlist_metrics[:10]] assert (followers == sorted(followers, reverse=True)), \ "Playlists are not sorted by followers in descending order." def assert_playlists(self): assert self.waiter(10, By.IOS_CLASS_CHAIN, self.playlist_element) def assert_products(self): assert self.waiter(10, By.IOS_CLASS_CHAIN, self.product_element) def filter(self, category, search_term): self.assert_top_tracks() self.click_with_retry(10, By.ACCESSIBILITY_ID, self.filter_icon) sleep(1) self.click_with_retry(10, By.ACCESSIBILITY_ID, f'filter{category.capitalize()}Link') sleep(5) try: self.waiter(10, By.XPATH, self.search_bar).send_keys(search_term) except TimeoutException: self.waiter(10, By.ID, category.upper()).click() self.waiter(10, By.XPATH, self.search_bar).send_keys(search_term) hide_keyboard(self.driver) try: self.waiter(10, By.XPATH, f'//*[@label="{search_term}"]').click() except TimeoutException: self.waiter( 5, By.XPATH, f'(//*[contains(@label, "{search_term}")])[last()]').click() self.waiter(10, By.ID, self.apply_btn).click() def assert_no_items_error_msg(self, items_type): no_items_titles = { 'No songs': self.no_songs_msg_title, 'No products': self.no_products_msg_title, 'No artists': self.no_artists_msg_title } assert self.waiter(20, By.ACCESSIBILITY_ID, no_items_titles[items_type]) assert self.waiter(1, By.ACCESSIBILITY_ID, self.no_items_msg_body) assert self.waiter(1, By.ACCESSIBILITY_ID, self.no_items_reset_btn) def reset_filters(self): self.click_with_retry(10, By.ACCESSIBILITY_ID, self.no_items_reset_btn) @retry_on_assertion() def assert_number_of_displayed_artists_exceeds(self, number): actual_artists = ( self.waiter(30, By.IOS_CLASS_CHAIN, self.artist_element, multiple=True)) every_4th_element = actual_artists[3::4] assert len(every_4th_element) > int(number), \ (f"Expected more than {number} artists, " f"but found {len(every_4th_element)}") @retry_on_assertion() def assert_number_of_displayed_products_exceeds(self, number): actual_num_of_products = ( len(self.waiter(30, By.IOS_CLASS_CHAIN, self.product_element, multiple=True)[3:])) assert actual_num_of_products/4 > int(number), \ (f"Expected more than {number} products, " f"but found {actual_num_of_products}") def assert_number_of_displayed_songs_exceeds(self, number): actual_num_of_songs = ( len(self.waiter(30, By.IOS_CLASS_CHAIN, self.top_track_item, multiple=True)[3:])) assert actual_num_of_songs/4 > int(number), \ (f"Expected more than {number} songs, " f"but found {actual_num_of_songs}") def reset_filters_via_panel(self): self.click_with_retry(10, By.ACCESSIBILITY_ID, self.filter_btn['ios']) self.click_with_retry(10, By.ACCESSIBILITY_ID, 'resetButton') self.click_apply_button() def _assert_pagination(self, locator_type, locator, scrolls=8, skip_first=False): first_elements = self.waiter(10, locator_type, locator, multiple=True) if skip_first: first_elements = first_elements[1:] assert len(first_elements) == 25, \ f"Expected 25 items before scroll, got {len(first_elements)}" self.scroll_page(times=scrolls) final_elements = self.waiter(10, locator_type, locator, multiple=True) if skip_first: final_elements = final_elements[1:] assert len(final_elements) == 50, \ f"Expected 50 items after scroll, got {len(final_elements)}" def assert_songs_pagination(self): self._assert_pagination(By.IOS_CLASS_CHAIN, '**/XCUIElementTypeScrollView/' 'XCUIElementTypeOther/XCUIElementTypeOther' '[`name CONTAINS "topTracksItem"`]') def assert_products_pagination(self): self._assert_pagination(By.IOS_CLASS_CHAIN, '**/XCUIElementTypeScrollView/' 'XCUIElementTypeOther/XCUIElementTypeOther' '[`name CONTAINS "product"`]') def assert_artists_pagination(self): self._assert_pagination(By.IOS_CLASS_CHAIN, '**/XCUIElementTypeScrollView/' 'XCUIElementTypeOther/XCUIElementTypeOther[' '`name CONTAINS "participantComponent"`]') def assert_playlists_pagination(self): self._assert_pagination(By.IOS_PREDICATE, 'name == "playlistCard"', scrolls=16) def verify_my_catalog_screen_is_localized(self, context): localization_data = ( localization.MY_CATALOG_LOCALIZATION)[context.current_language] for el in localization_data['tabs'].values(): self.waiter(10, By.IOS_CLASS_CHAIN, ios_other_by_name(el)) def open_profile_screen(self, context): self.click_with_retry(10, By.ACCESSIBILITY_ID, self.account_tab) self.waiter(10, By.ACCESSIBILITY_ID, context.profile_screen.profile_detail)