diff options
Diffstat (limited to 'poky/bitbake/lib/toaster/tests')
17 files changed, 1678 insertions, 32 deletions
diff --git a/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py index 9a4e27a3bc..e0ac43768e 100644 --- a/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py +++ b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py @@ -33,7 +33,13 @@ def create_selenium_driver(cls,browser='chrome'): browser = env_browser if browser == 'chrome': - return webdriver.Chrome() + options = webdriver.ChromeOptions() + options.add_argument('headless') + options.add_argument('--disable-infobars') + options.add_argument('--disable-dev-shm-usage') + options.add_argument('--no-sandbox') + options.add_argument('--remote-debugging-port=9222') + return webdriver.Chrome(options=options) elif browser == 'firefox': return webdriver.Firefox() elif browser == 'marionette': diff --git a/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py index d4312bb35b..4e9b9fd760 100644 --- a/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py +++ b/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py @@ -7,13 +7,16 @@ # SPDX-License-Identifier: GPL-2.0-only # -import re, time +import re +import time from django.urls import reverse +from selenium.webdriver.support.select import Select from django.utils import timezone +from bldcontrol.models import BuildRequest from tests.browser.selenium_helpers import SeleniumTestCase -from orm.models import BitbakeVersion, Release, Project, Build, Target +from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task from selenium.webdriver.common.by import By @@ -102,6 +105,66 @@ class TestAllBuildsPage(SeleniumTestCase): return found_row + def _get_create_builds(self, **kwargs): + """ Create a build and return the build object """ + build1 = Build.objects.create(**self.project1_build_success) + build2 = Build.objects.create(**self.project1_build_failure) + + # add some targets to these builds so they have recipe links + # (and so we can find the row in the ToasterTable corresponding to + # a particular build) + Target.objects.create(build=build1, target='foo') + Target.objects.create(build=build2, target='bar') + + if kwargs: + # Create kwargs.get('success') builds with success status with target + # and kwargs.get('failure') builds with failure status with target + for i in range(kwargs.get('success', 0)): + now = timezone.now() + self.project1_build_success['started_on'] = now + self.project1_build_success[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_success) + Target.objects.create(build=build, + target=f'{i}_success_recipe', + task=f'{i}_success_task') + + self._set_buildRequest_and_task_on_build(build) + for i in range(kwargs.get('failure', 0)): + now = timezone.now() + self.project1_build_failure['started_on'] = now + self.project1_build_failure[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_failure) + Target.objects.create(build=build, + target=f'{i}_fail_recipe', + task=f'{i}_fail_task') + self._set_buildRequest_and_task_on_build(build) + return build1, build2 + + def _create_recipe(self): + """ Add a recipe to the database and return it """ + layer = Layer.objects.create() + layer_version = Layer_Version.objects.create(layer=layer) + return Recipe.objects.create(name='recipe_foo', layer_version=layer_version) + + def _set_buildRequest_and_task_on_build(self, build): + """ Set buildRequest and task on build """ + build.recipes_parsed = 1 + build.save() + buildRequest = BuildRequest.objects.create( + build=build, + project=self.project1, + state=BuildRequest.REQ_COMPLETED) + build.build_request = buildRequest + recipe = self._create_recipe() + task = Task.objects.create(build=build, + recipe=recipe, + task_name='task', + outcome=Task.OUTCOME_SUCCESS) + task.save() + build.save() + def test_show_tasks_with_suffix(self): """ Task should be shown as suffix on build name """ build = Build.objects.create(**self.project1_build_success) @@ -128,7 +191,8 @@ class TestAllBuildsPage(SeleniumTestCase): but should be shown for other builds """ build1 = Build.objects.create(**self.project1_build_success) - default_build = Build.objects.create(**self.default_project_build_success) + default_build = Build.objects.create( + **self.default_project_build_success) url = reverse('all-builds') self.get(url) @@ -146,7 +210,6 @@ class TestAllBuildsPage(SeleniumTestCase): self.assertEqual(len(run_again_button), 0, 'should not see a rebuild button for cli builds') - def test_tooltips_on_project_name(self): """ Test tooltips shown next to project name in the main table @@ -188,14 +251,7 @@ class TestAllBuildsPage(SeleniumTestCase): recent builds area; failed builds should not have links on the time column, or in the recent builds area """ - build1 = Build.objects.create(**self.project1_build_success) - build2 = Build.objects.create(**self.project1_build_failure) - - # add some targets to these builds so they have recipe links - # (and so we can find the row in the ToasterTable corresponding to - # a particular build) - Target.objects.create(build=build1, target='foo') - Target.objects.create(build=build2, target='bar') + build1, build2 = self._get_create_builds() url = reverse('all-builds') self.get(url) @@ -223,3 +279,185 @@ class TestAllBuildsPage(SeleniumTestCase): links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a') msg = 'should not be a link on the build time for a failed build' self.assertEquals(len(links), 0, msg) + + def test_builds_table_search_box(self): + """ Test the search box in the builds table on the all builds page """ + self._get_create_builds() + + url = reverse('all-builds') + self.get(url) + + # Check search box is present and works + self.wait_until_present('#allbuildstable tbody tr') + search_box = self.find('#search-input-allbuildstable') + self.assertTrue(search_box.is_displayed()) + + # Check that we can search for a build by recipe name + search_box.send_keys('foo') + search_btn = self.find('#search-submit-allbuildstable') + search_btn.click() + self.wait_until_present('#allbuildstable tbody tr') + rows = self.find_all('#allbuildstable tbody tr') + self.assertTrue(len(rows) >= 1) + + def test_filtering_on_failure_tasks_column(self): + """ Test the filtering on failure tasks column in the builds table on the all builds page """ + self._get_create_builds(success=10, failure=10) + + url = reverse('all-builds') + self.get(url) + + # Check filtering on failure tasks column + self.wait_until_present('#allbuildstable tbody tr') + failed_tasks_filter = self.find('#failed_tasks_filter') + failed_tasks_filter.click() + # Check popup is visible + time.sleep(1) + self.wait_until_present('#filter-modal-allbuildstable') + self.assertTrue( + self.find('#filter-modal-allbuildstable').is_displayed()) + # Check that we can filter by failure tasks + build_without_failure_tasks = self.find( + '#failed_tasks_filter\\:without_failed_tasks') + build_without_failure_tasks.click() + # click on apply button + self.find('#filter-modal-allbuildstable .btn-primary').click() + self.wait_until_present('#allbuildstable tbody tr') + # Check if filter is applied, by checking if failed_tasks_filter has btn-primary class + self.assertTrue(self.find('#failed_tasks_filter').get_attribute( + 'class').find('btn-primary') != -1) + + def test_filtering_on_completedOn_column(self): + """ Test the filtering on completed_on column in the builds table on the all builds page """ + self._get_create_builds(success=10, failure=10) + + url = reverse('all-builds') + self.get(url) + + # Check filtering on failure tasks column + self.wait_until_present('#allbuildstable tbody tr') + completed_on_filter = self.find('#completed_on_filter') + completed_on_filter.click() + # Check popup is visible + time.sleep(1) + self.wait_until_present('#filter-modal-allbuildstable') + self.assertTrue( + self.find('#filter-modal-allbuildstable').is_displayed()) + # Check that we can filter by failure tasks + build_without_failure_tasks = self.find( + '#completed_on_filter\\:date_range') + build_without_failure_tasks.click() + # click on apply button + self.find('#filter-modal-allbuildstable .btn-primary').click() + self.wait_until_present('#allbuildstable tbody tr') + # Check if filter is applied, by checking if completed_on_filter has btn-primary class + self.assertTrue(self.find('#completed_on_filter').get_attribute( + 'class').find('btn-primary') != -1) + + # Filter by date range + self.find('#completed_on_filter').click() + self.wait_until_present('#filter-modal-allbuildstable') + date_ranges = self.driver.find_elements( + By.XPATH, '//input[@class="form-control hasDatepicker"]') + today = timezone.now() + yestersday = today - timezone.timedelta(days=1) + time.sleep(1) + date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d')) + date_ranges[1].send_keys(today.strftime('%Y-%m-%d')) + self.find('#filter-modal-allbuildstable .btn-primary').click() + self.wait_until_present('#allbuildstable tbody tr') + self.assertTrue(self.find('#completed_on_filter').get_attribute( + 'class').find('btn-primary') != -1) + # Check if filter is applied, number of builds displayed should be 6 + time.sleep(1) + self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) == 6) + + def test_builds_table_editColumn(self): + """ Test the edit column feature in the builds table on the all builds page """ + self._get_create_builds(success=10, failure=10) + + def test_edit_column(check_box_id): + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + url = reverse('all-builds') + self.get(url) + self.wait_until_present('#allbuildstable tbody tr') + + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + + # Check that we can hide the edit column + test_edit_column('checkbox-errors_no') + test_edit_column('checkbox-failed_tasks') + test_edit_column('checkbox-image_files') + test_edit_column('checkbox-project') + test_edit_column('checkbox-started_on') + test_edit_column('checkbox-time') + test_edit_column('checkbox-warnings_no') + + def test_builds_table_show_rows(self): + """ Test the show rows feature in the builds table on the all builds page """ + self._get_create_builds(success=100, failure=100) + + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_present('#allbuildstable tbody tr') + time.sleep(1) + self.assertTrue( + len(self.find_all('#allbuildstable tbody tr')) == row_to_show + ) + + url = reverse('all-builds') + self.get(url) + self.wait_until_present('#allbuildstable tbody tr') + + show_rows = self.driver.find_elements( + By.XPATH, + '//select[@class="form-control pagesize-allbuildstable"]' + ) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + test_show_rows(10, show_row_link) + test_show_rows(25, show_row_link) + test_show_rows(50, show_row_link) + test_show_rows(100, show_row_link) + test_show_rows(150, show_row_link) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py b/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py index 3389d32366..a880dbcc68 100644 --- a/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py +++ b/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py @@ -8,9 +8,11 @@ # import re +import time from django.urls import reverse from django.utils import timezone +from selenium.webdriver.support.select import Select from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import BitbakeVersion, Release, Project, Build @@ -37,6 +39,17 @@ class TestAllProjectsPage(SeleniumTestCase): self.release = None + def _create_projects(self, nb_project=10): + projects = [] + for i in range(1, nb_project + 1): + projects.append( + Project( + name='test project {}'.format(i), + release=self.release, + ) + ) + Project.objects.bulk_create(projects) + def _add_build_to_default_project(self): """ Add a build to the default project (not used in all tests) """ now = timezone.now() @@ -205,3 +218,116 @@ class TestAllProjectsPage(SeleniumTestCase): expected_url = reverse('project', args=(self.project.id,)) msg = 'link on project name should point to configuration but was %s' % link_url self.assertTrue(link_url.endswith(expected_url), msg) + + def test_allProject_table_search_box(self): + """ Test the search box in the all project table on the all projects page """ + self._create_projects() + + url = reverse('all-projects') + self.get(url) + + # Chseck search box is present and works + self.wait_until_present('#projectstable tbody tr') + search_box = self.find('#search-input-projectstable') + self.assertTrue(search_box.is_displayed()) + + # Check that we can search for a project by project name + search_box.send_keys('test project 10') + search_btn = self.find('#search-submit-projectstable') + search_btn.click() + self.wait_until_present('#projectstable tbody tr') + time.sleep(1) + rows = self.find_all('#projectstable tbody tr') + self.assertTrue(len(rows) == 1) + + def test_allProject_table_editColumn(self): + """ Test the edit column feature in the projects table on the all projects page """ + self._create_projects() + + def test_edit_column(check_box_id): + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + url = reverse('all-projects') + self.get(url) + self.wait_until_present('#projectstable tbody tr') + + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + + # Check that we can hide the edit column + test_edit_column('checkbox-errors') + test_edit_column('checkbox-image_files') + test_edit_column('checkbox-last_build_outcome') + test_edit_column('checkbox-recipe_name') + test_edit_column('checkbox-warnings') + + def test_allProject_table_show_rows(self): + """ Test the show rows feature in the projects table on the all projects page """ + self._create_projects(nb_project=200) + + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_present('#projectstable tbody tr') + sleep_time = 1 + if row_to_show == 150: + # wait more time for 150 rows + sleep_time = 2 + time.sleep(sleep_time) + self.assertTrue( + len(self.find_all('#projectstable tbody tr')) == row_to_show + ) + + url = reverse('all-projects') + self.get(url) + self.wait_until_present('#projectstable tbody tr') + + show_rows = self.driver.find_elements( + By.XPATH, + '//select[@class="form-control pagesize-projectstable"]' + ) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + test_show_rows(10, show_row_link) + test_show_rows(25, show_row_link) + test_show_rows(50, show_row_link) + test_show_rows(100, show_row_link) + test_show_rows(150, show_row_link) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_delete_project.py b/poky/bitbake/lib/toaster/tests/browser/test_delete_project.py new file mode 100644 index 0000000000..1941777ccc --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_delete_project.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux Inc +# +# SPDX-License-Identifier: GPL-2.0-only + +import pytest +from django.urls import reverse +from selenium.webdriver.support.ui import Select +from tests.browser.selenium_helpers import SeleniumTestCase +from orm.models import BitbakeVersion, Project, Release +from selenium.webdriver.common.by import By + +class TestDeleteProject(SeleniumTestCase): + + def setUp(self): + bitbake, _ = BitbakeVersion.objects.get_or_create( + name="master", + giturl="git://master", + branch="master", + dirpath="master") + + self.release, _ = Release.objects.get_or_create( + name="master", + description="Yocto Project master", + branch_name="master", + helptext="latest", + bitbake_version=bitbake) + + Release.objects.get_or_create( + name="foo", + description="Yocto Project foo", + branch_name="foo", + helptext="latest", + bitbake_version=bitbake) + + @pytest.mark.django_db + def test_delete_project(self): + """ Test delete a project + - Check delete modal is visible + - Check delete modal has right text + - Confirm delete + - Check project is deleted + """ + project_name = "project_to_delete" + url = reverse('newproject') + self.get(url) + self.enter_text('#new-project-name', project_name) + select = Select(self.find('#projectversion')) + select.select_by_value(str(self.release.pk)) + self.click("#create-project-button") + # We should get redirected to the new project's page with the + # notification at the top + element = self.wait_until_visible('#project-created-notification') + self.assertTrue(project_name in element.text, + "New project name not in new project notification") + self.assertTrue(Project.objects.filter(name=project_name).count(), + "New project not found in database") + + # Delete project + delete_project_link = self.driver.find_element( + By.XPATH, '//a[@href="#delete-project-modal"]') + delete_project_link.click() + + # Check delete modal is visible + self.wait_until_visible('#delete-project-modal') + + # Check delete modal has right text + modal_header_text = self.find('#delete-project-modal .modal-header').text + self.assertTrue( + "Are you sure you want to delete this project?" in modal_header_text, + "Delete project modal header text is wrong") + + modal_body_text = self.find('#delete-project-modal .modal-body').text + self.assertTrue( + "Cancel its builds currently in progress" in modal_body_text, + "Modal body doesn't contain: Cancel its builds currently in progress") + self.assertTrue( + "Remove its configuration information" in modal_body_text, + "Modal body doesn't contain: Remove its configuration information") + self.assertTrue( + "Remove its imported layers" in modal_body_text, + "Modal body doesn't contain: Remove its imported layers") + self.assertTrue( + "Remove its custom images" in modal_body_text, + "Modal body doesn't contain: Remove its custom images") + self.assertTrue( + "Remove all its build information" in modal_body_text, + "Modal body doesn't contain: Remove all its build information") + + # Confirm delete + delete_btn = self.find('#delete-project-confirmed') + delete_btn.click() + + # Check project is deleted + self.wait_until_visible('#change-notification') + delete_notification = self.find('#change-notification-msg') + self.assertTrue("You have deleted 1 project:" in delete_notification.text) + self.assertTrue(project_name in delete_notification.text) + self.assertFalse(Project.objects.filter(name=project_name).exists(), + "Project not deleted from database") diff --git a/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py b/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py index 8bb64b9f3e..7ec52a4b40 100644 --- a/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py +++ b/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py @@ -10,8 +10,9 @@ from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase +from selenium.webdriver.common.by import By -from orm.models import Project, Build +from orm.models import Layer, Layer_Version, Project, Build class TestLandingPage(SeleniumTestCase): """ Tests for redirects on the landing page """ @@ -29,6 +30,124 @@ class TestLandingPage(SeleniumTestCase): self.project.is_default = True self.project.save() + def test_icon_info_visible_and_clickable(self): + """ Test that the information icon is visible and clickable """ + self.get(reverse('landing')) + info_sign = self.find('#toaster-version-info-sign') + + # check that the info sign is visible + self.assertTrue(info_sign.is_displayed()) + + # check that the info sign is clickable + # and info modal is appearing when clicking on the info sign + info_sign.click() # click on the info sign make attribute 'aria-describedby' visible + info_model_id = info_sign.get_attribute('aria-describedby') + info_modal = self.find(f'#{info_model_id}') + self.assertTrue(info_modal.is_displayed()) + self.assertTrue("Toaster version information" in info_modal.text) + + def test_documentation_link_displayed(self): + """ Test that the documentation link is displayed """ + self.get(reverse('landing')) + documentation_link = self.find('#navbar-docs > a') + + # check that the documentation link is visible + self.assertTrue(documentation_link.is_displayed()) + + # check browser open new tab toaster manual when clicking on the documentation link + self.assertEqual(documentation_link.get_attribute('target') , '_blank') + self.assertEqual( + documentation_link.get_attribute('href'), + 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual') + self.assertTrue("Documentation" in documentation_link.text) + + def test_openembedded_jumbotron_link_visible_and_clickable(self): + """ Test OpenEmbedded link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check OpenEmbedded + openembedded = jumbotron.find_element(By.LINK_TEXT, 'OpenEmbedded') + self.assertTrue(openembedded.is_displayed()) + openembedded.click() + self.assertTrue("openembedded.org" in self.driver.current_url) + + def test_bitbake_jumbotron_link_visible_and_clickable(self): + """ Test BitBake link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check BitBake + bitbake = jumbotron.find_element(By.LINK_TEXT, 'BitBake') + self.assertTrue(bitbake.is_displayed()) + bitbake.click() + self.assertTrue("docs.yoctoproject.org/bitbake.html" in self.driver.current_url) + + def test_yoctoproject_jumbotron_link_visible_and_clickable(self): + """ Test Yocto Project link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Yocto Project + yoctoproject = jumbotron.find_element(By.LINK_TEXT, 'Yocto Project') + self.assertTrue(yoctoproject.is_displayed()) + yoctoproject.click() + self.assertTrue("yoctoproject.org" in self.driver.current_url) + + def test_link_setup_using_toaster_visible_and_clickable(self): + """ Test big magenta button setting up and using toaster link in jumbotron + if visible and clickable + """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Big magenta button + big_magenta_button = jumbotron.find_element(By.LINK_TEXT, + 'Toaster is ready to capture your command line builds' + ) + self.assertTrue(big_magenta_button.is_displayed()) + big_magenta_button.click() + self.assertTrue("docs.yoctoproject.org/toaster-manual/setup-and-use.html#setting-up-and-using-toaster" in self.driver.current_url) + + def test_link_create_new_project_in_jumbotron_visible_and_clickable(self): + """ Test big blue button create new project jumbotron if visible and clickable """ + # Create a layer and a layer version to make visible the big blue button + layer = Layer.objects.create(name='bar') + Layer_Version.objects.create(layer=layer) + + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Big Blue button + big_blue_button = jumbotron.find_element(By.LINK_TEXT, + 'Create your first Toaster project to run manage builds' + ) + self.assertTrue(big_blue_button.is_displayed()) + big_blue_button.click() + self.assertTrue("toastergui/newproject/" in self.driver.current_url) + + def test_toaster_manual_link_visible_and_clickable(self): + """ Test Read the Toaster manual link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Read the Toaster manual + toaster_manual = jumbotron.find_element(By.LINK_TEXT, 'Read the Toaster manual') + self.assertTrue(toaster_manual.is_displayed()) + toaster_manual.click() + self.assertTrue("https://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual" in self.driver.current_url) + + def test_contrib_to_toaster_link_visible_and_clickable(self): + """ Test Contribute to Toaster link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Contribute to Toaster + contribute_to_toaster = jumbotron.find_element(By.LINK_TEXT, 'Contribute to Toaster') + self.assertTrue(contribute_to_toaster.is_displayed()) + contribute_to_toaster.click() + self.assertTrue("wiki.yoctoproject.org/wiki/contribute_to_toaster" in str(self.driver.current_url).lower()) + def test_only_default_project(self): """ No projects except default diff --git a/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py b/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py index 71bdd2aafd..cb7b915bf0 100644 --- a/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py +++ b/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py @@ -97,6 +97,8 @@ class TestLayerDetailsPage(SeleniumTestCase): "Expecting any of \"%s\"but got \"%s\"" % (self.initial_values, value)) + # Make sure the input visible beofre sending keys + self.wait_until_visible("#layer-git input[type=text]") inputs.send_keys("-edited") # Save the new values diff --git a/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py b/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py index a34a092884..949a94768a 100644 --- a/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py +++ b/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py @@ -54,6 +54,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): build.outcome = Build.IN_PROGRESS build.recipes_to_parse = recipes_to_parse build.recipes_parsed = 0 + build.save() build_request.state = BuildRequest.REQ_INPROGRESS build_request.save() @@ -100,7 +101,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): 'Tasks starting', 'build should show "tasks starting" status') # first task finished; check tasks progress bar - task1.order = 1 + task1.outcome = Task.OUTCOME_SUCCESS task1.save() self.get(url) @@ -117,7 +118,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): element = Wait(self.driver).until(task_bar_updated, msg) # last task finished; check tasks progress bar updates - task2.order = 2 + task2.outcome = Task.OUTCOME_SUCCESS task2.save() self.get(url) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py b/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py index 6361f40347..34d1bd45c7 100644 --- a/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py +++ b/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py @@ -48,8 +48,12 @@ class TestNewCustomImagePage(SeleniumTestCase): self.recipe = Recipe.objects.create( name='core-image-minimal', layer_version=layer_version, + file_path='/tmp/core-image-minimal.bb', is_image=True ) + # create a tmp file for the recipe + with open(self.recipe.file_path, 'w') as f: + f.write('foo') # another project with a custom image already in it project2 = Project.objects.create(name='whoop', release=release) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_sample.py b/poky/bitbake/lib/toaster/tests/browser/test_sample.py index b0067c21cd..73973778e4 100644 --- a/poky/bitbake/lib/toaster/tests/browser/test_sample.py +++ b/poky/bitbake/lib/toaster/tests/browser/test_sample.py @@ -27,3 +27,12 @@ class TestSample(SeleniumTestCase): self.get(url) brand_link = self.find('.toaster-navbar-brand a.brand') self.assertEqual(brand_link.text.strip(), 'Toaster') + + def test_no_builds_message(self): + """ Test that a message is shown when there are no builds """ + url = reverse('all-builds') + self.get(url) + div_msg = self.find('#empty-state-allbuildstable .alert-info') + + msg = 'Sorry - no data found' + self.assertEqual(div_msg.text, msg) diff --git a/poky/bitbake/lib/toaster/tests/builds/buildtest.py b/poky/bitbake/lib/toaster/tests/builds/buildtest.py index 13b51fb0d8..53cd7a9ffa 100644 --- a/poky/bitbake/lib/toaster/tests/builds/buildtest.py +++ b/poky/bitbake/lib/toaster/tests/builds/buildtest.py @@ -116,6 +116,15 @@ class BuildTest(unittest.TestCase): project = Project.objects.create_project(name=BuildTest.PROJECT_NAME, release=release) + passthrough_variable_names = ["SSTATE_DIR", "DL_DIR"] + for variable_name in passthrough_variable_names: + current_variable = os.environ.get(variable_name) + if current_variable: + ProjectVariable.objects.get_or_create( + name=variable_name, + value=current_variable, + project=project) + if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"): ProjectVariable.objects.get_or_create( name="SSTATE_MIRRORS", diff --git a/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py b/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py index c3191f664a..b80d403bec 100644 --- a/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py +++ b/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py @@ -15,8 +15,6 @@ import time import re from tests.browser.selenium_helpers_base import SeleniumTestCaseBase -from tests.builds.buildtest import load_build_environment -from bldcontrol.models import BuildEnvironment from selenium.webdriver.common.by import By from selenium.common.exceptions import NoSuchElementException @@ -33,10 +31,6 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): raise RuntimeError("Please initialise django with the tests settings: " \ "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") - if BuildEnvironment.objects.count() == 0: - BuildEnvironment.objects.create(betype=BuildEnvironment.TYPE_LOCAL) - load_build_environment() - # start toaster cmd = "bash -c 'source toaster start'" p = subprocess.Popen( diff --git a/poky/bitbake/lib/toaster/tests/functional/test_create_new_project.py b/poky/bitbake/lib/toaster/tests/functional/test_create_new_project.py new file mode 100644 index 0000000000..dc7d1fc20b --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/functional/test_create_new_project.py @@ -0,0 +1,177 @@ +#! /usr/bin/env python3 +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only +# + +import re +import pytest +from django.urls import reverse +from selenium.webdriver.support.select import Select +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from orm.models import Project +from selenium.webdriver.common.by import By + + +@pytest.mark.django_db +class TestCreateNewProject(SeleniumFunctionalTestCase): + + def _create_test_new_project( + self, + project_name, + release, + release_title, + merge_toaster_settings, + ): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + self.get(reverse('newproject')) + self.driver.find_element(By.ID, + "new-project-name").send_keys(project_name) + + select = Select(self.find('#projectversion')) + select.select_by_value(release) + + # check merge toaster settings + checkbox = self.find('.checkbox-mergeattr') + if merge_toaster_settings: + if not checkbox.is_selected(): + checkbox.click() + else: + if checkbox.is_selected(): + checkbox.click() + + self.driver.find_element(By.ID, "create-project-button").click() + + element = self.wait_until_visible('#project-created-notification') + self.assertTrue( + self.element_exists('#project-created-notification'), + f"Project:{project_name} creation notification not shown" + ) + self.assertTrue( + project_name in element.text, + f"New project name:{project_name} not in new project notification" + ) + self.assertTrue( + Project.objects.filter(name=project_name).count(), + f"New project:{project_name} not found in database" + ) + + # check release + self.assertTrue(re.search( + release_title, + self.driver.find_element(By.XPATH, + "//span[@id='project-release-title']" + ).text), + 'The project release is not defined') + + def test_create_new_project_master(self): + """ Test create new project using: + - Project Name: Any string + - Release: Yocto Project master (option value: 3) + - Merge Toaster settings: False + """ + release = '3' + release_title = 'Yocto Project master' + project_name = 'projectmaster' + self._create_test_new_project( + project_name, + release, + release_title, + False, + ) + + def test_create_new_project_kirkstone(self): + """ Test create new project using: + - Project Name: Any string + - Release: Yocto Project 4.0 "Kirkstone" (option value: 1) + - Merge Toaster settings: True + """ + release = '1' + release_title = 'Yocto Project 4.0 "Kirkstone"' + project_name = 'projectkirkstone' + self._create_test_new_project( + project_name, + release, + release_title, + True, + ) + + def test_create_new_project_dunfell(self): + """ Test create new project using: + - Project Name: Any string + - Release: Yocto Project 3.1 "Dunfell" (option value: 5) + - Merge Toaster settings: False + """ + release = '5' + release_title = 'Yocto Project 3.1 "Dunfell"' + project_name = 'projectdunfull' + self._create_test_new_project( + project_name, + release, + release_title, + False, + ) + + def test_create_new_project_local(self): + """ Test create new project using: + - Project Name: Any string + - Release: Local Yocto Project (option value: 2) + - Merge Toaster settings: True + """ + release = '2' + release_title = 'Local Yocto Project' + project_name = 'projectlocal' + self._create_test_new_project( + project_name, + release, + release_title, + True, + ) + + def test_create_new_project_without_name(self): + """ Test create new project without project name """ + self.get(reverse('newproject')) + + select = Select(self.find('#projectversion')) + select.select_by_value(str(3)) + + # Check input name has required attribute + input_name = self.driver.find_element(By.ID, "new-project-name") + self.assertIsNotNone(input_name.get_attribute('required'), + 'Input name has not required attribute') + + # Check create button is disabled + create_btn = self.driver.find_element(By.ID, "create-project-button") + self.assertIsNotNone(create_btn.get_attribute('disabled'), + 'Create button is not disabled') + + def test_import_new_project(self): + """ Test import new project using: + - Project Name: Any string + - Project type: select (Import command line project) + - Import existing project directory: Wrong Path + """ + project_name = 'projectimport' + self.get(reverse('newproject')) + self.driver.find_element(By.ID, + "new-project-name").send_keys(project_name) + # select import project + self.find('#type-import').click() + + # set wrong path + wrong_path = '/wrongpath' + self.driver.find_element(By.ID, + "import-project-dir").send_keys(wrong_path) + self.driver.find_element(By.ID, "create-project-button").click() + + # check error message + self.assertTrue(self.element_exists('.alert-danger'), + 'Allert message not shown') + self.assertTrue(wrong_path in self.find('.alert-danger').text, + "Wrong path not in alert message") diff --git a/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py b/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py index 067ad99a9c..f558cce884 100644 --- a/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py +++ b/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py @@ -7,21 +7,26 @@ # SPDX-License-Identifier: GPL-2.0-only # -import re +import re, time +from django.urls import reverse +import pytest from tests.functional.functional_helpers import SeleniumFunctionalTestCase from orm.models import Project from selenium.webdriver.common.by import By + +@pytest.mark.order("last") class FuntionalTestBasic(SeleniumFunctionalTestCase): # testcase (1514) + @pytest.mark.django_db def test_create_slenium_project(self): project_name = 'selenium-project' - self.get('') - self.driver.find_element(By.LINK_TEXT, "To start building, create your first Toaster project").click() + self.get(reverse('newproject')) self.driver.find_element(By.ID, "new-project-name").send_keys(project_name) self.driver.find_element(By.ID, 'projectversion').click() self.driver.find_element(By.ID, "create-project-button").click() + time.sleep(2) element = self.wait_until_visible('#project-created-notification') self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown') self.assertTrue(project_name in element.text, @@ -31,15 +36,18 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase): # testcase (1515) def test_verify_left_bar_menu(self): - self.get('') + self.get(reverse('all-projects')) self.wait_until_visible('#projectstable') self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + time.sleep(2) self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist') project_URL=self.get_URL() self.driver.find_element(By.XPATH, '//a[@href="'+project_URL+'"]').click() + time.sleep(2) try: self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click() + time.sleep(2) 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') except: self.fail(msg='No Custom images tab available') @@ -78,14 +86,16 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase): def test_review_configuration_information(self): self.get('') self.driver.find_element(By.XPATH, "//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + time.sleep(2) self.wait_until_visible('#projectstable') self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() project_URL=self.get_URL() - + time.sleep(2) try: self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') self.assertTrue(re.search("qemux86",self.driver.find_element(By.XPATH, "//span[@id='project-machine-name']").text),'The machine type is not assigned') self.driver.find_element(By.XPATH, "//span[@id='change-machine-toggle']").click() + time.sleep(2) self.wait_until_visible('#select-machine-form') self.wait_until_visible('#cancel-machine-change') self.driver.find_element(By.XPATH, "//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click() @@ -123,13 +133,16 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase): def test_verify_machine_information(self): self.get('') self.driver.find_element(By.XPATH, "//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + time.sleep(2) self.wait_until_visible('#projectstable') self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + time.sleep(2) try: self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') self.assertTrue(re.search("qemux86",self.driver.find_element(By.ID, "project-machine-name").text),'The machine type is not assigned') self.driver.find_element(By.ID, "change-machine-toggle").click() + time.sleep(2) self.wait_until_visible('#select-machine-form') self.wait_until_visible('#cancel-machine-change') self.driver.find_element(By.ID, "cancel-machine-change").click() @@ -140,14 +153,15 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase): def test_verify_most_built_recipes_information(self): self.get('') self.driver.find_element(By.XPATH, "//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() - + time.sleep(2) self.wait_until_visible('#projectstable') self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() project_URL=self.get_URL() - + time.sleep(2) try: 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') self.driver.find_element(By.XPATH, "//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click() + time.sleep(2) 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') except: self.fail(msg='No Most built information in project detail page') @@ -156,8 +170,10 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase): def test_verify_project_release_information(self): self.get('') self.driver.find_element(By.XPATH, "//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + time.sleep(2) self.wait_until_visible('#projectstable') self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + time.sleep(2) try: self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.ID, "project-release-title").text),'The project release is not defined') @@ -171,12 +187,12 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase): self.wait_until_visible('#projectstable') self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() project_URL=self.get_URL() - + time.sleep(2) try: self.driver.find_element(By.XPATH, "//div[@id='layer-container']") 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') layer_list = self.driver.find_element(By.ID, "layers-in-project-list") - layers = layer_list.find_element(By.TAG_NAME, "li") + layers = layer_list.find_elements(By.TAG_NAME, "li") for layer in layers: if re.match ("openembedded-core",layer.text): @@ -199,10 +215,11 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase): def test_verify_project_detail_links(self): self.get('') self.driver.find_element(By.XPATH, "//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + time.sleep(2) self.wait_until_visible('#projectstable') self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() project_URL=self.get_URL() - + time.sleep(2) 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() 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') diff --git a/poky/bitbake/lib/toaster/tests/functional/test_project_page.py b/poky/bitbake/lib/toaster/tests/functional/test_project_page.py new file mode 100644 index 0000000000..3edf967a2c --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/functional/test_project_page.py @@ -0,0 +1,247 @@ +#! /usr/bin/env python3 # +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only +# + +import pytest +from django.urls import reverse +from selenium.webdriver.support.select import Select +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from selenium.webdriver.common.by import By + + +@pytest.mark.django_db +class TestProjectPage(SeleniumFunctionalTestCase): + + def setUp(self): + super().setUp() + release = '3' + project_name = 'projectmaster' + self._create_test_new_project( + project_name, + release, + False, + ) + + def _create_test_new_project( + self, + project_name, + release, + merge_toaster_settings, + ): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + self.get(reverse('newproject')) + self.driver.find_element(By.ID, + "new-project-name").send_keys(project_name) + + select = Select(self.find('#projectversion')) + select.select_by_value(release) + + # check merge toaster settings + checkbox = self.find('.checkbox-mergeattr') + if merge_toaster_settings: + if not checkbox.is_selected(): + checkbox.click() + else: + if checkbox.is_selected(): + checkbox.click() + + self.driver.find_element(By.ID, "create-project-button").click() + + def test_page_header_on_project_page(self): + """ Check page header in project page: + - AT LEFT -> Logo of Yocto project, displayed, clickable + - "Toaster"+" Information icon", displayed, clickable + - "Server Icon" + "All builds", displayed, clickable + - "Directory Icon" + "All projects", displayed, clickable + - "Book Icon" + "Documentation", displayed, clickable + - AT RIGHT -> button "New project", displayed, clickable + """ + # navigate to the project page + url = reverse("project", args=(1,)) + self.get(url) + + # check page header + # AT LEFT -> Logo of Yocto project + logo = self.driver.find_element( + By.XPATH, + "//div[@class='toaster-navbar-brand']", + ) + logo_img = logo.find_element(By.TAG_NAME, 'img') + self.assertTrue(logo_img.is_displayed(), + 'Logo of Yocto project not found') + self.assertTrue( + '/static/img/logo.png' in str(logo_img.get_attribute('src')), + 'Logo of Yocto project not found' + ) + # "Toaster"+" Information icon", clickable + toaster = self.driver.find_element( + By.XPATH, + "//div[@class='toaster-navbar-brand']//a[@class='brand']", + ) + self.assertTrue(toaster.is_displayed(), 'Toaster not found') + self.assertTrue(toaster.text == 'Toaster') + info_sign = self.find('.glyphicon-info-sign') + self.assertTrue(info_sign.is_displayed()) + + # "Server Icon" + "All builds" + all_builds = self.find('#navbar-all-builds') + all_builds_link = all_builds.find_element(By.TAG_NAME, 'a') + self.assertTrue("All builds" in all_builds_link.text) + self.assertTrue( + '/toastergui/builds/' in str(all_builds_link.get_attribute('href')) + ) + server_icon = all_builds.find_element(By.TAG_NAME, 'i') + self.assertTrue( + server_icon.get_attribute('class') == 'glyphicon glyphicon-tasks' + ) + self.assertTrue(server_icon.is_displayed()) + + # "Directory Icon" + "All projects" + all_projects = self.find('#navbar-all-projects') + all_projects_link = all_projects.find_element(By.TAG_NAME, 'a') + self.assertTrue("All projects" in all_projects_link.text) + self.assertTrue( + '/toastergui/projects/' in str(all_projects_link.get_attribute( + 'href')) + ) + dir_icon = all_projects.find_element(By.TAG_NAME, 'i') + self.assertTrue( + dir_icon.get_attribute('class') == 'icon-folder-open' + ) + self.assertTrue(dir_icon.is_displayed()) + + # "Book Icon" + "Documentation" + toaster_docs_link = self.find('#navbar-docs') + toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME, + 'a') + self.assertTrue("Documentation" in toaster_docs_link_link.text) + self.assertTrue( + toaster_docs_link_link.get_attribute('href') == 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual' + ) + book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i') + self.assertTrue( + book_icon.get_attribute('class') == 'glyphicon glyphicon-book' + ) + self.assertTrue(book_icon.is_displayed()) + + # AT RIGHT -> button "New project" + new_project_button = self.find('#new-project-button') + self.assertTrue(new_project_button.is_displayed()) + self.assertTrue(new_project_button.text == 'New project') + new_project_button.click() + self.assertTrue( + '/toastergui/newproject/' in str(self.driver.current_url) + ) + + def test_edit_project_name(self): + """ Test edit project name: + - Click on "Edit" icon button + - Change project name + - Click on "Save" button + - Check project name is changed + """ + # navigate to the project page + url = reverse("project", args=(1,)) + self.get(url) + + # click on "Edit" icon button + self.wait_until_visible('#project-name-container') + edit_button = self.find('#project-change-form-toggle') + edit_button.click() + project_name_input = self.find('#project-name-change-input') + self.assertTrue(project_name_input.is_displayed()) + project_name_input.clear() + project_name_input.send_keys('New Name') + self.find('#project-name-change-btn').click() + + # check project name is changed + self.wait_until_visible('#project-name-container') + self.assertTrue( + 'New Name' in str(self.find('#project-name-container').text) + ) + + def test_project_page_tabs(self): + """ Test project tabs: + - "configuration" tab + - "Builds" tab + - "Import layers" tab + - "New custom image" tab + Check search box used to build recipes + """ + # navigate to the project page + url = reverse("project", args=(1,)) + self.get(url) + + # check "configuration" tab + self.wait_until_visible('#topbar-configuration-tab') + config_tab = self.find('#topbar-configuration-tab') + self.assertTrue(config_tab.get_attribute('class') == 'active') + self.assertTrue('Configuration' in config_tab.text) + config_tab_link = config_tab.find_element(By.TAG_NAME, 'a') + self.assertTrue( + f"/toastergui/project/1" in str(config_tab_link.get_attribute( + 'href')) + ) + + def get_tabs(): + # tabs links list + return self.driver.find_elements( + By.XPATH, + '//div[@id="project-topbar"]//li' + ) + + def check_tab_link(tab_index, tab_name, url): + tab = get_tabs()[tab_index] + tab_link = tab.find_element(By.TAG_NAME, 'a') + self.assertTrue(url in tab_link.get_attribute('href')) + self.assertTrue(tab_name in tab_link.text) + self.assertTrue(tab.get_attribute('class') == 'active') + + # check "Builds" tab + builds_tab = get_tabs()[1] + builds_tab.find_element(By.TAG_NAME, 'a').click() + check_tab_link( + 1, + 'Builds', + f"/toastergui/project/1/builds" + ) + + # check "Import layers" tab + import_layers_tab = get_tabs()[2] + import_layers_tab.find_element(By.TAG_NAME, 'a').click() + check_tab_link( + 2, + 'Import layer', + f"/toastergui/project/1/importlayer" + ) + + # check "New custom image" tab + new_custom_image_tab = get_tabs()[3] + new_custom_image_tab.find_element(By.TAG_NAME, 'a').click() + check_tab_link( + 3, + 'New custom image', + f"/toastergui/project/1/newcustomimage" + ) + + # check search box can be use to build recipes + search_box = self.find('#build-input') + search_box.send_keys('core-image-minimal') + self.find('#build-button').click() + self.wait_until_visible('#latest-builds') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]', + ) + last_build = lastest_builds[0] + self.assertTrue( + 'core-image-minimal' in str(last_build.text) + ) diff --git a/poky/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py b/poky/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py new file mode 100644 index 0000000000..23012d7865 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py @@ -0,0 +1,578 @@ +#! /usr/bin/env python3 # +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only +# + +from time import sleep +import pytest +from django.utils import timezone +from django.urls import reverse +from selenium.webdriver import Keys +from selenium.webdriver.support.select import Select +from selenium.common.exceptions import NoSuchElementException +from orm.models import Build, Project, Target +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from selenium.webdriver.common.by import By + + +@pytest.mark.django_db +class TestProjectConfigTab(SeleniumFunctionalTestCase): + + def setUp(self): + self.recipe = None + super().setUp() + release = '3' + project_name = 'projectmaster' + self._create_test_new_project( + project_name, + release, + False, + ) + + def _create_test_new_project( + self, + project_name, + release, + merge_toaster_settings, + ): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + self.get(reverse('newproject')) + self.driver.find_element(By.ID, + "new-project-name").send_keys(project_name) + + select = Select(self.find('#projectversion')) + select.select_by_value(release) + + # check merge toaster settings + checkbox = self.find('.checkbox-mergeattr') + if merge_toaster_settings: + if not checkbox.is_selected(): + checkbox.click() + else: + if checkbox.is_selected(): + checkbox.click() + + self.driver.find_element(By.ID, "create-project-button").click() + + @classmethod + def _wait_until_build(cls, state): + while True: + try: + last_build_state = cls.driver.find_element( + By.XPATH, + '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]', + ) + build_state = last_build_state.get_attribute( + 'data-build-state') + state_text = state.lower().split() + if any(x in str(build_state).lower() for x in state_text): + break + except NoSuchElementException: + continue + sleep(1) + + def _create_builds(self): + # check search box can be use to build recipes + search_box = self.find('#build-input') + search_box.send_keys('core-image-minimal') + self.find('#build-button').click() + sleep(1) + self.wait_until_visible('#latest-builds') + # loop until reach the parsing state + self._wait_until_build('parsing starting cloning') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]/div', + ) + last_build = lastest_builds[0] + self.assertTrue( + 'core-image-minimal' in str(last_build.text) + ) + cancel_button = last_build.find_element( + By.XPATH, + '//span[@class="cancel-build-btn pull-right alert-link"]', + ) + cancel_button.click() + sleep(1) + self._wait_until_build('cancelled') + + def _get_tabs(self): + # tabs links list + return self.driver.find_elements( + By.XPATH, + '//div[@id="project-topbar"]//li' + ) + + def _get_config_nav_item(self, index): + config_nav = self.find('#config-nav') + return config_nav.find_elements(By.TAG_NAME, 'li')[index] + + def _get_create_builds(self, **kwargs): + """ Create a build and return the build object """ + # parameters for builds to associate with the projects + now = timezone.now() + release = '3' + project_name = 'projectmaster' + self._create_test_new_project( + project_name+"2", + release, + False, + ) + + self.project1_build_success = { + 'project': Project.objects.get(id=1), + 'started_on': now, + 'completed_on': now, + 'outcome': Build.SUCCEEDED + } + + self.project1_build_failure = { + 'project': Project.objects.get(id=1), + 'started_on': now, + 'completed_on': now, + 'outcome': Build.FAILED + } + build1 = Build.objects.create(**self.project1_build_success) + build2 = Build.objects.create(**self.project1_build_failure) + + # add some targets to these builds so they have recipe links + # (and so we can find the row in the ToasterTable corresponding to + # a particular build) + Target.objects.create(build=build1, target='foo') + Target.objects.create(build=build2, target='bar') + + if kwargs: + # Create kwargs.get('success') builds with success status with target + # and kwargs.get('failure') builds with failure status with target + for i in range(kwargs.get('success', 0)): + now = timezone.now() + self.project1_build_success['started_on'] = now + self.project1_build_success[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_success) + Target.objects.create(build=build, + target=f'{i}_success_recipe', + task=f'{i}_success_task') + + for i in range(kwargs.get('failure', 0)): + now = timezone.now() + self.project1_build_failure['started_on'] = now + self.project1_build_failure[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_failure) + Target.objects.create(build=build, + target=f'{i}_fail_recipe', + task=f'{i}_fail_task') + return build1, build2 + + def test_project_config_nav(self): + """ Test project config tab navigation: + - Check if the menu is displayed and contains the right elements: + - Configuration + - COMPATIBLE METADATA + - Custom images + - Image recipes + - Software recipes + - Machines + - Layers + - Distro + - EXTRA CONFIGURATION + - Bitbake variables + - Actions + - Delete project + """ + # navigate to the project page + url = reverse("project", args=(1,)) + self.get(url) + + # check if the menu is displayed + self.wait_until_visible('#config-nav') + + def _get_config_nav_item(index): + config_nav = self.find('#config-nav') + return config_nav.find_elements(By.TAG_NAME, 'li')[index] + + def check_config_nav_item(index, item_name, url): + item = _get_config_nav_item(index) + self.assertTrue(item_name in item.text) + self.assertTrue(item.get_attribute('class') == 'active') + self.assertTrue(url in self.driver.current_url) + + # check if the menu contains the right elements + # COMPATIBLE METADATA + compatible_metadata = _get_config_nav_item(1) + self.assertTrue( + "compatible metadata" in compatible_metadata.text.lower() + ) + # EXTRA CONFIGURATION + extra_configuration = _get_config_nav_item(8) + self.assertTrue( + "extra configuration" in extra_configuration.text.lower() + ) + # Actions + actions = _get_config_nav_item(10) + self.assertTrue("actions" in str(actions.text).lower()) + + conf_nav_list = [ + [0, 'Configuration', f"/toastergui/project/1"], # config + [2, 'Custom images', f"/toastergui/project/1/customimages"], # custom images + [3, 'Image recipes', f"/toastergui/project/1/images"], # image recipes + [4, 'Software recipes', f"/toastergui/project/1/softwarerecipes"], # software recipes + [5, 'Machines', f"/toastergui/project/1/machines"], # machines + [6, 'Layers', f"/toastergui/project/1/layers"], # layers + [7, 'Distro', f"/toastergui/project/1/distro"], # distro + [9, 'BitBake variables', f"/toastergui/project/1/configuration"], # bitbake variables + ] + for index, item_name, url in conf_nav_list: + item = _get_config_nav_item(index) + if item.get_attribute('class') != 'active': + item.click() + check_config_nav_item(index, item_name, url) + + def test_project_config_tab_right_section(self): + """ Test project config tab right section contains five blocks: + - Machine: + - check 'Machine' is displayed + - check can change Machine + - Distro: + - check 'Distro' is displayed + - check can change Distro + - Most built recipes: + - check 'Most built recipes' is displayed + - check can select a recipe and build it + - Project release: + - check 'Project release' is displayed + - check project has right release displayed + - Layers: + - check can add a layer if exists + - check at least three layers are displayed + - openembedded-core + - meta-poky + - meta-yocto-bsp + """ + # navigate to the project page + url = reverse("project", args=(1,)) + self.get(url) + + # check if the menu is displayed + self.wait_until_visible('#project-page') + block_l = self.driver.find_element( + By.XPATH, '//*[@id="project-page"]/div[2]') + machine = self.find('#machine-section') + distro = self.find('#distro-section') + most_built_recipes = self.driver.find_element( + By.XPATH, '//*[@id="project-page"]/div[1]/div[3]') + project_release = self.driver.find_element( + By.XPATH, '//*[@id="project-page"]/div[1]/div[4]') + layers = block_l.find_element(By.ID, 'layer-container') + + def check_machine_distro(self, item_name, new_item_name, block): + title = block.find_element(By.TAG_NAME, 'h3') + self.assertTrue(item_name.capitalize() in title.text) + edit_btn = block.find_element(By.ID, f'change-{item_name}-toggle') + edit_btn.click() + sleep(1) + name_input = block.find_element(By.ID, f'{item_name}-change-input') + name_input.clear() + name_input.send_keys(new_item_name) + change_btn = block.find_element(By.ID, f'{item_name}-change-btn') + change_btn.click() + sleep(1) + project_name = block.find_element(By.ID, f'project-{item_name}-name') + self.assertTrue(new_item_name in project_name.text) + # check change notificaiton is displayed + change_notification = self.find('#change-notification') + self.assertTrue( + f'You have changed the {item_name} to: {new_item_name}' in change_notification.text + ) + + # Machine + check_machine_distro(self, 'machine', 'qemux86-64', machine) + # Distro + check_machine_distro(self, 'distro', 'poky-altcfg', distro) + + # Project release + title = project_release.find_element(By.TAG_NAME, 'h3') + self.assertTrue("Project release" in title.text) + self.assertTrue( + "Yocto Project master" in self.find('#project-release-title').text + ) + + # Layers + title = layers.find_element(By.TAG_NAME, 'h3') + self.assertTrue("Layers" in title.text) + # check at least three layers are displayed + # openembedded-core + # meta-poky + # meta-yocto-bsp + layers_list = layers.find_element(By.ID, 'layers-in-project-list') + layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') + self.assertTrue(len(layers_list_items) == 3) + # check can add a layer if exists + add_layer_input = layers.find_element(By.ID, 'layer-add-input') + add_layer_input.send_keys('meta-oe') + self.wait_until_visible('#layer-container > form > div > span > div') + dropdown_item = self.driver.find_element( + By.XPATH, + '//*[@id="layer-container"]/form/div/span/div' + ) + dropdown_item.click() + add_layer_btn = layers.find_element(By.ID, 'add-layer-btn') + add_layer_btn.click() + sleep(1) + # check layer is added + layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') + self.assertTrue(len(layers_list_items) == 4) + + # Most built recipes + title = most_built_recipes.find_element(By.TAG_NAME, 'h3') + self.assertTrue("Most built recipes" in title.text) + # Create a new builds 5 + self._create_builds() + + # Refresh the page + self.get(url) + + sleep(1) # wait for page to load + self.wait_until_visible('#project-page') + # check can select a recipe and build it + most_built_recipes = self.driver.find_element( + By.XPATH, '//*[@id="project-page"]/div[1]/div[3]') + recipe_list = most_built_recipes.find_element(By.ID, 'freq-build-list') + recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li') + self.assertTrue( + len(recipe_list_items) > 0, + msg="No recipes found in the most built recipes list", + ) + checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input') + checkbox.click() + build_btn = self.find('#freq-build-btn') + build_btn.click() + sleep(1) # wait for page to load + self.wait_until_visible('#latest-builds') + self._wait_until_build('parsing starting cloning queueing') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]/div' + ) + last_build = lastest_builds[0] + cancel_button = last_build.find_element( + By.XPATH, + '//span[@class="cancel-build-btn pull-right alert-link"]', + ) + cancel_button.click() + self.assertTrue(len(lastest_builds) == 2) + + def test_project_page_tab_importlayer(self): + """ Test project page tab import layer """ + # navigate to the project page + url = reverse("project", args=(1,)) + self.get(url) + + # navigate to "Import layers" tab + import_layers_tab = self._get_tabs()[2] + import_layers_tab.find_element(By.TAG_NAME, 'a').click() + self.wait_until_visible('#layer-git-repo-url') + + # Check git repo radio button + git_repo_radio = self.find('#git-repo-radio') + git_repo_radio.click() + + # Set git repo url + input_repo_url = self.find('#layer-git-repo-url') + input_repo_url.send_keys('git://git.yoctoproject.org/meta-fake') + # Blur the input to trigger the validation + input_repo_url.send_keys(Keys.TAB) + + # Check name is set + input_layer_name = self.find('#import-layer-name') + self.assertTrue(input_layer_name.get_attribute('value') == 'meta-fake') + + # Set branch + input_branch = self.find('#layer-git-ref') + input_branch.send_keys('master') + + # Import layer + self.find('#import-and-add-btn').click() + + # Check layer is added + self.wait_until_visible('#layer-container') + block_l = self.driver.find_element( + By.XPATH, '//*[@id="project-page"]/div[2]') + layers = block_l.find_element(By.ID, 'layer-container') + layers_list = layers.find_element(By.ID, 'layers-in-project-list') + layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') + self.assertTrue( + 'meta-fake' in str(layers_list_items[-1].text) + ) + + def test_project_page_custom_image_no_image(self): + """ Test project page tab "New custom image" when no custom image """ + # navigate to the project page + url = reverse("project", args=(1,)) + self.get(url) + + # navigate to "Custom image" tab + custom_image_section = self._get_config_nav_item(2) + custom_image_section.click() + self.wait_until_visible('#empty-state-customimagestable') + + # Check message when no custom image + self.assertTrue( + "You have not created any custom images yet." in str( + self.find('#empty-state-customimagestable').text + ) + ) + div_empty_msg = self.find('#empty-state-customimagestable') + link_create_custom_image = div_empty_msg.find_element( + By.TAG_NAME, 'a') + self.assertTrue( + f"/toastergui/project/1/newcustomimage" in str( + link_create_custom_image.get_attribute('href') + ) + ) + self.assertTrue( + "Create your first custom image" in str( + link_create_custom_image.text + ) + ) + + def test_project_page_image_recipe(self): + """ Test project page section images + - Check image recipes are displayed + - Check search input + - Check image recipe build button works + - Check image recipe table features(show/hide column, pagination) + """ + # navigate to the project page + url = reverse("project", args=(1,)) + self.get(url) + self.wait_until_visible('#config-nav') + + # navigate to "Images section" + images_section = self._get_config_nav_item(3) + images_section.click() + self.wait_until_visible('#imagerecipestable') + rows = self.find_all('#imagerecipestable tbody tr') + self.assertTrue(len(rows) > 0) + + # Test search input + self.wait_until_visible('#search-input-imagerecipestable') + recipe_input = self.find('#search-input-imagerecipestable') + recipe_input.send_keys('core-image-minimal') + self.find('#search-submit-imagerecipestable').click() + self.wait_until_visible('#imagerecipestable tbody tr') + rows = self.find_all('#imagerecipestable tbody tr') + self.assertTrue(len(rows) > 0) + + # Test build button + image_to_build = rows[0] + build_btn = image_to_build.find_element( + By.XPATH, + '//td[@class="add-del-layers"]' + ) + build_btn.click() + self._wait_until_build('parsing starting cloning') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]/div' + ) + self.assertTrue(len(lastest_builds) > 0) + + def test_image_recipe_editColumn(self): + """ Test the edit column feature in image recipe table on project page """ + self._get_create_builds(success=10, failure=10) + + def test_edit_column(check_box_id): + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + + url = reverse('projectimagerecipes', args=(1,)) + self.get(url) + self.wait_until_present('#imagerecipestable tbody tr') + + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + + # Check that we can hide the edit column + test_edit_column('checkbox-get_description_or_summary') + test_edit_column('checkbox-layer_version__get_vcs_reference') + test_edit_column('checkbox-layer_version__layer__name') + test_edit_column('checkbox-license') + test_edit_column('checkbox-recipe-file') + test_edit_column('checkbox-section') + test_edit_column('checkbox-version') + + def test_image_recipe_show_rows(self): + """ Test the show rows feature in image recipe table on project page """ + self._get_create_builds(success=100, failure=100) + + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_present('#imagerecipestable tbody tr') + sleep(1) + self.assertTrue( + len(self.find_all('#imagerecipestable tbody tr')) == row_to_show + ) + + url = reverse('projectimagerecipes', args=(2,)) + self.get(url) + self.wait_until_present('#imagerecipestable tbody tr') + + show_rows = self.driver.find_elements( + By.XPATH, + '//select[@class="form-control pagesize-imagerecipestable"]' + ) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + test_show_rows(10, show_row_link) + test_show_rows(25, show_row_link) + test_show_rows(50, show_row_link) + test_show_rows(100, show_row_link) + test_show_rows(150, show_row_link) diff --git a/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt b/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt index f30ac0706c..71cc083436 100644 --- a/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt +++ b/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt @@ -1 +1,7 @@ selenium>=4.13.0 +pytest==7.4.2 +pytest-django==4.5.2 +pytest-env==1.1.0 +pytest-html==4.0.2 +pytest-metadata==3.0.0 +pytest-order==1.1.0 diff --git a/poky/bitbake/lib/toaster/tests/views/test_views.py b/poky/bitbake/lib/toaster/tests/views/test_views.py index f962e76287..349881ebf6 100644 --- a/poky/bitbake/lib/toaster/tests/views/test_views.py +++ b/poky/bitbake/lib/toaster/tests/views/test_views.py @@ -9,6 +9,7 @@ """Test cases for Toaster GUI and ReST.""" +import pytest from django.test import TestCase from django.test.client import RequestFactory from django.urls import reverse @@ -33,6 +34,7 @@ PROJECT_NAME2 = "test project 2" CLI_BUILDS_PROJECT_NAME = 'Command line builds' +@pytest.mark.order(1) class ViewTests(TestCase): """Tests to verify view APIs.""" @@ -41,7 +43,15 @@ class ViewTests(TestCase): def setUp(self): self.project = Project.objects.first() + self.recipe1 = Recipe.objects.get(pk=2) + # create a file and to recipe1 file_path + file_path = f"/tmp/{self.recipe1.name.strip().replace(' ', '-')}.bb" + with open(file_path, 'w') as f: + f.write('foo') + self.recipe1.file_path = file_path + self.recipe1.save() + self.customr = CustomImageRecipe.objects.first() self.cust_package = CustomImagePackage.objects.first() self.package = Package.objects.first() |