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 5c4ea71794..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
11import logging 11import logging
12import subprocess 12import subprocess
13import signal 13import signal
14import time
15import re 14import re
16 15
17from tests.browser.selenium_helpers_base import SeleniumTestCaseBase 16from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
18from tests.builds.buildtest import load_build_environment 17from selenium.webdriver.common.by import By
18from selenium.common.exceptions import NoSuchElementException
19 19
20logger = logging.getLogger("toaster") 20logger = logging.getLogger("toaster")
21toaster_processes = []
21 22
22class SeleniumFunctionalTestCase(SeleniumTestCaseBase): 23class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
23 wait_toaster_time = 5 24 wait_toaster_time = 10
24 25
25 @classmethod 26 @classmethod
26 def setUpClass(cls): 27 def setUpClass(cls):
27 # So that the buildinfo helper uses the test database' 28 # So that the buildinfo helper uses the test database'
28 if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \ 29 if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \
29 'toastermain.settings_test': 30 'toastermain.settings_test':
30 raise RuntimeError("Please initialise django with the tests settings: " \ 31 raise RuntimeError("Please initialise django with the tests settings: "
31 "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") 32 "DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
32 33
33 load_build_environment() 34 # Wait for any known toaster processes to exit
35 global toaster_processes
36 for toaster_process in toaster_processes:
37 try:
38 os.waitpid(toaster_process, os.WNOHANG)
39 except ChildProcessError:
40 pass
34 41
35 # start toaster 42 # start toaster
36 cmd = "bash -c 'source toaster start'" 43 cmd = "bash -c 'source toaster start'"
37 p = subprocess.Popen( 44 start_process = subprocess.Popen(
38 cmd, 45 cmd,
39 cwd=os.environ.get("BUILDDIR"), 46 cwd=os.environ.get("BUILDDIR"),
40 shell=True) 47 shell=True)
41 if p.wait() != 0: 48 toaster_processes = [start_process.pid]
42 raise RuntimeError("Can't initialize toaster") 49 if start_process.wait() != 0:
50 port_use = os.popen("lsof -i -P -n | grep '8000 (LISTEN)'").read().strip()
51 message = ''
52 if port_use:
53 process_id = port_use.split()[1]
54 process = os.popen(f"ps -o cmd= -p {process_id}").read().strip()
55 message = f"Port 8000 occupied by {process}"
56 raise RuntimeError(f"Can't initialize toaster. {message}")
57
58 builddir = os.environ.get("BUILDDIR")
59 with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f:
60 toaster_processes.append(int(f.read()))
61 with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f:
62 toaster_processes.append(int(f.read()))
43 63
44 super(SeleniumFunctionalTestCase, cls).setUpClass() 64 super(SeleniumFunctionalTestCase, cls).setUpClass()
45 cls.live_server_url = 'http://localhost:8000/' 65 cls.live_server_url = 'http://localhost:8000/'
@@ -48,22 +68,30 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
48 def tearDownClass(cls): 68 def tearDownClass(cls):
49 super(SeleniumFunctionalTestCase, cls).tearDownClass() 69 super(SeleniumFunctionalTestCase, cls).tearDownClass()
50 70
51 # XXX: source toaster stop gets blocked, to review why? 71 global toaster_processes
52 # from now send SIGTERM by hand
53 time.sleep(cls.wait_toaster_time)
54 builddir = os.environ.get("BUILDDIR")
55 72
56 with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f: 73 cmd = "bash -c 'source toaster stop'"
57 toastermain_pid = int(f.read()) 74 stop_process = subprocess.Popen(
58 os.kill(toastermain_pid, signal.SIGTERM) 75 cmd,
59 with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: 76 cwd=os.environ.get("BUILDDIR"),
60 runbuilds_pid = int(f.read()) 77 shell=True)
61 os.kill(runbuilds_pid, signal.SIGTERM) 78 # Toaster stop has been known to hang in these tests so force kill if it stalls
79 try:
80 if stop_process.wait(cls.wait_toaster_time) != 0:
81 raise Exception('Toaster stop process failed')
82 except Exception as e:
83 if e is subprocess.TimeoutExpired:
84 print('Toaster stop process took too long. Force killing toaster...')
85 else:
86 print('Toaster stop process failed. Force killing toaster...')
87 stop_process.kill()
88 for toaster_process in toaster_processes:
89 os.kill(toaster_process, signal.SIGTERM)
62 90
63 91
64 def get_URL(self): 92 def get_URL(self):
65 rc=self.get_page_source() 93 rc=self.get_page_source()
66 project_url=re.search("(projectPageUrl\s:\s\")(.*)(\",)",rc) 94 project_url=re.search(r"(projectPageUrl\s:\s\")(.*)(\",)",rc)
67 return project_url.group(2) 95 return project_url.group(2)
68 96
69 97
@@ -74,8 +102,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
74 """ 102 """
75 try: 103 try:
76 table_element = self.get_table_element(table_id) 104 table_element = self.get_table_element(table_id)
77 element = table_element.find_element_by_link_text(link_text) 105 element = table_element.find_element(By.LINK_TEXT, link_text)
78 except self.NoSuchElementException: 106 except NoSuchElementException:
79 print('no element found') 107 print('no element found')
80 raise 108 raise
81 return element 109 return element
@@ -85,8 +113,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
85#return whole-table element 113#return whole-table element
86 element_xpath = "//*[@id='" + table_id + "']" 114 element_xpath = "//*[@id='" + table_id + "']"
87 try: 115 try:
88 element = self.driver.find_element_by_xpath(element_xpath) 116 element = self.driver.find_element(By.XPATH, element_xpath)
89 except self.NoSuchElementException: 117 except NoSuchElementException:
90 raise 118 raise
91 return element 119 return element
92 row = coordinate[0] 120 row = coordinate[0]
@@ -95,8 +123,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
95#return whole-row element 123#return whole-row element
96 element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]" 124 element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]"
97 try: 125 try:
98 element = self.driver.find_element_by_xpath(element_xpath) 126 element = self.driver.find_element(By.XPATH, element_xpath)
99 except self.NoSuchElementException: 127 except NoSuchElementException:
100 return False 128 return False
101 return element 129 return element
102#now we are looking for an element with specified X and Y 130#now we are looking for an element with specified X and Y
@@ -104,7 +132,7 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
104 132
105 element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" 133 element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]"
106 try: 134 try:
107 element = self.driver.find_element_by_xpath(element_xpath) 135 element = self.driver.find_element(By.XPATH, element_xpath)
108 except self.NoSuchElementException: 136 except NoSuchElementException:
109 return False 137 return False
110 return element 138 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 @@
1#! /usr/bin/env python3
2# BitBake Toaster UI tests implementation
3#
4# Copyright (C) 2023 Savoir-faire Linux
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import re
10import pytest
11from django.urls import reverse
12from selenium.webdriver.support.select import Select
13from tests.functional.functional_helpers import SeleniumFunctionalTestCase
14from orm.models import Project
15from selenium.webdriver.common.by import By
16
17
18@pytest.mark.django_db
19@pytest.mark.order("last")
20class TestCreateNewProject(SeleniumFunctionalTestCase):
21
22 def _create_test_new_project(
23 self,
24 project_name,
25 release,
26 release_title,
27 merge_toaster_settings,
28 ):
29 """ Create/Test new project using:
30 - Project Name: Any string
31 - Release: Any string
32 - Merge Toaster settings: True or False
33 """
34 self.get(reverse('newproject'))
35 self.wait_until_visible('#new-project-name', poll=3)
36 self.driver.find_element(By.ID,
37 "new-project-name").send_keys(project_name)
38
39 select = Select(self.find('#projectversion'))
40 select.select_by_value(release)
41
42 # check merge toaster settings
43 checkbox = self.find('.checkbox-mergeattr')
44 if merge_toaster_settings:
45 if not checkbox.is_selected():
46 checkbox.click()
47 else:
48 if checkbox.is_selected():
49 checkbox.click()
50
51 self.driver.find_element(By.ID, "create-project-button").click()
52
53 element = self.wait_until_visible('#project-created-notification', poll=3)
54 self.assertTrue(
55 self.element_exists('#project-created-notification'),
56 f"Project:{project_name} creation notification not shown"
57 )
58 self.assertTrue(
59 project_name in element.text,
60 f"New project name:{project_name} not in new project notification"
61 )
62 self.assertTrue(
63 Project.objects.filter(name=project_name).count(),
64 f"New project:{project_name} not found in database"
65 )
66
67 # check release
68 self.assertTrue(re.search(
69 release_title,
70 self.driver.find_element(By.XPATH,
71 "//span[@id='project-release-title']"
72 ).text),
73 'The project release is not defined')
74
75 def test_create_new_project_master(self):
76 """ Test create new project using:
77 - Project Name: Any string
78 - Release: Yocto Project master (option value: 3)
79 - Merge Toaster settings: False
80 """
81 release = '3'
82 release_title = 'Yocto Project master'
83 project_name = 'projectmaster'
84 self._create_test_new_project(
85 project_name,
86 release,
87 release_title,
88 False,
89 )
90
91 def test_create_new_project_kirkstone(self):
92 """ Test create new project using:
93 - Project Name: Any string
94 - Release: Yocto Project 4.0 "Kirkstone" (option value: 1)
95 - Merge Toaster settings: True
96 """
97 release = '1'
98 release_title = 'Yocto Project 4.0 "Kirkstone"'
99 project_name = 'projectkirkstone'
100 self._create_test_new_project(
101 project_name,
102 release,
103 release_title,
104 True,
105 )
106
107 def test_create_new_project_dunfell(self):
108 """ Test create new project using:
109 - Project Name: Any string
110 - Release: Yocto Project 3.1 "Dunfell" (option value: 5)
111 - Merge Toaster settings: False
112 """
113 release = '5'
114 release_title = 'Yocto Project 3.1 "Dunfell"'
115 project_name = 'projectdunfell'
116 self._create_test_new_project(
117 project_name,
118 release,
119 release_title,
120 False,
121 )
122
123 def test_create_new_project_local(self):
124 """ Test create new project using:
125 - Project Name: Any string
126 - Release: Local Yocto Project (option value: 2)
127 - Merge Toaster settings: True
128 """
129 release = '2'
130 release_title = 'Local Yocto Project'
131 project_name = 'projectlocal'
132 self._create_test_new_project(
133 project_name,
134 release,
135 release_title,
136 True,
137 )
138
139 def test_create_new_project_without_name(self):
140 """ Test create new project without project name """
141 self.get(reverse('newproject'))
142
143 select = Select(self.find('#projectversion'))
144 select.select_by_value(str(3))
145
146 # Check input name has required attribute
147 input_name = self.driver.find_element(By.ID, "new-project-name")
148 self.assertIsNotNone(input_name.get_attribute('required'),
149 'Input name has not required attribute')
150
151 # Check create button is disabled
152 create_btn = self.driver.find_element(By.ID, "create-project-button")
153 self.assertIsNotNone(create_btn.get_attribute('disabled'),
154 'Create button is not disabled')
155
156 def test_import_new_project(self):
157 """ Test import new project using:
158 - Project Name: Any string
159 - Project type: select (Import command line project)
160 - Import existing project directory: Wrong Path
161 """
162 project_name = 'projectimport'
163 self.get(reverse('newproject'))
164 self.driver.find_element(By.ID,
165 "new-project-name").send_keys(project_name)
166 # select import project
167 self.find('#type-import').click()
168
169 # set wrong path
170 wrong_path = '/wrongpath'
171 self.driver.find_element(By.ID,
172 "import-project-dir").send_keys(wrong_path)
173 self.driver.find_element(By.ID, "create-project-button").click()
174
175 # check error message
176 self.assertTrue(self.element_exists('.alert-danger'),
177 'Allert message not shown')
178 self.assertTrue(wrong_path in self.find('.alert-danger').text,
179 "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 @@
8# 8#
9 9
10import re 10import re
11from django.urls import reverse
12import pytest
11from tests.functional.functional_helpers import SeleniumFunctionalTestCase 13from tests.functional.functional_helpers import SeleniumFunctionalTestCase
12from orm.models import Project 14from orm.models import Project
15from selenium.webdriver.common.by import By
13 16
17from tests.functional.utils import get_projectId_from_url
18
19
20@pytest.mark.django_db
21@pytest.mark.order("second_to_last")
14class FuntionalTestBasic(SeleniumFunctionalTestCase): 22class FuntionalTestBasic(SeleniumFunctionalTestCase):
23 """Basic functional tests for Toaster"""
24 project_id = None
25
26 def setUp(self):
27 super(FuntionalTestBasic, self).setUp()
28 if not FuntionalTestBasic.project_id:
29 self._create_slenium_project()
30 current_url = self.driver.current_url
31 FuntionalTestBasic.project_id = get_projectId_from_url(current_url)
15 32
16# testcase (1514) 33# testcase (1514)
17 def test_create_slenium_project(self): 34 def _create_slenium_project(self):
18 project_name = 'selenium-project' 35 project_name = 'selenium-project'
19 self.get('') 36 self.get(reverse('newproject'))
20 self.driver.find_element_by_link_text("To start building, create your first Toaster project").click() 37 self.wait_until_visible('#new-project-name', poll=3)
21 self.driver.find_element_by_id("new-project-name").send_keys(project_name) 38 self.driver.find_element(By.ID, "new-project-name").send_keys(project_name)
22 self.driver.find_element_by_id('projectversion').click() 39 self.driver.find_element(By.ID, 'projectversion').click()
23 self.driver.find_element_by_id("create-project-button").click() 40 self.driver.find_element(By.ID, "create-project-button").click()
24 element = self.wait_until_visible('#project-created-notification') 41 element = self.wait_until_visible('#project-created-notification', poll=10)
25 self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown') 42 self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown')
26 self.assertTrue(project_name in element.text, 43 self.assertTrue(project_name in element.text,
27 "New project name not in new project notification") 44 "New project name not in new project notification")
28 self.assertTrue(Project.objects.filter(name=project_name).count(), 45 self.assertTrue(Project.objects.filter(name=project_name).count(),
29 "New project not found in database") 46 "New project not found in database")
47 return Project.objects.last().id
30 48
31 # testcase (1515) 49 # testcase (1515)
32 def test_verify_left_bar_menu(self): 50 def test_verify_left_bar_menu(self):
33 self.get('') 51 self.get(reverse('all-projects'))
34 self.wait_until_visible('#projectstable') 52 self.wait_until_present('#projectstable', poll=10)
35 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 53 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
54 self.wait_until_present('#config-nav', poll=10)
36 self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist') 55 self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist')
37 project_URL=self.get_URL() 56 project_URL=self.get_URL()
38 self.driver.find_element_by_xpath('//a[@href="'+project_URL+'"]').click() 57 self.driver.find_element(By.XPATH, '//a[@href="'+project_URL+'"]').click()
58 self.wait_until_present('#config-nav', poll=10)
39 59
40 try: 60 try:
41 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click() 61 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click()
42 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') 62 self.wait_until_present('#config-nav', poll=10)
63 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')
43 except: 64 except:
44 self.fail(msg='No Custom images tab available') 65 self.fail(msg='No Custom images tab available')
45 66
46 try: 67 try:
47 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click() 68 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click()
48 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') 69 self.wait_until_present('#config-nav', poll=10)
70 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')
49 except: 71 except:
50 self.fail(msg='No Compatible image tab available') 72 self.fail(msg='No Compatible image tab available')
51 73
52 try: 74 try:
53 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click() 75 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click()
54 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') 76 self.wait_until_present('#config-nav', poll=10)
77 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')
55 except: 78 except:
56 self.fail(msg='No Compatible software recipe tab available') 79 self.fail(msg='No Compatible software recipe tab available')
57 80
58 try: 81 try:
59 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click() 82 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click()
60 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') 83 self.wait_until_present('#config-nav', poll=10)
84 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')
61 except: 85 except:
62 self.fail(msg='No Compatible machines tab available') 86 self.fail(msg='No Compatible machines tab available')
63 87
64 try: 88 try:
65 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click() 89 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click()
66 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') 90 self.wait_until_present('#config-nav', poll=10)
91 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')
67 except: 92 except:
68 self.fail(msg='No Compatible layers tab available') 93 self.fail(msg='No Compatible layers tab available')
69 94
70 try: 95 try:
71 self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click() 96 self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click()
72 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') 97 self.wait_until_present('#config-nav', poll=10)
98 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')
73 except: 99 except:
74 self.fail(msg='No Bitbake variables tab available') 100 self.fail(msg='No Bitbake variables tab available')
75 101
76# testcase (1516) 102# testcase (1516)
77 def test_review_configuration_information(self): 103 def test_review_configuration_information(self):
78 self.get('') 104 self.get(reverse('all-projects'))
79 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 105 self.wait_until_present('#projectstable', poll=10)
80 self.wait_until_visible('#projectstable')
81 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 106 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
82 project_URL=self.get_URL() 107 project_URL=self.get_URL()
83 108 self.wait_until_present('#config-nav', poll=10)
84 try: 109 try:
85 self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') 110 self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
86 self.assertTrue(re.search("qemux86",self.driver.find_element_by_xpath("//span[@id='project-machine-name']").text),'The machine type is not assigned') 111 self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.XPATH, "//span[@id='project-machine-name']").text),'The machine type is not assigned')
87 self.driver.find_element_by_xpath("//span[@id='change-machine-toggle']").click() 112 self.driver.find_element(By.XPATH, "//span[@id='change-machine-toggle']").click()
88 self.wait_until_visible('#select-machine-form') 113 self.wait_until_visible('#select-machine-form', poll=10)
89 self.wait_until_visible('#cancel-machine-change') 114 self.wait_until_visible('#cancel-machine-change', poll=10)
90 self.driver.find_element_by_xpath("//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click() 115 self.driver.find_element(By.XPATH, "//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click()
91 except: 116 except:
92 self.fail(msg='The machine information is wrong in the configuration page') 117 self.fail(msg='The machine information is wrong in the configuration page')
93 118
94 try: 119 try:
95 self.driver.find_element_by_id('no-most-built') 120 self.driver.find_element(By.ID, 'no-most-built')
96 except: 121 except:
97 self.fail(msg='No Most built information in project detail page') 122 self.fail(msg='No Most built information in project detail page')
98 123
99 try: 124 try:
100 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') 125 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')
101 except: 126 except:
102 self.fail(msg='No project release title information in project detail page') 127 self.fail(msg='No project release title information in project detail page')
103 128
104 try: 129 try:
105 self.driver.find_element_by_xpath("//div[@id='layer-container']") 130 self.driver.find_element(By.XPATH, "//div[@id='layer-container']")
106 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') 131 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')
107 layer_list = self.driver.find_element_by_id("layers-in-project-list") 132 layer_list = self.driver.find_element(By.ID, "layers-in-project-list")
108 layers = layer_list.find_elements_by_tag_name("li") 133 layers = layer_list.find_elements(By.TAG_NAME, "li")
109 for layer in layers: 134 for layer in layers:
110 if re.match ("openembedded-core",layer.text): 135 if re.match ("openembedded-core",layer.text):
111 print ("openembedded-core layer is a default layer in the project configuration") 136 print ("openembedded-core layer is a default layer in the project configuration")
@@ -120,61 +145,60 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase):
120 145
121# testcase (1517) 146# testcase (1517)
122 def test_verify_machine_information(self): 147 def test_verify_machine_information(self):
123 self.get('') 148 self.get(reverse('all-projects'))
124 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 149 self.wait_until_present('#projectstable', poll=10)
125 self.wait_until_visible('#projectstable')
126 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 150 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
151 self.wait_until_present('#config-nav', poll=10)
127 152
128 try: 153 try:
129 self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') 154 self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
130 self.assertTrue(re.search("qemux86",self.driver.find_element_by_id("project-machine-name").text),'The machine type is not assigned') 155 self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.ID, "project-machine-name").text),'The machine type is not assigned')
131 self.driver.find_element_by_id("change-machine-toggle").click() 156 self.driver.find_element(By.ID, "change-machine-toggle").click()
132 self.wait_until_visible('#select-machine-form') 157 self.wait_until_visible('#select-machine-form', poll=10)
133 self.wait_until_visible('#cancel-machine-change') 158 self.wait_until_visible('#cancel-machine-change', poll=10)
134 self.driver.find_element_by_id("cancel-machine-change").click() 159 self.driver.find_element(By.ID, "cancel-machine-change").click()
135 except: 160 except:
136 self.fail(msg='The machine information is wrong in the configuration page') 161 self.fail(msg='The machine information is wrong in the configuration page')
137 162
138# testcase (1518) 163# testcase (1518)
139 def test_verify_most_built_recipes_information(self): 164 def test_verify_most_built_recipes_information(self):
140 self.get('') 165 self.get(reverse('all-projects'))
141 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 166 self.wait_until_present('#projectstable', poll=10)
142 self.wait_until_visible('#projectstable')
143 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 167 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
168 self.wait_until_present('#config-nav', poll=10)
144 project_URL=self.get_URL() 169 project_URL=self.get_URL()
145
146 try: 170 try:
147 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') 171 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')
148 self.driver.find_element_by_xpath("//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click() 172 self.driver.find_element(By.XPATH, "//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click()
149 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') 173 self.wait_until_present('#config-nav', poll=10)
174 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')
150 except: 175 except:
151 self.fail(msg='No Most built information in project detail page') 176 self.fail(msg='No Most built information in project detail page')
152 177
153# testcase (1519) 178# testcase (1519)
154 def test_verify_project_release_information(self): 179 def test_verify_project_release_information(self):
155 self.get('') 180 self.get(reverse('all-projects'))
156 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 181 self.wait_until_present('#projectstable', poll=10)
157 self.wait_until_visible('#projectstable')
158 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 182 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
183 self.wait_until_present('#config-nav', poll=10)
159 184
160 try: 185 try:
161 self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_id("project-release-title").text),'The project release is not defined') 186 self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.ID, "project-release-title").text),'The project release is not defined')
162 except: 187 except:
163 self.fail(msg='No project release title information in project detail page') 188 self.fail(msg='No project release title information in project detail page')
164 189
165# testcase (1520) 190# testcase (1520)
166 def test_verify_layer_information(self): 191 def test_verify_layer_information(self):
167 self.get('') 192 self.get(reverse('all-projects'))
168 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 193 self.wait_until_present('#projectstable', poll=10)
169 self.wait_until_visible('#projectstable')
170 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 194 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
195 self.wait_until_present('#config-nav', poll=10)
171 project_URL=self.get_URL() 196 project_URL=self.get_URL()
172
173 try: 197 try:
174 self.driver.find_element_by_xpath("//div[@id='layer-container']") 198 self.driver.find_element(By.XPATH, "//div[@id='layer-container']")
175 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') 199 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')
176 layer_list = self.driver.find_element_by_id("layers-in-project-list") 200 layer_list = self.driver.find_element(By.ID, "layers-in-project-list")
177 layers = layer_list.find_elements_by_tag_name("li") 201 layers = layer_list.find_elements(By.TAG_NAME, "li")
178 202
179 for layer in layers: 203 for layer in layers:
180 if re.match ("openembedded-core",layer.text): 204 if re.match ("openembedded-core",layer.text):
@@ -186,43 +210,46 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase):
186 else: 210 else:
187 self.fail(msg='default layers are missing from the project configuration') 211 self.fail(msg='default layers are missing from the project configuration')
188 212
189 self.driver.find_element_by_xpath("//input[@id='layer-add-input']") 213 self.driver.find_element(By.XPATH, "//input[@id='layer-add-input']")
190 self.driver.find_element_by_xpath("//button[@id='add-layer-btn']") 214 self.driver.find_element(By.XPATH, "//button[@id='add-layer-btn']")
191 self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']") 215 self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']")
192 self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]") 216 self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]")
193 except: 217 except:
194 self.fail(msg='No Layer information in project detail page') 218 self.fail(msg='No Layer information in project detail page')
195 219
196# testcase (1521) 220# testcase (1521)
197 def test_verify_project_detail_links(self): 221 def test_verify_project_detail_links(self):
198 self.get('') 222 self.get(reverse('all-projects'))
199 self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() 223 self.wait_until_present('#projectstable', poll=10)
200 self.wait_until_visible('#projectstable')
201 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() 224 self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
225 self.wait_until_present('#config-nav', poll=10)
202 project_URL=self.get_URL() 226 project_URL=self.get_URL()
203 227 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()
204 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() 228 self.wait_until_present('#config-nav', poll=10)
205 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') 229 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')
206 230
207 try: 231 try:
208 self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click() 232 self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click()
209 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') 233 self.wait_until_visible('#project-topbar', poll=10)
210 self.driver.find_element_by_xpath("//div[@id='empty-state-projectbuildstable']") 234 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')
235 self.driver.find_element(By.XPATH, "//div[@id='empty-state-projectbuildstable']")
211 except: 236 except:
212 self.fail(msg='Builds tab information is not present') 237 self.fail(msg='Builds tab information is not present')
213 238
214 try: 239 try:
215 self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click() 240 self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click()
216 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') 241 self.wait_until_visible('#project-topbar', poll=10)
217 self.driver.find_element_by_xpath("//fieldset[@id='repo-select']") 242 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')
218 self.driver.find_element_by_xpath("//fieldset[@id='git-repo']") 243 self.driver.find_element(By.XPATH, "//fieldset[@id='repo-select']")
244 self.driver.find_element(By.XPATH, "//fieldset[@id='git-repo']")
219 except: 245 except:
220 self.fail(msg='Import layer tab not loading properly') 246 self.fail(msg='Import layer tab not loading properly')
221 247
222 try: 248 try:
223 self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click() 249 self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click()
224 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') 250 self.wait_until_visible('#project-topbar', poll=10)
225 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') 251 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')
252 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')
226 except: 253 except:
227 self.fail(msg='New custom image tab not loading properly') 254 self.fail(msg='New custom image tab not loading properly')
228 255
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 @@
1#! /usr/bin/env python3 #
2# BitBake Toaster UI tests implementation
3#
4# Copyright (C) 2023 Savoir-faire Linux
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import string
10import random
11import pytest
12from django.urls import reverse
13from selenium.webdriver import Keys
14from selenium.webdriver.support.select import Select
15from selenium.common.exceptions import TimeoutException
16from tests.functional.functional_helpers import SeleniumFunctionalTestCase
17from selenium.webdriver.common.by import By
18
19from .utils import get_projectId_from_url
20
21
22@pytest.mark.django_db
23@pytest.mark.order("last")
24class TestProjectConfig(SeleniumFunctionalTestCase):
25 project_id = None
26 PROJECT_NAME = 'TestProjectConfig'
27 INVALID_PATH_START_TEXT = 'The directory path should either start with a /'
28 INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \
29 'any of these characters'
30
31 def _create_project(self, project_name):
32 """ Create/Test new project using:
33 - Project Name: Any string
34 - Release: Any string
35 - Merge Toaster settings: True or False
36 """
37 self.get(reverse('newproject'))
38 self.wait_until_visible('#new-project-name', poll=2)
39 self.find("#new-project-name").send_keys(project_name)
40 select = Select(self.find("#projectversion"))
41 select.select_by_value('3')
42
43 # check merge toaster settings
44 checkbox = self.find('.checkbox-mergeattr')
45 if not checkbox.is_selected():
46 checkbox.click()
47
48 if self.PROJECT_NAME != 'TestProjectConfig':
49 # Reset project name if it's not the default one
50 self.PROJECT_NAME = 'TestProjectConfig'
51
52 self.find("#create-project-button").click()
53
54 try:
55 self.wait_until_visible('#hint-error-project-name', poll=2)
56 url = reverse('project', args=(TestProjectConfig.project_id, ))
57 self.get(url)
58 self.wait_until_visible('#config-nav', poll=3)
59 except TimeoutException:
60 self.wait_until_visible('#config-nav', poll=3)
61
62 def _random_string(self, length):
63 return ''.join(
64 random.choice(string.ascii_letters) for _ in range(length)
65 )
66
67 def _get_config_nav_item(self, index):
68 config_nav = self.find('#config-nav')
69 return config_nav.find_elements(By.TAG_NAME, 'li')[index]
70
71 def _navigate_bbv_page(self):
72 """ Navigate to project BitBake variables page """
73 # check if the menu is displayed
74 if TestProjectConfig.project_id is None:
75 self._create_project(project_name=self._random_string(10))
76 current_url = self.driver.current_url
77 TestProjectConfig.project_id = get_projectId_from_url(current_url)
78 else:
79 url = reverse('projectconf', args=(TestProjectConfig.project_id,))
80 self.get(url)
81 self.wait_until_visible('#config-nav', poll=3)
82 bbv_page_link = self._get_config_nav_item(9)
83 bbv_page_link.click()
84 self.wait_until_visible('#config-nav', poll=3)
85
86 def test_no_underscore_iamgefs_type(self):
87 """
88 Should not accept IMAGEFS_TYPE with an underscore
89 """
90 self._navigate_bbv_page()
91 imagefs_type = "foo_bar"
92
93 self.wait_until_visible('#change-image_fstypes-icon', poll=2)
94
95 self.click('#change-image_fstypes-icon')
96
97 self.enter_text('#new-imagefs_types', imagefs_type)
98
99 element = self.wait_until_visible('#hintError-image-fs_type', poll=2)
100
101 self.assertTrue(("A valid image type cannot include underscores" in element.text),
102 "Did not find underscore error message")
103
104 def test_checkbox_verification(self):
105 """
106 Should automatically check the checkbox if user enters value
107 text box, if value is there in the checkbox.
108 """
109 self._navigate_bbv_page()
110
111 imagefs_type = "btrfs"
112
113 self.wait_until_visible('#change-image_fstypes-icon', poll=2)
114
115 self.click('#change-image_fstypes-icon')
116
117 self.enter_text('#new-imagefs_types', imagefs_type)
118
119 checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']")
120
121 for checkbox in checkboxes:
122 if checkbox.get_attribute("value") == "btrfs":
123 self.assertEqual(checkbox.is_selected(), True)
124
125 def test_textbox_with_checkbox_verification(self):
126 """
127 Should automatically add or remove value in textbox, if user checks
128 or unchecks checkboxes.
129 """
130 self._navigate_bbv_page()
131
132 self.wait_until_visible('#change-image_fstypes-icon', poll=2)
133
134 self.click('#change-image_fstypes-icon')
135
136 checkboxes_selector = '.fs-checkbox-fstypes'
137
138 self.wait_until_visible(checkboxes_selector, poll=2)
139 checkboxes = self.find_all(checkboxes_selector)
140
141 for checkbox in checkboxes:
142 if checkbox.get_attribute("value") == "cpio":
143 checkbox.click()
144 element = self.driver.find_element(By.ID, 'new-imagefs_types')
145
146 self.wait_until_visible('#new-imagefs_types', poll=2)
147
148 self.assertTrue(("cpio" in element.get_attribute('value'),
149 "Imagefs not added into the textbox"))
150 checkbox.click()
151 self.assertTrue(("cpio" not in element.text),
152 "Image still present in the textbox")
153
154 def test_set_download_dir(self):
155 """
156 Validate the allowed and disallowed types in the directory field for
157 DL_DIR
158 """
159 self._navigate_bbv_page()
160
161 # activate the input to edit download dir
162 try:
163 change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2)
164 except TimeoutException:
165 # If download dir is not displayed, test is skipped
166 change_dl_dir_btn = None
167
168 if change_dl_dir_btn:
169 change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2)
170 change_dl_dir_btn.click()
171
172 # downloads dir path doesn't start with / or ${...}
173 input_field = self.wait_until_visible('#new-dl_dir', poll=2)
174 input_field.clear()
175 self.enter_text('#new-dl_dir', 'home/foo')
176 element = self.wait_until_visible('#hintError-initialChar-dl_dir', poll=2)
177
178 msg = 'downloads directory path starts with invalid character but ' \
179 'treated as valid'
180 self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)
181
182 # downloads dir path has a space
183 self.driver.find_element(By.ID, 'new-dl_dir').clear()
184 self.enter_text('#new-dl_dir', '/foo/bar a')
185
186 element = self.wait_until_visible('#hintError-dl_dir', poll=2)
187 msg = 'downloads directory path characters invalid but treated as valid'
188 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
189
190 # downloads dir path starts with ${...} but has a space
191 self.driver.find_element(By.ID,'new-dl_dir').clear()
192 self.enter_text('#new-dl_dir', '${TOPDIR}/down foo')
193
194 element = self.wait_until_visible('#hintError-dl_dir', poll=2)
195 msg = 'downloads directory path characters invalid but treated as valid'
196 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
197
198 # downloads dir path starts with /
199 self.driver.find_element(By.ID,'new-dl_dir').clear()
200 self.enter_text('#new-dl_dir', '/bar/foo')
201
202 hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
203 self.assertEqual(hidden_element.is_displayed(), False,
204 'downloads directory path valid but treated as invalid')
205
206 # downloads dir path starts with ${...}
207 self.driver.find_element(By.ID,'new-dl_dir').clear()
208 self.enter_text('#new-dl_dir', '${TOPDIR}/down')
209
210 hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
211 self.assertEqual(hidden_element.is_displayed(), False,
212 'downloads directory path valid but treated as invalid')
213
214 def test_set_sstate_dir(self):
215 """
216 Validate the allowed and disallowed types in the directory field for
217 SSTATE_DIR
218 """
219 self._navigate_bbv_page()
220
221 try:
222 btn_chg_sstate_dir = self.wait_until_visible(
223 '#change-sstate_dir-icon',
224 poll=2
225 )
226 self.click('#change-sstate_dir-icon')
227 except TimeoutException:
228 # If sstate_dir is not displayed, test is skipped
229 btn_chg_sstate_dir = None
230
231 if btn_chg_sstate_dir: # Skip continuation if sstate_dir is not displayed
232 # path doesn't start with / or ${...}
233 input_field = self.wait_until_visible('#new-sstate_dir', poll=2)
234 input_field.clear()
235 self.enter_text('#new-sstate_dir', 'home/foo')
236 element = self.wait_until_visible('#hintError-initialChar-sstate_dir', poll=2)
237
238 msg = 'sstate directory path starts with invalid character but ' \
239 'treated as valid'
240 self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)
241
242 # path has a space
243 self.driver.find_element(By.ID, 'new-sstate_dir').clear()
244 self.enter_text('#new-sstate_dir', '/foo/bar a')
245
246 element = self.wait_until_visible('#hintError-sstate_dir', poll=2)
247 msg = 'sstate directory path characters invalid but treated as valid'
248 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
249
250 # path starts with ${...} but has a space
251 self.driver.find_element(By.ID,'new-sstate_dir').clear()
252 self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo')
253
254 element = self.wait_until_visible('#hintError-sstate_dir', poll=2)
255 msg = 'sstate directory path characters invalid but treated as valid'
256 self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
257
258 # path starts with /
259 self.driver.find_element(By.ID,'new-sstate_dir').clear()
260 self.enter_text('#new-sstate_dir', '/bar/foo')
261
262 hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
263 self.assertEqual(hidden_element.is_displayed(), False,
264 'sstate directory path valid but treated as invalid')
265
266 # paths starts with ${...}
267 self.driver.find_element(By.ID, 'new-sstate_dir').clear()
268 self.enter_text('#new-sstate_dir', '${TOPDIR}/down')
269
270 hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
271 self.assertEqual(hidden_element.is_displayed(), False,
272 'sstate directory path valid but treated as invalid')
273
274 def _change_bbv_value(self, **kwargs):
275 var_name, field, btn_id, input_id, value, save_btn, *_ = kwargs.values()
276 """ Change bitbake variable value """
277 self._navigate_bbv_page()
278 self.wait_until_visible(f'#{btn_id}', poll=2)
279 if kwargs.get('new_variable'):
280 self.find(f"#{btn_id}").clear()
281 self.enter_text(f"#{btn_id}", f"{var_name}")
282 else:
283 self.click(f'#{btn_id}')
284 self.wait_until_visible(f'#{input_id}', poll=2)
285
286 if kwargs.get('is_select'):
287 select = Select(self.find(f'#{input_id}'))
288 select.select_by_visible_text(value)
289 else:
290 self.find(f"#{input_id}").clear()
291 self.enter_text(f'#{input_id}', f'{value}')
292 self.click(f'#{save_btn}')
293 value_displayed = str(self.wait_until_visible(f'#{field}').text).lower()
294 msg = f'{var_name} variable not changed'
295 self.assertTrue(str(value).lower() in value_displayed, msg)
296
297 def test_change_distro_var(self):
298 """ Test changing distro variable """
299 self._change_bbv_value(
300 var_name='DISTRO',
301 field='distro',
302 btn_id='change-distro-icon',
303 input_id='new-distro',
304 value='poky-changed',
305 save_btn="apply-change-distro",
306 )
307
308 def test_set_image_install_append_var(self):
309 """ Test setting IMAGE_INSTALL:append variable """
310 self._change_bbv_value(
311 var_name='IMAGE_INSTALL:append',
312 field='image_install',
313 btn_id='change-image_install-icon',
314 input_id='new-image_install',
315 value='bash, apt, busybox',
316 save_btn="apply-change-image_install",
317 )
318
319 def test_set_package_classes_var(self):
320 """ Test setting PACKAGE_CLASSES variable """
321 self._change_bbv_value(
322 var_name='PACKAGE_CLASSES',
323 field='package_classes',
324 btn_id='change-package_classes-icon',
325 input_id='package_classes-select',
326 value='package_deb',
327 save_btn="apply-change-package_classes",
328 is_select=True,
329 )
330
331 def test_create_new_bbv(self):
332 """ Test creating new bitbake variable """
333 self._change_bbv_value(
334 var_name='New_Custom_Variable',
335 field='configvar-list',
336 btn_id='variable',
337 input_id='value',
338 value='new variable value',
339 save_btn="add-configvar-button",
340 new_variable=True
341 )
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 @@
1#! /usr/bin/env python3 #
2# BitBake Toaster UI tests implementation
3#
4# Copyright (C) 2023 Savoir-faire Linux
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import os
10import random
11import string
12from unittest import skip
13import pytest
14from django.urls import reverse
15from django.utils import timezone
16from selenium.webdriver.common.keys import Keys
17from selenium.webdriver.support.select import Select
18from selenium.common.exceptions import TimeoutException
19from tests.functional.functional_helpers import SeleniumFunctionalTestCase
20from orm.models import Build, Project, Target
21from selenium.webdriver.common.by import By
22
23from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
24
25
26@pytest.mark.django_db
27@pytest.mark.order("last")
28class TestProjectPage(SeleniumFunctionalTestCase):
29 project_id = None
30 PROJECT_NAME = 'TestProjectPage'
31
32 def _create_project(self, project_name):
33 """ Create/Test new project using:
34 - Project Name: Any string
35 - Release: Any string
36 - Merge Toaster settings: True or False
37 """
38 self.get(reverse('newproject'))
39 self.wait_until_visible('#new-project-name')
40 self.find("#new-project-name").send_keys(project_name)
41 select = Select(self.find("#projectversion"))
42 select.select_by_value('3')
43
44 # check merge toaster settings
45 checkbox = self.find('.checkbox-mergeattr')
46 if not checkbox.is_selected():
47 checkbox.click()
48
49 if self.PROJECT_NAME != 'TestProjectPage':
50 # Reset project name if it's not the default one
51 self.PROJECT_NAME = 'TestProjectPage'
52
53 self.find("#create-project-button").click()
54
55 try:
56 self.wait_until_visible('#hint-error-project-name')
57 url = reverse('project', args=(TestProjectPage.project_id, ))
58 self.get(url)
59 self.wait_until_visible('#config-nav', poll=3)
60 except TimeoutException:
61 self.wait_until_visible('#config-nav', poll=3)
62
63 def _random_string(self, length):
64 return ''.join(
65 random.choice(string.ascii_letters) for _ in range(length)
66 )
67
68 def _navigate_to_project_page(self):
69 # Navigate to project page
70 if TestProjectPage.project_id is None:
71 self._create_project(project_name=self._random_string(10))
72 current_url = self.driver.current_url
73 TestProjectPage.project_id = get_projectId_from_url(current_url)
74 else:
75 url = reverse('project', args=(TestProjectPage.project_id,))
76 self.get(url)
77 self.wait_until_visible('#config-nav')
78
79 def _get_create_builds(self, **kwargs):
80 """ Create a build and return the build object """
81 # parameters for builds to associate with the projects
82 now = timezone.now()
83 self.project1_build_success = {
84 'project': Project.objects.get(id=TestProjectPage.project_id),
85 'started_on': now,
86 'completed_on': now,
87 'outcome': Build.SUCCEEDED
88 }
89
90 self.project1_build_failure = {
91 'project': Project.objects.get(id=TestProjectPage.project_id),
92 'started_on': now,
93 'completed_on': now,
94 'outcome': Build.FAILED
95 }
96 build1 = Build.objects.create(**self.project1_build_success)
97 build2 = Build.objects.create(**self.project1_build_failure)
98
99 # add some targets to these builds so they have recipe links
100 # (and so we can find the row in the ToasterTable corresponding to
101 # a particular build)
102 Target.objects.create(build=build1, target='foo')
103 Target.objects.create(build=build2, target='bar')
104
105 if kwargs:
106 # Create kwargs.get('success') builds with success status with target
107 # and kwargs.get('failure') builds with failure status with target
108 for i in range(kwargs.get('success', 0)):
109 now = timezone.now()
110 self.project1_build_success['started_on'] = now
111 self.project1_build_success[
112 'completed_on'] = now - timezone.timedelta(days=i)
113 build = Build.objects.create(**self.project1_build_success)
114 Target.objects.create(build=build,
115 target=f'{i}_success_recipe',
116 task=f'{i}_success_task')
117
118 for i in range(kwargs.get('failure', 0)):
119 now = timezone.now()
120 self.project1_build_failure['started_on'] = now
121 self.project1_build_failure[
122 'completed_on'] = now - timezone.timedelta(days=i)
123 build = Build.objects.create(**self.project1_build_failure)
124 Target.objects.create(build=build,
125 target=f'{i}_fail_recipe',
126 task=f'{i}_fail_task')
127 return build1, build2
128
129 def _mixin_test_table_edit_column(
130 self,
131 table_id,
132 edit_btn_id,
133 list_check_box_id: list
134 ):
135 # Check edit column
136 edit_column = self.find(f'#{edit_btn_id}')
137 self.assertTrue(edit_column.is_displayed())
138 edit_column.click()
139 # Check dropdown is visible
140 self.wait_until_visible('ul.dropdown-menu.editcol')
141 for check_box_id in list_check_box_id:
142 # Check that we can hide/show table column
143 check_box = self.find(f'#{check_box_id}')
144 th_class = str(check_box_id).replace('checkbox-', '')
145 if check_box.is_selected():
146 # check if column is visible in table
147 self.assertTrue(
148 self.find(
149 f'#{table_id} thead th.{th_class}'
150 ).is_displayed(),
151 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
152 )
153 check_box.click()
154 # check if column is hidden in table
155 self.assertFalse(
156 self.find(
157 f'#{table_id} thead th.{th_class}'
158 ).is_displayed(),
159 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
160 )
161 else:
162 # check if column is hidden in table
163 self.assertFalse(
164 self.find(
165 f'#{table_id} thead th.{th_class}'
166 ).is_displayed(),
167 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
168 )
169 check_box.click()
170 # check if column is visible in table
171 self.assertTrue(
172 self.find(
173 f'#{table_id} thead th.{th_class}'
174 ).is_displayed(),
175 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
176 )
177
178 def _get_config_nav_item(self, index):
179 config_nav = self.find('#config-nav')
180 return config_nav.find_elements(By.TAG_NAME, 'li')[index]
181
182 def _navigate_to_config_nav(self, nav_id, nav_index):
183 # navigate to the project page
184 self._navigate_to_project_page()
185 # click on "Software recipe" tab
186 soft_recipe = self._get_config_nav_item(nav_index)
187 soft_recipe.click()
188 self.wait_until_visible(f'#{nav_id}')
189
190 def _mixin_test_table_show_rows(self, table_selector, **kwargs):
191 """ Test the show rows feature in the builds table on the all builds page """
192 def test_show_rows(row_to_show, show_row_link):
193 # Check that we can show rows == row_to_show
194 show_row_link.select_by_value(str(row_to_show))
195 self.wait_until_visible(f'#{table_selector} tbody tr', poll=3)
196 # check at least some rows are visible
197 self.assertTrue(
198 len(self.find_all(f'#{table_selector} tbody tr')) > 0
199 )
200 self.wait_until_present(f'#{table_selector} tbody tr')
201 show_rows = self.driver.find_elements(
202 By.XPATH,
203 f'//select[@class="form-control pagesize-{table_selector}"]'
204 )
205 rows_to_show = [10, 25, 50, 100, 150]
206 to_skip = kwargs.get('to_skip', [])
207 # Check show rows
208 for show_row_link in show_rows:
209 show_row_link = Select(show_row_link)
210 for row_to_show in rows_to_show:
211 if row_to_show not in to_skip:
212 test_show_rows(row_to_show, show_row_link)
213
214 def _mixin_test_table_search_input(self, **kwargs):
215 input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values()
216 # Test search input
217 self.wait_until_visible(f'#{input_selector}')
218 recipe_input = self.find(f'#{input_selector}')
219 recipe_input.send_keys(input_text)
220 self.find(f'#{searchBtn_selector}').click()
221 self.wait_until_visible(f'#{table_selector} tbody tr')
222 rows = self.find_all(f'#{table_selector} tbody tr')
223 self.assertTrue(len(rows) > 0)
224
225 def test_create_project(self):
226 """ Create/Test new project using:
227 - Project Name: Any string
228 - Release: Any string
229 - Merge Toaster settings: True or False
230 """
231 self._create_project(project_name=self.PROJECT_NAME)
232
233 def test_image_recipe_editColumn(self):
234 """ Test the edit column feature in image recipe table on project page """
235 self._get_create_builds(success=10, failure=10)
236
237 url = reverse('projectimagerecipes', args=(TestProjectPage.project_id,))
238 self.get(url)
239 self.wait_until_present('#imagerecipestable tbody tr')
240
241 column_list = [
242 'get_description_or_summary', 'layer_version__get_vcs_reference',
243 'layer_version__layer__name', 'license', 'recipe-file', 'section',
244 'version'
245 ]
246
247 # Check that we can hide the edit column
248 self._mixin_test_table_edit_column(
249 'imagerecipestable',
250 'edit-columns-button',
251 [f'checkbox-{column}' for column in column_list]
252 )
253
254 def test_page_header_on_project_page(self):
255 """ Check page header in project page:
256 - AT LEFT -> Logo of Yocto project, displayed, clickable
257 - "Toaster"+" Information icon", displayed, clickable
258 - "Server Icon" + "All builds", displayed, clickable
259 - "Directory Icon" + "All projects", displayed, clickable
260 - "Book Icon" + "Documentation", displayed, clickable
261 - AT RIGHT -> button "New project", displayed, clickable
262 """
263 # navigate to the project page
264 self._navigate_to_project_page()
265
266 # check page header
267 # AT LEFT -> Logo of Yocto project
268 logo = self.driver.find_element(
269 By.XPATH,
270 "//div[@class='toaster-navbar-brand']",
271 )
272 logo_img = logo.find_element(By.TAG_NAME, 'img')
273 self.assertTrue(logo_img.is_displayed(),
274 'Logo of Yocto project not found')
275 self.assertTrue(
276 '/static/img/logo.png' in str(logo_img.get_attribute('src')),
277 'Logo of Yocto project not found'
278 )
279 # "Toaster"+" Information icon", clickable
280 toaster = self.driver.find_element(
281 By.XPATH,
282 "//div[@class='toaster-navbar-brand']//a[@class='brand']",
283 )
284 self.assertTrue(toaster.is_displayed(), 'Toaster not found')
285 self.assertTrue(toaster.text == 'Toaster')
286 info_sign = self.find('.glyphicon-info-sign')
287 self.assertTrue(info_sign.is_displayed())
288
289 # "Server Icon" + "All builds"
290 all_builds = self.find('#navbar-all-builds')
291 all_builds_link = all_builds.find_element(By.TAG_NAME, 'a')
292 self.assertTrue("All builds" in all_builds_link.text)
293 self.assertTrue(
294 '/toastergui/builds/' in str(all_builds_link.get_attribute('href'))
295 )
296 server_icon = all_builds.find_element(By.TAG_NAME, 'i')
297 self.assertTrue(
298 server_icon.get_attribute('class') == 'glyphicon glyphicon-tasks'
299 )
300 self.assertTrue(server_icon.is_displayed())
301
302 # "Directory Icon" + "All projects"
303 all_projects = self.find('#navbar-all-projects')
304 all_projects_link = all_projects.find_element(By.TAG_NAME, 'a')
305 self.assertTrue("All projects" in all_projects_link.text)
306 self.assertTrue(
307 '/toastergui/projects/' in str(all_projects_link.get_attribute(
308 'href'))
309 )
310 dir_icon = all_projects.find_element(By.TAG_NAME, 'i')
311 self.assertTrue(
312 dir_icon.get_attribute('class') == 'icon-folder-open'
313 )
314 self.assertTrue(dir_icon.is_displayed())
315
316 # "Book Icon" + "Documentation"
317 toaster_docs_link = self.find('#navbar-docs')
318 toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME,
319 'a')
320 self.assertTrue("Documentation" in toaster_docs_link_link.text)
321 self.assertTrue(
322 toaster_docs_link_link.get_attribute('href') == 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual'
323 )
324 book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i')
325 self.assertTrue(
326 book_icon.get_attribute('class') == 'glyphicon glyphicon-book'
327 )
328 self.assertTrue(book_icon.is_displayed())
329
330 # AT RIGHT -> button "New project"
331 new_project_button = self.find('#new-project-button')
332 self.assertTrue(new_project_button.is_displayed())
333 self.assertTrue(new_project_button.text == 'New project')
334 new_project_button.click()
335 self.assertTrue(
336 '/toastergui/newproject/' in str(self.driver.current_url)
337 )
338
339 def test_edit_project_name(self):
340 """ Test edit project name:
341 - Click on "Edit" icon button
342 - Change project name
343 - Click on "Save" button
344 - Check project name is changed
345 """
346 # navigate to the project page
347 self._navigate_to_project_page()
348
349 # click on "Edit" icon button
350 self.wait_until_visible('#project-name-container')
351 edit_button = self.find('#project-change-form-toggle')
352 edit_button.click()
353 project_name_input = self.find('#project-name-change-input')
354 self.assertTrue(project_name_input.is_displayed())
355 project_name_input.clear()
356 project_name_input.send_keys('New Name')
357 self.find('#project-name-change-btn').click()
358
359 # check project name is changed
360 self.wait_until_visible('#project-name-container')
361 self.assertTrue(
362 'New Name' in str(self.find('#project-name-container').text)
363 )
364
365 def test_project_page_tabs(self):
366 """ Test project tabs:
367 - "configuration" tab
368 - "Builds" tab
369 - "Import layers" tab
370 - "New custom image" tab
371 Check search box used to build recipes
372 """
373 # navigate to the project page
374 self._navigate_to_project_page()
375
376 # check "configuration" tab
377 self.wait_until_visible('#topbar-configuration-tab')
378 config_tab = self.find('#topbar-configuration-tab')
379 self.assertTrue(config_tab.get_attribute('class') == 'active')
380 self.assertTrue('Configuration' in str(config_tab.text))
381 self.assertTrue(
382 f"/toastergui/project/{TestProjectPage.project_id}" in str(self.driver.current_url)
383 )
384
385 def get_tabs():
386 # tabs links list
387 return self.driver.find_elements(
388 By.XPATH,
389 '//div[@id="project-topbar"]//li'
390 )
391
392 def check_tab_link(tab_index, tab_name, url):
393 tab = get_tabs()[tab_index]
394 tab_link = tab.find_element(By.TAG_NAME, 'a')
395 self.assertTrue(url in tab_link.get_attribute('href'))
396 self.assertTrue(tab_name in tab_link.text)
397 self.assertTrue(tab.get_attribute('class') == 'active')
398
399 # check "Builds" tab
400 builds_tab = get_tabs()[1]
401 builds_tab.find_element(By.TAG_NAME, 'a').click()
402 check_tab_link(
403 1,
404 'Builds',
405 f"/toastergui/project/{TestProjectPage.project_id}/builds"
406 )
407
408 # check "Import layers" tab
409 import_layers_tab = get_tabs()[2]
410 import_layers_tab.find_element(By.TAG_NAME, 'a').click()
411 check_tab_link(
412 2,
413 'Import layer',
414 f"/toastergui/project/{TestProjectPage.project_id}/importlayer"
415 )
416
417 # check "New custom image" tab
418 new_custom_image_tab = get_tabs()[3]
419 new_custom_image_tab.find_element(By.TAG_NAME, 'a').click()
420 check_tab_link(
421 3,
422 'New custom image',
423 f"/toastergui/project/{TestProjectPage.project_id}/newcustomimage"
424 )
425
426 # check search box can be use to build recipes
427 search_box = self.find('#build-input')
428 search_box.send_keys('core-image-minimal')
429 self.find('#build-button').click()
430 self.wait_until_visible('#latest-builds')
431 lastest_builds = self.driver.find_elements(
432 By.XPATH,
433 '//div[@id="latest-builds"]',
434 )
435 last_build = lastest_builds[0]
436 self.assertTrue(
437 'core-image-minimal' in str(last_build.text)
438 )
439
440 def test_softwareRecipe_page(self):
441 """ Test software recipe page
442 - Check title "Compatible software recipes" is displayed
443 - Check search input
444 - Check "build recipe" button works
445 - Check software recipe table feature(show/hide column, pagination)
446 """
447 self._navigate_to_config_nav('softwarerecipestable', 4)
448 # check title "Compatible software recipes" is displayed
449 self.assertTrue("Compatible software recipes" in self.get_page_source())
450 # Test search input
451 self._mixin_test_table_search_input(
452 input_selector='search-input-softwarerecipestable',
453 input_text='busybox',
454 searchBtn_selector='search-submit-softwarerecipestable',
455 table_selector='softwarerecipestable'
456 )
457 # check "build recipe" button works
458 rows = self.find_all('#softwarerecipestable tbody tr')
459 image_to_build = rows[0]
460 build_btn = image_to_build.find_element(
461 By.XPATH,
462 '//td[@class="add-del-layers"]//a[1]'
463 )
464 build_btn.click()
465 build_state = wait_until_build(self, 'queued cloning starting parsing failed')
466 lastest_builds = self.driver.find_elements(
467 By.XPATH,
468 '//div[@id="latest-builds"]/div'
469 )
470 self.assertTrue(len(lastest_builds) > 0)
471 last_build = lastest_builds[0]
472 cancel_button = last_build.find_element(
473 By.XPATH,
474 '//span[@class="cancel-build-btn pull-right alert-link"]',
475 )
476 cancel_button.click()
477 if 'starting' not in build_state: # change build state when cancelled in starting state
478 wait_until_build_cancelled(self)
479
480 # check software recipe table feature(show/hide column, pagination)
481 self._navigate_to_config_nav('softwarerecipestable', 4)
482 column_list = [
483 'get_description_or_summary',
484 'layer_version__get_vcs_reference',
485 'layer_version__layer__name',
486 'license',
487 'recipe-file',
488 'section',
489 'version',
490 ]
491 self._mixin_test_table_edit_column(
492 'softwarerecipestable',
493 'edit-columns-button',
494 [f'checkbox-{column}' for column in column_list]
495 )
496 self._navigate_to_config_nav('softwarerecipestable', 4)
497 # check show rows(pagination)
498 self._mixin_test_table_show_rows(
499 table_selector='softwarerecipestable',
500 to_skip=[150],
501 )
502
503 def test_machines_page(self):
504 """ Test Machine page
505 - Check if title "Compatible machines" is displayed
506 - Check search input
507 - Check "Select machine" button works
508 - Check "Add layer" button works
509 - Check Machine table feature(show/hide column, pagination)
510 """
511 self._navigate_to_config_nav('machinestable', 5)
512 # check title "Compatible software recipes" is displayed
513 self.assertTrue("Compatible machines" in self.get_page_source())
514 # Test search input
515 self._mixin_test_table_search_input(
516 input_selector='search-input-machinestable',
517 input_text='qemux86-64',
518 searchBtn_selector='search-submit-machinestable',
519 table_selector='machinestable'
520 )
521 # check "Select machine" button works
522 rows = self.find_all('#machinestable tbody tr')
523 machine_to_select = rows[0]
524 select_btn = machine_to_select.find_element(
525 By.XPATH,
526 '//td[@class="add-del-layers"]//a[1]'
527 )
528 select_btn.send_keys(Keys.RETURN)
529 self.wait_until_visible('#config-nav')
530 project_machine_name = self.find('#project-machine-name')
531 self.assertTrue(
532 'qemux86-64' in project_machine_name.text
533 )
534 # check "Add layer" button works
535 self._navigate_to_config_nav('machinestable', 5)
536 # Search for a machine whit layer not in project
537 self._mixin_test_table_search_input(
538 input_selector='search-input-machinestable',
539 input_text='qemux86-64-tpm2',
540 searchBtn_selector='search-submit-machinestable',
541 table_selector='machinestable'
542 )
543 self.wait_until_visible('#machinestable tbody tr', poll=3)
544 rows = self.find_all('#machinestable tbody tr')
545 machine_to_add = rows[0]
546 add_btn = machine_to_add.find_element(By.XPATH, '//td[@class="add-del-layers"]')
547 add_btn.click()
548 self.wait_until_visible('#change-notification')
549 change_notification = self.find('#change-notification')
550 self.assertTrue(
551 f'You have added 1 layer to your project' in str(change_notification.text)
552 )
553 # check Machine table feature(show/hide column, pagination)
554 self._navigate_to_config_nav('machinestable', 5)
555 column_list = [
556 'description',
557 'layer_version__get_vcs_reference',
558 'layer_version__layer__name',
559 'machinefile',
560 ]
561 self._mixin_test_table_edit_column(
562 'machinestable',
563 'edit-columns-button',
564 [f'checkbox-{column}' for column in column_list]
565 )
566 self._navigate_to_config_nav('machinestable', 5)
567 # check show rows(pagination)
568 self._mixin_test_table_show_rows(
569 table_selector='machinestable',
570 to_skip=[150],
571 )
572
573 def test_layers_page(self):
574 """ Test layers page
575 - Check if title "Compatible layerss" is displayed
576 - Check search input
577 - Check "Add layer" button works
578 - Check "Remove layer" button works
579 - Check layers table feature(show/hide column, pagination)
580 """
581 self._navigate_to_config_nav('layerstable', 6)
582 # check title "Compatible layers" is displayed
583 self.assertTrue("Compatible layers" in self.get_page_source())
584 # Test search input
585 input_text='meta-tanowrt'
586 self._mixin_test_table_search_input(
587 input_selector='search-input-layerstable',
588 input_text=input_text,
589 searchBtn_selector='search-submit-layerstable',
590 table_selector='layerstable'
591 )
592 # check "Add layer" button works
593 self.wait_until_visible('#layerstable tbody tr', poll=3)
594 rows = self.find_all('#layerstable tbody tr')
595 layer_to_add = rows[0]
596 add_btn = layer_to_add.find_element(
597 By.XPATH,
598 '//td[@class="add-del-layers"]'
599 )
600 add_btn.click()
601 # check modal is displayed
602 self.wait_until_visible('#dependencies-modal', poll=3)
603 list_dependencies = self.find_all('#dependencies-list li')
604 # click on add-layers button
605 add_layers_btn = self.driver.find_element(
606 By.XPATH,
607 '//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]'
608 )
609 add_layers_btn.click()
610 self.wait_until_visible('#change-notification')
611 change_notification = self.find('#change-notification')
612 self.assertTrue(
613 f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies' in str(change_notification.text)
614 )
615 # check "Remove layer" button works
616 self.wait_until_visible('#layerstable tbody tr', poll=3)
617 rows = self.find_all('#layerstable tbody tr')
618 layer_to_remove = rows[0]
619 remove_btn = layer_to_remove.find_element(
620 By.XPATH,
621 '//td[@class="add-del-layers"]'
622 )
623 remove_btn.click()
624 self.wait_until_visible('#change-notification', poll=2)
625 change_notification = self.find('#change-notification')
626 self.assertTrue(
627 f'You have removed 1 layer from your project: {input_text}' in str(change_notification.text)
628 )
629 # check layers table feature(show/hide column, pagination)
630 self._navigate_to_config_nav('layerstable', 6)
631 column_list = [
632 'dependencies',
633 'revision',
634 'layer__vcs_url',
635 'git_subdir',
636 'layer__summary',
637 ]
638 self._mixin_test_table_edit_column(
639 'layerstable',
640 'edit-columns-button',
641 [f'checkbox-{column}' for column in column_list]
642 )
643 self._navigate_to_config_nav('layerstable', 6)
644 # check show rows(pagination)
645 self._mixin_test_table_show_rows(
646 table_selector='layerstable',
647 to_skip=[150],
648 )
649
650 def test_distro_page(self):
651 """ Test distros page
652 - Check if title "Compatible distros" is displayed
653 - Check search input
654 - Check "Add layer" button works
655 - Check distro table feature(show/hide column, pagination)
656 """
657 self._navigate_to_config_nav('distrostable', 7)
658 # check title "Compatible distros" is displayed
659 self.assertTrue("Compatible Distros" in self.get_page_source())
660 # Test search input
661 input_text='poky-altcfg'
662 self._mixin_test_table_search_input(
663 input_selector='search-input-distrostable',
664 input_text=input_text,
665 searchBtn_selector='search-submit-distrostable',
666 table_selector='distrostable'
667 )
668 # check "Add distro" button works
669 rows = self.find_all('#distrostable tbody tr')
670 distro_to_add = rows[0]
671 add_btn = distro_to_add.find_element(
672 By.XPATH,
673 '//td[@class="add-del-layers"]//a[1]'
674 )
675 add_btn.click()
676 self.wait_until_visible('#change-notification', poll=2)
677 change_notification = self.find('#change-notification')
678 self.assertTrue(
679 f'You have changed the distro to: {input_text}' in str(change_notification.text)
680 )
681 # check distro table feature(show/hide column, pagination)
682 self._navigate_to_config_nav('distrostable', 7)
683 column_list = [
684 'description',
685 'templatefile',
686 'layer_version__get_vcs_reference',
687 'layer_version__layer__name',
688 ]
689 self._mixin_test_table_edit_column(
690 'distrostable',
691 'edit-columns-button',
692 [f'checkbox-{column}' for column in column_list]
693 )
694 self._navigate_to_config_nav('distrostable', 7)
695 # check show rows(pagination)
696 self._mixin_test_table_show_rows(
697 table_selector='distrostable',
698 to_skip=[150],
699 )
700
701 def test_single_layer_page(self):
702 """ Test layer page
703 - Check if title is displayed
704 - Check add/remove layer button works
705 - Check tabs(layers, recipes, machines) are displayed
706 - Check left section is displayed
707 - Check layer name
708 - Check layer summary
709 - Check layer description
710 """
711 url = reverse("layerdetails", args=(TestProjectPage.project_id, 8))
712 self.get(url)
713 self.wait_until_visible('.page-header')
714 # check title is displayed
715 self.assertTrue(self.find('.page-header h1').is_displayed())
716
717 # check add layer button works
718 remove_layer_btn = self.find('#add-remove-layer-btn')
719 remove_layer_btn.click()
720 self.wait_until_visible('#change-notification', poll=2)
721 change_notification = self.find('#change-notification')
722 self.assertTrue(
723 f'You have removed 1 layer from your project' in str(change_notification.text)
724 )
725 # check add layer button works, 18 is the random layer id
726 add_layer_btn = self.find('#add-remove-layer-btn')
727 add_layer_btn.click()
728 self.wait_until_visible('#change-notification')
729 change_notification = self.find('#change-notification')
730 self.assertTrue(
731 f'You have added 1 layer to your project' in str(change_notification.text)
732 )
733 # check tabs(layers, recipes, machines) are displayed
734 tabs = self.find_all('.nav-tabs li')
735 self.assertEqual(len(tabs), 3)
736 # Check first tab
737 tabs[0].click()
738 self.assertTrue(
739 'active' in str(self.find('#information').get_attribute('class'))
740 )
741 # Check second tab
742 tabs[1].click()
743 self.assertTrue(
744 'active' in str(self.find('#recipes').get_attribute('class'))
745 )
746 # Check third tab
747 tabs[2].click()
748 self.assertTrue(
749 'active' in str(self.find('#machines').get_attribute('class'))
750 )
751 # Check left section is displayed
752 section = self.find('.well')
753 # Check layer name
754 self.assertTrue(
755 section.find_element(By.XPATH, '//h2[1]').is_displayed()
756 )
757 # Check layer summary
758 self.assertTrue("Summary" in section.text)
759 # Check layer description
760 self.assertTrue("Description" in section.text)
761
762 def test_single_recipe_page(self):
763 """ Test recipe page
764 - Check if title is displayed
765 - Check add recipe layer displayed
766 - Check left section is displayed
767 - Check recipe: name, summary, description, Version, Section,
768 License, Approx. packages included, Approx. size, Recipe file
769 """
770 url = reverse("recipedetails", args=(TestProjectPage.project_id, 53428))
771 self.get(url)
772 self.wait_until_visible('.page-header')
773 # check title is displayed
774 self.assertTrue(self.find('.page-header h1').is_displayed())
775 # check add recipe layer displayed
776 add_recipe_layer_btn = self.find('#add-layer-btn')
777 self.assertTrue(add_recipe_layer_btn.is_displayed())
778 # check left section is displayed
779 section = self.find('.well')
780 # Check recipe name
781 self.assertTrue(
782 section.find_element(By.XPATH, '//h2[1]').is_displayed()
783 )
784 # Check recipe sections details info are displayed
785 self.assertTrue("Summary" in section.text)
786 self.assertTrue("Description" in section.text)
787 self.assertTrue("Version" in section.text)
788 self.assertTrue("Section" in section.text)
789 self.assertTrue("License" in section.text)
790 self.assertTrue("Approx. packages included" in section.text)
791 self.assertTrue("Approx. package size" in section.text)
792 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 @@
1#! /usr/bin/env python3 #
2# BitBake Toaster UI tests implementation
3#
4# Copyright (C) 2023 Savoir-faire Linux
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import string
10import random
11import pytest
12from django.urls import reverse
13from selenium.webdriver import Keys
14from selenium.webdriver.support.select import Select
15from selenium.common.exceptions import ElementClickInterceptedException, NoSuchElementException, TimeoutException
16from orm.models import Project
17from tests.functional.functional_helpers import SeleniumFunctionalTestCase
18from selenium.webdriver.common.by import By
19
20from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
21
22
23@pytest.mark.django_db
24@pytest.mark.order("last")
25class TestProjectConfigTab(SeleniumFunctionalTestCase):
26 PROJECT_NAME = 'TestProjectConfigTab'
27 project_id = None
28
29 def _create_project(self, project_name, **kwargs):
30 """ Create/Test new project using:
31 - Project Name: Any string
32 - Release: Any string
33 - Merge Toaster settings: True or False
34 """
35 release = kwargs.get('release', '3')
36 self.get(reverse('newproject'))
37 self.wait_until_visible('#new-project-name')
38 self.find("#new-project-name").send_keys(project_name)
39 select = Select(self.find("#projectversion"))
40 select.select_by_value(release)
41
42 # check merge toaster settings
43 checkbox = self.find('.checkbox-mergeattr')
44 if not checkbox.is_selected():
45 checkbox.click()
46
47 if self.PROJECT_NAME != 'TestProjectConfigTab':
48 # Reset project name if it's not the default one
49 self.PROJECT_NAME = 'TestProjectConfigTab'
50
51 self.find("#create-project-button").click()
52
53 try:
54 self.wait_until_visible('#hint-error-project-name', poll=3)
55 url = reverse('project', args=(TestProjectConfigTab.project_id, ))
56 self.get(url)
57 self.wait_until_visible('#config-nav', poll=3)
58 except TimeoutException:
59 self.wait_until_visible('#config-nav', poll=3)
60
61 def _random_string(self, length):
62 return ''.join(
63 random.choice(string.ascii_letters) for _ in range(length)
64 )
65
66 def _navigate_to_project_page(self):
67 # Navigate to project page
68 if TestProjectConfigTab.project_id is None:
69 self._create_project(project_name=self._random_string(10))
70 current_url = self.driver.current_url
71 TestProjectConfigTab.project_id = get_projectId_from_url(
72 current_url)
73 else:
74 url = reverse('project', args=(TestProjectConfigTab.project_id,))
75 self.get(url)
76 self.wait_until_visible('#config-nav')
77
78 def _create_builds(self):
79 # check search box can be use to build recipes
80 search_box = self.find('#build-input')
81 search_box.send_keys('foo')
82 self.find('#build-button').click()
83 self.wait_until_present('#latest-builds')
84 # loop until reach the parsing state
85 wait_until_build(self, 'queued cloning starting parsing failed')
86 lastest_builds = self.driver.find_elements(
87 By.XPATH,
88 '//div[@id="latest-builds"]/div',
89 )
90 last_build = lastest_builds[0]
91 self.assertTrue(
92 'foo' in str(last_build.text)
93 )
94 last_build = lastest_builds[0]
95 try:
96 cancel_button = last_build.find_element(
97 By.XPATH,
98 '//span[@class="cancel-build-btn pull-right alert-link"]',
99 )
100 cancel_button.click()
101 except NoSuchElementException:
102 # Skip if the build is already cancelled
103 pass
104 wait_until_build_cancelled(self)
105
106 def _get_tabs(self):
107 # tabs links list
108 return self.driver.find_elements(
109 By.XPATH,
110 '//div[@id="project-topbar"]//li'
111 )
112
113 def _get_config_nav_item(self, index):
114 config_nav = self.find('#config-nav')
115 return config_nav.find_elements(By.TAG_NAME, 'li')[index]
116
117 def test_project_config_nav(self):
118 """ Test project config tab navigation:
119 - Check if the menu is displayed and contains the right elements:
120 - Configuration
121 - COMPATIBLE METADATA
122 - Custom images
123 - Image recipes
124 - Software recipes
125 - Machines
126 - Layers
127 - Distro
128 - EXTRA CONFIGURATION
129 - Bitbake variables
130 - Actions
131 - Delete project
132 """
133 self._navigate_to_project_page()
134
135 def _get_config_nav_item(index):
136 config_nav = self.find('#config-nav')
137 return config_nav.find_elements(By.TAG_NAME, 'li')[index]
138
139 def check_config_nav_item(index, item_name, url):
140 item = _get_config_nav_item(index)
141 self.assertTrue(item_name in item.text)
142 self.assertTrue(item.get_attribute('class') == 'active')
143 self.assertTrue(url in self.driver.current_url)
144
145 # check if the menu contains the right elements
146 # COMPATIBLE METADATA
147 compatible_metadata = _get_config_nav_item(1)
148 self.assertTrue(
149 "compatible metadata" in compatible_metadata.text.lower()
150 )
151 # EXTRA CONFIGURATION
152 extra_configuration = _get_config_nav_item(8)
153 self.assertTrue(
154 "extra configuration" in extra_configuration.text.lower()
155 )
156 # Actions
157 actions = _get_config_nav_item(10)
158 self.assertTrue("actions" in str(actions.text).lower())
159
160 conf_nav_list = [
161 # config
162 [0, 'Configuration',
163 f"/toastergui/project/{TestProjectConfigTab.project_id}"],
164 # custom images
165 [2, 'Custom images',
166 f"/toastergui/project/{TestProjectConfigTab.project_id}/customimages"],
167 # image recipes
168 [3, 'Image recipes',
169 f"/toastergui/project/{TestProjectConfigTab.project_id}/images"],
170 # software recipes
171 [4, 'Software recipes',
172 f"/toastergui/project/{TestProjectConfigTab.project_id}/softwarerecipes"],
173 # machines
174 [5, 'Machines',
175 f"/toastergui/project/{TestProjectConfigTab.project_id}/machines"],
176 # layers
177 [6, 'Layers',
178 f"/toastergui/project/{TestProjectConfigTab.project_id}/layers"],
179 # distro
180 [7, 'Distros',
181 f"/toastergui/project/{TestProjectConfigTab.project_id}/distros"],
182 # [9, 'BitBake variables', f"/toastergui/project/{TestProjectConfigTab.project_id}/configuration"], # bitbake variables
183 ]
184 for index, item_name, url in conf_nav_list:
185 item = _get_config_nav_item(index)
186 if item.get_attribute('class') != 'active':
187 item.click()
188 check_config_nav_item(index, item_name, url)
189
190 def test_image_recipe_editColumn(self):
191 """ Test the edit column feature in image recipe table on project page """
192 def test_edit_column(check_box_id):
193 # Check that we can hide/show table column
194 check_box = self.find(f'#{check_box_id}')
195 th_class = str(check_box_id).replace('checkbox-', '')
196 if check_box.is_selected():
197 # check if column is visible in table
198 self.assertTrue(
199 self.find(
200 f'#imagerecipestable thead th.{th_class}'
201 ).is_displayed(),
202 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
203 )
204 check_box.click()
205 # check if column is hidden in table
206 self.assertFalse(
207 self.find(
208 f'#imagerecipestable thead th.{th_class}'
209 ).is_displayed(),
210 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
211 )
212 else:
213 # check if column is hidden in table
214 self.assertFalse(
215 self.find(
216 f'#imagerecipestable thead th.{th_class}'
217 ).is_displayed(),
218 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
219 )
220 check_box.click()
221 # check if column is visible in table
222 self.assertTrue(
223 self.find(
224 f'#imagerecipestable thead th.{th_class}'
225 ).is_displayed(),
226 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
227 )
228
229 self._navigate_to_project_page()
230 # navigate to project image recipe page
231 recipe_image_page_link = self._get_config_nav_item(3)
232 recipe_image_page_link.click()
233 self.wait_until_present('#imagerecipestable tbody tr')
234
235 # Check edit column
236 edit_column = self.find('#edit-columns-button')
237 self.assertTrue(edit_column.is_displayed())
238 edit_column.click()
239 # Check dropdown is visible
240 self.wait_until_visible('ul.dropdown-menu.editcol')
241
242 # Check that we can hide the edit column
243 test_edit_column('checkbox-get_description_or_summary')
244 test_edit_column('checkbox-layer_version__get_vcs_reference')
245 test_edit_column('checkbox-layer_version__layer__name')
246 test_edit_column('checkbox-license')
247 test_edit_column('checkbox-recipe-file')
248 test_edit_column('checkbox-section')
249 test_edit_column('checkbox-version')
250
251 def test_image_recipe_show_rows(self):
252 """ Test the show rows feature in image recipe table on project page """
253 def test_show_rows(row_to_show, show_row_link):
254 # Check that we can show rows == row_to_show
255 show_row_link.select_by_value(str(row_to_show))
256 self.wait_until_visible('#imagerecipestable tbody tr', poll=3)
257 # check at least some rows are visible
258 self.assertTrue(
259 len(self.find_all('#imagerecipestable tbody tr')) > 0
260 )
261
262 self._navigate_to_project_page()
263 # navigate to project image recipe page
264 recipe_image_page_link = self._get_config_nav_item(3)
265 recipe_image_page_link.click()
266 self.wait_until_present('#imagerecipestable tbody tr')
267
268 show_rows = self.driver.find_elements(
269 By.XPATH,
270 '//select[@class="form-control pagesize-imagerecipestable"]'
271 )
272 # Check show rows
273 for show_row_link in show_rows:
274 show_row_link = Select(show_row_link)
275 test_show_rows(10, show_row_link)
276 test_show_rows(25, show_row_link)
277 test_show_rows(50, show_row_link)
278 test_show_rows(100, show_row_link)
279 test_show_rows(150, show_row_link)
280
281 def test_project_config_tab_right_section(self):
282 """ Test project config tab right section contains five blocks:
283 - Machine:
284 - check 'Machine' is displayed
285 - check can change Machine
286 - Distro:
287 - check 'Distro' is displayed
288 - check can change Distro
289 - Most built recipes:
290 - check 'Most built recipes' is displayed
291 - check can select a recipe and build it
292 - Project release:
293 - check 'Project release' is displayed
294 - check project has right release displayed
295 - Layers:
296 - check can add a layer if exists
297 - check at least three layers are displayed
298 - openembedded-core
299 - meta-poky
300 - meta-yocto-bsp
301 """
302 # Create a new project for this test
303 project_name = self._random_string(10)
304 self._create_project(project_name=project_name)
305 # check if the menu is displayed
306 self.wait_until_visible('#project-page')
307 block_l = self.driver.find_element(
308 By.XPATH, '//*[@id="project-page"]/div[2]')
309 project_release = self.driver.find_element(
310 By.XPATH, '//*[@id="project-page"]/div[1]/div[4]')
311 layers = block_l.find_element(By.ID, 'layer-container')
312
313 def check_machine_distro(self, item_name, new_item_name, block_id):
314 block = self.find(f'#{block_id}')
315 title = block.find_element(By.TAG_NAME, 'h3')
316 self.assertTrue(item_name.capitalize() in title.text)
317 edit_btn = self.find(f'#change-{item_name}-toggle')
318 edit_btn.click()
319 self.wait_until_visible(f'#{item_name}-change-input')
320 name_input = self.find(f'#{item_name}-change-input')
321 name_input.clear()
322 name_input.send_keys(new_item_name)
323 change_btn = self.find(f'#{item_name}-change-btn')
324 change_btn.click()
325 self.wait_until_visible(f'#project-{item_name}-name')
326 project_name = self.find(f'#project-{item_name}-name')
327 self.assertTrue(new_item_name in project_name.text)
328 # check change notificaiton is displayed
329 change_notification = self.find('#change-notification')
330 self.assertTrue(
331 f'You have changed the {item_name} to: {new_item_name}' in change_notification.text
332 )
333
334 # Machine
335 check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section')
336 # Distro
337 check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section')
338
339 # Project release
340 title = project_release.find_element(By.TAG_NAME, 'h3')
341 self.assertTrue("Project release" in title.text)
342 self.assertTrue(
343 "Yocto Project master" in self.find('#project-release-title').text
344 )
345 # Layers
346 title = layers.find_element(By.TAG_NAME, 'h3')
347 self.assertTrue("Layers" in title.text)
348 # check at least three layers are displayed
349 # openembedded-core
350 # meta-poky
351 # meta-yocto-bsp
352 layers_list = layers.find_element(By.ID, 'layers-in-project-list')
353 layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
354 # remove all layers except the first three layers
355 for i in range(3, len(layers_list_items)):
356 layers_list_items[i].find_element(By.TAG_NAME, 'span').click()
357 # check can add a layer if exists
358 add_layer_input = layers.find_element(By.ID, 'layer-add-input')
359 add_layer_input.send_keys('meta-oe')
360 self.wait_until_visible('#layer-container > form > div > span > div')
361 dropdown_item = self.driver.find_element(
362 By.XPATH,
363 '//*[@id="layer-container"]/form/div/span/div'
364 )
365 try:
366 dropdown_item.click()
367 except ElementClickInterceptedException:
368 self.skipTest(
369 "layer-container dropdown item click intercepted. Element not properly visible.")
370 add_layer_btn = layers.find_element(By.ID, 'add-layer-btn')
371 add_layer_btn.click()
372 self.wait_until_visible('#layers-in-project-list')
373 # check layer is added
374 layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
375 self.assertTrue(len(layers_list_items) == 4)
376
377 def test_most_build_recipes(self):
378 """ Test most build recipes block contains"""
379 def rebuild_from_most_build_recipes(recipe_list_items):
380 checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input')
381 checkbox.click()
382 build_btn = self.find('#freq-build-btn')
383 build_btn.click()
384 self.wait_until_visible('#latest-builds')
385 wait_until_build(self, 'queued cloning starting parsing failed')
386 lastest_builds = self.driver.find_elements(
387 By.XPATH,
388 '//div[@id="latest-builds"]/div'
389 )
390 self.assertTrue(len(lastest_builds) >= 2)
391 last_build = lastest_builds[0]
392 try:
393 cancel_button = last_build.find_element(
394 By.XPATH,
395 '//span[@class="cancel-build-btn pull-right alert-link"]',
396 )
397 cancel_button.click()
398 except NoSuchElementException:
399 # Skip if the build is already cancelled
400 pass
401 wait_until_build_cancelled(self)
402 # Create a new project for remaining asserts
403 project_name = self._random_string(10)
404 self._create_project(project_name=project_name, release='2')
405 current_url = self.driver.current_url
406 TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
407 url = current_url.split('?')[0]
408
409 # Create a new builds
410 self._create_builds()
411
412 # back to project page
413 self.driver.get(url)
414
415 self.wait_until_visible('#project-page', poll=3)
416
417 # Most built recipes
418 most_built_recipes = self.driver.find_element(
419 By.XPATH, '//*[@id="project-page"]/div[1]/div[3]')
420 title = most_built_recipes.find_element(By.TAG_NAME, 'h3')
421 self.assertTrue("Most built recipes" in title.text)
422 # check can select a recipe and build it
423 self.wait_until_visible('#freq-build-list', poll=3)
424 recipe_list = self.find('#freq-build-list')
425 recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li')
426 self.assertTrue(
427 len(recipe_list_items) > 0,
428 msg="Any recipes found in the most built recipes list",
429 )
430 rebuild_from_most_build_recipes(recipe_list_items)
431 TestProjectConfigTab.project_id = None # reset project id
432
433 def test_project_page_tab_importlayer(self):
434 """ Test project page tab import layer """
435 self._navigate_to_project_page()
436 # navigate to "Import layers" tab
437 import_layers_tab = self._get_tabs()[2]
438 import_layers_tab.find_element(By.TAG_NAME, 'a').click()
439 self.wait_until_visible('#layer-git-repo-url')
440
441 # Check git repo radio button
442 git_repo_radio = self.find('#git-repo-radio')
443 git_repo_radio.click()
444
445 # Set git repo url
446 input_repo_url = self.find('#layer-git-repo-url')
447 input_repo_url.send_keys('git://git.yoctoproject.org/meta-fake')
448 # Blur the input to trigger the validation
449 input_repo_url.send_keys(Keys.TAB)
450
451 # Check name is set
452 input_layer_name = self.find('#import-layer-name')
453 self.assertTrue(input_layer_name.get_attribute('value') == 'meta-fake')
454
455 # Set branch
456 input_branch = self.find('#layer-git-ref')
457 input_branch.send_keys('master')
458
459 # Import layer
460 self.find('#import-and-add-btn').click()
461
462 # Check layer is added
463 self.wait_until_visible('#layer-container')
464 block_l = self.driver.find_element(
465 By.XPATH, '//*[@id="project-page"]/div[2]')
466 layers = block_l.find_element(By.ID, 'layer-container')
467 layers_list = layers.find_element(By.ID, 'layers-in-project-list')
468 layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
469 self.assertTrue(
470 'meta-fake' in str(layers_list_items[-1].text)
471 )
472
473 def test_project_page_custom_image_no_image(self):
474 """ Test project page tab "New custom image" when no custom image """
475 project_name = self._random_string(10)
476 self._create_project(project_name=project_name)
477 current_url = self.driver.current_url
478 TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
479 # navigate to "Custom image" tab
480 custom_image_section = self._get_config_nav_item(2)
481 custom_image_section.click()
482 self.wait_until_visible('#empty-state-customimagestable')
483
484 # Check message when no custom image
485 self.assertTrue(
486 "You have not created any custom images yet." in str(
487 self.find('#empty-state-customimagestable').text
488 )
489 )
490 div_empty_msg = self.find('#empty-state-customimagestable')
491 link_create_custom_image = div_empty_msg.find_element(
492 By.TAG_NAME, 'a')
493 self.assertTrue(TestProjectConfigTab.project_id is not None)
494 self.assertTrue(
495 f"/toastergui/project/{TestProjectConfigTab.project_id}/newcustomimage" in str(
496 link_create_custom_image.get_attribute('href')
497 )
498 )
499 self.assertTrue(
500 "Create your first custom image" in str(
501 link_create_custom_image.text
502 )
503 )
504 TestProjectConfigTab.project_id = None # reset project id
505
506 def test_project_page_image_recipe(self):
507 """ Test project page section images
508 - Check image recipes are displayed
509 - Check search input
510 - Check image recipe build button works
511 - Check image recipe table features(show/hide column, pagination)
512 """
513 self._navigate_to_project_page()
514 # navigate to "Images section"
515 images_section = self._get_config_nav_item(3)
516 images_section.click()
517 self.wait_until_visible('#imagerecipestable')
518 rows = self.find_all('#imagerecipestable tbody tr')
519 self.assertTrue(len(rows) > 0)
520
521 # Test search input
522 self.wait_until_visible('#search-input-imagerecipestable')
523 recipe_input = self.find('#search-input-imagerecipestable')
524 recipe_input.send_keys('core-image-minimal')
525 self.find('#search-submit-imagerecipestable').click()
526 self.wait_until_visible('#imagerecipestable tbody tr')
527 rows = self.find_all('#imagerecipestable tbody tr')
528 self.assertTrue(len(rows) > 0)
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 @@
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# BitBake Toaster UI tests implementation
4#
5# Copyright (C) 2023 Savoir-faire Linux
6#
7# SPDX-License-Identifier: GPL-2.0-only
8
9
10from time import sleep
11from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException
12from selenium.webdriver.common.by import By
13
14from orm.models import Build
15
16
17def wait_until_build(test_instance, state):
18 timeout = 60
19 start_time = 0
20 build_state = ''
21 while True:
22 try:
23 if start_time > timeout:
24 raise TimeoutException(
25 f'Build did not reach {state} state within {timeout} seconds'
26 )
27 last_build_state = test_instance.driver.find_element(
28 By.XPATH,
29 '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]',
30 )
31 build_state = last_build_state.get_attribute(
32 'data-build-state')
33 state_text = state.lower().split()
34 if any(x in str(build_state).lower() for x in state_text):
35 return str(build_state).lower()
36 if 'failed' in str(build_state).lower():
37 break
38 except NoSuchElementException:
39 continue
40 except TimeoutException:
41 break
42 start_time += 1
43 sleep(1) # take a breath and try again
44
45def wait_until_build_cancelled(test_instance):
46 """ Cancel build take a while sometime, the method is to wait driver action
47 until build being cancelled
48 """
49 timeout = 30
50 start_time = 0
51 build = None
52 while True:
53 try:
54 if start_time > timeout:
55 raise TimeoutException(
56 f'Build did not reach cancelled state within {timeout} seconds'
57 )
58 last_build_state = test_instance.driver.find_element(
59 By.XPATH,
60 '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]',
61 )
62 build_state = last_build_state.get_attribute(
63 'data-build-state')
64 if 'failed' in str(build_state).lower():
65 break
66 if 'cancelling' in str(build_state).lower():
67 # Change build state to cancelled
68 if not build: # get build object only once
69 build = Build.objects.last()
70 build.outcome = Build.CANCELLED
71 build.save()
72 if 'cancelled' in str(build_state).lower():
73 break
74 except NoSuchElementException:
75 continue
76 except StaleElementReferenceException:
77 continue
78 except TimeoutException:
79 break
80 start_time += 1
81 sleep(1) # take a breath and try again
82
83def get_projectId_from_url(url):
84 # url = 'http://domainename.com/toastergui/project/1656/whatever
85 # or url = 'http://domainename.com/toastergui/project/1/
86 # or url = 'http://domainename.com/toastergui/project/186
87 assert '/toastergui/project/' in url, "URL is not valid"
88 url_to_list = url.split('/toastergui/project/')
89 return int(url_to_list[1].split('/')[0]) # project_id