summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/toaster/tests/functional
diff options
context:
space:
mode:
Diffstat (limited to 'bitbake/lib/toaster/tests/functional')
-rw-r--r--bitbake/lib/toaster/tests/functional/functional_helpers.py82
-rw-r--r--bitbake/lib/toaster/tests/functional/test_create_new_project.py179
-rw-r--r--bitbake/lib/toaster/tests/functional/test_functional_basic.py195
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_config.py341
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_page.py792
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py528
-rw-r--r--bitbake/lib/toaster/tests/functional/utils.py89
7 files changed, 2095 insertions, 111 deletions
diff --git a/bitbake/lib/toaster/tests/functional/functional_helpers.py b/bitbake/lib/toaster/tests/functional/functional_helpers.py
index 455c408e94..7c20437d14 100644
--- a/bitbake/lib/toaster/tests/functional/functional_helpers.py
+++ b/bitbake/lib/toaster/tests/functional/functional_helpers.py
@@ -11,35 +11,55 @@ import os
import logging
import subprocess
import signal
-import time
import re
from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
-from tests.builds.buildtest import load_build_environment
+from selenium.webdriver.common.by import By
+from selenium.common.exceptions import NoSuchElementException
logger = logging.getLogger("toaster")
+toaster_processes = []
class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
- wait_toaster_time = 5
+ wait_toaster_time = 10
@classmethod
def setUpClass(cls):
# So that the buildinfo helper uses the test database'
if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \
'toastermain.settings_test':
- raise RuntimeError("Please initialise django with the tests settings: " \
+ raise RuntimeError("Please initialise django with the tests settings: "
"DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
- load_build_environment()
+ # Wait for any known toaster processes to exit
+ global toaster_processes
+ for toaster_process in toaster_processes:
+ try:
+ os.waitpid(toaster_process, os.WNOHANG)
+ except ChildProcessError:
+ pass
# start toaster
cmd = "bash -c 'source toaster start'"
- p = subprocess.Popen(
+ start_process = subprocess.Popen(
cmd,
cwd=os.environ.get("BUILDDIR"),
shell=True)
- if p.wait() != 0:
- raise RuntimeError("Can't initialize toaster")
+ toaster_processes = [start_process.pid]
+ if start_process.wait() != 0:
+ port_use = os.popen("lsof -i -P -n | grep '8000 (LISTEN)'").read().strip()
+ message = ''
+ if port_use:
+ process_id = port_use.split()[1]
+ process = os.popen(f"ps -o cmd= -p {process_id}").read().strip()
+ message = f"Port 8000 occupied by {process}"
+ raise RuntimeError(f"Can't initialize toaster. {message}")
+
+ builddir = os.environ.get("BUILDDIR")
+ with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f:
+ toaster_processes.append(int(f.read()))
+ with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f:
+ toaster_processes.append(int(f.read()))
super(SeleniumFunctionalTestCase, cls).setUpClass()
cls.live_server_url = 'http://localhost:8000/'
@@ -48,22 +68,30 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
def tearDownClass(cls):
super(SeleniumFunctionalTestCase, cls).tearDownClass()
- # XXX: source toaster stop gets blocked, to review why?
- # from now send SIGTERM by hand
- time.sleep(cls.wait_toaster_time)
- builddir = os.environ.get("BUILDDIR")
+ global toaster_processes
- with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f:
- toastermain_pid = int(f.read())
- os.kill(toastermain_pid, signal.SIGTERM)
- with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f:
- runbuilds_pid = int(f.read())
- os.kill(runbuilds_pid, signal.SIGTERM)
+ cmd = "bash -c 'source toaster stop'"
+ stop_process = subprocess.Popen(
+ cmd,
+ cwd=os.environ.get("BUILDDIR"),
+ shell=True)
+ # Toaster stop has been known to hang in these tests so force kill if it stalls
+ try:
+ if stop_process.wait(cls.wait_toaster_time) != 0:
+ raise Exception('Toaster stop process failed')
+ except Exception as e:
+ if e is subprocess.TimeoutExpired:
+ print('Toaster stop process took too long. Force killing toaster...')
+ else:
+ print('Toaster stop process failed. Force killing toaster...')
+ stop_process.kill()
+ for toaster_process in toaster_processes:
+ os.kill(toaster_process, signal.SIGTERM)
def get_URL(self):
rc=self.get_page_source()
- project_url=re.search("(projectPageUrl\s:\s\")(.*)(\",)",rc)
+ project_url=re.search(r"(projectPageUrl\s:\s\")(.*)(\",)",rc)
return project_url.group(2)
@@ -74,8 +102,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
"""
try:
table_element = self.get_table_element(table_id)
- element = table_element.find_element_by_link_text(link_text)
- except NoSuchElementException as e:
+ element = table_element.find_element(By.LINK_TEXT, link_text)
+ except NoSuchElementException:
print('no element found')
raise
return element
@@ -85,8 +113,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
#return whole-table element
element_xpath = "//*[@id='" + table_id + "']"
try:
- element = self.driver.find_element_by_xpath(element_xpath)
- except NoSuchElementException as e:
+ element = self.driver.find_element(By.XPATH, element_xpath)
+ except NoSuchElementException:
raise
return element
row = coordinate[0]
@@ -95,8 +123,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
#return whole-row element
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]"
try:
- element = self.driver.find_element_by_xpath(element_xpath)
- except NoSuchElementException as e:
+ element = self.driver.find_element(By.XPATH, element_xpath)
+ except NoSuchElementException:
return False
return element
#now we are looking for an element with specified X and Y
@@ -104,7 +132,7 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]"
try:
- element = self.driver.find_element_by_xpath(element_xpath)
- except NoSuchElementException as e:
+ element = self.driver.find_element(By.XPATH, element_xpath)
+ except NoSuchElementException:
return False
return element
diff --git a/bitbake/lib/toaster/tests/functional/test_create_new_project.py b/bitbake/lib/toaster/tests/functional/test_create_new_project.py
new file mode 100644
index 0000000000..94d90459e1
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/test_create_new_project.py
@@ -0,0 +1,179 @@
+#! /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
+@pytest.mark.order("last")
+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.wait_until_visible('#new-project-name', poll=3)
+ 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', poll=3)
+ 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 = 'projectdunfell'
+ 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/bitbake/lib/toaster/tests/functional/test_functional_basic.py b/bitbake/lib/toaster/tests/functional/test_functional_basic.py
index 5683e3873e..e4070fbb88 100644
--- a/bitbake/lib/toaster/tests/functional/test_functional_basic.py
+++ b/bitbake/lib/toaster/tests/functional/test_functional_basic.py
@@ -8,104 +8,129 @@
#
import re
+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
+from tests.functional.utils import get_projectId_from_url
+
+
+@pytest.mark.django_db
+@pytest.mark.order("second_to_last")
class FuntionalTestBasic(SeleniumFunctionalTestCase):
+ """Basic functional tests for Toaster"""
+ project_id = None
+
+ def setUp(self):
+ super(FuntionalTestBasic, self).setUp()
+ if not FuntionalTestBasic.project_id:
+ self._create_slenium_project()
+ current_url = self.driver.current_url
+ FuntionalTestBasic.project_id = get_projectId_from_url(current_url)
# testcase (1514)
- def test_create_slenium_project(self):
+ def _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.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()
- element = self.wait_until_visible('#project-created-notification')
+ self.get(reverse('newproject'))
+ self.wait_until_visible('#new-project-name', poll=3)
+ 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()
+ element = self.wait_until_visible('#project-created-notification', poll=10)
self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown')
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")
+ return Project.objects.last().id
# testcase (1515)
def test_verify_left_bar_menu(self):
- self.get('')
- self.wait_until_visible('#projectstable')
+ self.get(reverse('all-projects'))
+ self.wait_until_present('#projectstable', poll=10)
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
+ self.wait_until_present('#config-nav', poll=10)
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()
+ self.driver.find_element(By.XPATH, '//a[@href="'+project_URL+'"]').click()
+ self.wait_until_present('#config-nav', poll=10)
try:
- self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click()
- 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')
+ self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click()
+ self.wait_until_present('#config-nav', poll=10)
+ 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')
try:
- self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click()
- self.assertTrue(re.search("Compatible image recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly')
+ self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click()
+ self.wait_until_present('#config-nav', poll=10)
+ self.assertTrue(re.search("Compatible image recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly')
except:
self.fail(msg='No Compatible image tab available')
try:
- self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click()
- self.assertTrue(re.search("Compatible software recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly')
+ self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click()
+ self.wait_until_present('#config-nav', poll=10)
+ self.assertTrue(re.search("Compatible software recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly')
except:
self.fail(msg='No Compatible software recipe tab available')
try:
- self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click()
- self.assertTrue(re.search("Compatible machines",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly')
+ self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click()
+ self.wait_until_present('#config-nav', poll=10)
+ self.assertTrue(re.search("Compatible machines",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly')
except:
self.fail(msg='No Compatible machines tab available')
try:
- self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click()
- self.assertTrue(re.search("Compatible layers",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly')
+ self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click()
+ self.wait_until_present('#config-nav', poll=10)
+ self.assertTrue(re.search("Compatible layers",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly')
except:
self.fail(msg='No Compatible layers tab available')
try:
- self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click()
- self.assertTrue(re.search("Bitbake variables",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly')
+ self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click()
+ self.wait_until_present('#config-nav', poll=10)
+ self.assertTrue(re.search("Bitbake variables",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly')
except:
self.fail(msg='No Bitbake variables tab available')
# testcase (1516)
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()
- self.wait_until_visible('#projectstable')
+ self.get(reverse('all-projects'))
+ self.wait_until_present('#projectstable', poll=10)
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
project_URL=self.get_URL()
-
+ self.wait_until_present('#config-nav', poll=10)
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()
- 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()
+ self.assertTrue(re.search("qemux86-64",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()
+ self.wait_until_visible('#select-machine-form', poll=10)
+ self.wait_until_visible('#cancel-machine-change', poll=10)
+ self.driver.find_element(By.XPATH, "//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click()
except:
self.fail(msg='The machine information is wrong in the configuration page')
try:
- self.driver.find_element_by_id('no-most-built')
+ self.driver.find_element(By.ID, 'no-most-built')
except:
self.fail(msg='No Most built information in project detail page')
try:
- self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_xpath("//span[@id='project-release-title']").text),'The project release is not defined')
+ self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.XPATH, "//span[@id='project-release-title']").text),'The project release is not defined')
except:
self.fail(msg='No project release title information in project detail page')
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_elements_by_tag_name("li")
+ 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_elements(By.TAG_NAME, "li")
for layer in layers:
if re.match ("openembedded-core",layer.text):
print ("openembedded-core layer is a default layer in the project configuration")
@@ -120,61 +145,60 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase):
# testcase (1517)
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()
- self.wait_until_visible('#projectstable')
+ self.get(reverse('all-projects'))
+ self.wait_until_present('#projectstable', poll=10)
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
+ self.wait_until_present('#config-nav', poll=10)
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()
- self.wait_until_visible('#select-machine-form')
- self.wait_until_visible('#cancel-machine-change')
- self.driver.find_element_by_id("cancel-machine-change").click()
+ self.assertTrue(re.search("qemux86-64",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()
+ self.wait_until_visible('#select-machine-form', poll=10)
+ self.wait_until_visible('#cancel-machine-change', poll=10)
+ self.driver.find_element(By.ID, "cancel-machine-change").click()
except:
self.fail(msg='The machine information is wrong in the configuration page')
# testcase (1518)
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()
- self.wait_until_visible('#projectstable')
+ self.get(reverse('all-projects'))
+ self.wait_until_present('#projectstable', poll=10)
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
+ self.wait_until_present('#config-nav', poll=10)
project_URL=self.get_URL()
-
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()
- 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')
+ 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()
+ self.wait_until_present('#config-nav', poll=10)
+ 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')
# testcase (1519)
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()
- self.wait_until_visible('#projectstable')
+ self.get(reverse('all-projects'))
+ self.wait_until_present('#projectstable', poll=10)
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
+ self.wait_until_present('#config-nav', poll=10)
try:
- self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_id("project-release-title").text),'The project release is not defined')
+ self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.ID, "project-release-title").text),'The project release is not defined')
except:
self.fail(msg='No project release title information in project detail page')
# testcase (1520)
def test_verify_layer_information(self):
- self.get('')
- self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click()
- self.wait_until_visible('#projectstable')
+ self.get(reverse('all-projects'))
+ self.wait_until_present('#projectstable', poll=10)
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
+ self.wait_until_present('#config-nav', poll=10)
project_URL=self.get_URL()
-
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_elements_by_tag_name("li")
+ 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_elements(By.TAG_NAME, "li")
for layer in layers:
if re.match ("openembedded-core",layer.text):
@@ -186,43 +210,46 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase):
else:
self.fail(msg='default layers are missing from the project configuration')
- self.driver.find_element_by_xpath("//input[@id='layer-add-input']")
- self.driver.find_element_by_xpath("//button[@id='add-layer-btn']")
- self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']")
- self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]")
+ self.driver.find_element(By.XPATH, "//input[@id='layer-add-input']")
+ self.driver.find_element(By.XPATH, "//button[@id='add-layer-btn']")
+ self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']")
+ self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]")
except:
self.fail(msg='No Layer information in project detail page')
# testcase (1521)
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()
- self.wait_until_visible('#projectstable')
+ self.get(reverse('all-projects'))
+ self.wait_until_present('#projectstable', poll=10)
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
+ self.wait_until_present('#config-nav', poll=10)
project_URL=self.get_URL()
-
- 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')
+ 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.wait_until_present('#config-nav', poll=10)
+ 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')
try:
- self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click()
- self.assertTrue(re.search("Builds",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled')
- self.driver.find_element_by_xpath("//div[@id='empty-state-projectbuildstable']")
+ self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click()
+ self.wait_until_visible('#project-topbar', poll=10)
+ self.assertTrue(re.search("Builds",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled')
+ self.driver.find_element(By.XPATH, "//div[@id='empty-state-projectbuildstable']")
except:
self.fail(msg='Builds tab information is not present')
try:
- self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click()
- self.assertTrue(re.search("Import layer",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled')
- self.driver.find_element_by_xpath("//fieldset[@id='repo-select']")
- self.driver.find_element_by_xpath("//fieldset[@id='git-repo']")
+ self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click()
+ self.wait_until_visible('#project-topbar', poll=10)
+ self.assertTrue(re.search("Import layer",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled')
+ self.driver.find_element(By.XPATH, "//fieldset[@id='repo-select']")
+ self.driver.find_element(By.XPATH, "//fieldset[@id='git-repo']")
except:
self.fail(msg='Import layer tab not loading properly')
try:
- self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click()
- self.assertTrue(re.search("New custom image",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled')
- self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element_by_xpath("//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly')
+ self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click()
+ self.wait_until_visible('#project-topbar', poll=10)
+ self.assertTrue(re.search("New custom image",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled')
+ self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element(By.XPATH, "//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly')
except:
self.fail(msg='New custom image tab not loading properly')
diff --git a/bitbake/lib/toaster/tests/functional/test_project_config.py b/bitbake/lib/toaster/tests/functional/test_project_config.py
new file mode 100644
index 0000000000..dbee36aa4e
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/test_project_config.py
@@ -0,0 +1,341 @@
+#! /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
+ change_dl_dir_btn = None
+
+ if change_dl_dir_btn:
+ 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:
+ btn_chg_sstate_dir = 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
+ btn_chg_sstate_dir = None
+
+ if btn_chg_sstate_dir: # Skip continuation if sstate_dir is not displayed
+ # 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/bitbake/lib/toaster/tests/functional/test_project_page.py b/bitbake/lib/toaster/tests/functional/test_project_page.py
new file mode 100644
index 0000000000..adbe3587e4
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/test_project_page.py
@@ -0,0 +1,792 @@
+#! /usr/bin/env python3 #
+# BitBake Toaster UI tests implementation
+#
+# Copyright (C) 2023 Savoir-faire Linux
+#
+# SPDX-License-Identifier: GPL-2.0-only
+#
+
+import os
+import random
+import string
+from unittest import skip
+import pytest
+from django.urls import reverse
+from django.utils import timezone
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.support.select import Select
+from selenium.common.exceptions import TimeoutException
+from tests.functional.functional_helpers import SeleniumFunctionalTestCase
+from orm.models import Build, Project, Target
+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 TestProjectPage(SeleniumFunctionalTestCase):
+ project_id = None
+ PROJECT_NAME = 'TestProjectPage'
+
+ 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')
+ 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 != 'TestProjectPage':
+ # Reset project name if it's not the default one
+ self.PROJECT_NAME = 'TestProjectPage'
+
+ self.find("#create-project-button").click()
+
+ try:
+ self.wait_until_visible('#hint-error-project-name')
+ url = reverse('project', args=(TestProjectPage.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 TestProjectPage.project_id is None:
+ self._create_project(project_name=self._random_string(10))
+ current_url = self.driver.current_url
+ TestProjectPage.project_id = get_projectId_from_url(current_url)
+ else:
+ url = reverse('project', args=(TestProjectPage.project_id,))
+ self.get(url)
+ self.wait_until_visible('#config-nav')
+
+ 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()
+ self.project1_build_success = {
+ 'project': Project.objects.get(id=TestProjectPage.project_id),
+ 'started_on': now,
+ 'completed_on': now,
+ 'outcome': Build.SUCCEEDED
+ }
+
+ self.project1_build_failure = {
+ 'project': Project.objects.get(id=TestProjectPage.project_id),
+ '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 _mixin_test_table_edit_column(
+ self,
+ table_id,
+ edit_btn_id,
+ list_check_box_id: list
+ ):
+ # Check edit column
+ edit_column = self.find(f'#{edit_btn_id}')
+ self.assertTrue(edit_column.is_displayed())
+ edit_column.click()
+ # Check dropdown is visible
+ self.wait_until_visible('ul.dropdown-menu.editcol')
+ for check_box_id in list_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'#{table_id} 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'#{table_id} 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'#{table_id} 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'#{table_id} thead th.{th_class}'
+ ).is_displayed(),
+ f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
+ )
+
+ 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_to_config_nav(self, nav_id, nav_index):
+ # navigate to the project page
+ self._navigate_to_project_page()
+ # click on "Software recipe" tab
+ soft_recipe = self._get_config_nav_item(nav_index)
+ soft_recipe.click()
+ self.wait_until_visible(f'#{nav_id}')
+
+ def _mixin_test_table_show_rows(self, table_selector, **kwargs):
+ """ Test the show rows feature in the builds table on the all builds 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(f'#{table_selector} tbody tr', poll=3)
+ # check at least some rows are visible
+ self.assertTrue(
+ len(self.find_all(f'#{table_selector} tbody tr')) > 0
+ )
+ self.wait_until_present(f'#{table_selector} tbody tr')
+ show_rows = self.driver.find_elements(
+ By.XPATH,
+ f'//select[@class="form-control pagesize-{table_selector}"]'
+ )
+ rows_to_show = [10, 25, 50, 100, 150]
+ to_skip = kwargs.get('to_skip', [])
+ # Check show rows
+ for show_row_link in show_rows:
+ show_row_link = Select(show_row_link)
+ for row_to_show in rows_to_show:
+ if row_to_show not in to_skip:
+ test_show_rows(row_to_show, show_row_link)
+
+ def _mixin_test_table_search_input(self, **kwargs):
+ input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values()
+ # Test search input
+ self.wait_until_visible(f'#{input_selector}')
+ recipe_input = self.find(f'#{input_selector}')
+ recipe_input.send_keys(input_text)
+ self.find(f'#{searchBtn_selector}').click()
+ self.wait_until_visible(f'#{table_selector} tbody tr')
+ rows = self.find_all(f'#{table_selector} tbody tr')
+ self.assertTrue(len(rows) > 0)
+
+ def test_create_project(self):
+ """ Create/Test new project using:
+ - Project Name: Any string
+ - Release: Any string
+ - Merge Toaster settings: True or False
+ """
+ self._create_project(project_name=self.PROJECT_NAME)
+
+ 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)
+
+ url = reverse('projectimagerecipes', args=(TestProjectPage.project_id,))
+ self.get(url)
+ self.wait_until_present('#imagerecipestable tbody tr')
+
+ column_list = [
+ 'get_description_or_summary', 'layer_version__get_vcs_reference',
+ 'layer_version__layer__name', 'license', 'recipe-file', 'section',
+ 'version'
+ ]
+
+ # Check that we can hide the edit column
+ self._mixin_test_table_edit_column(
+ 'imagerecipestable',
+ 'edit-columns-button',
+ [f'checkbox-{column}' for column in column_list]
+ )
+
+ 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
+ self._navigate_to_project_page()
+
+ # 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
+ self._navigate_to_project_page()
+
+ # 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
+ self._navigate_to_project_page()
+
+ # 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 str(config_tab.text))
+ self.assertTrue(
+ f"/toastergui/project/{TestProjectPage.project_id}" in str(self.driver.current_url)
+ )
+
+ 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/{TestProjectPage.project_id}/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/{TestProjectPage.project_id}/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/{TestProjectPage.project_id}/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)
+ )
+
+ def test_softwareRecipe_page(self):
+ """ Test software recipe page
+ - Check title "Compatible software recipes" is displayed
+ - Check search input
+ - Check "build recipe" button works
+ - Check software recipe table feature(show/hide column, pagination)
+ """
+ self._navigate_to_config_nav('softwarerecipestable', 4)
+ # check title "Compatible software recipes" is displayed
+ self.assertTrue("Compatible software recipes" in self.get_page_source())
+ # Test search input
+ self._mixin_test_table_search_input(
+ input_selector='search-input-softwarerecipestable',
+ input_text='busybox',
+ searchBtn_selector='search-submit-softwarerecipestable',
+ table_selector='softwarerecipestable'
+ )
+ # check "build recipe" button works
+ rows = self.find_all('#softwarerecipestable tbody tr')
+ image_to_build = rows[0]
+ build_btn = image_to_build.find_element(
+ By.XPATH,
+ '//td[@class="add-del-layers"]//a[1]'
+ )
+ build_btn.click()
+ build_state = wait_until_build(self, 'queued cloning starting parsing failed')
+ lastest_builds = self.driver.find_elements(
+ By.XPATH,
+ '//div[@id="latest-builds"]/div'
+ )
+ self.assertTrue(len(lastest_builds) > 0)
+ 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()
+ if 'starting' not in build_state: # change build state when cancelled in starting state
+ wait_until_build_cancelled(self)
+
+ # check software recipe table feature(show/hide column, pagination)
+ self._navigate_to_config_nav('softwarerecipestable', 4)
+ column_list = [
+ 'get_description_or_summary',
+ 'layer_version__get_vcs_reference',
+ 'layer_version__layer__name',
+ 'license',
+ 'recipe-file',
+ 'section',
+ 'version',
+ ]
+ self._mixin_test_table_edit_column(
+ 'softwarerecipestable',
+ 'edit-columns-button',
+ [f'checkbox-{column}' for column in column_list]
+ )
+ self._navigate_to_config_nav('softwarerecipestable', 4)
+ # check show rows(pagination)
+ self._mixin_test_table_show_rows(
+ table_selector='softwarerecipestable',
+ to_skip=[150],
+ )
+
+ def test_machines_page(self):
+ """ Test Machine page
+ - Check if title "Compatible machines" is displayed
+ - Check search input
+ - Check "Select machine" button works
+ - Check "Add layer" button works
+ - Check Machine table feature(show/hide column, pagination)
+ """
+ self._navigate_to_config_nav('machinestable', 5)
+ # check title "Compatible software recipes" is displayed
+ self.assertTrue("Compatible machines" in self.get_page_source())
+ # Test search input
+ self._mixin_test_table_search_input(
+ input_selector='search-input-machinestable',
+ input_text='qemux86-64',
+ searchBtn_selector='search-submit-machinestable',
+ table_selector='machinestable'
+ )
+ # check "Select machine" button works
+ rows = self.find_all('#machinestable tbody tr')
+ machine_to_select = rows[0]
+ select_btn = machine_to_select.find_element(
+ By.XPATH,
+ '//td[@class="add-del-layers"]//a[1]'
+ )
+ select_btn.send_keys(Keys.RETURN)
+ self.wait_until_visible('#config-nav')
+ project_machine_name = self.find('#project-machine-name')
+ self.assertTrue(
+ 'qemux86-64' in project_machine_name.text
+ )
+ # check "Add layer" button works
+ self._navigate_to_config_nav('machinestable', 5)
+ # Search for a machine whit layer not in project
+ self._mixin_test_table_search_input(
+ input_selector='search-input-machinestable',
+ input_text='qemux86-64-tpm2',
+ searchBtn_selector='search-submit-machinestable',
+ table_selector='machinestable'
+ )
+ self.wait_until_visible('#machinestable tbody tr', poll=3)
+ rows = self.find_all('#machinestable tbody tr')
+ machine_to_add = rows[0]
+ add_btn = machine_to_add.find_element(By.XPATH, '//td[@class="add-del-layers"]')
+ add_btn.click()
+ self.wait_until_visible('#change-notification')
+ change_notification = self.find('#change-notification')
+ self.assertTrue(
+ f'You have added 1 layer to your project' in str(change_notification.text)
+ )
+ # check Machine table feature(show/hide column, pagination)
+ self._navigate_to_config_nav('machinestable', 5)
+ column_list = [
+ 'description',
+ 'layer_version__get_vcs_reference',
+ 'layer_version__layer__name',
+ 'machinefile',
+ ]
+ self._mixin_test_table_edit_column(
+ 'machinestable',
+ 'edit-columns-button',
+ [f'checkbox-{column}' for column in column_list]
+ )
+ self._navigate_to_config_nav('machinestable', 5)
+ # check show rows(pagination)
+ self._mixin_test_table_show_rows(
+ table_selector='machinestable',
+ to_skip=[150],
+ )
+
+ def test_layers_page(self):
+ """ Test layers page
+ - Check if title "Compatible layerss" is displayed
+ - Check search input
+ - Check "Add layer" button works
+ - Check "Remove layer" button works
+ - Check layers table feature(show/hide column, pagination)
+ """
+ self._navigate_to_config_nav('layerstable', 6)
+ # check title "Compatible layers" is displayed
+ self.assertTrue("Compatible layers" in self.get_page_source())
+ # Test search input
+ input_text='meta-tanowrt'
+ self._mixin_test_table_search_input(
+ input_selector='search-input-layerstable',
+ input_text=input_text,
+ searchBtn_selector='search-submit-layerstable',
+ table_selector='layerstable'
+ )
+ # check "Add layer" button works
+ self.wait_until_visible('#layerstable tbody tr', poll=3)
+ rows = self.find_all('#layerstable tbody tr')
+ layer_to_add = rows[0]
+ add_btn = layer_to_add.find_element(
+ By.XPATH,
+ '//td[@class="add-del-layers"]'
+ )
+ add_btn.click()
+ # check modal is displayed
+ self.wait_until_visible('#dependencies-modal', poll=3)
+ list_dependencies = self.find_all('#dependencies-list li')
+ # click on add-layers button
+ add_layers_btn = self.driver.find_element(
+ By.XPATH,
+ '//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]'
+ )
+ add_layers_btn.click()
+ self.wait_until_visible('#change-notification')
+ change_notification = self.find('#change-notification')
+ self.assertTrue(
+ f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies' in str(change_notification.text)
+ )
+ # check "Remove layer" button works
+ self.wait_until_visible('#layerstable tbody tr', poll=3)
+ rows = self.find_all('#layerstable tbody tr')
+ layer_to_remove = rows[0]
+ remove_btn = layer_to_remove.find_element(
+ By.XPATH,
+ '//td[@class="add-del-layers"]'
+ )
+ remove_btn.click()
+ self.wait_until_visible('#change-notification', poll=2)
+ change_notification = self.find('#change-notification')
+ self.assertTrue(
+ f'You have removed 1 layer from your project: {input_text}' in str(change_notification.text)
+ )
+ # check layers table feature(show/hide column, pagination)
+ self._navigate_to_config_nav('layerstable', 6)
+ column_list = [
+ 'dependencies',
+ 'revision',
+ 'layer__vcs_url',
+ 'git_subdir',
+ 'layer__summary',
+ ]
+ self._mixin_test_table_edit_column(
+ 'layerstable',
+ 'edit-columns-button',
+ [f'checkbox-{column}' for column in column_list]
+ )
+ self._navigate_to_config_nav('layerstable', 6)
+ # check show rows(pagination)
+ self._mixin_test_table_show_rows(
+ table_selector='layerstable',
+ to_skip=[150],
+ )
+
+ def test_distro_page(self):
+ """ Test distros page
+ - Check if title "Compatible distros" is displayed
+ - Check search input
+ - Check "Add layer" button works
+ - Check distro table feature(show/hide column, pagination)
+ """
+ self._navigate_to_config_nav('distrostable', 7)
+ # check title "Compatible distros" is displayed
+ self.assertTrue("Compatible Distros" in self.get_page_source())
+ # Test search input
+ input_text='poky-altcfg'
+ self._mixin_test_table_search_input(
+ input_selector='search-input-distrostable',
+ input_text=input_text,
+ searchBtn_selector='search-submit-distrostable',
+ table_selector='distrostable'
+ )
+ # check "Add distro" button works
+ rows = self.find_all('#distrostable tbody tr')
+ distro_to_add = rows[0]
+ add_btn = distro_to_add.find_element(
+ By.XPATH,
+ '//td[@class="add-del-layers"]//a[1]'
+ )
+ add_btn.click()
+ self.wait_until_visible('#change-notification', poll=2)
+ change_notification = self.find('#change-notification')
+ self.assertTrue(
+ f'You have changed the distro to: {input_text}' in str(change_notification.text)
+ )
+ # check distro table feature(show/hide column, pagination)
+ self._navigate_to_config_nav('distrostable', 7)
+ column_list = [
+ 'description',
+ 'templatefile',
+ 'layer_version__get_vcs_reference',
+ 'layer_version__layer__name',
+ ]
+ self._mixin_test_table_edit_column(
+ 'distrostable',
+ 'edit-columns-button',
+ [f'checkbox-{column}' for column in column_list]
+ )
+ self._navigate_to_config_nav('distrostable', 7)
+ # check show rows(pagination)
+ self._mixin_test_table_show_rows(
+ table_selector='distrostable',
+ to_skip=[150],
+ )
+
+ def test_single_layer_page(self):
+ """ Test layer page
+ - Check if title is displayed
+ - Check add/remove layer button works
+ - Check tabs(layers, recipes, machines) are displayed
+ - Check left section is displayed
+ - Check layer name
+ - Check layer summary
+ - Check layer description
+ """
+ url = reverse("layerdetails", args=(TestProjectPage.project_id, 8))
+ self.get(url)
+ self.wait_until_visible('.page-header')
+ # check title is displayed
+ self.assertTrue(self.find('.page-header h1').is_displayed())
+
+ # check add layer button works
+ remove_layer_btn = self.find('#add-remove-layer-btn')
+ remove_layer_btn.click()
+ self.wait_until_visible('#change-notification', poll=2)
+ change_notification = self.find('#change-notification')
+ self.assertTrue(
+ f'You have removed 1 layer from your project' in str(change_notification.text)
+ )
+ # check add layer button works, 18 is the random layer id
+ add_layer_btn = self.find('#add-remove-layer-btn')
+ add_layer_btn.click()
+ self.wait_until_visible('#change-notification')
+ change_notification = self.find('#change-notification')
+ self.assertTrue(
+ f'You have added 1 layer to your project' in str(change_notification.text)
+ )
+ # check tabs(layers, recipes, machines) are displayed
+ tabs = self.find_all('.nav-tabs li')
+ self.assertEqual(len(tabs), 3)
+ # Check first tab
+ tabs[0].click()
+ self.assertTrue(
+ 'active' in str(self.find('#information').get_attribute('class'))
+ )
+ # Check second tab
+ tabs[1].click()
+ self.assertTrue(
+ 'active' in str(self.find('#recipes').get_attribute('class'))
+ )
+ # Check third tab
+ tabs[2].click()
+ self.assertTrue(
+ 'active' in str(self.find('#machines').get_attribute('class'))
+ )
+ # Check left section is displayed
+ section = self.find('.well')
+ # Check layer name
+ self.assertTrue(
+ section.find_element(By.XPATH, '//h2[1]').is_displayed()
+ )
+ # Check layer summary
+ self.assertTrue("Summary" in section.text)
+ # Check layer description
+ self.assertTrue("Description" in section.text)
+
+ def test_single_recipe_page(self):
+ """ Test recipe page
+ - Check if title is displayed
+ - Check add recipe layer displayed
+ - Check left section is displayed
+ - Check recipe: name, summary, description, Version, Section,
+ License, Approx. packages included, Approx. size, Recipe file
+ """
+ url = reverse("recipedetails", args=(TestProjectPage.project_id, 53428))
+ self.get(url)
+ self.wait_until_visible('.page-header')
+ # check title is displayed
+ self.assertTrue(self.find('.page-header h1').is_displayed())
+ # check add recipe layer displayed
+ add_recipe_layer_btn = self.find('#add-layer-btn')
+ self.assertTrue(add_recipe_layer_btn.is_displayed())
+ # check left section is displayed
+ section = self.find('.well')
+ # Check recipe name
+ self.assertTrue(
+ section.find_element(By.XPATH, '//h2[1]').is_displayed()
+ )
+ # Check recipe sections details info are displayed
+ self.assertTrue("Summary" in section.text)
+ self.assertTrue("Description" in section.text)
+ self.assertTrue("Version" in section.text)
+ self.assertTrue("Section" in section.text)
+ self.assertTrue("License" in section.text)
+ self.assertTrue("Approx. packages included" in section.text)
+ self.assertTrue("Approx. package size" in section.text)
+ self.assertTrue("Recipe file" in section.text)
diff --git a/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py b/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py
new file mode 100644
index 0000000000..eb905ddf3f
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py
@@ -0,0 +1,528 @@
+#! /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 ElementClickInterceptedException, NoSuchElementException, 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 _create_project(self, project_name, **kwargs):
+ """ Create/Test new project using:
+ - Project Name: Any string
+ - Release: Any string
+ - Merge Toaster settings: True or False
+ """
+ release = kwargs.get('release', '3')
+ self.get(reverse('newproject'))
+ 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(release)
+
+ # check merge toaster settings
+ checkbox = self.find('.checkbox-mergeattr')
+ 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', poll=3)
+ 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:
+ 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('foo')
+ self.find('#build-button').click()
+ self.wait_until_present('#latest-builds')
+ # loop until reach the parsing state
+ wait_until_build(self, 'queued cloning starting parsing failed')
+ lastest_builds = self.driver.find_elements(
+ By.XPATH,
+ '//div[@id="latest-builds"]/div',
+ )
+ last_build = lastest_builds[0]
+ self.assertTrue(
+ 'foo' in str(last_build.text)
+ )
+ last_build = lastest_builds[0]
+ try:
+ cancel_button = last_build.find_element(
+ By.XPATH,
+ '//span[@class="cancel-build-btn pull-right alert-link"]',
+ )
+ cancel_button.click()
+ except NoSuchElementException:
+ # Skip if the build is already cancelled
+ pass
+ wait_until_build_cancelled(self)
+
+ 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 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
+ """
+ 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]
+
+ 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 = [
+ # config
+ [0, 'Configuration',
+ f"/toastergui/project/{TestProjectConfigTab.project_id}"],
+ # custom images
+ [2, 'Custom images',
+ f"/toastergui/project/{TestProjectConfigTab.project_id}/customimages"],
+ # image recipes
+ [3, 'Image recipes',
+ f"/toastergui/project/{TestProjectConfigTab.project_id}/images"],
+ # software recipes
+ [4, 'Software recipes',
+ f"/toastergui/project/{TestProjectConfigTab.project_id}/softwarerecipes"],
+ # machines
+ [5, 'Machines',
+ f"/toastergui/project/{TestProjectConfigTab.project_id}/machines"],
+ # layers
+ [6, 'Layers',
+ f"/toastergui/project/{TestProjectConfigTab.project_id}/layers"],
+ # distro
+ [7, 'Distros',
+ f"/toastergui/project/{TestProjectConfigTab.project_id}/distros"],
+ # [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)
+ if item.get_attribute('class') != 'active':
+ 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', poll=3)
+ # check at least some rows are visible
+ self.assertTrue(
+ len(self.find_all('#imagerecipestable tbody tr')) > 0
+ )
+
+ 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:
+ - 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
+ """
+ # Create a new project for this test
+ project_name = self._random_string(10)
+ self._create_project(project_name=project_name)
+ # 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]')
+ 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_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 = self.find(f'#change-{item_name}-toggle')
+ edit_btn.click()
+ 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 = self.find(f'#{item_name}-change-btn')
+ change_btn.click()
+ 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')
+ 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-section')
+ # Distro
+ check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section')
+
+ # 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')
+ # 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')
+ 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'
+ )
+ try:
+ dropdown_item.click()
+ except ElementClickInterceptedException:
+ self.skipTest(
+ "layer-container dropdown item click intercepted. Element not properly visible.")
+ add_layer_btn = layers.find_element(By.ID, 'add-layer-btn')
+ add_layer_btn.click()
+ 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)
+
+ def test_most_build_recipes(self):
+ """ Test most build recipes block contains"""
+ 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')
+ wait_until_build(self, 'queued cloning starting parsing failed')
+ lastest_builds = self.driver.find_elements(
+ By.XPATH,
+ '//div[@id="latest-builds"]/div'
+ )
+ self.assertTrue(len(lastest_builds) >= 2)
+ last_build = lastest_builds[0]
+ try:
+ cancel_button = last_build.find_element(
+ By.XPATH,
+ '//span[@class="cancel-build-btn pull-right alert-link"]',
+ )
+ cancel_button.click()
+ except NoSuchElementException:
+ # Skip if the build is already cancelled
+ pass
+ wait_until_build_cancelled(self)
+ # Create a new project for remaining asserts
+ project_name = self._random_string(10)
+ self._create_project(project_name=project_name, release='2')
+ current_url = self.driver.current_url
+ TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
+ url = current_url.split('?')[0]
+
+ # Create a new builds
+ self._create_builds()
+
+ # back to project page
+ self.driver.get(url)
+
+ self.wait_until_visible('#project-page', poll=3)
+
+ # Most built recipes
+ most_built_recipes = self.driver.find_element(
+ By.XPATH, '//*[@id="project-page"]/div[1]/div[3]')
+ title = most_built_recipes.find_element(By.TAG_NAME, 'h3')
+ self.assertTrue("Most built recipes" in title.text)
+ # check can select a recipe and build it
+ self.wait_until_visible('#freq-build-list', poll=3)
+ recipe_list = self.find('#freq-build-list')
+ recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li')
+ self.assertTrue(
+ len(recipe_list_items) > 0,
+ msg="Any recipes found in the most built recipes list",
+ )
+ rebuild_from_most_build_recipes(recipe_list_items)
+ TestProjectConfigTab.project_id = None # reset project id
+
+ def test_project_page_tab_importlayer(self):
+ """ Test project page tab import layer """
+ 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()
+ 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 """
+ 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()
+ 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(TestProjectConfigTab.project_id is not None)
+ self.assertTrue(
+ f"/toastergui/project/{TestProjectConfigTab.project_id}/newcustomimage" in str(
+ link_create_custom_image.get_attribute('href')
+ )
+ )
+ self.assertTrue(
+ "Create your first custom image" in str(
+ link_create_custom_image.text
+ )
+ )
+ TestProjectConfigTab.project_id = None # reset project id
+
+ 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)
+ """
+ self._navigate_to_project_page()
+ # 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)
diff --git a/bitbake/lib/toaster/tests/functional/utils.py b/bitbake/lib/toaster/tests/functional/utils.py
new file mode 100644
index 0000000000..7269fa1805
--- /dev/null
+++ b/bitbake/lib/toaster/tests/functional/utils.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# BitBake Toaster UI tests implementation
+#
+# Copyright (C) 2023 Savoir-faire Linux
+#
+# SPDX-License-Identifier: GPL-2.0-only
+
+
+from time import sleep
+from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException
+from selenium.webdriver.common.by import By
+
+from orm.models import Build
+
+
+def wait_until_build(test_instance, state):
+ timeout = 60
+ start_time = 0
+ build_state = ''
+ while True:
+ try:
+ if start_time > timeout:
+ raise TimeoutException(
+ f'Build did not reach {state} state within {timeout} seconds'
+ )
+ last_build_state = test_instance.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):
+ return str(build_state).lower()
+ if 'failed' in str(build_state).lower():
+ break
+ except NoSuchElementException:
+ continue
+ except TimeoutException:
+ break
+ start_time += 1
+ sleep(1) # take a breath and try again
+
+def wait_until_build_cancelled(test_instance):
+ """ Cancel build take a while sometime, the method is to wait driver action
+ until build being cancelled
+ """
+ timeout = 30
+ start_time = 0
+ build = None
+ while True:
+ try:
+ if start_time > timeout:
+ raise TimeoutException(
+ f'Build did not reach cancelled state within {timeout} seconds'
+ )
+ last_build_state = test_instance.driver.find_element(
+ By.XPATH,
+ '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]',
+ )
+ build_state = last_build_state.get_attribute(
+ 'data-build-state')
+ if 'failed' in str(build_state).lower():
+ break
+ if 'cancelling' in str(build_state).lower():
+ # Change build state to cancelled
+ if not build: # get build object only once
+ build = Build.objects.last()
+ build.outcome = Build.CANCELLED
+ build.save()
+ if 'cancelled' in str(build_state).lower():
+ break
+ except NoSuchElementException:
+ continue
+ except StaleElementReferenceException:
+ continue
+ except TimeoutException:
+ break
+ start_time += 1
+ sleep(1) # take a breath and try again
+
+def get_projectId_from_url(url):
+ # url = 'http://domainename.com/toastergui/project/1656/whatever
+ # or url = 'http://domainename.com/toastergui/project/1/
+ # or url = 'http://domainename.com/toastergui/project/186
+ assert '/toastergui/project/' in url, "URL is not valid"
+ url_to_list = url.split('/toastergui/project/')
+ return int(url_to_list[1].split('/')[0]) # project_id