From patchwork Fri Dec 8 01:53:48 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Alassane Yattara X-Patchwork-Id: 35892 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 7090FC46CA3 for ; Fri, 8 Dec 2023 01:54:09 +0000 (UTC) Received: from mail.savoirfairelinux.com (mail.savoirfairelinux.com [208.88.110.44]) by mx.groups.io with SMTP id smtpd.web11.10348.1702000439975487666 for ; Thu, 07 Dec 2023 17:54:00 -0800 Authentication-Results: mx.groups.io; dkim=pass header.i=@savoirfairelinux.com header.s=DFC430D2-D198-11EC-948E-34200CB392D2 header.b=Qion+QFA; spf=pass (domain: savoirfairelinux.com, ip: 208.88.110.44, mailfrom: alassane.yattara@savoirfairelinux.com) Received: from localhost (localhost [127.0.0.1]) by mail.savoirfairelinux.com (Postfix) with ESMTP id 58EFA9C3472 for ; Thu, 7 Dec 2023 20:53:59 -0500 (EST) Received: from mail.savoirfairelinux.com ([127.0.0.1]) by localhost (mail.savoirfairelinux.com [127.0.0.1]) (amavis, port 10032) with ESMTP id LSTVfyUjUo5l; Thu, 7 Dec 2023 20:53:57 -0500 (EST) Received: from localhost (localhost [127.0.0.1]) by mail.savoirfairelinux.com (Postfix) with ESMTP id E2F669C33BA; Thu, 7 Dec 2023 20:53:56 -0500 (EST) DKIM-Filter: OpenDKIM Filter v2.10.3 mail.savoirfairelinux.com E2F669C33BA DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=savoirfairelinux.com; s=DFC430D2-D198-11EC-948E-34200CB392D2; t=1702000436; bh=pD2TC+aByrxAp1AwQjbm4/ivgOg7xUgsyRvZN6Wb2Sw=; h=From:To:Date:Message-Id:MIME-Version; b=Qion+QFAcfqSnMyNp9Lcp4JEpiwtEvRmaeQV7kZFmUNlwUyseJlNxDtQAy6/sdGUQ Odxu7gzlA+txyODMpfWXrt0nlGWraeq2pDkYPdEECp0G4UDIoTcPgXtWabYMYYwCmp sscrMIKBspx6J62ZZvOsDFrqGHG/xJH/JFxZs9IUPjK1BFZhqVZsxVLp7xXACdINOx gLzxHKehD6B/xFmi3Ca/sz+8uko6OIyc7gXQTueIH2Qv72OqTAKNZDn+PRHKYen1dY rEJsTYZmfdwCYJVC5068wjuScYglZCNEIulnhnEd05yDBB1/TzZrerSW7E8vwo7t2I mHl7YglGa09aA== X-Virus-Scanned: amavis at mail.savoirfairelinux.com Received: from mail.savoirfairelinux.com ([127.0.0.1]) by localhost (mail.savoirfairelinux.com [127.0.0.1]) (amavis, port 10026) with ESMTP id 8H6M96EAkhnq; Thu, 7 Dec 2023 20:53:56 -0500 (EST) Received: from jedi.. (unknown [196.127.183.75]) by mail.savoirfairelinux.com (Postfix) with ESMTPSA id CABE99C33E2; Thu, 7 Dec 2023 20:53:55 -0500 (EST) From: Alassane Yattara To: toaster@lists.yoctoproject.org Cc: Alassane Yattara Subject: [PATCH 3/4] toaster/test: Refactorize tests/functional Date: Fri, 8 Dec 2023 02:53:48 +0100 Message-Id: <20231208015349.678997-3-alassane.yattara@savoirfairelinux.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20231208015349.678997-1-alassane.yattara@savoirfairelinux.com> References: <20231208015349.678997-1-alassane.yattara@savoirfairelinux.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 08 Dec 2023 01:54:09 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/toaster/message/6070 - Split testcases from test_project_page_tab_config into tow files - Added new testcases in test_project_config - Test changing distro variable - Test setting IMAGE_INSTALL:append variable - Test setting PACKAGE_CLASSES variable - Test creating new bitbake variable Signed-off-by: Alassane Yattara --- .../tests/functional/test_project_config.py | 335 ++++++++++++ .../test_project_page_tab_config.py | 495 ++++++++---------- 2 files changed, 554 insertions(+), 276 deletions(-) create mode 100644 lib/toaster/tests/functional/test_project_config.py diff --git a/lib/toaster/tests/functional/test_project_config.py b/lib/toaster/tests/functional/test_project_config.py new file mode 100644 index 00000000..2d162d81 --- /dev/null +++ b/lib/toaster/tests/functional/test_project_config.py @@ -0,0 +1,335 @@ +#! /usr/bin/env python3 # +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only +# + +import string +import random +import pytest +from django.urls import reverse +from selenium.webdriver import Keys +from selenium.webdriver.support.select import Select +from selenium.common.exceptions import TimeoutException +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from selenium.webdriver.common.by import By + +from .utils import get_projectId_from_url + + +@pytest.mark.django_db +@pytest.mark.order("last") +class TestProjectConfig(SeleniumFunctionalTestCase): + project_id = None + PROJECT_NAME = 'TestProjectConfig' + INVALID_PATH_START_TEXT = 'The directory path should either start with a /' + INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \ + 'any of these characters' + + def _create_project(self, project_name): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + self.get(reverse('newproject')) + self.wait_until_visible('#new-project-name', poll=2) + self.find("#new-project-name").send_keys(project_name) + select = Select(self.find("#projectversion")) + select.select_by_value('3') + + # check merge toaster settings + checkbox = self.find('.checkbox-mergeattr') + if not checkbox.is_selected(): + checkbox.click() + + if self.PROJECT_NAME != 'TestProjectConfig': + # Reset project name if it's not the default one + self.PROJECT_NAME = 'TestProjectConfig' + + self.find("#create-project-button").click() + + try: + self.wait_until_visible('#hint-error-project-name', poll=2) + url = reverse('project', args=(TestProjectConfig.project_id, )) + self.get(url) + self.wait_until_visible('#config-nav', poll=3) + except TimeoutException: + self.wait_until_visible('#config-nav', poll=3) + + def _random_string(self, length): + return ''.join( + random.choice(string.ascii_letters) for _ in range(length) + ) + + def _get_config_nav_item(self, index): + config_nav = self.find('#config-nav') + return config_nav.find_elements(By.TAG_NAME, 'li')[index] + + def _navigate_bbv_page(self): + """ Navigate to project BitBake variables page """ + # check if the menu is displayed + if TestProjectConfig.project_id is None: + self._create_project(project_name=self._random_string(10)) + current_url = self.driver.current_url + TestProjectConfig.project_id = get_projectId_from_url(current_url) + else: + url = reverse('projectconf', args=(TestProjectConfig.project_id,)) + self.get(url) + self.wait_until_visible('#config-nav', poll=3) + bbv_page_link = self._get_config_nav_item(9) + bbv_page_link.click() + self.wait_until_visible('#config-nav', poll=3) + + def test_no_underscore_iamgefs_type(self): + """ + Should not accept IMAGEFS_TYPE with an underscore + """ + self._navigate_bbv_page() + imagefs_type = "foo_bar" + + self.wait_until_visible('#change-image_fstypes-icon', poll=2) + + self.click('#change-image_fstypes-icon') + + self.enter_text('#new-imagefs_types', imagefs_type) + + element = self.wait_until_visible('#hintError-image-fs_type', poll=2) + + self.assertTrue(("A valid image type cannot include underscores" in element.text), + "Did not find underscore error message") + + def test_checkbox_verification(self): + """ + Should automatically check the checkbox if user enters value + text box, if value is there in the checkbox. + """ + self._navigate_bbv_page() + + imagefs_type = "btrfs" + + self.wait_until_visible('#change-image_fstypes-icon', poll=2) + + self.click('#change-image_fstypes-icon') + + self.enter_text('#new-imagefs_types', imagefs_type) + + checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']") + + for checkbox in checkboxes: + if checkbox.get_attribute("value") == "btrfs": + self.assertEqual(checkbox.is_selected(), True) + + def test_textbox_with_checkbox_verification(self): + """ + Should automatically add or remove value in textbox, if user checks + or unchecks checkboxes. + """ + self._navigate_bbv_page() + + self.wait_until_visible('#change-image_fstypes-icon', poll=2) + + self.click('#change-image_fstypes-icon') + + checkboxes_selector = '.fs-checkbox-fstypes' + + self.wait_until_visible(checkboxes_selector, poll=2) + checkboxes = self.find_all(checkboxes_selector) + + for checkbox in checkboxes: + if checkbox.get_attribute("value") == "cpio": + checkbox.click() + element = self.driver.find_element(By.ID, 'new-imagefs_types') + + self.wait_until_visible('#new-imagefs_types', poll=2) + + self.assertTrue(("cpio" in element.get_attribute('value'), + "Imagefs not added into the textbox")) + checkbox.click() + self.assertTrue(("cpio" not in element.text), + "Image still present in the textbox") + + def test_set_download_dir(self): + """ + Validate the allowed and disallowed types in the directory field for + DL_DIR + """ + self._navigate_bbv_page() + + # activate the input to edit download dir + try: + change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2) + except TimeoutException: + # If download dir is not displayed, test is skipped + return True + change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2) + change_dl_dir_btn.click() + + # downloads dir path doesn't start with / or ${...} + input_field = self.wait_until_visible('#new-dl_dir', poll=2) + input_field.clear() + self.enter_text('#new-dl_dir', 'home/foo') + element = self.wait_until_visible('#hintError-initialChar-dl_dir', poll=2) + + msg = 'downloads directory path starts with invalid character but ' \ + 'treated as valid' + self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) + + # downloads dir path has a space + self.driver.find_element(By.ID, 'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '/foo/bar a') + + element = self.wait_until_visible('#hintError-dl_dir', poll=2) + msg = 'downloads directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # downloads dir path starts with ${...} but has a space + self.driver.find_element(By.ID,'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') + + element = self.wait_until_visible('#hintError-dl_dir', poll=2) + msg = 'downloads directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # downloads dir path starts with / + self.driver.find_element(By.ID,'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '/bar/foo') + + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'downloads directory path valid but treated as invalid') + + # downloads dir path starts with ${...} + self.driver.find_element(By.ID,'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '${TOPDIR}/down') + + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'downloads directory path valid but treated as invalid') + + def test_set_sstate_dir(self): + """ + Validate the allowed and disallowed types in the directory field for + SSTATE_DIR + """ + self._navigate_bbv_page() + + try: + self.wait_until_visible('#change-sstate_dir-icon', poll=2) + self.click('#change-sstate_dir-icon') + except TimeoutException: + # If sstate_dir is not displayed, test is skipped + return True + + # path doesn't start with / or ${...} + input_field = self.wait_until_visible('#new-sstate_dir', poll=2) + input_field.clear() + self.enter_text('#new-sstate_dir', 'home/foo') + element = self.wait_until_visible('#hintError-initialChar-sstate_dir', poll=2) + + msg = 'sstate directory path starts with invalid character but ' \ + 'treated as valid' + self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) + + # path has a space + self.driver.find_element(By.ID, 'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '/foo/bar a') + + element = self.wait_until_visible('#hintError-sstate_dir', poll=2) + msg = 'sstate directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # path starts with ${...} but has a space + self.driver.find_element(By.ID,'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') + + element = self.wait_until_visible('#hintError-sstate_dir', poll=2) + msg = 'sstate directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # path starts with / + self.driver.find_element(By.ID,'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '/bar/foo') + + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'sstate directory path valid but treated as invalid') + + # paths starts with ${...} + self.driver.find_element(By.ID, 'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '${TOPDIR}/down') + + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'sstate directory path valid but treated as invalid') + + def _change_bbv_value(self, **kwargs): + var_name, field, btn_id, input_id, value, save_btn, *_ = kwargs.values() + """ Change bitbake variable value """ + self._navigate_bbv_page() + self.wait_until_visible(f'#{btn_id}', poll=2) + if kwargs.get('new_variable'): + self.find(f"#{btn_id}").clear() + self.enter_text(f"#{btn_id}", f"{var_name}") + else: + self.click(f'#{btn_id}') + self.wait_until_visible(f'#{input_id}', poll=2) + + if kwargs.get('is_select'): + select = Select(self.find(f'#{input_id}')) + select.select_by_visible_text(value) + else: + self.find(f"#{input_id}").clear() + self.enter_text(f'#{input_id}', f'{value}') + self.click(f'#{save_btn}') + value_displayed = str(self.wait_until_visible(f'#{field}').text).lower() + msg = f'{var_name} variable not changed' + self.assertTrue(str(value).lower() in value_displayed, msg) + + def test_change_distro_var(self): + """ Test changing distro variable """ + self._change_bbv_value( + var_name='DISTRO', + field='distro', + btn_id='change-distro-icon', + input_id='new-distro', + value='poky-changed', + save_btn="apply-change-distro", + ) + + def test_set_image_install_append_var(self): + """ Test setting IMAGE_INSTALL:append variable """ + self._change_bbv_value( + var_name='IMAGE_INSTALL:append', + field='image_install', + btn_id='change-image_install-icon', + input_id='new-image_install', + value='bash, apt, busybox', + save_btn="apply-change-image_install", + ) + + def test_set_package_classes_var(self): + """ Test setting PACKAGE_CLASSES variable """ + self._change_bbv_value( + var_name='PACKAGE_CLASSES', + field='package_classes', + btn_id='change-package_classes-icon', + input_id='package_classes-select', + value='package_deb', + save_btn="apply-change-package_classes", + is_select=True, + ) + + def test_create_new_bbv(self): + """ Test creating new bitbake variable """ + self._change_bbv_value( + var_name='New_Custom_Variable', + field='configvar-list', + btn_id='variable', + input_id='value', + value='new variable value', + save_btn="add-configvar-button", + new_variable=True + ) diff --git a/lib/toaster/tests/functional/test_project_page_tab_config.py b/lib/toaster/tests/functional/test_project_page_tab_config.py index 23012d78..d911ff00 100644 --- a/lib/toaster/tests/functional/test_project_page_tab_config.py +++ b/lib/toaster/tests/functional/test_project_page_tab_config.py @@ -6,87 +6,81 @@ # SPDX-License-Identifier: GPL-2.0-only # -from time import sleep +import string +import random 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 selenium.common.exceptions import TimeoutException +from orm.models import Project from tests.functional.functional_helpers import SeleniumFunctionalTestCase from selenium.webdriver.common.by import By +from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled + @pytest.mark.django_db +@pytest.mark.order("last") class TestProjectConfigTab(SeleniumFunctionalTestCase): + PROJECT_NAME = 'TestProjectConfigTab' + project_id = None - 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, - ): + def _create_project(self, project_name): """ 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) + self.wait_until_visible('#new-project-name') + self.find("#new-project-name").send_keys(project_name) + select = Select(self.find("#projectversion")) + select.select_by_value('3') # check merge toaster settings checkbox = self.find('.checkbox-mergeattr') - if merge_toaster_settings: - if not checkbox.is_selected(): - checkbox.click() + if not checkbox.is_selected(): + checkbox.click() + + if self.PROJECT_NAME != 'TestProjectConfigTab': + # Reset project name if it's not the default one + self.PROJECT_NAME = 'TestProjectConfigTab' + + self.find("#create-project-button").click() + + try: + self.wait_until_visible('#hint-error-project-name') + url = reverse('project', args=(TestProjectConfigTab.project_id, )) + self.get(url) + self.wait_until_visible('#config-nav', poll=3) + except TimeoutException: + self.wait_until_visible('#config-nav', poll=3) + + def _random_string(self, length): + return ''.join( + random.choice(string.ascii_letters) for _ in range(length) + ) + + def _navigate_to_project_page(self): + # Navigate to project page + if TestProjectConfigTab.project_id is None: + self._create_project(project_name=self._random_string(10)) + current_url = self.driver.current_url + TestProjectConfigTab.project_id = get_projectId_from_url(current_url) 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) + url = reverse('project', args=(TestProjectConfigTab.project_id,)) + self.get(url) + self.wait_until_visible('#config-nav') 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') + build_state = wait_until_build(self, 'parsing starting cloning') lastest_builds = self.driver.find_elements( By.XPATH, '//div[@id="latest-builds"]/div', @@ -100,8 +94,9 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): '//span[@class="cancel-build-btn pull-right alert-link"]', ) cancel_button.click() - sleep(1) - self._wait_until_build('cancelled') + if 'starting' not in build_state: # change build state when cancelled in starting state + wait_until_build_cancelled(self) + return build_state def _get_tabs(self): # tabs links list @@ -114,64 +109,6 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): 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: @@ -188,13 +125,7 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): - 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') - + self._navigate_to_project_page() def _get_config_nav_item(index): config_nav = self.find('#config-nav') return config_nav.find_elements(By.TAG_NAME, 'li')[index] @@ -221,14 +152,14 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): 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 + [0, 'Configuration', f"/toastergui/project/{TestProjectConfigTab.project_id}"], # config + [2, 'Custom images', f"/toastergui/project/{TestProjectConfigTab.project_id}/customimages"], # custom images + [3, 'Image recipes', f"/toastergui/project/{TestProjectConfigTab.project_id}/images"], # image recipes + [4, 'Software recipes', f"/toastergui/project/{TestProjectConfigTab.project_id}/softwarerecipes"], # software recipes + [5, 'Machines', f"/toastergui/project/{TestProjectConfigTab.project_id}/machines"], # machines + [6, 'Layers', f"/toastergui/project/{TestProjectConfigTab.project_id}/layers"], # layers + [7, 'Distros', f"/toastergui/project/{TestProjectConfigTab.project_id}/distros"], # distro + # [9, 'BitBake variables', f"/toastergui/project/{TestProjectConfigTab.project_id}/configuration"], # bitbake variables ] for index, item_name, url in conf_nav_list: item = _get_config_nav_item(index) @@ -236,6 +167,96 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): item.click() check_config_nav_item(index, item_name, url) + def test_image_recipe_editColumn(self): + """ Test the edit column feature in image recipe table on project page """ + 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" + ) + + self._navigate_to_project_page() + # navigate to project image recipe page + recipe_image_page_link = self._get_config_nav_item(3) + recipe_image_page_link.click() + 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 """ + 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_visible('#imagerecipestable tbody tr') + self.assertTrue( + len(self.find_all('#imagerecipestable tbody tr')) == row_to_show + ) + + self._navigate_to_project_page() + # navigate to project image recipe page + recipe_image_page_link = self._get_config_nav_item(3) + recipe_image_page_link.click() + 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) + def test_project_config_tab_right_section(self): """ Test project config tab right section contains five blocks: - Machine: @@ -257,35 +278,36 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): - meta-poky - meta-yocto-bsp """ - # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) - + # Create a new project for this test + project_name = self._random_string(10) + self._create_project(project_name=project_name) + current_url = self.driver.current_url + TestProjectConfigTab.project_id = get_projectId_from_url(current_url) + url = current_url.split('?')[0] # 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): + def check_machine_distro(self, item_name, new_item_name, block_id): + block = self.find(f'#{block_id}') 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 = self.find(f'#change-{item_name}-toggle') edit_btn.click() - sleep(1) - name_input = block.find_element(By.ID, f'{item_name}-change-input') + self.wait_until_visible(f'#{item_name}-change-input') + name_input = self.find(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 = self.find(f'#{item_name}-change-btn') change_btn.click() - sleep(1) - project_name = block.find_element(By.ID, f'project-{item_name}-name') + self.wait_until_visible(f'#project-{item_name}-name') + project_name = self.find(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') @@ -293,10 +315,30 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): f'You have changed the {item_name} to: {new_item_name}' in change_notification.text ) + def rebuild_from_most_build_recipes(recipe_list_items): + checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input') + checkbox.click() + build_btn = self.find('#freq-build-btn') + build_btn.click() + self.wait_until_visible('#latest-builds') + build_state = wait_until_build(self, 'parsing starting cloning queued') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]/div' + ) + last_build = lastest_builds[0] + self.assertTrue(len(lastest_builds) >= 2) + cancel_button = last_build.find_element( + By.XPATH, + '//span[@class="cancel-build-btn pull-right alert-link"]', + ) + cancel_button.click() + if 'starting' not in build_state: # change build state when cancelled in starting state + wait_until_build_cancelled(self) # Machine - check_machine_distro(self, 'machine', 'qemux86-64', machine) + check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section') # Distro - check_machine_distro(self, 'distro', 'poky-altcfg', distro) + check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section') # Project release title = project_release.find_element(By.TAG_NAME, 'h3') @@ -304,7 +346,6 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): 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) @@ -314,7 +355,9 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): # 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) + # remove all layers except the first three layers + for i in range(3, len(layers_list_items)): + layers_list_items[i].find_element(By.TAG_NAME, 'span').click() # 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') @@ -326,7 +369,7 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): dropdown_item.click() add_layer_btn = layers.find_element(By.ID, 'add-layer-btn') add_layer_btn.click() - sleep(1) + self.wait_until_visible('#layers-in-project-list') # check layer is added layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') self.assertTrue(len(layers_list_items) == 4) @@ -334,48 +377,33 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): # 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() + # Create a new builds + build_state = self._create_builds() # Refresh the page - self.get(url) + self.driver.get(url) - sleep(1) # wait for page to load - self.wait_until_visible('#project-page') + self.wait_until_visible('#project-page', poll=3) # 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) + if 'starting' not in build_state: # Build will not appear in the list if canceled in starting state + self.assertTrue( + len(recipe_list_items) > 0, + msg="No recipes found in the most built recipes list", + ) + rebuild_from_most_build_recipes(recipe_list_items) + else: + self.assertTrue( + len(recipe_list_items) == 0, + msg="Recipes found in the most built recipes list", + ) 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) - + self._navigate_to_project_page() # navigate to "Import layers" tab import_layers_tab = self._get_tabs()[2] import_layers_tab.find_element(By.TAG_NAME, 'a').click() @@ -415,10 +443,10 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): 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) - + project_name = self._random_string(10) + self._create_project(project_name=project_name) + current_url = self.driver.current_url + TestProjectConfigTab.project_id = get_projectId_from_url(current_url) # navigate to "Custom image" tab custom_image_section = self._get_config_nav_item(2) custom_image_section.click() @@ -433,8 +461,10 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): div_empty_msg = self.find('#empty-state-customimagestable') link_create_custom_image = div_empty_msg.find_element( By.TAG_NAME, 'a') + last_project_id = Project.objects.get(name=project_name).id + self.assertTrue(last_project_id is not None) self.assertTrue( - f"/toastergui/project/1/newcustomimage" in str( + f"/toastergui/project/{last_project_id}/newcustomimage" in str( link_create_custom_image.get_attribute('href') ) ) @@ -451,11 +481,7 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): - 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') - + self._navigate_to_project_page() # navigate to "Images section" images_section = self._get_config_nav_item(3) images_section.click() @@ -479,100 +505,17 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): '//td[@class="add-del-layers"]' ) build_btn.click() - self._wait_until_build('parsing starting cloning') + build_state = wait_until_build(self, 'parsing starting cloning queued') 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( + last_build = lastest_builds[0] + cancel_button = last_build.find_element( By.XPATH, - '//select[@class="form-control pagesize-imagerecipestable"]' + '//span[@class="cancel-build-btn pull-right alert-link"]', ) - # 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) + cancel_button.click() + if 'starting' not in build_state: # change build state when cancelled in starting state + wait_until_build_cancelled(self)