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 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 = 10 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 118 time.sleep(self._poll) 119 if time.time() > end_time: 120 break 121 122 raise TimeoutException(message) 123 124 def until_not(self, method, message=''): 125 """ 126 Calls the method provided with the driver as an argument until the 127 return value is False. 128 """ 129 130 end_time = time.time() + self._timeout 131 while True: 132 try: 133 value = method(self._driver) 134 if not value: 135 return value 136 except NoSuchElementException: 137 return True 138 except StaleElementReferenceException: 139 pass 140 141 time.sleep(self._poll) 142 if time.time() > end_time: 143 break 144 145 raise TimeoutException(message) 146 147class SeleniumTestCaseBase(unittest.TestCase): 148 """ 149 NB StaticLiveServerTestCase is used as the base test case so that 150 static files are served correctly in a Selenium test run context; see 151 https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing 152 """ 153 154 @classmethod 155 def setUpClass(cls): 156 """ Create a webdriver driver at the class level """ 157 158 super(SeleniumTestCaseBase, cls).setUpClass() 159 160 # instantiate the Selenium webdriver once for all the test methods 161 # in this test case 162 cls.driver = create_selenium_driver(cls) 163 cls.driver.maximize_window() 164 165 @classmethod 166 def tearDownClass(cls): 167 """ Clean up webdriver driver """ 168 169 cls.driver.quit() 170 # Allow driver resources to be properly freed before proceeding with further tests 171 time.sleep(5) 172 super(SeleniumTestCaseBase, cls).tearDownClass() 173 174 def get(self, url): 175 """ 176 Selenium requires absolute URLs, so convert Django URLs returned 177 by resolve() or similar to absolute ones and get using the 178 webdriver instance. 179 180 url: a relative URL 181 """ 182 abs_url = '%s%s' % (self.live_server_url, url) 183 self.driver.get(abs_url) 184 185 try: # Ensure page is loaded before proceeding 186 self.wait_until_visible("#global-nav", poll=3) 187 except NoSuchElementException: 188 self.driver.implicitly_wait(3) 189 except TimeoutException: 190 self.driver.implicitly_wait(3) 191 192 def find(self, selector): 193 """ Find single element by CSS selector """ 194 return self.driver.find_element(By.CSS_SELECTOR, selector) 195 196 def find_all(self, selector): 197 """ Find all elements matching CSS selector """ 198 return self.driver.find_elements(By.CSS_SELECTOR, selector) 199 200 def element_exists(self, selector): 201 """ 202 Return True if one element matching selector exists, 203 False otherwise 204 """ 205 return len(self.find_all(selector)) == 1 206 207 def focused_element(self): 208 """ Return the element which currently has focus on the page """ 209 return self.driver.switch_to.active_element 210 211 def wait_until_present(self, selector, poll=0.5): 212 """ Wait until element matching CSS selector is on the page """ 213 is_present = lambda driver: self.find(selector) 214 msg = 'An element matching "%s" should be on the page' % selector 215 element = Wait(self.driver, poll=poll).until(is_present, msg) 216 if poll > 2: 217 time.sleep(poll) # element need more delay to be present 218 return element 219 220 def wait_until_visible(self, selector, poll=1): 221 """ Wait until element matching CSS selector is visible on the page """ 222 is_visible = lambda driver: self.find(selector).is_displayed() 223 msg = 'An element matching "%s" should be visible' % selector 224 Wait(self.driver, poll=poll).until(is_visible, msg) 225 time.sleep(poll) # wait for visibility to settle 226 return self.find(selector) 227 228 def wait_until_clickable(self, selector, poll=1): 229 """ Wait until element matching CSS selector is visible on the page """ 230 WebDriverWait( 231 self.driver, 232 Wait._TIMEOUT, 233 poll_frequency=poll 234 ).until( 235 EC.element_to_be_clickable((By.ID, selector.removeprefix('#') 236 ) 237 ) 238 ) 239 return self.find(selector) 240 241 def wait_until_focused(self, selector): 242 """ Wait until element matching CSS selector has focus """ 243 is_focused = \ 244 lambda driver: self.find(selector) == self.focused_element() 245 msg = 'An element matching "%s" should be focused' % selector 246 Wait(self.driver).until(is_focused, msg) 247 return self.find(selector) 248 249 def enter_text(self, selector, value): 250 """ Insert text into element matching selector """ 251 # note that keyup events don't occur until the element is clicked 252 # (in the case of <input type="text"...>, for example), so simulate 253 # user clicking the element before inserting text into it 254 field = self.click(selector) 255 256 field.send_keys(value) 257 return field 258 259 def click(self, selector): 260 """ Click on element which matches CSS selector """ 261 element = self.wait_until_visible(selector) 262 element.click() 263 return element 264 265 def get_page_source(self): 266 """ Get raw HTML for the current page """ 267 return self.driver.page_source 268