diff options
Diffstat (limited to 'bitbake/lib/toaster/tests/functional')
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 | |||
11 | import logging | 11 | import logging |
12 | import subprocess | 12 | import subprocess |
13 | import signal | 13 | import signal |
14 | import time | ||
15 | import re | 14 | import re |
16 | 15 | ||
17 | from tests.browser.selenium_helpers_base import SeleniumTestCaseBase | 16 | from tests.browser.selenium_helpers_base import SeleniumTestCaseBase |
18 | from tests.builds.buildtest import load_build_environment | 17 | from selenium.webdriver.common.by import By |
18 | from selenium.common.exceptions import NoSuchElementException | ||
19 | 19 | ||
20 | logger = logging.getLogger("toaster") | 20 | logger = logging.getLogger("toaster") |
21 | toaster_processes = [] | ||
21 | 22 | ||
22 | class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | 23 | class 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 | |||
9 | import re | ||
10 | import pytest | ||
11 | from django.urls import reverse | ||
12 | from selenium.webdriver.support.select import Select | ||
13 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
14 | from orm.models import Project | ||
15 | from selenium.webdriver.common.by import By | ||
16 | |||
17 | |||
18 | @pytest.mark.django_db | ||
19 | @pytest.mark.order("last") | ||
20 | class 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 | ||
10 | import re | 10 | import re |
11 | from django.urls import reverse | ||
12 | import pytest | ||
11 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | 13 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase |
12 | from orm.models import Project | 14 | from orm.models import Project |
15 | from selenium.webdriver.common.by import By | ||
13 | 16 | ||
17 | from tests.functional.utils import get_projectId_from_url | ||
18 | |||
19 | |||
20 | @pytest.mark.django_db | ||
21 | @pytest.mark.order("second_to_last") | ||
14 | class FuntionalTestBasic(SeleniumFunctionalTestCase): | 22 | class 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 | |||
9 | import string | ||
10 | import random | ||
11 | import pytest | ||
12 | from django.urls import reverse | ||
13 | from selenium.webdriver import Keys | ||
14 | from selenium.webdriver.support.select import Select | ||
15 | from selenium.common.exceptions import TimeoutException | ||
16 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
17 | from selenium.webdriver.common.by import By | ||
18 | |||
19 | from .utils import get_projectId_from_url | ||
20 | |||
21 | |||
22 | @pytest.mark.django_db | ||
23 | @pytest.mark.order("last") | ||
24 | class 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 | |||
9 | import os | ||
10 | import random | ||
11 | import string | ||
12 | from unittest import skip | ||
13 | import pytest | ||
14 | from django.urls import reverse | ||
15 | from django.utils import timezone | ||
16 | from selenium.webdriver.common.keys import Keys | ||
17 | from selenium.webdriver.support.select import Select | ||
18 | from selenium.common.exceptions import TimeoutException | ||
19 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
20 | from orm.models import Build, Project, Target | ||
21 | from selenium.webdriver.common.by import By | ||
22 | |||
23 | from .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") | ||
28 | class 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 | |||
9 | import string | ||
10 | import random | ||
11 | import pytest | ||
12 | from django.urls import reverse | ||
13 | from selenium.webdriver import Keys | ||
14 | from selenium.webdriver.support.select import Select | ||
15 | from selenium.common.exceptions import ElementClickInterceptedException, NoSuchElementException, TimeoutException | ||
16 | from orm.models import Project | ||
17 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
18 | from selenium.webdriver.common.by import By | ||
19 | |||
20 | from .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") | ||
25 | class 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 | |||
10 | from time import sleep | ||
11 | from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException | ||
12 | from selenium.webdriver.common.by import By | ||
13 | |||
14 | from orm.models import Build | ||
15 | |||
16 | |||
17 | def 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 | |||
45 | def 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 | |||
83 | def 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 | ||