xref: /openbmc/openbmc/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py (revision 8460358c3d24c71d9d38fd126c745854a6301564)
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