summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py
diff options
context:
space:
mode:
Diffstat (limited to 'bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py')
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py528
1 files changed, 528 insertions, 0 deletions
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)