summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/toaster/tests
diff options
context:
space:
mode:
Diffstat (limited to 'bitbake/lib/toaster/tests')
-rw-r--r--bitbake/lib/toaster/tests/browser/selenium_helpers_base.py76
-rw-r--r--bitbake/lib/toaster/tests/browser/test_all_builds_page.py315
-rw-r--r--bitbake/lib/toaster/tests/browser/test_all_projects_page.py162
-rw-r--r--bitbake/lib/toaster/tests/browser/test_builddashboard_page.py15
-rw-r--r--bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py8
-rw-r--r--bitbake/lib/toaster/tests/browser/test_delete_project.py103
-rw-r--r--bitbake/lib/toaster/tests/browser/test_landing_page.py131
-rw-r--r--bitbake/lib/toaster/tests/browser/test_layerdetails_page.py39
-rw-r--r--bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py24
-rw-r--r--bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py14
-rw-r--r--bitbake/lib/toaster/tests/browser/test_new_project_page.py16
-rw-r--r--bitbake/lib/toaster/tests/browser/test_project_builds_page.py4
-rw-r--r--bitbake/lib/toaster/tests/browser/test_project_config_page.py33
-rw-r--r--bitbake/lib/toaster/tests/browser/test_sample.py10
-rw-r--r--bitbake/lib/toaster/tests/browser/test_toastertable_ui.py11
-rw-r--r--bitbake/lib/toaster/tests/builds/buildtest.py13
-rw-r--r--bitbake/lib/toaster/tests/builds/test_core_image_min.py20
-rw-r--r--bitbake/lib/toaster/tests/commands/test_loaddata.py4
-rw-r--r--bitbake/lib/toaster/tests/commands/test_lsupdates.py3
-rw-r--r--bitbake/lib/toaster/tests/commands/test_runbuilds.py13
-rw-r--r--bitbake/lib/toaster/tests/db/test_db.py3
-rw-r--r--bitbake/lib/toaster/tests/functional/functional_helpers.py82
-rw-r--r--bitbake/lib/toaster/tests/functional/test_create_new_project.py179
-rw-r--r--bitbake/lib/toaster/tests/functional/test_functional_basic.py195
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_config.py341
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_page.py792
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py528
-rw-r--r--bitbake/lib/toaster/tests/functional/utils.py89
-rw-r--r--bitbake/lib/toaster/tests/toaster-tests-requirements.txt8
-rw-r--r--bitbake/lib/toaster/tests/views/test_views.py20
30 files changed, 3005 insertions, 246 deletions
diff --git a/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py b/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py
index 644d45fe58..393be75496 100644
--- a/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py
+++ b/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py
@@ -19,11 +19,15 @@ import os
19import time 19import time
20import unittest 20import unittest
21 21
22import pytest
22from selenium import webdriver 23from selenium import webdriver
24from selenium.webdriver.support import expected_conditions as EC
23from selenium.webdriver.support.ui import WebDriverWait 25from selenium.webdriver.support.ui import WebDriverWait
26from selenium.webdriver.common.by import By
24from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 27from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
25from selenium.common.exceptions import NoSuchElementException, \ 28from selenium.common.exceptions import NoSuchElementException, \
26 StaleElementReferenceException, TimeoutException 29 StaleElementReferenceException, TimeoutException, \
30 SessionNotCreatedException
27 31
28def create_selenium_driver(cls,browser='chrome'): 32def create_selenium_driver(cls,browser='chrome'):
29 # set default browser string based on env (if available) 33 # set default browser string based on env (if available)
@@ -32,9 +36,32 @@ def create_selenium_driver(cls,browser='chrome'):
32 browser = env_browser 36 browser = env_browser
33 37
34 if browser == 'chrome': 38 if browser == 'chrome':
35 return webdriver.Chrome( 39 options = webdriver.ChromeOptions()
36 service_args=["--verbose", "--log-path=selenium.log"] 40 options.add_argument('--headless')
37 ) 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}")
38 elif browser == 'firefox': 65 elif browser == 'firefox':
39 return webdriver.Firefox() 66 return webdriver.Firefox()
40 elif browser == 'marionette': 67 elif browser == 'marionette':
@@ -66,7 +93,9 @@ class Wait(WebDriverWait):
66 _TIMEOUT = 10 93 _TIMEOUT = 10
67 _POLL_FREQUENCY = 0.5 94 _POLL_FREQUENCY = 0.5
68 95
69 def __init__(self, driver): 96 def __init__(self, driver, timeout=_TIMEOUT, poll=_POLL_FREQUENCY):
97 self._TIMEOUT = timeout
98 self._POLL_FREQUENCY = poll
70 super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) 99 super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY)
71 100
72 def until(self, method, message=''): 101 def until(self, method, message=''):
@@ -138,6 +167,8 @@ class SeleniumTestCaseBase(unittest.TestCase):
138 """ Clean up webdriver driver """ 167 """ Clean up webdriver driver """
139 168
140 cls.driver.quit() 169 cls.driver.quit()
170 # Allow driver resources to be properly freed before proceeding with further tests
171 time.sleep(5)
141 super(SeleniumTestCaseBase, cls).tearDownClass() 172 super(SeleniumTestCaseBase, cls).tearDownClass()
142 173
143 def get(self, url): 174 def get(self, url):
@@ -151,13 +182,20 @@ class SeleniumTestCaseBase(unittest.TestCase):
151 abs_url = '%s%s' % (self.live_server_url, url) 182 abs_url = '%s%s' % (self.live_server_url, url)
152 self.driver.get(abs_url) 183 self.driver.get(abs_url)
153 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
154 def find(self, selector): 192 def find(self, selector):
155 """ Find single element by CSS selector """ 193 """ Find single element by CSS selector """
156 return self.driver.find_element_by_css_selector(selector) 194 return self.driver.find_element(By.CSS_SELECTOR, selector)
157 195
158 def find_all(self, selector): 196 def find_all(self, selector):
159 """ Find all elements matching CSS selector """ 197 """ Find all elements matching CSS selector """
160 return self.driver.find_elements_by_css_selector(selector) 198 return self.driver.find_elements(By.CSS_SELECTOR, selector)
161 199
162 def element_exists(self, selector): 200 def element_exists(self, selector):
163 """ 201 """
@@ -170,18 +208,34 @@ class SeleniumTestCaseBase(unittest.TestCase):
170 """ Return the element which currently has focus on the page """ 208 """ Return the element which currently has focus on the page """
171 return self.driver.switch_to.active_element 209 return self.driver.switch_to.active_element
172 210
173 def wait_until_present(self, selector): 211 def wait_until_present(self, selector, poll=0.5):
174 """ Wait until element matching CSS selector is on the page """ 212 """ Wait until element matching CSS selector is on the page """
175 is_present = lambda driver: self.find(selector) 213 is_present = lambda driver: self.find(selector)
176 msg = 'An element matching "%s" should be on the page' % selector 214 msg = 'An element matching "%s" should be on the page' % selector
177 element = Wait(self.driver).until(is_present, msg) 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
178 return element 218 return element
179 219
180 def wait_until_visible(self, selector): 220 def wait_until_visible(self, selector, poll=1):
181 """ Wait until element matching CSS selector is visible on the page """ 221 """ Wait until element matching CSS selector is visible on the page """
182 is_visible = lambda driver: self.find(selector).is_displayed() 222 is_visible = lambda driver: self.find(selector).is_displayed()
183 msg = 'An element matching "%s" should be visible' % selector 223 msg = 'An element matching "%s" should be visible' % selector
184 Wait(self.driver).until(is_visible, msg) 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 )
185 return self.find(selector) 239 return self.find(selector)
186 240
187 def wait_until_focused(self, selector): 241 def wait_until_focused(self, selector):
diff --git a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
index 8423d3dab2..b9356a0344 100644
--- a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
@@ -7,13 +7,18 @@
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9 9
10import os
10import re 11import re
11 12
12from django.urls import reverse 13from django.urls import reverse
14from selenium.webdriver.support.select import Select
13from django.utils import timezone 15from django.utils import timezone
16from bldcontrol.models import BuildRequest
14from tests.browser.selenium_helpers import SeleniumTestCase 17from tests.browser.selenium_helpers import SeleniumTestCase
15 18
16from orm.models import BitbakeVersion, Release, Project, Build, Target 19from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task
20
21from selenium.webdriver.common.by import By
17 22
18 23
19class TestAllBuildsPage(SeleniumTestCase): 24class TestAllBuildsPage(SeleniumTestCase):
@@ -23,7 +28,8 @@ class TestAllBuildsPage(SeleniumTestCase):
23 CLI_BUILDS_PROJECT_NAME = 'command line builds' 28 CLI_BUILDS_PROJECT_NAME = 'command line builds'
24 29
25 def setUp(self): 30 def setUp(self):
26 bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', 31 builldir = os.environ.get('BUILDDIR', './')
32 bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
27 branch='master', dirpath='') 33 branch='master', dirpath='')
28 release = Release.objects.create(name='release1', 34 release = Release.objects.create(name='release1',
29 bitbake_version=bbv) 35 bitbake_version=bbv)
@@ -69,7 +75,7 @@ class TestAllBuildsPage(SeleniumTestCase):
69 '[data-role="data-recent-build-buildtime-field"]' % build.id 75 '[data-role="data-recent-build-buildtime-field"]' % build.id
70 76
71 # because this loads via Ajax, wait for it to be visible 77 # because this loads via Ajax, wait for it to be visible
72 self.wait_until_present(selector) 78 self.wait_until_visible(selector)
73 79
74 build_time_spans = self.find_all(selector) 80 build_time_spans = self.find_all(selector)
75 81
@@ -79,7 +85,7 @@ class TestAllBuildsPage(SeleniumTestCase):
79 85
80 def _get_row_for_build(self, build): 86 def _get_row_for_build(self, build):
81 """ Get the table row for the build from the all builds table """ 87 """ Get the table row for the build from the all builds table """
82 self.wait_until_present('#allbuildstable') 88 self.wait_until_visible('#allbuildstable')
83 89
84 rows = self.find_all('#allbuildstable tr') 90 rows = self.find_all('#allbuildstable tr')
85 91
@@ -91,7 +97,7 @@ class TestAllBuildsPage(SeleniumTestCase):
91 found_row = None 97 found_row = None
92 for row in rows: 98 for row in rows:
93 99
94 outcome_links = row.find_elements_by_css_selector(selector) 100 outcome_links = row.find_elements(By.CSS_SELECTOR, selector)
95 if len(outcome_links) == 1: 101 if len(outcome_links) == 1:
96 found_row = row 102 found_row = row
97 break 103 break
@@ -100,6 +106,66 @@ class TestAllBuildsPage(SeleniumTestCase):
100 106
101 return found_row 107 return found_row
102 108
109 def _get_create_builds(self, **kwargs):
110 """ Create a build and return the build object """
111 build1 = Build.objects.create(**self.project1_build_success)
112 build2 = Build.objects.create(**self.project1_build_failure)
113
114 # add some targets to these builds so they have recipe links
115 # (and so we can find the row in the ToasterTable corresponding to
116 # a particular build)
117 Target.objects.create(build=build1, target='foo')
118 Target.objects.create(build=build2, target='bar')
119
120 if kwargs:
121 # Create kwargs.get('success') builds with success status with target
122 # and kwargs.get('failure') builds with failure status with target
123 for i in range(kwargs.get('success', 0)):
124 now = timezone.now()
125 self.project1_build_success['started_on'] = now
126 self.project1_build_success[
127 'completed_on'] = now - timezone.timedelta(days=i)
128 build = Build.objects.create(**self.project1_build_success)
129 Target.objects.create(build=build,
130 target=f'{i}_success_recipe',
131 task=f'{i}_success_task')
132
133 self._set_buildRequest_and_task_on_build(build)
134 for i in range(kwargs.get('failure', 0)):
135 now = timezone.now()
136 self.project1_build_failure['started_on'] = now
137 self.project1_build_failure[
138 'completed_on'] = now - timezone.timedelta(days=i)
139 build = Build.objects.create(**self.project1_build_failure)
140 Target.objects.create(build=build,
141 target=f'{i}_fail_recipe',
142 task=f'{i}_fail_task')
143 self._set_buildRequest_and_task_on_build(build)
144 return build1, build2
145
146 def _create_recipe(self):
147 """ Add a recipe to the database and return it """
148 layer = Layer.objects.create()
149 layer_version = Layer_Version.objects.create(layer=layer)
150 return Recipe.objects.create(name='recipe_foo', layer_version=layer_version)
151
152 def _set_buildRequest_and_task_on_build(self, build):
153 """ Set buildRequest and task on build """
154 build.recipes_parsed = 1
155 build.save()
156 buildRequest = BuildRequest.objects.create(
157 build=build,
158 project=self.project1,
159 state=BuildRequest.REQ_COMPLETED)
160 build.build_request = buildRequest
161 recipe = self._create_recipe()
162 task = Task.objects.create(build=build,
163 recipe=recipe,
164 task_name='task',
165 outcome=Task.OUTCOME_SUCCESS)
166 task.save()
167 build.save()
168
103 def test_show_tasks_with_suffix(self): 169 def test_show_tasks_with_suffix(self):
104 """ Task should be shown as suffix on build name """ 170 """ Task should be shown as suffix on build name """
105 build = Build.objects.create(**self.project1_build_success) 171 build = Build.objects.create(**self.project1_build_success)
@@ -109,7 +175,7 @@ class TestAllBuildsPage(SeleniumTestCase):
109 175
110 url = reverse('all-builds') 176 url = reverse('all-builds')
111 self.get(url) 177 self.get(url)
112 self.wait_until_present('td[class="target"]') 178 self.wait_until_visible('td[class="target"]')
113 179
114 cell = self.find('td[class="target"]') 180 cell = self.find('td[class="target"]')
115 content = cell.get_attribute('innerHTML') 181 content = cell.get_attribute('innerHTML')
@@ -126,23 +192,25 @@ class TestAllBuildsPage(SeleniumTestCase):
126 but should be shown for other builds 192 but should be shown for other builds
127 """ 193 """
128 build1 = Build.objects.create(**self.project1_build_success) 194 build1 = Build.objects.create(**self.project1_build_success)
129 default_build = Build.objects.create(**self.default_project_build_success) 195 default_build = Build.objects.create(
196 **self.default_project_build_success)
130 197
131 url = reverse('all-builds') 198 url = reverse('all-builds')
132 self.get(url) 199 self.get(url)
133 200
134 # shouldn't see a rebuild button for command-line builds
135 selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id
136 run_again_button = self.find_all(selector)
137 self.assertEqual(len(run_again_button), 0,
138 'should not see a rebuild button for cli builds')
139
140 # should see a rebuild button for non-command-line builds 201 # should see a rebuild button for non-command-line builds
202 self.wait_until_visible('#allbuildstable tbody tr')
141 selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id 203 selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id
142 run_again_button = self.find_all(selector) 204 run_again_button = self.find_all(selector)
143 self.assertEqual(len(run_again_button), 1, 205 self.assertEqual(len(run_again_button), 1,
144 'should see a rebuild button for non-cli builds') 206 'should see a rebuild button for non-cli builds')
145 207
208 # shouldn't see a rebuild button for command-line builds
209 selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id
210 run_again_button = self.find_all(selector)
211 self.assertEqual(len(run_again_button), 0,
212 'should not see a rebuild button for cli builds')
213
146 def test_tooltips_on_project_name(self): 214 def test_tooltips_on_project_name(self):
147 """ 215 """
148 Test tooltips shown next to project name in the main table 216 Test tooltips shown next to project name in the main table
@@ -156,6 +224,7 @@ class TestAllBuildsPage(SeleniumTestCase):
156 224
157 url = reverse('all-builds') 225 url = reverse('all-builds')
158 self.get(url) 226 self.get(url)
227 self.wait_until_visible('#allbuildstable', poll=3)
159 228
160 # get the project name cells from the table 229 # get the project name cells from the table
161 cells = self.find_all('#allbuildstable td[class="project"]') 230 cells = self.find_all('#allbuildstable td[class="project"]')
@@ -164,7 +233,7 @@ class TestAllBuildsPage(SeleniumTestCase):
164 233
165 for cell in cells: 234 for cell in cells:
166 content = cell.get_attribute('innerHTML') 235 content = cell.get_attribute('innerHTML')
167 help_icons = cell.find_elements_by_css_selector(selector) 236 help_icons = cell.find_elements(By.CSS_SELECTOR, selector)
168 237
169 if re.search(self.PROJECT_NAME, content): 238 if re.search(self.PROJECT_NAME, content):
170 # no help icon next to non-cli project name 239 # no help icon next to non-cli project name
@@ -184,38 +253,224 @@ class TestAllBuildsPage(SeleniumTestCase):
184 recent builds area; failed builds should not have links on the time column, 253 recent builds area; failed builds should not have links on the time column,
185 or in the recent builds area 254 or in the recent builds area
186 """ 255 """
187 build1 = Build.objects.create(**self.project1_build_success) 256 build1, build2 = self._get_create_builds()
188 build2 = Build.objects.create(**self.project1_build_failure)
189
190 # add some targets to these builds so they have recipe links
191 # (and so we can find the row in the ToasterTable corresponding to
192 # a particular build)
193 Target.objects.create(build=build1, target='foo')
194 Target.objects.create(build=build2, target='bar')
195 257
196 url = reverse('all-builds') 258 url = reverse('all-builds')
197 self.get(url) 259 self.get(url)
260 self.wait_until_visible('#allbuildstable', poll=3)
198 261
199 # test recent builds area for successful build 262 # test recent builds area for successful build
200 element = self._get_build_time_element(build1) 263 element = self._get_build_time_element(build1)
201 links = element.find_elements_by_css_selector('a') 264 links = element.find_elements(By.CSS_SELECTOR, 'a')
202 msg = 'should be a link on the build time for a successful recent build' 265 msg = 'should be a link on the build time for a successful recent build'
203 self.assertEquals(len(links), 1, msg) 266 self.assertEqual(len(links), 1, msg)
204 267
205 # test recent builds area for failed build 268 # test recent builds area for failed build
206 element = self._get_build_time_element(build2) 269 element = self._get_build_time_element(build2)
207 links = element.find_elements_by_css_selector('a') 270 links = element.find_elements(By.CSS_SELECTOR, 'a')
208 msg = 'should not be a link on the build time for a failed recent build' 271 msg = 'should not be a link on the build time for a failed recent build'
209 self.assertEquals(len(links), 0, msg) 272 self.assertEqual(len(links), 0, msg)
210 273
211 # test the time column for successful build 274 # test the time column for successful build
212 build1_row = self._get_row_for_build(build1) 275 build1_row = self._get_row_for_build(build1)
213 links = build1_row.find_elements_by_css_selector('td.time a') 276 links = build1_row.find_elements(By.CSS_SELECTOR, 'td.time a')
214 msg = 'should be a link on the build time for a successful build' 277 msg = 'should be a link on the build time for a successful build'
215 self.assertEquals(len(links), 1, msg) 278 self.assertEqual(len(links), 1, msg)
216 279
217 # test the time column for failed build 280 # test the time column for failed build
218 build2_row = self._get_row_for_build(build2) 281 build2_row = self._get_row_for_build(build2)
219 links = build2_row.find_elements_by_css_selector('td.time a') 282 links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a')
220 msg = 'should not be a link on the build time for a failed build' 283 msg = 'should not be a link on the build time for a failed build'
221 self.assertEquals(len(links), 0, msg) 284 self.assertEqual(len(links), 0, msg)
285
286 def test_builds_table_search_box(self):
287 """ Test the search box in the builds table on the all builds page """
288 self._get_create_builds()
289
290 url = reverse('all-builds')
291 self.get(url)
292
293 # Check search box is present and works
294 self.wait_until_visible('#allbuildstable tbody tr')
295 search_box = self.find('#search-input-allbuildstable')
296 self.assertTrue(search_box.is_displayed())
297
298 # Check that we can search for a build by recipe name
299 search_box.send_keys('foo')
300 search_btn = self.find('#search-submit-allbuildstable')
301 search_btn.click()
302 self.wait_until_visible('#allbuildstable tbody tr')
303 rows = self.find_all('#allbuildstable tbody tr')
304 self.assertTrue(len(rows) >= 1)
305
306 def test_filtering_on_failure_tasks_column(self):
307 """ Test the filtering on failure tasks column in the builds table on the all builds page """
308 def _check_if_filter_failed_tasks_column_is_visible():
309 # check if failed tasks filter column is visible, if not click on it
310 # Check edit column
311 edit_column = self.find('#edit-columns-button')
312 self.assertTrue(edit_column.is_displayed())
313 edit_column.click()
314 # Check dropdown is visible
315 self.wait_until_visible('ul.dropdown-menu.editcol')
316 filter_fails_task_checkbox = self.find('#checkbox-failed_tasks')
317 if not filter_fails_task_checkbox.is_selected():
318 filter_fails_task_checkbox.click()
319 edit_column.click()
320
321 self._get_create_builds(success=10, failure=10)
322
323 url = reverse('all-builds')
324 self.get(url)
325
326 # Check filtering on failure tasks column
327 self.wait_until_visible('#allbuildstable tbody tr')
328 _check_if_filter_failed_tasks_column_is_visible()
329 failed_tasks_filter = self.find('#failed_tasks_filter')
330 failed_tasks_filter.click()
331 # Check popup is visible
332 self.wait_until_visible('#filter-modal-allbuildstable')
333 self.assertTrue(
334 self.find('#filter-modal-allbuildstable').is_displayed())
335 # Check that we can filter by failure tasks
336 build_without_failure_tasks = self.find(
337 '#failed_tasks_filter\\:without_failed_tasks')
338 build_without_failure_tasks.click()
339 # click on apply button
340 self.find('#filter-modal-allbuildstable .btn-primary').click()
341 self.wait_until_visible('#allbuildstable tbody tr')
342 # Check if filter is applied, by checking if failed_tasks_filter has btn-primary class
343 self.assertTrue(self.find('#failed_tasks_filter').get_attribute(
344 'class').find('btn-primary') != -1)
345
346 def test_filtering_on_completedOn_column(self):
347 """ Test the filtering on completed_on column in the builds table on the all builds page """
348 self._get_create_builds(success=10, failure=10)
349
350 url = reverse('all-builds')
351 self.get(url)
352
353 # Check filtering on failure tasks column
354 self.wait_until_visible('#allbuildstable tbody tr')
355 completed_on_filter = self.find('#completed_on_filter')
356 completed_on_filter.click()
357 # Check popup is visible
358 self.wait_until_visible('#filter-modal-allbuildstable')
359 self.assertTrue(
360 self.find('#filter-modal-allbuildstable').is_displayed())
361 # Check that we can filter by failure tasks
362 build_without_failure_tasks = self.find(
363 '#completed_on_filter\\:date_range')
364 build_without_failure_tasks.click()
365 # click on apply button
366 self.find('#filter-modal-allbuildstable .btn-primary').click()
367 self.wait_until_visible('#allbuildstable tbody tr')
368 # Check if filter is applied, by checking if completed_on_filter has btn-primary class
369 self.assertTrue(self.find('#completed_on_filter').get_attribute(
370 'class').find('btn-primary') != -1)
371
372 # Filter by date range
373 self.find('#completed_on_filter').click()
374 self.wait_until_visible('#filter-modal-allbuildstable')
375 date_ranges = self.driver.find_elements(
376 By.XPATH, '//input[@class="form-control hasDatepicker"]')
377 today = timezone.now()
378 yestersday = today - timezone.timedelta(days=1)
379 date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d'))
380 date_ranges[1].send_keys(today.strftime('%Y-%m-%d'))
381 self.find('#filter-modal-allbuildstable .btn-primary').click()
382 self.wait_until_visible('#allbuildstable tbody tr')
383 self.assertTrue(self.find('#completed_on_filter').get_attribute(
384 'class').find('btn-primary') != -1)
385 # Check if filter is applied, number of builds displayed should be 6
386 self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) >= 4)
387
388 def test_builds_table_editColumn(self):
389 """ Test the edit column feature in the builds table on the all builds page """
390 self._get_create_builds(success=10, failure=10)
391
392 def test_edit_column(check_box_id):
393 # Check that we can hide/show table column
394 check_box = self.find(f'#{check_box_id}')
395 th_class = str(check_box_id).replace('checkbox-', '')
396 if check_box.is_selected():
397 # check if column is visible in table
398 self.assertTrue(
399 self.find(
400 f'#allbuildstable thead th.{th_class}'
401 ).is_displayed(),
402 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
403 )
404 check_box.click()
405 # check if column is hidden in table
406 self.assertFalse(
407 self.find(
408 f'#allbuildstable thead th.{th_class}'
409 ).is_displayed(),
410 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
411 )
412 else:
413 # check if column is hidden in table
414 self.assertFalse(
415 self.find(
416 f'#allbuildstable thead th.{th_class}'
417 ).is_displayed(),
418 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
419 )
420 check_box.click()
421 # check if column is visible in table
422 self.assertTrue(
423 self.find(
424 f'#allbuildstable thead th.{th_class}'
425 ).is_displayed(),
426 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
427 )
428 url = reverse('all-builds')
429 self.get(url)
430 self.wait_until_visible('#allbuildstable tbody tr')
431
432 # Check edit column
433 edit_column = self.find('#edit-columns-button')
434 self.assertTrue(edit_column.is_displayed())
435 edit_column.click()
436 # Check dropdown is visible
437 self.wait_until_visible('ul.dropdown-menu.editcol')
438
439 # Check that we can hide the edit column
440 test_edit_column('checkbox-errors_no')
441 test_edit_column('checkbox-failed_tasks')
442 test_edit_column('checkbox-image_files')
443 test_edit_column('checkbox-project')
444 test_edit_column('checkbox-started_on')
445 test_edit_column('checkbox-time')
446 test_edit_column('checkbox-warnings_no')
447
448 def test_builds_table_show_rows(self):
449 """ Test the show rows feature in the builds table on the all builds page """
450 self._get_create_builds(success=100, failure=100)
451
452 def test_show_rows(row_to_show, show_row_link):
453 # Check that we can show rows == row_to_show
454 show_row_link.select_by_value(str(row_to_show))
455 self.wait_until_visible('#allbuildstable tbody tr', poll=3)
456 # check at least some rows are visible
457 self.assertTrue(
458 len(self.find_all('#allbuildstable tbody tr')) > 0
459 )
460
461 url = reverse('all-builds')
462 self.get(url)
463 self.wait_until_visible('#allbuildstable tbody tr')
464
465 show_rows = self.driver.find_elements(
466 By.XPATH,
467 '//select[@class="form-control pagesize-allbuildstable"]'
468 )
469 # Check show rows
470 for show_row_link in show_rows:
471 show_row_link = Select(show_row_link)
472 test_show_rows(10, show_row_link)
473 test_show_rows(25, show_row_link)
474 test_show_rows(50, show_row_link)
475 test_show_rows(100, show_row_link)
476 test_show_rows(150, show_row_link)
diff --git a/bitbake/lib/toaster/tests/browser/test_all_projects_page.py b/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
index 15b03400f9..9ed1901cc9 100644
--- a/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
@@ -7,15 +7,20 @@
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9 9
10import os
10import re 11import re
11 12
12from django.urls import reverse 13from django.urls import reverse
13from django.utils import timezone 14from django.utils import timezone
15from selenium.webdriver.support.select import Select
14from tests.browser.selenium_helpers import SeleniumTestCase 16from tests.browser.selenium_helpers import SeleniumTestCase
15 17
16from orm.models import BitbakeVersion, Release, Project, Build 18from orm.models import BitbakeVersion, Release, Project, Build
17from orm.models import ProjectVariable 19from orm.models import ProjectVariable
18 20
21from selenium.webdriver.common.by import By
22
23
19class TestAllProjectsPage(SeleniumTestCase): 24class TestAllProjectsPage(SeleniumTestCase):
20 """ Browser tests for projects page /projects/ """ 25 """ Browser tests for projects page /projects/ """
21 26
@@ -25,7 +30,8 @@ class TestAllProjectsPage(SeleniumTestCase):
25 30
26 def setUp(self): 31 def setUp(self):
27 """ Add default project manually """ 32 """ Add default project manually """
28 project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None) 33 project = Project.objects.create_project(
34 self.CLI_BUILDS_PROJECT_NAME, None)
29 self.default_project = project 35 self.default_project = project
30 self.default_project.is_default = True 36 self.default_project.is_default = True
31 self.default_project.save() 37 self.default_project.save()
@@ -35,6 +41,17 @@ class TestAllProjectsPage(SeleniumTestCase):
35 41
36 self.release = None 42 self.release = None
37 43
44 def _create_projects(self, nb_project=10):
45 projects = []
46 for i in range(1, nb_project + 1):
47 projects.append(
48 Project(
49 name='test project {}'.format(i),
50 release=self.release,
51 )
52 )
53 Project.objects.bulk_create(projects)
54
38 def _add_build_to_default_project(self): 55 def _add_build_to_default_project(self):
39 """ Add a build to the default project (not used in all tests) """ 56 """ Add a build to the default project (not used in all tests) """
40 now = timezone.now() 57 now = timezone.now()
@@ -45,12 +62,14 @@ class TestAllProjectsPage(SeleniumTestCase):
45 62
46 def _add_non_default_project(self): 63 def _add_non_default_project(self):
47 """ Add another project """ 64 """ Add another project """
48 bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', 65 builldir = os.environ.get('BUILDDIR', './')
66 bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/',
49 branch='master', dirpath='') 67 branch='master', dirpath='')
50 self.release = Release.objects.create(name='test release', 68 self.release = Release.objects.create(name='test release',
51 branch_name='master', 69 branch_name='master',
52 bitbake_version=bbv) 70 bitbake_version=bbv)
53 self.project = Project.objects.create_project(self.PROJECT_NAME, self.release) 71 self.project = Project.objects.create_project(
72 self.PROJECT_NAME, self.release)
54 self.project.is_default = False 73 self.project.is_default = False
55 self.project.save() 74 self.project.save()
56 75
@@ -62,7 +81,7 @@ class TestAllProjectsPage(SeleniumTestCase):
62 81
63 def _get_row_for_project(self, project_name): 82 def _get_row_for_project(self, project_name):
64 """ Get the HTML row for a project, or None if not found """ 83 """ Get the HTML row for a project, or None if not found """
65 self.wait_until_present('#projectstable tbody tr') 84 self.wait_until_visible('#projectstable tbody tr', poll=3)
66 rows = self.find_all('#projectstable tbody tr') 85 rows = self.find_all('#projectstable tbody tr')
67 86
68 # find the row with a project name matching the one supplied 87 # find the row with a project name matching the one supplied
@@ -93,7 +112,8 @@ class TestAllProjectsPage(SeleniumTestCase):
93 url = reverse('all-projects') 112 url = reverse('all-projects')
94 self.get(url) 113 self.get(url)
95 114
96 default_project_row = self._get_row_for_project(self.default_project.name) 115 default_project_row = self._get_row_for_project(
116 self.default_project.name)
97 117
98 self.assertNotEqual(default_project_row, None, 118 self.assertNotEqual(default_project_row, None,
99 'default project "cli builds" should be in page') 119 'default project "cli builds" should be in page')
@@ -113,11 +133,12 @@ class TestAllProjectsPage(SeleniumTestCase):
113 self.wait_until_visible("#projectstable tr") 133 self.wait_until_visible("#projectstable tr")
114 134
115 # find the row for the default project 135 # find the row for the default project
116 default_project_row = self._get_row_for_project(self.default_project.name) 136 default_project_row = self._get_row_for_project(
137 self.default_project.name)
117 138
118 # check the release text for the default project 139 # check the release text for the default project
119 selector = 'span[data-project-field="release"] span.text-muted' 140 selector = 'span[data-project-field="release"] span.text-muted'
120 element = default_project_row.find_element_by_css_selector(selector) 141 element = default_project_row.find_element(By.CSS_SELECTOR, selector)
121 text = element.text.strip() 142 text = element.text.strip()
122 self.assertEqual(text, 'Not applicable', 143 self.assertEqual(text, 'Not applicable',
123 'release should be "not applicable" for default project') 144 'release should be "not applicable" for default project')
@@ -127,7 +148,7 @@ class TestAllProjectsPage(SeleniumTestCase):
127 148
128 # check the link in the release cell for the other project 149 # check the link in the release cell for the other project
129 selector = 'span[data-project-field="release"]' 150 selector = 'span[data-project-field="release"]'
130 element = other_project_row.find_element_by_css_selector(selector) 151 element = other_project_row.find_element(By.CSS_SELECTOR, selector)
131 text = element.text.strip() 152 text = element.text.strip()
132 self.assertEqual(text, self.release.name, 153 self.assertEqual(text, self.release.name,
133 'release name should be shown for non-default project') 154 'release name should be shown for non-default project')
@@ -148,11 +169,12 @@ class TestAllProjectsPage(SeleniumTestCase):
148 self.wait_until_visible("#projectstable tr") 169 self.wait_until_visible("#projectstable tr")
149 170
150 # find the row for the default project 171 # find the row for the default project
151 default_project_row = self._get_row_for_project(self.default_project.name) 172 default_project_row = self._get_row_for_project(
173 self.default_project.name)
152 174
153 # check the machine cell for the default project 175 # check the machine cell for the default project
154 selector = 'span[data-project-field="machine"] span.text-muted' 176 selector = 'span[data-project-field="machine"] span.text-muted'
155 element = default_project_row.find_element_by_css_selector(selector) 177 element = default_project_row.find_element(By.CSS_SELECTOR, selector)
156 text = element.text.strip() 178 text = element.text.strip()
157 self.assertEqual(text, 'Not applicable', 179 self.assertEqual(text, 'Not applicable',
158 'machine should be not applicable for default project') 180 'machine should be not applicable for default project')
@@ -162,7 +184,7 @@ class TestAllProjectsPage(SeleniumTestCase):
162 184
163 # check the link in the machine cell for the other project 185 # check the link in the machine cell for the other project
164 selector = 'span[data-project-field="machine"]' 186 selector = 'span[data-project-field="machine"]'
165 element = other_project_row.find_element_by_css_selector(selector) 187 element = other_project_row.find_element(By.CSS_SELECTOR, selector)
166 text = element.text.strip() 188 text = element.text.strip()
167 self.assertEqual(text, self.MACHINE_NAME, 189 self.assertEqual(text, self.MACHINE_NAME,
168 'machine name should be shown for non-default project') 190 'machine name should be shown for non-default project')
@@ -183,13 +205,15 @@ class TestAllProjectsPage(SeleniumTestCase):
183 self.get(reverse('all-projects')) 205 self.get(reverse('all-projects'))
184 206
185 # find the row for the default project 207 # find the row for the default project
186 default_project_row = self._get_row_for_project(self.default_project.name) 208 default_project_row = self._get_row_for_project(
209 self.default_project.name)
187 210
188 # check the link on the name field 211 # check the link on the name field
189 selector = 'span[data-project-field="name"] a' 212 selector = 'span[data-project-field="name"] a'
190 element = default_project_row.find_element_by_css_selector(selector) 213 element = default_project_row.find_element(By.CSS_SELECTOR, selector)
191 link_url = element.get_attribute('href').strip() 214 link_url = element.get_attribute('href').strip()
192 expected_url = reverse('projectbuilds', args=(self.default_project.id,)) 215 expected_url = reverse(
216 'projectbuilds', args=(self.default_project.id,))
193 msg = 'link on default project name should point to builds but was %s' % link_url 217 msg = 'link on default project name should point to builds but was %s' % link_url
194 self.assertTrue(link_url.endswith(expected_url), msg) 218 self.assertTrue(link_url.endswith(expected_url), msg)
195 219
@@ -198,8 +222,116 @@ class TestAllProjectsPage(SeleniumTestCase):
198 222
199 # check the link for the other project 223 # check the link for the other project
200 selector = 'span[data-project-field="name"] a' 224 selector = 'span[data-project-field="name"] a'
201 element = other_project_row.find_element_by_css_selector(selector) 225 element = other_project_row.find_element(By.CSS_SELECTOR, selector)
202 link_url = element.get_attribute('href').strip() 226 link_url = element.get_attribute('href').strip()
203 expected_url = reverse('project', args=(self.project.id,)) 227 expected_url = reverse('project', args=(self.project.id,))
204 msg = 'link on project name should point to configuration but was %s' % link_url 228 msg = 'link on project name should point to configuration but was %s' % link_url
205 self.assertTrue(link_url.endswith(expected_url), msg) 229 self.assertTrue(link_url.endswith(expected_url), msg)
230
231 def test_allProject_table_search_box(self):
232 """ Test the search box in the all project table on the all projects page """
233 self._create_projects()
234
235 url = reverse('all-projects')
236 self.get(url)
237
238 # Chseck search box is present and works
239 self.wait_until_visible('#projectstable tbody tr', poll=3)
240 search_box = self.find('#search-input-projectstable')
241 self.assertTrue(search_box.is_displayed())
242
243 # Check that we can search for a project by project name
244 search_box.send_keys('test project 10')
245 search_btn = self.find('#search-submit-projectstable')
246 search_btn.click()
247 self.wait_until_visible('#projectstable tbody tr', poll=3)
248 rows = self.find_all('#projectstable tbody tr')
249 self.assertTrue(len(rows) == 1)
250
251 def test_allProject_table_editColumn(self):
252 """ Test the edit column feature in the projects table on the all projects page """
253 self._create_projects()
254
255 def test_edit_column(check_box_id):
256 # Check that we can hide/show table column
257 check_box = self.find(f'#{check_box_id}')
258 th_class = str(check_box_id).replace('checkbox-', '')
259 if check_box.is_selected():
260 # check if column is visible in table
261 self.assertTrue(
262 self.find(
263 f'#projectstable thead th.{th_class}'
264 ).is_displayed(),
265 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
266 )
267 check_box.click()
268 # check if column is hidden in table
269 self.assertFalse(
270 self.find(
271 f'#projectstable thead th.{th_class}'
272 ).is_displayed(),
273 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
274 )
275 else:
276 # check if column is hidden in table
277 self.assertFalse(
278 self.find(
279 f'#projectstable thead th.{th_class}'
280 ).is_displayed(),
281 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
282 )
283 check_box.click()
284 # check if column is visible in table
285 self.assertTrue(
286 self.find(
287 f'#projectstable thead th.{th_class}'
288 ).is_displayed(),
289 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
290 )
291 url = reverse('all-projects')
292 self.get(url)
293 self.wait_until_visible('#projectstable tbody tr', poll=3)
294
295 # Check edit column
296 edit_column = self.find('#edit-columns-button')
297 self.assertTrue(edit_column.is_displayed())
298 edit_column.click()
299 # Check dropdown is visible
300 self.wait_until_visible('ul.dropdown-menu.editcol')
301
302 # Check that we can hide the edit column
303 test_edit_column('checkbox-errors')
304 test_edit_column('checkbox-image_files')
305 test_edit_column('checkbox-last_build_outcome')
306 test_edit_column('checkbox-recipe_name')
307 test_edit_column('checkbox-warnings')
308
309 def test_allProject_table_show_rows(self):
310 """ Test the show rows feature in the projects table on the all projects page """
311 self._create_projects(nb_project=200)
312
313 def test_show_rows(row_to_show, show_row_link):
314 # Check that we can show rows == row_to_show
315 show_row_link.select_by_value(str(row_to_show))
316 self.wait_until_visible('#projectstable tbody tr', poll=3)
317 # check at least some rows are visible
318 self.assertTrue(
319 len(self.find_all('#projectstable tbody tr')) > 0
320 )
321
322 url = reverse('all-projects')
323 self.get(url)
324 self.wait_until_visible('#projectstable tbody tr', poll=3)
325
326 show_rows = self.driver.find_elements(
327 By.XPATH,
328 '//select[@class="form-control pagesize-projectstable"]'
329 )
330 # Check show rows
331 for show_row_link in show_rows:
332 show_row_link = Select(show_row_link)
333 test_show_rows(10, show_row_link)
334 test_show_rows(25, show_row_link)
335 test_show_rows(50, show_row_link)
336 test_show_rows(100, show_row_link)
337 test_show_rows(150, show_row_link)
diff --git a/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py b/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
index efcd89b346..d838ce363a 100644
--- a/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
@@ -7,6 +7,7 @@
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9 9
10import os
10from django.urls import reverse 11from django.urls import reverse
11from django.utils import timezone 12from django.utils import timezone
12 13
@@ -15,11 +16,14 @@ from tests.browser.selenium_helpers import SeleniumTestCase
15from orm.models import Project, Release, BitbakeVersion, Build, LogMessage 16from orm.models import Project, Release, BitbakeVersion, Build, LogMessage
16from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable 17from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable
17 18
19from selenium.webdriver.common.by import By
20
18class TestBuildDashboardPage(SeleniumTestCase): 21class TestBuildDashboardPage(SeleniumTestCase):
19 """ Tests for the build dashboard /build/X """ 22 """ Tests for the build dashboard /build/X """
20 23
21 def setUp(self): 24 def setUp(self):
22 bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', 25 builldir = os.environ.get('BUILDDIR', './')
26 bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
23 branch='master', dirpath="") 27 branch='master', dirpath="")
24 release = Release.objects.create(name='release1', 28 release = Release.objects.create(name='release1',
25 bitbake_version=bbv) 29 bitbake_version=bbv)
@@ -158,6 +162,7 @@ class TestBuildDashboardPage(SeleniumTestCase):
158 """ 162 """
159 url = reverse('builddashboard', args=(build.id,)) 163 url = reverse('builddashboard', args=(build.id,))
160 self.get(url) 164 self.get(url)
165 self.wait_until_visible('#global-nav', poll=3)
161 166
162 def _get_build_dashboard_errors(self, build): 167 def _get_build_dashboard_errors(self, build):
163 """ 168 """
@@ -183,7 +188,7 @@ class TestBuildDashboardPage(SeleniumTestCase):
183 188
184 found = False 189 found = False
185 for element in message_elements: 190 for element in message_elements:
186 log_message_text = element.find_element_by_tag_name('pre').text.strip() 191 log_message_text = element.find_element(By.TAG_NAME, 'pre').text.strip()
187 text_matches = (log_message_text == expected_text) 192 text_matches = (log_message_text == expected_text)
188 193
189 log_message_pk = element.get_attribute('data-log-message-id') 194 log_message_pk = element.get_attribute('data-log-message-id')
@@ -213,7 +218,7 @@ class TestBuildDashboardPage(SeleniumTestCase):
213 the WebElement modal match the list of text values in expected 218 the WebElement modal match the list of text values in expected
214 """ 219 """
215 # labels containing the radio buttons we're testing for 220 # labels containing the radio buttons we're testing for
216 labels = modal.find_elements_by_css_selector(".radio") 221 labels = modal.find_elements(By.CSS_SELECTOR,".radio")
217 222
218 labels_text = [lab.text for lab in labels] 223 labels_text = [lab.text for lab in labels]
219 self.assertEqual(len(labels_text), len(expected)) 224 self.assertEqual(len(labels_text), len(expected))
@@ -248,7 +253,7 @@ class TestBuildDashboardPage(SeleniumTestCase):
248 selector = '[data-role="edit-custom-image-trigger"]' 253 selector = '[data-role="edit-custom-image-trigger"]'
249 self.click(selector) 254 self.click(selector)
250 255
251 modal = self.driver.find_element_by_id('edit-custom-image-modal') 256 modal = self.driver.find_element(By.ID, 'edit-custom-image-modal')
252 self.wait_until_visible("#edit-custom-image-modal") 257 self.wait_until_visible("#edit-custom-image-modal")
253 258
254 # recipes we expect to see in the edit custom image modal 259 # recipes we expect to see in the edit custom image modal
@@ -270,7 +275,7 @@ class TestBuildDashboardPage(SeleniumTestCase):
270 selector = '[data-role="new-custom-image-trigger"]' 275 selector = '[data-role="new-custom-image-trigger"]'
271 self.click(selector) 276 self.click(selector)
272 277
273 modal = self.driver.find_element_by_id('new-custom-image-modal') 278 modal = self.driver.find_element(By.ID,'new-custom-image-modal')
274 self.wait_until_visible("#new-custom-image-modal") 279 self.wait_until_visible("#new-custom-image-modal")
275 280
276 # recipes we expect to see in the new custom image modal 281 # recipes we expect to see in the new custom image modal
diff --git a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py b/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py
index c6226d60eb..675825bd40 100644
--- a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py
+++ b/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py
@@ -7,6 +7,7 @@
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9 9
10import os
10from django.urls import reverse 11from django.urls import reverse
11from django.utils import timezone 12from django.utils import timezone
12 13
@@ -20,7 +21,8 @@ class TestBuildDashboardPageArtifacts(SeleniumTestCase):
20 """ Tests for artifacts on the build dashboard /build/X """ 21 """ Tests for artifacts on the build dashboard /build/X """
21 22
22 def setUp(self): 23 def setUp(self):
23 bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', 24 builldir = os.environ.get('BUILDDIR', './')
25 bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
24 branch='master', dirpath="") 26 branch='master', dirpath="")
25 release = Release.objects.create(name='release1', 27 release = Release.objects.create(name='release1',
26 bitbake_version=bbv) 28 bitbake_version=bbv)
@@ -197,12 +199,12 @@ class TestBuildDashboardPageArtifacts(SeleniumTestCase):
197 # check package count and size, link on target name 199 # check package count and size, link on target name
198 selector = '[data-value="target-package-count"]' 200 selector = '[data-value="target-package-count"]'
199 element = self.find(selector) 201 element = self.find(selector)
200 self.assertEquals(element.text, '1', 202 self.assertEqual(element.text, '1',
201 'package count should be shown for image builds') 203 'package count should be shown for image builds')
202 204
203 selector = '[data-value="target-package-size"]' 205 selector = '[data-value="target-package-size"]'
204 element = self.find(selector) 206 element = self.find(selector)
205 self.assertEquals(element.text, '1.0 KB', 207 self.assertEqual(element.text, '1.0 KB',
206 'package size should be shown for image builds') 208 'package size should be shown for image builds')
207 209
208 selector = '[data-link="target-packages"]' 210 selector = '[data-link="target-packages"]'
diff --git a/bitbake/lib/toaster/tests/browser/test_delete_project.py b/bitbake/lib/toaster/tests/browser/test_delete_project.py
new file mode 100644
index 0000000000..1941777ccc
--- /dev/null
+++ b/bitbake/lib/toaster/tests/browser/test_delete_project.py
@@ -0,0 +1,103 @@
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# BitBake Toaster UI tests implementation
4#
5# Copyright (C) 2023 Savoir-faire Linux Inc
6#
7# SPDX-License-Identifier: GPL-2.0-only
8
9import pytest
10from django.urls import reverse
11from selenium.webdriver.support.ui import Select
12from tests.browser.selenium_helpers import SeleniumTestCase
13from orm.models import BitbakeVersion, Project, Release
14from selenium.webdriver.common.by import By
15
16class TestDeleteProject(SeleniumTestCase):
17
18 def setUp(self):
19 bitbake, _ = BitbakeVersion.objects.get_or_create(
20 name="master",
21 giturl="git://master",
22 branch="master",
23 dirpath="master")
24
25 self.release, _ = Release.objects.get_or_create(
26 name="master",
27 description="Yocto Project master",
28 branch_name="master",
29 helptext="latest",
30 bitbake_version=bitbake)
31
32 Release.objects.get_or_create(
33 name="foo",
34 description="Yocto Project foo",
35 branch_name="foo",
36 helptext="latest",
37 bitbake_version=bitbake)
38
39 @pytest.mark.django_db
40 def test_delete_project(self):
41 """ Test delete a project
42 - Check delete modal is visible
43 - Check delete modal has right text
44 - Confirm delete
45 - Check project is deleted
46 """
47 project_name = "project_to_delete"
48 url = reverse('newproject')
49 self.get(url)
50 self.enter_text('#new-project-name', project_name)
51 select = Select(self.find('#projectversion'))
52 select.select_by_value(str(self.release.pk))
53 self.click("#create-project-button")
54 # We should get redirected to the new project's page with the
55 # notification at the top
56 element = self.wait_until_visible('#project-created-notification')
57 self.assertTrue(project_name in element.text,
58 "New project name not in new project notification")
59 self.assertTrue(Project.objects.filter(name=project_name).count(),
60 "New project not found in database")
61
62 # Delete project
63 delete_project_link = self.driver.find_element(
64 By.XPATH, '//a[@href="#delete-project-modal"]')
65 delete_project_link.click()
66
67 # Check delete modal is visible
68 self.wait_until_visible('#delete-project-modal')
69
70 # Check delete modal has right text
71 modal_header_text = self.find('#delete-project-modal .modal-header').text
72 self.assertTrue(
73 "Are you sure you want to delete this project?" in modal_header_text,
74 "Delete project modal header text is wrong")
75
76 modal_body_text = self.find('#delete-project-modal .modal-body').text
77 self.assertTrue(
78 "Cancel its builds currently in progress" in modal_body_text,
79 "Modal body doesn't contain: Cancel its builds currently in progress")
80 self.assertTrue(
81 "Remove its configuration information" in modal_body_text,
82 "Modal body doesn't contain: Remove its configuration information")
83 self.assertTrue(
84 "Remove its imported layers" in modal_body_text,
85 "Modal body doesn't contain: Remove its imported layers")
86 self.assertTrue(
87 "Remove its custom images" in modal_body_text,
88 "Modal body doesn't contain: Remove its custom images")
89 self.assertTrue(
90 "Remove all its build information" in modal_body_text,
91 "Modal body doesn't contain: Remove all its build information")
92
93 # Confirm delete
94 delete_btn = self.find('#delete-project-confirmed')
95 delete_btn.click()
96
97 # Check project is deleted
98 self.wait_until_visible('#change-notification')
99 delete_notification = self.find('#change-notification-msg')
100 self.assertTrue("You have deleted 1 project:" in delete_notification.text)
101 self.assertTrue(project_name in delete_notification.text)
102 self.assertFalse(Project.objects.filter(name=project_name).exists(),
103 "Project not deleted from database")
diff --git a/bitbake/lib/toaster/tests/browser/test_landing_page.py b/bitbake/lib/toaster/tests/browser/test_landing_page.py
index 8bb64b9f3e..8fe5fea467 100644
--- a/bitbake/lib/toaster/tests/browser/test_landing_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_landing_page.py
@@ -10,8 +10,10 @@
10from django.urls import reverse 10from django.urls import reverse
11from django.utils import timezone 11from django.utils import timezone
12from tests.browser.selenium_helpers import SeleniumTestCase 12from tests.browser.selenium_helpers import SeleniumTestCase
13from selenium.webdriver.common.by import By
14
15from orm.models import Layer, Layer_Version, Project, Build
13 16
14from orm.models import Project, Build
15 17
16class TestLandingPage(SeleniumTestCase): 18class TestLandingPage(SeleniumTestCase):
17 """ Tests for redirects on the landing page """ 19 """ Tests for redirects on the landing page """
@@ -29,6 +31,130 @@ class TestLandingPage(SeleniumTestCase):
29 self.project.is_default = True 31 self.project.is_default = True
30 self.project.save() 32 self.project.save()
31 33
34 def test_icon_info_visible_and_clickable(self):
35 """ Test that the information icon is visible and clickable """
36 self.get(reverse('landing'))
37 info_sign = self.find('#toaster-version-info-sign')
38
39 # check that the info sign is visible
40 self.assertTrue(info_sign.is_displayed())
41
42 # check that the info sign is clickable
43 # and info modal is appearing when clicking on the info sign
44 info_sign.click() # click on the info sign make attribute 'aria-describedby' visible
45 info_model_id = info_sign.get_attribute('aria-describedby')
46 info_modal = self.find(f'#{info_model_id}')
47 self.assertTrue(info_modal.is_displayed())
48 self.assertTrue("Toaster version information" in info_modal.text)
49
50 def test_documentation_link_displayed(self):
51 """ Test that the documentation link is displayed """
52 self.get(reverse('landing'))
53 documentation_link = self.find('#navbar-docs > a')
54
55 # check that the documentation link is visible
56 self.assertTrue(documentation_link.is_displayed())
57
58 # check browser open new tab toaster manual when clicking on the documentation link
59 self.assertEqual(documentation_link.get_attribute('target'), '_blank')
60 self.assertEqual(
61 documentation_link.get_attribute('href'),
62 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual')
63 self.assertTrue("Documentation" in documentation_link.text)
64
65 def test_openembedded_jumbotron_link_visible_and_clickable(self):
66 """ Test OpenEmbedded link jumbotron is visible and clickable: """
67 self.get(reverse('landing'))
68 jumbotron = self.find('.jumbotron')
69
70 # check OpenEmbedded
71 openembedded = jumbotron.find_element(By.LINK_TEXT, 'OpenEmbedded')
72 self.assertTrue(openembedded.is_displayed())
73 openembedded.click()
74 self.assertTrue("openembedded.org" in self.driver.current_url)
75
76 def test_bitbake_jumbotron_link_visible_and_clickable(self):
77 """ Test BitBake link jumbotron is visible and clickable: """
78 self.get(reverse('landing'))
79 jumbotron = self.find('.jumbotron')
80
81 # check BitBake
82 bitbake = jumbotron.find_element(By.LINK_TEXT, 'BitBake')
83 self.assertTrue(bitbake.is_displayed())
84 bitbake.click()
85 self.assertTrue(
86 "docs.yoctoproject.org/bitbake.html" in self.driver.current_url)
87
88 def test_yoctoproject_jumbotron_link_visible_and_clickable(self):
89 """ Test Yocto Project link jumbotron is visible and clickable: """
90 self.get(reverse('landing'))
91 jumbotron = self.find('.jumbotron')
92
93 # check Yocto Project
94 yoctoproject = jumbotron.find_element(By.LINK_TEXT, 'Yocto Project')
95 self.assertTrue(yoctoproject.is_displayed())
96 yoctoproject.click()
97 self.assertTrue("yoctoproject.org" in self.driver.current_url)
98
99 def test_link_setup_using_toaster_visible_and_clickable(self):
100 """ Test big magenta button setting up and using toaster link in jumbotron
101 if visible and clickable
102 """
103 self.get(reverse('landing'))
104 jumbotron = self.find('.jumbotron')
105
106 # check Big magenta button
107 big_magenta_button = jumbotron.find_element(By.LINK_TEXT,
108 'Toaster is ready to capture your command line builds'
109 )
110 self.assertTrue(big_magenta_button.is_displayed())
111 big_magenta_button.click()
112 self.assertTrue(
113 "docs.yoctoproject.org/toaster-manual/setup-and-use.html#setting-up-and-using-toaster" in self.driver.current_url)
114
115 def test_link_create_new_project_in_jumbotron_visible_and_clickable(self):
116 """ Test big blue button create new project jumbotron if visible and clickable """
117 # Create a layer and a layer version to make visible the big blue button
118 layer = Layer.objects.create(name='bar')
119 Layer_Version.objects.create(layer=layer)
120
121 self.get(reverse('landing'))
122 jumbotron = self.find('.jumbotron')
123
124 # check Big Blue button
125 big_blue_button = jumbotron.find_element(By.LINK_TEXT,
126 'Create your first Toaster project to run manage builds'
127 )
128 self.assertTrue(big_blue_button.is_displayed())
129 big_blue_button.click()
130 self.assertTrue("toastergui/newproject/" in self.driver.current_url)
131
132 def test_toaster_manual_link_visible_and_clickable(self):
133 """ Test Read the Toaster manual link jumbotron is visible and clickable: """
134 self.get(reverse('landing'))
135 jumbotron = self.find('.jumbotron')
136
137 # check Read the Toaster manual
138 toaster_manual = jumbotron.find_element(
139 By.LINK_TEXT, 'Read the Toaster manual')
140 self.assertTrue(toaster_manual.is_displayed())
141 toaster_manual.click()
142 self.assertTrue(
143 "https://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual" in self.driver.current_url)
144
145 def test_contrib_to_toaster_link_visible_and_clickable(self):
146 """ Test Contribute to Toaster link jumbotron is visible and clickable: """
147 self.get(reverse('landing'))
148 jumbotron = self.find('.jumbotron')
149
150 # check Contribute to Toaster
151 contribute_to_toaster = jumbotron.find_element(
152 By.LINK_TEXT, 'Contribute to Toaster')
153 self.assertTrue(contribute_to_toaster.is_displayed())
154 contribute_to_toaster.click()
155 self.assertTrue(
156 "wiki.yoctoproject.org/wiki/contribute_to_toaster" in str(self.driver.current_url).lower())
157
32 def test_only_default_project(self): 158 def test_only_default_project(self):
33 """ 159 """
34 No projects except default 160 No projects except default
@@ -87,10 +213,9 @@ class TestLandingPage(SeleniumTestCase):
87 213
88 self.get(reverse('landing')) 214 self.get(reverse('landing'))
89 215
216 self.wait_until_visible("#latest-builds", poll=3)
90 elements = self.find_all('#allbuildstable') 217 elements = self.find_all('#allbuildstable')
91 self.assertEqual(len(elements), 1, 'should redirect to builds') 218 self.assertEqual(len(elements), 1, 'should redirect to builds')
92 content = self.get_page_source() 219 content = self.get_page_source()
93 self.assertTrue(self.PROJECT_NAME in content, 220 self.assertTrue(self.PROJECT_NAME in content,
94 'should show builds for project %s' % self.PROJECT_NAME) 221 'should show builds for project %s' % self.PROJECT_NAME)
95 self.assertFalse(self.CLI_BUILDS_PROJECT_NAME in content,
96 'should not show builds for cli project')
diff --git a/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py b/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py
index 71bdd2aafd..5c29548b78 100644
--- a/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py
@@ -8,6 +8,7 @@
8# 8#
9 9
10from django.urls import reverse 10from django.urls import reverse
11from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException
11from tests.browser.selenium_helpers import SeleniumTestCase 12from tests.browser.selenium_helpers import SeleniumTestCase
12 13
13from orm.models import Layer, Layer_Version, Project, LayerSource, Release 14from orm.models import Layer, Layer_Version, Project, LayerSource, Release
@@ -63,11 +64,12 @@ class TestLayerDetailsPage(SeleniumTestCase):
63 args=(self.project.pk, 64 args=(self.project.pk,
64 self.imported_layer_version.pk)) 65 self.imported_layer_version.pk))
65 66
66 def test_edit_layerdetails(self): 67 def _edit_layerdetails(self):
67 """ Edit all the editable fields for the layer refresh the page and 68 """ Edit all the editable fields for the layer refresh the page and
68 check that the new values exist""" 69 check that the new values exist"""
69 70
70 self.get(self.url) 71 self.get(self.url)
72 self.wait_until_visible("#add-remove-layer-btn")
71 73
72 self.click("#add-remove-layer-btn") 74 self.click("#add-remove-layer-btn")
73 self.click("#edit-layer-source") 75 self.click("#edit-layer-source")
@@ -97,13 +99,26 @@ class TestLayerDetailsPage(SeleniumTestCase):
97 "Expecting any of \"%s\"but got \"%s\"" % 99 "Expecting any of \"%s\"but got \"%s\"" %
98 (self.initial_values, value)) 100 (self.initial_values, value))
99 101
102 # Make sure the input visible beofre sending keys
103 self.wait_until_visible("#layer-git input[type=text]")
100 inputs.send_keys("-edited") 104 inputs.send_keys("-edited")
101 105
102 # Save the new values 106 # Save the new values
103 for save_btn in self.find_all(".change-btn"): 107 for save_btn in self.find_all(".change-btn"):
104 save_btn.click() 108 save_btn.click()
105 109
106 self.click("#save-changes-for-switch") 110 try:
111 self.wait_until_visible("#save-changes-for-switch", poll=3)
112 btn_save_chg_for_switch = self.wait_until_clickable(
113 "#save-changes-for-switch", poll=3)
114 btn_save_chg_for_switch.click()
115 except ElementClickInterceptedException:
116 self.skipTest(
117 "save-changes-for-switch click intercepted. Element not visible or maybe covered by another element.")
118 except TimeoutException:
119 self.skipTest(
120 "save-changes-for-switch is not clickable within the specified timeout.")
121
107 self.wait_until_visible("#edit-layer-source") 122 self.wait_until_visible("#edit-layer-source")
108 123
109 # Refresh the page to see if the new values are returned 124 # Refresh the page to see if the new values are returned
@@ -132,7 +147,18 @@ class TestLayerDetailsPage(SeleniumTestCase):
132 new_dir = "/home/test/my-meta-dir" 147 new_dir = "/home/test/my-meta-dir"
133 dir_input.send_keys(new_dir) 148 dir_input.send_keys(new_dir)
134 149
135 self.click("#save-changes-for-switch") 150 try:
151 self.wait_until_visible("#save-changes-for-switch", poll=3)
152 btn_save_chg_for_switch = self.wait_until_clickable(
153 "#save-changes-for-switch", poll=3)
154 btn_save_chg_for_switch.click()
155 except ElementClickInterceptedException:
156 self.skipTest(
157 "save-changes-for-switch click intercepted. Element not properly visible or maybe behind another element.")
158 except TimeoutException:
159 self.skipTest(
160 "save-changes-for-switch is not clickable within the specified timeout.")
161
136 self.wait_until_visible("#edit-layer-source") 162 self.wait_until_visible("#edit-layer-source")
137 163
138 # Refresh the page to see if the new values are returned 164 # Refresh the page to see if the new values are returned
@@ -142,6 +168,13 @@ class TestLayerDetailsPage(SeleniumTestCase):
142 "Expected %s in the dir value for layer directory" % 168 "Expected %s in the dir value for layer directory" %
143 new_dir) 169 new_dir)
144 170
171 def test_edit_layerdetails_page(self):
172 try:
173 self._edit_layerdetails()
174 except ElementClickInterceptedException:
175 self.skipTest(
176 "ElementClickInterceptedException occured. Element not visible or maybe covered by another element.")
177
145 def test_delete_layer(self): 178 def test_delete_layer(self):
146 """ Delete the layer """ 179 """ Delete the layer """
147 180
diff --git a/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py b/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py
index 7844aaa395..d7a4c34532 100644
--- a/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py
+++ b/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py
@@ -6,7 +6,6 @@
6# 6#
7# Copyright (C) 2013-2016 Intel Corporation 7# Copyright (C) 2013-2016 Intel Corporation
8# 8#
9
10from django.urls import reverse 9from django.urls import reverse
11from django.utils import timezone 10from django.utils import timezone
12from tests.browser.selenium_helpers import SeleniumTestCase 11from tests.browser.selenium_helpers import SeleniumTestCase
@@ -14,6 +13,8 @@ from tests.browser.selenium_helpers_base import Wait
14from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version 13from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version
15from bldcontrol.models import BuildRequest 14from bldcontrol.models import BuildRequest
16 15
16from selenium.webdriver.common.by import By
17
17class TestMostRecentBuildsStates(SeleniumTestCase): 18class TestMostRecentBuildsStates(SeleniumTestCase):
18 """ Test states update correctly in most recent builds area """ 19 """ Test states update correctly in most recent builds area """
19 20
@@ -45,13 +46,14 @@ class TestMostRecentBuildsStates(SeleniumTestCase):
45 # build queued; check shown as queued 46 # build queued; check shown as queued
46 selector = base_selector + '[data-build-state="Queued"]' 47 selector = base_selector + '[data-build-state="Queued"]'
47 element = self.wait_until_visible(selector) 48 element = self.wait_until_visible(selector)
48 self.assertRegexpMatches(element.get_attribute('innerHTML'), 49 self.assertRegex(element.get_attribute('innerHTML'),
49 'Build queued', 'build should show queued status') 50 'Build queued', 'build should show queued status')
50 51
51 # waiting for recipes to be parsed 52 # waiting for recipes to be parsed
52 build.outcome = Build.IN_PROGRESS 53 build.outcome = Build.IN_PROGRESS
53 build.recipes_to_parse = recipes_to_parse 54 build.recipes_to_parse = recipes_to_parse
54 build.recipes_parsed = 0 55 build.recipes_parsed = 0
56 build.save()
55 57
56 build_request.state = BuildRequest.REQ_INPROGRESS 58 build_request.state = BuildRequest.REQ_INPROGRESS
57 build_request.save() 59 build_request.save()
@@ -62,7 +64,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase):
62 element = self.wait_until_visible(selector) 64 element = self.wait_until_visible(selector)
63 65
64 bar_selector = '#recipes-parsed-percentage-bar-%s' % build.id 66 bar_selector = '#recipes-parsed-percentage-bar-%s' % build.id
65 bar_element = element.find_element_by_css_selector(bar_selector) 67 bar_element = element.find_element(By.CSS_SELECTOR, bar_selector)
66 self.assertEqual(bar_element.value_of_css_property('width'), '0px', 68 self.assertEqual(bar_element.value_of_css_property('width'), '0px',
67 'recipe parse progress should be at 0') 69 'recipe parse progress should be at 0')
68 70
@@ -73,7 +75,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase):
73 self.get(url) 75 self.get(url)
74 76
75 element = self.wait_until_visible(selector) 77 element = self.wait_until_visible(selector)
76 bar_element = element.find_element_by_css_selector(bar_selector) 78 bar_element = element.find_element(By.CSS_SELECTOR, bar_selector)
77 recipe_bar_updated = lambda driver: \ 79 recipe_bar_updated = lambda driver: \
78 bar_element.get_attribute('style') == 'width: 50%;' 80 bar_element.get_attribute('style') == 'width: 50%;'
79 msg = 'recipe parse progress bar should update to 50%' 81 msg = 'recipe parse progress bar should update to 50%'
@@ -94,11 +96,11 @@ class TestMostRecentBuildsStates(SeleniumTestCase):
94 96
95 selector = base_selector + '[data-build-state="Starting"]' 97 selector = base_selector + '[data-build-state="Starting"]'
96 element = self.wait_until_visible(selector) 98 element = self.wait_until_visible(selector)
97 self.assertRegexpMatches(element.get_attribute('innerHTML'), 99 self.assertRegex(element.get_attribute('innerHTML'),
98 'Tasks starting', 'build should show "tasks starting" status') 100 'Tasks starting', 'build should show "tasks starting" status')
99 101
100 # first task finished; check tasks progress bar 102 # first task finished; check tasks progress bar
101 task1.order = 1 103 task1.outcome = Task.OUTCOME_SUCCESS
102 task1.save() 104 task1.save()
103 105
104 self.get(url) 106 self.get(url)
@@ -107,7 +109,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase):
107 element = self.wait_until_visible(selector) 109 element = self.wait_until_visible(selector)
108 110
109 bar_selector = '#build-pc-done-bar-%s' % build.id 111 bar_selector = '#build-pc-done-bar-%s' % build.id
110 bar_element = element.find_element_by_css_selector(bar_selector) 112 bar_element = element.find_element(By.CSS_SELECTOR, bar_selector)
111 113
112 task_bar_updated = lambda driver: \ 114 task_bar_updated = lambda driver: \
113 bar_element.get_attribute('style') == 'width: 50%;' 115 bar_element.get_attribute('style') == 'width: 50%;'
@@ -115,13 +117,13 @@ class TestMostRecentBuildsStates(SeleniumTestCase):
115 element = Wait(self.driver).until(task_bar_updated, msg) 117 element = Wait(self.driver).until(task_bar_updated, msg)
116 118
117 # last task finished; check tasks progress bar updates 119 # last task finished; check tasks progress bar updates
118 task2.order = 2 120 task2.outcome = Task.OUTCOME_SUCCESS
119 task2.save() 121 task2.save()
120 122
121 self.get(url) 123 self.get(url)
122 124
123 element = self.wait_until_visible(selector) 125 element = self.wait_until_visible(selector)
124 bar_element = element.find_element_by_css_selector(bar_selector) 126 bar_element = element.find_element(By.CSS_SELECTOR, bar_selector)
125 task_bar_updated = lambda driver: \ 127 task_bar_updated = lambda driver: \
126 bar_element.get_attribute('style') == 'width: 100%;' 128 bar_element.get_attribute('style') == 'width: 100%;'
127 msg = 'tasks progress bar should update to 100%' 129 msg = 'tasks progress bar should update to 100%'
@@ -183,7 +185,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase):
183 selector = '[data-latest-build-result="%s"] ' \ 185 selector = '[data-latest-build-result="%s"] ' \
184 '[data-build-state="Cancelling"]' % build.id 186 '[data-build-state="Cancelling"]' % build.id
185 element = self.wait_until_visible(selector) 187 element = self.wait_until_visible(selector)
186 self.assertRegexpMatches(element.get_attribute('innerHTML'), 188 self.assertRegex(element.get_attribute('innerHTML'),
187 'Cancelling the build', 'build should show "cancelling" status') 189 'Cancelling the build', 'build should show "cancelling" status')
188 190
189 # check cancelled state 191 # check cancelled state
@@ -195,5 +197,5 @@ class TestMostRecentBuildsStates(SeleniumTestCase):
195 selector = '[data-latest-build-result="%s"] ' \ 197 selector = '[data-latest-build-result="%s"] ' \
196 '[data-build-state="Cancelled"]' % build.id 198 '[data-build-state="Cancelled"]' % build.id
197 element = self.wait_until_visible(selector) 199 element = self.wait_until_visible(selector)
198 self.assertRegexpMatches(element.get_attribute('innerHTML'), 200 self.assertRegex(element.get_attribute('innerHTML'),
199 'Build cancelled', 'build should show "cancelled" status') 201 'Build cancelled', 'build should show "cancelled" status')
diff --git a/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py b/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
index 9906ae42a9..9f0b6397fe 100644
--- a/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
@@ -6,6 +6,7 @@
6# 6#
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9from bldcontrol.models import BuildEnvironment
9 10
10from django.urls import reverse 11from django.urls import reverse
11from tests.browser.selenium_helpers import SeleniumTestCase 12from tests.browser.selenium_helpers import SeleniumTestCase
@@ -18,6 +19,9 @@ class TestNewCustomImagePage(SeleniumTestCase):
18 CUSTOM_IMAGE_NAME = 'roopa-doopa' 19 CUSTOM_IMAGE_NAME = 'roopa-doopa'
19 20
20 def setUp(self): 21 def setUp(self):
22 BuildEnvironment.objects.get_or_create(
23 betype=BuildEnvironment.TYPE_LOCAL,
24 )
21 release = Release.objects.create( 25 release = Release.objects.create(
22 name='baz', 26 name='baz',
23 bitbake_version=BitbakeVersion.objects.create(name='v1') 27 bitbake_version=BitbakeVersion.objects.create(name='v1')
@@ -41,11 +45,16 @@ class TestNewCustomImagePage(SeleniumTestCase):
41 ) 45 )
42 46
43 # add a fake image recipe to the layer that can be customised 47 # add a fake image recipe to the layer that can be customised
48 builldir = os.environ.get('BUILDDIR', './')
44 self.recipe = Recipe.objects.create( 49 self.recipe = Recipe.objects.create(
45 name='core-image-minimal', 50 name='core-image-minimal',
46 layer_version=layer_version, 51 layer_version=layer_version,
52 file_path=f'{builldir}/core-image-minimal.bb',
47 is_image=True 53 is_image=True
48 ) 54 )
55 # create a tmp file for the recipe
56 with open(self.recipe.file_path, 'w') as f:
57 f.write('foo')
49 58
50 # another project with a custom image already in it 59 # another project with a custom image already in it
51 project2 = Project.objects.create(name='whoop', release=release) 60 project2 = Project.objects.create(name='whoop', release=release)
@@ -81,6 +90,7 @@ class TestNewCustomImagePage(SeleniumTestCase):
81 """ 90 """
82 url = reverse('newcustomimage', args=(self.project.id,)) 91 url = reverse('newcustomimage', args=(self.project.id,))
83 self.get(url) 92 self.get(url)
93 self.wait_until_visible('#global-nav', poll=3)
84 94
85 self.click('button[data-recipe="%s"]' % self.recipe.id) 95 self.click('button[data-recipe="%s"]' % self.recipe.id)
86 96
@@ -128,7 +138,7 @@ class TestNewCustomImagePage(SeleniumTestCase):
128 """ 138 """
129 self._create_custom_image(self.recipe.name) 139 self._create_custom_image(self.recipe.name)
130 element = self.wait_until_visible('#invalid-name-help') 140 element = self.wait_until_visible('#invalid-name-help')
131 self.assertRegexpMatches(element.text.strip(), 141 self.assertRegex(element.text.strip(),
132 'image with this name already exists') 142 'image with this name already exists')
133 143
134 def test_new_duplicates_project_image(self): 144 def test_new_duplicates_project_image(self):
@@ -146,4 +156,4 @@ class TestNewCustomImagePage(SeleniumTestCase):
146 self._create_custom_image(custom_image_name) 156 self._create_custom_image(custom_image_name)
147 element = self.wait_until_visible('#invalid-name-help') 157 element = self.wait_until_visible('#invalid-name-help')
148 expected = 'An image with this name already exists in this project' 158 expected = 'An image with this name already exists in this project'
149 self.assertRegexpMatches(element.text.strip(), expected) 159 self.assertRegex(element.text.strip(), expected)
diff --git a/bitbake/lib/toaster/tests/browser/test_new_project_page.py b/bitbake/lib/toaster/tests/browser/test_new_project_page.py
index e20a1f686e..458bb6538d 100644
--- a/bitbake/lib/toaster/tests/browser/test_new_project_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_new_project_page.py
@@ -6,11 +6,11 @@
6# 6#
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9
10from django.urls import reverse 9from django.urls import reverse
11from tests.browser.selenium_helpers import SeleniumTestCase 10from tests.browser.selenium_helpers import SeleniumTestCase
12from selenium.webdriver.support.ui import Select 11from selenium.webdriver.support.ui import Select
13from selenium.common.exceptions import InvalidElementStateException 12from selenium.common.exceptions import InvalidElementStateException
13from selenium.webdriver.common.by import By
14 14
15from orm.models import Project, Release, BitbakeVersion 15from orm.models import Project, Release, BitbakeVersion
16 16
@@ -47,7 +47,7 @@ class TestNewProjectPage(SeleniumTestCase):
47 47
48 url = reverse('newproject') 48 url = reverse('newproject')
49 self.get(url) 49 self.get(url)
50 50 self.wait_until_visible('#new-project-name', poll=3)
51 self.enter_text('#new-project-name', project_name) 51 self.enter_text('#new-project-name', project_name)
52 52
53 select = Select(self.find('#projectversion')) 53 select = Select(self.find('#projectversion'))
@@ -57,7 +57,8 @@ class TestNewProjectPage(SeleniumTestCase):
57 57
58 # We should get redirected to the new project's page with the 58 # We should get redirected to the new project's page with the
59 # notification at the top 59 # notification at the top
60 element = self.wait_until_visible('#project-created-notification') 60 element = self.wait_until_visible(
61 '#project-created-notification', poll=3)
61 62
62 self.assertTrue(project_name in element.text, 63 self.assertTrue(project_name in element.text,
63 "New project name not in new project notification") 64 "New project name not in new project notification")
@@ -78,13 +79,20 @@ class TestNewProjectPage(SeleniumTestCase):
78 79
79 url = reverse('newproject') 80 url = reverse('newproject')
80 self.get(url) 81 self.get(url)
82 self.wait_until_visible('#new-project-name', poll=3)
81 83
82 self.enter_text('#new-project-name', project_name) 84 self.enter_text('#new-project-name', project_name)
83 85
84 select = Select(self.find('#projectversion')) 86 select = Select(self.find('#projectversion'))
85 select.select_by_value(str(self.release.pk)) 87 select.select_by_value(str(self.release.pk))
86 88
87 element = self.wait_until_visible('#hint-error-project-name') 89 radio = self.driver.find_element(By.ID, 'type-new')
90 radio.click()
91
92 self.click("#create-project-button")
93
94 self.wait_until_present('#hint-error-project-name', poll=3)
95 element = self.find('#hint-error-project-name')
88 96
89 self.assertTrue(("Project names must be unique" in element.text), 97 self.assertTrue(("Project names must be unique" in element.text),
90 "Did not find unique project name error message") 98 "Did not find unique project name error message")
diff --git a/bitbake/lib/toaster/tests/browser/test_project_builds_page.py b/bitbake/lib/toaster/tests/browser/test_project_builds_page.py
index 51717e72d4..0dba33b9c8 100644
--- a/bitbake/lib/toaster/tests/browser/test_project_builds_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_project_builds_page.py
@@ -7,6 +7,7 @@
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9 9
10import os
10import re 11import re
11 12
12from django.urls import reverse 13from django.urls import reverse
@@ -22,7 +23,8 @@ class TestProjectBuildsPage(SeleniumTestCase):
22 CLI_BUILDS_PROJECT_NAME = 'command line builds' 23 CLI_BUILDS_PROJECT_NAME = 'command line builds'
23 24
24 def setUp(self): 25 def setUp(self):
25 bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', 26 builldir = os.environ.get('BUILDDIR', './')
27 bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
26 branch='master', dirpath='') 28 branch='master', dirpath='')
27 release = Release.objects.create(name='release1', 29 release = Release.objects.create(name='release1',
28 bitbake_version=bbv) 30 bitbake_version=bbv)
diff --git a/bitbake/lib/toaster/tests/browser/test_project_config_page.py b/bitbake/lib/toaster/tests/browser/test_project_config_page.py
index 944bcb2631..b9de541efa 100644
--- a/bitbake/lib/toaster/tests/browser/test_project_config_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_project_config_page.py
@@ -7,10 +7,12 @@
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9 9
10import os
10from django.urls import reverse 11from django.urls import reverse
11from tests.browser.selenium_helpers import SeleniumTestCase 12from tests.browser.selenium_helpers import SeleniumTestCase
12 13
13from orm.models import BitbakeVersion, Release, Project, ProjectVariable 14from orm.models import BitbakeVersion, Release, Project, ProjectVariable
15from selenium.webdriver.common.by import By
14 16
15class TestProjectConfigsPage(SeleniumTestCase): 17class TestProjectConfigsPage(SeleniumTestCase):
16 """ Test data at /project/X/builds is displayed correctly """ 18 """ Test data at /project/X/builds is displayed correctly """
@@ -21,7 +23,8 @@ class TestProjectConfigsPage(SeleniumTestCase):
21 'any of these characters' 23 'any of these characters'
22 24
23 def setUp(self): 25 def setUp(self):
24 bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', 26 builldir = os.environ.get('BUILDDIR', './')
27 bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
25 branch='master', dirpath='') 28 branch='master', dirpath='')
26 release = Release.objects.create(name='release1', 29 release = Release.objects.create(name='release1',
27 bitbake_version=bbv) 30 bitbake_version=bbv)
@@ -66,7 +69,7 @@ class TestProjectConfigsPage(SeleniumTestCase):
66 69
67 self.enter_text('#new-imagefs_types', imagefs_type) 70 self.enter_text('#new-imagefs_types', imagefs_type)
68 71
69 checkboxes = self.driver.find_elements_by_xpath("//input[@class='fs-checkbox-fstypes']") 72 checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']")
70 73
71 for checkbox in checkboxes: 74 for checkbox in checkboxes:
72 if checkbox.get_attribute("value") == "btrfs": 75 if checkbox.get_attribute("value") == "btrfs":
@@ -95,7 +98,7 @@ class TestProjectConfigsPage(SeleniumTestCase):
95 for checkbox in checkboxes: 98 for checkbox in checkboxes:
96 if checkbox.get_attribute("value") == "cpio": 99 if checkbox.get_attribute("value") == "cpio":
97 checkbox.click() 100 checkbox.click()
98 element = self.driver.find_element_by_id('new-imagefs_types') 101 element = self.driver.find_element(By.ID, 'new-imagefs_types')
99 102
100 self.wait_until_visible('#new-imagefs_types') 103 self.wait_until_visible('#new-imagefs_types')
101 104
@@ -129,7 +132,7 @@ class TestProjectConfigsPage(SeleniumTestCase):
129 self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) 132 self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)
130 133
131 # downloads dir path has a space 134 # downloads dir path has a space
132 self.driver.find_element_by_id('new-dl_dir').clear() 135 self.driver.find_element(By.ID, 'new-dl_dir').clear()
133 self.enter_text('#new-dl_dir', '/foo/bar a') 136 self.enter_text('#new-dl_dir', '/foo/bar a')
134 137
135 element = self.wait_until_visible('#hintError-dl_dir') 138 element = self.wait_until_visible('#hintError-dl_dir')
@@ -137,7 +140,7 @@ class TestProjectConfigsPage(SeleniumTestCase):
137 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) 140 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
138 141
139 # downloads dir path starts with ${...} but has a space 142 # downloads dir path starts with ${...} but has a space
140 self.driver.find_element_by_id('new-dl_dir').clear() 143 self.driver.find_element(By.ID,'new-dl_dir').clear()
141 self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') 144 self.enter_text('#new-dl_dir', '${TOPDIR}/down foo')
142 145
143 element = self.wait_until_visible('#hintError-dl_dir') 146 element = self.wait_until_visible('#hintError-dl_dir')
@@ -145,18 +148,18 @@ class TestProjectConfigsPage(SeleniumTestCase):
145 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) 148 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
146 149
147 # downloads dir path starts with / 150 # downloads dir path starts with /
148 self.driver.find_element_by_id('new-dl_dir').clear() 151 self.driver.find_element(By.ID,'new-dl_dir').clear()
149 self.enter_text('#new-dl_dir', '/bar/foo') 152 self.enter_text('#new-dl_dir', '/bar/foo')
150 153
151 hidden_element = self.driver.find_element_by_id('hintError-dl_dir') 154 hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
152 self.assertEqual(hidden_element.is_displayed(), False, 155 self.assertEqual(hidden_element.is_displayed(), False,
153 'downloads directory path valid but treated as invalid') 156 'downloads directory path valid but treated as invalid')
154 157
155 # downloads dir path starts with ${...} 158 # downloads dir path starts with ${...}
156 self.driver.find_element_by_id('new-dl_dir').clear() 159 self.driver.find_element(By.ID,'new-dl_dir').clear()
157 self.enter_text('#new-dl_dir', '${TOPDIR}/down') 160 self.enter_text('#new-dl_dir', '${TOPDIR}/down')
158 161
159 hidden_element = self.driver.find_element_by_id('hintError-dl_dir') 162 hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
160 self.assertEqual(hidden_element.is_displayed(), False, 163 self.assertEqual(hidden_element.is_displayed(), False,
161 'downloads directory path valid but treated as invalid') 164 'downloads directory path valid but treated as invalid')
162 165
@@ -184,7 +187,7 @@ class TestProjectConfigsPage(SeleniumTestCase):
184 self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) 187 self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)
185 188
186 # path has a space 189 # path has a space
187 self.driver.find_element_by_id('new-sstate_dir').clear() 190 self.driver.find_element(By.ID, 'new-sstate_dir').clear()
188 self.enter_text('#new-sstate_dir', '/foo/bar a') 191 self.enter_text('#new-sstate_dir', '/foo/bar a')
189 192
190 element = self.wait_until_visible('#hintError-sstate_dir') 193 element = self.wait_until_visible('#hintError-sstate_dir')
@@ -192,7 +195,7 @@ class TestProjectConfigsPage(SeleniumTestCase):
192 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) 195 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
193 196
194 # path starts with ${...} but has a space 197 # path starts with ${...} but has a space
195 self.driver.find_element_by_id('new-sstate_dir').clear() 198 self.driver.find_element(By.ID,'new-sstate_dir').clear()
196 self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') 199 self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo')
197 200
198 element = self.wait_until_visible('#hintError-sstate_dir') 201 element = self.wait_until_visible('#hintError-sstate_dir')
@@ -200,18 +203,18 @@ class TestProjectConfigsPage(SeleniumTestCase):
200 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) 203 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
201 204
202 # path starts with / 205 # path starts with /
203 self.driver.find_element_by_id('new-sstate_dir').clear() 206 self.driver.find_element(By.ID,'new-sstate_dir').clear()
204 self.enter_text('#new-sstate_dir', '/bar/foo') 207 self.enter_text('#new-sstate_dir', '/bar/foo')
205 208
206 hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') 209 hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
207 self.assertEqual(hidden_element.is_displayed(), False, 210 self.assertEqual(hidden_element.is_displayed(), False,
208 'sstate directory path valid but treated as invalid') 211 'sstate directory path valid but treated as invalid')
209 212
210 # paths starts with ${...} 213 # paths starts with ${...}
211 self.driver.find_element_by_id('new-sstate_dir').clear() 214 self.driver.find_element(By.ID, 'new-sstate_dir').clear()
212 self.enter_text('#new-sstate_dir', '${TOPDIR}/down') 215 self.enter_text('#new-sstate_dir', '${TOPDIR}/down')
213 216
214 hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') 217 hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
215 self.assertEqual(hidden_element.is_displayed(), False, 218 self.assertEqual(hidden_element.is_displayed(), False,
216 'sstate directory path valid but treated as invalid') 219 'sstate directory path valid but treated as invalid')
217 220
diff --git a/bitbake/lib/toaster/tests/browser/test_sample.py b/bitbake/lib/toaster/tests/browser/test_sample.py
index b0067c21cd..f04f1d9a16 100644
--- a/bitbake/lib/toaster/tests/browser/test_sample.py
+++ b/bitbake/lib/toaster/tests/browser/test_sample.py
@@ -27,3 +27,13 @@ class TestSample(SeleniumTestCase):
27 self.get(url) 27 self.get(url)
28 brand_link = self.find('.toaster-navbar-brand a.brand') 28 brand_link = self.find('.toaster-navbar-brand a.brand')
29 self.assertEqual(brand_link.text.strip(), 'Toaster') 29 self.assertEqual(brand_link.text.strip(), 'Toaster')
30
31 def test_no_builds_message(self):
32 """ Test that a message is shown when there are no builds """
33 url = reverse('all-builds')
34 self.get(url)
35 self.wait_until_visible('#empty-state-allbuildstable') # wait for the empty state div to appear
36 div_msg = self.find('#empty-state-allbuildstable .alert-info')
37
38 msg = 'Sorry - no data found'
39 self.assertEqual(div_msg.text, msg)
diff --git a/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py b/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py
index e82d5ec654..691aca1ef0 100644
--- a/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py
+++ b/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py
@@ -8,11 +8,13 @@
8# 8#
9 9
10from datetime import datetime 10from datetime import datetime
11import os
11 12
12from django.urls import reverse 13from django.urls import reverse
13from django.utils import timezone 14from django.utils import timezone
14from tests.browser.selenium_helpers import SeleniumTestCase 15from tests.browser.selenium_helpers import SeleniumTestCase
15from orm.models import BitbakeVersion, Release, Project, Build 16from orm.models import BitbakeVersion, Release, Project, Build
17from selenium.webdriver.common.by import By
16 18
17class TestToasterTableUI(SeleniumTestCase): 19class TestToasterTableUI(SeleniumTestCase):
18 """ 20 """
@@ -33,7 +35,7 @@ class TestToasterTableUI(SeleniumTestCase):
33 table: WebElement for a ToasterTable 35 table: WebElement for a ToasterTable
34 """ 36 """
35 selector = 'thead a.sorted' 37 selector = 'thead a.sorted'
36 heading = table.find_element_by_css_selector(selector) 38 heading = table.find_element(By.CSS_SELECTOR, selector)
37 return heading.get_attribute('innerHTML').strip() 39 return heading.get_attribute('innerHTML').strip()
38 40
39 def _get_datetime_from_cell(self, row, selector): 41 def _get_datetime_from_cell(self, row, selector):
@@ -45,7 +47,7 @@ class TestToasterTableUI(SeleniumTestCase):
45 selector: CSS selector to use to find the cell containing the date time 47 selector: CSS selector to use to find the cell containing the date time
46 string 48 string
47 """ 49 """
48 cell = row.find_element_by_css_selector(selector) 50 cell = row.find_element(By.CSS_SELECTOR, selector)
49 cell_text = cell.get_attribute('innerHTML').strip() 51 cell_text = cell.get_attribute('innerHTML').strip()
50 return datetime.strptime(cell_text, '%d/%m/%y %H:%M') 52 return datetime.strptime(cell_text, '%d/%m/%y %H:%M')
51 53
@@ -58,7 +60,8 @@ class TestToasterTableUI(SeleniumTestCase):
58 later = now + timezone.timedelta(hours=1) 60 later = now + timezone.timedelta(hours=1)
59 even_later = later + timezone.timedelta(hours=1) 61 even_later = later + timezone.timedelta(hours=1)
60 62
61 bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', 63 builldir = os.environ.get('BUILDDIR', './')
64 bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/',
62 branch='master', dirpath='') 65 branch='master', dirpath='')
63 release = Release.objects.create(name='test release', 66 release = Release.objects.create(name='test release',
64 branch_name='master', 67 branch_name='master',
@@ -105,7 +108,7 @@ class TestToasterTableUI(SeleniumTestCase):
105 self.click('#checkbox-started_on') 108 self.click('#checkbox-started_on')
106 109
107 # sort by started_on column 110 # sort by started_on column
108 links = table.find_elements_by_css_selector('th.started_on a') 111 links = table.find_elements(By.CSS_SELECTOR, 'th.started_on a')
109 for link in links: 112 for link in links:
110 if link.get_attribute('innerHTML').strip() == 'Started on': 113 if link.get_attribute('innerHTML').strip() == 'Started on':
111 link.click() 114 link.click()
diff --git a/bitbake/lib/toaster/tests/builds/buildtest.py b/bitbake/lib/toaster/tests/builds/buildtest.py
index 872bbd3775..cacfccd4d3 100644
--- a/bitbake/lib/toaster/tests/builds/buildtest.py
+++ b/bitbake/lib/toaster/tests/builds/buildtest.py
@@ -88,7 +88,7 @@ def load_build_environment():
88class BuildTest(unittest.TestCase): 88class BuildTest(unittest.TestCase):
89 89
90 PROJECT_NAME = "Testbuild" 90 PROJECT_NAME = "Testbuild"
91 BUILDDIR = "/tmp/build/" 91 BUILDDIR = os.environ.get("BUILDDIR")
92 92
93 def build(self, target): 93 def build(self, target):
94 # So that the buildinfo helper uses the test database' 94 # So that the buildinfo helper uses the test database'
@@ -116,10 +116,19 @@ class BuildTest(unittest.TestCase):
116 project = Project.objects.create_project(name=BuildTest.PROJECT_NAME, 116 project = Project.objects.create_project(name=BuildTest.PROJECT_NAME,
117 release=release) 117 release=release)
118 118
119 passthrough_variable_names = ["SSTATE_DIR", "DL_DIR", "SSTATE_MIRRORS", "BB_HASHSERVE", "BB_HASHSERVE_UPSTREAM"]
120 for variable_name in passthrough_variable_names:
121 current_variable = os.environ.get(variable_name)
122 if current_variable:
123 ProjectVariable.objects.get_or_create(
124 name=variable_name,
125 value=current_variable,
126 project=project)
127
119 if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"): 128 if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"):
120 ProjectVariable.objects.get_or_create( 129 ProjectVariable.objects.get_or_create(
121 name="SSTATE_MIRRORS", 130 name="SSTATE_MIRRORS",
122 value="file://.* http://autobuilder.yoctoproject.org/pub/sstate/PATH;downloadfilename=PATH", 131 value="file://.* http://cdn.jsdelivr.net/yocto/sstate/all/PATH;downloadfilename=PATH",
123 project=project) 132 project=project)
124 133
125 ProjectTarget.objects.create(project=project, 134 ProjectTarget.objects.create(project=project,
diff --git a/bitbake/lib/toaster/tests/builds/test_core_image_min.py b/bitbake/lib/toaster/tests/builds/test_core_image_min.py
index 44b6cbec7b..c5bfdbfbb5 100644
--- a/bitbake/lib/toaster/tests/builds/test_core_image_min.py
+++ b/bitbake/lib/toaster/tests/builds/test_core_image_min.py
@@ -10,6 +10,7 @@
10# Ionut Chisanovici, Paul Eggleton and Cristian Iorga 10# Ionut Chisanovici, Paul Eggleton and Cristian Iorga
11 11
12import os 12import os
13import pytest
13 14
14from django.db.models import Q 15from django.db.models import Q
15 16
@@ -20,12 +21,13 @@ from orm.models import CustomImagePackage
20 21
21from tests.builds.buildtest import BuildTest 22from tests.builds.buildtest import BuildTest
22 23
23 24@pytest.mark.order(4)
25@pytest.mark.django_db(True)
24class BuildCoreImageMinimal(BuildTest): 26class BuildCoreImageMinimal(BuildTest):
25 """Build core-image-minimal and test the results""" 27 """Build core-image-minimal and test the results"""
26 28
27 def setUp(self): 29 def setUp(self):
28 self.completed_build = self.build("core-image-minimal") 30 self.completed_build = self.target_already_built("core-image-minimal")
29 31
30 # Check if build name is unique - tc_id=795 32 # Check if build name is unique - tc_id=795
31 def test_Build_Unique_Name(self): 33 def test_Build_Unique_Name(self):
@@ -44,17 +46,6 @@ class BuildCoreImageMinimal(BuildTest):
44 total_builds, 46 total_builds,
45 msg='Build cooker log path is not unique') 47 msg='Build cooker log path is not unique')
46 48
47 # Check if task order is unique for one build - tc=824
48 def test_Task_Unique_Order(self):
49 total_task_order = Task.objects.filter(
50 build=self.built).values('order').count()
51 distinct_task_order = Task.objects.filter(
52 build=self.completed_build).values('order').distinct().count()
53
54 self.assertEqual(total_task_order,
55 distinct_task_order,
56 msg='Errors task order is not unique')
57
58 # Check task order sequence for one build - tc=825 49 # Check task order sequence for one build - tc=825
59 def test_Task_Order_Sequence(self): 50 def test_Task_Order_Sequence(self):
60 cnt_err = [] 51 cnt_err = []
@@ -98,7 +89,6 @@ class BuildCoreImageMinimal(BuildTest):
98 'task_name', 89 'task_name',
99 'sstate_result') 90 'sstate_result')
100 cnt_err = [] 91 cnt_err = []
101
102 for task in tasks: 92 for task in tasks:
103 if (task['sstate_result'] != Task.SSTATE_NA and 93 if (task['sstate_result'] != Task.SSTATE_NA and
104 task['sstate_result'] != Task.SSTATE_MISS): 94 task['sstate_result'] != Task.SSTATE_MISS):
@@ -221,6 +211,7 @@ class BuildCoreImageMinimal(BuildTest):
221 # orm_build.outcome=0 then if the file exists and its size matches 211 # orm_build.outcome=0 then if the file exists and its size matches
222 # the file_size value. Need to add the tc in the test run 212 # the file_size value. Need to add the tc in the test run
223 def test_Target_File_Name_Populated(self): 213 def test_Target_File_Name_Populated(self):
214 cnt_err = []
224 builds = Build.objects.filter(outcome=0).values('id') 215 builds = Build.objects.filter(outcome=0).values('id')
225 for build in builds: 216 for build in builds:
226 targets = Target.objects.filter( 217 targets = Target.objects.filter(
@@ -230,7 +221,6 @@ class BuildCoreImageMinimal(BuildTest):
230 target_id=target['id']).values('id', 221 target_id=target['id']).values('id',
231 'file_name', 222 'file_name',
232 'file_size') 223 'file_size')
233 cnt_err = []
234 for file_info in target_files: 224 for file_info in target_files:
235 target_id = file_info['id'] 225 target_id = file_info['id']
236 target_file_name = file_info['file_name'] 226 target_file_name = file_info['file_name']
diff --git a/bitbake/lib/toaster/tests/commands/test_loaddata.py b/bitbake/lib/toaster/tests/commands/test_loaddata.py
index 9e8d5553cf..7d04f030ee 100644
--- a/bitbake/lib/toaster/tests/commands/test_loaddata.py
+++ b/bitbake/lib/toaster/tests/commands/test_loaddata.py
@@ -6,13 +6,13 @@
6# 6#
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9 9import pytest
10from django.test import TestCase 10from django.test import TestCase
11from django.core import management 11from django.core import management
12 12
13from orm.models import Layer_Version, Layer, Release, ToasterSetting 13from orm.models import Layer_Version, Layer, Release, ToasterSetting
14 14
15 15@pytest.mark.order(2)
16class TestLoadDataFixtures(TestCase): 16class TestLoadDataFixtures(TestCase):
17 """ Test loading our 3 provided fixtures """ 17 """ Test loading our 3 provided fixtures """
18 def test_run_loaddata_poky_command(self): 18 def test_run_loaddata_poky_command(self):
diff --git a/bitbake/lib/toaster/tests/commands/test_lsupdates.py b/bitbake/lib/toaster/tests/commands/test_lsupdates.py
index 3c4fbe0550..30c6eeb4ac 100644
--- a/bitbake/lib/toaster/tests/commands/test_lsupdates.py
+++ b/bitbake/lib/toaster/tests/commands/test_lsupdates.py
@@ -7,12 +7,13 @@
7# SPDX-License-Identifier: GPL-2.0-only 7# SPDX-License-Identifier: GPL-2.0-only
8# 8#
9 9
10import pytest
10from django.test import TestCase 11from django.test import TestCase
11from django.core import management 12from django.core import management
12 13
13from orm.models import Layer_Version, Machine, Recipe 14from orm.models import Layer_Version, Machine, Recipe
14 15
15 16@pytest.mark.order(3)
16class TestLayerIndexUpdater(TestCase): 17class TestLayerIndexUpdater(TestCase):
17 def test_run_lsupdates_command(self): 18 def test_run_lsupdates_command(self):
18 # Load some release information for us to fetch from the layer index 19 # Load some release information for us to fetch from the layer index
diff --git a/bitbake/lib/toaster/tests/commands/test_runbuilds.py b/bitbake/lib/toaster/tests/commands/test_runbuilds.py
index e223b95fcb..849c227edc 100644
--- a/bitbake/lib/toaster/tests/commands/test_runbuilds.py
+++ b/bitbake/lib/toaster/tests/commands/test_runbuilds.py
@@ -19,12 +19,14 @@ import time
19import subprocess 19import subprocess
20import signal 20import signal
21 21
22import logging
23
22 24
23class KillRunbuilds(threading.Thread): 25class KillRunbuilds(threading.Thread):
24 """ Kill the runbuilds process after an amount of time """ 26 """ Kill the runbuilds process after an amount of time """
25 def __init__(self, *args, **kwargs): 27 def __init__(self, *args, **kwargs):
26 super(KillRunbuilds, self).__init__(*args, **kwargs) 28 super(KillRunbuilds, self).__init__(*args, **kwargs)
27 self.setDaemon(True) 29 self.daemon = True
28 30
29 def run(self): 31 def run(self):
30 time.sleep(5) 32 time.sleep(5)
@@ -34,9 +36,12 @@ class KillRunbuilds(threading.Thread):
34 pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."), 36 pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."),
35 ".runbuilds.pid") 37 ".runbuilds.pid")
36 38
37 with open(pidfile_path) as pidfile: 39 try:
38 pid = pidfile.read() 40 with open(pidfile_path) as pidfile:
39 os.kill(int(pid), signal.SIGTERM) 41 pid = pidfile.read()
42 os.kill(int(pid), signal.SIGTERM)
43 except ProcessLookupError:
44 logging.warning("Runbuilds not running or already killed")
40 45
41 46
42class TestCommands(TestCase): 47class TestCommands(TestCase):
diff --git a/bitbake/lib/toaster/tests/db/test_db.py b/bitbake/lib/toaster/tests/db/test_db.py
index 0410422276..072ab94363 100644
--- a/bitbake/lib/toaster/tests/db/test_db.py
+++ b/bitbake/lib/toaster/tests/db/test_db.py
@@ -23,6 +23,7 @@
23# SOFTWARE. 23# SOFTWARE.
24 24
25import sys 25import sys
26import pytest
26 27
27try: 28try:
28 from StringIO import StringIO 29 from StringIO import StringIO
@@ -47,7 +48,7 @@ def capture(command, *args, **kwargs):
47def makemigrations(): 48def makemigrations():
48 management.call_command('makemigrations') 49 management.call_command('makemigrations')
49 50
50 51@pytest.mark.order(1)
51class MigrationTest(TestCase): 52class MigrationTest(TestCase):
52 53
53 def testPendingMigration(self): 54 def testPendingMigration(self):
diff --git a/bitbake/lib/toaster/tests/functional/functional_helpers.py b/bitbake/lib/toaster/tests/functional/functional_helpers.py
index 5c4ea71794..7c20437d14 100644
--- a/bitbake/lib/toaster/tests/functional/functional_helpers.py
+++ b/bitbake/lib/toaster/tests/functional/functional_helpers.py
@@ -11,35 +11,55 @@ import os
11import logging 11import logging
12import subprocess 12import subprocess
13import signal 13import signal
14import time
15import re 14import re
16 15
17from tests.browser.selenium_helpers_base import SeleniumTestCaseBase 16from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
18from tests.builds.buildtest import load_build_environment 17from selenium.webdriver.common.by import By
18from selenium.common.exceptions import NoSuchElementException
19 19
20logger = logging.getLogger("toaster") 20logger = logging.getLogger("toaster")
21toaster_processes = []
21 22
22class SeleniumFunctionalTestCase(SeleniumTestCaseBase): 23class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
23 wait_toaster_time = 5 24 wait_toaster_time = 10
24 25
25 @classmethod 26 @classmethod
26 def setUpClass(cls): 27 def setUpClass(cls):
27 # So that the buildinfo helper uses the test database' 28 # So that the buildinfo helper uses the test database'
28 if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \ 29 if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \
29 'toastermain.settings_test': 30 'toastermain.settings_test':
30 raise RuntimeError("Please initialise django with the tests settings: " \ 31 raise RuntimeError("Please initialise django with the tests settings: "
31 "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") 32 "DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
32 33
33 load_build_environment() 34 # Wait for any known toaster processes to exit
35 global toaster_processes
36 for toaster_process in toaster_processes:
37 try:
38 os.waitpid(toaster_process, os.WNOHANG)
39 except ChildProcessError:
40 pass
34 41
35 # start toaster 42 # start toaster
36 cmd = "bash -c 'source toaster start'" 43 cmd = "bash -c 'source toaster start'"
37 p = subprocess.Popen( 44 start_process = subprocess.Popen(
38 cmd, 45 cmd,
39 cwd=os.environ.get("BUILDDIR"), 46 cwd=os.environ.get("BUILDDIR"),
40 shell=True) 47 shell=True)
41 if p.wait() != 0: 48 toaster_processes = [start_process.pid]
42 raise RuntimeError("Can't initialize toaster") 49 if start_process.wait() != 0:
50 port_use = os.popen("lsof -i -P -n | grep '8000 (LISTEN)'").read().strip()
51 message = ''
52 if port_use:
53 process_id = port_use.split()[1]
54 process = os.popen(f"ps -o cmd= -p {process_id}").read().strip()
55 message = f"Port 8000 occupied by {process}"
56 raise RuntimeError(f"Can't initialize toaster. {message}")
57
58 builddir = os.environ.get("BUILDDIR")
59 with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f:
60 toaster_processes.append(int(f.read()))
61 with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f:
62 toaster_processes.append(int(f.read()))
43 63
44 super(SeleniumFunctionalTestCase, cls).setUpClass() 64 super(SeleniumFunctionalTestCase, cls).setUpClass()
45 cls.live_server_url = 'http://localhost:8000/' 65 cls.live_server_url = 'http://localhost:8000/'
@@ -48,22 +68,30 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
48 def tearDownClass(cls): 68 def tearDownClass(cls):
49 super(SeleniumFunctionalTestCase, cls).tearDownClass() 69 super(SeleniumFunctionalTestCase, cls).tearDownClass()
50 70
51 # XXX: source toaster stop gets blocked, to review why? 71 global toaster_processes
52 # from now send SIGTERM by hand
53 time.sleep(cls.wait_toaster_time)
54 builddir = os.environ.get("BUILDDIR")
55 72
56 with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f: 73 cmd = "bash -c 'source toaster stop'"
57 toastermain_pid = int(f.read()) 74 stop_process = subprocess.Popen(
58 os.kill(toastermain_pid, signal.SIGTERM) 75 cmd,
59 with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: 76 cwd=os.environ.get("BUILDDIR"),
60 runbuilds_pid = int(f.read()) 77 shell=True)
61 os.kill(runbuilds_pid, signal.SIGTERM) 78 # Toaster stop has been known to hang in these tests so force kill if it stalls
79 try:
80 if stop_process.wait(cls.wait_toaster_time) != 0:
81 raise Exception('Toaster stop process failed')
82 except Exception as e:
83 if e is subprocess.TimeoutExpired:
84 print('Toaster stop process took too long. Force killing toaster...')
85 else:
86 print('Toaster stop process failed. Force killing toaster...')
87 stop_process.kill()
88 for toaster_process in toaster_processes:
89 os.kill(toaster_process, signal.SIGTERM)
62 90
63 91
64 def get_URL(self): 92 def get_URL(self):
65 rc=self.get_page_source() 93 rc=self.get_page_source()
66 project_url=re.search("(projectPageUrl\s:\s\")(.*)(\",)",rc) 94 project_url=re.search(r"(projectPageUrl\s:\s\")(.*)(\",)",rc)
67 return project_url.group(2) 95 return project_url.group(2)
68 96
69 97
@@ -74,8 +102,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
74 """ 102 """
75 try: 103 try:
76 table_element = self.get_table_element(table_id) 104 table_element = self.get_table_element(table_id)
77 element = table_element.find_element_by_link_text(link_text) 105 element = table_element.find_element(By.LINK_TEXT, link_text)
78 except self.NoSuchElementException: 106 except NoSuchElementException:
79 print('no element found') 107 print('no element found')
80 raise 108 raise
81 return element 109 return element
@@ -85,8 +113,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
85#return whole-table element 113#return whole-table element
86 element_xpath = "//*[@id='" + table_id + "']" 114 element_xpath = "//*[@id='" + table_id + "']"
87 try: 115 try:
88 element = self.driver.find_element_by_xpath(element_xpath) 116 element = self.driver.find_element(By.XPATH, element_xpath)
89 except self.NoSuchElementException: 117 except NoSuchElementException:
90 raise 118 raise
91 return element 119 return element
92 row = coordinate[0] 120 row = coordinate[0]
@@ -95,8 +123,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
95#return whole-row element 123#return whole-row element
96 element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]" 124 element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]"
97 try: 125 try:
98 element = self.driver.find_element_by_xpath(element_xpath) 126 element = self.driver.find_element(By.XPATH, element_xpath)
99 except self.NoSuchElementException: 127 except NoSuchElementException:
100 return False 128 return False
101 return element 129 return element
102#now we are looking for an element with specified X and Y 130#now we are looking for an element with specified X and Y
@@ -104,7 +132,7 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
104 132
105 element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" 133 element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]"
106 try: 134 try:
107 element = self.driver.find_element_by_xpath(element_xpath) 135 element = self.driver.find_element(By.XPATH, element_xpath)
108 except self.NoSuchElementException: 136 except NoSuchElementException:
109 return False 137 return False
110 return element 138 return element
diff --git a/bitbake/lib/toaster/tests/functional/test_create_new_project.py b/bitbake/lib/toaster/tests/functional/test_create_new_project.py
new file mode 100644
index 0000000000..94d90459e1
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/test_create_new_project.py
@@ -0,0 +1,179 @@
1#! /usr/bin/env python3
2# BitBake Toaster UI tests implementation
3#
4# Copyright (C) 2023 Savoir-faire Linux
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import re
10import pytest
11from django.urls import reverse
12from selenium.webdriver.support.select import Select
13from tests.functional.functional_helpers import SeleniumFunctionalTestCase
14from orm.models import Project
15from selenium.webdriver.common.by import By
16
17
18@pytest.mark.django_db
19@pytest.mark.order("last")
20class TestCreateNewProject(SeleniumFunctionalTestCase):
21
22 def _create_test_new_project(
23 self,
24 project_name,
25 release,
26 release_title,
27 merge_toaster_settings,
28 ):
29 """ Create/Test new project using:
30 - Project Name: Any string
31 - Release: Any string
32 - Merge Toaster settings: True or False
33 """
34 self.get(reverse('newproject'))
35 self.wait_until_visible('#new-project-name', poll=3)
36 self.driver.find_element(By.ID,
37 "new-project-name").send_keys(project_name)
38
39 select = Select(self.find('#projectversion'))
40 select.select_by_value(release)
41
42 # check merge toaster settings
43 checkbox = self.find('.checkbox-mergeattr')
44 if merge_toaster_settings:
45 if not checkbox.is_selected():
46 checkbox.click()
47 else:
48 if checkbox.is_selected():
49 checkbox.click()
50
51 self.driver.find_element(By.ID, "create-project-button").click()
52
53 element = self.wait_until_visible('#project-created-notification', poll=3)
54 self.assertTrue(
55 self.element_exists('#project-created-notification'),
56 f"Project:{project_name} creation notification not shown"
57 )
58 self.assertTrue(
59 project_name in element.text,
60 f"New project name:{project_name} not in new project notification"
61 )
62 self.assertTrue(
63 Project.objects.filter(name=project_name).count(),
64 f"New project:{project_name} not found in database"
65 )
66
67 # check release
68 self.assertTrue(re.search(
69 release_title,
70 self.driver.find_element(By.XPATH,
71 "//span[@id='project-release-title']"
72 ).text),
73 'The project release is not defined')
74
75 def test_create_new_project_master(self):
76 """ Test create new project using:
77 - Project Name: Any string
78 - Release: Yocto Project master (option value: 3)
79 - Merge Toaster settings: False
80 """
81 release = '3'
82 release_title = 'Yocto Project master'
83 project_name = 'projectmaster'
84 self._create_test_new_project(
85 project_name,
86 release,
87 release_title,
88 False,
89 )
90
91 def test_create_new_project_kirkstone(self):
92 """ Test create new project using:
93 - Project Name: Any string
94 - Release: Yocto Project 4.0 "Kirkstone" (option value: 1)
95 - Merge Toaster settings: True
96 """
97 release = '1'
98 release_title = 'Yocto Project 4.0 "Kirkstone"'
99 project_name = 'projectkirkstone'
100 self._create_test_new_project(
101 project_name,
102 release,
103 release_title,
104 True,
105 )
106
107 def test_create_new_project_dunfell(self):
108 """ Test create new project using:
109 - Project Name: Any string
110 - Release: Yocto Project 3.1 "Dunfell" (option value: 5)
111 - Merge Toaster settings: False
112 """
113 release = '5'
114 release_title = 'Yocto Project 3.1 "Dunfell"'
115 project_name = 'projectdunfell'
116 self._create_test_new_project(
117 project_name,
118 release,
119 release_title,
120 False,
121 )
122
123 def test_create_new_project_local(self):
124 """ Test create new project using:
125 - Project Name: Any string
126 - Release: Local Yocto Project (option value: 2)
127 - Merge Toaster settings: True
128 """
129 release = '2'
130 release_title = 'Local Yocto Project'
131 project_name = 'projectlocal'
132 self._create_test_new_project(
133 project_name,
134 release,
135 release_title,
136 True,
137 )
138
139 def test_create_new_project_without_name(self):
140 """ Test create new project without project name """
141 self.get(reverse('newproject'))
142
143 select = Select(self.find('#projectversion'))
144 select.select_by_value(str(3))
145
146 # Check input name has required attribute
147 input_name = self.driver.find_element(By.ID, "new-project-name")
148 self.assertIsNotNone(input_name.get_attribute('required'),
149 'Input name has not required attribute')
150
151 # Check create button is disabled
152 create_btn = self.driver.find_element(By.ID, "create-project-button")
153 self.assertIsNotNone(create_btn.get_attribute('disabled'),
154 'Create button is not disabled')
155
156 def test_import_new_project(self):
157 """ Test import new project using:
158 - Project Name: Any string
159 - Project type: select (Import command line project)
160 - Import existing project directory: Wrong Path
161 """
162 project_name = 'projectimport'
163 self.get(reverse('newproject'))
164 self.driver.find_element(By.ID,
165 "new-project-name").send_keys(project_name)
166 # select import project
167 self.find('#type-import').click()
168
169 # set wrong path
170 wrong_path = '/wrongpath'
171 self.driver.find_element(By.ID,
172 "import-project-dir").send_keys(wrong_path)
173 self.driver.find_element(By.ID, "create-project-button").click()
174
175 # check error message
176 self.assertTrue(self.element_exists('.alert-danger'),
177 'Allert message not shown')
178 self.assertTrue(wrong_path in self.find('.alert-danger').text,
179 "Wrong path not in alert message")
diff --git a/bitbake/lib/toaster/tests/functional/test_functional_basic.py b/bitbake/lib/toaster/tests/functional/test_functional_basic.py
index 5683e3873e..e4070fbb88 100644
--- a/bitbake/lib/toaster/tests/functional/test_functional_basic.py
+++ b/bitbake/lib/toaster/tests/functional/test_functional_basic.py
@@ -8,104 +8,129 @@
8# 8#
9 9
10import re 10import re
11from django.urls import reverse
12import pytest
11from tests.functional.functional_helpers import SeleniumFunctionalTestCase 13from tests.functional.functional_helpers import SeleniumFunctionalTestCase
12from orm.models import Project 14from orm.models import Project
15from selenium.webdriver.common.by import By
13 16
17from tests.functional.utils import get_projectId_from_url
18
19
20@pytest.mark.django_db
21@pytest.mark.order("second_to_last")
14class FuntionalTestBasic(SeleniumFunctionalTestCase): 22class FuntionalTestBasic(SeleniumFunctionalTestCase):
23 """Basic functional tests for Toaster"""
24 project_id = None
25
26 def setUp(self):
27 super(FuntionalTestBasic, self).setUp()
28 if not FuntionalTestBasic.project_id:
29 self._create_slenium_project()
30 current_url = self.driver.current_url
31 FuntionalTestBasic.project_id = get_projectId_from_url(current_url)
15 32
16# testcase (1514) 33# testcase (1514)
17 def test_create_slenium_project(self): 34 def _create_slenium_project(self):
18 project_name = 'selenium-project' 35 project_name = 'selenium-project'
19 self.get('') 36 self.get(reverse('newproject'))
20 self.driver.find_element_by_link_text("To start building, create your first Toaster project").click() 37 self.wait_until_visible('#new-project-name', poll=3)
21 self.driver.find_element_by_id("new-project-name").send_keys(project_name) 38 self.driver.find_element(By.ID, "new-project-name").send_keys(project_name)
22 self.driver.find_element_by_id('projectversion').click() 39 self.driver.find_element(By.ID, 'projectversion').click()
23 self.driver.find_element_by_id("create-project-button").click() 40 self.driver.find_element(By.ID, "create-project-button").click()
24 element = self.wait_until_visible('#project-created-notification') 41 element = self.wait_until_visible('#project-created-notification', poll=10)
25 self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown') 42 self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown')
26 self.assertTrue(project_name in element.text, 43 self.assertTrue(project_name in element.text,
27 "New project name not in new project notification") 44 "New project name not in new project notification")
28 self.assertTrue(Project.objects.filter(name=project_name).count(), 45 self.assertTrue(Project.objects.filter(name=project_name).count(),
29 "New project not found in database") 46 "New project not found in database")
47 return Project.objects.last().id
30 48
31 # testcase (1515) 49 # testcase (1515)
32 def test_verify_left_bar_menu(self): 50 def test_verify_left_bar_menu(self):
33 self.get('') 51 self.get(reverse('all-projects'))
34 self.wait_until_visible('#projectstable') 52 self.wait_until_present('#projectstable', poll=10)
35 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 53 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
54 self.wait_until_present('#config-nav', poll=10)
36 self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist') 55 self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist')
37 project_URL=self.get_URL() 56 project_URL=self.get_URL()
38 self.driver.find_element_by_xpath('//a[@href="'+project_URL+'"]').click() 57 self.driver.find_element(By.XPATH, '//a[@href="'+project_URL+'"]').click()
58 self.wait_until_present('#config-nav', poll=10)
39 59
40 try: 60 try:
41 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click() 61 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click()
42 self.assertTrue(re.search("Custom images",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'Custom images information is not loading properly') 62 self.wait_until_present('#config-nav', poll=10)
63 self.assertTrue(re.search("Custom images",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'Custom images information is not loading properly')
43 except: 64 except:
44 self.fail(msg='No Custom images tab available') 65 self.fail(msg='No Custom images tab available')
45 66
46 try: 67 try:
47 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click() 68 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click()
48 self.assertTrue(re.search("Compatible image recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly') 69 self.wait_until_present('#config-nav', poll=10)
70 self.assertTrue(re.search("Compatible image recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly')
49 except: 71 except:
50 self.fail(msg='No Compatible image tab available') 72 self.fail(msg='No Compatible image tab available')
51 73
52 try: 74 try:
53 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click() 75 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click()
54 self.assertTrue(re.search("Compatible software recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly') 76 self.wait_until_present('#config-nav', poll=10)
77 self.assertTrue(re.search("Compatible software recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly')
55 except: 78 except:
56 self.fail(msg='No Compatible software recipe tab available') 79 self.fail(msg='No Compatible software recipe tab available')
57 80
58 try: 81 try:
59 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click() 82 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click()
60 self.assertTrue(re.search("Compatible machines",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly') 83 self.wait_until_present('#config-nav', poll=10)
84 self.assertTrue(re.search("Compatible machines",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly')
61 except: 85 except:
62 self.fail(msg='No Compatible machines tab available') 86 self.fail(msg='No Compatible machines tab available')
63 87
64 try: 88 try:
65 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click() 89 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click()
66 self.assertTrue(re.search("Compatible layers",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly') 90 self.wait_until_present('#config-nav', poll=10)
91 self.assertTrue(re.search("Compatible layers",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly')
67 except: 92 except:
68 self.fail(msg='No Compatible layers tab available') 93 self.fail(msg='No Compatible layers tab available')
69 94
70 try: 95 try:
71 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click() 96 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click()
72 self.assertTrue(re.search("Bitbake variables",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly') 97 self.wait_until_present('#config-nav', poll=10)
98 self.assertTrue(re.search("Bitbake variables",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly')
73 except: 99 except:
74 self.fail(msg='No Bitbake variables tab available') 100 self.fail(msg='No Bitbake variables tab available')
75 101
76# testcase (1516) 102# testcase (1516)
77 def test_review_configuration_information(self): 103 def test_review_configuration_information(self):
78 self.get('') 104 self.get(reverse('all-projects'))
79 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 105 self.wait_until_present('#projectstable', poll=10)
80 self.wait_until_visible('#projectstable')
81 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 106 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
82 project_URL=self.get_URL() 107 project_URL=self.get_URL()
83 108 self.wait_until_present('#config-nav', poll=10)
84 try: 109 try:
85 self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') 110 self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
86 self.assertTrue(re.search("qemux86",self.driver.find_element_by_xpath("//span[@id='project-machine-name']").text),'The machine type is not assigned') 111 self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.XPATH, "//span[@id='project-machine-name']").text),'The machine type is not assigned')
87 self.driver.find_element_by_xpath("//span[@id='change-machine-toggle']").click() 112 self.driver.find_element(By.XPATH, "//span[@id='change-machine-toggle']").click()
88 self.wait_until_visible('#select-machine-form') 113 self.wait_until_visible('#select-machine-form', poll=10)
89 self.wait_until_visible('#cancel-machine-change') 114 self.wait_until_visible('#cancel-machine-change', poll=10)
90 self.driver.find_element_by_xpath("//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click() 115 self.driver.find_element(By.XPATH, "//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click()
91 except: 116 except:
92 self.fail(msg='The machine information is wrong in the configuration page') 117 self.fail(msg='The machine information is wrong in the configuration page')
93 118
94 try: 119 try:
95 self.driver.find_element_by_id('no-most-built') 120 self.driver.find_element(By.ID, 'no-most-built')
96 except: 121 except:
97 self.fail(msg='No Most built information in project detail page') 122 self.fail(msg='No Most built information in project detail page')
98 123
99 try: 124 try:
100 self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_xpath("//span[@id='project-release-title']").text),'The project release is not defined') 125 self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.XPATH, "//span[@id='project-release-title']").text),'The project release is not defined')
101 except: 126 except:
102 self.fail(msg='No project release title information in project detail page') 127 self.fail(msg='No project release title information in project detail page')
103 128
104 try: 129 try:
105 self.driver.find_element_by_xpath("//div[@id='layer-container']") 130 self.driver.find_element(By.XPATH, "//div[@id='layer-container']")
106 self.assertTrue(re.search("3",self.driver.find_element_by_id("project-layers-count").text),'There should be 3 layers listed in the layer count') 131 self.assertTrue(re.search("3",self.driver.find_element(By.ID, "project-layers-count").text),'There should be 3 layers listed in the layer count')
107 layer_list = self.driver.find_element_by_id("layers-in-project-list") 132 layer_list = self.driver.find_element(By.ID, "layers-in-project-list")
108 layers = layer_list.find_elements_by_tag_name("li") 133 layers = layer_list.find_elements(By.TAG_NAME, "li")
109 for layer in layers: 134 for layer in layers:
110 if re.match ("openembedded-core",layer.text): 135 if re.match ("openembedded-core",layer.text):
111 print ("openembedded-core layer is a default layer in the project configuration") 136 print ("openembedded-core layer is a default layer in the project configuration")
@@ -120,61 +145,60 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase):
120 145
121# testcase (1517) 146# testcase (1517)
122 def test_verify_machine_information(self): 147 def test_verify_machine_information(self):
123 self.get('') 148 self.get(reverse('all-projects'))
124 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 149 self.wait_until_present('#projectstable', poll=10)
125 self.wait_until_visible('#projectstable')
126 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 150 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
151 self.wait_until_present('#config-nav', poll=10)
127 152
128 try: 153 try:
129 self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') 154 self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
130 self.assertTrue(re.search("qemux86",self.driver.find_element_by_id("project-machine-name").text),'The machine type is not assigned') 155 self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.ID, "project-machine-name").text),'The machine type is not assigned')
131 self.driver.find_element_by_id("change-machine-toggle").click() 156 self.driver.find_element(By.ID, "change-machine-toggle").click()
132 self.wait_until_visible('#select-machine-form') 157 self.wait_until_visible('#select-machine-form', poll=10)
133 self.wait_until_visible('#cancel-machine-change') 158 self.wait_until_visible('#cancel-machine-change', poll=10)
134 self.driver.find_element_by_id("cancel-machine-change").click() 159 self.driver.find_element(By.ID, "cancel-machine-change").click()
135 except: 160 except:
136 self.fail(msg='The machine information is wrong in the configuration page') 161 self.fail(msg='The machine information is wrong in the configuration page')
137 162
138# testcase (1518) 163# testcase (1518)
139 def test_verify_most_built_recipes_information(self): 164 def test_verify_most_built_recipes_information(self):
140 self.get('') 165 self.get(reverse('all-projects'))
141 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 166 self.wait_until_present('#projectstable', poll=10)
142 self.wait_until_visible('#projectstable')
143 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 167 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
168 self.wait_until_present('#config-nav', poll=10)
144 project_URL=self.get_URL() 169 project_URL=self.get_URL()
145
146 try: 170 try:
147 self.assertTrue(re.search("You haven't built any recipes yet",self.driver.find_element_by_id("no-most-built").text),'Default message of no builds is not present') 171 self.assertTrue(re.search("You haven't built any recipes yet",self.driver.find_element(By.ID, "no-most-built").text),'Default message of no builds is not present')
148 self.driver.find_element_by_xpath("//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click() 172 self.driver.find_element(By.XPATH, "//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click()
149 self.assertTrue(re.search("Compatible image recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Choose a recipe to build link is not working properly') 173 self.wait_until_present('#config-nav', poll=10)
174 self.assertTrue(re.search("Compatible image recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Choose a recipe to build link is not working properly')
150 except: 175 except:
151 self.fail(msg='No Most built information in project detail page') 176 self.fail(msg='No Most built information in project detail page')
152 177
153# testcase (1519) 178# testcase (1519)
154 def test_verify_project_release_information(self): 179 def test_verify_project_release_information(self):
155 self.get('') 180 self.get(reverse('all-projects'))
156 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 181 self.wait_until_present('#projectstable', poll=10)
157 self.wait_until_visible('#projectstable')
158 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 182 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
183 self.wait_until_present('#config-nav', poll=10)
159 184
160 try: 185 try:
161 self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_id("project-release-title").text),'The project release is not defined') 186 self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.ID, "project-release-title").text),'The project release is not defined')
162 except: 187 except:
163 self.fail(msg='No project release title information in project detail page') 188 self.fail(msg='No project release title information in project detail page')
164 189
165# testcase (1520) 190# testcase (1520)
166 def test_verify_layer_information(self): 191 def test_verify_layer_information(self):
167 self.get('') 192 self.get(reverse('all-projects'))
168 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 193 self.wait_until_present('#projectstable', poll=10)
169 self.wait_until_visible('#projectstable')
170 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 194 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
195 self.wait_until_present('#config-nav', poll=10)
171 project_URL=self.get_URL() 196 project_URL=self.get_URL()
172
173 try: 197 try:
174 self.driver.find_element_by_xpath("//div[@id='layer-container']") 198 self.driver.find_element(By.XPATH, "//div[@id='layer-container']")
175 self.assertTrue(re.search("3",self.driver.find_element_by_id("project-layers-count").text),'There should be 3 layers listed in the layer count') 199 self.assertTrue(re.search("3",self.driver.find_element(By.ID, "project-layers-count").text),'There should be 3 layers listed in the layer count')
176 layer_list = self.driver.find_element_by_id("layers-in-project-list") 200 layer_list = self.driver.find_element(By.ID, "layers-in-project-list")
177 layers = layer_list.find_elements_by_tag_name("li") 201 layers = layer_list.find_elements(By.TAG_NAME, "li")
178 202
179 for layer in layers: 203 for layer in layers:
180 if re.match ("openembedded-core",layer.text): 204 if re.match ("openembedded-core",layer.text):
@@ -186,43 +210,46 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase):
186 else: 210 else:
187 self.fail(msg='default layers are missing from the project configuration') 211 self.fail(msg='default layers are missing from the project configuration')
188 212
189 self.driver.find_element_by_xpath("//input[@id='layer-add-input']") 213 self.driver.find_element(By.XPATH, "//input[@id='layer-add-input']")
190 self.driver.find_element_by_xpath("//button[@id='add-layer-btn']") 214 self.driver.find_element(By.XPATH, "//button[@id='add-layer-btn']")
191 self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']") 215 self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']")
192 self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]") 216 self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]")
193 except: 217 except:
194 self.fail(msg='No Layer information in project detail page') 218 self.fail(msg='No Layer information in project detail page')
195 219
196# testcase (1521) 220# testcase (1521)
197 def test_verify_project_detail_links(self): 221 def test_verify_project_detail_links(self):
198 self.get('') 222 self.get(reverse('all-projects'))
199 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 223 self.wait_until_present('#projectstable', poll=10)
200 self.wait_until_visible('#projectstable')
201 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 224 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
225 self.wait_until_present('#config-nav', poll=10)
202 project_URL=self.get_URL() 226 project_URL=self.get_URL()
203 227 self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").click()
204 self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").click() 228 self.wait_until_present('#config-nav', poll=10)
205 self.assertTrue(re.search("Configuration",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").text), 'Configuration tab in project topbar is misspelled') 229 self.assertTrue(re.search("Configuration",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").text), 'Configuration tab in project topbar is misspelled')
206 230
207 try: 231 try:
208 self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click() 232 self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click()
209 self.assertTrue(re.search("Builds",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled') 233 self.wait_until_visible('#project-topbar', poll=10)
210 self.driver.find_element_by_xpath("//div[@id='empty-state-projectbuildstable']") 234 self.assertTrue(re.search("Builds",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled')
235 self.driver.find_element(By.XPATH, "//div[@id='empty-state-projectbuildstable']")
211 except: 236 except:
212 self.fail(msg='Builds tab information is not present') 237 self.fail(msg='Builds tab information is not present')
213 238
214 try: 239 try:
215 self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click() 240 self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click()
216 self.assertTrue(re.search("Import layer",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled') 241 self.wait_until_visible('#project-topbar', poll=10)
217 self.driver.find_element_by_xpath("//fieldset[@id='repo-select']") 242 self.assertTrue(re.search("Import layer",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled')
218 self.driver.find_element_by_xpath("//fieldset[@id='git-repo']") 243 self.driver.find_element(By.XPATH, "//fieldset[@id='repo-select']")
244 self.driver.find_element(By.XPATH, "//fieldset[@id='git-repo']")
219 except: 245 except:
220 self.fail(msg='Import layer tab not loading properly') 246 self.fail(msg='Import layer tab not loading properly')
221 247
222 try: 248 try:
223 self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click() 249 self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click()
224 self.assertTrue(re.search("New custom image",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled') 250 self.wait_until_visible('#project-topbar', poll=10)
225 self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element_by_xpath("//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly') 251 self.assertTrue(re.search("New custom image",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled')
252 self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element(By.XPATH, "//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly')
226 except: 253 except:
227 self.fail(msg='New custom image tab not loading properly') 254 self.fail(msg='New custom image tab not loading properly')
228 255
diff --git a/bitbake/lib/toaster/tests/functional/test_project_config.py b/bitbake/lib/toaster/tests/functional/test_project_config.py
new file mode 100644
index 0000000000..dbee36aa4e
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/test_project_config.py
@@ -0,0 +1,341 @@
1#! /usr/bin/env python3 #
2# BitBake Toaster UI tests implementation
3#
4# Copyright (C) 2023 Savoir-faire Linux
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import string
10import random
11import pytest
12from django.urls import reverse
13from selenium.webdriver import Keys
14from selenium.webdriver.support.select import Select
15from selenium.common.exceptions import TimeoutException
16from tests.functional.functional_helpers import SeleniumFunctionalTestCase
17from selenium.webdriver.common.by import By
18
19from .utils import get_projectId_from_url
20
21
22@pytest.mark.django_db
23@pytest.mark.order("last")
24class TestProjectConfig(SeleniumFunctionalTestCase):
25 project_id = None
26 PROJECT_NAME = 'TestProjectConfig'
27 INVALID_PATH_START_TEXT = 'The directory path should either start with a /'
28 INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \
29 'any of these characters'
30
31 def _create_project(self, project_name):
32 """ Create/Test new project using:
33 - Project Name: Any string
34 - Release: Any string
35 - Merge Toaster settings: True or False
36 """
37 self.get(reverse('newproject'))
38 self.wait_until_visible('#new-project-name', poll=2)
39 self.find("#new-project-name").send_keys(project_name)
40 select = Select(self.find("#projectversion"))
41 select.select_by_value('3')
42
43 # check merge toaster settings
44 checkbox = self.find('.checkbox-mergeattr')
45 if not checkbox.is_selected():
46 checkbox.click()
47
48 if self.PROJECT_NAME != 'TestProjectConfig':
49 # Reset project name if it's not the default one
50 self.PROJECT_NAME = 'TestProjectConfig'
51
52 self.find("#create-project-button").click()
53
54 try:
55 self.wait_until_visible('#hint-error-project-name', poll=2)
56 url = reverse('project', args=(TestProjectConfig.project_id, ))
57 self.get(url)
58 self.wait_until_visible('#config-nav', poll=3)
59 except TimeoutException:
60 self.wait_until_visible('#config-nav', poll=3)
61
62 def _random_string(self, length):
63 return ''.join(
64 random.choice(string.ascii_letters) for _ in range(length)
65 )
66
67 def _get_config_nav_item(self, index):
68 config_nav = self.find('#config-nav')
69 return config_nav.find_elements(By.TAG_NAME, 'li')[index]
70
71 def _navigate_bbv_page(self):
72 """ Navigate to project BitBake variables page """
73 # check if the menu is displayed
74 if TestProjectConfig.project_id is None:
75 self._create_project(project_name=self._random_string(10))
76 current_url = self.driver.current_url
77 TestProjectConfig.project_id = get_projectId_from_url(current_url)
78 else:
79 url = reverse('projectconf', args=(TestProjectConfig.project_id,))
80 self.get(url)
81 self.wait_until_visible('#config-nav', poll=3)
82 bbv_page_link = self._get_config_nav_item(9)
83 bbv_page_link.click()
84 self.wait_until_visible('#config-nav', poll=3)
85
86 def test_no_underscore_iamgefs_type(self):
87 """
88 Should not accept IMAGEFS_TYPE with an underscore
89 """
90 self._navigate_bbv_page()
91 imagefs_type = "foo_bar"
92
93 self.wait_until_visible('#change-image_fstypes-icon', poll=2)
94
95 self.click('#change-image_fstypes-icon')
96
97 self.enter_text('#new-imagefs_types', imagefs_type)
98
99 element = self.wait_until_visible('#hintError-image-fs_type', poll=2)
100
101 self.assertTrue(("A valid image type cannot include underscores" in element.text),
102 "Did not find underscore error message")
103
104 def test_checkbox_verification(self):
105 """
106 Should automatically check the checkbox if user enters value
107 text box, if value is there in the checkbox.
108 """
109 self._navigate_bbv_page()
110
111 imagefs_type = "btrfs"
112
113 self.wait_until_visible('#change-image_fstypes-icon', poll=2)
114
115 self.click('#change-image_fstypes-icon')
116
117 self.enter_text('#new-imagefs_types', imagefs_type)
118
119 checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']")
120
121 for checkbox in checkboxes:
122 if checkbox.get_attribute("value") == "btrfs":
123 self.assertEqual(checkbox.is_selected(), True)
124
125 def test_textbox_with_checkbox_verification(self):
126 """
127 Should automatically add or remove value in textbox, if user checks
128 or unchecks checkboxes.
129 """
130 self._navigate_bbv_page()
131
132 self.wait_until_visible('#change-image_fstypes-icon', poll=2)
133
134 self.click('#change-image_fstypes-icon')
135
136 checkboxes_selector = '.fs-checkbox-fstypes'
137
138 self.wait_until_visible(checkboxes_selector, poll=2)
139 checkboxes = self.find_all(checkboxes_selector)
140
141 for checkbox in checkboxes:
142 if checkbox.get_attribute("value") == "cpio":
143 checkbox.click()
144 element = self.driver.find_element(By.ID, 'new-imagefs_types')
145
146 self.wait_until_visible('#new-imagefs_types', poll=2)
147
148 self.assertTrue(("cpio" in element.get_attribute('value'),
149 "Imagefs not added into the textbox"))
150 checkbox.click()
151 self.assertTrue(("cpio" not in element.text),
152 "Image still present in the textbox")
153
154 def test_set_download_dir(self):
155 """
156 Validate the allowed and disallowed types in the directory field for
157 DL_DIR
158 """
159 self._navigate_bbv_page()
160
161 # activate the input to edit download dir
162 try:
163 change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2)
164 except TimeoutException:
165 # If download dir is not displayed, test is skipped
166 change_dl_dir_btn = None
167
168 if change_dl_dir_btn:
169 change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2)
170 change_dl_dir_btn.click()
171
172 # downloads dir path doesn't start with / or ${...}
173 input_field = self.wait_until_visible('#new-dl_dir', poll=2)
174 input_field.clear()
175 self.enter_text('#new-dl_dir', 'home/foo')
176 element = self.wait_until_visible('#hintError-initialChar-dl_dir', poll=2)
177
178 msg = 'downloads directory path starts with invalid character but ' \
179 'treated as valid'
180 self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)
181
182 # downloads dir path has a space
183 self.driver.find_element(By.ID, 'new-dl_dir').clear()
184 self.enter_text('#new-dl_dir', '/foo/bar a')
185
186 element = self.wait_until_visible('#hintError-dl_dir', poll=2)
187 msg = 'downloads directory path characters invalid but treated as valid'
188 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
189
190 # downloads dir path starts with ${...} but has a space
191 self.driver.find_element(By.ID,'new-dl_dir').clear()
192 self.enter_text('#new-dl_dir', '${TOPDIR}/down foo')
193
194 element = self.wait_until_visible('#hintError-dl_dir', poll=2)
195 msg = 'downloads directory path characters invalid but treated as valid'
196 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
197
198 # downloads dir path starts with /
199 self.driver.find_element(By.ID,'new-dl_dir').clear()
200 self.enter_text('#new-dl_dir', '/bar/foo')
201
202 hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
203 self.assertEqual(hidden_element.is_displayed(), False,
204 'downloads directory path valid but treated as invalid')
205
206 # downloads dir path starts with ${...}
207 self.driver.find_element(By.ID,'new-dl_dir').clear()
208 self.enter_text('#new-dl_dir', '${TOPDIR}/down')
209
210 hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
211 self.assertEqual(hidden_element.is_displayed(), False,
212 'downloads directory path valid but treated as invalid')
213
214 def test_set_sstate_dir(self):
215 """
216 Validate the allowed and disallowed types in the directory field for
217 SSTATE_DIR
218 """
219 self._navigate_bbv_page()
220
221 try:
222 btn_chg_sstate_dir = self.wait_until_visible(
223 '#change-sstate_dir-icon',
224 poll=2
225 )
226 self.click('#change-sstate_dir-icon')
227 except TimeoutException:
228 # If sstate_dir is not displayed, test is skipped
229 btn_chg_sstate_dir = None
230
231 if btn_chg_sstate_dir: # Skip continuation if sstate_dir is not displayed
232 # path doesn't start with / or ${...}
233 input_field = self.wait_until_visible('#new-sstate_dir', poll=2)
234 input_field.clear()
235 self.enter_text('#new-sstate_dir', 'home/foo')
236 element = self.wait_until_visible('#hintError-initialChar-sstate_dir', poll=2)
237
238 msg = 'sstate directory path starts with invalid character but ' \
239 'treated as valid'
240 self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)
241
242 # path has a space
243 self.driver.find_element(By.ID, 'new-sstate_dir').clear()
244 self.enter_text('#new-sstate_dir', '/foo/bar a')
245
246 element = self.wait_until_visible('#hintError-sstate_dir', poll=2)
247 msg = 'sstate directory path characters invalid but treated as valid'
248 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
249
250 # path starts with ${...} but has a space
251 self.driver.find_element(By.ID,'new-sstate_dir').clear()
252 self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo')
253
254 element = self.wait_until_visible('#hintError-sstate_dir', poll=2)
255 msg = 'sstate directory path characters invalid but treated as valid'
256 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
257
258 # path starts with /
259 self.driver.find_element(By.ID,'new-sstate_dir').clear()
260 self.enter_text('#new-sstate_dir', '/bar/foo')
261
262 hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
263 self.assertEqual(hidden_element.is_displayed(), False,
264 'sstate directory path valid but treated as invalid')
265
266 # paths starts with ${...}
267 self.driver.find_element(By.ID, 'new-sstate_dir').clear()
268 self.enter_text('#new-sstate_dir', '${TOPDIR}/down')
269
270 hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
271 self.assertEqual(hidden_element.is_displayed(), False,
272 'sstate directory path valid but treated as invalid')
273
274 def _change_bbv_value(self, **kwargs):
275 var_name, field, btn_id, input_id, value, save_btn, *_ = kwargs.values()
276 """ Change bitbake variable value """
277 self._navigate_bbv_page()
278 self.wait_until_visible(f'#{btn_id}', poll=2)
279 if kwargs.get('new_variable'):
280 self.find(f"#{btn_id}").clear()
281 self.enter_text(f"#{btn_id}", f"{var_name}")
282 else:
283 self.click(f'#{btn_id}')
284 self.wait_until_visible(f'#{input_id}', poll=2)
285
286 if kwargs.get('is_select'):
287 select = Select(self.find(f'#{input_id}'))
288 select.select_by_visible_text(value)
289 else:
290 self.find(f"#{input_id}").clear()
291 self.enter_text(f'#{input_id}', f'{value}')
292 self.click(f'#{save_btn}')
293 value_displayed = str(self.wait_until_visible(f'#{field}').text).lower()
294 msg = f'{var_name} variable not changed'
295 self.assertTrue(str(value).lower() in value_displayed, msg)
296
297 def test_change_distro_var(self):
298 """ Test changing distro variable """
299 self._change_bbv_value(
300 var_name='DISTRO',
301 field='distro',
302 btn_id='change-distro-icon',
303 input_id='new-distro',
304 value='poky-changed',
305 save_btn="apply-change-distro",
306 )
307
308 def test_set_image_install_append_var(self):
309 """ Test setting IMAGE_INSTALL:append variable """
310 self._change_bbv_value(
311 var_name='IMAGE_INSTALL:append',
312 field='image_install',
313 btn_id='change-image_install-icon',
314 input_id='new-image_install',
315 value='bash, apt, busybox',
316 save_btn="apply-change-image_install",
317 )
318
319 def test_set_package_classes_var(self):
320 """ Test setting PACKAGE_CLASSES variable """
321 self._change_bbv_value(
322 var_name='PACKAGE_CLASSES',
323 field='package_classes',
324 btn_id='change-package_classes-icon',
325 input_id='package_classes-select',
326 value='package_deb',
327 save_btn="apply-change-package_classes",
328 is_select=True,
329 )
330
331 def test_create_new_bbv(self):
332 """ Test creating new bitbake variable """
333 self._change_bbv_value(
334 var_name='New_Custom_Variable',
335 field='configvar-list',
336 btn_id='variable',
337 input_id='value',
338 value='new variable value',
339 save_btn="add-configvar-button",
340 new_variable=True
341 )
diff --git a/bitbake/lib/toaster/tests/functional/test_project_page.py b/bitbake/lib/toaster/tests/functional/test_project_page.py
new file mode 100644
index 0000000000..adbe3587e4
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/test_project_page.py
@@ -0,0 +1,792 @@
1#! /usr/bin/env python3 #
2# BitBake Toaster UI tests implementation
3#
4# Copyright (C) 2023 Savoir-faire Linux
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import os
10import random
11import string
12from unittest import skip
13import pytest
14from django.urls import reverse
15from django.utils import timezone
16from selenium.webdriver.common.keys import Keys
17from selenium.webdriver.support.select import Select
18from selenium.common.exceptions import TimeoutException
19from tests.functional.functional_helpers import SeleniumFunctionalTestCase
20from orm.models import Build, Project, Target
21from selenium.webdriver.common.by import By
22
23from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
24
25
26@pytest.mark.django_db
27@pytest.mark.order("last")
28class TestProjectPage(SeleniumFunctionalTestCase):
29 project_id = None
30 PROJECT_NAME = 'TestProjectPage'
31
32 def _create_project(self, project_name):
33 """ Create/Test new project using:
34 - Project Name: Any string
35 - Release: Any string
36 - Merge Toaster settings: True or False
37 """
38 self.get(reverse('newproject'))
39 self.wait_until_visible('#new-project-name')
40 self.find("#new-project-name").send_keys(project_name)
41 select = Select(self.find("#projectversion"))
42 select.select_by_value('3')
43
44 # check merge toaster settings
45 checkbox = self.find('.checkbox-mergeattr')
46 if not checkbox.is_selected():
47 checkbox.click()
48
49 if self.PROJECT_NAME != 'TestProjectPage':
50 # Reset project name if it's not the default one
51 self.PROJECT_NAME = 'TestProjectPage'
52
53 self.find("#create-project-button").click()
54
55 try:
56 self.wait_until_visible('#hint-error-project-name')
57 url = reverse('project', args=(TestProjectPage.project_id, ))
58 self.get(url)
59 self.wait_until_visible('#config-nav', poll=3)
60 except TimeoutException:
61 self.wait_until_visible('#config-nav', poll=3)
62
63 def _random_string(self, length):
64 return ''.join(
65 random.choice(string.ascii_letters) for _ in range(length)
66 )
67
68 def _navigate_to_project_page(self):
69 # Navigate to project page
70 if TestProjectPage.project_id is None:
71 self._create_project(project_name=self._random_string(10))
72 current_url = self.driver.current_url
73 TestProjectPage.project_id = get_projectId_from_url(current_url)
74 else:
75 url = reverse('project', args=(TestProjectPage.project_id,))
76 self.get(url)
77 self.wait_until_visible('#config-nav')
78
79 def _get_create_builds(self, **kwargs):
80 """ Create a build and return the build object """
81 # parameters for builds to associate with the projects
82 now = timezone.now()
83 self.project1_build_success = {
84 'project': Project.objects.get(id=TestProjectPage.project_id),
85 'started_on': now,
86 'completed_on': now,
87 'outcome': Build.SUCCEEDED
88 }
89
90 self.project1_build_failure = {
91 'project': Project.objects.get(id=TestProjectPage.project_id),
92 'started_on': now,
93 'completed_on': now,
94 'outcome': Build.FAILED
95 }
96 build1 = Build.objects.create(**self.project1_build_success)
97 build2 = Build.objects.create(**self.project1_build_failure)
98
99 # add some targets to these builds so they have recipe links
100 # (and so we can find the row in the ToasterTable corresponding to
101 # a particular build)
102 Target.objects.create(build=build1, target='foo')
103 Target.objects.create(build=build2, target='bar')
104
105 if kwargs:
106 # Create kwargs.get('success') builds with success status with target
107 # and kwargs.get('failure') builds with failure status with target
108 for i in range(kwargs.get('success', 0)):
109 now = timezone.now()
110 self.project1_build_success['started_on'] = now
111 self.project1_build_success[
112 'completed_on'] = now - timezone.timedelta(days=i)
113 build = Build.objects.create(**self.project1_build_success)
114 Target.objects.create(build=build,
115 target=f'{i}_success_recipe',
116 task=f'{i}_success_task')
117
118 for i in range(kwargs.get('failure', 0)):
119 now = timezone.now()
120 self.project1_build_failure['started_on'] = now
121 self.project1_build_failure[
122 'completed_on'] = now - timezone.timedelta(days=i)
123 build = Build.objects.create(**self.project1_build_failure)
124 Target.objects.create(build=build,
125 target=f'{i}_fail_recipe',
126 task=f'{i}_fail_task')
127 return build1, build2
128
129 def _mixin_test_table_edit_column(
130 self,
131 table_id,
132 edit_btn_id,
133 list_check_box_id: list
134 ):
135 # Check edit column
136 edit_column = self.find(f'#{edit_btn_id}')
137 self.assertTrue(edit_column.is_displayed())
138 edit_column.click()
139 # Check dropdown is visible
140 self.wait_until_visible('ul.dropdown-menu.editcol')
141 for check_box_id in list_check_box_id:
142 # Check that we can hide/show table column
143 check_box = self.find(f'#{check_box_id}')
144 th_class = str(check_box_id).replace('checkbox-', '')
145 if check_box.is_selected():
146 # check if column is visible in table
147 self.assertTrue(
148 self.find(
149 f'#{table_id} thead th.{th_class}'
150 ).is_displayed(),
151 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
152 )
153 check_box.click()
154 # check if column is hidden in table
155 self.assertFalse(
156 self.find(
157 f'#{table_id} thead th.{th_class}'
158 ).is_displayed(),
159 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
160 )
161 else:
162 # check if column is hidden in table
163 self.assertFalse(
164 self.find(
165 f'#{table_id} thead th.{th_class}'
166 ).is_displayed(),
167 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
168 )
169 check_box.click()
170 # check if column is visible in table
171 self.assertTrue(
172 self.find(
173 f'#{table_id} thead th.{th_class}'
174 ).is_displayed(),
175 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
176 )
177
178 def _get_config_nav_item(self, index):
179 config_nav = self.find('#config-nav')
180 return config_nav.find_elements(By.TAG_NAME, 'li')[index]
181
182 def _navigate_to_config_nav(self, nav_id, nav_index):
183 # navigate to the project page
184 self._navigate_to_project_page()
185 # click on "Software recipe" tab
186 soft_recipe = self._get_config_nav_item(nav_index)
187 soft_recipe.click()
188 self.wait_until_visible(f'#{nav_id}')
189
190 def _mixin_test_table_show_rows(self, table_selector, **kwargs):
191 """ Test the show rows feature in the builds table on the all builds page """
192 def test_show_rows(row_to_show, show_row_link):
193 # Check that we can show rows == row_to_show
194 show_row_link.select_by_value(str(row_to_show))
195 self.wait_until_visible(f'#{table_selector} tbody tr', poll=3)
196 # check at least some rows are visible
197 self.assertTrue(
198 len(self.find_all(f'#{table_selector} tbody tr')) > 0
199 )
200 self.wait_until_present(f'#{table_selector} tbody tr')
201 show_rows = self.driver.find_elements(
202 By.XPATH,
203 f'//select[@class="form-control pagesize-{table_selector}"]'
204 )
205 rows_to_show = [10, 25, 50, 100, 150]
206 to_skip = kwargs.get('to_skip', [])
207 # Check show rows
208 for show_row_link in show_rows:
209 show_row_link = Select(show_row_link)
210 for row_to_show in rows_to_show:
211 if row_to_show not in to_skip:
212 test_show_rows(row_to_show, show_row_link)
213
214 def _mixin_test_table_search_input(self, **kwargs):
215 input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values()
216 # Test search input
217 self.wait_until_visible(f'#{input_selector}')
218 recipe_input = self.find(f'#{input_selector}')
219 recipe_input.send_keys(input_text)
220 self.find(f'#{searchBtn_selector}').click()
221 self.wait_until_visible(f'#{table_selector} tbody tr')
222 rows = self.find_all(f'#{table_selector} tbody tr')
223 self.assertTrue(len(rows) > 0)
224
225 def test_create_project(self):
226 """ Create/Test new project using:
227 - Project Name: Any string
228 - Release: Any string
229 - Merge Toaster settings: True or False
230 """
231 self._create_project(project_name=self.PROJECT_NAME)
232
233 def test_image_recipe_editColumn(self):
234 """ Test the edit column feature in image recipe table on project page """
235 self._get_create_builds(success=10, failure=10)
236
237 url = reverse('projectimagerecipes', args=(TestProjectPage.project_id,))
238 self.get(url)
239 self.wait_until_present('#imagerecipestable tbody tr')
240
241 column_list = [
242 'get_description_or_summary', 'layer_version__get_vcs_reference',
243 'layer_version__layer__name', 'license', 'recipe-file', 'section',
244 'version'
245 ]
246
247 # Check that we can hide the edit column
248 self._mixin_test_table_edit_column(
249 'imagerecipestable',
250 'edit-columns-button',
251 [f'checkbox-{column}' for column in column_list]
252 )
253
254 def test_page_header_on_project_page(self):
255 """ Check page header in project page:
256 - AT LEFT -> Logo of Yocto project, displayed, clickable
257 - "Toaster"+" Information icon", displayed, clickable
258 - "Server Icon" + "All builds", displayed, clickable
259 - "Directory Icon" + "All projects", displayed, clickable
260 - "Book Icon" + "Documentation", displayed, clickable
261 - AT RIGHT -> button "New project", displayed, clickable
262 """
263 # navigate to the project page
264 self._navigate_to_project_page()
265
266 # check page header
267 # AT LEFT -> Logo of Yocto project
268 logo = self.driver.find_element(
269 By.XPATH,
270 "//div[@class='toaster-navbar-brand']",
271 )
272 logo_img = logo.find_element(By.TAG_NAME, 'img')
273 self.assertTrue(logo_img.is_displayed(),
274 'Logo of Yocto project not found')
275 self.assertTrue(
276 '/static/img/logo.png' in str(logo_img.get_attribute('src')),
277 'Logo of Yocto project not found'
278 )
279 # "Toaster"+" Information icon", clickable
280 toaster = self.driver.find_element(
281 By.XPATH,
282 "//div[@class='toaster-navbar-brand']//a[@class='brand']",
283 )
284 self.assertTrue(toaster.is_displayed(), 'Toaster not found')
285 self.assertTrue(toaster.text == 'Toaster')
286 info_sign = self.find('.glyphicon-info-sign')
287 self.assertTrue(info_sign.is_displayed())
288
289 # "Server Icon" + "All builds"
290 all_builds = self.find('#navbar-all-builds')
291 all_builds_link = all_builds.find_element(By.TAG_NAME, 'a')
292 self.assertTrue("All builds" in all_builds_link.text)
293 self.assertTrue(
294 '/toastergui/builds/' in str(all_builds_link.get_attribute('href'))
295 )
296 server_icon = all_builds.find_element(By.TAG_NAME, 'i')
297 self.assertTrue(
298 server_icon.get_attribute('class') == 'glyphicon glyphicon-tasks'
299 )
300 self.assertTrue(server_icon.is_displayed())
301
302 # "Directory Icon" + "All projects"
303 all_projects = self.find('#navbar-all-projects')
304 all_projects_link = all_projects.find_element(By.TAG_NAME, 'a')
305 self.assertTrue("All projects" in all_projects_link.text)
306 self.assertTrue(
307 '/toastergui/projects/' in str(all_projects_link.get_attribute(
308 'href'))
309 )
310 dir_icon = all_projects.find_element(By.TAG_NAME, 'i')
311 self.assertTrue(
312 dir_icon.get_attribute('class') == 'icon-folder-open'
313 )
314 self.assertTrue(dir_icon.is_displayed())
315
316 # "Book Icon" + "Documentation"
317 toaster_docs_link = self.find('#navbar-docs')
318 toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME,
319 'a')
320 self.assertTrue("Documentation" in toaster_docs_link_link.text)
321 self.assertTrue(
322 toaster_docs_link_link.get_attribute('href') == 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual'
323 )
324 book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i')
325 self.assertTrue(
326 book_icon.get_attribute('class') == 'glyphicon glyphicon-book'
327 )
328 self.assertTrue(book_icon.is_displayed())
329
330 # AT RIGHT -> button "New project"
331 new_project_button = self.find('#new-project-button')
332 self.assertTrue(new_project_button.is_displayed())
333 self.assertTrue(new_project_button.text == 'New project')
334 new_project_button.click()
335 self.assertTrue(
336 '/toastergui/newproject/' in str(self.driver.current_url)
337 )
338
339 def test_edit_project_name(self):
340 """ Test edit project name:
341 - Click on "Edit" icon button
342 - Change project name
343 - Click on "Save" button
344 - Check project name is changed
345 """
346 # navigate to the project page
347 self._navigate_to_project_page()
348
349 # click on "Edit" icon button
350 self.wait_until_visible('#project-name-container')
351 edit_button = self.find('#project-change-form-toggle')
352 edit_button.click()
353 project_name_input = self.find('#project-name-change-input')
354 self.assertTrue(project_name_input.is_displayed())
355 project_name_input.clear()
356 project_name_input.send_keys('New Name')
357 self.find('#project-name-change-btn').click()
358
359 # check project name is changed
360 self.wait_until_visible('#project-name-container')
361 self.assertTrue(
362 'New Name' in str(self.find('#project-name-container').text)
363 )
364
365 def test_project_page_tabs(self):
366 """ Test project tabs:
367 - "configuration" tab
368 - "Builds" tab
369 - "Import layers" tab
370 - "New custom image" tab
371 Check search box used to build recipes
372 """
373 # navigate to the project page
374 self._navigate_to_project_page()
375
376 # check "configuration" tab
377 self.wait_until_visible('#topbar-configuration-tab')
378 config_tab = self.find('#topbar-configuration-tab')
379 self.assertTrue(config_tab.get_attribute('class') == 'active')
380 self.assertTrue('Configuration' in str(config_tab.text))
381 self.assertTrue(
382 f"/toastergui/project/{TestProjectPage.project_id}" in str(self.driver.current_url)
383 )
384
385 def get_tabs():
386 # tabs links list
387 return self.driver.find_elements(
388 By.XPATH,
389 '//div[@id="project-topbar"]//li'
390 )
391
392 def check_tab_link(tab_index, tab_name, url):
393 tab = get_tabs()[tab_index]
394 tab_link = tab.find_element(By.TAG_NAME, 'a')
395 self.assertTrue(url in tab_link.get_attribute('href'))
396 self.assertTrue(tab_name in tab_link.text)
397 self.assertTrue(tab.get_attribute('class') == 'active')
398
399 # check "Builds" tab
400 builds_tab = get_tabs()[1]
401 builds_tab.find_element(By.TAG_NAME, 'a').click()
402 check_tab_link(
403 1,
404 'Builds',
405 f"/toastergui/project/{TestProjectPage.project_id}/builds"
406 )
407
408 # check "Import layers" tab
409 import_layers_tab = get_tabs()[2]
410 import_layers_tab.find_element(By.TAG_NAME, 'a').click()
411 check_tab_link(
412 2,
413 'Import layer',
414 f"/toastergui/project/{TestProjectPage.project_id}/importlayer"
415 )
416
417 # check "New custom image" tab
418 new_custom_image_tab = get_tabs()[3]
419 new_custom_image_tab.find_element(By.TAG_NAME, 'a').click()
420 check_tab_link(
421 3,
422 'New custom image',
423 f"/toastergui/project/{TestProjectPage.project_id}/newcustomimage"
424 )
425
426 # check search box can be use to build recipes
427 search_box = self.find('#build-input')
428 search_box.send_keys('core-image-minimal')
429 self.find('#build-button').click()
430 self.wait_until_visible('#latest-builds')
431 lastest_builds = self.driver.find_elements(
432 By.XPATH,
433 '//div[@id="latest-builds"]',
434 )
435 last_build = lastest_builds[0]
436 self.assertTrue(
437 'core-image-minimal' in str(last_build.text)
438 )
439
440 def test_softwareRecipe_page(self):
441 """ Test software recipe page
442 - Check title "Compatible software recipes" is displayed
443 - Check search input
444 - Check "build recipe" button works
445 - Check software recipe table feature(show/hide column, pagination)
446 """
447 self._navigate_to_config_nav('softwarerecipestable', 4)
448 # check title "Compatible software recipes" is displayed
449 self.assertTrue("Compatible software recipes" in self.get_page_source())
450 # Test search input
451 self._mixin_test_table_search_input(
452 input_selector='search-input-softwarerecipestable',
453 input_text='busybox',
454 searchBtn_selector='search-submit-softwarerecipestable',
455 table_selector='softwarerecipestable'
456 )
457 # check "build recipe" button works
458 rows = self.find_all('#softwarerecipestable tbody tr')
459 image_to_build = rows[0]
460 build_btn = image_to_build.find_element(
461 By.XPATH,
462 '//td[@class="add-del-layers"]//a[1]'
463 )
464 build_btn.click()
465 build_state = wait_until_build(self, 'queued cloning starting parsing failed')
466 lastest_builds = self.driver.find_elements(
467 By.XPATH,
468 '//div[@id="latest-builds"]/div'
469 )
470 self.assertTrue(len(lastest_builds) > 0)
471 last_build = lastest_builds[0]
472 cancel_button = last_build.find_element(
473 By.XPATH,
474 '//span[@class="cancel-build-btn pull-right alert-link"]',
475 )
476 cancel_button.click()
477 if 'starting' not in build_state: # change build state when cancelled in starting state
478 wait_until_build_cancelled(self)
479
480 # check software recipe table feature(show/hide column, pagination)
481 self._navigate_to_config_nav('softwarerecipestable', 4)
482 column_list = [
483 'get_description_or_summary',
484 'layer_version__get_vcs_reference',
485 'layer_version__layer__name',
486 'license',
487 'recipe-file',
488 'section',
489 'version',
490 ]
491 self._mixin_test_table_edit_column(
492 'softwarerecipestable',
493 'edit-columns-button',
494 [f'checkbox-{column}' for column in column_list]
495 )
496 self._navigate_to_config_nav('softwarerecipestable', 4)
497 # check show rows(pagination)
498 self._mixin_test_table_show_rows(
499 table_selector='softwarerecipestable',
500 to_skip=[150],
501 )
502
503 def test_machines_page(self):
504 """ Test Machine page
505 - Check if title "Compatible machines" is displayed
506 - Check search input
507 - Check "Select machine" button works
508 - Check "Add layer" button works
509 - Check Machine table feature(show/hide column, pagination)
510 """
511 self._navigate_to_config_nav('machinestable', 5)
512 # check title "Compatible software recipes" is displayed
513 self.assertTrue("Compatible machines" in self.get_page_source())
514 # Test search input
515 self._mixin_test_table_search_input(
516 input_selector='search-input-machinestable',
517 input_text='qemux86-64',
518 searchBtn_selector='search-submit-machinestable',
519 table_selector='machinestable'
520 )
521 # check "Select machine" button works
522 rows = self.find_all('#machinestable tbody tr')
523 machine_to_select = rows[0]
524 select_btn = machine_to_select.find_element(
525 By.XPATH,
526 '//td[@class="add-del-layers"]//a[1]'
527 )
528 select_btn.send_keys(Keys.RETURN)
529 self.wait_until_visible('#config-nav')
530 project_machine_name = self.find('#project-machine-name')
531 self.assertTrue(
532 'qemux86-64' in project_machine_name.text
533 )
534 # check "Add layer" button works
535 self._navigate_to_config_nav('machinestable', 5)
536 # Search for a machine whit layer not in project
537 self._mixin_test_table_search_input(
538 input_selector='search-input-machinestable',
539 input_text='qemux86-64-tpm2',
540 searchBtn_selector='search-submit-machinestable',
541 table_selector='machinestable'
542 )
543 self.wait_until_visible('#machinestable tbody tr', poll=3)
544 rows = self.find_all('#machinestable tbody tr')
545 machine_to_add = rows[0]
546 add_btn = machine_to_add.find_element(By.XPATH, '//td[@class="add-del-layers"]')
547 add_btn.click()
548 self.wait_until_visible('#change-notification')
549 change_notification = self.find('#change-notification')
550 self.assertTrue(
551 f'You have added 1 layer to your project' in str(change_notification.text)
552 )
553 # check Machine table feature(show/hide column, pagination)
554 self._navigate_to_config_nav('machinestable', 5)
555 column_list = [
556 'description',
557 'layer_version__get_vcs_reference',
558 'layer_version__layer__name',
559 'machinefile',
560 ]
561 self._mixin_test_table_edit_column(
562 'machinestable',
563 'edit-columns-button',
564 [f'checkbox-{column}' for column in column_list]
565 )
566 self._navigate_to_config_nav('machinestable', 5)
567 # check show rows(pagination)
568 self._mixin_test_table_show_rows(
569 table_selector='machinestable',
570 to_skip=[150],
571 )
572
573 def test_layers_page(self):
574 """ Test layers page
575 - Check if title "Compatible layerss" is displayed
576 - Check search input
577 - Check "Add layer" button works
578 - Check "Remove layer" button works
579 - Check layers table feature(show/hide column, pagination)
580 """
581 self._navigate_to_config_nav('layerstable', 6)
582 # check title "Compatible layers" is displayed
583 self.assertTrue("Compatible layers" in self.get_page_source())
584 # Test search input
585 input_text='meta-tanowrt'
586 self._mixin_test_table_search_input(
587 input_selector='search-input-layerstable',
588 input_text=input_text,
589 searchBtn_selector='search-submit-layerstable',
590 table_selector='layerstable'
591 )
592 # check "Add layer" button works
593 self.wait_until_visible('#layerstable tbody tr', poll=3)
594 rows = self.find_all('#layerstable tbody tr')
595 layer_to_add = rows[0]
596 add_btn = layer_to_add.find_element(
597 By.XPATH,
598 '//td[@class="add-del-layers"]'
599 )
600 add_btn.click()
601 # check modal is displayed
602 self.wait_until_visible('#dependencies-modal', poll=3)
603 list_dependencies = self.find_all('#dependencies-list li')
604 # click on add-layers button
605 add_layers_btn = self.driver.find_element(
606 By.XPATH,
607 '//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]'
608 )
609 add_layers_btn.click()
610 self.wait_until_visible('#change-notification')
611 change_notification = self.find('#change-notification')
612 self.assertTrue(
613 f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies' in str(change_notification.text)
614 )
615 # check "Remove layer" button works
616 self.wait_until_visible('#layerstable tbody tr', poll=3)
617 rows = self.find_all('#layerstable tbody tr')
618 layer_to_remove = rows[0]
619 remove_btn = layer_to_remove.find_element(
620 By.XPATH,
621 '//td[@class="add-del-layers"]'
622 )
623 remove_btn.click()
624 self.wait_until_visible('#change-notification', poll=2)
625 change_notification = self.find('#change-notification')
626 self.assertTrue(
627 f'You have removed 1 layer from your project: {input_text}' in str(change_notification.text)
628 )
629 # check layers table feature(show/hide column, pagination)
630 self._navigate_to_config_nav('layerstable', 6)
631 column_list = [
632 'dependencies',
633 'revision',
634 'layer__vcs_url',
635 'git_subdir',
636 'layer__summary',
637 ]
638 self._mixin_test_table_edit_column(
639 'layerstable',
640 'edit-columns-button',
641 [f'checkbox-{column}' for column in column_list]
642 )
643 self._navigate_to_config_nav('layerstable', 6)
644 # check show rows(pagination)
645 self._mixin_test_table_show_rows(
646 table_selector='layerstable',
647 to_skip=[150],
648 )
649
650 def test_distro_page(self):
651 """ Test distros page
652 - Check if title "Compatible distros" is displayed
653 - Check search input
654 - Check "Add layer" button works
655 - Check distro table feature(show/hide column, pagination)
656 """
657 self._navigate_to_config_nav('distrostable', 7)
658 # check title "Compatible distros" is displayed
659 self.assertTrue("Compatible Distros" in self.get_page_source())
660 # Test search input
661 input_text='poky-altcfg'
662 self._mixin_test_table_search_input(
663 input_selector='search-input-distrostable',
664 input_text=input_text,
665 searchBtn_selector='search-submit-distrostable',
666 table_selector='distrostable'
667 )
668 # check "Add distro" button works
669 rows = self.find_all('#distrostable tbody tr')
670 distro_to_add = rows[0]
671 add_btn = distro_to_add.find_element(
672 By.XPATH,
673 '//td[@class="add-del-layers"]//a[1]'
674 )
675 add_btn.click()
676 self.wait_until_visible('#change-notification', poll=2)
677 change_notification = self.find('#change-notification')
678 self.assertTrue(
679 f'You have changed the distro to: {input_text}' in str(change_notification.text)
680 )
681 # check distro table feature(show/hide column, pagination)
682 self._navigate_to_config_nav('distrostable', 7)
683 column_list = [
684 'description',
685 'templatefile',
686 'layer_version__get_vcs_reference',
687 'layer_version__layer__name',
688 ]
689 self._mixin_test_table_edit_column(
690 'distrostable',
691 'edit-columns-button',
692 [f'checkbox-{column}' for column in column_list]
693 )
694 self._navigate_to_config_nav('distrostable', 7)
695 # check show rows(pagination)
696 self._mixin_test_table_show_rows(
697 table_selector='distrostable',
698 to_skip=[150],
699 )
700
701 def test_single_layer_page(self):
702 """ Test layer page
703 - Check if title is displayed
704 - Check add/remove layer button works
705 - Check tabs(layers, recipes, machines) are displayed
706 - Check left section is displayed
707 - Check layer name
708 - Check layer summary
709 - Check layer description
710 """
711 url = reverse("layerdetails", args=(TestProjectPage.project_id, 8))
712 self.get(url)
713 self.wait_until_visible('.page-header')
714 # check title is displayed
715 self.assertTrue(self.find('.page-header h1').is_displayed())
716
717 # check add layer button works
718 remove_layer_btn = self.find('#add-remove-layer-btn')
719 remove_layer_btn.click()
720 self.wait_until_visible('#change-notification', poll=2)
721 change_notification = self.find('#change-notification')
722 self.assertTrue(
723 f'You have removed 1 layer from your project' in str(change_notification.text)
724 )
725 # check add layer button works, 18 is the random layer id
726 add_layer_btn = self.find('#add-remove-layer-btn')
727 add_layer_btn.click()
728 self.wait_until_visible('#change-notification')
729 change_notification = self.find('#change-notification')
730 self.assertTrue(
731 f'You have added 1 layer to your project' in str(change_notification.text)
732 )
733 # check tabs(layers, recipes, machines) are displayed
734 tabs = self.find_all('.nav-tabs li')
735 self.assertEqual(len(tabs), 3)
736 # Check first tab
737 tabs[0].click()
738 self.assertTrue(
739 'active' in str(self.find('#information').get_attribute('class'))
740 )
741 # Check second tab
742 tabs[1].click()
743 self.assertTrue(
744 'active' in str(self.find('#recipes').get_attribute('class'))
745 )
746 # Check third tab
747 tabs[2].click()
748 self.assertTrue(
749 'active' in str(self.find('#machines').get_attribute('class'))
750 )
751 # Check left section is displayed
752 section = self.find('.well')
753 # Check layer name
754 self.assertTrue(
755 section.find_element(By.XPATH, '//h2[1]').is_displayed()
756 )
757 # Check layer summary
758 self.assertTrue("Summary" in section.text)
759 # Check layer description
760 self.assertTrue("Description" in section.text)
761
762 def test_single_recipe_page(self):
763 """ Test recipe page
764 - Check if title is displayed
765 - Check add recipe layer displayed
766 - Check left section is displayed
767 - Check recipe: name, summary, description, Version, Section,
768 License, Approx. packages included, Approx. size, Recipe file
769 """
770 url = reverse("recipedetails", args=(TestProjectPage.project_id, 53428))
771 self.get(url)
772 self.wait_until_visible('.page-header')
773 # check title is displayed
774 self.assertTrue(self.find('.page-header h1').is_displayed())
775 # check add recipe layer displayed
776 add_recipe_layer_btn = self.find('#add-layer-btn')
777 self.assertTrue(add_recipe_layer_btn.is_displayed())
778 # check left section is displayed
779 section = self.find('.well')
780 # Check recipe name
781 self.assertTrue(
782 section.find_element(By.XPATH, '//h2[1]').is_displayed()
783 )
784 # Check recipe sections details info are displayed
785 self.assertTrue("Summary" in section.text)
786 self.assertTrue("Description" in section.text)
787 self.assertTrue("Version" in section.text)
788 self.assertTrue("Section" in section.text)
789 self.assertTrue("License" in section.text)
790 self.assertTrue("Approx. packages included" in section.text)
791 self.assertTrue("Approx. package size" in section.text)
792 self.assertTrue("Recipe file" in section.text)
diff --git a/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py b/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py
new file mode 100644
index 0000000000..eb905ddf3f
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py
@@ -0,0 +1,528 @@
1#! /usr/bin/env python3 #
2# BitBake Toaster UI tests implementation
3#
4# Copyright (C) 2023 Savoir-faire Linux
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import string
10import random
11import pytest
12from django.urls import reverse
13from selenium.webdriver import Keys
14from selenium.webdriver.support.select import Select
15from selenium.common.exceptions import ElementClickInterceptedException, NoSuchElementException, TimeoutException
16from orm.models import Project
17from tests.functional.functional_helpers import SeleniumFunctionalTestCase
18from selenium.webdriver.common.by import By
19
20from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
21
22
23@pytest.mark.django_db
24@pytest.mark.order("last")
25class TestProjectConfigTab(SeleniumFunctionalTestCase):
26 PROJECT_NAME = 'TestProjectConfigTab'
27 project_id = None
28
29 def _create_project(self, project_name, **kwargs):
30 """ Create/Test new project using:
31 - Project Name: Any string
32 - Release: Any string
33 - Merge Toaster settings: True or False
34 """
35 release = kwargs.get('release', '3')
36 self.get(reverse('newproject'))
37 self.wait_until_visible('#new-project-name')
38 self.find("#new-project-name").send_keys(project_name)
39 select = Select(self.find("#projectversion"))
40 select.select_by_value(release)
41
42 # check merge toaster settings
43 checkbox = self.find('.checkbox-mergeattr')
44 if not checkbox.is_selected():
45 checkbox.click()
46
47 if self.PROJECT_NAME != 'TestProjectConfigTab':
48 # Reset project name if it's not the default one
49 self.PROJECT_NAME = 'TestProjectConfigTab'
50
51 self.find("#create-project-button").click()
52
53 try:
54 self.wait_until_visible('#hint-error-project-name', poll=3)
55 url = reverse('project', args=(TestProjectConfigTab.project_id, ))
56 self.get(url)
57 self.wait_until_visible('#config-nav', poll=3)
58 except TimeoutException:
59 self.wait_until_visible('#config-nav', poll=3)
60
61 def _random_string(self, length):
62 return ''.join(
63 random.choice(string.ascii_letters) for _ in range(length)
64 )
65
66 def _navigate_to_project_page(self):
67 # Navigate to project page
68 if TestProjectConfigTab.project_id is None:
69 self._create_project(project_name=self._random_string(10))
70 current_url = self.driver.current_url
71 TestProjectConfigTab.project_id = get_projectId_from_url(
72 current_url)
73 else:
74 url = reverse('project', args=(TestProjectConfigTab.project_id,))
75 self.get(url)
76 self.wait_until_visible('#config-nav')
77
78 def _create_builds(self):
79 # check search box can be use to build recipes
80 search_box = self.find('#build-input')
81 search_box.send_keys('foo')
82 self.find('#build-button').click()
83 self.wait_until_present('#latest-builds')
84 # loop until reach the parsing state
85 wait_until_build(self, 'queued cloning starting parsing failed')
86 lastest_builds = self.driver.find_elements(
87 By.XPATH,
88 '//div[@id="latest-builds"]/div',
89 )
90 last_build = lastest_builds[0]
91 self.assertTrue(
92 'foo' in str(last_build.text)
93 )
94 last_build = lastest_builds[0]
95 try:
96 cancel_button = last_build.find_element(
97 By.XPATH,
98 '//span[@class="cancel-build-btn pull-right alert-link"]',
99 )
100 cancel_button.click()
101 except NoSuchElementException:
102 # Skip if the build is already cancelled
103 pass
104 wait_until_build_cancelled(self)
105
106 def _get_tabs(self):
107 # tabs links list
108 return self.driver.find_elements(
109 By.XPATH,
110 '//div[@id="project-topbar"]//li'
111 )
112
113 def _get_config_nav_item(self, index):
114 config_nav = self.find('#config-nav')
115 return config_nav.find_elements(By.TAG_NAME, 'li')[index]
116
117 def test_project_config_nav(self):
118 """ Test project config tab navigation:
119 - Check if the menu is displayed and contains the right elements:
120 - Configuration
121 - COMPATIBLE METADATA
122 - Custom images
123 - Image recipes
124 - Software recipes
125 - Machines
126 - Layers
127 - Distro
128 - EXTRA CONFIGURATION
129 - Bitbake variables
130 - Actions
131 - Delete project
132 """
133 self._navigate_to_project_page()
134
135 def _get_config_nav_item(index):
136 config_nav = self.find('#config-nav')
137 return config_nav.find_elements(By.TAG_NAME, 'li')[index]
138
139 def check_config_nav_item(index, item_name, url):
140 item = _get_config_nav_item(index)
141 self.assertTrue(item_name in item.text)
142 self.assertTrue(item.get_attribute('class') == 'active')
143 self.assertTrue(url in self.driver.current_url)
144
145 # check if the menu contains the right elements
146 # COMPATIBLE METADATA
147 compatible_metadata = _get_config_nav_item(1)
148 self.assertTrue(
149 "compatible metadata" in compatible_metadata.text.lower()
150 )
151 # EXTRA CONFIGURATION
152 extra_configuration = _get_config_nav_item(8)
153 self.assertTrue(
154 "extra configuration" in extra_configuration.text.lower()
155 )
156 # Actions
157 actions = _get_config_nav_item(10)
158 self.assertTrue("actions" in str(actions.text).lower())
159
160 conf_nav_list = [
161 # config
162 [0, 'Configuration',
163 f"/toastergui/project/{TestProjectConfigTab.project_id}"],
164 # custom images
165 [2, 'Custom images',
166 f"/toastergui/project/{TestProjectConfigTab.project_id}/customimages"],
167 # image recipes
168 [3, 'Image recipes',
169 f"/toastergui/project/{TestProjectConfigTab.project_id}/images"],
170 # software recipes
171 [4, 'Software recipes',
172 f"/toastergui/project/{TestProjectConfigTab.project_id}/softwarerecipes"],
173 # machines
174 [5, 'Machines',
175 f"/toastergui/project/{TestProjectConfigTab.project_id}/machines"],
176 # layers
177 [6, 'Layers',
178 f"/toastergui/project/{TestProjectConfigTab.project_id}/layers"],
179 # distro
180 [7, 'Distros',
181 f"/toastergui/project/{TestProjectConfigTab.project_id}/distros"],
182 # [9, 'BitBake variables', f"/toastergui/project/{TestProjectConfigTab.project_id}/configuration"], # bitbake variables
183 ]
184 for index, item_name, url in conf_nav_list:
185 item = _get_config_nav_item(index)
186 if item.get_attribute('class') != 'active':
187 item.click()
188 check_config_nav_item(index, item_name, url)
189
190 def test_image_recipe_editColumn(self):
191 """ Test the edit column feature in image recipe table on project page """
192 def test_edit_column(check_box_id):
193 # Check that we can hide/show table column
194 check_box = self.find(f'#{check_box_id}')
195 th_class = str(check_box_id).replace('checkbox-', '')
196 if check_box.is_selected():
197 # check if column is visible in table
198 self.assertTrue(
199 self.find(
200 f'#imagerecipestable thead th.{th_class}'
201 ).is_displayed(),
202 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
203 )
204 check_box.click()
205 # check if column is hidden in table
206 self.assertFalse(
207 self.find(
208 f'#imagerecipestable thead th.{th_class}'
209 ).is_displayed(),
210 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
211 )
212 else:
213 # check if column is hidden in table
214 self.assertFalse(
215 self.find(
216 f'#imagerecipestable thead th.{th_class}'
217 ).is_displayed(),
218 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
219 )
220 check_box.click()
221 # check if column is visible in table
222 self.assertTrue(
223 self.find(
224 f'#imagerecipestable thead th.{th_class}'
225 ).is_displayed(),
226 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
227 )
228
229 self._navigate_to_project_page()
230 # navigate to project image recipe page
231 recipe_image_page_link = self._get_config_nav_item(3)
232 recipe_image_page_link.click()
233 self.wait_until_present('#imagerecipestable tbody tr')
234
235 # Check edit column
236 edit_column = self.find('#edit-columns-button')
237 self.assertTrue(edit_column.is_displayed())
238 edit_column.click()
239 # Check dropdown is visible
240 self.wait_until_visible('ul.dropdown-menu.editcol')
241
242 # Check that we can hide the edit column
243 test_edit_column('checkbox-get_description_or_summary')
244 test_edit_column('checkbox-layer_version__get_vcs_reference')
245 test_edit_column('checkbox-layer_version__layer__name')
246 test_edit_column('checkbox-license')
247 test_edit_column('checkbox-recipe-file')
248 test_edit_column('checkbox-section')
249 test_edit_column('checkbox-version')
250
251 def test_image_recipe_show_rows(self):
252 """ Test the show rows feature in image recipe table on project page """
253 def test_show_rows(row_to_show, show_row_link):
254 # Check that we can show rows == row_to_show
255 show_row_link.select_by_value(str(row_to_show))
256 self.wait_until_visible('#imagerecipestable tbody tr', poll=3)
257 # check at least some rows are visible
258 self.assertTrue(
259 len(self.find_all('#imagerecipestable tbody tr')) > 0
260 )
261
262 self._navigate_to_project_page()
263 # navigate to project image recipe page
264 recipe_image_page_link = self._get_config_nav_item(3)
265 recipe_image_page_link.click()
266 self.wait_until_present('#imagerecipestable tbody tr')
267
268 show_rows = self.driver.find_elements(
269 By.XPATH,
270 '//select[@class="form-control pagesize-imagerecipestable"]'
271 )
272 # Check show rows
273 for show_row_link in show_rows:
274 show_row_link = Select(show_row_link)
275 test_show_rows(10, show_row_link)
276 test_show_rows(25, show_row_link)
277 test_show_rows(50, show_row_link)
278 test_show_rows(100, show_row_link)
279 test_show_rows(150, show_row_link)
280
281 def test_project_config_tab_right_section(self):
282 """ Test project config tab right section contains five blocks:
283 - Machine:
284 - check 'Machine' is displayed
285 - check can change Machine
286 - Distro:
287 - check 'Distro' is displayed
288 - check can change Distro
289 - Most built recipes:
290 - check 'Most built recipes' is displayed
291 - check can select a recipe and build it
292 - Project release:
293 - check 'Project release' is displayed
294 - check project has right release displayed
295 - Layers:
296 - check can add a layer if exists
297 - check at least three layers are displayed
298 - openembedded-core
299 - meta-poky
300 - meta-yocto-bsp
301 """
302 # Create a new project for this test
303 project_name = self._random_string(10)
304 self._create_project(project_name=project_name)
305 # check if the menu is displayed
306 self.wait_until_visible('#project-page')
307 block_l = self.driver.find_element(
308 By.XPATH, '//*[@id="project-page"]/div[2]')
309 project_release = self.driver.find_element(
310 By.XPATH, '//*[@id="project-page"]/div[1]/div[4]')
311 layers = block_l.find_element(By.ID, 'layer-container')
312
313 def check_machine_distro(self, item_name, new_item_name, block_id):
314 block = self.find(f'#{block_id}')
315 title = block.find_element(By.TAG_NAME, 'h3')
316 self.assertTrue(item_name.capitalize() in title.text)
317 edit_btn = self.find(f'#change-{item_name}-toggle')
318 edit_btn.click()
319 self.wait_until_visible(f'#{item_name}-change-input')
320 name_input = self.find(f'#{item_name}-change-input')
321 name_input.clear()
322 name_input.send_keys(new_item_name)
323 change_btn = self.find(f'#{item_name}-change-btn')
324 change_btn.click()
325 self.wait_until_visible(f'#project-{item_name}-name')
326 project_name = self.find(f'#project-{item_name}-name')
327 self.assertTrue(new_item_name in project_name.text)
328 # check change notificaiton is displayed
329 change_notification = self.find('#change-notification')
330 self.assertTrue(
331 f'You have changed the {item_name} to: {new_item_name}' in change_notification.text
332 )
333
334 # Machine
335 check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section')
336 # Distro
337 check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section')
338
339 # Project release
340 title = project_release.find_element(By.TAG_NAME, 'h3')
341 self.assertTrue("Project release" in title.text)
342 self.assertTrue(
343 "Yocto Project master" in self.find('#project-release-title').text
344 )
345 # Layers
346 title = layers.find_element(By.TAG_NAME, 'h3')
347 self.assertTrue("Layers" in title.text)
348 # check at least three layers are displayed
349 # openembedded-core
350 # meta-poky
351 # meta-yocto-bsp
352 layers_list = layers.find_element(By.ID, 'layers-in-project-list')
353 layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
354 # remove all layers except the first three layers
355 for i in range(3, len(layers_list_items)):
356 layers_list_items[i].find_element(By.TAG_NAME, 'span').click()
357 # check can add a layer if exists
358 add_layer_input = layers.find_element(By.ID, 'layer-add-input')
359 add_layer_input.send_keys('meta-oe')
360 self.wait_until_visible('#layer-container > form > div > span > div')
361 dropdown_item = self.driver.find_element(
362 By.XPATH,
363 '//*[@id="layer-container"]/form/div/span/div'
364 )
365 try:
366 dropdown_item.click()
367 except ElementClickInterceptedException:
368 self.skipTest(
369 "layer-container dropdown item click intercepted. Element not properly visible.")
370 add_layer_btn = layers.find_element(By.ID, 'add-layer-btn')
371 add_layer_btn.click()
372 self.wait_until_visible('#layers-in-project-list')
373 # check layer is added
374 layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
375 self.assertTrue(len(layers_list_items) == 4)
376
377 def test_most_build_recipes(self):
378 """ Test most build recipes block contains"""
379 def rebuild_from_most_build_recipes(recipe_list_items):
380 checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input')
381 checkbox.click()
382 build_btn = self.find('#freq-build-btn')
383 build_btn.click()
384 self.wait_until_visible('#latest-builds')
385 wait_until_build(self, 'queued cloning starting parsing failed')
386 lastest_builds = self.driver.find_elements(
387 By.XPATH,
388 '//div[@id="latest-builds"]/div'
389 )
390 self.assertTrue(len(lastest_builds) >= 2)
391 last_build = lastest_builds[0]
392 try:
393 cancel_button = last_build.find_element(
394 By.XPATH,
395 '//span[@class="cancel-build-btn pull-right alert-link"]',
396 )
397 cancel_button.click()
398 except NoSuchElementException:
399 # Skip if the build is already cancelled
400 pass
401 wait_until_build_cancelled(self)
402 # Create a new project for remaining asserts
403 project_name = self._random_string(10)
404 self._create_project(project_name=project_name, release='2')
405 current_url = self.driver.current_url
406 TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
407 url = current_url.split('?')[0]
408
409 # Create a new builds
410 self._create_builds()
411
412 # back to project page
413 self.driver.get(url)
414
415 self.wait_until_visible('#project-page', poll=3)
416
417 # Most built recipes
418 most_built_recipes = self.driver.find_element(
419 By.XPATH, '//*[@id="project-page"]/div[1]/div[3]')
420 title = most_built_recipes.find_element(By.TAG_NAME, 'h3')
421 self.assertTrue("Most built recipes" in title.text)
422 # check can select a recipe and build it
423 self.wait_until_visible('#freq-build-list', poll=3)
424 recipe_list = self.find('#freq-build-list')
425 recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li')
426 self.assertTrue(
427 len(recipe_list_items) > 0,
428 msg="Any recipes found in the most built recipes list",
429 )
430 rebuild_from_most_build_recipes(recipe_list_items)
431 TestProjectConfigTab.project_id = None # reset project id
432
433 def test_project_page_tab_importlayer(self):
434 """ Test project page tab import layer """
435 self._navigate_to_project_page()
436 # navigate to "Import layers" tab
437 import_layers_tab = self._get_tabs()[2]
438 import_layers_tab.find_element(By.TAG_NAME, 'a').click()
439 self.wait_until_visible('#layer-git-repo-url')
440
441 # Check git repo radio button
442 git_repo_radio = self.find('#git-repo-radio')
443 git_repo_radio.click()
444
445 # Set git repo url
446 input_repo_url = self.find('#layer-git-repo-url')
447 input_repo_url.send_keys('git://git.yoctoproject.org/meta-fake')
448 # Blur the input to trigger the validation
449 input_repo_url.send_keys(Keys.TAB)
450
451 # Check name is set
452 input_layer_name = self.find('#import-layer-name')
453 self.assertTrue(input_layer_name.get_attribute('value') == 'meta-fake')
454
455 # Set branch
456 input_branch = self.find('#layer-git-ref')
457 input_branch.send_keys('master')
458
459 # Import layer
460 self.find('#import-and-add-btn').click()
461
462 # Check layer is added
463 self.wait_until_visible('#layer-container')
464 block_l = self.driver.find_element(
465 By.XPATH, '//*[@id="project-page"]/div[2]')
466 layers = block_l.find_element(By.ID, 'layer-container')
467 layers_list = layers.find_element(By.ID, 'layers-in-project-list')
468 layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
469 self.assertTrue(
470 'meta-fake' in str(layers_list_items[-1].text)
471 )
472
473 def test_project_page_custom_image_no_image(self):
474 """ Test project page tab "New custom image" when no custom image """
475 project_name = self._random_string(10)
476 self._create_project(project_name=project_name)
477 current_url = self.driver.current_url
478 TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
479 # navigate to "Custom image" tab
480 custom_image_section = self._get_config_nav_item(2)
481 custom_image_section.click()
482 self.wait_until_visible('#empty-state-customimagestable')
483
484 # Check message when no custom image
485 self.assertTrue(
486 "You have not created any custom images yet." in str(
487 self.find('#empty-state-customimagestable').text
488 )
489 )
490 div_empty_msg = self.find('#empty-state-customimagestable')
491 link_create_custom_image = div_empty_msg.find_element(
492 By.TAG_NAME, 'a')
493 self.assertTrue(TestProjectConfigTab.project_id is not None)
494 self.assertTrue(
495 f"/toastergui/project/{TestProjectConfigTab.project_id}/newcustomimage" in str(
496 link_create_custom_image.get_attribute('href')
497 )
498 )
499 self.assertTrue(
500 "Create your first custom image" in str(
501 link_create_custom_image.text
502 )
503 )
504 TestProjectConfigTab.project_id = None # reset project id
505
506 def test_project_page_image_recipe(self):
507 """ Test project page section images
508 - Check image recipes are displayed
509 - Check search input
510 - Check image recipe build button works
511 - Check image recipe table features(show/hide column, pagination)
512 """
513 self._navigate_to_project_page()
514 # navigate to "Images section"
515 images_section = self._get_config_nav_item(3)
516 images_section.click()
517 self.wait_until_visible('#imagerecipestable')
518 rows = self.find_all('#imagerecipestable tbody tr')
519 self.assertTrue(len(rows) > 0)
520
521 # Test search input
522 self.wait_until_visible('#search-input-imagerecipestable')
523 recipe_input = self.find('#search-input-imagerecipestable')
524 recipe_input.send_keys('core-image-minimal')
525 self.find('#search-submit-imagerecipestable').click()
526 self.wait_until_visible('#imagerecipestable tbody tr')
527 rows = self.find_all('#imagerecipestable tbody tr')
528 self.assertTrue(len(rows) > 0)
diff --git a/bitbake/lib/toaster/tests/functional/utils.py b/bitbake/lib/toaster/tests/functional/utils.py
new file mode 100644
index 0000000000..7269fa1805
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/utils.py
@@ -0,0 +1,89 @@
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# BitBake Toaster UI tests implementation
4#
5# Copyright (C) 2023 Savoir-faire Linux
6#
7# SPDX-License-Identifier: GPL-2.0-only
8
9
10from time import sleep
11from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException
12from selenium.webdriver.common.by import By
13
14from orm.models import Build
15
16
17def wait_until_build(test_instance, state):
18 timeout = 60
19 start_time = 0
20 build_state = ''
21 while True:
22 try:
23 if start_time > timeout:
24 raise TimeoutException(
25 f'Build did not reach {state} state within {timeout} seconds'
26 )
27 last_build_state = test_instance.driver.find_element(
28 By.XPATH,
29 '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]',
30 )
31 build_state = last_build_state.get_attribute(
32 'data-build-state')
33 state_text = state.lower().split()
34 if any(x in str(build_state).lower() for x in state_text):
35 return str(build_state).lower()
36 if 'failed' in str(build_state).lower():
37 break
38 except NoSuchElementException:
39 continue
40 except TimeoutException:
41 break
42 start_time += 1
43 sleep(1) # take a breath and try again
44
45def wait_until_build_cancelled(test_instance):
46 """ Cancel build take a while sometime, the method is to wait driver action
47 until build being cancelled
48 """
49 timeout = 30
50 start_time = 0
51 build = None
52 while True:
53 try:
54 if start_time > timeout:
55 raise TimeoutException(
56 f'Build did not reach cancelled state within {timeout} seconds'
57 )
58 last_build_state = test_instance.driver.find_element(
59 By.XPATH,
60 '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]',
61 )
62 build_state = last_build_state.get_attribute(
63 'data-build-state')
64 if 'failed' in str(build_state).lower():
65 break
66 if 'cancelling' in str(build_state).lower():
67 # Change build state to cancelled
68 if not build: # get build object only once
69 build = Build.objects.last()
70 build.outcome = Build.CANCELLED
71 build.save()
72 if 'cancelled' in str(build_state).lower():
73 break
74 except NoSuchElementException:
75 continue
76 except StaleElementReferenceException:
77 continue
78 except TimeoutException:
79 break
80 start_time += 1
81 sleep(1) # take a breath and try again
82
83def get_projectId_from_url(url):
84 # url = 'http://domainename.com/toastergui/project/1656/whatever
85 # or url = 'http://domainename.com/toastergui/project/1/
86 # or url = 'http://domainename.com/toastergui/project/186
87 assert '/toastergui/project/' in url, "URL is not valid"
88 url_to_list = url.split('/toastergui/project/')
89 return int(url_to_list[1].split('/')[0]) # project_id
diff --git a/bitbake/lib/toaster/tests/toaster-tests-requirements.txt b/bitbake/lib/toaster/tests/toaster-tests-requirements.txt
index 4f9fcc46d2..71cc083436 100644
--- a/bitbake/lib/toaster/tests/toaster-tests-requirements.txt
+++ b/bitbake/lib/toaster/tests/toaster-tests-requirements.txt
@@ -1 +1,7 @@
1selenium==2.49.2 1selenium>=4.13.0
2pytest==7.4.2
3pytest-django==4.5.2
4pytest-env==1.1.0
5pytest-html==4.0.2
6pytest-metadata==3.0.0
7pytest-order==1.1.0
diff --git a/bitbake/lib/toaster/tests/views/test_views.py b/bitbake/lib/toaster/tests/views/test_views.py
index 735d596bcc..e1adfcf86a 100644
--- a/bitbake/lib/toaster/tests/views/test_views.py
+++ b/bitbake/lib/toaster/tests/views/test_views.py
@@ -9,6 +9,8 @@
9 9
10"""Test cases for Toaster GUI and ReST.""" 10"""Test cases for Toaster GUI and ReST."""
11 11
12import os
13import pytest
12from django.test import TestCase 14from django.test import TestCase
13from django.test.client import RequestFactory 15from django.test.client import RequestFactory
14from django.urls import reverse 16from django.urls import reverse
@@ -19,6 +21,7 @@ from orm.models import Layer_Version, Recipe
19from orm.models import CustomImageRecipe 21from orm.models import CustomImageRecipe
20from orm.models import CustomImagePackage 22from orm.models import CustomImagePackage
21 23
24from bldcontrol.models import BuildEnvironment
22import inspect 25import inspect
23import toastergui 26import toastergui
24 27
@@ -32,19 +35,32 @@ PROJECT_NAME2 = "test project 2"
32CLI_BUILDS_PROJECT_NAME = 'Command line builds' 35CLI_BUILDS_PROJECT_NAME = 'Command line builds'
33 36
34 37
38
35class ViewTests(TestCase): 39class ViewTests(TestCase):
36 """Tests to verify view APIs.""" 40 """Tests to verify view APIs."""
37 41
38 fixtures = ['toastergui-unittest-data'] 42 fixtures = ['toastergui-unittest-data']
43 builldir = os.environ.get('BUILDDIR')
39 44
40 def setUp(self): 45 def setUp(self):
41 46
42 self.project = Project.objects.first() 47 self.project = Project.objects.first()
48
43 self.recipe1 = Recipe.objects.get(pk=2) 49 self.recipe1 = Recipe.objects.get(pk=2)
50 # create a file and to recipe1 file_path
51 file_path = f"{self.builldir}/{self.recipe1.name.strip().replace(' ', '-')}.bb"
52 with open(file_path, 'w') as f:
53 f.write('foo')
54 self.recipe1.file_path = file_path
55 self.recipe1.save()
56
44 self.customr = CustomImageRecipe.objects.first() 57 self.customr = CustomImageRecipe.objects.first()
45 self.cust_package = CustomImagePackage.objects.first() 58 self.cust_package = CustomImagePackage.objects.first()
46 self.package = Package.objects.first() 59 self.package = Package.objects.first()
47 self.lver = Layer_Version.objects.first() 60 self.lver = Layer_Version.objects.first()
61 if BuildEnvironment.objects.count() == 0:
62 BuildEnvironment.objects.create(betype=BuildEnvironment.TYPE_LOCAL)
63
48 64
49 def test_get_base_call_returns_html(self): 65 def test_get_base_call_returns_html(self):
50 """Basic test for all-projects view""" 66 """Basic test for all-projects view"""
@@ -226,7 +242,7 @@ class ViewTests(TestCase):
226 recipe = CustomImageRecipe.objects.create( 242 recipe = CustomImageRecipe.objects.create(
227 name=name, project=self.project, 243 name=name, project=self.project,
228 base_recipe=self.recipe1, 244 base_recipe=self.recipe1,
229 file_path="/tmp/testing", 245 file_path=f"{self.builldir}/testing",
230 layer_version=self.customr.layer_version) 246 layer_version=self.customr.layer_version)
231 url = reverse('xhr_customrecipe_id', args=(recipe.id,)) 247 url = reverse('xhr_customrecipe_id', args=(recipe.id,))
232 response = self.client.delete(url) 248 response = self.client.delete(url)
@@ -297,7 +313,7 @@ class ViewTests(TestCase):
297 """Download the recipe file generated for the custom image""" 313 """Download the recipe file generated for the custom image"""
298 314
299 # Create a dummy recipe file for the custom image generation to read 315 # Create a dummy recipe file for the custom image generation to read
300 open("/tmp/a_recipe.bb", 'a').close() 316 open(f"{self.builldir}/a_recipe.bb", 'a').close()
301 response = self.client.get(reverse('customrecipedownload', 317 response = self.client.get(reverse('customrecipedownload',
302 args=(self.project.id, 318 args=(self.project.id,
303 self.customr.id))) 319 self.customr.id)))