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