# -*- coding: utf-8 -*- u""" Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U. This file is part of Toolium. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ # Python 2.7 from __future__ import division import logging import os import time from io import open import requests from datetime import datetime from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.ui import WebDriverWait from six.moves.urllib.parse import urlparse # Python 2 and 3 compatibility from toolium.utils.path_utils import get_valid_filename, makedirs_safe class Utils(object): _window_size = None #: dict with window width and height def __init__(self, driver_wrapper=None): """Initialize Utils instance :param driver_wrapper: driver wrapper instance """ from toolium.driver_wrappers_pool import DriverWrappersPool self.driver_wrapper = driver_wrapper if driver_wrapper else DriverWrappersPool.get_default_wrapper() # Configure logger self.logger = logging.getLogger(__name__) def get_implicitly_wait(self): """Read implicitly timeout from configuration properties""" return self.driver_wrapper.config.get_optional('Driver', 'implicitly_wait') def set_implicitly_wait(self): """Read implicitly timeout from configuration properties and configure driver implicitly wait""" implicitly_wait = self.get_implicitly_wait() if implicitly_wait: self.driver_wrapper.driver.implicitly_wait(implicitly_wait) def get_explicitly_wait(self): """Read explicitly timeout from configuration properties :returns: configured explicitly timeout (default timeout 10 seconds) """ return int(self.driver_wrapper.config.get_optional('Driver', 'explicitly_wait', '10')) def capture_screenshot(self, name): """Capture screenshot and save it in screenshots folder :param name: screenshot name suffix :returns: screenshot path """ from toolium.driver_wrappers_pool import DriverWrappersPool filename = '{0:0=2d}_{1}'.format(DriverWrappersPool.screenshots_number, name) filename = '{}.png'.format(get_valid_filename(filename)) filepath = os.path.join(DriverWrappersPool.screenshots_directory, filename) makedirs_safe(DriverWrappersPool.screenshots_directory) if self.driver_wrapper.driver.get_screenshot_as_file(filepath): self.logger.info('Screenshot saved in %s', filepath) DriverWrappersPool.screenshots_number += 1 return filepath return None def save_webdriver_logs(self, test_name): """Get webdriver logs and write them to log files :param test_name: test that has generated these logs """ try: log_types = self.driver_wrapper.driver.log_types except Exception: # geckodriver does not implement log_types, but it implements get_log for client and server log_types = ['client', 'server'] self.logger.debug("Reading logs from '%s' and writing them to log files", ', '.join(log_types)) for log_type in log_types: try: self.save_webdriver_logs_by_type(log_type, test_name) except Exception: # Capture exceptions to avoid errors in teardown method pass def save_webdriver_logs_by_type(self, log_type, test_name): """Get webdriver logs of the specified type and write them to a log file :param log_type: browser, client, driver, performance, server, syslog, crashlog or logcat :param test_name: test that has generated these logs """ try: logs = self.driver_wrapper.driver.get_log(log_type) except Exception: return if len(logs) > 0: from toolium.driver_wrappers_pool import DriverWrappersPool log_file_name = '{}_{}.txt'.format(get_valid_filename(test_name), log_type) log_file_name = os.path.join(DriverWrappersPool.logs_directory, log_file_name) with open(log_file_name, 'a+', encoding='utf-8') as log_file: driver_type = self.driver_wrapper.config.get('Driver', 'type') log_file.write( u"\n{} '{}' test logs with driver = {}\n\n".format(datetime.now(), test_name, driver_type)) for entry in logs: timestamp = datetime.fromtimestamp(float(entry['timestamp']) / 1000.).strftime( '%Y-%m-%d %H:%M:%S.%f') log_file.write(u'{}\t{}\t{}\n'.format(timestamp, entry['level'], entry['message'].rstrip())) def discard_logcat_logs(self): """Discard previous logcat logs""" if self.driver_wrapper.is_android_test(): try: self.driver_wrapper.driver.get_log('logcat') except Exception: pass def _expected_condition_find_element(self, element): """Tries to find the element, but does not thrown an exception if the element is not found :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :returns: the web element if it has been found or False :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ from toolium.pageelements.page_element import PageElement web_element = False try: if isinstance(element, PageElement): # Use _find_web_element() instead of web_element to avoid logging error message element._web_element = None element._find_web_element() web_element = element._web_element elif isinstance(element, tuple): web_element = self.driver_wrapper.driver.find_element(*element) except NoSuchElementException: pass return web_element def _expected_condition_find_element_visible(self, element): """Tries to find the element and checks that it is visible, but does not thrown an exception if the element is not found :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :returns: the web element if it is visible or False :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ web_element = self._expected_condition_find_element(element) try: return web_element if web_element and web_element.is_displayed() else False except StaleElementReferenceException: return False def _expected_condition_find_element_not_visible(self, element): """Tries to find the element and checks that it is visible, but does not thrown an exception if the element is not found :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :returns: True if the web element is not found or it is not visible """ web_element = self._expected_condition_find_element(element) try: return True if not web_element or not web_element.is_displayed() else False except StaleElementReferenceException: return False def _expected_condition_find_first_element(self, elements): """Try to find sequentially the elements of the list and return the first element found :param elements: list of PageElements or element locators as a tuple (locator_type, locator_value) to be found sequentially :returns: first element found or None :rtype: toolium.pageelements.PageElement or tuple """ from toolium.pageelements.page_element import PageElement element_found = None for element in elements: try: if isinstance(element, PageElement): element._web_element = None element._find_web_element() else: self.driver_wrapper.driver.find_element(*element) element_found = element break except (NoSuchElementException, TypeError): pass return element_found def _expected_condition_find_element_clickable(self, element): """Tries to find the element and checks that it is clickable, but does not thrown an exception if the element is not found :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :returns: the web element if it is clickable or False :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ web_element = self._expected_condition_find_element_visible(element) try: return web_element if web_element and web_element.is_enabled() else False except StaleElementReferenceException: return False def _expected_condition_find_element_stopped(self, element_times): """Tries to find the element and checks that it has stopped moving, but does not thrown an exception if the element is not found :param element_times: Tuple with 2 items where: [0] element: PageElement or element locator as a tuple (locator_type, locator_value) to be found [1] times: number of iterations checking the element's location that must be the same for all of them in order to considering the element has stopped :returns: the web element if it is clickable or False :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ element, times = element_times web_element = self._expected_condition_find_element(element) try: locations_list = [tuple(web_element.location.values()) for i in range(int(times)) if not time.sleep(0.001)] return web_element if set(locations_list) == set(locations_list[-1:]) else False except StaleElementReferenceException: return False def _expected_condition_find_element_containing_text(self, element_text_pair): """Tries to find the element and checks that it contains the specified text, but does not thrown an exception if the element is not found :param element_text_pair: Tuple with 2 items where: [0] element: PageElement or element locator as a tuple (locator_type, locator_value) to be found [1] text: text to be contained into the element :returns: the web element if it contains the text or False :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ element, text = element_text_pair web_element = self._expected_condition_find_element(element) try: return web_element if web_element and text in web_element.text else False except StaleElementReferenceException: return False def _expected_condition_find_element_not_containing_text(self, element_text_pair): """Tries to find the element and checks that it does not contain the specified text, but does not thrown an exception if the element is found :param element_text_pair: Tuple with 2 items where: [0] element: PageElement or element locator as a tuple (locator_type, locator_value) to be found [1] text: text to not be contained into the element :returns: the web element if it does not contain the text or False :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ element, text = element_text_pair web_element = self._expected_condition_find_element(element) try: return web_element if web_element and text not in web_element.text else False except StaleElementReferenceException: return False def _expected_condition_value_in_element_attribute(self, element_attribute_value): """Tries to find the element and checks that it contains the requested attribute with the expected value, but does not thrown an exception if the element is not found :param element_attribute_value: Tuple with 3 items where: [0] element: PageElement or element locator as a tuple (locator_type, locator_value) to be found [1] attribute: element's attribute where to check its value [2] value: expected value for the element's attribute :returns: the web element if it contains the expected value for the requested attribute or False :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ element, attribute, value = element_attribute_value web_element = self._expected_condition_find_element(element) try: return web_element if web_element and web_element.get_attribute(attribute) == value else False except StaleElementReferenceException: return False def _wait_until(self, condition_method, condition_input, timeout=None): """ Common method to wait until condition met :param condition_method: method to check the condition :param condition_input: parameter that will be passed to the condition method :param timeout: max time to wait :returns: condition method response """ # Remove implicitly wait timeout implicitly_wait = self.get_implicitly_wait() if implicitly_wait != 0: self.driver_wrapper.driver.implicitly_wait(0) try: # Get explicitly wait timeout timeout = timeout if timeout else self.get_explicitly_wait() # Wait for condition condition_response = WebDriverWait(self.driver_wrapper.driver, timeout).until( lambda s: condition_method(condition_input)) finally: # Restore implicitly wait timeout from properties if implicitly_wait != 0: self.set_implicitly_wait() return condition_response def wait_until_element_present(self, element, timeout=None): """Search element and wait until it is found :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :param timeout: max time to wait :returns: the web element if it is present :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement :raises TimeoutException: If the element is not found after the timeout """ return self._wait_until(self._expected_condition_find_element, element, timeout) def wait_until_element_visible(self, element, timeout=None): """Search element and wait until it is visible :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :param timeout: max time to wait :returns: the web element if it is visible :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement :raises TimeoutException: If the element is still not visible after the timeout """ return self._wait_until(self._expected_condition_find_element_visible, element, timeout) def wait_until_element_not_visible(self, element, timeout=None): """Search element and wait until it is not visible :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :param timeout: max time to wait :returns: the web element if it exists but is not visible :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement :raises TimeoutException: If the element is still visible after the timeout """ return self._wait_until(self._expected_condition_find_element_not_visible, element, timeout) def wait_until_first_element_is_found(self, elements, timeout=None): """Search list of elements and wait until one of them is found :param elements: list of PageElements or element locators as a tuple (locator_type, locator_value) to be found sequentially :param timeout: max time to wait :returns: first element found :rtype: toolium.pageelements.PageElement or tuple :raises TimeoutException: If no element in the list is found after the timeout """ try: return self._wait_until(self._expected_condition_find_first_element, elements, timeout) except TimeoutException as exception: msg = 'None of the page elements has been found after %s seconds' timeout = timeout if timeout else self.get_explicitly_wait() self.logger.error(msg, timeout) exception.msg += "\n {}".format(msg % timeout) raise exception def wait_until_element_clickable(self, element, timeout=None): """Search element and wait until it is clickable :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :param timeout: max time to wait :returns: the web element if it is clickable :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement :raises TimeoutException: If the element is not clickable after the timeout """ return self._wait_until(self._expected_condition_find_element_clickable, element, timeout) def wait_until_element_stops(self, element, times=1000, timeout=None): """Search element and wait until it has stopped moving :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :param times: number of iterations checking the element's location that must be the same for all of them in order to considering the element has stopped :returns: the web element if the element is stopped :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement :raises TimeoutException: If the element does not stop after the timeout """ return self._wait_until(self._expected_condition_find_element_stopped, (element, times), timeout) def wait_until_element_contains_text(self, element, text, timeout=None): """Search element and wait until it contains the expected text :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :param text: text expected to be contained into the element :param timeout: max time to wait :returns: the web element if it contains the expected text :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement :raises TimeoutException: If the element does not contain the expected text after the timeout """ return self._wait_until(self._expected_condition_find_element_containing_text, (element, text), timeout) def wait_until_element_not_contain_text(self, element, text, timeout=None): """Search element and wait until it does not contain the expected text :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :param text: text expected to be contained into the element :param timeout: max time to wait :returns: the web element if it does not contain the given text :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement :raises TimeoutException: If the element contains the expected text after the timeout """ return self._wait_until(self._expected_condition_find_element_not_containing_text, (element, text), timeout) def wait_until_element_attribute_is(self, element, attribute, value, timeout=None): """Search element and wait until the requested attribute contains the expected value :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :param attribute: attribute belonging to the element :param value: expected value for the attribute of the element :param timeout: max time to wait :returns: the web element if the element's attribute contains the expected value :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement :raises TimeoutException: If the element's attribute does not contain the expected value after the timeout """ return self._wait_until(self._expected_condition_value_in_element_attribute, (element, attribute, value), timeout) def get_remote_node(self): """Return the remote node that it's executing the actual test session :returns: tuple with server type (local, grid, ggr, selenium) and remote node name """ logging.getLogger("requests").setLevel(logging.WARNING) remote_node = None server_type = 'local' if self.driver_wrapper.config.getboolean_optional('Server', 'enabled'): # Request session info from grid hub session_id = self.driver_wrapper.driver.session_id self.logger.debug("Trying to identify remote node") try: # Request session info from grid hub and extract remote node url = '{}/grid/api/testsession?session={}'.format(self.get_server_url(), session_id) proxy_id = requests.get(url).json()['proxyId'] remote_node = urlparse(proxy_id).hostname if urlparse(proxy_id).hostname else proxy_id server_type = 'grid' self.logger.debug("Test running in remote node %s", remote_node) except (ValueError, KeyError): try: # Request session info from GGR and extract remote node from toolium.selenoid import Selenoid remote_node = Selenoid(self.driver_wrapper).get_selenoid_info()['Name'] server_type = 'ggr' self.logger.debug("Test running in a GGR remote node %s", remote_node) except Exception: try: # The remote node is a Selenoid node url = '{}/status'.format(self.get_server_url()) requests.get(url).json()['total'] remote_node = self.driver_wrapper.config.get('Server', 'host') server_type = 'selenoid' self.logger.debug("Test running in a Selenoid node %s", remote_node) except Exception: # The remote node is not a grid node or the session has been closed remote_node = self.driver_wrapper.config.get('Server', 'host') server_type = 'selenium' self.logger.debug("Test running in a Selenium node %s", remote_node) return server_type, remote_node def get_server_url(self): """Return the configured server url :returns: server url """ server_host = self.driver_wrapper.config.get('Server', 'host') server_port = self.driver_wrapper.config.get('Server', 'port') server_ssl = 'https' if self.driver_wrapper.config.getboolean_optional('Server', 'ssl') else 'http' server_username = self.driver_wrapper.config.get_optional('Server', 'username') server_password = self.driver_wrapper.config.get_optional('Server', 'password') server_auth = '{}:{}@'.format(server_username, server_password) if server_username and server_password else '' server_url = '{}://{}{}:{}'.format(server_ssl, server_auth, server_host, server_port) return server_url def download_remote_video(self, remote_node, session_id, video_name): """Download the video recorded in the remote node during the specified test session and save it in videos folder :param remote_node: remote node name :param session_id: test session id :param video_name: video name """ try: video_url = self._get_remote_video_url(remote_node, session_id) except requests.exceptions.ConnectionError: self.logger.warning("Remote server seems not to have video capabilities") return if not video_url: self.logger.warning("Test video not found in node '%s'", remote_node) return self._download_video(video_url, video_name) def _get_remote_node_url(self, remote_node): """Get grid-extras url of a node :param remote_node: remote node name :returns: grid-extras url """ logging.getLogger("requests").setLevel(logging.WARNING) gridextras_port = 3000 return 'http://{}:{}'.format(remote_node, gridextras_port) def _get_remote_video_url(self, remote_node, session_id): """Get grid-extras url to download videos :param remote_node: remote node name :param session_id: test session id :returns: grid-extras url to download videos """ url = '{}/video'.format(self._get_remote_node_url(remote_node)) timeout = time.time() + 5 # 5 seconds from now # Requests videos list until timeout or the video url is found video_url = None while time.time() < timeout: response = requests.get(url).json() try: video_url = response['available_videos'][session_id]['download_url'] break except KeyError: time.sleep(1) return video_url def _download_video(self, video_url, video_name): """Download a video from the remote node :param video_url: video url :param video_name: video name """ from toolium.driver_wrappers_pool import DriverWrappersPool filename = '{0:0=2d}_{1}'.format(DriverWrappersPool.videos_number, video_name) filename = '{}.mp4'.format(get_valid_filename(filename)) filepath = os.path.join(DriverWrappersPool.videos_directory, filename) makedirs_safe(DriverWrappersPool.videos_directory) response = requests.get(video_url) open(filepath, 'wb').write(response.content) self.logger.info("Video saved in '%s'", filepath) DriverWrappersPool.videos_number += 1 def is_remote_video_enabled(self, remote_node): """Check if the remote node has the video recorder enabled :param remote_node: remote node name :returns: true if it has the video recorder enabled """ enabled = False if remote_node: url = '{}/config'.format(self._get_remote_node_url(remote_node)) try: response = requests.get(url, timeout=5).json() record_videos = response['config_runtime']['theConfigMap']['video_recording_options'][ 'record_test_videos'] except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout, KeyError): record_videos = 'false' if record_videos == 'true': # Wait to the video recorder start time.sleep(1) enabled = True return enabled def get_center(self, element): """Get center coordinates of an element :param element: either a WebElement, PageElement or element locator as a tuple (locator_type, locator_value) :returns: dict with center coordinates """ web_element = self.get_web_element(element) location = web_element.location size = web_element.size return {'x': location['x'] + (size['width'] / 2), 'y': location['y'] + (size['height'] / 2)} def get_safari_navigation_bar_height(self): """Get the height of Safari navigation bar :returns: height of navigation bar """ status_bar_height = 0 if self.driver_wrapper.is_ios_test() and self.driver_wrapper.is_web_test(): # ios 7.1, 8.3 status_bar_height = 64 return status_bar_height def get_window_size(self): """Generic method to get window size using a javascript workaround for Android web tests :returns: dict with window width and height """ if not self._window_size: if self.driver_wrapper.is_android_web_test() and self.driver_wrapper.driver.current_context != 'NATIVE_APP': window_width = self.driver_wrapper.driver.execute_script("return window.innerWidth") window_height = self.driver_wrapper.driver.execute_script("return window.innerHeight") self._window_size = {'width': window_width, 'height': window_height} else: self._window_size = self.driver_wrapper.driver.get_window_size() return self._window_size def get_native_coords(self, coords): """Convert web coords into native coords. Assumes that the initial context is WEBVIEW and switches to NATIVE_APP context. :param coords: dict with web coords, e.g. {'x': 10, 'y': 10} :returns: dict with native coords """ web_window_size = self.get_window_size() self.driver_wrapper.driver.switch_to.context('NATIVE_APP') native_window_size = self.driver_wrapper.driver.get_window_size() scale = native_window_size['width'] / web_window_size['width'] offset_y = self.get_safari_navigation_bar_height() native_coords = {'x': coords['x'] * scale, 'y': coords['y'] * scale + offset_y} self.logger.debug('Converted web coords %s into native coords %s', coords, native_coords) return native_coords def swipe(self, element, x, y, duration=None): """Swipe over an element :param element: either a WebElement, PageElement or element locator as a tuple (locator_type, locator_value) :param x: horizontal movement :param y: vertical movement :param duration: time to take the swipe, in ms """ if not self.driver_wrapper.is_mobile_test(): raise Exception('Swipe method is not implemented in Selenium') # Get center coordinates of element center = self.get_center(element) initial_context = self.driver_wrapper.driver.current_context if self.driver_wrapper.is_web_test() or initial_context != 'NATIVE_APP': center = self.get_native_coords(center) # Android needs absolute end coordinates and ios needs movement end_x = x if self.driver_wrapper.is_ios_test() else center['x'] + x end_y = y if self.driver_wrapper.is_ios_test() else center['y'] + y self.driver_wrapper.driver.swipe(center['x'], center['y'], end_x, end_y, duration) if self.driver_wrapper.is_web_test() or initial_context != 'NATIVE_APP': self.driver_wrapper.driver.switch_to.context(initial_context) def get_web_element(self, element): """Return the web element from a page element or its locator :param element: either a WebElement, PageElement or element locator as a tuple (locator_type, locator_value) :returns: WebElement object """ from toolium.pageelements.page_element import PageElement if isinstance(element, WebElement): web_element = element elif isinstance(element, PageElement): web_element = element.web_element elif isinstance(element, tuple): web_element = self.driver_wrapper.driver.find_element(*element) else: web_element = None return web_element def get_first_webview_context(self): """Return the first WEBVIEW context or raise an exception if it is not found :returns: first WEBVIEW context """ for context in self.driver_wrapper.driver.contexts: if context.startswith('WEBVIEW'): return context raise Exception('No WEBVIEW context has been found') def switch_to_first_webview_context(self): """Switch to the first WEBVIEW context""" self.driver_wrapper.driver.switch_to.context(self.get_first_webview_context())