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