196ff1984SBrad Bishop#! /usr/bin/env python3 2eb8dc403SDave Cobbley# 3eb8dc403SDave Cobbley# BitBake Toaster Implementation 4eb8dc403SDave Cobbley# 5eb8dc403SDave Cobbley# Copyright (C) 2013-2016 Intel Corporation 6eb8dc403SDave Cobbley# 7c342db35SBrad Bishop# SPDX-License-Identifier: GPL-2.0-only 8eb8dc403SDave Cobbley# 9eb8dc403SDave Cobbley# The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are 10eb8dc403SDave Cobbley# modified from Patchwork, released under the same licence terms as Toaster: 11eb8dc403SDave Cobbley# https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py 12eb8dc403SDave Cobbley 13eb8dc403SDave Cobbley""" 14eb8dc403SDave CobbleyHelper methods for creating Toaster Selenium tests which run within 15eb8dc403SDave Cobbleythe context of Django unit tests. 16eb8dc403SDave Cobbley""" 17eb8dc403SDave Cobbley 18eb8dc403SDave Cobbleyimport os 19eb8dc403SDave Cobbleyimport time 20eb8dc403SDave Cobbleyimport unittest 21eb8dc403SDave Cobbley 22169d7bccSPatrick Williamsimport pytest 23eb8dc403SDave Cobbleyfrom selenium import webdriver 24169d7bccSPatrick Williamsfrom selenium.webdriver.support import expected_conditions as EC 25eb8dc403SDave Cobbleyfrom selenium.webdriver.support.ui import WebDriverWait 2620137395SAndrew Geisslerfrom selenium.webdriver.common.by import By 27eb8dc403SDave Cobbleyfrom selenium.webdriver.common.desired_capabilities import DesiredCapabilities 28eb8dc403SDave Cobbleyfrom selenium.common.exceptions import NoSuchElementException, \ 29169d7bccSPatrick Williams StaleElementReferenceException, TimeoutException, \ 30169d7bccSPatrick Williams SessionNotCreatedException 31eb8dc403SDave Cobbley 32eb8dc403SDave Cobbleydef create_selenium_driver(cls,browser='chrome'): 33eb8dc403SDave Cobbley # set default browser string based on env (if available) 34eb8dc403SDave Cobbley env_browser = os.environ.get('TOASTER_TESTS_BROWSER') 35eb8dc403SDave Cobbley if env_browser: 36eb8dc403SDave Cobbley browser = env_browser 37eb8dc403SDave Cobbley 38eb8dc403SDave Cobbley if browser == 'chrome': 39ac13d5f3SPatrick Williams options = webdriver.ChromeOptions() 40169d7bccSPatrick Williams options.add_argument('--headless') 41ac13d5f3SPatrick Williams options.add_argument('--disable-infobars') 42ac13d5f3SPatrick Williams options.add_argument('--disable-dev-shm-usage') 43ac13d5f3SPatrick Williams options.add_argument('--no-sandbox') 44ac13d5f3SPatrick Williams options.add_argument('--remote-debugging-port=9222') 45169d7bccSPatrick Williams try: 46ac13d5f3SPatrick Williams return webdriver.Chrome(options=options) 47169d7bccSPatrick Williams except SessionNotCreatedException as e: 48169d7bccSPatrick Williams exit_message = "Halting tests prematurely to avoid cascading errors." 49169d7bccSPatrick Williams # check if chrome / chromedriver exists 50169d7bccSPatrick Williams chrome_path = os.popen("find ~/.cache/selenium/chrome/ -name 'chrome' -type f -print -quit").read().strip() 51169d7bccSPatrick Williams if not chrome_path: 52169d7bccSPatrick Williams pytest.exit(f"Failed to install/find chrome.\n{exit_message}") 53169d7bccSPatrick Williams chromedriver_path = os.popen("find ~/.cache/selenium/chromedriver/ -name 'chromedriver' -type f -print -quit").read().strip() 54169d7bccSPatrick Williams if not chromedriver_path: 55169d7bccSPatrick Williams pytest.exit(f"Failed to install/find chromedriver.\n{exit_message}") 56169d7bccSPatrick Williams # check if depends on each are fulfilled 57169d7bccSPatrick Williams depends_chrome = os.popen(f"ldd {chrome_path} | grep 'not found'").read().strip() 58169d7bccSPatrick Williams if depends_chrome: 59169d7bccSPatrick Williams pytest.exit(f"Missing chrome dependencies.\n{depends_chrome}\n{exit_message}") 60169d7bccSPatrick Williams depends_chromedriver = os.popen(f"ldd {chromedriver_path} | grep 'not found'").read().strip() 61169d7bccSPatrick Williams if depends_chromedriver: 62169d7bccSPatrick Williams pytest.exit(f"Missing chromedriver dependencies.\n{depends_chromedriver}\n{exit_message}") 63169d7bccSPatrick Williams # print original error otherwise 64169d7bccSPatrick Williams pytest.exit(f"Failed to start chromedriver.\n{e}\n{exit_message}") 65eb8dc403SDave Cobbley elif browser == 'firefox': 66eb8dc403SDave Cobbley return webdriver.Firefox() 67eb8dc403SDave Cobbley elif browser == 'marionette': 68eb8dc403SDave Cobbley capabilities = DesiredCapabilities.FIREFOX 69eb8dc403SDave Cobbley capabilities['marionette'] = True 70eb8dc403SDave Cobbley return webdriver.Firefox(capabilities=capabilities) 71eb8dc403SDave Cobbley elif browser == 'ie': 72eb8dc403SDave Cobbley return webdriver.Ie() 73eb8dc403SDave Cobbley elif browser == 'phantomjs': 74eb8dc403SDave Cobbley return webdriver.PhantomJS() 75eb8dc403SDave Cobbley elif browser == 'remote': 76eb8dc403SDave Cobbley # if we were to add yet another env variable like TOASTER_REMOTE_BROWSER 77eb8dc403SDave Cobbley # we could let people pick firefox or chrome, left for later 78eb8dc403SDave Cobbley remote_hub= os.environ.get('TOASTER_REMOTE_HUB') 79eb8dc403SDave Cobbley driver = webdriver.Remote(remote_hub, 80eb8dc403SDave Cobbley webdriver.DesiredCapabilities.FIREFOX.copy()) 81eb8dc403SDave Cobbley 82eb8dc403SDave Cobbley driver.get("http://%s:%s"%(cls.server_thread.host,cls.server_thread.port)) 83eb8dc403SDave Cobbley return driver 84eb8dc403SDave Cobbley else: 85eb8dc403SDave Cobbley msg = 'Selenium driver for browser %s is not available' % browser 86eb8dc403SDave Cobbley raise RuntimeError(msg) 87eb8dc403SDave Cobbley 88eb8dc403SDave Cobbleyclass Wait(WebDriverWait): 89eb8dc403SDave Cobbley """ 90eb8dc403SDave Cobbley Subclass of WebDriverWait with predetermined timeout and poll 91eb8dc403SDave Cobbley frequency. Also deals with a wider variety of exceptions. 92eb8dc403SDave Cobbley """ 93eb8dc403SDave Cobbley _TIMEOUT = 10 94eb8dc403SDave Cobbley _POLL_FREQUENCY = 0.5 95eb8dc403SDave Cobbley 96da295319SPatrick Williams def __init__(self, driver, timeout=_TIMEOUT, poll=_POLL_FREQUENCY): 97da295319SPatrick Williams self._TIMEOUT = timeout 98da295319SPatrick Williams self._POLL_FREQUENCY = poll 99eb8dc403SDave Cobbley super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) 100eb8dc403SDave Cobbley 101eb8dc403SDave Cobbley def until(self, method, message=''): 102eb8dc403SDave Cobbley """ 103eb8dc403SDave Cobbley Calls the method provided with the driver as an argument until the 104eb8dc403SDave Cobbley return value is not False. 105eb8dc403SDave Cobbley """ 106eb8dc403SDave Cobbley 107eb8dc403SDave Cobbley end_time = time.time() + self._timeout 108eb8dc403SDave Cobbley while True: 109eb8dc403SDave Cobbley try: 110eb8dc403SDave Cobbley value = method(self._driver) 111eb8dc403SDave Cobbley if value: 112eb8dc403SDave Cobbley return value 113eb8dc403SDave Cobbley except NoSuchElementException: 114eb8dc403SDave Cobbley pass 115eb8dc403SDave Cobbley except StaleElementReferenceException: 116eb8dc403SDave Cobbley pass 117eb8dc403SDave Cobbley 118eb8dc403SDave Cobbley time.sleep(self._poll) 119eb8dc403SDave Cobbley if time.time() > end_time: 120eb8dc403SDave Cobbley break 121eb8dc403SDave Cobbley 122eb8dc403SDave Cobbley raise TimeoutException(message) 123eb8dc403SDave Cobbley 124eb8dc403SDave Cobbley def until_not(self, method, message=''): 125eb8dc403SDave Cobbley """ 126eb8dc403SDave Cobbley Calls the method provided with the driver as an argument until the 127eb8dc403SDave Cobbley return value is False. 128eb8dc403SDave Cobbley """ 129eb8dc403SDave Cobbley 130eb8dc403SDave Cobbley end_time = time.time() + self._timeout 131eb8dc403SDave Cobbley while True: 132eb8dc403SDave Cobbley try: 133eb8dc403SDave Cobbley value = method(self._driver) 134eb8dc403SDave Cobbley if not value: 135eb8dc403SDave Cobbley return value 136eb8dc403SDave Cobbley except NoSuchElementException: 137eb8dc403SDave Cobbley return True 138eb8dc403SDave Cobbley except StaleElementReferenceException: 139eb8dc403SDave Cobbley pass 140eb8dc403SDave Cobbley 141eb8dc403SDave Cobbley time.sleep(self._poll) 142eb8dc403SDave Cobbley if time.time() > end_time: 143eb8dc403SDave Cobbley break 144eb8dc403SDave Cobbley 145eb8dc403SDave Cobbley raise TimeoutException(message) 146eb8dc403SDave Cobbley 147eb8dc403SDave Cobbleyclass SeleniumTestCaseBase(unittest.TestCase): 148eb8dc403SDave Cobbley """ 149eb8dc403SDave Cobbley NB StaticLiveServerTestCase is used as the base test case so that 150eb8dc403SDave Cobbley static files are served correctly in a Selenium test run context; see 151eb8dc403SDave Cobbley https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing 152eb8dc403SDave Cobbley """ 153eb8dc403SDave Cobbley 154eb8dc403SDave Cobbley @classmethod 155eb8dc403SDave Cobbley def setUpClass(cls): 156eb8dc403SDave Cobbley """ Create a webdriver driver at the class level """ 157eb8dc403SDave Cobbley 158eb8dc403SDave Cobbley super(SeleniumTestCaseBase, cls).setUpClass() 159eb8dc403SDave Cobbley 160eb8dc403SDave Cobbley # instantiate the Selenium webdriver once for all the test methods 161eb8dc403SDave Cobbley # in this test case 162eb8dc403SDave Cobbley cls.driver = create_selenium_driver(cls) 163eb8dc403SDave Cobbley cls.driver.maximize_window() 164eb8dc403SDave Cobbley 165eb8dc403SDave Cobbley @classmethod 166eb8dc403SDave Cobbley def tearDownClass(cls): 167eb8dc403SDave Cobbley """ Clean up webdriver driver """ 168eb8dc403SDave Cobbley 169eb8dc403SDave Cobbley cls.driver.quit() 170169d7bccSPatrick Williams # Allow driver resources to be properly freed before proceeding with further tests 171169d7bccSPatrick Williams time.sleep(5) 172eb8dc403SDave Cobbley super(SeleniumTestCaseBase, cls).tearDownClass() 173eb8dc403SDave Cobbley 174eb8dc403SDave Cobbley def get(self, url): 175eb8dc403SDave Cobbley """ 176eb8dc403SDave Cobbley Selenium requires absolute URLs, so convert Django URLs returned 177eb8dc403SDave Cobbley by resolve() or similar to absolute ones and get using the 178eb8dc403SDave Cobbley webdriver instance. 179eb8dc403SDave Cobbley 180eb8dc403SDave Cobbley url: a relative URL 181eb8dc403SDave Cobbley """ 182eb8dc403SDave Cobbley abs_url = '%s%s' % (self.live_server_url, url) 183eb8dc403SDave Cobbley self.driver.get(abs_url) 184eb8dc403SDave Cobbley 185*705982a5SPatrick Williams try: # Ensure page is loaded before proceeding 186*705982a5SPatrick Williams self.wait_until_visible("#global-nav", poll=3) 187*705982a5SPatrick Williams except NoSuchElementException: 188*705982a5SPatrick Williams self.driver.implicitly_wait(3) 189*705982a5SPatrick Williams except TimeoutException: 190*705982a5SPatrick Williams self.driver.implicitly_wait(3) 191*705982a5SPatrick Williams 192eb8dc403SDave Cobbley def find(self, selector): 193eb8dc403SDave Cobbley """ Find single element by CSS selector """ 19420137395SAndrew Geissler return self.driver.find_element(By.CSS_SELECTOR, selector) 195eb8dc403SDave Cobbley 196eb8dc403SDave Cobbley def find_all(self, selector): 197eb8dc403SDave Cobbley """ Find all elements matching CSS selector """ 19820137395SAndrew Geissler return self.driver.find_elements(By.CSS_SELECTOR, selector) 199eb8dc403SDave Cobbley 200eb8dc403SDave Cobbley def element_exists(self, selector): 201eb8dc403SDave Cobbley """ 202eb8dc403SDave Cobbley Return True if one element matching selector exists, 203eb8dc403SDave Cobbley False otherwise 204eb8dc403SDave Cobbley """ 205eb8dc403SDave Cobbley return len(self.find_all(selector)) == 1 206eb8dc403SDave Cobbley 207eb8dc403SDave Cobbley def focused_element(self): 208eb8dc403SDave Cobbley """ Return the element which currently has focus on the page """ 209eb8dc403SDave Cobbley return self.driver.switch_to.active_element 210eb8dc403SDave Cobbley 211da295319SPatrick Williams def wait_until_present(self, selector, poll=0.5): 212eb8dc403SDave Cobbley """ Wait until element matching CSS selector is on the page """ 213eb8dc403SDave Cobbley is_present = lambda driver: self.find(selector) 214eb8dc403SDave Cobbley msg = 'An element matching "%s" should be on the page' % selector 215da295319SPatrick Williams element = Wait(self.driver, poll=poll).until(is_present, msg) 216169d7bccSPatrick Williams if poll > 2: 217169d7bccSPatrick Williams time.sleep(poll) # element need more delay to be present 218eb8dc403SDave Cobbley return element 219eb8dc403SDave Cobbley 220da295319SPatrick Williams def wait_until_visible(self, selector, poll=1): 221eb8dc403SDave Cobbley """ Wait until element matching CSS selector is visible on the page """ 222eb8dc403SDave Cobbley is_visible = lambda driver: self.find(selector).is_displayed() 223eb8dc403SDave Cobbley msg = 'An element matching "%s" should be visible' % selector 224da295319SPatrick Williams Wait(self.driver, poll=poll).until(is_visible, msg) 225da295319SPatrick Williams time.sleep(poll) # wait for visibility to settle 226eb8dc403SDave Cobbley return self.find(selector) 227eb8dc403SDave Cobbley 228169d7bccSPatrick Williams def wait_until_clickable(self, selector, poll=1): 229169d7bccSPatrick Williams """ Wait until element matching CSS selector is visible on the page """ 230169d7bccSPatrick Williams WebDriverWait( 231169d7bccSPatrick Williams self.driver, 232169d7bccSPatrick Williams Wait._TIMEOUT, 233169d7bccSPatrick Williams poll_frequency=poll 234169d7bccSPatrick Williams ).until( 235169d7bccSPatrick Williams EC.element_to_be_clickable((By.ID, selector.removeprefix('#') 236169d7bccSPatrick Williams ) 237169d7bccSPatrick Williams ) 238169d7bccSPatrick Williams ) 239169d7bccSPatrick Williams return self.find(selector) 240169d7bccSPatrick Williams 241eb8dc403SDave Cobbley def wait_until_focused(self, selector): 242eb8dc403SDave Cobbley """ Wait until element matching CSS selector has focus """ 243eb8dc403SDave Cobbley is_focused = \ 244eb8dc403SDave Cobbley lambda driver: self.find(selector) == self.focused_element() 245eb8dc403SDave Cobbley msg = 'An element matching "%s" should be focused' % selector 246eb8dc403SDave Cobbley Wait(self.driver).until(is_focused, msg) 247eb8dc403SDave Cobbley return self.find(selector) 248eb8dc403SDave Cobbley 249eb8dc403SDave Cobbley def enter_text(self, selector, value): 250eb8dc403SDave Cobbley """ Insert text into element matching selector """ 251eb8dc403SDave Cobbley # note that keyup events don't occur until the element is clicked 252eb8dc403SDave Cobbley # (in the case of <input type="text"...>, for example), so simulate 253eb8dc403SDave Cobbley # user clicking the element before inserting text into it 254eb8dc403SDave Cobbley field = self.click(selector) 255eb8dc403SDave Cobbley 256eb8dc403SDave Cobbley field.send_keys(value) 257eb8dc403SDave Cobbley return field 258eb8dc403SDave Cobbley 259eb8dc403SDave Cobbley def click(self, selector): 260eb8dc403SDave Cobbley """ Click on element which matches CSS selector """ 261eb8dc403SDave Cobbley element = self.wait_until_visible(selector) 262eb8dc403SDave Cobbley element.click() 263eb8dc403SDave Cobbley return element 264eb8dc403SDave Cobbley 265eb8dc403SDave Cobbley def get_page_source(self): 266eb8dc403SDave Cobbley """ Get raw HTML for the current page """ 267eb8dc403SDave Cobbley return self.driver.page_source 268