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 page_objects.mixins.utils import ( parse_custom_format, retry_on_stale_element, retry_on_assertion) from helpers.platform.android.actions import slow_typing_with_actions from helpers.platform.android.locators import ( resource_id_xpath, content_desc_xpath, text_view_xpath) from helpers.platform.ios.locators import ( ios_label_equals, ios_other_by_name, ios_static_text_by_name) from helpers.appium.keyboard import hide_keyboard class PlaylistsMixin: access = By.ACCESSIBILITY_ID playlist_element = resource_id_xpath('cardPlaylist') playlist_cover = resource_id_xpath('coverArt') playlist_store_icon = resource_id_xpath('storeIcon') playlist_card_title = resource_id_xpath('playlistCardTitle') playlist_card_type = resource_id_xpath('playlistCardType') playlist_country_label = resource_id_xpath('countryLabel') song_stream_quantity = resource_id_xpath('songTileStreamQuantity') added_date = resource_id_xpath('songTileAddedDate') followers = ( resource_id_xpath('followersLabel') + '/android.widget.TextView') sorting_btn = { 'ios': 'sortButton', 'android': resource_id_xpath('sortButton') } filter_btn = { 'ios': 'filterToggle', 'android': resource_id_xpath('filterToggle') } filter_search = { 'ios': '**/XCUIElementTypeTextField[`name == "searchBar"`]', 'android': content_desc_xpath('searchBar') } apply_btn = 'APPLY' no_playlists_title = { 'ios': 'No Result', 'android': text_view_xpath('No Result') } no_playlists_content = { 'ios': 'You can adjust the filters to broaden your search.', 'android': text_view_xpath( 'You can adjust the filters to broaden your search.') } no_playlists_reset_btn = { 'ios': 'reset', 'android': 'RESET' } @retry_on_stale_element(max_retries=2, delay=3) def assert_playlists_sorted_by_streams(self, streams_type): stream_quantity_elems = self.waiter( 30, By.XPATH, self.song_stream_quantity, multiple=True) stream_quantity_values = \ [parse_custom_format(elem.text) for elem in stream_quantity_elems] assert (stream_quantity_values == sorted(stream_quantity_values, reverse=True)) @staticmethod def _find_added_data(text_el): match = re.search(r'Added: \d{1,2} \w{3} \d{4}', text_el) if match: result = match.group(0) return result @retry_on_assertion() @retry_on_stale_element(max_retries=2, delay=3) def assert_playlists_sorted_by_most_recent(self): if self.is_ios(): loc = ios_other_by_name('songTile') elements = self.waiter(30, By.IOS_CLASS_CHAIN, loc, multiple=True) date_strings = [self._find_added_data(e.text) for e in elements] else: playlists_added_date = self.waiter( 30, By.XPATH, self.added_date, multiple=True) date_strings = [elem.text for elem in playlists_added_date] datetime_objects = [ datetime.strptime(date_string.replace('Added: ', ''), '%d %b %Y') for date_string in date_strings ] assert datetime_objects == sorted(datetime_objects, reverse=True) @retry_on_stale_element(max_retries=2, delay=3) def assert_playlists_sorted_by_followers(self): sleep(5) follower_elements = self.waiter( 30, By.XPATH, self.followers, multiple=True) follower_texts = [elem.text for elem in follower_elements] follower_values = [parse_custom_format(text) for text in follower_texts] assert follower_values == sorted(follower_values, reverse=True), \ f"Playlists are not sorted by followers. Order: {follower_values}" def change_playlists_sorting_option(self, sorting_option): if self.is_ios(): sort_by = { '1d Streams': 'streamsSort1Day', '7d Streams': 'streamsSort7Days', 'Most Recent': 'addedSort', 'Followers': 'followersSort' } self.waiter(10, self.access, self.sorting_btn['ios']).click() self.waiter(20, self.access, sort_by[sorting_option]).click() else: self.waiter(10, By.XPATH, self.sorting_btn['android']).click() self.waiter(20, self.access, sorting_option).click() sleep(3) # Could be a load spinner that blocks tapping on all elements @retry_on_stale_element() def tap_filter_playlists_button(self): if self.is_ios(): self.click_with_retry(10, self.access, self.filter_btn['ios']) else: activity_spinner = resource_id_xpath("activityIndicator") try: self.waiter(5, By.XPATH, activity_spinner) except TimeoutException: pass self.wait_for_element_invisibility(By.XPATH, activity_spinner, 30) self.click_with_retry(10, By.XPATH, self.filter_btn['android']) def _click_filter_option(self, value): if self.is_ios(): self.click_with_retry(10, self.access, value) else: self.click_with_retry(10, By.XPATH, content_desc_xpath(value)) def filter_playlists_by_store(self, values): for value in values: self._click_filter_option(value) def filter_playlists_by_deselection(self, values): for value in values: self._click_filter_option(value) def filter_playlists_by_types(self, values): for value in values: self._click_filter_option(value) def filter_playlists_by_markets(self, values): if self.is_ios(): try: curator_btn = 'label BEGINSWITH "CURATOR MARKET"' self.waiter(30, By.IOS_PREDICATE, curator_btn).click() except TimeoutException: fallback_btn = 'filterCountryLink' self.waiter(10, self.access, fallback_btn).click() self.waiter(10, By.IOS_CLASS_CHAIN, self.filter_search['ios']).click() else: try: curator_btn = ('//android.view.ViewGroup[' 'contains(@content-desc, "CURATOR MARKET")]') self.click_with_retry(10, By.XPATH, curator_btn) except TimeoutException: fallback_btn = resource_id_xpath("filterCountryLink") self.click_with_retry(10, By.XPATH, fallback_btn) self.click_with_retry(10, By.XPATH, self.filter_search['android']) if self.is_ios(): for country in values: loc = f'name == "countryRow" AND label == "{country}"' filter_search = self.waiter(5, By.IOS_CLASS_CHAIN, self.filter_search['ios']) slow_typing_with_actions(self.driver, filter_search, country, delay=0.25) self.tap_middle_of_screen() sleep(2) # Because on a real device, a tap may not register self.click_with_retry(10, By.IOS_PREDICATE, loc) else: for country in values: loc = (f'//android.view.ViewGroup[' f'@content-desc="{country}" and ' f'@resource-id="countryRow"]') filter_search = self.waiter(5, By.XPATH, self.filter_search['android']) slow_typing_with_actions(self.driver, filter_search, country, delay=0.25) hide_keyboard(self.driver) sleep(2) # Because on a real device, a tap may not register self.click_with_retry(10, By.XPATH, loc) self.click_apply_button() def _click_filter_type(self, label): if self.is_ios(): locator = ios_label_equals( label, element_type='XCUIElementTypeOther', ) self.click_with_retry(10, By.IOS_CLASS_CHAIN, locator) else: self.click_with_retry(10, self.access, label) def filter_playlists_by_labels(self, labels): self._click_filter_type("LABEL") for label in labels: if self.is_ios(): loc = f'name BEGINSWITH "{label}" AND name ENDSWITH "Label"' try: self.click_with_retry(10, By.IOS_PREDICATE, loc) except TimeoutException: filter_search = self.waiter(5, By.IOS_CLASS_CHAIN, self.filter_search['ios']) slow_typing_with_actions(self.driver, filter_search, label, delay=0.25) self.click_with_retry(10, By.IOS_PREDICATE, loc) else: loc = (f'//android.view.ViewGroup[starts-with(' f'@content-desc, "{label}")]') try: self.click_with_retry(10, By.XPATH, loc) except TimeoutException: filter_search = self.waiter(5, By.XPATH, self.filter_search['android']) slow_typing_with_actions(self.driver, filter_search, label, delay=0.25) hide_keyboard(self.driver) self.click_with_retry(10, By.XPATH, loc) self.click_apply_button() def filter_playlists_by_artists(self, artists): self._click_filter_type("ARTIST") for artist in artists: if self.is_ios(): loc = ios_other_by_name(artist) self.click_with_retry(10, By.IOS_CLASS_CHAIN, loc) else: loc = (f'//android.view.ViewGroup[' f'starts-with(@content-desc, "{artist}")]') try: self.click_with_retry(10, By.XPATH, loc) except TimeoutException: filter_search = self.waiter(5, By.XPATH, self.filter_search['android']) slow_typing_with_actions(self.driver, filter_search, artist, delay=0.25) hide_keyboard(self.driver) self.click_with_retry(10, By.XPATH, loc) self.click_apply_button() def verify_top_playlists_cards_content(self, context): sleep(5) try: self.waiter(30, By.XPATH, self.playlist_cover) self.waiter(3, By.XPATH, self.playlist_store_icon) self.waiter(3, By.XPATH, self.playlist_card_title) self.waiter(3, By.XPATH, self.playlist_card_type) self.waiter(3, By.XPATH, self.playlist_country_label) except TimeoutException: self.waiter(3, By.XPATH, self.no_playlists_title['android']) @retry_on_stale_element(max_retries=2, delay=3) def assert_playlist_types(self, expected_values): card_type = ios_static_text_by_name('playlistCardType') if self.is_ios(): card_types = self.waiter(10, By.IOS_CLASS_CHAIN, card_type, multiple=True) else: card_types = self.waiter(10, By.XPATH, self.playlist_card_type, multiple=True) card_types_text = [i.text for i in card_types] assert set(card_types_text) == set(expected_values), \ (f'Expected: {set(expected_values)}, ' f'but got: {set(card_types_text)}') @retry_on_assertion(max_retries=2, delay=3) def assert_country_labels(self, expected_values): if self.is_ios(): country_label = ios_other_by_name('countryLabel') card_types = self.waiter(10, By.IOS_CLASS_CHAIN, country_label, multiple=True) else: card_types = self.waiter(10, By.XPATH, self.playlist_country_label + '/android.widget.TextView', multiple=True) card_types_text = [i.text for i in card_types] assert set(card_types_text) == set(expected_values), \ (f'Expected: {set(expected_values)}, ' f'but got: {set(card_types_text)}') @retry_on_assertion(max_retries=2, delay=3) def assert_store_icon(self, expected_values): if self.is_ios(): store_icon = ios_other_by_name('storeIcon') expected_store_icon = ios_other_by_name( f'{expected_values[0].lower()}Icon') expected_store_icons = self.waiter(10, By.IOS_CLASS_CHAIN, expected_store_icon, multiple=True) all_store_icons = self.waiter(10, By.IOS_CLASS_CHAIN, store_icon, multiple=True) else: store_icon = resource_id_xpath('storeIcon') expected_store_icon = ( resource_id_xpath(f'{expected_values[0].lower()}Icon')) expected_store_icons = ( self.waiter(10, By.XPATH, expected_store_icon, multiple=True)) all_store_icons = self.waiter(10, By.XPATH, store_icon, multiple=True) assert len(expected_store_icons) == len(all_store_icons) def assert_artists_name(self, expected_values): if self.is_ios(): pass else: artist = resource_id_xpath('songTileArtistName') all_artists = self.waiter(10, By.XPATH, artist, multiple=True) all_artists_name = \ [True for i in all_artists if i.text == expected_values[0]] assert all(all_artists_name) def assert_no_playlists_displayed(self): try: if self.is_ios(): self.waiter(20, self.access, self.no_playlists_title['ios']) self.waiter(3, self.access, self.no_playlists_content['ios']) self.waiter(3, self.access, self.no_playlists_reset_btn['ios']) else: self.waiter(20, By.XPATH, self.no_playlists_title['android']) self.waiter(3, By.XPATH, self.no_playlists_content['android']) self.waiter(3, self.access, self.no_playlists_reset_btn['android']) return True except Exception: return False def reset_playlist_filters(self): if self.is_ios(): self.waiter(3, self.access, self.no_playlists_reset_btn['ios']).click() else: self.waiter(3, self.access, self.no_playlists_reset_btn['android']).click()