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 string 11import time 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 25class TestProjectPageBase(SeleniumFunctionalTestCase): 26 project_id = None 27 PROJECT_NAME = 'TestProjectPage' 28 29 def _navigate_to_project_page(self): 30 # Navigate to project page 31 if TestProjectPageBase.project_id is None: 32 TestProjectPageBase.project_id = self.create_new_project(self.PROJECT_NAME, '3', None, True) 33 34 url = reverse('project', args=(TestProjectPageBase.project_id,)) 35 self.get(url) 36 self.wait_until_visible('#config-nav') 37 38 def _get_create_builds(self, **kwargs): 39 """ Create a build and return the build object """ 40 # parameters for builds to associate with the projects 41 now = timezone.now() 42 self.project1_build_success = { 43 'project': Project.objects.get(id=TestProjectPageBase.project_id), 44 'started_on': now, 45 'completed_on': now, 46 'outcome': Build.SUCCEEDED 47 } 48 49 self.project1_build_failure = { 50 'project': Project.objects.get(id=TestProjectPageBase.project_id), 51 'started_on': now, 52 'completed_on': now, 53 'outcome': Build.FAILED 54 } 55 build1 = Build.objects.create(**self.project1_build_success) 56 build2 = Build.objects.create(**self.project1_build_failure) 57 58 # add some targets to these builds so they have recipe links 59 # (and so we can find the row in the ToasterTable corresponding to 60 # a particular build) 61 Target.objects.create(build=build1, target='foo') 62 Target.objects.create(build=build2, target='bar') 63 64 if kwargs: 65 # Create kwargs.get('success') builds with success status with target 66 # and kwargs.get('failure') builds with failure status with target 67 for i in range(kwargs.get('success', 0)): 68 now = timezone.now() 69 self.project1_build_success['started_on'] = now 70 self.project1_build_success[ 71 'completed_on'] = now - timezone.timedelta(days=i) 72 build = Build.objects.create(**self.project1_build_success) 73 Target.objects.create(build=build, 74 target=f'{i}_success_recipe', 75 task=f'{i}_success_task') 76 77 for i in range(kwargs.get('failure', 0)): 78 now = timezone.now() 79 self.project1_build_failure['started_on'] = now 80 self.project1_build_failure[ 81 'completed_on'] = now - timezone.timedelta(days=i) 82 build = Build.objects.create(**self.project1_build_failure) 83 Target.objects.create(build=build, 84 target=f'{i}_fail_recipe', 85 task=f'{i}_fail_task') 86 return build1, build2 87 88 def _mixin_test_table_edit_column( 89 self, 90 table_id, 91 edit_btn_id, 92 list_check_box_id: list 93 ): 94 # Check edit column 95 finder = lambda driver: self.find(f'#{edit_btn_id}') 96 edit_column = self.wait_until_element_clickable(finder) 97 self.assertTrue(edit_column.is_displayed()) 98 edit_column.click() 99 # Check dropdown is visible 100 self.wait_until_visible('ul.dropdown-menu.editcol') 101 for check_box_id in list_check_box_id: 102 # Check that we can hide/show table column 103 check_box = self.find(f'#{check_box_id}') 104 th_class = str(check_box_id).replace('checkbox-', '') 105 if check_box.is_selected(): 106 # check if column is visible in table 107 self.assertTrue( 108 self.find( 109 f'#{table_id} thead th.{th_class}' 110 ).is_displayed(), 111 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" 112 ) 113 check_box.click() 114 # check if column is hidden in table 115 self.assertFalse( 116 self.find( 117 f'#{table_id} thead th.{th_class}' 118 ).is_displayed(), 119 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" 120 ) 121 else: 122 # check if column is hidden in table 123 self.assertFalse( 124 self.find( 125 f'#{table_id} thead th.{th_class}' 126 ).is_displayed(), 127 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" 128 ) 129 check_box.click() 130 # check if column is visible in table 131 self.assertTrue( 132 self.find( 133 f'#{table_id} thead th.{th_class}' 134 ).is_displayed(), 135 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" 136 ) 137 138 def _get_config_nav_item(self, index): 139 config_nav = self.find('#config-nav') 140 return config_nav.find_elements(By.TAG_NAME, 'li')[index] 141 142 def _navigate_to_config_nav(self, nav_id, nav_index): 143 # navigate to the project page 144 self._navigate_to_project_page() 145 # click on "Software recipe" tab 146 soft_recipe = self._get_config_nav_item(nav_index) 147 soft_recipe.click() 148 self.wait_until_visible(f'#{nav_id}') 149 150 def _mixin_test_table_show_rows(self, table_selector, **kwargs): 151 """ Test the show rows feature in the builds table on the all builds page """ 152 def test_show_rows(row_to_show, show_row_link): 153 # Check that we can show rows == row_to_show 154 show_row_link.select_by_value(str(row_to_show)) 155 self.wait_until_visible(f'#{table_selector} tbody tr') 156 # check at least some rows are visible 157 self.assertTrue( 158 len(self.find_all(f'#{table_selector} tbody tr')) > 0 159 ) 160 self.wait_until_present(f'#{table_selector} tbody tr') 161 show_rows = self.driver.find_elements( 162 By.XPATH, 163 f'//select[@class="form-control pagesize-{table_selector}"]' 164 ) 165 rows_to_show = [10, 25, 50, 100, 150] 166 to_skip = kwargs.get('to_skip', []) 167 # Check show rows 168 for show_row_link in show_rows: 169 show_row_link = Select(show_row_link) 170 for row_to_show in rows_to_show: 171 if row_to_show not in to_skip: 172 test_show_rows(row_to_show, show_row_link) 173 174 def _mixin_test_table_search_input(self, **kwargs): 175 input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values() 176 # Test search input 177 self.wait_until_visible(f'#{input_selector}') 178 recipe_input = self.find(f'#{input_selector}') 179 recipe_input.send_keys(input_text) 180 self.find(f'#{searchBtn_selector}').click() 181 self.wait_until_visible(f'#{table_selector} tbody tr') 182 rows = self.find_all(f'#{table_selector} tbody tr') 183 self.assertTrue(len(rows) > 0) 184 185class TestProjectPage(TestProjectPageBase): 186 187 def test_page_header_on_project_page(self): 188 """ Check page header in project page: 189 - AT LEFT -> Logo of Yocto project, displayed, clickable 190 - "Toaster"+" Information icon", displayed, clickable 191 - "Server Icon" + "All builds", displayed, clickable 192 - "Directory Icon" + "All projects", displayed, clickable 193 - "Book Icon" + "Documentation", displayed, clickable 194 - AT RIGHT -> button "New project", displayed, clickable 195 """ 196 # navigate to the project page 197 self._navigate_to_project_page() 198 199 # check page header 200 # AT LEFT -> Logo of Yocto project 201 logo = self.driver.find_element( 202 By.XPATH, 203 "//div[@class='toaster-navbar-brand']", 204 ) 205 logo_img = logo.find_element(By.TAG_NAME, 'img') 206 self.assertTrue(logo_img.is_displayed(), 207 'Logo of Yocto project not found') 208 self.assertIn( 209 '/static/img/logo.png', str(logo_img.get_attribute('src')), 210 'Logo of Yocto project not found' 211 ) 212 # "Toaster"+" Information icon", clickable 213 toaster = self.driver.find_element( 214 By.XPATH, 215 "//div[@class='toaster-navbar-brand']//a[@class='brand']", 216 ) 217 self.assertTrue(toaster.is_displayed(), 'Toaster not found') 218 self.assertEqual(toaster.text, 'Toaster') 219 info_sign = self.find('.glyphicon-info-sign') 220 self.assertTrue(info_sign.is_displayed()) 221 222 # "Server Icon" + "All builds" 223 all_builds = self.find('#navbar-all-builds') 224 all_builds_link = all_builds.find_element(By.TAG_NAME, 'a') 225 self.assertIn("All builds", all_builds_link.text) 226 self.assertIn( 227 '/toastergui/builds/', str(all_builds_link.get_attribute('href')) 228 ) 229 server_icon = all_builds.find_element(By.TAG_NAME, 'i') 230 self.assertEqual( 231 server_icon.get_attribute('class'), 'glyphicon glyphicon-tasks' 232 ) 233 self.assertTrue(server_icon.is_displayed()) 234 235 # "Directory Icon" + "All projects" 236 all_projects = self.find('#navbar-all-projects') 237 all_projects_link = all_projects.find_element(By.TAG_NAME, 'a') 238 self.assertIn("All projects", all_projects_link.text) 239 self.assertIn( 240 '/toastergui/projects/', str(all_projects_link.get_attribute( 241 'href')) 242 ) 243 dir_icon = all_projects.find_element(By.TAG_NAME, 'i') 244 self.assertEqual( 245 dir_icon.get_attribute('class'), 'icon-folder-open' 246 ) 247 self.assertTrue(dir_icon.is_displayed()) 248 249 # "Book Icon" + "Documentation" 250 toaster_docs_link = self.find('#navbar-docs') 251 toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME, 252 'a') 253 self.assertIn("Documentation", toaster_docs_link_link.text) 254 self.assertEqual( 255 toaster_docs_link_link.get_attribute('href'), 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual' 256 ) 257 book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i') 258 self.assertEqual( 259 book_icon.get_attribute('class'), 'glyphicon glyphicon-book' 260 ) 261 self.assertTrue(book_icon.is_displayed()) 262 263 # AT RIGHT -> button "New project" 264 new_project_button = self.find('#new-project-button') 265 self.assertTrue(new_project_button.is_displayed()) 266 self.assertEqual(new_project_button.text, 'New project') 267 new_project_button.click() 268 self.assertIn( 269 '/toastergui/newproject/', str(self.driver.current_url) 270 ) 271 272 def test_edit_project_name(self): 273 """ Test edit project name: 274 - Click on "Edit" icon button 275 - Change project name 276 - Click on "Save" button 277 - Check project name is changed 278 """ 279 # navigate to the project page 280 self._navigate_to_project_page() 281 282 # click on "Edit" icon button 283 self.wait_until_visible('#project-name-container') 284 finder = lambda driver: self.find('#project-change-form-toggle') 285 edit_button = self.wait_until_element_clickable(finder) 286 edit_button.click() 287 project_name_input = self.find('#project-name-change-input') 288 self.assertTrue(project_name_input.is_displayed()) 289 project_name_input.clear() 290 project_name_input.send_keys('New Name') 291 self.find('#project-name-change-btn').click() 292 293 # check project name is changed 294 self.wait_until_visible('#project-name-container') 295 self.assertIn( 296 'New Name', str(self.find('#project-name-container').text) 297 ) 298 299 def test_project_page_tabs(self): 300 """ Test project tabs: 301 - "configuration" tab 302 - "Builds" tab 303 - "Import layers" tab 304 - "New custom image" tab 305 Check search box used to build recipes 306 """ 307 # navigate to the project page 308 self._navigate_to_project_page() 309 310 # check "configuration" tab 311 self.wait_until_visible('#topbar-configuration-tab') 312 config_tab = self.find('#topbar-configuration-tab') 313 self.assertEqual(config_tab.get_attribute('class'), 'active') 314 self.assertIn('Configuration', str(config_tab.text)) 315 self.assertIn( 316 f"/toastergui/project/{TestProjectPageBase.project_id}", str(self.driver.current_url) 317 ) 318 319 def get_tabs(): 320 # tabs links list 321 return self.driver.find_elements( 322 By.XPATH, 323 '//div[@id="project-topbar"]//li' 324 ) 325 326 def check_tab_link(tab_index, tab_name, url): 327 tab = get_tabs()[tab_index] 328 tab_link = tab.find_element(By.TAG_NAME, 'a') 329 self.assertIn(url, tab_link.get_attribute('href')) 330 self.assertIn(tab_name, tab_link.text) 331 self.assertEqual(tab.get_attribute('class'), 'active') 332 333 # check "Builds" tab 334 builds_tab = get_tabs()[1] 335 builds_tab.find_element(By.TAG_NAME, 'a').click() 336 check_tab_link( 337 1, 338 'Builds', 339 f"/toastergui/project/{TestProjectPageBase.project_id}/builds" 340 ) 341 342 # check "Import layers" tab 343 import_layers_tab = get_tabs()[2] 344 import_layers_tab.find_element(By.TAG_NAME, 'a').click() 345 check_tab_link( 346 2, 347 'Import layer', 348 f"/toastergui/project/{TestProjectPageBase.project_id}/importlayer" 349 ) 350 351 # check "New custom image" tab 352 new_custom_image_tab = get_tabs()[3] 353 new_custom_image_tab.find_element(By.TAG_NAME, 'a').click() 354 check_tab_link( 355 3, 356 'New custom image', 357 f"/toastergui/project/{TestProjectPageBase.project_id}/newcustomimage" 358 ) 359 360 # check search box can be use to build recipes 361 search_box = self.find('#build-input') 362 search_box.send_keys('core-image-minimal') 363 self.find('#build-button').click() 364 self.wait_until_visible('#latest-builds') 365 buildtext = "Loading" 366 while "Loading" in buildtext: 367 time.sleep(1) 368 lastest_builds = self.driver.find_elements( 369 By.XPATH, 370 '//div[@id="latest-builds"]', 371 ) 372 last_build = lastest_builds[0] 373 buildtext = last_build.text 374 self.assertIn( 375 'core-image-minimal', str(last_build.text) 376 ) 377 378 def test_softwareRecipe_page(self): 379 """ Test software recipe page 380 - Check title "Compatible software recipes" is displayed 381 - Check search input 382 - Check "build recipe" button works 383 - Check software recipe table feature(show/hide column, pagination) 384 """ 385 self._navigate_to_config_nav('softwarerecipestable', 4) 386 # check title "Compatible software recipes" is displayed 387 self.assertIn("Compatible software recipes", self.get_page_source()) 388 # Test search input 389 self._mixin_test_table_search_input( 390 input_selector='search-input-softwarerecipestable', 391 input_text='busybox', 392 searchBtn_selector='search-submit-softwarerecipestable', 393 table_selector='softwarerecipestable' 394 ) 395 # check "build recipe" button works 396 finder = lambda driver: self.find_all('#softwarerecipestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a') 397 build_btn = self.wait_until_element_clickable(finder) 398 build_btn.click() 399 build_state = wait_until_build(self, 'queued cloning starting parsing failed') 400 lastest_builds = self.driver.find_elements( 401 By.XPATH, 402 '//div[@id="latest-builds"]/div' 403 ) 404 self.assertTrue(len(lastest_builds) > 0) 405 # Find the latest builds, the last build and then the cancel button 406 407 finder = lambda driver: driver.find_elements(By.XPATH, '//div[@id="latest-builds"]/div')[0].find_element(By.XPATH, '//span[@class="cancel-build-btn pull-right alert-link"]') 408 cancel_button = self.wait_until_element_clickable(finder) 409 cancel_button.click() 410 if 'starting' not in build_state: # change build state when cancelled in starting state 411 wait_until_build_cancelled(self) 412 413 # check software recipe table feature(show/hide column, pagination) 414 self._navigate_to_config_nav('softwarerecipestable', 4) 415 column_list = [ 416 'get_description_or_summary', 417 'layer_version__get_vcs_reference', 418 'layer_version__layer__name', 419 'license', 420 'recipe-file', 421 'section', 422 'version', 423 ] 424 self._mixin_test_table_edit_column( 425 'softwarerecipestable', 426 'edit-columns-button', 427 [f'checkbox-{column}' for column in column_list] 428 ) 429 self._navigate_to_config_nav('softwarerecipestable', 4) 430 # check show rows(pagination) 431 self._mixin_test_table_show_rows( 432 table_selector='softwarerecipestable', 433 to_skip=[150], 434 ) 435 436 def test_machines_page(self): 437 """ Test Machine page 438 - Check if title "Compatible machines" is displayed 439 - Check search input 440 - Check "Select machine" button works 441 - Check "Add layer" button works 442 - Check Machine table feature(show/hide column, pagination) 443 """ 444 self._navigate_to_config_nav('machinestable', 5) 445 # check title "Compatible software recipes" is displayed 446 self.assertIn("Compatible machines", self.get_page_source()) 447 # Test search input 448 self._mixin_test_table_search_input( 449 input_selector='search-input-machinestable', 450 input_text='qemux86-64', 451 searchBtn_selector='search-submit-machinestable', 452 table_selector='machinestable' 453 ) 454 # check "Select machine" button works 455 finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]') 456 select_btn = self.wait_until_element_clickable(finder) 457 select_btn.click() 458 self.wait_until_visible('#project-machine-name') 459 project_machine_name = self.find('#project-machine-name') 460 self.assertIn( 461 'qemux86-64', project_machine_name.text 462 ) 463 # check "Add layer" button works 464 self._navigate_to_config_nav('machinestable', 5) 465 # Search for a machine whit layer not in project 466 self._mixin_test_table_search_input( 467 input_selector='search-input-machinestable', 468 input_text='qemux86-64-tpm2', 469 searchBtn_selector='search-submit-machinestable', 470 table_selector='machinestable' 471 ) 472 473 self.wait_until_visible('#machinestable tbody tr') 474 # Locate a machine to add button 475 finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]') 476 add_btn = self.wait_until_element_clickable(finder) 477 add_btn.click() 478 self.wait_until_visible('#change-notification') 479 change_notification = self.find('#change-notification') 480 self.assertIn( 481 f'You have added 1 layer to your project', str(change_notification.text) 482 ) 483 484 finder = lambda driver: self.find('#hide-alert') 485 hide_button = self.wait_until_element_clickable(finder) 486 hide_button.click() 487 self.wait_until_not_visible('#change-notification') 488 489 # check Machine table feature(show/hide column, pagination) 490 self._navigate_to_config_nav('machinestable', 5) 491 column_list = [ 492 'description', 493 'layer_version__get_vcs_reference', 494 'layer_version__layer__name', 495 'machinefile', 496 ] 497 self._mixin_test_table_edit_column( 498 'machinestable', 499 'edit-columns-button', 500 [f'checkbox-{column}' for column in column_list] 501 ) 502 self._navigate_to_config_nav('machinestable', 5) 503 # check show rows(pagination) 504 self._mixin_test_table_show_rows( 505 table_selector='machinestable', 506 to_skip=[150], 507 ) 508 509 def test_layers_page(self): 510 """ Test layers page 511 - Check if title "Compatible layerss" is displayed 512 - Check search input 513 - Check "Add layer" button works 514 - Check "Remove layer" button works 515 - Check layers table feature(show/hide column, pagination) 516 """ 517 self._navigate_to_config_nav('layerstable', 6) 518 # check title "Compatible layers" is displayed 519 self.assertIn("Compatible layers", self.get_page_source()) 520 # Test search input 521 input_text='meta-tanowrt' 522 self._mixin_test_table_search_input( 523 input_selector='search-input-layerstable', 524 input_text=input_text, 525 searchBtn_selector='search-submit-layerstable', 526 table_selector='layerstable' 527 ) 528 # check "Add layer" button works 529 self.wait_until_visible('#layerstable tbody tr') 530 finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="add"]') 531 add_btn = self.wait_until_element_clickable(finder) 532 add_btn.click() 533 # check modal is displayed 534 self.wait_until_visible('#dependencies-modal') 535 list_dependencies = self.find_all('#dependencies-list li') 536 # click on add-layers button 537 finder = lambda driver: self.driver.find_element(By.XPATH, '//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]') 538 add_layers_btn = self.wait_until_element_clickable(finder) 539 add_layers_btn.click() 540 self.wait_until_visible('#change-notification') 541 change_notification = self.find('#change-notification') 542 self.assertIn( 543 f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies', str(change_notification.text) 544 ) 545 546 finder = lambda driver: self.find('#hide-alert') 547 hide_button = self.wait_until_element_clickable(finder) 548 hide_button.click() 549 self.wait_until_not_visible('#change-notification') 550 551 # check "Remove layer" button works 552 self.wait_until_visible('#layerstable tbody tr') 553 finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="remove"]') 554 remove_btn = self.wait_until_element_clickable(finder) 555 remove_btn.click() 556 self.wait_until_visible('#change-notification') 557 change_notification = self.find('#change-notification') 558 self.assertIn( 559 f'You have removed 1 layer from your project: {input_text}', str(change_notification.text) 560 ) 561 562 finder = lambda driver: self.find('#hide-alert') 563 hide_button = self.wait_until_element_clickable(finder) 564 hide_button.click() 565 self.wait_until_not_visible('#change-notification') 566 567 # check layers table feature(show/hide column, pagination) 568 self._navigate_to_config_nav('layerstable', 6) 569 column_list = [ 570 'dependencies', 571 'revision', 572 'layer__vcs_url', 573 'git_subdir', 574 'layer__summary', 575 ] 576 self._mixin_test_table_edit_column( 577 'layerstable', 578 'edit-columns-button', 579 [f'checkbox-{column}' for column in column_list] 580 ) 581 self._navigate_to_config_nav('layerstable', 6) 582 # check show rows(pagination) 583 self._mixin_test_table_show_rows( 584 table_selector='layerstable', 585 to_skip=[150], 586 ) 587 588 def test_distro_page(self): 589 """ Test distros page 590 - Check if title "Compatible distros" is displayed 591 - Check search input 592 - Check "Add layer" button works 593 - Check distro table feature(show/hide column, pagination) 594 """ 595 self._navigate_to_config_nav('distrostable', 7) 596 # check title "Compatible distros" is displayed 597 self.assertIn("Compatible Distros", self.get_page_source()) 598 # Test search input 599 input_text='poky-altcfg' 600 self._mixin_test_table_search_input( 601 input_selector='search-input-distrostable', 602 input_text=input_text, 603 searchBtn_selector='search-submit-distrostable', 604 table_selector='distrostable' 605 ) 606 # check "Add distro" button works 607 self.wait_until_visible(".add-del-layers") 608 finder = lambda driver: self.find_all('#distrostable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]') 609 add_btn = self.wait_until_element_clickable(finder) 610 add_btn.click() 611 self.wait_until_visible('#change-notification') 612 change_notification = self.find('#change-notification') 613 self.assertIn( 614 f'You have changed the distro to: {input_text}', str(change_notification.text) 615 ) 616 # check distro table feature(show/hide column, pagination) 617 self._navigate_to_config_nav('distrostable', 7) 618 column_list = [ 619 'description', 620 'templatefile', 621 'layer_version__get_vcs_reference', 622 'layer_version__layer__name', 623 ] 624 self._mixin_test_table_edit_column( 625 'distrostable', 626 'edit-columns-button', 627 [f'checkbox-{column}' for column in column_list] 628 ) 629 self._navigate_to_config_nav('distrostable', 7) 630 # check show rows(pagination) 631 self._mixin_test_table_show_rows( 632 table_selector='distrostable', 633 to_skip=[150], 634 ) 635 636 def test_single_layer_page(self): 637 """ Test layer details page using meta-poky as an example (assumes is added to start with) 638 - Check if title is displayed 639 - Check add/remove layer button works 640 - Check tabs(layers, recipes, machines) are displayed 641 - Check left section is displayed 642 - Check layer name 643 - Check layer summary 644 - Check layer description 645 """ 646 self._navigate_to_config_nav('layerstable', 6) 647 layer_link = self.driver.find_element(By.XPATH, '//tr/td[@class="layer__name"]/a[contains(text(),"meta-poky")]') 648 layer_link.click() 649 self.wait_until_visible('.page-header') 650 # check title is displayed 651 self.assertTrue(self.find('.page-header h1').is_displayed()) 652 653 # check remove layer button works 654 finder = lambda driver: self.find('#add-remove-layer-btn') 655 remove_layer_btn = self.wait_until_element_clickable(finder) 656 remove_layer_btn.click() 657 self.wait_until_visible('#change-notification') 658 change_notification = self.find('#change-notification') 659 self.assertIn( 660 f'You have removed 1 layer from your project', str(change_notification.text) 661 ) 662 finder = lambda driver: self.find('#hide-alert') 663 hide_button = self.wait_until_element_clickable(finder) 664 hide_button.click() 665 # check add layer button works 666 self.wait_until_not_visible('#change-notification') 667 finder = lambda driver: self.find('#add-remove-layer-btn') 668 add_layer_btn = self.wait_until_element_clickable(finder) 669 add_layer_btn.click() 670 self.wait_until_visible('#change-notification') 671 change_notification = self.find('#change-notification') 672 self.assertIn( 673 f'You have added 1 layer to your project', str(change_notification.text) 674 ) 675 finder = lambda driver: self.find('#hide-alert') 676 hide_button = self.wait_until_element_clickable(finder) 677 hide_button.click() 678 self.wait_until_not_visible('#change-notification') 679 # check tabs(layers, recipes, machines) are displayed 680 tabs = self.find_all('.nav-tabs li') 681 self.assertEqual(len(tabs), 3) 682 # Check first tab 683 tabs[0].click() 684 self.assertIn( 685 'active', str(self.find('#information').get_attribute('class')) 686 ) 687 # Check second tab (recipes) 688 # Ensure page is scrolled to the top 689 self.driver.find_element(By.XPATH, '//body').send_keys(Keys.CONTROL + Keys.HOME) 690 self.wait_until_visible('.nav-tabs') 691 tabs[1].click() 692 self.assertIn( 693 'active', str(self.find('#recipes').get_attribute('class')) 694 ) 695 # Check third tab (machines) 696 # Ensure page is scrolled to the top 697 self.driver.find_element(By.XPATH, '//body').send_keys(Keys.CONTROL + Keys.HOME) 698 self.wait_until_visible('.nav-tabs') 699 tabs[2].click() 700 self.assertIn( 701 'active', str(self.find('#machines').get_attribute('class')) 702 ) 703 # Check left section is displayed 704 section = self.find('.well') 705 # Check layer name 706 self.assertTrue( 707 section.find_element(By.XPATH, '//h2[1]').is_displayed() 708 ) 709 # Check layer summary 710 self.assertIn("Summary", section.text) 711 # Check layer description 712 self.assertIn("Description", section.text) 713 714@pytest.mark.django_db 715@pytest.mark.order("last") 716class TestProjectPageRecipes(TestProjectPageBase): 717 718 def test_single_recipe_page(self): 719 """ Test recipe page 720 - Check if title is displayed 721 - Check add recipe layer displayed 722 - Check left section is displayed 723 - Check recipe: name, summary, description, Version, Section, 724 License, Approx. packages included, Approx. size, Recipe file 725 """ 726 # Use a recipe which is likely to exist in the layer index but not enabled 727 # in poky out the box - xen-image-minimal from meta-virtualization 728 self._navigate_to_project_page() 729 prj = Project.objects.get(pk=TestProjectPageBase.project_id) 730 recipe_id = prj.get_all_compatible_recipes().get(name="xen-image-minimal").pk 731 url = reverse("recipedetails", args=(TestProjectPageBase.project_id, recipe_id)) 732 self.get(url) 733 self.wait_until_visible('.page-header') 734 # check title is displayed 735 self.assertTrue(self.find('.page-header h1').is_displayed()) 736 # check add recipe layer displayed 737 add_recipe_layer_btn = self.find('#add-layer-btn') 738 self.assertTrue(add_recipe_layer_btn.is_displayed()) 739 # check left section is displayed 740 section = self.find('.well') 741 # Check recipe name 742 self.assertTrue( 743 section.find_element(By.XPATH, '//h2[1]').is_displayed() 744 ) 745 # Check recipe sections details info are displayed 746 self.assertIn("Summary", section.text) 747 self.assertIn("Description", section.text) 748 self.assertIn("Version", section.text) 749 self.assertIn("Section", section.text) 750 self.assertIn("License", section.text) 751 self.assertIn("Approx. packages included", section.text) 752 self.assertIn("Approx. package size", section.text) 753 self.assertIn("Recipe file", section.text) 754 755 def test_image_recipe_editColumn(self): 756 """ Test the edit column feature in image recipe table on project page """ 757 self._get_create_builds(success=10, failure=10) 758 759 url = reverse('projectimagerecipes', args=(TestProjectPageBase.project_id,)) 760 self.get(url) 761 self.wait_until_present('#imagerecipestable tbody tr') 762 763 column_list = [ 764 'get_description_or_summary', 'layer_version__get_vcs_reference', 765 'layer_version__layer__name', 'license', 'recipe-file', 'section', 766 'version' 767 ] 768 769 # Check that we can hide the edit column 770 self._mixin_test_table_edit_column( 771 'imagerecipestable', 772 'edit-columns-button', 773 [f'checkbox-{column}' for column in column_list] 774 ) 775 776