1#! /usr/bin/env python
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
22from django.contrib.staticfiles.testing import StaticLiveServerTestCase
23from selenium import webdriver
24from selenium.webdriver.support.ui import WebDriverWait
25from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
26from selenium.common.exceptions import NoSuchElementException, \
27        StaleElementReferenceException, TimeoutException
28
29def create_selenium_driver(cls,browser='chrome'):
30    # set default browser string based on env (if available)
31    env_browser = os.environ.get('TOASTER_TESTS_BROWSER')
32    if env_browser:
33        browser = env_browser
34
35    if browser == 'chrome':
36        return webdriver.Chrome(
37            service_args=["--verbose", "--log-path=selenium.log"]
38        )
39    elif browser == 'firefox':
40        return webdriver.Firefox()
41    elif browser == 'marionette':
42        capabilities = DesiredCapabilities.FIREFOX
43        capabilities['marionette'] = True
44        return webdriver.Firefox(capabilities=capabilities)
45    elif browser == 'ie':
46        return webdriver.Ie()
47    elif browser == 'phantomjs':
48        return webdriver.PhantomJS()
49    elif browser == 'remote':
50        # if we were to add yet another env variable like TOASTER_REMOTE_BROWSER
51        # we could let people pick firefox or chrome, left for later
52        remote_hub= os.environ.get('TOASTER_REMOTE_HUB')
53        driver = webdriver.Remote(remote_hub,
54                                  webdriver.DesiredCapabilities.FIREFOX.copy())
55
56        driver.get("http://%s:%s"%(cls.server_thread.host,cls.server_thread.port))
57        return driver
58    else:
59        msg = 'Selenium driver for browser %s is not available' % browser
60        raise RuntimeError(msg)
61
62class Wait(WebDriverWait):
63    """
64    Subclass of WebDriverWait with predetermined timeout and poll
65    frequency. Also deals with a wider variety of exceptions.
66    """
67    _TIMEOUT = 10
68    _POLL_FREQUENCY = 0.5
69
70    def __init__(self, driver):
71        super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY)
72
73    def until(self, method, message=''):
74        """
75        Calls the method provided with the driver as an argument until the
76        return value is not False.
77        """
78
79        end_time = time.time() + self._timeout
80        while True:
81            try:
82                value = method(self._driver)
83                if value:
84                    return value
85            except NoSuchElementException:
86                pass
87            except StaleElementReferenceException:
88                pass
89
90            time.sleep(self._poll)
91            if time.time() > end_time:
92                break
93
94        raise TimeoutException(message)
95
96    def until_not(self, method, message=''):
97        """
98        Calls the method provided with the driver as an argument until the
99        return value is False.
100        """
101
102        end_time = time.time() + self._timeout
103        while True:
104            try:
105                value = method(self._driver)
106                if not value:
107                    return value
108            except NoSuchElementException:
109                return True
110            except StaleElementReferenceException:
111                pass
112
113            time.sleep(self._poll)
114            if time.time() > end_time:
115                break
116
117        raise TimeoutException(message)
118
119class SeleniumTestCaseBase(unittest.TestCase):
120    """
121    NB StaticLiveServerTestCase is used as the base test case so that
122    static files are served correctly in a Selenium test run context; see
123    https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing
124    """
125
126    @classmethod
127    def setUpClass(cls):
128        """ Create a webdriver driver at the class level """
129
130        super(SeleniumTestCaseBase, cls).setUpClass()
131
132        # instantiate the Selenium webdriver once for all the test methods
133        # in this test case
134        cls.driver = create_selenium_driver(cls)
135        cls.driver.maximize_window()
136
137    @classmethod
138    def tearDownClass(cls):
139        """ Clean up webdriver driver """
140
141        cls.driver.quit()
142        super(SeleniumTestCaseBase, cls).tearDownClass()
143
144    def get(self, url):
145        """
146        Selenium requires absolute URLs, so convert Django URLs returned
147        by resolve() or similar to absolute ones and get using the
148        webdriver instance.
149
150        url: a relative URL
151        """
152        abs_url = '%s%s' % (self.live_server_url, url)
153        self.driver.get(abs_url)
154
155    def find(self, selector):
156        """ Find single element by CSS selector """
157        return self.driver.find_element_by_css_selector(selector)
158
159    def find_all(self, selector):
160        """ Find all elements matching CSS selector """
161        return self.driver.find_elements_by_css_selector(selector)
162
163    def element_exists(self, selector):
164        """
165        Return True if one element matching selector exists,
166        False otherwise
167        """
168        return len(self.find_all(selector)) == 1
169
170    def focused_element(self):
171        """ Return the element which currently has focus on the page """
172        return self.driver.switch_to.active_element
173
174    def wait_until_present(self, selector):
175        """ Wait until element matching CSS selector is on the page """
176        is_present = lambda driver: self.find(selector)
177        msg = 'An element matching "%s" should be on the page' % selector
178        element = Wait(self.driver).until(is_present, msg)
179        return element
180
181    def wait_until_visible(self, selector):
182        """ Wait until element matching CSS selector is visible on the page """
183        is_visible = lambda driver: self.find(selector).is_displayed()
184        msg = 'An element matching "%s" should be visible' % selector
185        Wait(self.driver).until(is_visible, msg)
186        return self.find(selector)
187
188    def wait_until_focused(self, selector):
189        """ Wait until element matching CSS selector has focus """
190        is_focused = \
191            lambda driver: self.find(selector) == self.focused_element()
192        msg = 'An element matching "%s" should be focused' % selector
193        Wait(self.driver).until(is_focused, msg)
194        return self.find(selector)
195
196    def enter_text(self, selector, value):
197        """ Insert text into element matching selector """
198        # note that keyup events don't occur until the element is clicked
199        # (in the case of <input type="text"...>, for example), so simulate
200        # user clicking the element before inserting text into it
201        field = self.click(selector)
202
203        field.send_keys(value)
204        return field
205
206    def click(self, selector):
207        """ Click on element which matches CSS selector """
208        element = self.wait_until_visible(selector)
209        element.click()
210        return element
211
212    def get_page_source(self):
213        """ Get raw HTML for the current page """
214        return self.driver.page_source
215