diff options
Diffstat (limited to 'bitbake/lib/toaster/tests')
30 files changed, 2990 insertions, 277 deletions
diff --git a/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py b/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py index 644d45fe58..6953541ab5 100644 --- a/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py +++ b/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py | |||
@@ -19,11 +19,15 @@ import os | |||
19 | import time | 19 | import time |
20 | import unittest | 20 | import unittest |
21 | 21 | ||
22 | import pytest | ||
22 | from selenium import webdriver | 23 | from selenium import webdriver |
24 | from selenium.webdriver.support import expected_conditions as EC | ||
23 | from selenium.webdriver.support.ui import WebDriverWait | 25 | from selenium.webdriver.support.ui import WebDriverWait |
26 | from selenium.webdriver.common.by import By | ||
24 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities | 27 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities |
25 | from selenium.common.exceptions import NoSuchElementException, \ | 28 | from selenium.common.exceptions import NoSuchElementException, \ |
26 | StaleElementReferenceException, TimeoutException | 29 | StaleElementReferenceException, TimeoutException, \ |
30 | SessionNotCreatedException, WebDriverException | ||
27 | 31 | ||
28 | def create_selenium_driver(cls,browser='chrome'): | 32 | def create_selenium_driver(cls,browser='chrome'): |
29 | # set default browser string based on env (if available) | 33 | # set default browser string based on env (if available) |
@@ -32,9 +36,32 @@ def create_selenium_driver(cls,browser='chrome'): | |||
32 | browser = env_browser | 36 | browser = env_browser |
33 | 37 | ||
34 | if browser == 'chrome': | 38 | if browser == 'chrome': |
35 | return webdriver.Chrome( | 39 | options = webdriver.ChromeOptions() |
36 | service_args=["--verbose", "--log-path=selenium.log"] | 40 | options.add_argument('--headless') |
37 | ) | 41 | options.add_argument('--disable-infobars') |
42 | options.add_argument('--disable-dev-shm-usage') | ||
43 | options.add_argument('--no-sandbox') | ||
44 | options.add_argument('--remote-debugging-port=9222') | ||
45 | try: | ||
46 | return webdriver.Chrome(options=options) | ||
47 | except SessionNotCreatedException as e: | ||
48 | exit_message = "Halting tests prematurely to avoid cascading errors." | ||
49 | # check if chrome / chromedriver exists | ||
50 | chrome_path = os.popen("find ~/.cache/selenium/chrome/ -name 'chrome' -type f -print -quit").read().strip() | ||
51 | if not chrome_path: | ||
52 | pytest.exit(f"Failed to install/find chrome.\n{exit_message}") | ||
53 | chromedriver_path = os.popen("find ~/.cache/selenium/chromedriver/ -name 'chromedriver' -type f -print -quit").read().strip() | ||
54 | if not chromedriver_path: | ||
55 | pytest.exit(f"Failed to install/find chromedriver.\n{exit_message}") | ||
56 | # check if depends on each are fulfilled | ||
57 | depends_chrome = os.popen(f"ldd {chrome_path} | grep 'not found'").read().strip() | ||
58 | if depends_chrome: | ||
59 | pytest.exit(f"Missing chrome dependencies.\n{depends_chrome}\n{exit_message}") | ||
60 | depends_chromedriver = os.popen(f"ldd {chromedriver_path} | grep 'not found'").read().strip() | ||
61 | if depends_chromedriver: | ||
62 | pytest.exit(f"Missing chromedriver dependencies.\n{depends_chromedriver}\n{exit_message}") | ||
63 | # print original error otherwise | ||
64 | pytest.exit(f"Failed to start chromedriver.\n{e}\n{exit_message}") | ||
38 | elif browser == 'firefox': | 65 | elif browser == 'firefox': |
39 | return webdriver.Firefox() | 66 | return webdriver.Firefox() |
40 | elif browser == 'marionette': | 67 | elif browser == 'marionette': |
@@ -63,10 +90,12 @@ class Wait(WebDriverWait): | |||
63 | Subclass of WebDriverWait with predetermined timeout and poll | 90 | Subclass of WebDriverWait with predetermined timeout and poll |
64 | frequency. Also deals with a wider variety of exceptions. | 91 | frequency. Also deals with a wider variety of exceptions. |
65 | """ | 92 | """ |
66 | _TIMEOUT = 10 | 93 | _TIMEOUT = 20 |
67 | _POLL_FREQUENCY = 0.5 | 94 | _POLL_FREQUENCY = 0.5 |
68 | 95 | ||
69 | def __init__(self, driver): | 96 | def __init__(self, driver, timeout=_TIMEOUT, poll=_POLL_FREQUENCY): |
97 | self._TIMEOUT = timeout | ||
98 | self._POLL_FREQUENCY = poll | ||
70 | super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) | 99 | super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) |
71 | 100 | ||
72 | def until(self, method, message=''): | 101 | def until(self, method, message=''): |
@@ -85,6 +114,9 @@ class Wait(WebDriverWait): | |||
85 | pass | 114 | pass |
86 | except StaleElementReferenceException: | 115 | except StaleElementReferenceException: |
87 | pass | 116 | pass |
117 | except WebDriverException: | ||
118 | # selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Node with given id does not belong to the document"} | ||
119 | pass | ||
88 | 120 | ||
89 | time.sleep(self._poll) | 121 | time.sleep(self._poll) |
90 | if time.time() > end_time: | 122 | if time.time() > end_time: |
@@ -138,6 +170,8 @@ class SeleniumTestCaseBase(unittest.TestCase): | |||
138 | """ Clean up webdriver driver """ | 170 | """ Clean up webdriver driver """ |
139 | 171 | ||
140 | cls.driver.quit() | 172 | cls.driver.quit() |
173 | # Allow driver resources to be properly freed before proceeding with further tests | ||
174 | time.sleep(5) | ||
141 | super(SeleniumTestCaseBase, cls).tearDownClass() | 175 | super(SeleniumTestCaseBase, cls).tearDownClass() |
142 | 176 | ||
143 | def get(self, url): | 177 | def get(self, url): |
@@ -151,13 +185,20 @@ class SeleniumTestCaseBase(unittest.TestCase): | |||
151 | abs_url = '%s%s' % (self.live_server_url, url) | 185 | abs_url = '%s%s' % (self.live_server_url, url) |
152 | self.driver.get(abs_url) | 186 | self.driver.get(abs_url) |
153 | 187 | ||
188 | try: # Ensure page is loaded before proceeding | ||
189 | self.wait_until_visible("#global-nav") | ||
190 | except NoSuchElementException: | ||
191 | self.driver.implicitly_wait(3) | ||
192 | except TimeoutException: | ||
193 | self.driver.implicitly_wait(3) | ||
194 | |||
154 | def find(self, selector): | 195 | def find(self, selector): |
155 | """ Find single element by CSS selector """ | 196 | """ Find single element by CSS selector """ |
156 | return self.driver.find_element_by_css_selector(selector) | 197 | return self.driver.find_element(By.CSS_SELECTOR, selector) |
157 | 198 | ||
158 | def find_all(self, selector): | 199 | def find_all(self, selector): |
159 | """ Find all elements matching CSS selector """ | 200 | """ Find all elements matching CSS selector """ |
160 | return self.driver.find_elements_by_css_selector(selector) | 201 | return self.driver.find_elements(By.CSS_SELECTOR, selector) |
161 | 202 | ||
162 | def element_exists(self, selector): | 203 | def element_exists(self, selector): |
163 | """ | 204 | """ |
@@ -170,20 +211,43 @@ class SeleniumTestCaseBase(unittest.TestCase): | |||
170 | """ Return the element which currently has focus on the page """ | 211 | """ Return the element which currently has focus on the page """ |
171 | return self.driver.switch_to.active_element | 212 | return self.driver.switch_to.active_element |
172 | 213 | ||
173 | def wait_until_present(self, selector): | 214 | def wait_until_present(self, selector, timeout=Wait._TIMEOUT): |
174 | """ Wait until element matching CSS selector is on the page """ | 215 | """ Wait until element matching CSS selector is on the page """ |
175 | is_present = lambda driver: self.find(selector) | 216 | is_present = lambda driver: self.find(selector) |
176 | msg = 'An element matching "%s" should be on the page' % selector | 217 | msg = 'An element matching "%s" should be on the page' % selector |
177 | element = Wait(self.driver).until(is_present, msg) | 218 | element = Wait(self.driver, timeout=timeout).until(is_present, msg) |
178 | return element | 219 | return element |
179 | 220 | ||
180 | def wait_until_visible(self, selector): | 221 | def wait_until_visible(self, selector, timeout=Wait._TIMEOUT): |
181 | """ Wait until element matching CSS selector is visible on the page """ | 222 | """ Wait until element matching CSS selector is visible on the page """ |
182 | is_visible = lambda driver: self.find(selector).is_displayed() | 223 | is_visible = lambda driver: self.find(selector).is_displayed() |
183 | msg = 'An element matching "%s" should be visible' % selector | 224 | msg = 'An element matching "%s" should be visible' % selector |
184 | Wait(self.driver).until(is_visible, msg) | 225 | Wait(self.driver, timeout=timeout).until(is_visible, msg) |
226 | return self.find(selector) | ||
227 | |||
228 | def wait_until_not_visible(self, selector, timeout=Wait._TIMEOUT): | ||
229 | """ Wait until element matching CSS selector is not visible on the page """ | ||
230 | is_visible = lambda driver: self.find(selector).is_displayed() | ||
231 | msg = 'An element matching "%s" should be visible' % selector | ||
232 | Wait(self.driver, timeout=timeout).until_not(is_visible, msg) | ||
185 | return self.find(selector) | 233 | return self.find(selector) |
186 | 234 | ||
235 | def wait_until_clickable(self, selector, timeout=Wait._TIMEOUT): | ||
236 | """ Wait until element matching CSS selector is visible on the page """ | ||
237 | WebDriverWait(self.driver, timeout=timeout).until(lambda driver: self.driver.execute_script("return jQuery.active == 0")) | ||
238 | is_clickable = lambda driver: (self.find(selector).is_displayed() and self.find(selector).is_enabled()) | ||
239 | msg = 'An element matching "%s" should be clickable' % selector | ||
240 | Wait(self.driver, timeout=timeout).until(is_clickable, msg) | ||
241 | return self.find(selector) | ||
242 | |||
243 | def wait_until_element_clickable(self, finder, timeout=Wait._TIMEOUT): | ||
244 | """ Wait until element is clickable """ | ||
245 | WebDriverWait(self.driver, timeout=timeout).until(lambda driver: self.driver.execute_script("return jQuery.active == 0")) | ||
246 | is_clickable = lambda driver: (finder(driver).is_displayed() and finder(driver).is_enabled()) | ||
247 | msg = 'A matching element never became be clickable' | ||
248 | Wait(self.driver, timeout=timeout).until(is_clickable, msg) | ||
249 | return finder(self.driver) | ||
250 | |||
187 | def wait_until_focused(self, selector): | 251 | def wait_until_focused(self, selector): |
188 | """ Wait until element matching CSS selector has focus """ | 252 | """ Wait until element matching CSS selector has focus """ |
189 | is_focused = \ | 253 | is_focused = \ |
diff --git a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/bitbake/lib/toaster/tests/browser/test_all_builds_page.py index 8423d3dab2..9ab81fb11b 100644 --- a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py +++ b/bitbake/lib/toaster/tests/browser/test_all_builds_page.py | |||
@@ -7,13 +7,18 @@ | |||
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | 9 | ||
10 | import os | ||
10 | import re | 11 | import re |
11 | 12 | ||
12 | from django.urls import reverse | 13 | from django.urls import reverse |
14 | from selenium.webdriver.support.select import Select | ||
13 | from django.utils import timezone | 15 | from django.utils import timezone |
16 | from bldcontrol.models import BuildRequest | ||
14 | from tests.browser.selenium_helpers import SeleniumTestCase | 17 | from tests.browser.selenium_helpers import SeleniumTestCase |
15 | 18 | ||
16 | from orm.models import BitbakeVersion, Release, Project, Build, Target | 19 | from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task |
20 | |||
21 | from selenium.webdriver.common.by import By | ||
17 | 22 | ||
18 | 23 | ||
19 | class TestAllBuildsPage(SeleniumTestCase): | 24 | class TestAllBuildsPage(SeleniumTestCase): |
@@ -23,7 +28,8 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
23 | CLI_BUILDS_PROJECT_NAME = 'command line builds' | 28 | CLI_BUILDS_PROJECT_NAME = 'command line builds' |
24 | 29 | ||
25 | def setUp(self): | 30 | def setUp(self): |
26 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', | 31 | builldir = os.environ.get('BUILDDIR', './') |
32 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
27 | branch='master', dirpath='') | 33 | branch='master', dirpath='') |
28 | release = Release.objects.create(name='release1', | 34 | release = Release.objects.create(name='release1', |
29 | bitbake_version=bbv) | 35 | bitbake_version=bbv) |
@@ -69,7 +75,7 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
69 | '[data-role="data-recent-build-buildtime-field"]' % build.id | 75 | '[data-role="data-recent-build-buildtime-field"]' % build.id |
70 | 76 | ||
71 | # because this loads via Ajax, wait for it to be visible | 77 | # because this loads via Ajax, wait for it to be visible |
72 | self.wait_until_present(selector) | 78 | self.wait_until_visible(selector) |
73 | 79 | ||
74 | build_time_spans = self.find_all(selector) | 80 | build_time_spans = self.find_all(selector) |
75 | 81 | ||
@@ -79,7 +85,7 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
79 | 85 | ||
80 | def _get_row_for_build(self, build): | 86 | def _get_row_for_build(self, build): |
81 | """ Get the table row for the build from the all builds table """ | 87 | """ Get the table row for the build from the all builds table """ |
82 | self.wait_until_present('#allbuildstable') | 88 | self.wait_until_visible('#allbuildstable') |
83 | 89 | ||
84 | rows = self.find_all('#allbuildstable tr') | 90 | rows = self.find_all('#allbuildstable tr') |
85 | 91 | ||
@@ -91,7 +97,7 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
91 | found_row = None | 97 | found_row = None |
92 | for row in rows: | 98 | for row in rows: |
93 | 99 | ||
94 | outcome_links = row.find_elements_by_css_selector(selector) | 100 | outcome_links = row.find_elements(By.CSS_SELECTOR, selector) |
95 | if len(outcome_links) == 1: | 101 | if len(outcome_links) == 1: |
96 | found_row = row | 102 | found_row = row |
97 | break | 103 | break |
@@ -100,6 +106,66 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
100 | 106 | ||
101 | return found_row | 107 | return found_row |
102 | 108 | ||
109 | def _get_create_builds(self, **kwargs): | ||
110 | """ Create a build and return the build object """ | ||
111 | build1 = Build.objects.create(**self.project1_build_success) | ||
112 | build2 = Build.objects.create(**self.project1_build_failure) | ||
113 | |||
114 | # add some targets to these builds so they have recipe links | ||
115 | # (and so we can find the row in the ToasterTable corresponding to | ||
116 | # a particular build) | ||
117 | Target.objects.create(build=build1, target='foo') | ||
118 | Target.objects.create(build=build2, target='bar') | ||
119 | |||
120 | if kwargs: | ||
121 | # Create kwargs.get('success') builds with success status with target | ||
122 | # and kwargs.get('failure') builds with failure status with target | ||
123 | for i in range(kwargs.get('success', 0)): | ||
124 | now = timezone.now() | ||
125 | self.project1_build_success['started_on'] = now | ||
126 | self.project1_build_success[ | ||
127 | 'completed_on'] = now - timezone.timedelta(days=i) | ||
128 | build = Build.objects.create(**self.project1_build_success) | ||
129 | Target.objects.create(build=build, | ||
130 | target=f'{i}_success_recipe', | ||
131 | task=f'{i}_success_task') | ||
132 | |||
133 | self._set_buildRequest_and_task_on_build(build) | ||
134 | for i in range(kwargs.get('failure', 0)): | ||
135 | now = timezone.now() | ||
136 | self.project1_build_failure['started_on'] = now | ||
137 | self.project1_build_failure[ | ||
138 | 'completed_on'] = now - timezone.timedelta(days=i) | ||
139 | build = Build.objects.create(**self.project1_build_failure) | ||
140 | Target.objects.create(build=build, | ||
141 | target=f'{i}_fail_recipe', | ||
142 | task=f'{i}_fail_task') | ||
143 | self._set_buildRequest_and_task_on_build(build) | ||
144 | return build1, build2 | ||
145 | |||
146 | def _create_recipe(self): | ||
147 | """ Add a recipe to the database and return it """ | ||
148 | layer = Layer.objects.create() | ||
149 | layer_version = Layer_Version.objects.create(layer=layer) | ||
150 | return Recipe.objects.create(name='recipe_foo', layer_version=layer_version) | ||
151 | |||
152 | def _set_buildRequest_and_task_on_build(self, build): | ||
153 | """ Set buildRequest and task on build """ | ||
154 | build.recipes_parsed = 1 | ||
155 | build.save() | ||
156 | buildRequest = BuildRequest.objects.create( | ||
157 | build=build, | ||
158 | project=self.project1, | ||
159 | state=BuildRequest.REQ_COMPLETED) | ||
160 | build.build_request = buildRequest | ||
161 | recipe = self._create_recipe() | ||
162 | task = Task.objects.create(build=build, | ||
163 | recipe=recipe, | ||
164 | task_name='task', | ||
165 | outcome=Task.OUTCOME_SUCCESS) | ||
166 | task.save() | ||
167 | build.save() | ||
168 | |||
103 | def test_show_tasks_with_suffix(self): | 169 | def test_show_tasks_with_suffix(self): |
104 | """ Task should be shown as suffix on build name """ | 170 | """ Task should be shown as suffix on build name """ |
105 | build = Build.objects.create(**self.project1_build_success) | 171 | build = Build.objects.create(**self.project1_build_success) |
@@ -109,7 +175,7 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
109 | 175 | ||
110 | url = reverse('all-builds') | 176 | url = reverse('all-builds') |
111 | self.get(url) | 177 | self.get(url) |
112 | self.wait_until_present('td[class="target"]') | 178 | self.wait_until_visible('td[class="target"]') |
113 | 179 | ||
114 | cell = self.find('td[class="target"]') | 180 | cell = self.find('td[class="target"]') |
115 | content = cell.get_attribute('innerHTML') | 181 | content = cell.get_attribute('innerHTML') |
@@ -126,23 +192,26 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
126 | but should be shown for other builds | 192 | but should be shown for other builds |
127 | """ | 193 | """ |
128 | build1 = Build.objects.create(**self.project1_build_success) | 194 | build1 = Build.objects.create(**self.project1_build_success) |
129 | default_build = Build.objects.create(**self.default_project_build_success) | 195 | default_build = Build.objects.create( |
196 | **self.default_project_build_success) | ||
130 | 197 | ||
131 | url = reverse('all-builds') | 198 | url = reverse('all-builds') |
132 | self.get(url) | 199 | self.get(url) |
133 | 200 | ||
134 | # shouldn't see a rebuild button for command-line builds | ||
135 | selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id | ||
136 | run_again_button = self.find_all(selector) | ||
137 | self.assertEqual(len(run_again_button), 0, | ||
138 | 'should not see a rebuild button for cli builds') | ||
139 | |||
140 | # should see a rebuild button for non-command-line builds | 201 | # should see a rebuild button for non-command-line builds |
202 | self.wait_until_visible('#allbuildstable tbody tr') | ||
203 | self.wait_until_visible('.rebuild-btn') | ||
141 | selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id | 204 | selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id |
142 | run_again_button = self.find_all(selector) | 205 | run_again_button = self.find_all(selector) |
143 | self.assertEqual(len(run_again_button), 1, | 206 | self.assertEqual(len(run_again_button), 1, |
144 | 'should see a rebuild button for non-cli builds') | 207 | 'should see a rebuild button for non-cli builds') |
145 | 208 | ||
209 | # shouldn't see a rebuild button for command-line builds | ||
210 | selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id | ||
211 | run_again_button = self.find_all(selector) | ||
212 | self.assertEqual(len(run_again_button), 0, | ||
213 | 'should not see a rebuild button for cli builds') | ||
214 | |||
146 | def test_tooltips_on_project_name(self): | 215 | def test_tooltips_on_project_name(self): |
147 | """ | 216 | """ |
148 | Test tooltips shown next to project name in the main table | 217 | Test tooltips shown next to project name in the main table |
@@ -156,6 +225,7 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
156 | 225 | ||
157 | url = reverse('all-builds') | 226 | url = reverse('all-builds') |
158 | self.get(url) | 227 | self.get(url) |
228 | self.wait_until_visible('#allbuildstable') | ||
159 | 229 | ||
160 | # get the project name cells from the table | 230 | # get the project name cells from the table |
161 | cells = self.find_all('#allbuildstable td[class="project"]') | 231 | cells = self.find_all('#allbuildstable td[class="project"]') |
@@ -164,7 +234,7 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
164 | 234 | ||
165 | for cell in cells: | 235 | for cell in cells: |
166 | content = cell.get_attribute('innerHTML') | 236 | content = cell.get_attribute('innerHTML') |
167 | help_icons = cell.find_elements_by_css_selector(selector) | 237 | help_icons = cell.find_elements(By.CSS_SELECTOR, selector) |
168 | 238 | ||
169 | if re.search(self.PROJECT_NAME, content): | 239 | if re.search(self.PROJECT_NAME, content): |
170 | # no help icon next to non-cli project name | 240 | # no help icon next to non-cli project name |
@@ -184,38 +254,224 @@ class TestAllBuildsPage(SeleniumTestCase): | |||
184 | recent builds area; failed builds should not have links on the time column, | 254 | recent builds area; failed builds should not have links on the time column, |
185 | or in the recent builds area | 255 | or in the recent builds area |
186 | """ | 256 | """ |
187 | build1 = Build.objects.create(**self.project1_build_success) | 257 | build1, build2 = self._get_create_builds() |
188 | build2 = Build.objects.create(**self.project1_build_failure) | ||
189 | |||
190 | # add some targets to these builds so they have recipe links | ||
191 | # (and so we can find the row in the ToasterTable corresponding to | ||
192 | # a particular build) | ||
193 | Target.objects.create(build=build1, target='foo') | ||
194 | Target.objects.create(build=build2, target='bar') | ||
195 | 258 | ||
196 | url = reverse('all-builds') | 259 | url = reverse('all-builds') |
197 | self.get(url) | 260 | self.get(url) |
261 | self.wait_until_visible('#allbuildstable') | ||
198 | 262 | ||
199 | # test recent builds area for successful build | 263 | # test recent builds area for successful build |
200 | element = self._get_build_time_element(build1) | 264 | element = self._get_build_time_element(build1) |
201 | links = element.find_elements_by_css_selector('a') | 265 | links = element.find_elements(By.CSS_SELECTOR, 'a') |
202 | msg = 'should be a link on the build time for a successful recent build' | 266 | msg = 'should be a link on the build time for a successful recent build' |
203 | self.assertEquals(len(links), 1, msg) | 267 | self.assertEqual(len(links), 1, msg) |
204 | 268 | ||
205 | # test recent builds area for failed build | 269 | # test recent builds area for failed build |
206 | element = self._get_build_time_element(build2) | 270 | element = self._get_build_time_element(build2) |
207 | links = element.find_elements_by_css_selector('a') | 271 | links = element.find_elements(By.CSS_SELECTOR, 'a') |
208 | msg = 'should not be a link on the build time for a failed recent build' | 272 | msg = 'should not be a link on the build time for a failed recent build' |
209 | self.assertEquals(len(links), 0, msg) | 273 | self.assertEqual(len(links), 0, msg) |
210 | 274 | ||
211 | # test the time column for successful build | 275 | # test the time column for successful build |
212 | build1_row = self._get_row_for_build(build1) | 276 | build1_row = self._get_row_for_build(build1) |
213 | links = build1_row.find_elements_by_css_selector('td.time a') | 277 | links = build1_row.find_elements(By.CSS_SELECTOR, 'td.time a') |
214 | msg = 'should be a link on the build time for a successful build' | 278 | msg = 'should be a link on the build time for a successful build' |
215 | self.assertEquals(len(links), 1, msg) | 279 | self.assertEqual(len(links), 1, msg) |
216 | 280 | ||
217 | # test the time column for failed build | 281 | # test the time column for failed build |
218 | build2_row = self._get_row_for_build(build2) | 282 | build2_row = self._get_row_for_build(build2) |
219 | links = build2_row.find_elements_by_css_selector('td.time a') | 283 | links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a') |
220 | msg = 'should not be a link on the build time for a failed build' | 284 | msg = 'should not be a link on the build time for a failed build' |
221 | self.assertEquals(len(links), 0, msg) | 285 | self.assertEqual(len(links), 0, msg) |
286 | |||
287 | def test_builds_table_search_box(self): | ||
288 | """ Test the search box in the builds table on the all builds page """ | ||
289 | self._get_create_builds() | ||
290 | |||
291 | url = reverse('all-builds') | ||
292 | self.get(url) | ||
293 | |||
294 | # Check search box is present and works | ||
295 | self.wait_until_visible('#allbuildstable tbody tr') | ||
296 | search_box = self.find('#search-input-allbuildstable') | ||
297 | self.assertTrue(search_box.is_displayed()) | ||
298 | |||
299 | # Check that we can search for a build by recipe name | ||
300 | search_box.send_keys('foo') | ||
301 | search_btn = self.find('#search-submit-allbuildstable') | ||
302 | search_btn.click() | ||
303 | self.wait_until_visible('#allbuildstable tbody tr') | ||
304 | rows = self.find_all('#allbuildstable tbody tr') | ||
305 | self.assertTrue(len(rows) >= 1) | ||
306 | |||
307 | def test_filtering_on_failure_tasks_column(self): | ||
308 | """ Test the filtering on failure tasks column in the builds table on the all builds page """ | ||
309 | def _check_if_filter_failed_tasks_column_is_visible(): | ||
310 | # check if failed tasks filter column is visible, if not click on it | ||
311 | # Check edit column | ||
312 | edit_column = self.find('#edit-columns-button') | ||
313 | self.assertTrue(edit_column.is_displayed()) | ||
314 | edit_column.click() | ||
315 | # Check dropdown is visible | ||
316 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
317 | filter_fails_task_checkbox = self.find('#checkbox-failed_tasks') | ||
318 | if not filter_fails_task_checkbox.is_selected(): | ||
319 | filter_fails_task_checkbox.click() | ||
320 | edit_column.click() | ||
321 | |||
322 | self._get_create_builds(success=10, failure=10) | ||
323 | |||
324 | url = reverse('all-builds') | ||
325 | self.get(url) | ||
326 | |||
327 | # Check filtering on failure tasks column | ||
328 | self.wait_until_visible('#allbuildstable tbody tr') | ||
329 | _check_if_filter_failed_tasks_column_is_visible() | ||
330 | failed_tasks_filter = self.find('#failed_tasks_filter') | ||
331 | failed_tasks_filter.click() | ||
332 | # Check popup is visible | ||
333 | self.wait_until_visible('#filter-modal-allbuildstable') | ||
334 | self.assertTrue( | ||
335 | self.find('#filter-modal-allbuildstable').is_displayed()) | ||
336 | # Check that we can filter by failure tasks | ||
337 | build_without_failure_tasks = self.find( | ||
338 | '#failed_tasks_filter\\:without_failed_tasks') | ||
339 | build_without_failure_tasks.click() | ||
340 | # click on apply button | ||
341 | self.find('#filter-modal-allbuildstable .btn-primary').click() | ||
342 | self.wait_until_visible('#allbuildstable tbody tr') | ||
343 | # Check if filter is applied, by checking if failed_tasks_filter has btn-primary class | ||
344 | self.assertTrue(self.find('#failed_tasks_filter').get_attribute( | ||
345 | 'class').find('btn-primary') != -1) | ||
346 | |||
347 | def test_filtering_on_completedOn_column(self): | ||
348 | """ Test the filtering on completed_on column in the builds table on the all builds page """ | ||
349 | self._get_create_builds(success=10, failure=10) | ||
350 | |||
351 | url = reverse('all-builds') | ||
352 | self.get(url) | ||
353 | |||
354 | # Check filtering on failure tasks column | ||
355 | self.wait_until_visible('#allbuildstable tbody tr') | ||
356 | completed_on_filter = self.find('#completed_on_filter') | ||
357 | completed_on_filter.click() | ||
358 | # Check popup is visible | ||
359 | self.wait_until_visible('#filter-modal-allbuildstable') | ||
360 | self.assertTrue( | ||
361 | self.find('#filter-modal-allbuildstable').is_displayed()) | ||
362 | # Check that we can filter by failure tasks | ||
363 | build_without_failure_tasks = self.find( | ||
364 | '#completed_on_filter\\:date_range') | ||
365 | build_without_failure_tasks.click() | ||
366 | # click on apply button | ||
367 | self.find('#filter-modal-allbuildstable .btn-primary').click() | ||
368 | self.wait_until_visible('#allbuildstable tbody tr') | ||
369 | # Check if filter is applied, by checking if completed_on_filter has btn-primary class | ||
370 | self.assertTrue(self.find('#completed_on_filter').get_attribute( | ||
371 | 'class').find('btn-primary') != -1) | ||
372 | |||
373 | # Filter by date range | ||
374 | self.find('#completed_on_filter').click() | ||
375 | self.wait_until_visible('#filter-modal-allbuildstable') | ||
376 | date_ranges = self.driver.find_elements( | ||
377 | By.XPATH, '//input[@class="form-control hasDatepicker"]') | ||
378 | today = timezone.now() | ||
379 | yestersday = today - timezone.timedelta(days=1) | ||
380 | date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d')) | ||
381 | date_ranges[1].send_keys(today.strftime('%Y-%m-%d')) | ||
382 | self.find('#filter-modal-allbuildstable .btn-primary').click() | ||
383 | self.wait_until_visible('#allbuildstable tbody tr') | ||
384 | self.assertTrue(self.find('#completed_on_filter').get_attribute( | ||
385 | 'class').find('btn-primary') != -1) | ||
386 | # Check if filter is applied, number of builds displayed should be 6 | ||
387 | self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) >= 4) | ||
388 | |||
389 | def test_builds_table_editColumn(self): | ||
390 | """ Test the edit column feature in the builds table on the all builds page """ | ||
391 | self._get_create_builds(success=10, failure=10) | ||
392 | |||
393 | def test_edit_column(check_box_id): | ||
394 | # Check that we can hide/show table column | ||
395 | check_box = self.find(f'#{check_box_id}') | ||
396 | th_class = str(check_box_id).replace('checkbox-', '') | ||
397 | if check_box.is_selected(): | ||
398 | # check if column is visible in table | ||
399 | self.assertTrue( | ||
400 | self.find( | ||
401 | f'#allbuildstable thead th.{th_class}' | ||
402 | ).is_displayed(), | ||
403 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
404 | ) | ||
405 | check_box.click() | ||
406 | # check if column is hidden in table | ||
407 | self.assertFalse( | ||
408 | self.find( | ||
409 | f'#allbuildstable thead th.{th_class}' | ||
410 | ).is_displayed(), | ||
411 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
412 | ) | ||
413 | else: | ||
414 | # check if column is hidden in table | ||
415 | self.assertFalse( | ||
416 | self.find( | ||
417 | f'#allbuildstable thead th.{th_class}' | ||
418 | ).is_displayed(), | ||
419 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
420 | ) | ||
421 | check_box.click() | ||
422 | # check if column is visible in table | ||
423 | self.assertTrue( | ||
424 | self.find( | ||
425 | f'#allbuildstable thead th.{th_class}' | ||
426 | ).is_displayed(), | ||
427 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
428 | ) | ||
429 | url = reverse('all-builds') | ||
430 | self.get(url) | ||
431 | self.wait_until_visible('#allbuildstable tbody tr') | ||
432 | |||
433 | # Check edit column | ||
434 | edit_column = self.find('#edit-columns-button') | ||
435 | self.assertTrue(edit_column.is_displayed()) | ||
436 | edit_column.click() | ||
437 | # Check dropdown is visible | ||
438 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
439 | |||
440 | # Check that we can hide the edit column | ||
441 | test_edit_column('checkbox-errors_no') | ||
442 | test_edit_column('checkbox-failed_tasks') | ||
443 | test_edit_column('checkbox-image_files') | ||
444 | test_edit_column('checkbox-project') | ||
445 | test_edit_column('checkbox-started_on') | ||
446 | test_edit_column('checkbox-time') | ||
447 | test_edit_column('checkbox-warnings_no') | ||
448 | |||
449 | def test_builds_table_show_rows(self): | ||
450 | """ Test the show rows feature in the builds table on the all builds page """ | ||
451 | self._get_create_builds(success=100, failure=100) | ||
452 | |||
453 | def test_show_rows(row_to_show, show_row_link): | ||
454 | # Check that we can show rows == row_to_show | ||
455 | show_row_link.select_by_value(str(row_to_show)) | ||
456 | self.wait_until_visible('#allbuildstable tbody tr') | ||
457 | # check at least some rows are visible | ||
458 | self.assertTrue( | ||
459 | len(self.find_all('#allbuildstable tbody tr')) > 0 | ||
460 | ) | ||
461 | |||
462 | url = reverse('all-builds') | ||
463 | self.get(url) | ||
464 | self.wait_until_visible('#allbuildstable tbody tr') | ||
465 | |||
466 | show_rows = self.driver.find_elements( | ||
467 | By.XPATH, | ||
468 | '//select[@class="form-control pagesize-allbuildstable"]' | ||
469 | ) | ||
470 | # Check show rows | ||
471 | for show_row_link in show_rows: | ||
472 | show_row_link = Select(show_row_link) | ||
473 | test_show_rows(10, show_row_link) | ||
474 | test_show_rows(25, show_row_link) | ||
475 | test_show_rows(50, show_row_link) | ||
476 | test_show_rows(100, show_row_link) | ||
477 | test_show_rows(150, show_row_link) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_all_projects_page.py b/bitbake/lib/toaster/tests/browser/test_all_projects_page.py index 15b03400f9..05e12892be 100644 --- a/bitbake/lib/toaster/tests/browser/test_all_projects_page.py +++ b/bitbake/lib/toaster/tests/browser/test_all_projects_page.py | |||
@@ -7,15 +7,20 @@ | |||
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | 9 | ||
10 | import os | ||
10 | import re | 11 | import re |
11 | 12 | ||
12 | from django.urls import reverse | 13 | from django.urls import reverse |
13 | from django.utils import timezone | 14 | from django.utils import timezone |
15 | from selenium.webdriver.support.select import Select | ||
14 | from tests.browser.selenium_helpers import SeleniumTestCase | 16 | from tests.browser.selenium_helpers import SeleniumTestCase |
15 | 17 | ||
16 | from orm.models import BitbakeVersion, Release, Project, Build | 18 | from orm.models import BitbakeVersion, Release, Project, Build |
17 | from orm.models import ProjectVariable | 19 | from orm.models import ProjectVariable |
18 | 20 | ||
21 | from selenium.webdriver.common.by import By | ||
22 | |||
23 | |||
19 | class TestAllProjectsPage(SeleniumTestCase): | 24 | class TestAllProjectsPage(SeleniumTestCase): |
20 | """ Browser tests for projects page /projects/ """ | 25 | """ Browser tests for projects page /projects/ """ |
21 | 26 | ||
@@ -25,7 +30,8 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
25 | 30 | ||
26 | def setUp(self): | 31 | def setUp(self): |
27 | """ Add default project manually """ | 32 | """ Add default project manually """ |
28 | project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None) | 33 | project = Project.objects.create_project( |
34 | self.CLI_BUILDS_PROJECT_NAME, None) | ||
29 | self.default_project = project | 35 | self.default_project = project |
30 | self.default_project.is_default = True | 36 | self.default_project.is_default = True |
31 | self.default_project.save() | 37 | self.default_project.save() |
@@ -35,6 +41,17 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
35 | 41 | ||
36 | self.release = None | 42 | self.release = None |
37 | 43 | ||
44 | def _create_projects(self, nb_project=10): | ||
45 | projects = [] | ||
46 | for i in range(1, nb_project + 1): | ||
47 | projects.append( | ||
48 | Project( | ||
49 | name='test project {}'.format(i), | ||
50 | release=self.release, | ||
51 | ) | ||
52 | ) | ||
53 | Project.objects.bulk_create(projects) | ||
54 | |||
38 | def _add_build_to_default_project(self): | 55 | def _add_build_to_default_project(self): |
39 | """ Add a build to the default project (not used in all tests) """ | 56 | """ Add a build to the default project (not used in all tests) """ |
40 | now = timezone.now() | 57 | now = timezone.now() |
@@ -45,12 +62,14 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
45 | 62 | ||
46 | def _add_non_default_project(self): | 63 | def _add_non_default_project(self): |
47 | """ Add another project """ | 64 | """ Add another project """ |
48 | bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', | 65 | builldir = os.environ.get('BUILDDIR', './') |
66 | bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/', | ||
49 | branch='master', dirpath='') | 67 | branch='master', dirpath='') |
50 | self.release = Release.objects.create(name='test release', | 68 | self.release = Release.objects.create(name='test release', |
51 | branch_name='master', | 69 | branch_name='master', |
52 | bitbake_version=bbv) | 70 | bitbake_version=bbv) |
53 | self.project = Project.objects.create_project(self.PROJECT_NAME, self.release) | 71 | self.project = Project.objects.create_project( |
72 | self.PROJECT_NAME, self.release) | ||
54 | self.project.is_default = False | 73 | self.project.is_default = False |
55 | self.project.save() | 74 | self.project.save() |
56 | 75 | ||
@@ -62,7 +81,7 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
62 | 81 | ||
63 | def _get_row_for_project(self, project_name): | 82 | def _get_row_for_project(self, project_name): |
64 | """ Get the HTML row for a project, or None if not found """ | 83 | """ Get the HTML row for a project, or None if not found """ |
65 | self.wait_until_present('#projectstable tbody tr') | 84 | self.wait_until_visible('#projectstable tbody tr') |
66 | rows = self.find_all('#projectstable tbody tr') | 85 | rows = self.find_all('#projectstable tbody tr') |
67 | 86 | ||
68 | # find the row with a project name matching the one supplied | 87 | # find the row with a project name matching the one supplied |
@@ -93,7 +112,8 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
93 | url = reverse('all-projects') | 112 | url = reverse('all-projects') |
94 | self.get(url) | 113 | self.get(url) |
95 | 114 | ||
96 | default_project_row = self._get_row_for_project(self.default_project.name) | 115 | default_project_row = self._get_row_for_project( |
116 | self.default_project.name) | ||
97 | 117 | ||
98 | self.assertNotEqual(default_project_row, None, | 118 | self.assertNotEqual(default_project_row, None, |
99 | 'default project "cli builds" should be in page') | 119 | 'default project "cli builds" should be in page') |
@@ -113,11 +133,12 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
113 | self.wait_until_visible("#projectstable tr") | 133 | self.wait_until_visible("#projectstable tr") |
114 | 134 | ||
115 | # find the row for the default project | 135 | # find the row for the default project |
116 | default_project_row = self._get_row_for_project(self.default_project.name) | 136 | default_project_row = self._get_row_for_project( |
137 | self.default_project.name) | ||
117 | 138 | ||
118 | # check the release text for the default project | 139 | # check the release text for the default project |
119 | selector = 'span[data-project-field="release"] span.text-muted' | 140 | selector = 'span[data-project-field="release"] span.text-muted' |
120 | element = default_project_row.find_element_by_css_selector(selector) | 141 | element = default_project_row.find_element(By.CSS_SELECTOR, selector) |
121 | text = element.text.strip() | 142 | text = element.text.strip() |
122 | self.assertEqual(text, 'Not applicable', | 143 | self.assertEqual(text, 'Not applicable', |
123 | 'release should be "not applicable" for default project') | 144 | 'release should be "not applicable" for default project') |
@@ -127,7 +148,7 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
127 | 148 | ||
128 | # check the link in the release cell for the other project | 149 | # check the link in the release cell for the other project |
129 | selector = 'span[data-project-field="release"]' | 150 | selector = 'span[data-project-field="release"]' |
130 | element = other_project_row.find_element_by_css_selector(selector) | 151 | element = other_project_row.find_element(By.CSS_SELECTOR, selector) |
131 | text = element.text.strip() | 152 | text = element.text.strip() |
132 | self.assertEqual(text, self.release.name, | 153 | self.assertEqual(text, self.release.name, |
133 | 'release name should be shown for non-default project') | 154 | 'release name should be shown for non-default project') |
@@ -148,11 +169,12 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
148 | self.wait_until_visible("#projectstable tr") | 169 | self.wait_until_visible("#projectstable tr") |
149 | 170 | ||
150 | # find the row for the default project | 171 | # find the row for the default project |
151 | default_project_row = self._get_row_for_project(self.default_project.name) | 172 | default_project_row = self._get_row_for_project( |
173 | self.default_project.name) | ||
152 | 174 | ||
153 | # check the machine cell for the default project | 175 | # check the machine cell for the default project |
154 | selector = 'span[data-project-field="machine"] span.text-muted' | 176 | selector = 'span[data-project-field="machine"] span.text-muted' |
155 | element = default_project_row.find_element_by_css_selector(selector) | 177 | element = default_project_row.find_element(By.CSS_SELECTOR, selector) |
156 | text = element.text.strip() | 178 | text = element.text.strip() |
157 | self.assertEqual(text, 'Not applicable', | 179 | self.assertEqual(text, 'Not applicable', |
158 | 'machine should be not applicable for default project') | 180 | 'machine should be not applicable for default project') |
@@ -162,7 +184,7 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
162 | 184 | ||
163 | # check the link in the machine cell for the other project | 185 | # check the link in the machine cell for the other project |
164 | selector = 'span[data-project-field="machine"]' | 186 | selector = 'span[data-project-field="machine"]' |
165 | element = other_project_row.find_element_by_css_selector(selector) | 187 | element = other_project_row.find_element(By.CSS_SELECTOR, selector) |
166 | text = element.text.strip() | 188 | text = element.text.strip() |
167 | self.assertEqual(text, self.MACHINE_NAME, | 189 | self.assertEqual(text, self.MACHINE_NAME, |
168 | 'machine name should be shown for non-default project') | 190 | 'machine name should be shown for non-default project') |
@@ -183,13 +205,15 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
183 | self.get(reverse('all-projects')) | 205 | self.get(reverse('all-projects')) |
184 | 206 | ||
185 | # find the row for the default project | 207 | # find the row for the default project |
186 | default_project_row = self._get_row_for_project(self.default_project.name) | 208 | default_project_row = self._get_row_for_project( |
209 | self.default_project.name) | ||
187 | 210 | ||
188 | # check the link on the name field | 211 | # check the link on the name field |
189 | selector = 'span[data-project-field="name"] a' | 212 | selector = 'span[data-project-field="name"] a' |
190 | element = default_project_row.find_element_by_css_selector(selector) | 213 | element = default_project_row.find_element(By.CSS_SELECTOR, selector) |
191 | link_url = element.get_attribute('href').strip() | 214 | link_url = element.get_attribute('href').strip() |
192 | expected_url = reverse('projectbuilds', args=(self.default_project.id,)) | 215 | expected_url = reverse( |
216 | 'projectbuilds', args=(self.default_project.id,)) | ||
193 | msg = 'link on default project name should point to builds but was %s' % link_url | 217 | msg = 'link on default project name should point to builds but was %s' % link_url |
194 | self.assertTrue(link_url.endswith(expected_url), msg) | 218 | self.assertTrue(link_url.endswith(expected_url), msg) |
195 | 219 | ||
@@ -198,8 +222,116 @@ class TestAllProjectsPage(SeleniumTestCase): | |||
198 | 222 | ||
199 | # check the link for the other project | 223 | # check the link for the other project |
200 | selector = 'span[data-project-field="name"] a' | 224 | selector = 'span[data-project-field="name"] a' |
201 | element = other_project_row.find_element_by_css_selector(selector) | 225 | element = other_project_row.find_element(By.CSS_SELECTOR, selector) |
202 | link_url = element.get_attribute('href').strip() | 226 | link_url = element.get_attribute('href').strip() |
203 | expected_url = reverse('project', args=(self.project.id,)) | 227 | expected_url = reverse('project', args=(self.project.id,)) |
204 | msg = 'link on project name should point to configuration but was %s' % link_url | 228 | msg = 'link on project name should point to configuration but was %s' % link_url |
205 | self.assertTrue(link_url.endswith(expected_url), msg) | 229 | self.assertTrue(link_url.endswith(expected_url), msg) |
230 | |||
231 | def test_allProject_table_search_box(self): | ||
232 | """ Test the search box in the all project table on the all projects page """ | ||
233 | self._create_projects() | ||
234 | |||
235 | url = reverse('all-projects') | ||
236 | self.get(url) | ||
237 | |||
238 | # Chseck search box is present and works | ||
239 | self.wait_until_visible('#projectstable tbody tr') | ||
240 | search_box = self.find('#search-input-projectstable') | ||
241 | self.assertTrue(search_box.is_displayed()) | ||
242 | |||
243 | # Check that we can search for a project by project name | ||
244 | search_box.send_keys('test project 10') | ||
245 | search_btn = self.find('#search-submit-projectstable') | ||
246 | search_btn.click() | ||
247 | self.wait_until_visible('#projectstable tbody tr') | ||
248 | rows = self.find_all('#projectstable tbody tr') | ||
249 | self.assertTrue(len(rows) == 1) | ||
250 | |||
251 | def test_allProject_table_editColumn(self): | ||
252 | """ Test the edit column feature in the projects table on the all projects page """ | ||
253 | self._create_projects() | ||
254 | |||
255 | def test_edit_column(check_box_id): | ||
256 | # Check that we can hide/show table column | ||
257 | check_box = self.find(f'#{check_box_id}') | ||
258 | th_class = str(check_box_id).replace('checkbox-', '') | ||
259 | if check_box.is_selected(): | ||
260 | # check if column is visible in table | ||
261 | self.assertTrue( | ||
262 | self.find( | ||
263 | f'#projectstable thead th.{th_class}' | ||
264 | ).is_displayed(), | ||
265 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
266 | ) | ||
267 | check_box.click() | ||
268 | # check if column is hidden in table | ||
269 | self.assertFalse( | ||
270 | self.find( | ||
271 | f'#projectstable thead th.{th_class}' | ||
272 | ).is_displayed(), | ||
273 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
274 | ) | ||
275 | else: | ||
276 | # check if column is hidden in table | ||
277 | self.assertFalse( | ||
278 | self.find( | ||
279 | f'#projectstable thead th.{th_class}' | ||
280 | ).is_displayed(), | ||
281 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
282 | ) | ||
283 | check_box.click() | ||
284 | # check if column is visible in table | ||
285 | self.assertTrue( | ||
286 | self.find( | ||
287 | f'#projectstable thead th.{th_class}' | ||
288 | ).is_displayed(), | ||
289 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
290 | ) | ||
291 | url = reverse('all-projects') | ||
292 | self.get(url) | ||
293 | self.wait_until_visible('#projectstable tbody tr') | ||
294 | |||
295 | # Check edit column | ||
296 | edit_column = self.find('#edit-columns-button') | ||
297 | self.assertTrue(edit_column.is_displayed()) | ||
298 | edit_column.click() | ||
299 | # Check dropdown is visible | ||
300 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
301 | |||
302 | # Check that we can hide the edit column | ||
303 | test_edit_column('checkbox-errors') | ||
304 | test_edit_column('checkbox-image_files') | ||
305 | test_edit_column('checkbox-last_build_outcome') | ||
306 | test_edit_column('checkbox-recipe_name') | ||
307 | test_edit_column('checkbox-warnings') | ||
308 | |||
309 | def test_allProject_table_show_rows(self): | ||
310 | """ Test the show rows feature in the projects table on the all projects page """ | ||
311 | self._create_projects(nb_project=200) | ||
312 | |||
313 | def test_show_rows(row_to_show, show_row_link): | ||
314 | # Check that we can show rows == row_to_show | ||
315 | show_row_link.select_by_value(str(row_to_show)) | ||
316 | self.wait_until_visible('#projectstable tbody tr') | ||
317 | # check at least some rows are visible | ||
318 | self.assertTrue( | ||
319 | len(self.find_all('#projectstable tbody tr')) > 0 | ||
320 | ) | ||
321 | |||
322 | url = reverse('all-projects') | ||
323 | self.get(url) | ||
324 | self.wait_until_visible('#projectstable tbody tr') | ||
325 | |||
326 | show_rows = self.driver.find_elements( | ||
327 | By.XPATH, | ||
328 | '//select[@class="form-control pagesize-projectstable"]' | ||
329 | ) | ||
330 | # Check show rows | ||
331 | for show_row_link in show_rows: | ||
332 | show_row_link = Select(show_row_link) | ||
333 | test_show_rows(10, show_row_link) | ||
334 | test_show_rows(25, show_row_link) | ||
335 | test_show_rows(50, show_row_link) | ||
336 | test_show_rows(100, show_row_link) | ||
337 | test_show_rows(150, show_row_link) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py b/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py index efcd89b346..82367108e2 100644 --- a/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py +++ b/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py | |||
@@ -7,6 +7,7 @@ | |||
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | 9 | ||
10 | import os | ||
10 | from django.urls import reverse | 11 | from django.urls import reverse |
11 | from django.utils import timezone | 12 | from django.utils import timezone |
12 | 13 | ||
@@ -15,11 +16,14 @@ from tests.browser.selenium_helpers import SeleniumTestCase | |||
15 | from orm.models import Project, Release, BitbakeVersion, Build, LogMessage | 16 | from orm.models import Project, Release, BitbakeVersion, Build, LogMessage |
16 | from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable | 17 | from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable |
17 | 18 | ||
19 | from selenium.webdriver.common.by import By | ||
20 | |||
18 | class TestBuildDashboardPage(SeleniumTestCase): | 21 | class TestBuildDashboardPage(SeleniumTestCase): |
19 | """ Tests for the build dashboard /build/X """ | 22 | """ Tests for the build dashboard /build/X """ |
20 | 23 | ||
21 | def setUp(self): | 24 | def setUp(self): |
22 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', | 25 | builldir = os.environ.get('BUILDDIR', './') |
26 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
23 | branch='master', dirpath="") | 27 | branch='master', dirpath="") |
24 | release = Release.objects.create(name='release1', | 28 | release = Release.objects.create(name='release1', |
25 | bitbake_version=bbv) | 29 | bitbake_version=bbv) |
@@ -158,6 +162,7 @@ class TestBuildDashboardPage(SeleniumTestCase): | |||
158 | """ | 162 | """ |
159 | url = reverse('builddashboard', args=(build.id,)) | 163 | url = reverse('builddashboard', args=(build.id,)) |
160 | self.get(url) | 164 | self.get(url) |
165 | self.wait_until_visible('#global-nav') | ||
161 | 166 | ||
162 | def _get_build_dashboard_errors(self, build): | 167 | def _get_build_dashboard_errors(self, build): |
163 | """ | 168 | """ |
@@ -183,7 +188,7 @@ class TestBuildDashboardPage(SeleniumTestCase): | |||
183 | 188 | ||
184 | found = False | 189 | found = False |
185 | for element in message_elements: | 190 | for element in message_elements: |
186 | log_message_text = element.find_element_by_tag_name('pre').text.strip() | 191 | log_message_text = element.find_element(By.TAG_NAME, 'pre').text.strip() |
187 | text_matches = (log_message_text == expected_text) | 192 | text_matches = (log_message_text == expected_text) |
188 | 193 | ||
189 | log_message_pk = element.get_attribute('data-log-message-id') | 194 | log_message_pk = element.get_attribute('data-log-message-id') |
@@ -213,7 +218,7 @@ class TestBuildDashboardPage(SeleniumTestCase): | |||
213 | the WebElement modal match the list of text values in expected | 218 | the WebElement modal match the list of text values in expected |
214 | """ | 219 | """ |
215 | # labels containing the radio buttons we're testing for | 220 | # labels containing the radio buttons we're testing for |
216 | labels = modal.find_elements_by_css_selector(".radio") | 221 | labels = modal.find_elements(By.CSS_SELECTOR,".radio") |
217 | 222 | ||
218 | labels_text = [lab.text for lab in labels] | 223 | labels_text = [lab.text for lab in labels] |
219 | self.assertEqual(len(labels_text), len(expected)) | 224 | self.assertEqual(len(labels_text), len(expected)) |
@@ -248,7 +253,7 @@ class TestBuildDashboardPage(SeleniumTestCase): | |||
248 | selector = '[data-role="edit-custom-image-trigger"]' | 253 | selector = '[data-role="edit-custom-image-trigger"]' |
249 | self.click(selector) | 254 | self.click(selector) |
250 | 255 | ||
251 | modal = self.driver.find_element_by_id('edit-custom-image-modal') | 256 | modal = self.driver.find_element(By.ID, 'edit-custom-image-modal') |
252 | self.wait_until_visible("#edit-custom-image-modal") | 257 | self.wait_until_visible("#edit-custom-image-modal") |
253 | 258 | ||
254 | # recipes we expect to see in the edit custom image modal | 259 | # recipes we expect to see in the edit custom image modal |
@@ -270,7 +275,7 @@ class TestBuildDashboardPage(SeleniumTestCase): | |||
270 | selector = '[data-role="new-custom-image-trigger"]' | 275 | selector = '[data-role="new-custom-image-trigger"]' |
271 | self.click(selector) | 276 | self.click(selector) |
272 | 277 | ||
273 | modal = self.driver.find_element_by_id('new-custom-image-modal') | 278 | modal = self.driver.find_element(By.ID,'new-custom-image-modal') |
274 | self.wait_until_visible("#new-custom-image-modal") | 279 | self.wait_until_visible("#new-custom-image-modal") |
275 | 280 | ||
276 | # recipes we expect to see in the new custom image modal | 281 | # recipes we expect to see in the new custom image modal |
diff --git a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py b/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py index c6226d60eb..675825bd40 100644 --- a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py +++ b/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py | |||
@@ -7,6 +7,7 @@ | |||
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | 9 | ||
10 | import os | ||
10 | from django.urls import reverse | 11 | from django.urls import reverse |
11 | from django.utils import timezone | 12 | from django.utils import timezone |
12 | 13 | ||
@@ -20,7 +21,8 @@ class TestBuildDashboardPageArtifacts(SeleniumTestCase): | |||
20 | """ Tests for artifacts on the build dashboard /build/X """ | 21 | """ Tests for artifacts on the build dashboard /build/X """ |
21 | 22 | ||
22 | def setUp(self): | 23 | def setUp(self): |
23 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', | 24 | builldir = os.environ.get('BUILDDIR', './') |
25 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
24 | branch='master', dirpath="") | 26 | branch='master', dirpath="") |
25 | release = Release.objects.create(name='release1', | 27 | release = Release.objects.create(name='release1', |
26 | bitbake_version=bbv) | 28 | bitbake_version=bbv) |
@@ -197,12 +199,12 @@ class TestBuildDashboardPageArtifacts(SeleniumTestCase): | |||
197 | # check package count and size, link on target name | 199 | # check package count and size, link on target name |
198 | selector = '[data-value="target-package-count"]' | 200 | selector = '[data-value="target-package-count"]' |
199 | element = self.find(selector) | 201 | element = self.find(selector) |
200 | self.assertEquals(element.text, '1', | 202 | self.assertEqual(element.text, '1', |
201 | 'package count should be shown for image builds') | 203 | 'package count should be shown for image builds') |
202 | 204 | ||
203 | selector = '[data-value="target-package-size"]' | 205 | selector = '[data-value="target-package-size"]' |
204 | element = self.find(selector) | 206 | element = self.find(selector) |
205 | self.assertEquals(element.text, '1.0 KB', | 207 | self.assertEqual(element.text, '1.0 KB', |
206 | 'package size should be shown for image builds') | 208 | 'package size should be shown for image builds') |
207 | 209 | ||
208 | selector = '[data-link="target-packages"]' | 210 | selector = '[data-link="target-packages"]' |
diff --git a/bitbake/lib/toaster/tests/browser/test_delete_project.py b/bitbake/lib/toaster/tests/browser/test_delete_project.py new file mode 100644 index 0000000000..1941777ccc --- /dev/null +++ b/bitbake/lib/toaster/tests/browser/test_delete_project.py | |||
@@ -0,0 +1,103 @@ | |||
1 | #!/usr/bin/env python3 | ||
2 | # -*- coding: utf-8 -*- | ||
3 | # BitBake Toaster UI tests implementation | ||
4 | # | ||
5 | # Copyright (C) 2023 Savoir-faire Linux Inc | ||
6 | # | ||
7 | # SPDX-License-Identifier: GPL-2.0-only | ||
8 | |||
9 | import pytest | ||
10 | from django.urls import reverse | ||
11 | from selenium.webdriver.support.ui import Select | ||
12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
13 | from orm.models import BitbakeVersion, Project, Release | ||
14 | from selenium.webdriver.common.by import By | ||
15 | |||
16 | class TestDeleteProject(SeleniumTestCase): | ||
17 | |||
18 | def setUp(self): | ||
19 | bitbake, _ = BitbakeVersion.objects.get_or_create( | ||
20 | name="master", | ||
21 | giturl="git://master", | ||
22 | branch="master", | ||
23 | dirpath="master") | ||
24 | |||
25 | self.release, _ = Release.objects.get_or_create( | ||
26 | name="master", | ||
27 | description="Yocto Project master", | ||
28 | branch_name="master", | ||
29 | helptext="latest", | ||
30 | bitbake_version=bitbake) | ||
31 | |||
32 | Release.objects.get_or_create( | ||
33 | name="foo", | ||
34 | description="Yocto Project foo", | ||
35 | branch_name="foo", | ||
36 | helptext="latest", | ||
37 | bitbake_version=bitbake) | ||
38 | |||
39 | @pytest.mark.django_db | ||
40 | def test_delete_project(self): | ||
41 | """ Test delete a project | ||
42 | - Check delete modal is visible | ||
43 | - Check delete modal has right text | ||
44 | - Confirm delete | ||
45 | - Check project is deleted | ||
46 | """ | ||
47 | project_name = "project_to_delete" | ||
48 | url = reverse('newproject') | ||
49 | self.get(url) | ||
50 | self.enter_text('#new-project-name', project_name) | ||
51 | select = Select(self.find('#projectversion')) | ||
52 | select.select_by_value(str(self.release.pk)) | ||
53 | self.click("#create-project-button") | ||
54 | # We should get redirected to the new project's page with the | ||
55 | # notification at the top | ||
56 | element = self.wait_until_visible('#project-created-notification') | ||
57 | self.assertTrue(project_name in element.text, | ||
58 | "New project name not in new project notification") | ||
59 | self.assertTrue(Project.objects.filter(name=project_name).count(), | ||
60 | "New project not found in database") | ||
61 | |||
62 | # Delete project | ||
63 | delete_project_link = self.driver.find_element( | ||
64 | By.XPATH, '//a[@href="#delete-project-modal"]') | ||
65 | delete_project_link.click() | ||
66 | |||
67 | # Check delete modal is visible | ||
68 | self.wait_until_visible('#delete-project-modal') | ||
69 | |||
70 | # Check delete modal has right text | ||
71 | modal_header_text = self.find('#delete-project-modal .modal-header').text | ||
72 | self.assertTrue( | ||
73 | "Are you sure you want to delete this project?" in modal_header_text, | ||
74 | "Delete project modal header text is wrong") | ||
75 | |||
76 | modal_body_text = self.find('#delete-project-modal .modal-body').text | ||
77 | self.assertTrue( | ||
78 | "Cancel its builds currently in progress" in modal_body_text, | ||
79 | "Modal body doesn't contain: Cancel its builds currently in progress") | ||
80 | self.assertTrue( | ||
81 | "Remove its configuration information" in modal_body_text, | ||
82 | "Modal body doesn't contain: Remove its configuration information") | ||
83 | self.assertTrue( | ||
84 | "Remove its imported layers" in modal_body_text, | ||
85 | "Modal body doesn't contain: Remove its imported layers") | ||
86 | self.assertTrue( | ||
87 | "Remove its custom images" in modal_body_text, | ||
88 | "Modal body doesn't contain: Remove its custom images") | ||
89 | self.assertTrue( | ||
90 | "Remove all its build information" in modal_body_text, | ||
91 | "Modal body doesn't contain: Remove all its build information") | ||
92 | |||
93 | # Confirm delete | ||
94 | delete_btn = self.find('#delete-project-confirmed') | ||
95 | delete_btn.click() | ||
96 | |||
97 | # Check project is deleted | ||
98 | self.wait_until_visible('#change-notification') | ||
99 | delete_notification = self.find('#change-notification-msg') | ||
100 | self.assertTrue("You have deleted 1 project:" in delete_notification.text) | ||
101 | self.assertTrue(project_name in delete_notification.text) | ||
102 | self.assertFalse(Project.objects.filter(name=project_name).exists(), | ||
103 | "Project not deleted from database") | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_landing_page.py b/bitbake/lib/toaster/tests/browser/test_landing_page.py index 8bb64b9f3e..210359d561 100644 --- a/bitbake/lib/toaster/tests/browser/test_landing_page.py +++ b/bitbake/lib/toaster/tests/browser/test_landing_page.py | |||
@@ -10,8 +10,10 @@ | |||
10 | from django.urls import reverse | 10 | from django.urls import reverse |
11 | from django.utils import timezone | 11 | from django.utils import timezone |
12 | from tests.browser.selenium_helpers import SeleniumTestCase | 12 | from tests.browser.selenium_helpers import SeleniumTestCase |
13 | from selenium.webdriver.common.by import By | ||
14 | |||
15 | from orm.models import Layer, Layer_Version, Project, Build | ||
13 | 16 | ||
14 | from orm.models import Project, Build | ||
15 | 17 | ||
16 | class TestLandingPage(SeleniumTestCase): | 18 | class TestLandingPage(SeleniumTestCase): |
17 | """ Tests for redirects on the landing page """ | 19 | """ Tests for redirects on the landing page """ |
@@ -29,12 +31,147 @@ class TestLandingPage(SeleniumTestCase): | |||
29 | self.project.is_default = True | 31 | self.project.is_default = True |
30 | self.project.save() | 32 | self.project.save() |
31 | 33 | ||
34 | def test_icon_info_visible_and_clickable(self): | ||
35 | """ Test that the information icon is visible and clickable """ | ||
36 | self.get(reverse('landing')) | ||
37 | self.wait_until_visible('#toaster-version-info-sign') | ||
38 | info_sign = self.find('#toaster-version-info-sign') | ||
39 | |||
40 | # check that the info sign is visible | ||
41 | self.assertTrue(info_sign.is_displayed()) | ||
42 | |||
43 | # check that the info sign is clickable | ||
44 | # and info modal is appearing when clicking on the info sign | ||
45 | info_sign.click() # click on the info sign make attribute 'aria-describedby' visible | ||
46 | info_model_id = info_sign.get_attribute('aria-describedby') | ||
47 | self.wait_until_visible(f'#{info_model_id}') | ||
48 | info_modal = self.find(f'#{info_model_id}') | ||
49 | self.assertTrue(info_modal.is_displayed()) | ||
50 | self.assertTrue("Toaster version information" in info_modal.text) | ||
51 | |||
52 | def test_documentation_link_displayed(self): | ||
53 | """ Test that the documentation link is displayed """ | ||
54 | self.get(reverse('landing')) | ||
55 | self.wait_until_visible('#navbar-docs') | ||
56 | documentation_link = self.find('#navbar-docs > a') | ||
57 | |||
58 | # check that the documentation link is visible | ||
59 | self.assertTrue(documentation_link.is_displayed()) | ||
60 | |||
61 | # check browser open new tab toaster manual when clicking on the documentation link | ||
62 | self.assertEqual(documentation_link.get_attribute('target'), '_blank') | ||
63 | self.assertEqual( | ||
64 | documentation_link.get_attribute('href'), | ||
65 | 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual') | ||
66 | self.assertTrue("Documentation" in documentation_link.text) | ||
67 | |||
68 | def test_openembedded_jumbotron_link_visible_and_clickable(self): | ||
69 | """ Test OpenEmbedded link jumbotron is visible and clickable: """ | ||
70 | self.get(reverse('landing')) | ||
71 | self.wait_until_visible('.jumbotron') | ||
72 | jumbotron = self.find('.jumbotron') | ||
73 | |||
74 | # check OpenEmbedded | ||
75 | openembedded = jumbotron.find_element(By.LINK_TEXT, 'OpenEmbedded') | ||
76 | self.assertTrue(openembedded.is_displayed()) | ||
77 | openembedded.click() | ||
78 | self.assertTrue("openembedded.org" in self.driver.current_url) | ||
79 | |||
80 | def test_bitbake_jumbotron_link_visible_and_clickable(self): | ||
81 | """ Test BitBake link jumbotron is visible and clickable: """ | ||
82 | self.get(reverse('landing')) | ||
83 | self.wait_until_visible('.jumbotron') | ||
84 | jumbotron = self.find('.jumbotron') | ||
85 | |||
86 | # check BitBake | ||
87 | bitbake = jumbotron.find_element(By.LINK_TEXT, 'BitBake') | ||
88 | self.assertTrue(bitbake.is_displayed()) | ||
89 | bitbake.click() | ||
90 | self.assertTrue( | ||
91 | "docs.yoctoproject.org/bitbake.html" in self.driver.current_url) | ||
92 | |||
93 | def test_yoctoproject_jumbotron_link_visible_and_clickable(self): | ||
94 | """ Test Yocto Project link jumbotron is visible and clickable: """ | ||
95 | self.get(reverse('landing')) | ||
96 | self.wait_until_visible('.jumbotron') | ||
97 | jumbotron = self.find('.jumbotron') | ||
98 | |||
99 | # check Yocto Project | ||
100 | yoctoproject = jumbotron.find_element(By.LINK_TEXT, 'Yocto Project') | ||
101 | self.assertTrue(yoctoproject.is_displayed()) | ||
102 | yoctoproject.click() | ||
103 | self.assertTrue("yoctoproject.org" in self.driver.current_url) | ||
104 | |||
105 | def test_link_setup_using_toaster_visible_and_clickable(self): | ||
106 | """ Test big magenta button setting up and using toaster link in jumbotron | ||
107 | if visible and clickable | ||
108 | """ | ||
109 | self.get(reverse('landing')) | ||
110 | self.wait_until_visible('.jumbotron') | ||
111 | jumbotron = self.find('.jumbotron') | ||
112 | |||
113 | # check Big magenta button | ||
114 | big_magenta_button = jumbotron.find_element(By.LINK_TEXT, | ||
115 | 'Toaster is ready to capture your command line builds' | ||
116 | ) | ||
117 | self.assertTrue(big_magenta_button.is_displayed()) | ||
118 | big_magenta_button.click() | ||
119 | self.assertTrue( | ||
120 | "docs.yoctoproject.org/toaster-manual/setup-and-use.html#setting-up-and-using-toaster" in self.driver.current_url) | ||
121 | |||
122 | def test_link_create_new_project_in_jumbotron_visible_and_clickable(self): | ||
123 | """ Test big blue button create new project jumbotron if visible and clickable """ | ||
124 | # Create a layer and a layer version to make visible the big blue button | ||
125 | layer = Layer.objects.create(name='bar') | ||
126 | Layer_Version.objects.create(layer=layer) | ||
127 | |||
128 | self.get(reverse('landing')) | ||
129 | self.wait_until_visible('.jumbotron') | ||
130 | jumbotron = self.find('.jumbotron') | ||
131 | |||
132 | # check Big Blue button | ||
133 | big_blue_button = jumbotron.find_element(By.LINK_TEXT, | ||
134 | 'Create your first Toaster project to run manage builds' | ||
135 | ) | ||
136 | self.assertTrue(big_blue_button.is_displayed()) | ||
137 | big_blue_button.click() | ||
138 | self.assertTrue("toastergui/newproject/" in self.driver.current_url) | ||
139 | |||
140 | def test_toaster_manual_link_visible_and_clickable(self): | ||
141 | """ Test Read the Toaster manual link jumbotron is visible and clickable: """ | ||
142 | self.get(reverse('landing')) | ||
143 | self.wait_until_visible('.jumbotron') | ||
144 | jumbotron = self.find('.jumbotron') | ||
145 | |||
146 | # check Read the Toaster manual | ||
147 | toaster_manual = jumbotron.find_element( | ||
148 | By.LINK_TEXT, 'Read the Toaster manual') | ||
149 | self.assertTrue(toaster_manual.is_displayed()) | ||
150 | toaster_manual.click() | ||
151 | self.assertTrue( | ||
152 | "https://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual" in self.driver.current_url) | ||
153 | |||
154 | def test_contrib_to_toaster_link_visible_and_clickable(self): | ||
155 | """ Test Contribute to Toaster link jumbotron is visible and clickable: """ | ||
156 | self.get(reverse('landing')) | ||
157 | self.wait_until_visible('.jumbotron') | ||
158 | jumbotron = self.find('.jumbotron') | ||
159 | |||
160 | # check Contribute to Toaster | ||
161 | contribute_to_toaster = jumbotron.find_element( | ||
162 | By.LINK_TEXT, 'Contribute to Toaster') | ||
163 | self.assertTrue(contribute_to_toaster.is_displayed()) | ||
164 | contribute_to_toaster.click() | ||
165 | self.assertTrue( | ||
166 | "wiki.yoctoproject.org/wiki/contribute_to_toaster" in str(self.driver.current_url).lower()) | ||
167 | |||
32 | def test_only_default_project(self): | 168 | def test_only_default_project(self): |
33 | """ | 169 | """ |
34 | No projects except default | 170 | No projects except default |
35 | => should see the landing page | 171 | => should see the landing page |
36 | """ | 172 | """ |
37 | self.get(reverse('landing')) | 173 | self.get(reverse('landing')) |
174 | self.wait_until_visible('.jumbotron') | ||
38 | self.assertTrue(self.LANDING_PAGE_TITLE in self.get_page_source()) | 175 | self.assertTrue(self.LANDING_PAGE_TITLE in self.get_page_source()) |
39 | 176 | ||
40 | def test_default_project_has_build(self): | 177 | def test_default_project_has_build(self): |
@@ -67,6 +204,7 @@ class TestLandingPage(SeleniumTestCase): | |||
67 | user_project.save() | 204 | user_project.save() |
68 | 205 | ||
69 | self.get(reverse('landing')) | 206 | self.get(reverse('landing')) |
207 | self.wait_until_visible('#projectstable') | ||
70 | 208 | ||
71 | elements = self.find_all('#projectstable') | 209 | elements = self.find_all('#projectstable') |
72 | self.assertEqual(len(elements), 1, 'should redirect to projects') | 210 | self.assertEqual(len(elements), 1, 'should redirect to projects') |
@@ -87,10 +225,9 @@ class TestLandingPage(SeleniumTestCase): | |||
87 | 225 | ||
88 | self.get(reverse('landing')) | 226 | self.get(reverse('landing')) |
89 | 227 | ||
228 | self.wait_until_visible("#latest-builds") | ||
90 | elements = self.find_all('#allbuildstable') | 229 | elements = self.find_all('#allbuildstable') |
91 | self.assertEqual(len(elements), 1, 'should redirect to builds') | 230 | self.assertEqual(len(elements), 1, 'should redirect to builds') |
92 | content = self.get_page_source() | 231 | content = self.get_page_source() |
93 | self.assertTrue(self.PROJECT_NAME in content, | 232 | self.assertTrue(self.PROJECT_NAME in content, |
94 | 'should show builds for project %s' % self.PROJECT_NAME) | 233 | 'should show builds for project %s' % self.PROJECT_NAME) |
95 | self.assertFalse(self.CLI_BUILDS_PROJECT_NAME in content, | ||
96 | 'should not show builds for cli project') | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py b/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py index 71bdd2aafd..6abfdef699 100644 --- a/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py +++ b/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py | |||
@@ -8,6 +8,7 @@ | |||
8 | # | 8 | # |
9 | 9 | ||
10 | from django.urls import reverse | 10 | from django.urls import reverse |
11 | from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException | ||
11 | from tests.browser.selenium_helpers import SeleniumTestCase | 12 | from tests.browser.selenium_helpers import SeleniumTestCase |
12 | 13 | ||
13 | from orm.models import Layer, Layer_Version, Project, LayerSource, Release | 14 | from orm.models import Layer, Layer_Version, Project, LayerSource, Release |
@@ -63,11 +64,12 @@ class TestLayerDetailsPage(SeleniumTestCase): | |||
63 | args=(self.project.pk, | 64 | args=(self.project.pk, |
64 | self.imported_layer_version.pk)) | 65 | self.imported_layer_version.pk)) |
65 | 66 | ||
66 | def test_edit_layerdetails(self): | 67 | def test_edit_layerdetails_page(self): |
67 | """ Edit all the editable fields for the layer refresh the page and | 68 | """ Edit all the editable fields for the layer refresh the page and |
68 | check that the new values exist""" | 69 | check that the new values exist""" |
69 | 70 | ||
70 | self.get(self.url) | 71 | self.get(self.url) |
72 | self.wait_until_visible("#add-remove-layer-btn") | ||
71 | 73 | ||
72 | self.click("#add-remove-layer-btn") | 74 | self.click("#add-remove-layer-btn") |
73 | self.click("#edit-layer-source") | 75 | self.click("#edit-layer-source") |
@@ -97,13 +99,21 @@ class TestLayerDetailsPage(SeleniumTestCase): | |||
97 | "Expecting any of \"%s\"but got \"%s\"" % | 99 | "Expecting any of \"%s\"but got \"%s\"" % |
98 | (self.initial_values, value)) | 100 | (self.initial_values, value)) |
99 | 101 | ||
102 | # Make sure the input visible beofre sending keys | ||
103 | self.wait_until_clickable("#layer-git input[type=text]") | ||
100 | inputs.send_keys("-edited") | 104 | inputs.send_keys("-edited") |
101 | 105 | ||
102 | # Save the new values | 106 | # Save the new values |
103 | for save_btn in self.find_all(".change-btn"): | 107 | for save_btn in self.find_all(".change-btn"): |
104 | save_btn.click() | 108 | save_btn.click() |
105 | 109 | ||
106 | self.click("#save-changes-for-switch") | 110 | self.wait_until_visible("#save-changes-for-switch") |
111 | # Ensure scrolled into view | ||
112 | self.driver.execute_script('window.scrollTo({behavior: "instant", top: 0, left: 0})') | ||
113 | btn_save_chg_for_switch = self.wait_until_clickable( | ||
114 | "#save-changes-for-switch") | ||
115 | btn_save_chg_for_switch.click() | ||
116 | |||
107 | self.wait_until_visible("#edit-layer-source") | 117 | self.wait_until_visible("#edit-layer-source") |
108 | 118 | ||
109 | # Refresh the page to see if the new values are returned | 119 | # Refresh the page to see if the new values are returned |
@@ -132,7 +142,11 @@ class TestLayerDetailsPage(SeleniumTestCase): | |||
132 | new_dir = "/home/test/my-meta-dir" | 142 | new_dir = "/home/test/my-meta-dir" |
133 | dir_input.send_keys(new_dir) | 143 | dir_input.send_keys(new_dir) |
134 | 144 | ||
135 | self.click("#save-changes-for-switch") | 145 | self.wait_until_visible("#save-changes-for-switch") |
146 | btn_save_chg_for_switch = self.wait_until_clickable( | ||
147 | "#save-changes-for-switch") | ||
148 | btn_save_chg_for_switch.click() | ||
149 | |||
136 | self.wait_until_visible("#edit-layer-source") | 150 | self.wait_until_visible("#edit-layer-source") |
137 | 151 | ||
138 | # Refresh the page to see if the new values are returned | 152 | # Refresh the page to see if the new values are returned |
@@ -142,6 +156,7 @@ class TestLayerDetailsPage(SeleniumTestCase): | |||
142 | "Expected %s in the dir value for layer directory" % | 156 | "Expected %s in the dir value for layer directory" % |
143 | new_dir) | 157 | new_dir) |
144 | 158 | ||
159 | |||
145 | def test_delete_layer(self): | 160 | def test_delete_layer(self): |
146 | """ Delete the layer """ | 161 | """ Delete the layer """ |
147 | 162 | ||
@@ -178,6 +193,7 @@ class TestLayerDetailsPage(SeleniumTestCase): | |||
178 | self.get(self.url) | 193 | self.get(self.url) |
179 | 194 | ||
180 | # Add the layer | 195 | # Add the layer |
196 | self.wait_until_clickable("#add-remove-layer-btn") | ||
181 | self.click("#add-remove-layer-btn") | 197 | self.click("#add-remove-layer-btn") |
182 | 198 | ||
183 | notification = self.wait_until_visible("#change-notification-msg") | 199 | notification = self.wait_until_visible("#change-notification-msg") |
@@ -185,12 +201,17 @@ class TestLayerDetailsPage(SeleniumTestCase): | |||
185 | expected_text = "You have added 1 layer to your project: %s" % \ | 201 | expected_text = "You have added 1 layer to your project: %s" % \ |
186 | self.imported_layer_version.layer.name | 202 | self.imported_layer_version.layer.name |
187 | 203 | ||
188 | self.assertTrue(expected_text in notification.text, | 204 | self.assertIn(expected_text, notification.text, |
189 | "Expected notification text %s not found was " | 205 | "Expected notification text %s not found was " |
190 | " \"%s\" instead" % | 206 | " \"%s\" instead" % |
191 | (expected_text, notification.text)) | 207 | (expected_text, notification.text)) |
192 | 208 | ||
209 | hide_button = self.find('#hide-alert') | ||
210 | hide_button.click() | ||
211 | self.wait_until_not_visible('#change-notification') | ||
212 | |||
193 | # Remove the layer | 213 | # Remove the layer |
214 | self.wait_until_clickable("#add-remove-layer-btn") | ||
194 | self.click("#add-remove-layer-btn") | 215 | self.click("#add-remove-layer-btn") |
195 | 216 | ||
196 | notification = self.wait_until_visible("#change-notification-msg") | 217 | notification = self.wait_until_visible("#change-notification-msg") |
@@ -198,7 +219,7 @@ class TestLayerDetailsPage(SeleniumTestCase): | |||
198 | expected_text = "You have removed 1 layer from your project: %s" % \ | 219 | expected_text = "You have removed 1 layer from your project: %s" % \ |
199 | self.imported_layer_version.layer.name | 220 | self.imported_layer_version.layer.name |
200 | 221 | ||
201 | self.assertTrue(expected_text in notification.text, | 222 | self.assertIn(expected_text, notification.text, |
202 | "Expected notification text %s not found was " | 223 | "Expected notification text %s not found was " |
203 | " \"%s\" instead" % | 224 | " \"%s\" instead" % |
204 | (expected_text, notification.text)) | 225 | (expected_text, notification.text)) |
diff --git a/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py b/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py index 7844aaa395..d7a4c34532 100644 --- a/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py +++ b/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py | |||
@@ -6,7 +6,6 @@ | |||
6 | # | 6 | # |
7 | # Copyright (C) 2013-2016 Intel Corporation | 7 | # Copyright (C) 2013-2016 Intel Corporation |
8 | # | 8 | # |
9 | |||
10 | from django.urls import reverse | 9 | from django.urls import reverse |
11 | from django.utils import timezone | 10 | from django.utils import timezone |
12 | from tests.browser.selenium_helpers import SeleniumTestCase | 11 | from tests.browser.selenium_helpers import SeleniumTestCase |
@@ -14,6 +13,8 @@ from tests.browser.selenium_helpers_base import Wait | |||
14 | from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version | 13 | from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version |
15 | from bldcontrol.models import BuildRequest | 14 | from bldcontrol.models import BuildRequest |
16 | 15 | ||
16 | from selenium.webdriver.common.by import By | ||
17 | |||
17 | class TestMostRecentBuildsStates(SeleniumTestCase): | 18 | class TestMostRecentBuildsStates(SeleniumTestCase): |
18 | """ Test states update correctly in most recent builds area """ | 19 | """ Test states update correctly in most recent builds area """ |
19 | 20 | ||
@@ -45,13 +46,14 @@ class TestMostRecentBuildsStates(SeleniumTestCase): | |||
45 | # build queued; check shown as queued | 46 | # build queued; check shown as queued |
46 | selector = base_selector + '[data-build-state="Queued"]' | 47 | selector = base_selector + '[data-build-state="Queued"]' |
47 | element = self.wait_until_visible(selector) | 48 | element = self.wait_until_visible(selector) |
48 | self.assertRegexpMatches(element.get_attribute('innerHTML'), | 49 | self.assertRegex(element.get_attribute('innerHTML'), |
49 | 'Build queued', 'build should show queued status') | 50 | 'Build queued', 'build should show queued status') |
50 | 51 | ||
51 | # waiting for recipes to be parsed | 52 | # waiting for recipes to be parsed |
52 | build.outcome = Build.IN_PROGRESS | 53 | build.outcome = Build.IN_PROGRESS |
53 | build.recipes_to_parse = recipes_to_parse | 54 | build.recipes_to_parse = recipes_to_parse |
54 | build.recipes_parsed = 0 | 55 | build.recipes_parsed = 0 |
56 | build.save() | ||
55 | 57 | ||
56 | build_request.state = BuildRequest.REQ_INPROGRESS | 58 | build_request.state = BuildRequest.REQ_INPROGRESS |
57 | build_request.save() | 59 | build_request.save() |
@@ -62,7 +64,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): | |||
62 | element = self.wait_until_visible(selector) | 64 | element = self.wait_until_visible(selector) |
63 | 65 | ||
64 | bar_selector = '#recipes-parsed-percentage-bar-%s' % build.id | 66 | bar_selector = '#recipes-parsed-percentage-bar-%s' % build.id |
65 | bar_element = element.find_element_by_css_selector(bar_selector) | 67 | bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) |
66 | self.assertEqual(bar_element.value_of_css_property('width'), '0px', | 68 | self.assertEqual(bar_element.value_of_css_property('width'), '0px', |
67 | 'recipe parse progress should be at 0') | 69 | 'recipe parse progress should be at 0') |
68 | 70 | ||
@@ -73,7 +75,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): | |||
73 | self.get(url) | 75 | self.get(url) |
74 | 76 | ||
75 | element = self.wait_until_visible(selector) | 77 | element = self.wait_until_visible(selector) |
76 | bar_element = element.find_element_by_css_selector(bar_selector) | 78 | bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) |
77 | recipe_bar_updated = lambda driver: \ | 79 | recipe_bar_updated = lambda driver: \ |
78 | bar_element.get_attribute('style') == 'width: 50%;' | 80 | bar_element.get_attribute('style') == 'width: 50%;' |
79 | msg = 'recipe parse progress bar should update to 50%' | 81 | msg = 'recipe parse progress bar should update to 50%' |
@@ -94,11 +96,11 @@ class TestMostRecentBuildsStates(SeleniumTestCase): | |||
94 | 96 | ||
95 | selector = base_selector + '[data-build-state="Starting"]' | 97 | selector = base_selector + '[data-build-state="Starting"]' |
96 | element = self.wait_until_visible(selector) | 98 | element = self.wait_until_visible(selector) |
97 | self.assertRegexpMatches(element.get_attribute('innerHTML'), | 99 | self.assertRegex(element.get_attribute('innerHTML'), |
98 | 'Tasks starting', 'build should show "tasks starting" status') | 100 | 'Tasks starting', 'build should show "tasks starting" status') |
99 | 101 | ||
100 | # first task finished; check tasks progress bar | 102 | # first task finished; check tasks progress bar |
101 | task1.order = 1 | 103 | task1.outcome = Task.OUTCOME_SUCCESS |
102 | task1.save() | 104 | task1.save() |
103 | 105 | ||
104 | self.get(url) | 106 | self.get(url) |
@@ -107,7 +109,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): | |||
107 | element = self.wait_until_visible(selector) | 109 | element = self.wait_until_visible(selector) |
108 | 110 | ||
109 | bar_selector = '#build-pc-done-bar-%s' % build.id | 111 | bar_selector = '#build-pc-done-bar-%s' % build.id |
110 | bar_element = element.find_element_by_css_selector(bar_selector) | 112 | bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) |
111 | 113 | ||
112 | task_bar_updated = lambda driver: \ | 114 | task_bar_updated = lambda driver: \ |
113 | bar_element.get_attribute('style') == 'width: 50%;' | 115 | bar_element.get_attribute('style') == 'width: 50%;' |
@@ -115,13 +117,13 @@ class TestMostRecentBuildsStates(SeleniumTestCase): | |||
115 | element = Wait(self.driver).until(task_bar_updated, msg) | 117 | element = Wait(self.driver).until(task_bar_updated, msg) |
116 | 118 | ||
117 | # last task finished; check tasks progress bar updates | 119 | # last task finished; check tasks progress bar updates |
118 | task2.order = 2 | 120 | task2.outcome = Task.OUTCOME_SUCCESS |
119 | task2.save() | 121 | task2.save() |
120 | 122 | ||
121 | self.get(url) | 123 | self.get(url) |
122 | 124 | ||
123 | element = self.wait_until_visible(selector) | 125 | element = self.wait_until_visible(selector) |
124 | bar_element = element.find_element_by_css_selector(bar_selector) | 126 | bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) |
125 | task_bar_updated = lambda driver: \ | 127 | task_bar_updated = lambda driver: \ |
126 | bar_element.get_attribute('style') == 'width: 100%;' | 128 | bar_element.get_attribute('style') == 'width: 100%;' |
127 | msg = 'tasks progress bar should update to 100%' | 129 | msg = 'tasks progress bar should update to 100%' |
@@ -183,7 +185,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): | |||
183 | selector = '[data-latest-build-result="%s"] ' \ | 185 | selector = '[data-latest-build-result="%s"] ' \ |
184 | '[data-build-state="Cancelling"]' % build.id | 186 | '[data-build-state="Cancelling"]' % build.id |
185 | element = self.wait_until_visible(selector) | 187 | element = self.wait_until_visible(selector) |
186 | self.assertRegexpMatches(element.get_attribute('innerHTML'), | 188 | self.assertRegex(element.get_attribute('innerHTML'), |
187 | 'Cancelling the build', 'build should show "cancelling" status') | 189 | 'Cancelling the build', 'build should show "cancelling" status') |
188 | 190 | ||
189 | # check cancelled state | 191 | # check cancelled state |
@@ -195,5 +197,5 @@ class TestMostRecentBuildsStates(SeleniumTestCase): | |||
195 | selector = '[data-latest-build-result="%s"] ' \ | 197 | selector = '[data-latest-build-result="%s"] ' \ |
196 | '[data-build-state="Cancelled"]' % build.id | 198 | '[data-build-state="Cancelled"]' % build.id |
197 | element = self.wait_until_visible(selector) | 199 | element = self.wait_until_visible(selector) |
198 | self.assertRegexpMatches(element.get_attribute('innerHTML'), | 200 | self.assertRegex(element.get_attribute('innerHTML'), |
199 | 'Build cancelled', 'build should show "cancelled" status') | 201 | 'Build cancelled', 'build should show "cancelled" status') |
diff --git a/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py b/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py index 9906ae42a9..bf0304dbec 100644 --- a/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py +++ b/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py | |||
@@ -6,6 +6,7 @@ | |||
6 | # | 6 | # |
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | from bldcontrol.models import BuildEnvironment | ||
9 | 10 | ||
10 | from django.urls import reverse | 11 | from django.urls import reverse |
11 | from tests.browser.selenium_helpers import SeleniumTestCase | 12 | from tests.browser.selenium_helpers import SeleniumTestCase |
@@ -18,6 +19,9 @@ class TestNewCustomImagePage(SeleniumTestCase): | |||
18 | CUSTOM_IMAGE_NAME = 'roopa-doopa' | 19 | CUSTOM_IMAGE_NAME = 'roopa-doopa' |
19 | 20 | ||
20 | def setUp(self): | 21 | def setUp(self): |
22 | BuildEnvironment.objects.get_or_create( | ||
23 | betype=BuildEnvironment.TYPE_LOCAL, | ||
24 | ) | ||
21 | release = Release.objects.create( | 25 | release = Release.objects.create( |
22 | name='baz', | 26 | name='baz', |
23 | bitbake_version=BitbakeVersion.objects.create(name='v1') | 27 | bitbake_version=BitbakeVersion.objects.create(name='v1') |
@@ -41,11 +45,16 @@ class TestNewCustomImagePage(SeleniumTestCase): | |||
41 | ) | 45 | ) |
42 | 46 | ||
43 | # add a fake image recipe to the layer that can be customised | 47 | # add a fake image recipe to the layer that can be customised |
48 | builldir = os.environ.get('BUILDDIR', './') | ||
44 | self.recipe = Recipe.objects.create( | 49 | self.recipe = Recipe.objects.create( |
45 | name='core-image-minimal', | 50 | name='core-image-minimal', |
46 | layer_version=layer_version, | 51 | layer_version=layer_version, |
52 | file_path=f'{builldir}/core-image-minimal.bb', | ||
47 | is_image=True | 53 | is_image=True |
48 | ) | 54 | ) |
55 | # create a tmp file for the recipe | ||
56 | with open(self.recipe.file_path, 'w') as f: | ||
57 | f.write('foo') | ||
49 | 58 | ||
50 | # another project with a custom image already in it | 59 | # another project with a custom image already in it |
51 | project2 = Project.objects.create(name='whoop', release=release) | 60 | project2 = Project.objects.create(name='whoop', release=release) |
@@ -81,6 +90,7 @@ class TestNewCustomImagePage(SeleniumTestCase): | |||
81 | """ | 90 | """ |
82 | url = reverse('newcustomimage', args=(self.project.id,)) | 91 | url = reverse('newcustomimage', args=(self.project.id,)) |
83 | self.get(url) | 92 | self.get(url) |
93 | self.wait_until_visible('#global-nav') | ||
84 | 94 | ||
85 | self.click('button[data-recipe="%s"]' % self.recipe.id) | 95 | self.click('button[data-recipe="%s"]' % self.recipe.id) |
86 | 96 | ||
@@ -128,7 +138,7 @@ class TestNewCustomImagePage(SeleniumTestCase): | |||
128 | """ | 138 | """ |
129 | self._create_custom_image(self.recipe.name) | 139 | self._create_custom_image(self.recipe.name) |
130 | element = self.wait_until_visible('#invalid-name-help') | 140 | element = self.wait_until_visible('#invalid-name-help') |
131 | self.assertRegexpMatches(element.text.strip(), | 141 | self.assertRegex(element.text.strip(), |
132 | 'image with this name already exists') | 142 | 'image with this name already exists') |
133 | 143 | ||
134 | def test_new_duplicates_project_image(self): | 144 | def test_new_duplicates_project_image(self): |
@@ -146,4 +156,4 @@ class TestNewCustomImagePage(SeleniumTestCase): | |||
146 | self._create_custom_image(custom_image_name) | 156 | self._create_custom_image(custom_image_name) |
147 | element = self.wait_until_visible('#invalid-name-help') | 157 | element = self.wait_until_visible('#invalid-name-help') |
148 | expected = 'An image with this name already exists in this project' | 158 | expected = 'An image with this name already exists in this project' |
149 | self.assertRegexpMatches(element.text.strip(), expected) | 159 | self.assertRegex(element.text.strip(), expected) |
diff --git a/bitbake/lib/toaster/tests/browser/test_new_project_page.py b/bitbake/lib/toaster/tests/browser/test_new_project_page.py index e20a1f686e..e50f236c32 100644 --- a/bitbake/lib/toaster/tests/browser/test_new_project_page.py +++ b/bitbake/lib/toaster/tests/browser/test_new_project_page.py | |||
@@ -6,11 +6,11 @@ | |||
6 | # | 6 | # |
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | |||
10 | from django.urls import reverse | 9 | from django.urls import reverse |
11 | from tests.browser.selenium_helpers import SeleniumTestCase | 10 | from tests.browser.selenium_helpers import SeleniumTestCase |
12 | from selenium.webdriver.support.ui import Select | 11 | from selenium.webdriver.support.ui import Select |
13 | from selenium.common.exceptions import InvalidElementStateException | 12 | from selenium.common.exceptions import InvalidElementStateException |
13 | from selenium.webdriver.common.by import By | ||
14 | 14 | ||
15 | from orm.models import Project, Release, BitbakeVersion | 15 | from orm.models import Project, Release, BitbakeVersion |
16 | 16 | ||
@@ -47,7 +47,7 @@ class TestNewProjectPage(SeleniumTestCase): | |||
47 | 47 | ||
48 | url = reverse('newproject') | 48 | url = reverse('newproject') |
49 | self.get(url) | 49 | self.get(url) |
50 | 50 | self.wait_until_visible('#new-project-name') | |
51 | self.enter_text('#new-project-name', project_name) | 51 | self.enter_text('#new-project-name', project_name) |
52 | 52 | ||
53 | select = Select(self.find('#projectversion')) | 53 | select = Select(self.find('#projectversion')) |
@@ -57,7 +57,8 @@ class TestNewProjectPage(SeleniumTestCase): | |||
57 | 57 | ||
58 | # We should get redirected to the new project's page with the | 58 | # We should get redirected to the new project's page with the |
59 | # notification at the top | 59 | # notification at the top |
60 | element = self.wait_until_visible('#project-created-notification') | 60 | element = self.wait_until_visible( |
61 | '#project-created-notification') | ||
61 | 62 | ||
62 | self.assertTrue(project_name in element.text, | 63 | self.assertTrue(project_name in element.text, |
63 | "New project name not in new project notification") | 64 | "New project name not in new project notification") |
@@ -78,15 +79,20 @@ class TestNewProjectPage(SeleniumTestCase): | |||
78 | 79 | ||
79 | url = reverse('newproject') | 80 | url = reverse('newproject') |
80 | self.get(url) | 81 | self.get(url) |
82 | self.wait_until_visible('#new-project-name') | ||
81 | 83 | ||
82 | self.enter_text('#new-project-name', project_name) | 84 | self.enter_text('#new-project-name', project_name) |
83 | 85 | ||
84 | select = Select(self.find('#projectversion')) | 86 | select = Select(self.find('#projectversion')) |
85 | select.select_by_value(str(self.release.pk)) | 87 | select.select_by_value(str(self.release.pk)) |
86 | 88 | ||
87 | element = self.wait_until_visible('#hint-error-project-name') | 89 | radio = self.driver.find_element(By.ID, 'type-new') |
90 | radio.click() | ||
91 | |||
92 | self.wait_until_visible('#hint-error-project-name') | ||
93 | element = self.find('#hint-error-project-name') | ||
88 | 94 | ||
89 | self.assertTrue(("Project names must be unique" in element.text), | 95 | self.assertIn("Project names must be unique", element.text, |
90 | "Did not find unique project name error message") | 96 | "Did not find unique project name error message") |
91 | 97 | ||
92 | # Try and click it anyway, if it submits we'll have a new project in | 98 | # Try and click it anyway, if it submits we'll have a new project in |
diff --git a/bitbake/lib/toaster/tests/browser/test_project_builds_page.py b/bitbake/lib/toaster/tests/browser/test_project_builds_page.py index 51717e72d4..0dba33b9c8 100644 --- a/bitbake/lib/toaster/tests/browser/test_project_builds_page.py +++ b/bitbake/lib/toaster/tests/browser/test_project_builds_page.py | |||
@@ -7,6 +7,7 @@ | |||
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | 9 | ||
10 | import os | ||
10 | import re | 11 | import re |
11 | 12 | ||
12 | from django.urls import reverse | 13 | from django.urls import reverse |
@@ -22,7 +23,8 @@ class TestProjectBuildsPage(SeleniumTestCase): | |||
22 | CLI_BUILDS_PROJECT_NAME = 'command line builds' | 23 | CLI_BUILDS_PROJECT_NAME = 'command line builds' |
23 | 24 | ||
24 | def setUp(self): | 25 | def setUp(self): |
25 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', | 26 | builldir = os.environ.get('BUILDDIR', './') |
27 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
26 | branch='master', dirpath='') | 28 | branch='master', dirpath='') |
27 | release = Release.objects.create(name='release1', | 29 | release = Release.objects.create(name='release1', |
28 | bitbake_version=bbv) | 30 | bitbake_version=bbv) |
diff --git a/bitbake/lib/toaster/tests/browser/test_project_config_page.py b/bitbake/lib/toaster/tests/browser/test_project_config_page.py index 944bcb2631..b9de541efa 100644 --- a/bitbake/lib/toaster/tests/browser/test_project_config_page.py +++ b/bitbake/lib/toaster/tests/browser/test_project_config_page.py | |||
@@ -7,10 +7,12 @@ | |||
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | 9 | ||
10 | import os | ||
10 | from django.urls import reverse | 11 | from django.urls import reverse |
11 | from tests.browser.selenium_helpers import SeleniumTestCase | 12 | from tests.browser.selenium_helpers import SeleniumTestCase |
12 | 13 | ||
13 | from orm.models import BitbakeVersion, Release, Project, ProjectVariable | 14 | from orm.models import BitbakeVersion, Release, Project, ProjectVariable |
15 | from selenium.webdriver.common.by import By | ||
14 | 16 | ||
15 | class TestProjectConfigsPage(SeleniumTestCase): | 17 | class TestProjectConfigsPage(SeleniumTestCase): |
16 | """ Test data at /project/X/builds is displayed correctly """ | 18 | """ Test data at /project/X/builds is displayed correctly """ |
@@ -21,7 +23,8 @@ class TestProjectConfigsPage(SeleniumTestCase): | |||
21 | 'any of these characters' | 23 | 'any of these characters' |
22 | 24 | ||
23 | def setUp(self): | 25 | def setUp(self): |
24 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', | 26 | builldir = os.environ.get('BUILDDIR', './') |
27 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
25 | branch='master', dirpath='') | 28 | branch='master', dirpath='') |
26 | release = Release.objects.create(name='release1', | 29 | release = Release.objects.create(name='release1', |
27 | bitbake_version=bbv) | 30 | bitbake_version=bbv) |
@@ -66,7 +69,7 @@ class TestProjectConfigsPage(SeleniumTestCase): | |||
66 | 69 | ||
67 | self.enter_text('#new-imagefs_types', imagefs_type) | 70 | self.enter_text('#new-imagefs_types', imagefs_type) |
68 | 71 | ||
69 | checkboxes = self.driver.find_elements_by_xpath("//input[@class='fs-checkbox-fstypes']") | 72 | checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']") |
70 | 73 | ||
71 | for checkbox in checkboxes: | 74 | for checkbox in checkboxes: |
72 | if checkbox.get_attribute("value") == "btrfs": | 75 | if checkbox.get_attribute("value") == "btrfs": |
@@ -95,7 +98,7 @@ class TestProjectConfigsPage(SeleniumTestCase): | |||
95 | for checkbox in checkboxes: | 98 | for checkbox in checkboxes: |
96 | if checkbox.get_attribute("value") == "cpio": | 99 | if checkbox.get_attribute("value") == "cpio": |
97 | checkbox.click() | 100 | checkbox.click() |
98 | element = self.driver.find_element_by_id('new-imagefs_types') | 101 | element = self.driver.find_element(By.ID, 'new-imagefs_types') |
99 | 102 | ||
100 | self.wait_until_visible('#new-imagefs_types') | 103 | self.wait_until_visible('#new-imagefs_types') |
101 | 104 | ||
@@ -129,7 +132,7 @@ class TestProjectConfigsPage(SeleniumTestCase): | |||
129 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) | 132 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) |
130 | 133 | ||
131 | # downloads dir path has a space | 134 | # downloads dir path has a space |
132 | self.driver.find_element_by_id('new-dl_dir').clear() | 135 | self.driver.find_element(By.ID, 'new-dl_dir').clear() |
133 | self.enter_text('#new-dl_dir', '/foo/bar a') | 136 | self.enter_text('#new-dl_dir', '/foo/bar a') |
134 | 137 | ||
135 | element = self.wait_until_visible('#hintError-dl_dir') | 138 | element = self.wait_until_visible('#hintError-dl_dir') |
@@ -137,7 +140,7 @@ class TestProjectConfigsPage(SeleniumTestCase): | |||
137 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | 140 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) |
138 | 141 | ||
139 | # downloads dir path starts with ${...} but has a space | 142 | # downloads dir path starts with ${...} but has a space |
140 | self.driver.find_element_by_id('new-dl_dir').clear() | 143 | self.driver.find_element(By.ID,'new-dl_dir').clear() |
141 | self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') | 144 | self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') |
142 | 145 | ||
143 | element = self.wait_until_visible('#hintError-dl_dir') | 146 | element = self.wait_until_visible('#hintError-dl_dir') |
@@ -145,18 +148,18 @@ class TestProjectConfigsPage(SeleniumTestCase): | |||
145 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | 148 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) |
146 | 149 | ||
147 | # downloads dir path starts with / | 150 | # downloads dir path starts with / |
148 | self.driver.find_element_by_id('new-dl_dir').clear() | 151 | self.driver.find_element(By.ID,'new-dl_dir').clear() |
149 | self.enter_text('#new-dl_dir', '/bar/foo') | 152 | self.enter_text('#new-dl_dir', '/bar/foo') |
150 | 153 | ||
151 | hidden_element = self.driver.find_element_by_id('hintError-dl_dir') | 154 | hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') |
152 | self.assertEqual(hidden_element.is_displayed(), False, | 155 | self.assertEqual(hidden_element.is_displayed(), False, |
153 | 'downloads directory path valid but treated as invalid') | 156 | 'downloads directory path valid but treated as invalid') |
154 | 157 | ||
155 | # downloads dir path starts with ${...} | 158 | # downloads dir path starts with ${...} |
156 | self.driver.find_element_by_id('new-dl_dir').clear() | 159 | self.driver.find_element(By.ID,'new-dl_dir').clear() |
157 | self.enter_text('#new-dl_dir', '${TOPDIR}/down') | 160 | self.enter_text('#new-dl_dir', '${TOPDIR}/down') |
158 | 161 | ||
159 | hidden_element = self.driver.find_element_by_id('hintError-dl_dir') | 162 | hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') |
160 | self.assertEqual(hidden_element.is_displayed(), False, | 163 | self.assertEqual(hidden_element.is_displayed(), False, |
161 | 'downloads directory path valid but treated as invalid') | 164 | 'downloads directory path valid but treated as invalid') |
162 | 165 | ||
@@ -184,7 +187,7 @@ class TestProjectConfigsPage(SeleniumTestCase): | |||
184 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) | 187 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) |
185 | 188 | ||
186 | # path has a space | 189 | # path has a space |
187 | self.driver.find_element_by_id('new-sstate_dir').clear() | 190 | self.driver.find_element(By.ID, 'new-sstate_dir').clear() |
188 | self.enter_text('#new-sstate_dir', '/foo/bar a') | 191 | self.enter_text('#new-sstate_dir', '/foo/bar a') |
189 | 192 | ||
190 | element = self.wait_until_visible('#hintError-sstate_dir') | 193 | element = self.wait_until_visible('#hintError-sstate_dir') |
@@ -192,7 +195,7 @@ class TestProjectConfigsPage(SeleniumTestCase): | |||
192 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | 195 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) |
193 | 196 | ||
194 | # path starts with ${...} but has a space | 197 | # path starts with ${...} but has a space |
195 | self.driver.find_element_by_id('new-sstate_dir').clear() | 198 | self.driver.find_element(By.ID,'new-sstate_dir').clear() |
196 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') | 199 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') |
197 | 200 | ||
198 | element = self.wait_until_visible('#hintError-sstate_dir') | 201 | element = self.wait_until_visible('#hintError-sstate_dir') |
@@ -200,18 +203,18 @@ class TestProjectConfigsPage(SeleniumTestCase): | |||
200 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | 203 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) |
201 | 204 | ||
202 | # path starts with / | 205 | # path starts with / |
203 | self.driver.find_element_by_id('new-sstate_dir').clear() | 206 | self.driver.find_element(By.ID,'new-sstate_dir').clear() |
204 | self.enter_text('#new-sstate_dir', '/bar/foo') | 207 | self.enter_text('#new-sstate_dir', '/bar/foo') |
205 | 208 | ||
206 | hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') | 209 | hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') |
207 | self.assertEqual(hidden_element.is_displayed(), False, | 210 | self.assertEqual(hidden_element.is_displayed(), False, |
208 | 'sstate directory path valid but treated as invalid') | 211 | 'sstate directory path valid but treated as invalid') |
209 | 212 | ||
210 | # paths starts with ${...} | 213 | # paths starts with ${...} |
211 | self.driver.find_element_by_id('new-sstate_dir').clear() | 214 | self.driver.find_element(By.ID, 'new-sstate_dir').clear() |
212 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down') | 215 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down') |
213 | 216 | ||
214 | hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') | 217 | hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') |
215 | self.assertEqual(hidden_element.is_displayed(), False, | 218 | self.assertEqual(hidden_element.is_displayed(), False, |
216 | 'sstate directory path valid but treated as invalid') | 219 | 'sstate directory path valid but treated as invalid') |
217 | 220 | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_sample.py b/bitbake/lib/toaster/tests/browser/test_sample.py index b0067c21cd..f04f1d9a16 100644 --- a/bitbake/lib/toaster/tests/browser/test_sample.py +++ b/bitbake/lib/toaster/tests/browser/test_sample.py | |||
@@ -27,3 +27,13 @@ class TestSample(SeleniumTestCase): | |||
27 | self.get(url) | 27 | self.get(url) |
28 | brand_link = self.find('.toaster-navbar-brand a.brand') | 28 | brand_link = self.find('.toaster-navbar-brand a.brand') |
29 | self.assertEqual(brand_link.text.strip(), 'Toaster') | 29 | self.assertEqual(brand_link.text.strip(), 'Toaster') |
30 | |||
31 | def test_no_builds_message(self): | ||
32 | """ Test that a message is shown when there are no builds """ | ||
33 | url = reverse('all-builds') | ||
34 | self.get(url) | ||
35 | self.wait_until_visible('#empty-state-allbuildstable') # wait for the empty state div to appear | ||
36 | div_msg = self.find('#empty-state-allbuildstable .alert-info') | ||
37 | |||
38 | msg = 'Sorry - no data found' | ||
39 | self.assertEqual(div_msg.text, msg) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py b/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py index e82d5ec654..691aca1ef0 100644 --- a/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py +++ b/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py | |||
@@ -8,11 +8,13 @@ | |||
8 | # | 8 | # |
9 | 9 | ||
10 | from datetime import datetime | 10 | from datetime import datetime |
11 | import os | ||
11 | 12 | ||
12 | from django.urls import reverse | 13 | from django.urls import reverse |
13 | from django.utils import timezone | 14 | from django.utils import timezone |
14 | from tests.browser.selenium_helpers import SeleniumTestCase | 15 | from tests.browser.selenium_helpers import SeleniumTestCase |
15 | from orm.models import BitbakeVersion, Release, Project, Build | 16 | from orm.models import BitbakeVersion, Release, Project, Build |
17 | from selenium.webdriver.common.by import By | ||
16 | 18 | ||
17 | class TestToasterTableUI(SeleniumTestCase): | 19 | class TestToasterTableUI(SeleniumTestCase): |
18 | """ | 20 | """ |
@@ -33,7 +35,7 @@ class TestToasterTableUI(SeleniumTestCase): | |||
33 | table: WebElement for a ToasterTable | 35 | table: WebElement for a ToasterTable |
34 | """ | 36 | """ |
35 | selector = 'thead a.sorted' | 37 | selector = 'thead a.sorted' |
36 | heading = table.find_element_by_css_selector(selector) | 38 | heading = table.find_element(By.CSS_SELECTOR, selector) |
37 | return heading.get_attribute('innerHTML').strip() | 39 | return heading.get_attribute('innerHTML').strip() |
38 | 40 | ||
39 | def _get_datetime_from_cell(self, row, selector): | 41 | def _get_datetime_from_cell(self, row, selector): |
@@ -45,7 +47,7 @@ class TestToasterTableUI(SeleniumTestCase): | |||
45 | selector: CSS selector to use to find the cell containing the date time | 47 | selector: CSS selector to use to find the cell containing the date time |
46 | string | 48 | string |
47 | """ | 49 | """ |
48 | cell = row.find_element_by_css_selector(selector) | 50 | cell = row.find_element(By.CSS_SELECTOR, selector) |
49 | cell_text = cell.get_attribute('innerHTML').strip() | 51 | cell_text = cell.get_attribute('innerHTML').strip() |
50 | return datetime.strptime(cell_text, '%d/%m/%y %H:%M') | 52 | return datetime.strptime(cell_text, '%d/%m/%y %H:%M') |
51 | 53 | ||
@@ -58,7 +60,8 @@ class TestToasterTableUI(SeleniumTestCase): | |||
58 | later = now + timezone.timedelta(hours=1) | 60 | later = now + timezone.timedelta(hours=1) |
59 | even_later = later + timezone.timedelta(hours=1) | 61 | even_later = later + timezone.timedelta(hours=1) |
60 | 62 | ||
61 | bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', | 63 | builldir = os.environ.get('BUILDDIR', './') |
64 | bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/', | ||
62 | branch='master', dirpath='') | 65 | branch='master', dirpath='') |
63 | release = Release.objects.create(name='test release', | 66 | release = Release.objects.create(name='test release', |
64 | branch_name='master', | 67 | branch_name='master', |
@@ -105,7 +108,7 @@ class TestToasterTableUI(SeleniumTestCase): | |||
105 | self.click('#checkbox-started_on') | 108 | self.click('#checkbox-started_on') |
106 | 109 | ||
107 | # sort by started_on column | 110 | # sort by started_on column |
108 | links = table.find_elements_by_css_selector('th.started_on a') | 111 | links = table.find_elements(By.CSS_SELECTOR, 'th.started_on a') |
109 | for link in links: | 112 | for link in links: |
110 | if link.get_attribute('innerHTML').strip() == 'Started on': | 113 | if link.get_attribute('innerHTML').strip() == 'Started on': |
111 | link.click() | 114 | link.click() |
diff --git a/bitbake/lib/toaster/tests/builds/buildtest.py b/bitbake/lib/toaster/tests/builds/buildtest.py index 872bbd3775..e54d561334 100644 --- a/bitbake/lib/toaster/tests/builds/buildtest.py +++ b/bitbake/lib/toaster/tests/builds/buildtest.py | |||
@@ -88,7 +88,7 @@ def load_build_environment(): | |||
88 | class BuildTest(unittest.TestCase): | 88 | class BuildTest(unittest.TestCase): |
89 | 89 | ||
90 | PROJECT_NAME = "Testbuild" | 90 | PROJECT_NAME = "Testbuild" |
91 | BUILDDIR = "/tmp/build/" | 91 | BUILDDIR = os.environ.get("BUILDDIR") |
92 | 92 | ||
93 | def build(self, target): | 93 | def build(self, target): |
94 | # So that the buildinfo helper uses the test database' | 94 | # So that the buildinfo helper uses the test database' |
@@ -116,10 +116,19 @@ class BuildTest(unittest.TestCase): | |||
116 | project = Project.objects.create_project(name=BuildTest.PROJECT_NAME, | 116 | project = Project.objects.create_project(name=BuildTest.PROJECT_NAME, |
117 | release=release) | 117 | release=release) |
118 | 118 | ||
119 | passthrough_variable_names = ["SSTATE_DIR", "DL_DIR", "SSTATE_MIRRORS", "BB_HASHSERVE", "BB_HASHSERVE_UPSTREAM"] | ||
120 | for variable_name in passthrough_variable_names: | ||
121 | current_variable = os.environ.get(variable_name) | ||
122 | if current_variable: | ||
123 | ProjectVariable.objects.get_or_create( | ||
124 | name=variable_name, | ||
125 | value=current_variable, | ||
126 | project=project) | ||
127 | |||
119 | if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"): | 128 | if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"): |
120 | ProjectVariable.objects.get_or_create( | 129 | ProjectVariable.objects.get_or_create( |
121 | name="SSTATE_MIRRORS", | 130 | name="SSTATE_MIRRORS", |
122 | value="file://.* http://autobuilder.yoctoproject.org/pub/sstate/PATH;downloadfilename=PATH", | 131 | value="file://.* http://sstate.yoctoproject.org/all/PATH;downloadfilename=PATH", |
123 | project=project) | 132 | project=project) |
124 | 133 | ||
125 | ProjectTarget.objects.create(project=project, | 134 | ProjectTarget.objects.create(project=project, |
diff --git a/bitbake/lib/toaster/tests/builds/test_core_image_min.py b/bitbake/lib/toaster/tests/builds/test_core_image_min.py index 44b6cbec7b..c5bfdbfbb5 100644 --- a/bitbake/lib/toaster/tests/builds/test_core_image_min.py +++ b/bitbake/lib/toaster/tests/builds/test_core_image_min.py | |||
@@ -10,6 +10,7 @@ | |||
10 | # Ionut Chisanovici, Paul Eggleton and Cristian Iorga | 10 | # Ionut Chisanovici, Paul Eggleton and Cristian Iorga |
11 | 11 | ||
12 | import os | 12 | import os |
13 | import pytest | ||
13 | 14 | ||
14 | from django.db.models import Q | 15 | from django.db.models import Q |
15 | 16 | ||
@@ -20,12 +21,13 @@ from orm.models import CustomImagePackage | |||
20 | 21 | ||
21 | from tests.builds.buildtest import BuildTest | 22 | from tests.builds.buildtest import BuildTest |
22 | 23 | ||
23 | 24 | @pytest.mark.order(4) | |
25 | @pytest.mark.django_db(True) | ||
24 | class BuildCoreImageMinimal(BuildTest): | 26 | class BuildCoreImageMinimal(BuildTest): |
25 | """Build core-image-minimal and test the results""" | 27 | """Build core-image-minimal and test the results""" |
26 | 28 | ||
27 | def setUp(self): | 29 | def setUp(self): |
28 | self.completed_build = self.build("core-image-minimal") | 30 | self.completed_build = self.target_already_built("core-image-minimal") |
29 | 31 | ||
30 | # Check if build name is unique - tc_id=795 | 32 | # Check if build name is unique - tc_id=795 |
31 | def test_Build_Unique_Name(self): | 33 | def test_Build_Unique_Name(self): |
@@ -44,17 +46,6 @@ class BuildCoreImageMinimal(BuildTest): | |||
44 | total_builds, | 46 | total_builds, |
45 | msg='Build cooker log path is not unique') | 47 | msg='Build cooker log path is not unique') |
46 | 48 | ||
47 | # Check if task order is unique for one build - tc=824 | ||
48 | def test_Task_Unique_Order(self): | ||
49 | total_task_order = Task.objects.filter( | ||
50 | build=self.built).values('order').count() | ||
51 | distinct_task_order = Task.objects.filter( | ||
52 | build=self.completed_build).values('order').distinct().count() | ||
53 | |||
54 | self.assertEqual(total_task_order, | ||
55 | distinct_task_order, | ||
56 | msg='Errors task order is not unique') | ||
57 | |||
58 | # Check task order sequence for one build - tc=825 | 49 | # Check task order sequence for one build - tc=825 |
59 | def test_Task_Order_Sequence(self): | 50 | def test_Task_Order_Sequence(self): |
60 | cnt_err = [] | 51 | cnt_err = [] |
@@ -98,7 +89,6 @@ class BuildCoreImageMinimal(BuildTest): | |||
98 | 'task_name', | 89 | 'task_name', |
99 | 'sstate_result') | 90 | 'sstate_result') |
100 | cnt_err = [] | 91 | cnt_err = [] |
101 | |||
102 | for task in tasks: | 92 | for task in tasks: |
103 | if (task['sstate_result'] != Task.SSTATE_NA and | 93 | if (task['sstate_result'] != Task.SSTATE_NA and |
104 | task['sstate_result'] != Task.SSTATE_MISS): | 94 | task['sstate_result'] != Task.SSTATE_MISS): |
@@ -221,6 +211,7 @@ class BuildCoreImageMinimal(BuildTest): | |||
221 | # orm_build.outcome=0 then if the file exists and its size matches | 211 | # orm_build.outcome=0 then if the file exists and its size matches |
222 | # the file_size value. Need to add the tc in the test run | 212 | # the file_size value. Need to add the tc in the test run |
223 | def test_Target_File_Name_Populated(self): | 213 | def test_Target_File_Name_Populated(self): |
214 | cnt_err = [] | ||
224 | builds = Build.objects.filter(outcome=0).values('id') | 215 | builds = Build.objects.filter(outcome=0).values('id') |
225 | for build in builds: | 216 | for build in builds: |
226 | targets = Target.objects.filter( | 217 | targets = Target.objects.filter( |
@@ -230,7 +221,6 @@ class BuildCoreImageMinimal(BuildTest): | |||
230 | target_id=target['id']).values('id', | 221 | target_id=target['id']).values('id', |
231 | 'file_name', | 222 | 'file_name', |
232 | 'file_size') | 223 | 'file_size') |
233 | cnt_err = [] | ||
234 | for file_info in target_files: | 224 | for file_info in target_files: |
235 | target_id = file_info['id'] | 225 | target_id = file_info['id'] |
236 | target_file_name = file_info['file_name'] | 226 | target_file_name = file_info['file_name'] |
diff --git a/bitbake/lib/toaster/tests/commands/test_loaddata.py b/bitbake/lib/toaster/tests/commands/test_loaddata.py index 9e8d5553cf..7d04f030ee 100644 --- a/bitbake/lib/toaster/tests/commands/test_loaddata.py +++ b/bitbake/lib/toaster/tests/commands/test_loaddata.py | |||
@@ -6,13 +6,13 @@ | |||
6 | # | 6 | # |
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | 9 | import pytest | |
10 | from django.test import TestCase | 10 | from django.test import TestCase |
11 | from django.core import management | 11 | from django.core import management |
12 | 12 | ||
13 | from orm.models import Layer_Version, Layer, Release, ToasterSetting | 13 | from orm.models import Layer_Version, Layer, Release, ToasterSetting |
14 | 14 | ||
15 | 15 | @pytest.mark.order(2) | |
16 | class TestLoadDataFixtures(TestCase): | 16 | class TestLoadDataFixtures(TestCase): |
17 | """ Test loading our 3 provided fixtures """ | 17 | """ Test loading our 3 provided fixtures """ |
18 | def test_run_loaddata_poky_command(self): | 18 | def test_run_loaddata_poky_command(self): |
diff --git a/bitbake/lib/toaster/tests/commands/test_lsupdates.py b/bitbake/lib/toaster/tests/commands/test_lsupdates.py index 3c4fbe0550..30c6eeb4ac 100644 --- a/bitbake/lib/toaster/tests/commands/test_lsupdates.py +++ b/bitbake/lib/toaster/tests/commands/test_lsupdates.py | |||
@@ -7,12 +7,13 @@ | |||
7 | # SPDX-License-Identifier: GPL-2.0-only | 7 | # SPDX-License-Identifier: GPL-2.0-only |
8 | # | 8 | # |
9 | 9 | ||
10 | import pytest | ||
10 | from django.test import TestCase | 11 | from django.test import TestCase |
11 | from django.core import management | 12 | from django.core import management |
12 | 13 | ||
13 | from orm.models import Layer_Version, Machine, Recipe | 14 | from orm.models import Layer_Version, Machine, Recipe |
14 | 15 | ||
15 | 16 | @pytest.mark.order(3) | |
16 | class TestLayerIndexUpdater(TestCase): | 17 | class TestLayerIndexUpdater(TestCase): |
17 | def test_run_lsupdates_command(self): | 18 | def test_run_lsupdates_command(self): |
18 | # Load some release information for us to fetch from the layer index | 19 | # Load some release information for us to fetch from the layer index |
diff --git a/bitbake/lib/toaster/tests/commands/test_runbuilds.py b/bitbake/lib/toaster/tests/commands/test_runbuilds.py index e223b95fcb..849c227edc 100644 --- a/bitbake/lib/toaster/tests/commands/test_runbuilds.py +++ b/bitbake/lib/toaster/tests/commands/test_runbuilds.py | |||
@@ -19,12 +19,14 @@ import time | |||
19 | import subprocess | 19 | import subprocess |
20 | import signal | 20 | import signal |
21 | 21 | ||
22 | import logging | ||
23 | |||
22 | 24 | ||
23 | class KillRunbuilds(threading.Thread): | 25 | class KillRunbuilds(threading.Thread): |
24 | """ Kill the runbuilds process after an amount of time """ | 26 | """ Kill the runbuilds process after an amount of time """ |
25 | def __init__(self, *args, **kwargs): | 27 | def __init__(self, *args, **kwargs): |
26 | super(KillRunbuilds, self).__init__(*args, **kwargs) | 28 | super(KillRunbuilds, self).__init__(*args, **kwargs) |
27 | self.setDaemon(True) | 29 | self.daemon = True |
28 | 30 | ||
29 | def run(self): | 31 | def run(self): |
30 | time.sleep(5) | 32 | time.sleep(5) |
@@ -34,9 +36,12 @@ class KillRunbuilds(threading.Thread): | |||
34 | pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."), | 36 | pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."), |
35 | ".runbuilds.pid") | 37 | ".runbuilds.pid") |
36 | 38 | ||
37 | with open(pidfile_path) as pidfile: | 39 | try: |
38 | pid = pidfile.read() | 40 | with open(pidfile_path) as pidfile: |
39 | os.kill(int(pid), signal.SIGTERM) | 41 | pid = pidfile.read() |
42 | os.kill(int(pid), signal.SIGTERM) | ||
43 | except ProcessLookupError: | ||
44 | logging.warning("Runbuilds not running or already killed") | ||
40 | 45 | ||
41 | 46 | ||
42 | class TestCommands(TestCase): | 47 | class TestCommands(TestCase): |
diff --git a/bitbake/lib/toaster/tests/db/test_db.py b/bitbake/lib/toaster/tests/db/test_db.py index 0410422276..072ab94363 100644 --- a/bitbake/lib/toaster/tests/db/test_db.py +++ b/bitbake/lib/toaster/tests/db/test_db.py | |||
@@ -23,6 +23,7 @@ | |||
23 | # SOFTWARE. | 23 | # SOFTWARE. |
24 | 24 | ||
25 | import sys | 25 | import sys |
26 | import pytest | ||
26 | 27 | ||
27 | try: | 28 | try: |
28 | from StringIO import StringIO | 29 | from StringIO import StringIO |
@@ -47,7 +48,7 @@ def capture(command, *args, **kwargs): | |||
47 | def makemigrations(): | 48 | def makemigrations(): |
48 | management.call_command('makemigrations') | 49 | management.call_command('makemigrations') |
49 | 50 | ||
50 | 51 | @pytest.mark.order(1) | |
51 | class MigrationTest(TestCase): | 52 | class MigrationTest(TestCase): |
52 | 53 | ||
53 | def testPendingMigration(self): | 54 | def testPendingMigration(self): |
diff --git a/bitbake/lib/toaster/tests/functional/functional_helpers.py b/bitbake/lib/toaster/tests/functional/functional_helpers.py index 5c4ea71794..e28f2024f5 100644 --- a/bitbake/lib/toaster/tests/functional/functional_helpers.py +++ b/bitbake/lib/toaster/tests/functional/functional_helpers.py | |||
@@ -11,35 +11,58 @@ 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 |
15 | import requests | ||
16 | 16 | ||
17 | from django.urls import reverse | ||
17 | from tests.browser.selenium_helpers_base import SeleniumTestCaseBase | 18 | from tests.browser.selenium_helpers_base import SeleniumTestCaseBase |
18 | from tests.builds.buildtest import load_build_environment | 19 | from selenium.webdriver.common.by import By |
20 | from selenium.webdriver.support.select import Select | ||
21 | from selenium.common.exceptions import NoSuchElementException | ||
19 | 22 | ||
20 | logger = logging.getLogger("toaster") | 23 | logger = logging.getLogger("toaster") |
24 | toaster_processes = [] | ||
21 | 25 | ||
22 | class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | 26 | class SeleniumFunctionalTestCase(SeleniumTestCaseBase): |
23 | wait_toaster_time = 5 | 27 | wait_toaster_time = 10 |
24 | 28 | ||
25 | @classmethod | 29 | @classmethod |
26 | def setUpClass(cls): | 30 | def setUpClass(cls): |
27 | # So that the buildinfo helper uses the test database' | 31 | # So that the buildinfo helper uses the test database' |
28 | if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \ | 32 | if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \ |
29 | 'toastermain.settings_test': | 33 | 'toastermain.settings_test': |
30 | raise RuntimeError("Please initialise django with the tests settings: " \ | 34 | raise RuntimeError("Please initialise django with the tests settings: " |
31 | "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") | 35 | "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") |
32 | 36 | ||
33 | load_build_environment() | 37 | # Wait for any known toaster processes to exit |
38 | global toaster_processes | ||
39 | for toaster_process in toaster_processes: | ||
40 | try: | ||
41 | os.waitpid(toaster_process, os.WNOHANG) | ||
42 | except ChildProcessError: | ||
43 | pass | ||
34 | 44 | ||
35 | # start toaster | 45 | # start toaster |
36 | cmd = "bash -c 'source toaster start'" | 46 | cmd = "bash -c 'source toaster start'" |
37 | p = subprocess.Popen( | 47 | start_process = subprocess.Popen( |
38 | cmd, | 48 | cmd, |
39 | cwd=os.environ.get("BUILDDIR"), | 49 | cwd=os.environ.get("BUILDDIR"), |
40 | shell=True) | 50 | shell=True) |
41 | if p.wait() != 0: | 51 | toaster_processes = [start_process.pid] |
42 | raise RuntimeError("Can't initialize toaster") | 52 | if start_process.wait() != 0: |
53 | port_use = os.popen("lsof -i -P -n | grep '8000 (LISTEN)'").read().strip() | ||
54 | message = '' | ||
55 | if port_use: | ||
56 | process_id = port_use.split()[1] | ||
57 | process = os.popen(f"ps -o cmd= -p {process_id}").read().strip() | ||
58 | message = f"Port 8000 occupied by {process}" | ||
59 | raise RuntimeError(f"Can't initialize toaster. {message}") | ||
60 | |||
61 | builddir = os.environ.get("BUILDDIR") | ||
62 | with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f: | ||
63 | toaster_processes.append(int(f.read())) | ||
64 | with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: | ||
65 | toaster_processes.append(int(f.read())) | ||
43 | 66 | ||
44 | super(SeleniumFunctionalTestCase, cls).setUpClass() | 67 | super(SeleniumFunctionalTestCase, cls).setUpClass() |
45 | cls.live_server_url = 'http://localhost:8000/' | 68 | cls.live_server_url = 'http://localhost:8000/' |
@@ -48,22 +71,30 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
48 | def tearDownClass(cls): | 71 | def tearDownClass(cls): |
49 | super(SeleniumFunctionalTestCase, cls).tearDownClass() | 72 | super(SeleniumFunctionalTestCase, cls).tearDownClass() |
50 | 73 | ||
51 | # XXX: source toaster stop gets blocked, to review why? | 74 | global toaster_processes |
52 | # from now send SIGTERM by hand | ||
53 | time.sleep(cls.wait_toaster_time) | ||
54 | builddir = os.environ.get("BUILDDIR") | ||
55 | 75 | ||
56 | with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f: | 76 | cmd = "bash -c 'source toaster stop'" |
57 | toastermain_pid = int(f.read()) | 77 | stop_process = subprocess.Popen( |
58 | os.kill(toastermain_pid, signal.SIGTERM) | 78 | cmd, |
59 | with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: | 79 | cwd=os.environ.get("BUILDDIR"), |
60 | runbuilds_pid = int(f.read()) | 80 | shell=True) |
61 | os.kill(runbuilds_pid, signal.SIGTERM) | 81 | # Toaster stop has been known to hang in these tests so force kill if it stalls |
82 | try: | ||
83 | if stop_process.wait(cls.wait_toaster_time) != 0: | ||
84 | raise Exception('Toaster stop process failed') | ||
85 | except Exception as e: | ||
86 | if e is subprocess.TimeoutExpired: | ||
87 | print('Toaster stop process took too long. Force killing toaster...') | ||
88 | else: | ||
89 | print('Toaster stop process failed. Force killing toaster...') | ||
90 | stop_process.kill() | ||
91 | for toaster_process in toaster_processes: | ||
92 | os.kill(toaster_process, signal.SIGTERM) | ||
62 | 93 | ||
63 | 94 | ||
64 | def get_URL(self): | 95 | def get_URL(self): |
65 | rc=self.get_page_source() | 96 | rc=self.get_page_source() |
66 | project_url=re.search("(projectPageUrl\s:\s\")(.*)(\",)",rc) | 97 | project_url=re.search(r"(projectPageUrl\s:\s\")(.*)(\",)",rc) |
67 | return project_url.group(2) | 98 | return project_url.group(2) |
68 | 99 | ||
69 | 100 | ||
@@ -74,8 +105,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
74 | """ | 105 | """ |
75 | try: | 106 | try: |
76 | table_element = self.get_table_element(table_id) | 107 | table_element = self.get_table_element(table_id) |
77 | element = table_element.find_element_by_link_text(link_text) | 108 | element = table_element.find_element(By.LINK_TEXT, link_text) |
78 | except self.NoSuchElementException: | 109 | except NoSuchElementException: |
79 | print('no element found') | 110 | print('no element found') |
80 | raise | 111 | raise |
81 | return element | 112 | return element |
@@ -85,8 +116,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
85 | #return whole-table element | 116 | #return whole-table element |
86 | element_xpath = "//*[@id='" + table_id + "']" | 117 | element_xpath = "//*[@id='" + table_id + "']" |
87 | try: | 118 | try: |
88 | element = self.driver.find_element_by_xpath(element_xpath) | 119 | element = self.driver.find_element(By.XPATH, element_xpath) |
89 | except self.NoSuchElementException: | 120 | except NoSuchElementException: |
90 | raise | 121 | raise |
91 | return element | 122 | return element |
92 | row = coordinate[0] | 123 | row = coordinate[0] |
@@ -95,8 +126,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
95 | #return whole-row element | 126 | #return whole-row element |
96 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]" | 127 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]" |
97 | try: | 128 | try: |
98 | element = self.driver.find_element_by_xpath(element_xpath) | 129 | element = self.driver.find_element(By.XPATH, element_xpath) |
99 | except self.NoSuchElementException: | 130 | except NoSuchElementException: |
100 | return False | 131 | return False |
101 | return element | 132 | return element |
102 | #now we are looking for an element with specified X and Y | 133 | #now we are looking for an element with specified X and Y |
@@ -104,7 +135,90 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
104 | 135 | ||
105 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" | 136 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" |
106 | try: | 137 | try: |
107 | element = self.driver.find_element_by_xpath(element_xpath) | 138 | element = self.driver.find_element(By.XPATH, element_xpath) |
108 | except self.NoSuchElementException: | 139 | except NoSuchElementException: |
109 | return False | 140 | return False |
110 | return element | 141 | return element |
142 | |||
143 | def create_new_project( | ||
144 | self, | ||
145 | project_name, | ||
146 | release, | ||
147 | release_title, | ||
148 | merge_toaster_settings, | ||
149 | ): | ||
150 | """ Create/Test new project using: | ||
151 | - Project Name: Any string | ||
152 | - Release: Any string | ||
153 | - Merge Toaster settings: True or False | ||
154 | """ | ||
155 | |||
156 | # Obtain a CSRF token from a suitable URL | ||
157 | projs = requests.get(self.live_server_url + reverse('newproject')) | ||
158 | csrftoken = projs.cookies.get('csrftoken') | ||
159 | |||
160 | # Use the projects typeahead to find out if the project already exists | ||
161 | req = requests.get(self.live_server_url + reverse('xhr_projectstypeahead'), {'search': project_name, 'format' : 'json'}) | ||
162 | data = req.json() | ||
163 | # Delete any existing projects | ||
164 | for result in data['results']: | ||
165 | del_url = reverse('xhr_project', args=(result['id'],)) | ||
166 | del_response = requests.delete(self.live_server_url + del_url, cookies={'csrftoken': csrftoken}, headers={'X-CSRFToken': csrftoken}) | ||
167 | self.assertEqual(del_response.status_code, 200) | ||
168 | |||
169 | self.get(reverse('newproject')) | ||
170 | self.wait_until_visible('#new-project-name') | ||
171 | self.driver.find_element(By.ID, | ||
172 | "new-project-name").send_keys(project_name) | ||
173 | |||
174 | select = Select(self.find('#projectversion')) | ||
175 | select.select_by_value(release) | ||
176 | |||
177 | # check merge toaster settings | ||
178 | checkbox = self.find('.checkbox-mergeattr') | ||
179 | if merge_toaster_settings: | ||
180 | if not checkbox.is_selected(): | ||
181 | checkbox.click() | ||
182 | else: | ||
183 | if checkbox.is_selected(): | ||
184 | checkbox.click() | ||
185 | |||
186 | self.wait_until_clickable('#create-project-button') | ||
187 | |||
188 | self.driver.find_element(By.ID, "create-project-button").click() | ||
189 | |||
190 | element = self.wait_until_visible('#project-created-notification') | ||
191 | self.assertTrue( | ||
192 | self.element_exists('#project-created-notification'), | ||
193 | f"Project:{project_name} creation notification not shown" | ||
194 | ) | ||
195 | self.assertTrue( | ||
196 | project_name in element.text, | ||
197 | f"New project name:{project_name} not in new project notification" | ||
198 | ) | ||
199 | |||
200 | # Use the projects typeahead again to check the project now exists | ||
201 | req = requests.get(self.live_server_url + reverse('xhr_projectstypeahead'), {'search': project_name, 'format' : 'json'}) | ||
202 | data = req.json() | ||
203 | self.assertGreater(len(data['results']), 0, f"New project:{project_name} not found in database") | ||
204 | |||
205 | project_id = data['results'][0]['id'] | ||
206 | |||
207 | self.wait_until_visible('#project-release-title') | ||
208 | |||
209 | # check release | ||
210 | if release_title is not None: | ||
211 | self.assertTrue(re.search( | ||
212 | release_title, | ||
213 | self.driver.find_element(By.XPATH, | ||
214 | "//span[@id='project-release-title']" | ||
215 | ).text), | ||
216 | 'The project release is not defined') | ||
217 | |||
218 | return project_id | ||
219 | |||
220 | def load_projects_page_helper(self): | ||
221 | self.wait_until_present('#projectstable') | ||
222 | # Need to wait for some data in the table too | ||
223 | self.wait_until_present('td[class="updated"]') | ||
224 | |||
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..66213c736e --- /dev/null +++ b/bitbake/lib/toaster/tests/functional/test_create_new_project.py | |||
@@ -0,0 +1,124 @@ | |||
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 selenium.webdriver.common.by import By | ||
15 | |||
16 | class TestCreateNewProject(SeleniumFunctionalTestCase): | ||
17 | |||
18 | def test_create_new_project_master(self): | ||
19 | """ Test create new project using: | ||
20 | - Project Name: Any string | ||
21 | - Release: Yocto Project master (option value: 3) | ||
22 | - Merge Toaster settings: False | ||
23 | """ | ||
24 | release = '3' | ||
25 | release_title = 'Yocto Project master' | ||
26 | project_name = 'projectmaster' | ||
27 | self.create_new_project( | ||
28 | project_name, | ||
29 | release, | ||
30 | release_title, | ||
31 | False, | ||
32 | ) | ||
33 | |||
34 | def test_create_new_project_scarthgap(self): | ||
35 | """ Test create new project using: | ||
36 | - Project Name: Any string | ||
37 | - Release: Yocto Project 5.0 "Scarthgap" (option value: 1) | ||
38 | - Merge Toaster settings: True | ||
39 | """ | ||
40 | release = '1' | ||
41 | release_title = 'Yocto Project 5.0 "Scarthgap"' | ||
42 | project_name = 'projectscarthgap' | ||
43 | self.create_new_project( | ||
44 | project_name, | ||
45 | release, | ||
46 | release_title, | ||
47 | True, | ||
48 | ) | ||
49 | |||
50 | def test_create_new_project_kirkstone(self): | ||
51 | """ Test create new project using: | ||
52 | - Project Name: Any string | ||
53 | - Release: Yocto Project 4.0 "Kirkstone" (option value: 6) | ||
54 | - Merge Toaster settings: True | ||
55 | """ | ||
56 | release = '7' | ||
57 | release_title = 'Yocto Project 4.0 "Kirkstone"' | ||
58 | project_name = 'projectkirkstone' | ||
59 | self.create_new_project( | ||
60 | project_name, | ||
61 | release, | ||
62 | release_title, | ||
63 | True, | ||
64 | ) | ||
65 | |||
66 | def test_create_new_project_local(self): | ||
67 | """ Test create new project using: | ||
68 | - Project Name: Any string | ||
69 | - Release: Local Yocto Project (option value: 2) | ||
70 | - Merge Toaster settings: True | ||
71 | """ | ||
72 | release = '2' | ||
73 | release_title = 'Local Yocto Project' | ||
74 | project_name = 'projectlocal' | ||
75 | self.create_new_project( | ||
76 | project_name, | ||
77 | release, | ||
78 | release_title, | ||
79 | True, | ||
80 | ) | ||
81 | |||
82 | def test_create_new_project_without_name(self): | ||
83 | """ Test create new project without project name """ | ||
84 | self.get(reverse('newproject')) | ||
85 | |||
86 | select = Select(self.find('#projectversion')) | ||
87 | select.select_by_value(str(3)) | ||
88 | |||
89 | # Check input name has required attribute | ||
90 | input_name = self.driver.find_element(By.ID, "new-project-name") | ||
91 | self.assertIsNotNone(input_name.get_attribute('required'), | ||
92 | 'Input name has not required attribute') | ||
93 | |||
94 | # Check create button is disabled | ||
95 | create_btn = self.driver.find_element(By.ID, "create-project-button") | ||
96 | self.assertIsNotNone(create_btn.get_attribute('disabled'), | ||
97 | 'Create button is not disabled') | ||
98 | |||
99 | def test_import_new_project(self): | ||
100 | """ Test import new project using: | ||
101 | - Project Name: Any string | ||
102 | - Project type: select (Import command line project) | ||
103 | - Import existing project directory: Wrong Path | ||
104 | """ | ||
105 | project_name = 'projectimport' | ||
106 | self.get(reverse('newproject')) | ||
107 | self.driver.find_element(By.ID, | ||
108 | "new-project-name").send_keys(project_name) | ||
109 | # select import project | ||
110 | self.find('#type-import').click() | ||
111 | |||
112 | # set wrong path | ||
113 | wrong_path = '/wrongpath' | ||
114 | self.driver.find_element(By.ID, | ||
115 | "import-project-dir").send_keys(wrong_path) | ||
116 | self.driver.find_element(By.ID, "create-project-button").click() | ||
117 | |||
118 | self.wait_until_visible('.alert-danger') | ||
119 | |||
120 | # check error message | ||
121 | self.assertTrue(self.element_exists('.alert-danger'), | ||
122 | 'Alert message not shown') | ||
123 | self.assertTrue(wrong_path in self.find('.alert-danger').text, | ||
124 | "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..d5c9708617 100644 --- a/bitbake/lib/toaster/tests/functional/test_functional_basic.py +++ b/bitbake/lib/toaster/tests/functional/test_functional_basic.py | |||
@@ -8,223 +8,250 @@ | |||
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 | ||
16 | |||
17 | from tests.functional.utils import get_projectId_from_url | ||
18 | |||
13 | 19 | ||
14 | class FuntionalTestBasic(SeleniumFunctionalTestCase): | 20 | class FuntionalTestBasic(SeleniumFunctionalTestCase): |
21 | """Basic functional tests for Toaster""" | ||
22 | project_id = None | ||
23 | project_url = None | ||
15 | 24 | ||
16 | # testcase (1514) | 25 | def setUp(self): |
17 | def test_create_slenium_project(self): | 26 | super(FuntionalTestBasic, self).setUp() |
18 | project_name = 'selenium-project' | 27 | if not FuntionalTestBasic.project_id: |
19 | self.get('') | 28 | FuntionalTestBasic.project_id = self.create_new_project('selenium-project', '3', None, False) |
20 | self.driver.find_element_by_link_text("To start building, create your first Toaster project").click() | ||
21 | self.driver.find_element_by_id("new-project-name").send_keys(project_name) | ||
22 | self.driver.find_element_by_id('projectversion').click() | ||
23 | self.driver.find_element_by_id("create-project-button").click() | ||
24 | element = self.wait_until_visible('#project-created-notification') | ||
25 | self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown') | ||
26 | self.assertTrue(project_name in element.text, | ||
27 | "New project name not in new project notification") | ||
28 | self.assertTrue(Project.objects.filter(name=project_name).count(), | ||
29 | "New project not found in database") | ||
30 | 29 | ||
31 | # testcase (1515) | 30 | # testcase (1515) |
32 | def test_verify_left_bar_menu(self): | 31 | def test_verify_left_bar_menu(self): |
33 | self.get('') | 32 | self.get(reverse('all-projects')) |
34 | self.wait_until_visible('#projectstable') | 33 | self.load_projects_page_helper() |
35 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | 34 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() |
35 | self.wait_until_present('#config-nav') | ||
36 | self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist') | 36 | self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist') |
37 | project_URL=self.get_URL() | 37 | project_URL=self.get_URL() |
38 | self.driver.find_element_by_xpath('//a[@href="'+project_URL+'"]').click() | 38 | self.driver.find_element(By.XPATH, '//a[@href="'+project_URL+'"]').click() |
39 | 39 | ||
40 | try: | 40 | try: |
41 | self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click() | 41 | self.wait_until_present('#config-nav') |
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') | 42 | self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click() |
43 | self.wait_until_present('#filter-modal-customimagestable') | ||
43 | except: | 44 | except: |
44 | self.fail(msg='No Custom images tab available') | 45 | self.fail(msg='No Custom images tab available') |
46 | 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') | ||
45 | 47 | ||
46 | try: | 48 | try: |
47 | self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click() | 49 | 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') | 50 | self.wait_until_present('#filter-modal-imagerecipestable') |
49 | except: | 51 | except: |
50 | self.fail(msg='No Compatible image tab available') | 52 | self.fail(msg='No Compatible image tab available') |
53 | 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') | ||
51 | 54 | ||
52 | try: | 55 | try: |
53 | self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click() | 56 | 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') | 57 | self.wait_until_present('#filter-modal-softwarerecipestable') |
55 | except: | 58 | except: |
56 | self.fail(msg='No Compatible software recipe tab available') | 59 | self.fail(msg='No Compatible software recipe tab available') |
60 | 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') | ||
57 | 61 | ||
58 | try: | 62 | try: |
59 | self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click() | 63 | 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') | 64 | self.wait_until_present('#filter-modal-machinestable') |
61 | except: | 65 | except: |
62 | self.fail(msg='No Compatible machines tab available') | 66 | self.fail(msg='No Compatible machines tab available') |
67 | 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') | ||
63 | 68 | ||
64 | try: | 69 | try: |
65 | self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click() | 70 | 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') | 71 | self.wait_until_present('#filter-modal-layerstable') |
67 | except: | 72 | except: |
68 | self.fail(msg='No Compatible layers tab available') | 73 | self.fail(msg='No Compatible layers tab available') |
74 | 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') | ||
69 | 75 | ||
70 | try: | 76 | try: |
71 | self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click() | 77 | 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') | 78 | self.wait_until_present('#configvar-list') |
73 | except: | 79 | except: |
74 | self.fail(msg='No Bitbake variables tab available') | 80 | self.fail(msg='No Bitbake variables tab available') |
81 | 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') | ||
75 | 82 | ||
76 | # testcase (1516) | 83 | # testcase (1516) |
77 | def test_review_configuration_information(self): | 84 | def test_review_configuration_information(self): |
78 | self.get('') | 85 | self.get(reverse('all-projects')) |
79 | self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() | 86 | self.load_projects_page_helper() |
80 | self.wait_until_visible('#projectstable') | ||
81 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | 87 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() |
82 | project_URL=self.get_URL() | 88 | project_URL=self.get_URL() |
83 | 89 | ||
90 | # Machine section of page | ||
91 | self.wait_until_visible('#machine-section') | ||
92 | self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') | ||
93 | self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.XPATH, "//span[@id='project-machine-name']").text),'The machine type is not assigned') | ||
84 | try: | 94 | try: |
85 | self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') | 95 | self.driver.find_element(By.XPATH, "//span[@id='change-machine-toggle']").click() |
86 | self.assertTrue(re.search("qemux86",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() | ||
88 | self.wait_until_visible('#select-machine-form') | 96 | self.wait_until_visible('#select-machine-form') |
89 | self.wait_until_visible('#cancel-machine-change') | 97 | self.wait_until_visible('#cancel-machine-change') |
90 | self.driver.find_element_by_xpath("//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click() | 98 | self.driver.find_element(By.XPATH, "//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click() |
91 | except: | 99 | except: |
92 | self.fail(msg='The machine information is wrong in the configuration page') | 100 | self.fail(msg='The machine information is wrong in the configuration page') |
93 | 101 | ||
102 | # Most built recipes section | ||
103 | self.wait_until_visible('#no-most-built') | ||
94 | try: | 104 | try: |
95 | self.driver.find_element_by_id('no-most-built') | 105 | self.driver.find_element(By.ID, 'no-most-built') |
96 | except: | 106 | except: |
97 | self.fail(msg='No Most built information in project detail page') | 107 | self.fail(msg='No Most built information in project detail page') |
98 | 108 | ||
99 | try: | 109 | # Project Release title |
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') | 110 | 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 in the project detail page') |
101 | except: | ||
102 | self.fail(msg='No project release title information in project detail page') | ||
103 | 111 | ||
112 | # List of layers in project | ||
113 | self.wait_until_visible('#layer-container') | ||
114 | self.driver.find_element(By.XPATH, "//div[@id='layer-container']") | ||
115 | 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') | ||
104 | try: | 116 | try: |
105 | self.driver.find_element_by_xpath("//div[@id='layer-container']") | 117 | layer_list = self.driver.find_element(By.ID, "layers-in-project-list") |
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') | 118 | layers = layer_list.find_elements(By.TAG_NAME, "li") |
107 | layer_list = self.driver.find_element_by_id("layers-in-project-list") | ||
108 | layers = layer_list.find_elements_by_tag_name("li") | ||
109 | for layer in layers: | ||
110 | if re.match ("openembedded-core",layer.text): | ||
111 | print ("openembedded-core layer is a default layer in the project configuration") | ||
112 | elif re.match ("meta-poky",layer.text): | ||
113 | print ("meta-poky layer is a default layer in the project configuration") | ||
114 | elif re.match ("meta-yocto-bsp",layer.text): | ||
115 | print ("meta-yocto-bsp is a default layer in the project configuratoin") | ||
116 | else: | ||
117 | self.fail(msg='default layers are missing from the project configuration') | ||
118 | except: | 119 | except: |
119 | self.fail(msg='No Layer information in project detail page') | 120 | self.fail(msg='No Layer information in project detail page') |
120 | 121 | ||
122 | for layer in layers: | ||
123 | if re.match ("openembedded-core", layer.text): | ||
124 | print ("openembedded-core layer is a default layer in the project configuration") | ||
125 | elif re.match ("meta-poky", layer.text): | ||
126 | print ("meta-poky layer is a default layer in the project configuration") | ||
127 | elif re.match ("meta-yocto-bsp", layer.text): | ||
128 | print ("meta-yocto-bsp is a default layer in the project configuratoin") | ||
129 | else: | ||
130 | self.fail(msg='default layers are missing from the project configuration') | ||
131 | |||
121 | # testcase (1517) | 132 | # testcase (1517) |
122 | def test_verify_machine_information(self): | 133 | def test_verify_machine_information(self): |
123 | self.get('') | 134 | self.get(reverse('all-projects')) |
124 | self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() | 135 | self.load_projects_page_helper() |
125 | self.wait_until_visible('#projectstable') | ||
126 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | 136 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() |
127 | 137 | ||
138 | self.wait_until_visible('#machine-section') | ||
139 | self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') | ||
140 | self.wait_until_visible('#project-machine-name') | ||
141 | self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.ID, "project-machine-name").text),'The machine type is not assigned') | ||
128 | try: | 142 | try: |
129 | self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') | 143 | self.driver.find_element(By.ID, "change-machine-toggle").click() |
130 | self.assertTrue(re.search("qemux86",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() | ||
132 | self.wait_until_visible('#select-machine-form') | 144 | self.wait_until_visible('#select-machine-form') |
133 | self.wait_until_visible('#cancel-machine-change') | 145 | self.wait_until_visible('#cancel-machine-change') |
134 | self.driver.find_element_by_id("cancel-machine-change").click() | 146 | self.driver.find_element(By.ID, "cancel-machine-change").click() |
135 | except: | 147 | except: |
136 | self.fail(msg='The machine information is wrong in the configuration page') | 148 | self.fail(msg='The machine information is wrong in the configuration page') |
137 | 149 | ||
138 | # testcase (1518) | 150 | # testcase (1518) |
139 | def test_verify_most_built_recipes_information(self): | 151 | def test_verify_most_built_recipes_information(self): |
140 | self.get('') | 152 | self.get(reverse('all-projects')) |
141 | self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() | 153 | self.load_projects_page_helper() |
142 | self.wait_until_visible('#projectstable') | ||
143 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | 154 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() |
155 | self.wait_until_present('#config-nav') | ||
144 | project_URL=self.get_URL() | 156 | project_URL=self.get_URL() |
145 | 157 | ||
158 | self.wait_until_visible('#no-most-built') | ||
159 | 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') | ||
146 | try: | 160 | 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') | 161 | self.driver.find_element(By.XPATH, "//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click() |
148 | 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') | ||
150 | except: | 162 | except: |
151 | self.fail(msg='No Most built information in project detail page') | 163 | self.fail(msg='No Most built information in project detail page') |
164 | self.wait_until_visible('#config-nav') | ||
165 | 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') | ||
152 | 166 | ||
153 | # testcase (1519) | 167 | # testcase (1519) |
154 | def test_verify_project_release_information(self): | 168 | def test_verify_project_release_information(self): |
155 | self.get('') | 169 | self.get(reverse('all-projects')) |
156 | self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() | 170 | self.load_projects_page_helper() |
157 | self.wait_until_visible('#projectstable') | ||
158 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | 171 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() |
159 | 172 | self.wait_until_visible('#project-release-title') | |
160 | try: | 173 | self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.ID, "project-release-title").text), 'No project release title information in project detail page') |
161 | 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: | ||
163 | self.fail(msg='No project release title information in project detail page') | ||
164 | 174 | ||
165 | # testcase (1520) | 175 | # testcase (1520) |
166 | def test_verify_layer_information(self): | 176 | def test_verify_layer_information(self): |
167 | self.get('') | 177 | self.get(reverse('all-projects')) |
168 | self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() | 178 | self.load_projects_page_helper() |
169 | self.wait_until_visible('#projectstable') | ||
170 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | 179 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() |
180 | self.wait_until_present('#config-nav') | ||
171 | project_URL=self.get_URL() | 181 | project_URL=self.get_URL() |
182 | self.wait_until_visible('#layer-container') | ||
183 | self.driver.find_element(By.XPATH, "//div[@id='layer-container']") | ||
184 | self.wait_until_visible('#project-layers-count') | ||
185 | 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') | ||
172 | 186 | ||
173 | try: | 187 | try: |
174 | self.driver.find_element_by_xpath("//div[@id='layer-container']") | 188 | layer_list = self.driver.find_element(By.ID, "layers-in-project-list") |
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') | 189 | layers = layer_list.find_elements(By.TAG_NAME, "li") |
176 | layer_list = self.driver.find_element_by_id("layers-in-project-list") | 190 | except: |
177 | layers = layer_list.find_elements_by_tag_name("li") | 191 | self.fail(msg='No Layer information in project detail page') |
178 | 192 | ||
179 | for layer in layers: | 193 | for layer in layers: |
180 | if re.match ("openembedded-core",layer.text): | 194 | if re.match ("openembedded-core",layer.text): |
181 | print ("openembedded-core layer is a default layer in the project configuration") | 195 | print ("openembedded-core layer is a default layer in the project configuration") |
182 | elif re.match ("meta-poky",layer.text): | 196 | elif re.match ("meta-poky",layer.text): |
183 | print ("meta-poky layer is a default layer in the project configuration") | 197 | print ("meta-poky layer is a default layer in the project configuration") |
184 | elif re.match ("meta-yocto-bsp",layer.text): | 198 | elif re.match ("meta-yocto-bsp",layer.text): |
185 | print ("meta-yocto-bsp is a default layer in the project configuratoin") | 199 | print ("meta-yocto-bsp is a default layer in the project configuratoin") |
186 | else: | 200 | else: |
187 | self.fail(msg='default layers are missing from the project configuration') | 201 | self.fail(msg='default layers are missing from the project configuration') |
188 | 202 | ||
189 | self.driver.find_element_by_xpath("//input[@id='layer-add-input']") | 203 | try: |
190 | self.driver.find_element_by_xpath("//button[@id='add-layer-btn']") | 204 | self.driver.find_element(By.XPATH, "//input[@id='layer-add-input']") |
191 | self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']") | 205 | self.driver.find_element(By.XPATH, "//button[@id='add-layer-btn']") |
192 | self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]") | 206 | self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']") |
207 | self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]") | ||
193 | except: | 208 | except: |
194 | self.fail(msg='No Layer information in project detail page') | 209 | self.fail(msg='Layer configuration controls missing') |
195 | 210 | ||
196 | # testcase (1521) | 211 | # testcase (1521) |
197 | def test_verify_project_detail_links(self): | 212 | def test_verify_project_detail_links(self): |
198 | self.get('') | 213 | self.get(reverse('all-projects')) |
199 | self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() | 214 | self.load_projects_page_helper() |
200 | self.wait_until_visible('#projectstable') | ||
201 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | 215 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() |
216 | self.wait_until_present('#config-nav') | ||
202 | project_URL=self.get_URL() | 217 | project_URL=self.get_URL() |
218 | 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() | ||
219 | self.wait_until_visible('#topbar-configuration-tab') | ||
220 | 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') | ||
203 | 221 | ||
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() | 222 | try: |
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') | 223 | self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click() |
224 | except: | ||
225 | self.fail(msg='Builds tab information is not present') | ||
206 | 226 | ||
227 | self.wait_until_visible('#project-topbar') | ||
228 | 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') | ||
207 | try: | 229 | try: |
208 | self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click() | 230 | self.driver.find_element(By.XPATH, "//div[@id='empty-state-projectbuildstable']") |
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') | ||
210 | self.driver.find_element_by_xpath("//div[@id='empty-state-projectbuildstable']") | ||
211 | except: | 231 | except: |
212 | self.fail(msg='Builds tab information is not present') | 232 | self.fail(msg='Builds tab information is not present') |
213 | 233 | ||
214 | try: | 234 | try: |
215 | self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click() | 235 | 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') | ||
217 | self.driver.find_element_by_xpath("//fieldset[@id='repo-select']") | ||
218 | self.driver.find_element_by_xpath("//fieldset[@id='git-repo']") | ||
219 | except: | 236 | except: |
220 | self.fail(msg='Import layer tab not loading properly') | 237 | self.fail(msg='Import layer tab not loading properly') |
221 | 238 | ||
239 | self.wait_until_visible('#project-topbar') | ||
240 | 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') | ||
222 | try: | 241 | try: |
223 | self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click() | 242 | self.driver.find_element(By.XPATH, "//fieldset[@id='repo-select']") |
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') | 243 | self.driver.find_element(By.XPATH, "//fieldset[@id='git-repo']") |
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') | 244 | except: |
245 | self.fail(msg='Import layer tab not loading properly') | ||
246 | |||
247 | try: | ||
248 | self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click() | ||
226 | except: | 249 | except: |
227 | self.fail(msg='New custom image tab not loading properly') | 250 | self.fail(msg='New custom image tab not loading properly') |
228 | 251 | ||
252 | self.wait_until_visible('#project-topbar') | ||
253 | 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') | ||
254 | 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') | ||
255 | |||
229 | 256 | ||
230 | 257 | ||
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..fcb1bc3284 --- /dev/null +++ b/bitbake/lib/toaster/tests/functional/test_project_config.py | |||
@@ -0,0 +1,294 @@ | |||
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 pytest | ||
11 | from django.urls import reverse | ||
12 | from selenium.webdriver import Keys | ||
13 | from selenium.webdriver.support.select import Select | ||
14 | from selenium.common.exceptions import TimeoutException | ||
15 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
16 | from selenium.webdriver.common.by import By | ||
17 | |||
18 | from .utils import get_projectId_from_url | ||
19 | |||
20 | class TestProjectConfig(SeleniumFunctionalTestCase): | ||
21 | project_id = None | ||
22 | PROJECT_NAME = 'TestProjectConfig' | ||
23 | INVALID_PATH_START_TEXT = 'The directory path should either start with a /' | ||
24 | INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \ | ||
25 | 'any of these characters' | ||
26 | |||
27 | def _get_config_nav_item(self, index): | ||
28 | config_nav = self.find('#config-nav') | ||
29 | return config_nav.find_elements(By.TAG_NAME, 'li')[index] | ||
30 | |||
31 | def _navigate_bbv_page(self): | ||
32 | """ Navigate to project BitBake variables page """ | ||
33 | # check if the menu is displayed | ||
34 | if TestProjectConfig.project_id is None: | ||
35 | TestProjectConfig.project_id = self.create_new_project(self.PROJECT_NAME, '3', None, True) | ||
36 | |||
37 | url = reverse('projectconf', args=(TestProjectConfig.project_id,)) | ||
38 | self.get(url) | ||
39 | self.wait_until_visible('#config-nav') | ||
40 | bbv_page_link = self._get_config_nav_item(9) | ||
41 | bbv_page_link.click() | ||
42 | self.wait_until_visible('#config-nav') | ||
43 | |||
44 | def test_no_underscore_iamgefs_type(self): | ||
45 | """ | ||
46 | Should not accept IMAGEFS_TYPE with an underscore | ||
47 | """ | ||
48 | self._navigate_bbv_page() | ||
49 | imagefs_type = "foo_bar" | ||
50 | |||
51 | self.wait_until_visible('#change-image_fstypes-icon') | ||
52 | |||
53 | self.click('#change-image_fstypes-icon') | ||
54 | |||
55 | self.enter_text('#new-imagefs_types', imagefs_type) | ||
56 | |||
57 | element = self.wait_until_visible('#hintError-image-fs_type') | ||
58 | |||
59 | self.assertTrue(("A valid image type cannot include underscores" in element.text), | ||
60 | "Did not find underscore error message") | ||
61 | |||
62 | def test_checkbox_verification(self): | ||
63 | """ | ||
64 | Should automatically check the checkbox if user enters value | ||
65 | text box, if value is there in the checkbox. | ||
66 | """ | ||
67 | self._navigate_bbv_page() | ||
68 | |||
69 | imagefs_type = "btrfs" | ||
70 | |||
71 | self.wait_until_visible('#change-image_fstypes-icon') | ||
72 | |||
73 | self.click('#change-image_fstypes-icon') | ||
74 | |||
75 | self.enter_text('#new-imagefs_types', imagefs_type) | ||
76 | |||
77 | checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']") | ||
78 | |||
79 | for checkbox in checkboxes: | ||
80 | if checkbox.get_attribute("value") == "btrfs": | ||
81 | self.assertEqual(checkbox.is_selected(), True) | ||
82 | |||
83 | def test_textbox_with_checkbox_verification(self): | ||
84 | """ | ||
85 | Should automatically add or remove value in textbox, if user checks | ||
86 | or unchecks checkboxes. | ||
87 | """ | ||
88 | self._navigate_bbv_page() | ||
89 | |||
90 | self.wait_until_visible('#change-image_fstypes-icon') | ||
91 | self.click('#change-image_fstypes-icon') | ||
92 | |||
93 | checkboxes_selector = '.fs-checkbox-fstypes' | ||
94 | |||
95 | self.wait_until_visible(checkboxes_selector) | ||
96 | checkboxes = self.find_all(checkboxes_selector) | ||
97 | |||
98 | for checkbox in checkboxes: | ||
99 | if checkbox.get_attribute("value") == "cpio": | ||
100 | checkbox.click() | ||
101 | self.wait_until_visible('#new-imagefs_types') | ||
102 | element = self.driver.find_element(By.ID, 'new-imagefs_types') | ||
103 | |||
104 | self.assertTrue(("cpio" in element.get_attribute('value'), | ||
105 | "Imagefs not added into the textbox")) | ||
106 | checkbox.click() | ||
107 | self.assertTrue(("cpio" not in element.text), | ||
108 | "Image still present in the textbox") | ||
109 | |||
110 | def test_set_download_dir(self): | ||
111 | """ | ||
112 | Validate the allowed and disallowed types in the directory field for | ||
113 | DL_DIR | ||
114 | """ | ||
115 | self._navigate_bbv_page() | ||
116 | |||
117 | # activate the input to edit download dir | ||
118 | try: | ||
119 | change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon') | ||
120 | except TimeoutException: | ||
121 | # If download dir is not displayed, test is skipped | ||
122 | change_dl_dir_btn = None | ||
123 | |||
124 | if change_dl_dir_btn: | ||
125 | change_dl_dir_btn.click() | ||
126 | |||
127 | # downloads dir path doesn't start with / or ${...} | ||
128 | input_field = self.wait_until_visible('#new-dl_dir') | ||
129 | input_field.clear() | ||
130 | self.enter_text('#new-dl_dir', 'home/foo') | ||
131 | element = self.wait_until_visible('#hintError-initialChar-dl_dir') | ||
132 | |||
133 | msg = 'downloads directory path starts with invalid character but ' \ | ||
134 | 'treated as valid' | ||
135 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) | ||
136 | |||
137 | # downloads dir path has a space | ||
138 | self.driver.find_element(By.ID, 'new-dl_dir').clear() | ||
139 | self.enter_text('#new-dl_dir', '/foo/bar a') | ||
140 | |||
141 | element = self.wait_until_visible('#hintError-dl_dir') | ||
142 | msg = 'downloads directory path characters invalid but treated as valid' | ||
143 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
144 | |||
145 | # downloads dir path starts with ${...} but has a space | ||
146 | self.driver.find_element(By.ID,'new-dl_dir').clear() | ||
147 | self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') | ||
148 | |||
149 | element = self.wait_until_visible('#hintError-dl_dir') | ||
150 | msg = 'downloads directory path characters invalid but treated as valid' | ||
151 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
152 | |||
153 | # downloads dir path starts with / | ||
154 | self.driver.find_element(By.ID,'new-dl_dir').clear() | ||
155 | self.enter_text('#new-dl_dir', '/bar/foo') | ||
156 | |||
157 | hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') | ||
158 | self.assertEqual(hidden_element.is_displayed(), False, | ||
159 | 'downloads directory path valid but treated as invalid') | ||
160 | |||
161 | # downloads dir path starts with ${...} | ||
162 | self.driver.find_element(By.ID,'new-dl_dir').clear() | ||
163 | self.enter_text('#new-dl_dir', '${TOPDIR}/down') | ||
164 | |||
165 | hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') | ||
166 | self.assertEqual(hidden_element.is_displayed(), False, | ||
167 | 'downloads directory path valid but treated as invalid') | ||
168 | |||
169 | def test_set_sstate_dir(self): | ||
170 | """ | ||
171 | Validate the allowed and disallowed types in the directory field for | ||
172 | SSTATE_DIR | ||
173 | """ | ||
174 | self._navigate_bbv_page() | ||
175 | |||
176 | try: | ||
177 | btn_chg_sstate_dir = self.wait_until_visible('#change-sstate_dir-icon') | ||
178 | self.click('#change-sstate_dir-icon') | ||
179 | except TimeoutException: | ||
180 | # If sstate_dir is not displayed, test is skipped | ||
181 | btn_chg_sstate_dir = None | ||
182 | |||
183 | if btn_chg_sstate_dir: # Skip continuation if sstate_dir is not displayed | ||
184 | # path doesn't start with / or ${...} | ||
185 | input_field = self.wait_until_visible('#new-sstate_dir') | ||
186 | input_field.clear() | ||
187 | self.enter_text('#new-sstate_dir', 'home/foo') | ||
188 | element = self.wait_until_visible('#hintError-initialChar-sstate_dir') | ||
189 | |||
190 | msg = 'sstate directory path starts with invalid character but ' \ | ||
191 | 'treated as valid' | ||
192 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) | ||
193 | |||
194 | # path has a space | ||
195 | self.driver.find_element(By.ID, 'new-sstate_dir').clear() | ||
196 | self.enter_text('#new-sstate_dir', '/foo/bar a') | ||
197 | |||
198 | element = self.wait_until_visible('#hintError-sstate_dir') | ||
199 | msg = 'sstate directory path characters invalid but treated as valid' | ||
200 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
201 | |||
202 | # path starts with ${...} but has a space | ||
203 | self.driver.find_element(By.ID,'new-sstate_dir').clear() | ||
204 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') | ||
205 | |||
206 | element = self.wait_until_visible('#hintError-sstate_dir') | ||
207 | msg = 'sstate directory path characters invalid but treated as valid' | ||
208 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
209 | |||
210 | # path starts with / | ||
211 | self.driver.find_element(By.ID,'new-sstate_dir').clear() | ||
212 | self.enter_text('#new-sstate_dir', '/bar/foo') | ||
213 | |||
214 | hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') | ||
215 | self.assertEqual(hidden_element.is_displayed(), False, | ||
216 | 'sstate directory path valid but treated as invalid') | ||
217 | |||
218 | # paths starts with ${...} | ||
219 | self.driver.find_element(By.ID, 'new-sstate_dir').clear() | ||
220 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down') | ||
221 | |||
222 | hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') | ||
223 | self.assertEqual(hidden_element.is_displayed(), False, | ||
224 | 'sstate directory path valid but treated as invalid') | ||
225 | |||
226 | def _change_bbv_value(self, **kwargs): | ||
227 | var_name, field, btn_id, input_id, value, save_btn, *_ = kwargs.values() | ||
228 | """ Change bitbake variable value """ | ||
229 | self._navigate_bbv_page() | ||
230 | self.wait_until_visible(f'#{btn_id}') | ||
231 | if kwargs.get('new_variable'): | ||
232 | self.find(f"#{btn_id}").clear() | ||
233 | self.enter_text(f"#{btn_id}", f"{var_name}") | ||
234 | else: | ||
235 | self.click(f'#{btn_id}') | ||
236 | |||
237 | self.wait_until_visible(f'#{input_id}') | ||
238 | |||
239 | if kwargs.get('is_select'): | ||
240 | select = Select(self.find(f'#{input_id}')) | ||
241 | select.select_by_visible_text(value) | ||
242 | else: | ||
243 | self.find(f"#{input_id}").clear() | ||
244 | self.enter_text(f'#{input_id}', f'{value}') | ||
245 | self.click(f'#{save_btn}') | ||
246 | value_displayed = str(self.wait_until_visible(f'#{field}').text).lower() | ||
247 | msg = f'{var_name} variable not changed' | ||
248 | self.assertTrue(str(value).lower() in value_displayed, msg) | ||
249 | |||
250 | def test_change_distro_var(self): | ||
251 | """ Test changing distro variable """ | ||
252 | self._change_bbv_value( | ||
253 | var_name='DISTRO', | ||
254 | field='distro', | ||
255 | btn_id='change-distro-icon', | ||
256 | input_id='new-distro', | ||
257 | value='poky-changed', | ||
258 | save_btn="apply-change-distro", | ||
259 | ) | ||
260 | |||
261 | def test_set_image_install_append_var(self): | ||
262 | """ Test setting IMAGE_INSTALL:append variable """ | ||
263 | self._change_bbv_value( | ||
264 | var_name='IMAGE_INSTALL:append', | ||
265 | field='image_install', | ||
266 | btn_id='change-image_install-icon', | ||
267 | input_id='new-image_install', | ||
268 | value='bash, apt, busybox', | ||
269 | save_btn="apply-change-image_install", | ||
270 | ) | ||
271 | |||
272 | def test_set_package_classes_var(self): | ||
273 | """ Test setting PACKAGE_CLASSES variable """ | ||
274 | self._change_bbv_value( | ||
275 | var_name='PACKAGE_CLASSES', | ||
276 | field='package_classes', | ||
277 | btn_id='change-package_classes-icon', | ||
278 | input_id='package_classes-select', | ||
279 | value='package_deb', | ||
280 | save_btn="apply-change-package_classes", | ||
281 | is_select=True, | ||
282 | ) | ||
283 | |||
284 | def test_create_new_bbv(self): | ||
285 | """ Test creating new bitbake variable """ | ||
286 | self._change_bbv_value( | ||
287 | var_name='New_Custom_Variable', | ||
288 | field='configvar-list', | ||
289 | btn_id='variable', | ||
290 | input_id='value', | ||
291 | value='new variable value', | ||
292 | save_btn="add-configvar-button", | ||
293 | new_variable=True | ||
294 | ) | ||
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..429d86feba --- /dev/null +++ b/bitbake/lib/toaster/tests/functional/test_project_page.py | |||
@@ -0,0 +1,775 @@ | |||
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 string | ||
11 | import time | ||
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 | class TestProjectPageBase(SeleniumFunctionalTestCase): | ||
26 | project_id = None | ||
27 | PROJECT_NAME = 'TestProjectPage' | ||
28 | |||
29 | def _navigate_to_project_page(self): | ||
30 | # Navigate to project page | ||
31 | if TestProjectPageBase.project_id is None: | ||
32 | TestProjectPageBase.project_id = self.create_new_project(self.PROJECT_NAME, '3', None, True) | ||
33 | |||
34 | url = reverse('project', args=(TestProjectPageBase.project_id,)) | ||
35 | self.get(url) | ||
36 | self.wait_until_visible('#config-nav') | ||
37 | |||
38 | def _get_create_builds(self, **kwargs): | ||
39 | """ Create a build and return the build object """ | ||
40 | # parameters for builds to associate with the projects | ||
41 | now = timezone.now() | ||
42 | self.project1_build_success = { | ||
43 | 'project': Project.objects.get(id=TestProjectPageBase.project_id), | ||
44 | 'started_on': now, | ||
45 | 'completed_on': now, | ||
46 | 'outcome': Build.SUCCEEDED | ||
47 | } | ||
48 | |||
49 | self.project1_build_failure = { | ||
50 | 'project': Project.objects.get(id=TestProjectPageBase.project_id), | ||
51 | 'started_on': now, | ||
52 | 'completed_on': now, | ||
53 | 'outcome': Build.FAILED | ||
54 | } | ||
55 | build1 = Build.objects.create(**self.project1_build_success) | ||
56 | build2 = Build.objects.create(**self.project1_build_failure) | ||
57 | |||
58 | # add some targets to these builds so they have recipe links | ||
59 | # (and so we can find the row in the ToasterTable corresponding to | ||
60 | # a particular build) | ||
61 | Target.objects.create(build=build1, target='foo') | ||
62 | Target.objects.create(build=build2, target='bar') | ||
63 | |||
64 | if kwargs: | ||
65 | # Create kwargs.get('success') builds with success status with target | ||
66 | # and kwargs.get('failure') builds with failure status with target | ||
67 | for i in range(kwargs.get('success', 0)): | ||
68 | now = timezone.now() | ||
69 | self.project1_build_success['started_on'] = now | ||
70 | self.project1_build_success[ | ||
71 | 'completed_on'] = now - timezone.timedelta(days=i) | ||
72 | build = Build.objects.create(**self.project1_build_success) | ||
73 | Target.objects.create(build=build, | ||
74 | target=f'{i}_success_recipe', | ||
75 | task=f'{i}_success_task') | ||
76 | |||
77 | for i in range(kwargs.get('failure', 0)): | ||
78 | now = timezone.now() | ||
79 | self.project1_build_failure['started_on'] = now | ||
80 | self.project1_build_failure[ | ||
81 | 'completed_on'] = now - timezone.timedelta(days=i) | ||
82 | build = Build.objects.create(**self.project1_build_failure) | ||
83 | Target.objects.create(build=build, | ||
84 | target=f'{i}_fail_recipe', | ||
85 | task=f'{i}_fail_task') | ||
86 | return build1, build2 | ||
87 | |||
88 | def _mixin_test_table_edit_column( | ||
89 | self, | ||
90 | table_id, | ||
91 | edit_btn_id, | ||
92 | list_check_box_id: list | ||
93 | ): | ||
94 | # Check edit column | ||
95 | finder = lambda driver: self.find(f'#{edit_btn_id}') | ||
96 | edit_column = self.wait_until_element_clickable(finder) | ||
97 | self.assertTrue(edit_column.is_displayed()) | ||
98 | edit_column.click() | ||
99 | # Check dropdown is visible | ||
100 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
101 | for check_box_id in list_check_box_id: | ||
102 | # Check that we can hide/show table column | ||
103 | check_box = self.find(f'#{check_box_id}') | ||
104 | th_class = str(check_box_id).replace('checkbox-', '') | ||
105 | if check_box.is_selected(): | ||
106 | # check if column is visible in table | ||
107 | self.assertTrue( | ||
108 | self.find( | ||
109 | f'#{table_id} thead th.{th_class}' | ||
110 | ).is_displayed(), | ||
111 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
112 | ) | ||
113 | check_box.click() | ||
114 | # check if column is hidden in table | ||
115 | self.assertFalse( | ||
116 | self.find( | ||
117 | f'#{table_id} thead th.{th_class}' | ||
118 | ).is_displayed(), | ||
119 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
120 | ) | ||
121 | else: | ||
122 | # check if column is hidden in table | ||
123 | self.assertFalse( | ||
124 | self.find( | ||
125 | f'#{table_id} thead th.{th_class}' | ||
126 | ).is_displayed(), | ||
127 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
128 | ) | ||
129 | check_box.click() | ||
130 | # check if column is visible in table | ||
131 | self.assertTrue( | ||
132 | self.find( | ||
133 | f'#{table_id} thead th.{th_class}' | ||
134 | ).is_displayed(), | ||
135 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
136 | ) | ||
137 | |||
138 | def _get_config_nav_item(self, index): | ||
139 | config_nav = self.find('#config-nav') | ||
140 | return config_nav.find_elements(By.TAG_NAME, 'li')[index] | ||
141 | |||
142 | def _navigate_to_config_nav(self, nav_id, nav_index): | ||
143 | # navigate to the project page | ||
144 | self._navigate_to_project_page() | ||
145 | # click on "Software recipe" tab | ||
146 | soft_recipe = self._get_config_nav_item(nav_index) | ||
147 | soft_recipe.click() | ||
148 | self.wait_until_visible(f'#{nav_id}') | ||
149 | |||
150 | def _mixin_test_table_show_rows(self, table_selector, **kwargs): | ||
151 | """ Test the show rows feature in the builds table on the all builds page """ | ||
152 | def test_show_rows(row_to_show, show_row_link): | ||
153 | # Check that we can show rows == row_to_show | ||
154 | show_row_link.select_by_value(str(row_to_show)) | ||
155 | self.wait_until_visible(f'#{table_selector} tbody tr') | ||
156 | # check at least some rows are visible | ||
157 | self.assertTrue( | ||
158 | len(self.find_all(f'#{table_selector} tbody tr')) > 0 | ||
159 | ) | ||
160 | self.wait_until_present(f'#{table_selector} tbody tr') | ||
161 | show_rows = self.driver.find_elements( | ||
162 | By.XPATH, | ||
163 | f'//select[@class="form-control pagesize-{table_selector}"]' | ||
164 | ) | ||
165 | rows_to_show = [10, 25, 50, 100, 150] | ||
166 | to_skip = kwargs.get('to_skip', []) | ||
167 | # Check show rows | ||
168 | for show_row_link in show_rows: | ||
169 | show_row_link = Select(show_row_link) | ||
170 | for row_to_show in rows_to_show: | ||
171 | if row_to_show not in to_skip: | ||
172 | test_show_rows(row_to_show, show_row_link) | ||
173 | |||
174 | def _mixin_test_table_search_input(self, **kwargs): | ||
175 | input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values() | ||
176 | # Test search input | ||
177 | self.wait_until_visible(f'#{input_selector}') | ||
178 | recipe_input = self.find(f'#{input_selector}') | ||
179 | recipe_input.send_keys(input_text) | ||
180 | self.find(f'#{searchBtn_selector}').click() | ||
181 | self.wait_until_visible(f'#{table_selector} tbody tr') | ||
182 | rows = self.find_all(f'#{table_selector} tbody tr') | ||
183 | self.assertTrue(len(rows) > 0) | ||
184 | |||
185 | class TestProjectPage(TestProjectPageBase): | ||
186 | |||
187 | def test_page_header_on_project_page(self): | ||
188 | """ Check page header in project page: | ||
189 | - AT LEFT -> Logo of Yocto project, displayed, clickable | ||
190 | - "Toaster"+" Information icon", displayed, clickable | ||
191 | - "Server Icon" + "All builds", displayed, clickable | ||
192 | - "Directory Icon" + "All projects", displayed, clickable | ||
193 | - "Book Icon" + "Documentation", displayed, clickable | ||
194 | - AT RIGHT -> button "New project", displayed, clickable | ||
195 | """ | ||
196 | # navigate to the project page | ||
197 | self._navigate_to_project_page() | ||
198 | |||
199 | # check page header | ||
200 | # AT LEFT -> Logo of Yocto project | ||
201 | logo = self.driver.find_element( | ||
202 | By.XPATH, | ||
203 | "//div[@class='toaster-navbar-brand']", | ||
204 | ) | ||
205 | logo_img = logo.find_element(By.TAG_NAME, 'img') | ||
206 | self.assertTrue(logo_img.is_displayed(), | ||
207 | 'Logo of Yocto project not found') | ||
208 | self.assertIn( | ||
209 | '/static/img/logo.png', str(logo_img.get_attribute('src')), | ||
210 | 'Logo of Yocto project not found' | ||
211 | ) | ||
212 | # "Toaster"+" Information icon", clickable | ||
213 | toaster = self.driver.find_element( | ||
214 | By.XPATH, | ||
215 | "//div[@class='toaster-navbar-brand']//a[@class='brand']", | ||
216 | ) | ||
217 | self.assertTrue(toaster.is_displayed(), 'Toaster not found') | ||
218 | self.assertEqual(toaster.text, 'Toaster') | ||
219 | info_sign = self.find('.glyphicon-info-sign') | ||
220 | self.assertTrue(info_sign.is_displayed()) | ||
221 | |||
222 | # "Server Icon" + "All builds" | ||
223 | all_builds = self.find('#navbar-all-builds') | ||
224 | all_builds_link = all_builds.find_element(By.TAG_NAME, 'a') | ||
225 | self.assertIn("All builds", all_builds_link.text) | ||
226 | self.assertIn( | ||
227 | '/toastergui/builds/', str(all_builds_link.get_attribute('href')) | ||
228 | ) | ||
229 | server_icon = all_builds.find_element(By.TAG_NAME, 'i') | ||
230 | self.assertEqual( | ||
231 | server_icon.get_attribute('class'), 'glyphicon glyphicon-tasks' | ||
232 | ) | ||
233 | self.assertTrue(server_icon.is_displayed()) | ||
234 | |||
235 | # "Directory Icon" + "All projects" | ||
236 | all_projects = self.find('#navbar-all-projects') | ||
237 | all_projects_link = all_projects.find_element(By.TAG_NAME, 'a') | ||
238 | self.assertIn("All projects", all_projects_link.text) | ||
239 | self.assertIn( | ||
240 | '/toastergui/projects/', str(all_projects_link.get_attribute( | ||
241 | 'href')) | ||
242 | ) | ||
243 | dir_icon = all_projects.find_element(By.TAG_NAME, 'i') | ||
244 | self.assertEqual( | ||
245 | dir_icon.get_attribute('class'), 'icon-folder-open' | ||
246 | ) | ||
247 | self.assertTrue(dir_icon.is_displayed()) | ||
248 | |||
249 | # "Book Icon" + "Documentation" | ||
250 | toaster_docs_link = self.find('#navbar-docs') | ||
251 | toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME, | ||
252 | 'a') | ||
253 | self.assertIn("Documentation", toaster_docs_link_link.text) | ||
254 | self.assertEqual( | ||
255 | toaster_docs_link_link.get_attribute('href'), 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual' | ||
256 | ) | ||
257 | book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i') | ||
258 | self.assertEqual( | ||
259 | book_icon.get_attribute('class'), 'glyphicon glyphicon-book' | ||
260 | ) | ||
261 | self.assertTrue(book_icon.is_displayed()) | ||
262 | |||
263 | # AT RIGHT -> button "New project" | ||
264 | new_project_button = self.find('#new-project-button') | ||
265 | self.assertTrue(new_project_button.is_displayed()) | ||
266 | self.assertEqual(new_project_button.text, 'New project') | ||
267 | new_project_button.click() | ||
268 | self.assertIn( | ||
269 | '/toastergui/newproject/', str(self.driver.current_url) | ||
270 | ) | ||
271 | |||
272 | def test_edit_project_name(self): | ||
273 | """ Test edit project name: | ||
274 | - Click on "Edit" icon button | ||
275 | - Change project name | ||
276 | - Click on "Save" button | ||
277 | - Check project name is changed | ||
278 | """ | ||
279 | # navigate to the project page | ||
280 | self._navigate_to_project_page() | ||
281 | |||
282 | # click on "Edit" icon button | ||
283 | self.wait_until_visible('#project-name-container') | ||
284 | finder = lambda driver: self.find('#project-change-form-toggle') | ||
285 | edit_button = self.wait_until_element_clickable(finder) | ||
286 | edit_button.click() | ||
287 | project_name_input = self.find('#project-name-change-input') | ||
288 | self.assertTrue(project_name_input.is_displayed()) | ||
289 | project_name_input.clear() | ||
290 | project_name_input.send_keys('New Name') | ||
291 | self.find('#project-name-change-btn').click() | ||
292 | |||
293 | # check project name is changed | ||
294 | self.wait_until_visible('#project-name-container') | ||
295 | self.assertIn( | ||
296 | 'New Name', str(self.find('#project-name-container').text) | ||
297 | ) | ||
298 | |||
299 | def test_project_page_tabs(self): | ||
300 | """ Test project tabs: | ||
301 | - "configuration" tab | ||
302 | - "Builds" tab | ||
303 | - "Import layers" tab | ||
304 | - "New custom image" tab | ||
305 | Check search box used to build recipes | ||
306 | """ | ||
307 | # navigate to the project page | ||
308 | self._navigate_to_project_page() | ||
309 | |||
310 | # check "configuration" tab | ||
311 | self.wait_until_visible('#topbar-configuration-tab') | ||
312 | config_tab = self.find('#topbar-configuration-tab') | ||
313 | self.assertEqual(config_tab.get_attribute('class'), 'active') | ||
314 | self.assertIn('Configuration', str(config_tab.text)) | ||
315 | self.assertIn( | ||
316 | f"/toastergui/project/{TestProjectPageBase.project_id}", str(self.driver.current_url) | ||
317 | ) | ||
318 | |||
319 | def get_tabs(): | ||
320 | # tabs links list | ||
321 | return self.driver.find_elements( | ||
322 | By.XPATH, | ||
323 | '//div[@id="project-topbar"]//li' | ||
324 | ) | ||
325 | |||
326 | def check_tab_link(tab_index, tab_name, url): | ||
327 | tab = get_tabs()[tab_index] | ||
328 | tab_link = tab.find_element(By.TAG_NAME, 'a') | ||
329 | self.assertIn(url, tab_link.get_attribute('href')) | ||
330 | self.assertIn(tab_name, tab_link.text) | ||
331 | self.assertEqual(tab.get_attribute('class'), 'active') | ||
332 | |||
333 | # check "Builds" tab | ||
334 | builds_tab = get_tabs()[1] | ||
335 | builds_tab.find_element(By.TAG_NAME, 'a').click() | ||
336 | check_tab_link( | ||
337 | 1, | ||
338 | 'Builds', | ||
339 | f"/toastergui/project/{TestProjectPageBase.project_id}/builds" | ||
340 | ) | ||
341 | |||
342 | # check "Import layers" tab | ||
343 | import_layers_tab = get_tabs()[2] | ||
344 | import_layers_tab.find_element(By.TAG_NAME, 'a').click() | ||
345 | check_tab_link( | ||
346 | 2, | ||
347 | 'Import layer', | ||
348 | f"/toastergui/project/{TestProjectPageBase.project_id}/importlayer" | ||
349 | ) | ||
350 | |||
351 | # check "New custom image" tab | ||
352 | new_custom_image_tab = get_tabs()[3] | ||
353 | new_custom_image_tab.find_element(By.TAG_NAME, 'a').click() | ||
354 | check_tab_link( | ||
355 | 3, | ||
356 | 'New custom image', | ||
357 | f"/toastergui/project/{TestProjectPageBase.project_id}/newcustomimage" | ||
358 | ) | ||
359 | |||
360 | # check search box can be use to build recipes | ||
361 | search_box = self.find('#build-input') | ||
362 | search_box.send_keys('core-image-minimal') | ||
363 | self.find('#build-button').click() | ||
364 | self.wait_until_visible('#latest-builds') | ||
365 | buildtext = "Loading" | ||
366 | while "Loading" in buildtext: | ||
367 | time.sleep(1) | ||
368 | lastest_builds = self.driver.find_elements( | ||
369 | By.XPATH, | ||
370 | '//div[@id="latest-builds"]', | ||
371 | ) | ||
372 | last_build = lastest_builds[0] | ||
373 | buildtext = last_build.text | ||
374 | self.assertIn( | ||
375 | 'core-image-minimal', str(last_build.text) | ||
376 | ) | ||
377 | |||
378 | def test_softwareRecipe_page(self): | ||
379 | """ Test software recipe page | ||
380 | - Check title "Compatible software recipes" is displayed | ||
381 | - Check search input | ||
382 | - Check "build recipe" button works | ||
383 | - Check software recipe table feature(show/hide column, pagination) | ||
384 | """ | ||
385 | self._navigate_to_config_nav('softwarerecipestable', 4) | ||
386 | # check title "Compatible software recipes" is displayed | ||
387 | self.assertIn("Compatible software recipes", self.get_page_source()) | ||
388 | # Test search input | ||
389 | self._mixin_test_table_search_input( | ||
390 | input_selector='search-input-softwarerecipestable', | ||
391 | input_text='busybox', | ||
392 | searchBtn_selector='search-submit-softwarerecipestable', | ||
393 | table_selector='softwarerecipestable' | ||
394 | ) | ||
395 | # check "build recipe" button works | ||
396 | finder = lambda driver: self.find_all('#softwarerecipestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a') | ||
397 | build_btn = self.wait_until_element_clickable(finder) | ||
398 | build_btn.click() | ||
399 | build_state = wait_until_build(self, 'queued cloning starting parsing failed') | ||
400 | lastest_builds = self.driver.find_elements( | ||
401 | By.XPATH, | ||
402 | '//div[@id="latest-builds"]/div' | ||
403 | ) | ||
404 | self.assertTrue(len(lastest_builds) > 0) | ||
405 | # Find the latest builds, the last build and then the cancel button | ||
406 | |||
407 | finder = lambda driver: driver.find_elements(By.XPATH, '//div[@id="latest-builds"]/div')[0].find_element(By.XPATH, '//span[@class="cancel-build-btn pull-right alert-link"]') | ||
408 | cancel_button = self.wait_until_element_clickable(finder) | ||
409 | cancel_button.click() | ||
410 | if 'starting' not in build_state: # change build state when cancelled in starting state | ||
411 | wait_until_build_cancelled(self) | ||
412 | |||
413 | # check software recipe table feature(show/hide column, pagination) | ||
414 | self._navigate_to_config_nav('softwarerecipestable', 4) | ||
415 | column_list = [ | ||
416 | 'get_description_or_summary', | ||
417 | 'layer_version__get_vcs_reference', | ||
418 | 'layer_version__layer__name', | ||
419 | 'license', | ||
420 | 'recipe-file', | ||
421 | 'section', | ||
422 | 'version', | ||
423 | ] | ||
424 | self._mixin_test_table_edit_column( | ||
425 | 'softwarerecipestable', | ||
426 | 'edit-columns-button', | ||
427 | [f'checkbox-{column}' for column in column_list] | ||
428 | ) | ||
429 | self._navigate_to_config_nav('softwarerecipestable', 4) | ||
430 | # check show rows(pagination) | ||
431 | self._mixin_test_table_show_rows( | ||
432 | table_selector='softwarerecipestable', | ||
433 | to_skip=[150], | ||
434 | ) | ||
435 | |||
436 | def test_machines_page(self): | ||
437 | """ Test Machine page | ||
438 | - Check if title "Compatible machines" is displayed | ||
439 | - Check search input | ||
440 | - Check "Select machine" button works | ||
441 | - Check "Add layer" button works | ||
442 | - Check Machine table feature(show/hide column, pagination) | ||
443 | """ | ||
444 | self._navigate_to_config_nav('machinestable', 5) | ||
445 | # check title "Compatible software recipes" is displayed | ||
446 | self.assertIn("Compatible machines", self.get_page_source()) | ||
447 | # Test search input | ||
448 | self._mixin_test_table_search_input( | ||
449 | input_selector='search-input-machinestable', | ||
450 | input_text='qemux86-64', | ||
451 | searchBtn_selector='search-submit-machinestable', | ||
452 | table_selector='machinestable' | ||
453 | ) | ||
454 | # check "Select machine" button works | ||
455 | finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]') | ||
456 | select_btn = self.wait_until_element_clickable(finder) | ||
457 | select_btn.click() | ||
458 | self.wait_until_visible('#project-machine-name') | ||
459 | project_machine_name = self.find('#project-machine-name') | ||
460 | self.assertIn( | ||
461 | 'qemux86-64', project_machine_name.text | ||
462 | ) | ||
463 | # check "Add layer" button works | ||
464 | self._navigate_to_config_nav('machinestable', 5) | ||
465 | # Search for a machine whit layer not in project | ||
466 | self._mixin_test_table_search_input( | ||
467 | input_selector='search-input-machinestable', | ||
468 | input_text='qemux86-64-tpm2', | ||
469 | searchBtn_selector='search-submit-machinestable', | ||
470 | table_selector='machinestable' | ||
471 | ) | ||
472 | |||
473 | self.wait_until_visible('#machinestable tbody tr') | ||
474 | # Locate a machine to add button | ||
475 | finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]') | ||
476 | add_btn = self.wait_until_element_clickable(finder) | ||
477 | add_btn.click() | ||
478 | self.wait_until_visible('#change-notification') | ||
479 | change_notification = self.find('#change-notification') | ||
480 | self.assertIn( | ||
481 | f'You have added 1 layer to your project', str(change_notification.text) | ||
482 | ) | ||
483 | |||
484 | finder = lambda driver: self.find('#hide-alert') | ||
485 | hide_button = self.wait_until_element_clickable(finder) | ||
486 | hide_button.click() | ||
487 | self.wait_until_not_visible('#change-notification') | ||
488 | |||
489 | # check Machine table feature(show/hide column, pagination) | ||
490 | self._navigate_to_config_nav('machinestable', 5) | ||
491 | column_list = [ | ||
492 | 'description', | ||
493 | 'layer_version__get_vcs_reference', | ||
494 | 'layer_version__layer__name', | ||
495 | 'machinefile', | ||
496 | ] | ||
497 | self._mixin_test_table_edit_column( | ||
498 | 'machinestable', | ||
499 | 'edit-columns-button', | ||
500 | [f'checkbox-{column}' for column in column_list] | ||
501 | ) | ||
502 | self._navigate_to_config_nav('machinestable', 5) | ||
503 | # check show rows(pagination) | ||
504 | self._mixin_test_table_show_rows( | ||
505 | table_selector='machinestable', | ||
506 | to_skip=[150], | ||
507 | ) | ||
508 | |||
509 | def test_layers_page(self): | ||
510 | """ Test layers page | ||
511 | - Check if title "Compatible layerss" is displayed | ||
512 | - Check search input | ||
513 | - Check "Add layer" button works | ||
514 | - Check "Remove layer" button works | ||
515 | - Check layers table feature(show/hide column, pagination) | ||
516 | """ | ||
517 | self._navigate_to_config_nav('layerstable', 6) | ||
518 | # check title "Compatible layers" is displayed | ||
519 | self.assertIn("Compatible layers", self.get_page_source()) | ||
520 | # Test search input | ||
521 | input_text='meta-tanowrt' | ||
522 | self._mixin_test_table_search_input( | ||
523 | input_selector='search-input-layerstable', | ||
524 | input_text=input_text, | ||
525 | searchBtn_selector='search-submit-layerstable', | ||
526 | table_selector='layerstable' | ||
527 | ) | ||
528 | # check "Add layer" button works | ||
529 | self.wait_until_visible('#layerstable tbody tr') | ||
530 | finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="add"]') | ||
531 | add_btn = self.wait_until_element_clickable(finder) | ||
532 | add_btn.click() | ||
533 | # check modal is displayed | ||
534 | self.wait_until_visible('#dependencies-modal') | ||
535 | list_dependencies = self.find_all('#dependencies-list li') | ||
536 | # click on add-layers button | ||
537 | finder = lambda driver: self.driver.find_element(By.XPATH, '//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]') | ||
538 | add_layers_btn = self.wait_until_element_clickable(finder) | ||
539 | add_layers_btn.click() | ||
540 | self.wait_until_visible('#change-notification') | ||
541 | change_notification = self.find('#change-notification') | ||
542 | self.assertIn( | ||
543 | f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies', str(change_notification.text) | ||
544 | ) | ||
545 | |||
546 | finder = lambda driver: self.find('#hide-alert') | ||
547 | hide_button = self.wait_until_element_clickable(finder) | ||
548 | hide_button.click() | ||
549 | self.wait_until_not_visible('#change-notification') | ||
550 | |||
551 | # check "Remove layer" button works | ||
552 | self.wait_until_visible('#layerstable tbody tr') | ||
553 | finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="remove"]') | ||
554 | remove_btn = self.wait_until_element_clickable(finder) | ||
555 | remove_btn.click() | ||
556 | self.wait_until_visible('#change-notification') | ||
557 | change_notification = self.find('#change-notification') | ||
558 | self.assertIn( | ||
559 | f'You have removed 1 layer from your project: {input_text}', str(change_notification.text) | ||
560 | ) | ||
561 | |||
562 | finder = lambda driver: self.find('#hide-alert') | ||
563 | hide_button = self.wait_until_element_clickable(finder) | ||
564 | hide_button.click() | ||
565 | self.wait_until_not_visible('#change-notification') | ||
566 | |||
567 | # check layers table feature(show/hide column, pagination) | ||
568 | self._navigate_to_config_nav('layerstable', 6) | ||
569 | column_list = [ | ||
570 | 'dependencies', | ||
571 | 'revision', | ||
572 | 'layer__vcs_url', | ||
573 | 'git_subdir', | ||
574 | 'layer__summary', | ||
575 | ] | ||
576 | self._mixin_test_table_edit_column( | ||
577 | 'layerstable', | ||
578 | 'edit-columns-button', | ||
579 | [f'checkbox-{column}' for column in column_list] | ||
580 | ) | ||
581 | self._navigate_to_config_nav('layerstable', 6) | ||
582 | # check show rows(pagination) | ||
583 | self._mixin_test_table_show_rows( | ||
584 | table_selector='layerstable', | ||
585 | to_skip=[150], | ||
586 | ) | ||
587 | |||
588 | def test_distro_page(self): | ||
589 | """ Test distros page | ||
590 | - Check if title "Compatible distros" is displayed | ||
591 | - Check search input | ||
592 | - Check "Add layer" button works | ||
593 | - Check distro table feature(show/hide column, pagination) | ||
594 | """ | ||
595 | self._navigate_to_config_nav('distrostable', 7) | ||
596 | # check title "Compatible distros" is displayed | ||
597 | self.assertIn("Compatible Distros", self.get_page_source()) | ||
598 | # Test search input | ||
599 | input_text='poky-altcfg' | ||
600 | self._mixin_test_table_search_input( | ||
601 | input_selector='search-input-distrostable', | ||
602 | input_text=input_text, | ||
603 | searchBtn_selector='search-submit-distrostable', | ||
604 | table_selector='distrostable' | ||
605 | ) | ||
606 | # check "Add distro" button works | ||
607 | self.wait_until_visible(".add-del-layers") | ||
608 | finder = lambda driver: self.find_all('#distrostable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]') | ||
609 | add_btn = self.wait_until_element_clickable(finder) | ||
610 | add_btn.click() | ||
611 | self.wait_until_visible('#change-notification') | ||
612 | change_notification = self.find('#change-notification') | ||
613 | self.assertIn( | ||
614 | f'You have changed the distro to: {input_text}', str(change_notification.text) | ||
615 | ) | ||
616 | # check distro table feature(show/hide column, pagination) | ||
617 | self._navigate_to_config_nav('distrostable', 7) | ||
618 | column_list = [ | ||
619 | 'description', | ||
620 | 'templatefile', | ||
621 | 'layer_version__get_vcs_reference', | ||
622 | 'layer_version__layer__name', | ||
623 | ] | ||
624 | self._mixin_test_table_edit_column( | ||
625 | 'distrostable', | ||
626 | 'edit-columns-button', | ||
627 | [f'checkbox-{column}' for column in column_list] | ||
628 | ) | ||
629 | self._navigate_to_config_nav('distrostable', 7) | ||
630 | # check show rows(pagination) | ||
631 | self._mixin_test_table_show_rows( | ||
632 | table_selector='distrostable', | ||
633 | to_skip=[150], | ||
634 | ) | ||
635 | |||
636 | def test_single_layer_page(self): | ||
637 | """ Test layer details page using meta-poky as an example (assumes is added to start with) | ||
638 | - Check if title is displayed | ||
639 | - Check add/remove layer button works | ||
640 | - Check tabs(layers, recipes, machines) are displayed | ||
641 | - Check left section is displayed | ||
642 | - Check layer name | ||
643 | - Check layer summary | ||
644 | - Check layer description | ||
645 | """ | ||
646 | self._navigate_to_config_nav('layerstable', 6) | ||
647 | layer_link = self.driver.find_element(By.XPATH, '//tr/td[@class="layer__name"]/a[contains(text(),"meta-poky")]') | ||
648 | layer_link.click() | ||
649 | self.wait_until_visible('.page-header') | ||
650 | # check title is displayed | ||
651 | self.assertTrue(self.find('.page-header h1').is_displayed()) | ||
652 | |||
653 | # check remove layer button works | ||
654 | finder = lambda driver: self.find('#add-remove-layer-btn') | ||
655 | remove_layer_btn = self.wait_until_element_clickable(finder) | ||
656 | remove_layer_btn.click() | ||
657 | self.wait_until_visible('#change-notification') | ||
658 | change_notification = self.find('#change-notification') | ||
659 | self.assertIn( | ||
660 | f'You have removed 1 layer from your project', str(change_notification.text) | ||
661 | ) | ||
662 | finder = lambda driver: self.find('#hide-alert') | ||
663 | hide_button = self.wait_until_element_clickable(finder) | ||
664 | hide_button.click() | ||
665 | # check add layer button works | ||
666 | self.wait_until_not_visible('#change-notification') | ||
667 | finder = lambda driver: self.find('#add-remove-layer-btn') | ||
668 | add_layer_btn = self.wait_until_element_clickable(finder) | ||
669 | add_layer_btn.click() | ||
670 | self.wait_until_visible('#change-notification') | ||
671 | change_notification = self.find('#change-notification') | ||
672 | self.assertIn( | ||
673 | f'You have added 1 layer to your project', str(change_notification.text) | ||
674 | ) | ||
675 | finder = lambda driver: self.find('#hide-alert') | ||
676 | hide_button = self.wait_until_element_clickable(finder) | ||
677 | hide_button.click() | ||
678 | self.wait_until_not_visible('#change-notification') | ||
679 | # check tabs(layers, recipes, machines) are displayed | ||
680 | tabs = self.find_all('.nav-tabs li') | ||
681 | self.assertEqual(len(tabs), 3) | ||
682 | # Check first tab | ||
683 | tabs[0].click() | ||
684 | self.assertIn( | ||
685 | 'active', str(self.find('#information').get_attribute('class')) | ||
686 | ) | ||
687 | # Check second tab (recipes) | ||
688 | self.wait_until_visible('.nav-tabs') | ||
689 | # Ensure page is scrolled to the top | ||
690 | self.driver.execute_script('window.scrollTo({behavior: "instant", top: 0, left: 0})') | ||
691 | tabs[1].click() | ||
692 | self.assertIn( | ||
693 | 'active', str(self.find('#recipes').get_attribute('class')) | ||
694 | ) | ||
695 | # Check third tab (machines) | ||
696 | self.wait_until_visible('.nav-tabs') | ||
697 | # Ensure page is scrolled to the top | ||
698 | self.driver.execute_script('window.scrollTo({behavior: "instant", top: 0, left: 0})') | ||
699 | tabs[2].click() | ||
700 | self.assertIn( | ||
701 | 'active', str(self.find('#machines').get_attribute('class')) | ||
702 | ) | ||
703 | # Check left section is displayed | ||
704 | section = self.find('.well') | ||
705 | # Check layer name | ||
706 | self.assertTrue( | ||
707 | section.find_element(By.XPATH, '//h2[1]').is_displayed() | ||
708 | ) | ||
709 | # Check layer summary | ||
710 | self.assertIn("Summary", section.text) | ||
711 | # Check layer description | ||
712 | self.assertIn("Description", section.text) | ||
713 | |||
714 | @pytest.mark.django_db | ||
715 | @pytest.mark.order("last") | ||
716 | class TestProjectPageRecipes(TestProjectPageBase): | ||
717 | |||
718 | def test_single_recipe_page(self): | ||
719 | """ Test recipe page | ||
720 | - Check if title is displayed | ||
721 | - Check add recipe layer displayed | ||
722 | - Check left section is displayed | ||
723 | - Check recipe: name, summary, description, Version, Section, | ||
724 | License, Approx. packages included, Approx. size, Recipe file | ||
725 | """ | ||
726 | # Use a recipe which is likely to exist in the layer index but not enabled | ||
727 | # in poky out the box - xen-image-minimal from meta-virtualization | ||
728 | self._navigate_to_project_page() | ||
729 | prj = Project.objects.get(pk=TestProjectPageBase.project_id) | ||
730 | recipe_id = prj.get_all_compatible_recipes().get(name="xen-image-minimal").pk | ||
731 | url = reverse("recipedetails", args=(TestProjectPageBase.project_id, recipe_id)) | ||
732 | self.get(url) | ||
733 | self.wait_until_visible('.page-header') | ||
734 | # check title is displayed | ||
735 | self.assertTrue(self.find('.page-header h1').is_displayed()) | ||
736 | # check add recipe layer displayed | ||
737 | add_recipe_layer_btn = self.find('#add-layer-btn') | ||
738 | self.assertTrue(add_recipe_layer_btn.is_displayed()) | ||
739 | # check left section is displayed | ||
740 | section = self.find('.well') | ||
741 | # Check recipe name | ||
742 | self.assertTrue( | ||
743 | section.find_element(By.XPATH, '//h2[1]').is_displayed() | ||
744 | ) | ||
745 | # Check recipe sections details info are displayed | ||
746 | self.assertIn("Summary", section.text) | ||
747 | self.assertIn("Description", section.text) | ||
748 | self.assertIn("Version", section.text) | ||
749 | self.assertIn("Section", section.text) | ||
750 | self.assertIn("License", section.text) | ||
751 | self.assertIn("Approx. packages included", section.text) | ||
752 | self.assertIn("Approx. package size", section.text) | ||
753 | self.assertIn("Recipe file", section.text) | ||
754 | |||
755 | def test_image_recipe_editColumn(self): | ||
756 | """ Test the edit column feature in image recipe table on project page """ | ||
757 | self._get_create_builds(success=10, failure=10) | ||
758 | |||
759 | url = reverse('projectimagerecipes', args=(TestProjectPageBase.project_id,)) | ||
760 | self.get(url) | ||
761 | self.wait_until_present('#imagerecipestable tbody tr') | ||
762 | |||
763 | column_list = [ | ||
764 | 'get_description_or_summary', 'layer_version__get_vcs_reference', | ||
765 | 'layer_version__layer__name', 'license', 'recipe-file', 'section', | ||
766 | 'version' | ||
767 | ] | ||
768 | |||
769 | # Check that we can hide the edit column | ||
770 | self._mixin_test_table_edit_column( | ||
771 | 'imagerecipestable', | ||
772 | 'edit-columns-button', | ||
773 | [f'checkbox-{column}' for column in column_list] | ||
774 | ) | ||
775 | |||
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..80c53e1544 --- /dev/null +++ b/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py | |||
@@ -0,0 +1,507 @@ | |||
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 time | ||
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 tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
17 | from selenium.webdriver.common.by import By | ||
18 | |||
19 | from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled | ||
20 | |||
21 | class TestProjectConfigTabBase(SeleniumFunctionalTestCase): | ||
22 | PROJECT_NAME = 'TestProjectConfigTab' | ||
23 | project_id = None | ||
24 | |||
25 | def _navigate_to_project_page(self): | ||
26 | # Navigate to project page | ||
27 | if TestProjectConfigTabBase.project_id is None: | ||
28 | TestProjectConfigTabBase.project_id = self.create_new_project(self.PROJECT_NAME, '3', None, True) | ||
29 | url = reverse('project', args=(TestProjectConfigTabBase.project_id,)) | ||
30 | self.get(url) | ||
31 | self.wait_until_visible('#config-nav') | ||
32 | |||
33 | def _create_builds(self): | ||
34 | # check search box can be use to build recipes | ||
35 | search_box = self.find('#build-input') | ||
36 | search_box.send_keys('foo') | ||
37 | self.find('#build-button').click() | ||
38 | self.wait_until_present('#latest-builds') | ||
39 | # loop until reach the parsing state | ||
40 | wait_until_build(self, 'queued cloning starting parsing failed') | ||
41 | lastest_builds = self.driver.find_elements( | ||
42 | By.XPATH, | ||
43 | '//div[@id="latest-builds"]/div', | ||
44 | ) | ||
45 | last_build = lastest_builds[0] | ||
46 | self.assertIn( | ||
47 | 'foo', str(last_build.text) | ||
48 | ) | ||
49 | last_build = lastest_builds[0] | ||
50 | try: | ||
51 | cancel_button = last_build.find_element( | ||
52 | By.XPATH, | ||
53 | '//span[@class="cancel-build-btn pull-right alert-link"]', | ||
54 | ) | ||
55 | cancel_button.click() | ||
56 | except NoSuchElementException: | ||
57 | # Skip if the build is already cancelled | ||
58 | pass | ||
59 | wait_until_build_cancelled(self) | ||
60 | |||
61 | def _get_tabs(self): | ||
62 | # tabs links list | ||
63 | return self.driver.find_elements( | ||
64 | By.XPATH, | ||
65 | '//div[@id="project-topbar"]//li' | ||
66 | ) | ||
67 | |||
68 | def _get_config_nav_item(self, index): | ||
69 | config_nav = self.find('#config-nav') | ||
70 | return config_nav.find_elements(By.TAG_NAME, 'li')[index] | ||
71 | |||
72 | class TestProjectConfigTab(TestProjectConfigTabBase): | ||
73 | |||
74 | def test_project_config_nav(self): | ||
75 | """ Test project config tab navigation: | ||
76 | - Check if the menu is displayed and contains the right elements: | ||
77 | - Configuration | ||
78 | - COMPATIBLE METADATA | ||
79 | - Custom images | ||
80 | - Image recipes | ||
81 | - Software recipes | ||
82 | - Machines | ||
83 | - Layers | ||
84 | - Distro | ||
85 | - EXTRA CONFIGURATION | ||
86 | - Bitbake variables | ||
87 | - Actions | ||
88 | - Delete project | ||
89 | """ | ||
90 | self._navigate_to_project_page() | ||
91 | |||
92 | def _get_config_nav_item(index): | ||
93 | config_nav = self.find('#config-nav') | ||
94 | return config_nav.find_elements(By.TAG_NAME, 'li')[index] | ||
95 | |||
96 | def check_config_nav_item(index, item_name, url): | ||
97 | item = _get_config_nav_item(index) | ||
98 | self.assertIn(item_name, item.text) | ||
99 | self.assertEqual(item.get_attribute('class'), 'active') | ||
100 | self.assertIn(url, self.driver.current_url) | ||
101 | |||
102 | # check if the menu contains the right elements | ||
103 | # COMPATIBLE METADATA | ||
104 | compatible_metadata = _get_config_nav_item(1) | ||
105 | self.assertIn( | ||
106 | "compatible metadata", compatible_metadata.text.lower() | ||
107 | ) | ||
108 | # EXTRA CONFIGURATION | ||
109 | extra_configuration = _get_config_nav_item(8) | ||
110 | self.assertIn( | ||
111 | "extra configuration", extra_configuration.text.lower() | ||
112 | ) | ||
113 | # Actions | ||
114 | actions = _get_config_nav_item(10) | ||
115 | self.assertIn("actions", str(actions.text).lower()) | ||
116 | |||
117 | conf_nav_list = [ | ||
118 | # config | ||
119 | [0, 'Configuration', | ||
120 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}"], | ||
121 | # custom images | ||
122 | [2, 'Custom images', | ||
123 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/customimages"], | ||
124 | # image recipes | ||
125 | [3, 'Image recipes', | ||
126 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/images"], | ||
127 | # software recipes | ||
128 | [4, 'Software recipes', | ||
129 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/softwarerecipes"], | ||
130 | # machines | ||
131 | [5, 'Machines', | ||
132 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/machines"], | ||
133 | # layers | ||
134 | [6, 'Layers', | ||
135 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/layers"], | ||
136 | # distro | ||
137 | [7, 'Distros', | ||
138 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/distros"], | ||
139 | # [9, 'BitBake variables', f"/toastergui/project/{TestProjectConfigTabBase.project_id}/configuration"], # bitbake variables | ||
140 | ] | ||
141 | for index, item_name, url in conf_nav_list: | ||
142 | item = _get_config_nav_item(index) | ||
143 | if item.get_attribute('class') != 'active': | ||
144 | item.click() | ||
145 | check_config_nav_item(index, item_name, url) | ||
146 | |||
147 | def test_image_recipe_editColumn(self): | ||
148 | """ Test the edit column feature in image recipe table on project page """ | ||
149 | def test_edit_column(check_box_id): | ||
150 | # Check that we can hide/show table column | ||
151 | check_box = self.find(f'#{check_box_id}') | ||
152 | th_class = str(check_box_id).replace('checkbox-', '') | ||
153 | if check_box.is_selected(): | ||
154 | # check if column is visible in table | ||
155 | self.assertTrue( | ||
156 | self.find( | ||
157 | f'#imagerecipestable thead th.{th_class}' | ||
158 | ).is_displayed(), | ||
159 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
160 | ) | ||
161 | check_box.click() | ||
162 | # check if column is hidden in table | ||
163 | self.assertFalse( | ||
164 | self.find( | ||
165 | f'#imagerecipestable 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 | else: | ||
170 | # check if column is hidden in table | ||
171 | self.assertFalse( | ||
172 | self.find( | ||
173 | f'#imagerecipestable thead th.{th_class}' | ||
174 | ).is_displayed(), | ||
175 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
176 | ) | ||
177 | check_box.click() | ||
178 | # check if column is visible in table | ||
179 | self.assertTrue( | ||
180 | self.find( | ||
181 | f'#imagerecipestable thead th.{th_class}' | ||
182 | ).is_displayed(), | ||
183 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
184 | ) | ||
185 | |||
186 | self._navigate_to_project_page() | ||
187 | # navigate to project image recipe page | ||
188 | recipe_image_page_link = self._get_config_nav_item(3) | ||
189 | recipe_image_page_link.click() | ||
190 | self.wait_until_present('#imagerecipestable tbody tr') | ||
191 | |||
192 | # Check edit column | ||
193 | edit_column = self.find('#edit-columns-button') | ||
194 | self.assertTrue(edit_column.is_displayed()) | ||
195 | edit_column.click() | ||
196 | # Check dropdown is visible | ||
197 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
198 | |||
199 | # Check that we can hide the edit column | ||
200 | test_edit_column('checkbox-get_description_or_summary') | ||
201 | test_edit_column('checkbox-layer_version__get_vcs_reference') | ||
202 | test_edit_column('checkbox-layer_version__layer__name') | ||
203 | test_edit_column('checkbox-license') | ||
204 | test_edit_column('checkbox-recipe-file') | ||
205 | test_edit_column('checkbox-section') | ||
206 | test_edit_column('checkbox-version') | ||
207 | |||
208 | def test_image_recipe_show_rows(self): | ||
209 | """ Test the show rows feature in image recipe table on project page """ | ||
210 | def test_show_rows(row_to_show, show_row_link): | ||
211 | # Check that we can show rows == row_to_show | ||
212 | show_row_link.select_by_value(str(row_to_show)) | ||
213 | self.wait_until_visible('#imagerecipestable tbody tr') | ||
214 | # check at least some rows are visible | ||
215 | self.assertTrue( | ||
216 | len(self.find_all('#imagerecipestable tbody tr')) > 0 | ||
217 | ) | ||
218 | |||
219 | self._navigate_to_project_page() | ||
220 | # navigate to project image recipe page | ||
221 | recipe_image_page_link = self._get_config_nav_item(3) | ||
222 | recipe_image_page_link.click() | ||
223 | self.wait_until_present('#imagerecipestable tbody tr') | ||
224 | |||
225 | show_rows = self.driver.find_elements( | ||
226 | By.XPATH, | ||
227 | '//select[@class="form-control pagesize-imagerecipestable"]' | ||
228 | ) | ||
229 | # Check show rows | ||
230 | for show_row_link in show_rows: | ||
231 | show_row_link = Select(show_row_link) | ||
232 | test_show_rows(10, show_row_link) | ||
233 | test_show_rows(25, show_row_link) | ||
234 | test_show_rows(50, show_row_link) | ||
235 | test_show_rows(100, show_row_link) | ||
236 | test_show_rows(150, show_row_link) | ||
237 | |||
238 | def test_project_config_tab_right_section(self): | ||
239 | """ Test project config tab right section contains five blocks: | ||
240 | - Machine: | ||
241 | - check 'Machine' is displayed | ||
242 | - check can change Machine | ||
243 | - Distro: | ||
244 | - check 'Distro' is displayed | ||
245 | - check can change Distro | ||
246 | - Most built recipes: | ||
247 | - check 'Most built recipes' is displayed | ||
248 | - check can select a recipe and build it | ||
249 | - Project release: | ||
250 | - check 'Project release' is displayed | ||
251 | - check project has right release displayed | ||
252 | - Layers: | ||
253 | - check can add a layer if exists | ||
254 | - check at least three layers are displayed | ||
255 | - openembedded-core | ||
256 | - meta-poky | ||
257 | - meta-yocto-bsp | ||
258 | """ | ||
259 | project_id = self.create_new_project(self.PROJECT_NAME + "-ST", '3', None, True) | ||
260 | url = reverse('project', args=(project_id,)) | ||
261 | self.get(url) | ||
262 | self.wait_until_visible('#config-nav') | ||
263 | |||
264 | # check if the menu is displayed | ||
265 | self.wait_until_visible('#project-page') | ||
266 | block_l = self.driver.find_element( | ||
267 | By.XPATH, '//*[@id="project-page"]/div[2]') | ||
268 | project_release = self.driver.find_element( | ||
269 | By.XPATH, '//*[@id="project-page"]/div[1]/div[4]') | ||
270 | layers = block_l.find_element(By.ID, 'layer-container') | ||
271 | |||
272 | def check_machine_distro(self, item_name, new_item_name, block_id): | ||
273 | block = self.find(f'#{block_id}') | ||
274 | title = block.find_element(By.TAG_NAME, 'h3') | ||
275 | self.assertIn(item_name.capitalize(), title.text) | ||
276 | edit_btn = self.find(f'#change-{item_name}-toggle') | ||
277 | edit_btn.click() | ||
278 | self.wait_until_visible(f'#{item_name}-change-input') | ||
279 | name_input = self.find(f'#{item_name}-change-input') | ||
280 | name_input.clear() | ||
281 | name_input.send_keys(new_item_name) | ||
282 | change_btn = self.find(f'#{item_name}-change-btn') | ||
283 | change_btn.click() | ||
284 | self.wait_until_visible(f'#project-{item_name}-name') | ||
285 | project_name = self.find(f'#project-{item_name}-name') | ||
286 | self.assertIn(new_item_name, project_name.text) | ||
287 | # check change notificaiton is displayed | ||
288 | change_notification = self.find('#change-notification') | ||
289 | self.assertIn( | ||
290 | f'You have changed the {item_name} to: {new_item_name}', change_notification.text | ||
291 | ) | ||
292 | hide_button = self.find('#hide-alert') | ||
293 | hide_button.click() | ||
294 | self.wait_until_not_visible('#change-notification') | ||
295 | |||
296 | # Machine | ||
297 | check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section') | ||
298 | # Distro | ||
299 | check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section') | ||
300 | |||
301 | # Project release | ||
302 | title = project_release.find_element(By.TAG_NAME, 'h3') | ||
303 | self.assertIn("Project release", title.text) | ||
304 | self.assertIn( | ||
305 | "Yocto Project master", self.find('#project-release-title').text | ||
306 | ) | ||
307 | # Layers | ||
308 | title = layers.find_element(By.TAG_NAME, 'h3') | ||
309 | self.assertIn("Layers", title.text) | ||
310 | self.wait_until_clickable('#layer-add-input') | ||
311 | # check at least three layers are displayed | ||
312 | # openembedded-core | ||
313 | # meta-poky | ||
314 | # meta-yocto-bsp | ||
315 | layer_list_items = [] | ||
316 | starttime = time.time() | ||
317 | while len(layer_list_items) < 3: | ||
318 | layers_list = self.driver.find_element(By.ID, 'layers-in-project-list') | ||
319 | layer_list_items = layers_list.find_elements(By.TAG_NAME, 'li') | ||
320 | if time.time() > (starttime + 30): | ||
321 | self.fail("Layer list didn't contain at least 3 items within 30s (contained %d)" % len(layer_list_items)) | ||
322 | |||
323 | # remove all layers except the first three layers | ||
324 | for i in range(3, len(layer_list_items)): | ||
325 | layer_list_items[i].find_element(By.TAG_NAME, 'span').click() | ||
326 | |||
327 | # check can add a layer if exists | ||
328 | add_layer_input = layers.find_element(By.ID, 'layer-add-input') | ||
329 | add_layer_input.send_keys('meta-oe') | ||
330 | self.wait_until_visible('#layer-container > form > div > span > div') | ||
331 | self.wait_until_visible('.dropdown-menu') | ||
332 | finder = lambda driver: driver.find_element(By.XPATH, '//*[@id="layer-container"]/form/div/span/div/div/div') | ||
333 | dropdown_item = self.wait_until_element_clickable(finder) | ||
334 | dropdown_item.click() | ||
335 | self.wait_until_clickable('#add-layer-btn') | ||
336 | add_layer_btn = layers.find_element(By.ID, 'add-layer-btn') | ||
337 | add_layer_btn.click() | ||
338 | self.wait_until_visible('#layers-in-project-list') | ||
339 | |||
340 | # check layer is added | ||
341 | layer_list_items = [] | ||
342 | starttime = time.time() | ||
343 | while len(layer_list_items) < 4: | ||
344 | layers_list = self.driver.find_element(By.ID, 'layers-in-project-list') | ||
345 | layer_list_items = layers_list.find_elements(By.TAG_NAME, 'li') | ||
346 | if time.time() > (starttime + 30): | ||
347 | self.fail("Layer list didn't contain at least 4 items within 30s (contained %d)" % len(layer_list_items)) | ||
348 | |||
349 | def test_project_page_tab_importlayer(self): | ||
350 | """ Test project page tab import layer """ | ||
351 | self._navigate_to_project_page() | ||
352 | # navigate to "Import layers" tab | ||
353 | import_layers_tab = self._get_tabs()[2] | ||
354 | import_layers_tab.find_element(By.TAG_NAME, 'a').click() | ||
355 | self.wait_until_visible('#layer-git-repo-url') | ||
356 | |||
357 | # Check git repo radio button | ||
358 | git_repo_radio = self.find('#git-repo-radio') | ||
359 | git_repo_radio.click() | ||
360 | |||
361 | # Set git repo url | ||
362 | input_repo_url = self.find('#layer-git-repo-url') | ||
363 | input_repo_url.send_keys('git://git.yoctoproject.org/meta-fake') | ||
364 | # Blur the input to trigger the validation | ||
365 | input_repo_url.send_keys(Keys.TAB) | ||
366 | |||
367 | # Check name is set | ||
368 | input_layer_name = self.find('#import-layer-name') | ||
369 | self.assertTrue(input_layer_name.get_attribute('value') == 'meta-fake') | ||
370 | |||
371 | # Set branch | ||
372 | input_branch = self.find('#layer-git-ref') | ||
373 | input_branch.send_keys('master') | ||
374 | |||
375 | # Import layer | ||
376 | self.find('#import-and-add-btn').click() | ||
377 | |||
378 | # Check layer is added | ||
379 | self.wait_until_visible('#layer-container') | ||
380 | block_l = self.driver.find_element( | ||
381 | By.XPATH, '//*[@id="project-page"]/div[2]') | ||
382 | layers = block_l.find_element(By.ID, 'layer-container') | ||
383 | layers_list = layers.find_element(By.ID, 'layers-in-project-list') | ||
384 | layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') | ||
385 | self.assertIn( | ||
386 | 'meta-fake', str(layers_list_items[-1].text) | ||
387 | ) | ||
388 | |||
389 | def test_project_page_custom_image_no_image(self): | ||
390 | """ Test project page tab "New custom image" when no custom image """ | ||
391 | project_id = self.create_new_project(self.PROJECT_NAME + "-CustomImage", '3', None, True) | ||
392 | url = reverse('project', args=(project_id,)) | ||
393 | self.get(url) | ||
394 | self.wait_until_visible('#config-nav') | ||
395 | |||
396 | # navigate to "Custom image" tab | ||
397 | custom_image_section = self._get_config_nav_item(2) | ||
398 | custom_image_section.click() | ||
399 | self.wait_until_visible('#empty-state-customimagestable') | ||
400 | |||
401 | # Check message when no custom image | ||
402 | self.assertIn( | ||
403 | "You have not created any custom images yet.", str( | ||
404 | self.find('#empty-state-customimagestable').text | ||
405 | ) | ||
406 | ) | ||
407 | div_empty_msg = self.find('#empty-state-customimagestable') | ||
408 | link_create_custom_image = div_empty_msg.find_element( | ||
409 | By.TAG_NAME, 'a') | ||
410 | self.assertTrue(TestProjectConfigTabBase.project_id is not None) | ||
411 | self.assertIn( | ||
412 | f"/toastergui/project/{project_id}/newcustomimage", str( | ||
413 | link_create_custom_image.get_attribute('href') | ||
414 | ) | ||
415 | ) | ||
416 | self.assertIn( | ||
417 | "Create your first custom image", str( | ||
418 | link_create_custom_image.text | ||
419 | ) | ||
420 | ) | ||
421 | |||
422 | def test_project_page_image_recipe(self): | ||
423 | """ Test project page section images | ||
424 | - Check image recipes are displayed | ||
425 | - Check search input | ||
426 | - Check image recipe build button works | ||
427 | - Check image recipe table features(show/hide column, pagination) | ||
428 | """ | ||
429 | self._navigate_to_project_page() | ||
430 | # navigate to "Images section" | ||
431 | images_section = self._get_config_nav_item(3) | ||
432 | images_section.click() | ||
433 | self.wait_until_visible('#imagerecipestable') | ||
434 | rows = self.find_all('#imagerecipestable tbody tr') | ||
435 | self.assertTrue(len(rows) > 0) | ||
436 | |||
437 | # Test search input | ||
438 | self.wait_until_visible('#search-input-imagerecipestable') | ||
439 | recipe_input = self.find('#search-input-imagerecipestable') | ||
440 | recipe_input.send_keys('core-image-minimal') | ||
441 | self.find('#search-submit-imagerecipestable').click() | ||
442 | self.wait_until_visible('#imagerecipestable tbody tr') | ||
443 | rows = self.find_all('#imagerecipestable tbody tr') | ||
444 | self.assertTrue(len(rows) > 0) | ||
445 | |||
446 | @pytest.mark.django_db | ||
447 | @pytest.mark.order("last") | ||
448 | class TestProjectConfigTabDB(TestProjectConfigTabBase): | ||
449 | |||
450 | def test_most_build_recipes(self): | ||
451 | """ Test most build recipes block contains""" | ||
452 | def rebuild_from_most_build_recipes(recipe_list_items): | ||
453 | checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input') | ||
454 | checkbox.click() | ||
455 | build_btn = self.find('#freq-build-btn') | ||
456 | build_btn.click() | ||
457 | self.wait_until_visible('#latest-builds') | ||
458 | wait_until_build(self, 'queued cloning starting parsing failed') | ||
459 | lastest_builds = self.driver.find_elements( | ||
460 | By.XPATH, | ||
461 | '//div[@id="latest-builds"]/div' | ||
462 | ) | ||
463 | self.assertTrue(len(lastest_builds) >= 2) | ||
464 | last_build = lastest_builds[0] | ||
465 | try: | ||
466 | cancel_button = last_build.find_element( | ||
467 | By.XPATH, | ||
468 | '//span[@class="cancel-build-btn pull-right alert-link"]', | ||
469 | ) | ||
470 | cancel_button.click() | ||
471 | except NoSuchElementException: | ||
472 | # Skip if the build is already cancelled | ||
473 | pass | ||
474 | wait_until_build_cancelled(self) | ||
475 | |||
476 | # Create a new project for remaining asserts | ||
477 | project_id = self.create_new_project(self.PROJECT_NAME + "-MostBuilt", '2', None, True) | ||
478 | url = reverse('project', args=(project_id,)) | ||
479 | self.get(url) | ||
480 | self.wait_until_visible('#config-nav') | ||
481 | |||
482 | current_url = self.driver.current_url | ||
483 | url = current_url.split('?')[0] | ||
484 | |||
485 | # Create a new builds | ||
486 | self._create_builds() | ||
487 | |||
488 | # back to project page | ||
489 | self.driver.get(url) | ||
490 | |||
491 | self.wait_until_visible('#project-page') | ||
492 | |||
493 | # Most built recipes | ||
494 | most_built_recipes = self.driver.find_element( | ||
495 | By.XPATH, '//*[@id="project-page"]/div[1]/div[3]') | ||
496 | title = most_built_recipes.find_element(By.TAG_NAME, 'h3') | ||
497 | self.assertIn("Most built recipes", title.text) | ||
498 | # check can select a recipe and build it | ||
499 | self.wait_until_visible('#freq-build-list') | ||
500 | recipe_list = self.find('#freq-build-list') | ||
501 | recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li') | ||
502 | self.assertTrue( | ||
503 | len(recipe_list_items) > 0, | ||
504 | msg="No recipes found in the most built recipes list", | ||
505 | ) | ||
506 | rebuild_from_most_build_recipes(recipe_list_items) | ||
507 | |||
diff --git a/bitbake/lib/toaster/tests/functional/utils.py b/bitbake/lib/toaster/tests/functional/utils.py new file mode 100644 index 0000000000..72345aef9f --- /dev/null +++ b/bitbake/lib/toaster/tests/functional/utils.py | |||
@@ -0,0 +1,86 @@ | |||
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, WebDriverException | ||
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 | pass | ||
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 | while True: | ||
52 | try: | ||
53 | if start_time > timeout: | ||
54 | raise TimeoutException( | ||
55 | f'Build did not reach cancelled state within {timeout} seconds' | ||
56 | ) | ||
57 | last_build_state = test_instance.driver.find_element( | ||
58 | By.XPATH, | ||
59 | '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]', | ||
60 | ) | ||
61 | build_state = last_build_state.get_attribute( | ||
62 | 'data-build-state') | ||
63 | if 'failed' in str(build_state).lower(): | ||
64 | break | ||
65 | if 'cancelling' in str(build_state).lower(): | ||
66 | pass | ||
67 | if 'cancelled' in str(build_state).lower(): | ||
68 | break | ||
69 | except TimeoutException: | ||
70 | break | ||
71 | except NoSuchElementException: | ||
72 | pass | ||
73 | except StaleElementReferenceException: | ||
74 | pass | ||
75 | except WebDriverException: | ||
76 | pass | ||
77 | start_time += 1 | ||
78 | sleep(1) # take a breath and try again | ||
79 | |||
80 | def get_projectId_from_url(url): | ||
81 | # url = 'http://domainename.com/toastergui/project/1656/whatever | ||
82 | # or url = 'http://domainename.com/toastergui/project/1/ | ||
83 | # or url = 'http://domainename.com/toastergui/project/186 | ||
84 | assert '/toastergui/project/' in url, "URL is not valid" | ||
85 | url_to_list = url.split('/toastergui/project/') | ||
86 | return int(url_to_list[1].split('/')[0]) # project_id | ||
diff --git a/bitbake/lib/toaster/tests/toaster-tests-requirements.txt b/bitbake/lib/toaster/tests/toaster-tests-requirements.txt index 4f9fcc46d2..6243c00a36 100644 --- a/bitbake/lib/toaster/tests/toaster-tests-requirements.txt +++ b/bitbake/lib/toaster/tests/toaster-tests-requirements.txt | |||
@@ -1 +1,9 @@ | |||
1 | selenium==2.49.2 | 1 | selenium>=4.13.0 |
2 | pytest==7.4.2 | ||
3 | pytest-django==4.5.2 | ||
4 | pytest-env==1.1.0 | ||
5 | pytest-html==4.0.2 | ||
6 | pytest-metadata==3.0.0 | ||
7 | pytest-order==1.1.0 | ||
8 | requests | ||
9 | |||
diff --git a/bitbake/lib/toaster/tests/views/test_views.py b/bitbake/lib/toaster/tests/views/test_views.py index 735d596bcc..e1adfcf86a 100644 --- a/bitbake/lib/toaster/tests/views/test_views.py +++ b/bitbake/lib/toaster/tests/views/test_views.py | |||
@@ -9,6 +9,8 @@ | |||
9 | 9 | ||
10 | """Test cases for Toaster GUI and ReST.""" | 10 | """Test cases for Toaster GUI and ReST.""" |
11 | 11 | ||
12 | import os | ||
13 | import pytest | ||
12 | from django.test import TestCase | 14 | from django.test import TestCase |
13 | from django.test.client import RequestFactory | 15 | from django.test.client import RequestFactory |
14 | from django.urls import reverse | 16 | from django.urls import reverse |
@@ -19,6 +21,7 @@ from orm.models import Layer_Version, Recipe | |||
19 | from orm.models import CustomImageRecipe | 21 | from orm.models import CustomImageRecipe |
20 | from orm.models import CustomImagePackage | 22 | from orm.models import CustomImagePackage |
21 | 23 | ||
24 | from bldcontrol.models import BuildEnvironment | ||
22 | import inspect | 25 | import inspect |
23 | import toastergui | 26 | import toastergui |
24 | 27 | ||
@@ -32,19 +35,32 @@ PROJECT_NAME2 = "test project 2" | |||
32 | CLI_BUILDS_PROJECT_NAME = 'Command line builds' | 35 | CLI_BUILDS_PROJECT_NAME = 'Command line builds' |
33 | 36 | ||
34 | 37 | ||
38 | |||
35 | class ViewTests(TestCase): | 39 | class ViewTests(TestCase): |
36 | """Tests to verify view APIs.""" | 40 | """Tests to verify view APIs.""" |
37 | 41 | ||
38 | fixtures = ['toastergui-unittest-data'] | 42 | fixtures = ['toastergui-unittest-data'] |
43 | builldir = os.environ.get('BUILDDIR') | ||
39 | 44 | ||
40 | def setUp(self): | 45 | def setUp(self): |
41 | 46 | ||
42 | self.project = Project.objects.first() | 47 | self.project = Project.objects.first() |
48 | |||
43 | self.recipe1 = Recipe.objects.get(pk=2) | 49 | self.recipe1 = Recipe.objects.get(pk=2) |
50 | # create a file and to recipe1 file_path | ||
51 | file_path = f"{self.builldir}/{self.recipe1.name.strip().replace(' ', '-')}.bb" | ||
52 | with open(file_path, 'w') as f: | ||
53 | f.write('foo') | ||
54 | self.recipe1.file_path = file_path | ||
55 | self.recipe1.save() | ||
56 | |||
44 | self.customr = CustomImageRecipe.objects.first() | 57 | self.customr = CustomImageRecipe.objects.first() |
45 | self.cust_package = CustomImagePackage.objects.first() | 58 | self.cust_package = CustomImagePackage.objects.first() |
46 | self.package = Package.objects.first() | 59 | self.package = Package.objects.first() |
47 | self.lver = Layer_Version.objects.first() | 60 | self.lver = Layer_Version.objects.first() |
61 | if BuildEnvironment.objects.count() == 0: | ||
62 | BuildEnvironment.objects.create(betype=BuildEnvironment.TYPE_LOCAL) | ||
63 | |||
48 | 64 | ||
49 | def test_get_base_call_returns_html(self): | 65 | def test_get_base_call_returns_html(self): |
50 | """Basic test for all-projects view""" | 66 | """Basic test for all-projects view""" |
@@ -226,7 +242,7 @@ class ViewTests(TestCase): | |||
226 | recipe = CustomImageRecipe.objects.create( | 242 | recipe = CustomImageRecipe.objects.create( |
227 | name=name, project=self.project, | 243 | name=name, project=self.project, |
228 | base_recipe=self.recipe1, | 244 | base_recipe=self.recipe1, |
229 | file_path="/tmp/testing", | 245 | file_path=f"{self.builldir}/testing", |
230 | layer_version=self.customr.layer_version) | 246 | layer_version=self.customr.layer_version) |
231 | url = reverse('xhr_customrecipe_id', args=(recipe.id,)) | 247 | url = reverse('xhr_customrecipe_id', args=(recipe.id,)) |
232 | response = self.client.delete(url) | 248 | response = self.client.delete(url) |
@@ -297,7 +313,7 @@ class ViewTests(TestCase): | |||
297 | """Download the recipe file generated for the custom image""" | 313 | """Download the recipe file generated for the custom image""" |
298 | 314 | ||
299 | # Create a dummy recipe file for the custom image generation to read | 315 | # Create a dummy recipe file for the custom image generation to read |
300 | open("/tmp/a_recipe.bb", 'a').close() | 316 | open(f"{self.builldir}/a_recipe.bb", 'a').close() |
301 | response = self.client.get(reverse('customrecipedownload', | 317 | response = self.client.get(reverse('customrecipedownload', |
302 | args=(self.project.id, | 318 | args=(self.project.id, |
303 | self.customr.id))) | 319 | self.customr.id))) |