1#! /usr/bin/env python3 2# 3# BitBake Toaster Implementation 4# 5# Copyright (C) 2013-2016 Intel Corporation 6# 7# SPDX-License-Identifier: GPL-2.0-only 8# 9# The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are 10# modified from Patchwork, released under the same licence terms as Toaster: 11# https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py 12 13""" 14Helper methods for creating Toaster Selenium tests which run within 15the context of Django unit tests. 16""" 17 18import os 19import time 20import unittest 21 22import pytest 23from selenium import webdriver 24from selenium.webdriver.support import expected_conditions as EC 25from selenium.webdriver.support.ui import WebDriverWait 26from selenium.webdriver.common.by import By 27from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 28from selenium.common.exceptions import NoSuchElementException, \ 29 StaleElementReferenceException, TimeoutException, \ 30 SessionNotCreatedException, WebDriverException 31 32def create_selenium_driver(cls,browser='chrome'): 33 # set default browser string based on env (if available) 34 env_browser = os.environ.get('TOASTER_TESTS_BROWSER') 35 if env_browser: 36 browser = env_browser 37 38 if browser == 'chrome': 39 options = webdriver.ChromeOptions() 40 options.add_argument('--headless') 41 options.add_argument('--disable-infobars') 42 options.add_argument('--disable-dev-shm-usage') 43 options.add_argument('--no-sandbox') 44 options.add_argument('--remote-debugging-port=9222') 45 try: 46 return webdriver.Chrome(options=options) 47 except SessionNotCreatedException as e: 48 exit_message = "Halting tests prematurely to avoid cascading errors." 49 # check if chrome / chromedriver exists 50 chrome_path = os.popen("find ~/.cache/selenium/chrome/ -name 'chrome' -type f -print -quit").read().strip() 51 if not chrome_path: 52 pytest.exit(f"Failed to install/find chrome.\n{exit_message}") 53 chromedriver_path = os.popen("find ~/.cache/selenium/chromedriver/ -name 'chromedriver' -type f -print -quit").read().strip() 54 if not chromedriver_path: 55 pytest.exit(f"Failed to install/find chromedriver.\n{exit_message}") 56 # check if depends on each are fulfilled 57 depends_chrome = os.popen(f"ldd {chrome_path} | grep 'not found'").read().strip() 58 if depends_chrome: 59 pytest.exit(f"Missing chrome dependencies.\n{depends_chrome}\n{exit_message}") 60 depends_chromedriver = os.popen(f"ldd {chromedriver_path} | grep 'not found'").read().strip() 61 if depends_chromedriver: 62 pytest.exit(f"Missing chromedriver dependencies.\n{depends_chromedriver}\n{exit_message}") 63 # print original error otherwise 64 pytest.exit(f"Failed to start chromedriver.\n{e}\n{exit_message}") 65 elif browser == 'firefox': 66 return webdriver.Firefox() 67 elif browser == 'marionette': 68 capabilities = DesiredCapabilities.FIREFOX 69 capabilities['marionette'] = True 70 return webdriver.Firefox(capabilities=capabilities) 71 elif browser == 'ie': 72 return webdriver.Ie() 73 elif browser == 'phantomjs': 74 return webdriver.PhantomJS() 75 elif browser == 'remote': 76 # if we were to add yet another env variable like TOASTER_REMOTE_BROWSER 77 # we could let people pick firefox or chrome, left for later 78 remote_hub= os.environ.get('TOASTER_REMOTE_HUB') 79 driver = webdriver.Remote(remote_hub, 80 webdriver.DesiredCapabilities.FIREFOX.copy()) 81 82 driver.get("http://%s:%s"%(cls.server_thread.host,cls.server_thread.port)) 83 return driver 84 else: 85 msg = 'Selenium driver for browser %s is not available' % browser 86 raise RuntimeError(msg) 87 88class Wait(WebDriverWait): 89 """ 90 Subclass of WebDriverWait with predetermined timeout and poll 91 frequency. Also deals with a wider variety of exceptions. 92 """ 93 _TIMEOUT = 20 94 _POLL_FREQUENCY = 0.5 95 96 def __init__(self, driver, timeout=_TIMEOUT, poll=_POLL_FREQUENCY): 97 self._TIMEOUT = timeout 98 self._POLL_FREQUENCY = poll 99 super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) 100 101 def until(self, method, message=''): 102 """ 103 Calls the method provided with the driver as an argument until the 104 return value is not False. 105 """ 106 107 end_time = time.time() + self._timeout 108 while True: 109 try: 110 value = method(self._driver) 111 if value: 112 return value 113 except NoSuchElementException: 114 pass 115 except StaleElementReferenceException: 116 pass 117 except WebDriverException: 118 # selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Node with given id does not belong to the document"} 119 pass 120 121 time.sleep(self._poll) 122 if time.time() > end_time: 123 break 124 125 raise TimeoutException(message) 126 127 def until_not(self, method, message=''): 128 """ 129 Calls the method provided with the driver as an argument until the 130 return value is False. 131 """ 132 133 end_time = time.time() + self._timeout 134 while True: 135 try: 136 value = method(self._driver) 137 if not value: 138 return value 139 except NoSuchElementException: 140 return True 141 except StaleElementReferenceException: 142 pass 143 144 time.sleep(self._poll) 145 if time.time() > end_time: 146 break 147 148 raise TimeoutException(message) 149 150class SeleniumTestCaseBase(unittest.TestCase): 151 """ 152 NB StaticLiveServerTestCase is used as the base test case so that 153 static files are served correctly in a Selenium test run context; see 154 https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing 155 """ 156 157 @classmethod 158 def setUpClass(cls): 159 """ Create a webdriver driver at the class level """ 160 161 super(SeleniumTestCaseBase, cls).setUpClass() 162 163 # instantiate the Selenium webdriver once for all the test methods 164 # in this test case 165 cls.driver = create_selenium_driver(cls) 166 cls.driver.maximize_window() 167 168 @classmethod 169 def tearDownClass(cls): 170 """ Clean up webdriver driver """ 171 172 cls.driver.quit() 173 # Allow driver resources to be properly freed before proceeding with further tests 174 time.sleep(5) 175 super(SeleniumTestCaseBase, cls).tearDownClass() 176 177 def get(self, url): 178 """ 179 Selenium requires absolute URLs, so convert Django URLs returned 180 by resolve() or similar to absolute ones and get using the 181 webdriver instance. 182 183 url: a relative URL 184 """ 185 abs_url = '%s%s' % (self.live_server_url, url) 186 self.driver.get(abs_url) 187 188 try: # Ensure page is loaded before proceeding 189 self.wait_until_visible("#global-nav") 190 except NoSuchElementException: 191 self.driver.implicitly_wait(3) 192 except TimeoutException: 193 self.driver.implicitly_wait(3) 194 195 def find(self, selector): 196 """ Find single element by CSS selector """ 197 return self.driver.find_element(By.CSS_SELECTOR, selector) 198 199 def find_all(self, selector): 200 """ Find all elements matching CSS selector """ 201 return self.driver.find_elements(By.CSS_SELECTOR, selector) 202 203 def element_exists(self, selector): 204 """ 205 Return True if one element matching selector exists, 206 False otherwise 207 """ 208 return len(self.find_all(selector)) == 1 209 210 def focused_element(self): 211 """ Return the element which currently has focus on the page """ 212 return self.driver.switch_to.active_element 213 214 def wait_until_present(self, selector, timeout=Wait._TIMEOUT): 215 """ Wait until element matching CSS selector is on the page """ 216 is_present = lambda driver: self.find(selector) 217 msg = 'An element matching "%s" should be on the page' % selector 218 element = Wait(self.driver, timeout=timeout).until(is_present, msg) 219 return element 220 221 def wait_until_visible(self, selector, timeout=Wait._TIMEOUT): 222 """ Wait until element matching CSS selector is visible on the page """ 223 is_visible = lambda driver: self.find(selector).is_displayed() 224 msg = 'An element matching "%s" should be visible' % selector 225 Wait(self.driver, timeout=timeout).until(is_visible, msg) 226 return self.find(selector) 227 228 def wait_until_not_visible(self, selector, timeout=Wait._TIMEOUT): 229 """ Wait until element matching CSS selector is not visible on the page """ 230 is_visible = lambda driver: self.find(selector).is_displayed() 231 msg = 'An element matching "%s" should be visible' % selector 232 Wait(self.driver, timeout=timeout).until_not(is_visible, msg) 233 return self.find(selector) 234 235 def wait_until_clickable(self, selector, timeout=Wait._TIMEOUT): 236 """ Wait until element matching CSS selector is visible on the page """ 237 WebDriverWait(self.driver, timeout=timeout).until(lambda driver: self.driver.execute_script("return jQuery.active == 0")) 238 is_clickable = lambda driver: (self.find(selector).is_displayed() and self.find(selector).is_enabled()) 239 msg = 'An element matching "%s" should be clickable' % selector 240 Wait(self.driver, timeout=timeout).until(is_clickable, msg) 241 return self.find(selector) 242 243 def wait_until_element_clickable(self, finder, timeout=Wait._TIMEOUT): 244 """ Wait until element is clickable """ 245 WebDriverWait(self.driver, timeout=timeout).until(lambda driver: self.driver.execute_script("return jQuery.active == 0")) 246 is_clickable = lambda driver: (finder(driver).is_displayed() and finder(driver).is_enabled()) 247 msg = 'A matching element never became be clickable' 248 Wait(self.driver, timeout=timeout).until(is_clickable, msg) 249 return finder(self.driver) 250 251 def wait_until_focused(self, selector): 252 """ Wait until element matching CSS selector has focus """ 253 is_focused = \ 254 lambda driver: self.find(selector) == self.focused_element() 255 msg = 'An element matching "%s" should be focused' % selector 256 Wait(self.driver).until(is_focused, msg) 257 return self.find(selector) 258 259 def enter_text(self, selector, value): 260 """ Insert text into element matching selector """ 261 # note that keyup events don't occur until the element is clicked 262 # (in the case of <input type="text"...>, for example), so simulate 263 # user clicking the element before inserting text into it 264 field = self.click(selector) 265 266 field.send_keys(value) 267 return field 268 269 def click(self, selector): 270 """ Click on element which matches CSS selector """ 271 element = self.wait_until_visible(selector) 272 element.click() 273 return element 274 275 def get_page_source(self): 276 """ Get raw HTML for the current page """ 277 return self.driver.page_source 278