blob: 393be75496ccf80ad8af3fad4e2d7e5f3e43b2cc [file] [log] [blame]
Brad Bishop96ff1982019-08-19 13:50:42 -04001#! /usr/bin/env python3
Patrick Williamsc0f7c042017-02-23 20:41:17 -06002#
3# BitBake Toaster Implementation
4#
5# Copyright (C) 2013-2016 Intel Corporation
6#
Brad Bishopc342db32019-05-15 21:57:59 -04007# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsc0f7c042017-02-23 20:41:17 -06008#
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
Patrick Williams169d7bc2024-01-05 11:33:25 -060022import pytest
Patrick Williamsc0f7c042017-02-23 20:41:17 -060023from selenium import webdriver
Patrick Williams169d7bc2024-01-05 11:33:25 -060024from selenium.webdriver.support import expected_conditions as EC
Patrick Williamsc0f7c042017-02-23 20:41:17 -060025from selenium.webdriver.support.ui import WebDriverWait
Andrew Geissler20137392023-10-12 04:59:14 -060026from selenium.webdriver.common.by import By
Patrick Williamsc0f7c042017-02-23 20:41:17 -060027from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
28from selenium.common.exceptions import NoSuchElementException, \
Patrick Williams169d7bc2024-01-05 11:33:25 -060029 StaleElementReferenceException, TimeoutException, \
30 SessionNotCreatedException
Patrick Williamsc0f7c042017-02-23 20:41:17 -060031
Brad Bishop6e60e8b2018-02-01 10:27:11 -050032def create_selenium_driver(cls,browser='chrome'):
Patrick Williamsc0f7c042017-02-23 20:41:17 -060033 # 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':
Patrick Williamsac13d5f2023-11-24 18:59:46 -060039 options = webdriver.ChromeOptions()
Patrick Williams169d7bc2024-01-05 11:33:25 -060040 options.add_argument('--headless')
Patrick Williamsac13d5f2023-11-24 18:59:46 -060041 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')
Patrick Williams169d7bc2024-01-05 11:33:25 -060045 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}")
Patrick Williamsc0f7c042017-02-23 20:41:17 -060065 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()
Brad Bishop6e60e8b2018-02-01 10:27:11 -050075 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -060084 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
Patrick Williamsda295312023-12-05 16:48:56 -060096 def __init__(self, driver, timeout=_TIMEOUT, poll=_POLL_FREQUENCY):
97 self._TIMEOUT = timeout
98 self._POLL_FREQUENCY = poll
Patrick Williamsc0f7c042017-02-23 20:41:17 -060099 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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500162 cls.driver = create_selenium_driver(cls)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600163 cls.driver.maximize_window()
164
165 @classmethod
166 def tearDownClass(cls):
167 """ Clean up webdriver driver """
168
169 cls.driver.quit()
Patrick Williams169d7bc2024-01-05 11:33:25 -0600170 # Allow driver resources to be properly freed before proceeding with further tests
171 time.sleep(5)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600172 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
Patrick Williams705982a2024-01-12 09:51:57 -0600185 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600192 def find(self, selector):
193 """ Find single element by CSS selector """
Andrew Geissler20137392023-10-12 04:59:14 -0600194 return self.driver.find_element(By.CSS_SELECTOR, selector)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600195
196 def find_all(self, selector):
197 """ Find all elements matching CSS selector """
Andrew Geissler20137392023-10-12 04:59:14 -0600198 return self.driver.find_elements(By.CSS_SELECTOR, selector)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600199
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
Patrick Williamsda295312023-12-05 16:48:56 -0600211 def wait_until_present(self, selector, poll=0.5):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600212 """ 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
Patrick Williamsda295312023-12-05 16:48:56 -0600215 element = Wait(self.driver, poll=poll).until(is_present, msg)
Patrick Williams169d7bc2024-01-05 11:33:25 -0600216 if poll > 2:
217 time.sleep(poll) # element need more delay to be present
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600218 return element
219
Patrick Williamsda295312023-12-05 16:48:56 -0600220 def wait_until_visible(self, selector, poll=1):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600221 """ 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
Patrick Williamsda295312023-12-05 16:48:56 -0600224 Wait(self.driver, poll=poll).until(is_visible, msg)
225 time.sleep(poll) # wait for visibility to settle
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600226 return self.find(selector)
227
Patrick Williams169d7bc2024-01-05 11:33:25 -0600228 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600241 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