diff options
Diffstat (limited to 'lib/toaster/tests/browser')
-rw-r--r-- | lib/toaster/tests/browser/selenium_helpers_base.py | 76 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_all_builds_page.py | 315 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_all_projects_page.py | 162 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_builddashboard_page.py | 15 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_builddashboard_page_artifacts.py | 8 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_delete_project.py | 103 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_landing_page.py | 131 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_layerdetails_page.py | 39 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_most_recent_builds_states.py | 24 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_new_custom_image_page.py | 14 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_new_project_page.py | 16 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_project_builds_page.py | 4 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_project_config_page.py | 33 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_sample.py | 10 | ||||
-rw-r--r-- | lib/toaster/tests/browser/test_toastertable_ui.py | 11 |
15 files changed, 854 insertions, 107 deletions
diff --git a/lib/toaster/tests/browser/selenium_helpers_base.py b/lib/toaster/tests/browser/selenium_helpers_base.py index 644d45fe5..393be7549 100644 --- a/lib/toaster/tests/browser/selenium_helpers_base.py +++ b/lib/toaster/tests/browser/selenium_helpers_base.py @@ -19,11 +19,15 @@ import os import time import unittest +import pytest from selenium import webdriver +from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.by import By from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.common.exceptions import NoSuchElementException, \ - StaleElementReferenceException, TimeoutException + StaleElementReferenceException, TimeoutException, \ + SessionNotCreatedException def create_selenium_driver(cls,browser='chrome'): # set default browser string based on env (if available) @@ -32,9 +36,32 @@ def create_selenium_driver(cls,browser='chrome'): browser = env_browser if browser == 'chrome': - return webdriver.Chrome( - service_args=["--verbose", "--log-path=selenium.log"] - ) + options = webdriver.ChromeOptions() + options.add_argument('--headless') + options.add_argument('--disable-infobars') + options.add_argument('--disable-dev-shm-usage') + options.add_argument('--no-sandbox') + options.add_argument('--remote-debugging-port=9222') + try: + return webdriver.Chrome(options=options) + except SessionNotCreatedException as e: + exit_message = "Halting tests prematurely to avoid cascading errors." + # check if chrome / chromedriver exists + chrome_path = os.popen("find ~/.cache/selenium/chrome/ -name 'chrome' -type f -print -quit").read().strip() + if not chrome_path: + pytest.exit(f"Failed to install/find chrome.\n{exit_message}") + chromedriver_path = os.popen("find ~/.cache/selenium/chromedriver/ -name 'chromedriver' -type f -print -quit").read().strip() + if not chromedriver_path: + pytest.exit(f"Failed to install/find chromedriver.\n{exit_message}") + # check if depends on each are fulfilled + depends_chrome = os.popen(f"ldd {chrome_path} | grep 'not found'").read().strip() + if depends_chrome: + pytest.exit(f"Missing chrome dependencies.\n{depends_chrome}\n{exit_message}") + depends_chromedriver = os.popen(f"ldd {chromedriver_path} | grep 'not found'").read().strip() + if depends_chromedriver: + pytest.exit(f"Missing chromedriver dependencies.\n{depends_chromedriver}\n{exit_message}") + # print original error otherwise + pytest.exit(f"Failed to start chromedriver.\n{e}\n{exit_message}") elif browser == 'firefox': return webdriver.Firefox() elif browser == 'marionette': @@ -66,7 +93,9 @@ class Wait(WebDriverWait): _TIMEOUT = 10 _POLL_FREQUENCY = 0.5 - def __init__(self, driver): + def __init__(self, driver, timeout=_TIMEOUT, poll=_POLL_FREQUENCY): + self._TIMEOUT = timeout + self._POLL_FREQUENCY = poll super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) def until(self, method, message=''): @@ -138,6 +167,8 @@ class SeleniumTestCaseBase(unittest.TestCase): """ Clean up webdriver driver """ cls.driver.quit() + # Allow driver resources to be properly freed before proceeding with further tests + time.sleep(5) super(SeleniumTestCaseBase, cls).tearDownClass() def get(self, url): @@ -151,13 +182,20 @@ class SeleniumTestCaseBase(unittest.TestCase): abs_url = '%s%s' % (self.live_server_url, url) self.driver.get(abs_url) + try: # Ensure page is loaded before proceeding + self.wait_until_visible("#global-nav", poll=3) + except NoSuchElementException: + self.driver.implicitly_wait(3) + except TimeoutException: + self.driver.implicitly_wait(3) + def find(self, selector): """ Find single element by CSS selector """ - return self.driver.find_element_by_css_selector(selector) + return self.driver.find_element(By.CSS_SELECTOR, selector) def find_all(self, selector): """ Find all elements matching CSS selector """ - return self.driver.find_elements_by_css_selector(selector) + return self.driver.find_elements(By.CSS_SELECTOR, selector) def element_exists(self, selector): """ @@ -170,18 +208,34 @@ class SeleniumTestCaseBase(unittest.TestCase): """ Return the element which currently has focus on the page """ return self.driver.switch_to.active_element - def wait_until_present(self, selector): + def wait_until_present(self, selector, poll=0.5): """ Wait until element matching CSS selector is on the page """ is_present = lambda driver: self.find(selector) msg = 'An element matching "%s" should be on the page' % selector - element = Wait(self.driver).until(is_present, msg) + element = Wait(self.driver, poll=poll).until(is_present, msg) + if poll > 2: + time.sleep(poll) # element need more delay to be present return element - def wait_until_visible(self, selector): + def wait_until_visible(self, selector, poll=1): """ Wait until element matching CSS selector is visible on the page """ is_visible = lambda driver: self.find(selector).is_displayed() msg = 'An element matching "%s" should be visible' % selector - Wait(self.driver).until(is_visible, msg) + Wait(self.driver, poll=poll).until(is_visible, msg) + time.sleep(poll) # wait for visibility to settle + return self.find(selector) + + def wait_until_clickable(self, selector, poll=1): + """ Wait until element matching CSS selector is visible on the page """ + WebDriverWait( + self.driver, + Wait._TIMEOUT, + poll_frequency=poll + ).until( + EC.element_to_be_clickable((By.ID, selector.removeprefix('#') + ) + ) + ) return self.find(selector) def wait_until_focused(self, selector): diff --git a/lib/toaster/tests/browser/test_all_builds_page.py b/lib/toaster/tests/browser/test_all_builds_page.py index 8423d3dab..b9356a034 100644 --- a/lib/toaster/tests/browser/test_all_builds_page.py +++ b/lib/toaster/tests/browser/test_all_builds_page.py @@ -7,13 +7,18 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os import re from django.urls import reverse +from selenium.webdriver.support.select import Select from django.utils import timezone +from bldcontrol.models import BuildRequest from tests.browser.selenium_helpers import SeleniumTestCase -from orm.models import BitbakeVersion, Release, Project, Build, Target +from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task + +from selenium.webdriver.common.by import By class TestAllBuildsPage(SeleniumTestCase): @@ -23,7 +28,8 @@ class TestAllBuildsPage(SeleniumTestCase): CLI_BUILDS_PROJECT_NAME = 'command line builds' def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath='') release = Release.objects.create(name='release1', bitbake_version=bbv) @@ -69,7 +75,7 @@ class TestAllBuildsPage(SeleniumTestCase): '[data-role="data-recent-build-buildtime-field"]' % build.id # because this loads via Ajax, wait for it to be visible - self.wait_until_present(selector) + self.wait_until_visible(selector) build_time_spans = self.find_all(selector) @@ -79,7 +85,7 @@ class TestAllBuildsPage(SeleniumTestCase): def _get_row_for_build(self, build): """ Get the table row for the build from the all builds table """ - self.wait_until_present('#allbuildstable') + self.wait_until_visible('#allbuildstable') rows = self.find_all('#allbuildstable tr') @@ -91,7 +97,7 @@ class TestAllBuildsPage(SeleniumTestCase): found_row = None for row in rows: - outcome_links = row.find_elements_by_css_selector(selector) + outcome_links = row.find_elements(By.CSS_SELECTOR, selector) if len(outcome_links) == 1: found_row = row break @@ -100,6 +106,66 @@ class TestAllBuildsPage(SeleniumTestCase): return found_row + def _get_create_builds(self, **kwargs): + """ Create a build and return the build object """ + build1 = Build.objects.create(**self.project1_build_success) + build2 = Build.objects.create(**self.project1_build_failure) + + # add some targets to these builds so they have recipe links + # (and so we can find the row in the ToasterTable corresponding to + # a particular build) + Target.objects.create(build=build1, target='foo') + Target.objects.create(build=build2, target='bar') + + if kwargs: + # Create kwargs.get('success') builds with success status with target + # and kwargs.get('failure') builds with failure status with target + for i in range(kwargs.get('success', 0)): + now = timezone.now() + self.project1_build_success['started_on'] = now + self.project1_build_success[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_success) + Target.objects.create(build=build, + target=f'{i}_success_recipe', + task=f'{i}_success_task') + + self._set_buildRequest_and_task_on_build(build) + for i in range(kwargs.get('failure', 0)): + now = timezone.now() + self.project1_build_failure['started_on'] = now + self.project1_build_failure[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_failure) + Target.objects.create(build=build, + target=f'{i}_fail_recipe', + task=f'{i}_fail_task') + self._set_buildRequest_and_task_on_build(build) + return build1, build2 + + def _create_recipe(self): + """ Add a recipe to the database and return it """ + layer = Layer.objects.create() + layer_version = Layer_Version.objects.create(layer=layer) + return Recipe.objects.create(name='recipe_foo', layer_version=layer_version) + + def _set_buildRequest_and_task_on_build(self, build): + """ Set buildRequest and task on build """ + build.recipes_parsed = 1 + build.save() + buildRequest = BuildRequest.objects.create( + build=build, + project=self.project1, + state=BuildRequest.REQ_COMPLETED) + build.build_request = buildRequest + recipe = self._create_recipe() + task = Task.objects.create(build=build, + recipe=recipe, + task_name='task', + outcome=Task.OUTCOME_SUCCESS) + task.save() + build.save() + def test_show_tasks_with_suffix(self): """ Task should be shown as suffix on build name """ build = Build.objects.create(**self.project1_build_success) @@ -109,7 +175,7 @@ class TestAllBuildsPage(SeleniumTestCase): url = reverse('all-builds') self.get(url) - self.wait_until_present('td[class="target"]') + self.wait_until_visible('td[class="target"]') cell = self.find('td[class="target"]') content = cell.get_attribute('innerHTML') @@ -126,23 +192,25 @@ class TestAllBuildsPage(SeleniumTestCase): but should be shown for other builds """ build1 = Build.objects.create(**self.project1_build_success) - default_build = Build.objects.create(**self.default_project_build_success) + default_build = Build.objects.create( + **self.default_project_build_success) url = reverse('all-builds') self.get(url) - # shouldn't see a rebuild button for command-line builds - selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id - run_again_button = self.find_all(selector) - self.assertEqual(len(run_again_button), 0, - 'should not see a rebuild button for cli builds') - # should see a rebuild button for non-command-line builds + self.wait_until_visible('#allbuildstable tbody tr') selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id run_again_button = self.find_all(selector) self.assertEqual(len(run_again_button), 1, 'should see a rebuild button for non-cli builds') + # shouldn't see a rebuild button for command-line builds + selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id + run_again_button = self.find_all(selector) + self.assertEqual(len(run_again_button), 0, + 'should not see a rebuild button for cli builds') + def test_tooltips_on_project_name(self): """ Test tooltips shown next to project name in the main table @@ -156,6 +224,7 @@ class TestAllBuildsPage(SeleniumTestCase): url = reverse('all-builds') self.get(url) + self.wait_until_visible('#allbuildstable', poll=3) # get the project name cells from the table cells = self.find_all('#allbuildstable td[class="project"]') @@ -164,7 +233,7 @@ class TestAllBuildsPage(SeleniumTestCase): for cell in cells: content = cell.get_attribute('innerHTML') - help_icons = cell.find_elements_by_css_selector(selector) + help_icons = cell.find_elements(By.CSS_SELECTOR, selector) if re.search(self.PROJECT_NAME, content): # no help icon next to non-cli project name @@ -184,38 +253,224 @@ class TestAllBuildsPage(SeleniumTestCase): recent builds area; failed builds should not have links on the time column, or in the recent builds area """ - build1 = Build.objects.create(**self.project1_build_success) - build2 = Build.objects.create(**self.project1_build_failure) - - # add some targets to these builds so they have recipe links - # (and so we can find the row in the ToasterTable corresponding to - # a particular build) - Target.objects.create(build=build1, target='foo') - Target.objects.create(build=build2, target='bar') + build1, build2 = self._get_create_builds() url = reverse('all-builds') self.get(url) + self.wait_until_visible('#allbuildstable', poll=3) # test recent builds area for successful build element = self._get_build_time_element(build1) - links = element.find_elements_by_css_selector('a') + links = element.find_elements(By.CSS_SELECTOR, 'a') msg = 'should be a link on the build time for a successful recent build' - self.assertEquals(len(links), 1, msg) + self.assertEqual(len(links), 1, msg) # test recent builds area for failed build element = self._get_build_time_element(build2) - links = element.find_elements_by_css_selector('a') + links = element.find_elements(By.CSS_SELECTOR, 'a') msg = 'should not be a link on the build time for a failed recent build' - self.assertEquals(len(links), 0, msg) + self.assertEqual(len(links), 0, msg) # test the time column for successful build build1_row = self._get_row_for_build(build1) - links = build1_row.find_elements_by_css_selector('td.time a') + links = build1_row.find_elements(By.CSS_SELECTOR, 'td.time a') msg = 'should be a link on the build time for a successful build' - self.assertEquals(len(links), 1, msg) + self.assertEqual(len(links), 1, msg) # test the time column for failed build build2_row = self._get_row_for_build(build2) - links = build2_row.find_elements_by_css_selector('td.time a') + links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a') msg = 'should not be a link on the build time for a failed build' - self.assertEquals(len(links), 0, msg) + self.assertEqual(len(links), 0, msg) + + def test_builds_table_search_box(self): + """ Test the search box in the builds table on the all builds page """ + self._get_create_builds() + + url = reverse('all-builds') + self.get(url) + + # Check search box is present and works + self.wait_until_visible('#allbuildstable tbody tr') + search_box = self.find('#search-input-allbuildstable') + self.assertTrue(search_box.is_displayed()) + + # Check that we can search for a build by recipe name + search_box.send_keys('foo') + search_btn = self.find('#search-submit-allbuildstable') + search_btn.click() + self.wait_until_visible('#allbuildstable tbody tr') + rows = self.find_all('#allbuildstable tbody tr') + self.assertTrue(len(rows) >= 1) + + def test_filtering_on_failure_tasks_column(self): + """ Test the filtering on failure tasks column in the builds table on the all builds page """ + def _check_if_filter_failed_tasks_column_is_visible(): + # check if failed tasks filter column is visible, if not click on it + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + filter_fails_task_checkbox = self.find('#checkbox-failed_tasks') + if not filter_fails_task_checkbox.is_selected(): + filter_fails_task_checkbox.click() + edit_column.click() + + self._get_create_builds(success=10, failure=10) + + url = reverse('all-builds') + self.get(url) + + # Check filtering on failure tasks column + self.wait_until_visible('#allbuildstable tbody tr') + _check_if_filter_failed_tasks_column_is_visible() + failed_tasks_filter = self.find('#failed_tasks_filter') + failed_tasks_filter.click() + # Check popup is visible + self.wait_until_visible('#filter-modal-allbuildstable') + self.assertTrue( + self.find('#filter-modal-allbuildstable').is_displayed()) + # Check that we can filter by failure tasks + build_without_failure_tasks = self.find( + '#failed_tasks_filter\\:without_failed_tasks') + build_without_failure_tasks.click() + # click on apply button + self.find('#filter-modal-allbuildstable .btn-primary').click() + self.wait_until_visible('#allbuildstable tbody tr') + # Check if filter is applied, by checking if failed_tasks_filter has btn-primary class + self.assertTrue(self.find('#failed_tasks_filter').get_attribute( + 'class').find('btn-primary') != -1) + + def test_filtering_on_completedOn_column(self): + """ Test the filtering on completed_on column in the builds table on the all builds page """ + self._get_create_builds(success=10, failure=10) + + url = reverse('all-builds') + self.get(url) + + # Check filtering on failure tasks column + self.wait_until_visible('#allbuildstable tbody tr') + completed_on_filter = self.find('#completed_on_filter') + completed_on_filter.click() + # Check popup is visible + self.wait_until_visible('#filter-modal-allbuildstable') + self.assertTrue( + self.find('#filter-modal-allbuildstable').is_displayed()) + # Check that we can filter by failure tasks + build_without_failure_tasks = self.find( + '#completed_on_filter\\:date_range') + build_without_failure_tasks.click() + # click on apply button + self.find('#filter-modal-allbuildstable .btn-primary').click() + self.wait_until_visible('#allbuildstable tbody tr') + # Check if filter is applied, by checking if completed_on_filter has btn-primary class + self.assertTrue(self.find('#completed_on_filter').get_attribute( + 'class').find('btn-primary') != -1) + + # Filter by date range + self.find('#completed_on_filter').click() + self.wait_until_visible('#filter-modal-allbuildstable') + date_ranges = self.driver.find_elements( + By.XPATH, '//input[@class="form-control hasDatepicker"]') + today = timezone.now() + yestersday = today - timezone.timedelta(days=1) + date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d')) + date_ranges[1].send_keys(today.strftime('%Y-%m-%d')) + self.find('#filter-modal-allbuildstable .btn-primary').click() + self.wait_until_visible('#allbuildstable tbody tr') + self.assertTrue(self.find('#completed_on_filter').get_attribute( + 'class').find('btn-primary') != -1) + # Check if filter is applied, number of builds displayed should be 6 + self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) >= 4) + + def test_builds_table_editColumn(self): + """ Test the edit column feature in the builds table on the all builds page """ + self._get_create_builds(success=10, failure=10) + + def test_edit_column(check_box_id): + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + url = reverse('all-builds') + self.get(url) + self.wait_until_visible('#allbuildstable tbody tr') + + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + + # Check that we can hide the edit column + test_edit_column('checkbox-errors_no') + test_edit_column('checkbox-failed_tasks') + test_edit_column('checkbox-image_files') + test_edit_column('checkbox-project') + test_edit_column('checkbox-started_on') + test_edit_column('checkbox-time') + test_edit_column('checkbox-warnings_no') + + def test_builds_table_show_rows(self): + """ Test the show rows feature in the builds table on the all builds page """ + self._get_create_builds(success=100, failure=100) + + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_visible('#allbuildstable tbody tr', poll=3) + # check at least some rows are visible + self.assertTrue( + len(self.find_all('#allbuildstable tbody tr')) > 0 + ) + + url = reverse('all-builds') + self.get(url) + self.wait_until_visible('#allbuildstable tbody tr') + + show_rows = self.driver.find_elements( + By.XPATH, + '//select[@class="form-control pagesize-allbuildstable"]' + ) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + test_show_rows(10, show_row_link) + test_show_rows(25, show_row_link) + test_show_rows(50, show_row_link) + test_show_rows(100, show_row_link) + test_show_rows(150, show_row_link) diff --git a/lib/toaster/tests/browser/test_all_projects_page.py b/lib/toaster/tests/browser/test_all_projects_page.py index 15b03400f..9ed1901cc 100644 --- a/lib/toaster/tests/browser/test_all_projects_page.py +++ b/lib/toaster/tests/browser/test_all_projects_page.py @@ -7,15 +7,20 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os import re from django.urls import reverse from django.utils import timezone +from selenium.webdriver.support.select import Select from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import BitbakeVersion, Release, Project, Build from orm.models import ProjectVariable +from selenium.webdriver.common.by import By + + class TestAllProjectsPage(SeleniumTestCase): """ Browser tests for projects page /projects/ """ @@ -25,7 +30,8 @@ class TestAllProjectsPage(SeleniumTestCase): def setUp(self): """ Add default project manually """ - project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None) + project = Project.objects.create_project( + self.CLI_BUILDS_PROJECT_NAME, None) self.default_project = project self.default_project.is_default = True self.default_project.save() @@ -35,6 +41,17 @@ class TestAllProjectsPage(SeleniumTestCase): self.release = None + def _create_projects(self, nb_project=10): + projects = [] + for i in range(1, nb_project + 1): + projects.append( + Project( + name='test project {}'.format(i), + release=self.release, + ) + ) + Project.objects.bulk_create(projects) + def _add_build_to_default_project(self): """ Add a build to the default project (not used in all tests) """ now = timezone.now() @@ -45,12 +62,14 @@ class TestAllProjectsPage(SeleniumTestCase): def _add_non_default_project(self): """ Add another project """ - bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/', branch='master', dirpath='') self.release = Release.objects.create(name='test release', branch_name='master', bitbake_version=bbv) - self.project = Project.objects.create_project(self.PROJECT_NAME, self.release) + self.project = Project.objects.create_project( + self.PROJECT_NAME, self.release) self.project.is_default = False self.project.save() @@ -62,7 +81,7 @@ class TestAllProjectsPage(SeleniumTestCase): def _get_row_for_project(self, project_name): """ Get the HTML row for a project, or None if not found """ - self.wait_until_present('#projectstable tbody tr') + self.wait_until_visible('#projectstable tbody tr', poll=3) rows = self.find_all('#projectstable tbody tr') # find the row with a project name matching the one supplied @@ -93,7 +112,8 @@ class TestAllProjectsPage(SeleniumTestCase): url = reverse('all-projects') self.get(url) - default_project_row = self._get_row_for_project(self.default_project.name) + default_project_row = self._get_row_for_project( + self.default_project.name) self.assertNotEqual(default_project_row, None, 'default project "cli builds" should be in page') @@ -113,11 +133,12 @@ class TestAllProjectsPage(SeleniumTestCase): self.wait_until_visible("#projectstable tr") # find the row for the default project - default_project_row = self._get_row_for_project(self.default_project.name) + default_project_row = self._get_row_for_project( + self.default_project.name) # check the release text for the default project selector = 'span[data-project-field="release"] span.text-muted' - element = default_project_row.find_element_by_css_selector(selector) + element = default_project_row.find_element(By.CSS_SELECTOR, selector) text = element.text.strip() self.assertEqual(text, 'Not applicable', 'release should be "not applicable" for default project') @@ -127,7 +148,7 @@ class TestAllProjectsPage(SeleniumTestCase): # check the link in the release cell for the other project selector = 'span[data-project-field="release"]' - element = other_project_row.find_element_by_css_selector(selector) + element = other_project_row.find_element(By.CSS_SELECTOR, selector) text = element.text.strip() self.assertEqual(text, self.release.name, 'release name should be shown for non-default project') @@ -148,11 +169,12 @@ class TestAllProjectsPage(SeleniumTestCase): self.wait_until_visible("#projectstable tr") # find the row for the default project - default_project_row = self._get_row_for_project(self.default_project.name) + default_project_row = self._get_row_for_project( + self.default_project.name) # check the machine cell for the default project selector = 'span[data-project-field="machine"] span.text-muted' - element = default_project_row.find_element_by_css_selector(selector) + element = default_project_row.find_element(By.CSS_SELECTOR, selector) text = element.text.strip() self.assertEqual(text, 'Not applicable', 'machine should be not applicable for default project') @@ -162,7 +184,7 @@ class TestAllProjectsPage(SeleniumTestCase): # check the link in the machine cell for the other project selector = 'span[data-project-field="machine"]' - element = other_project_row.find_element_by_css_selector(selector) + element = other_project_row.find_element(By.CSS_SELECTOR, selector) text = element.text.strip() self.assertEqual(text, self.MACHINE_NAME, 'machine name should be shown for non-default project') @@ -183,13 +205,15 @@ class TestAllProjectsPage(SeleniumTestCase): self.get(reverse('all-projects')) # find the row for the default project - default_project_row = self._get_row_for_project(self.default_project.name) + default_project_row = self._get_row_for_project( + self.default_project.name) # check the link on the name field selector = 'span[data-project-field="name"] a' - element = default_project_row.find_element_by_css_selector(selector) + element = default_project_row.find_element(By.CSS_SELECTOR, selector) link_url = element.get_attribute('href').strip() - expected_url = reverse('projectbuilds', args=(self.default_project.id,)) + expected_url = reverse( + 'projectbuilds', args=(self.default_project.id,)) msg = 'link on default project name should point to builds but was %s' % link_url self.assertTrue(link_url.endswith(expected_url), msg) @@ -198,8 +222,116 @@ class TestAllProjectsPage(SeleniumTestCase): # check the link for the other project selector = 'span[data-project-field="name"] a' - element = other_project_row.find_element_by_css_selector(selector) + element = other_project_row.find_element(By.CSS_SELECTOR, selector) link_url = element.get_attribute('href').strip() expected_url = reverse('project', args=(self.project.id,)) msg = 'link on project name should point to configuration but was %s' % link_url self.assertTrue(link_url.endswith(expected_url), msg) + + def test_allProject_table_search_box(self): + """ Test the search box in the all project table on the all projects page """ + self._create_projects() + + url = reverse('all-projects') + self.get(url) + + # Chseck search box is present and works + self.wait_until_visible('#projectstable tbody tr', poll=3) + search_box = self.find('#search-input-projectstable') + self.assertTrue(search_box.is_displayed()) + + # Check that we can search for a project by project name + search_box.send_keys('test project 10') + search_btn = self.find('#search-submit-projectstable') + search_btn.click() + self.wait_until_visible('#projectstable tbody tr', poll=3) + rows = self.find_all('#projectstable tbody tr') + self.assertTrue(len(rows) == 1) + + def test_allProject_table_editColumn(self): + """ Test the edit column feature in the projects table on the all projects page """ + self._create_projects() + + def test_edit_column(check_box_id): + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + url = reverse('all-projects') + self.get(url) + self.wait_until_visible('#projectstable tbody tr', poll=3) + + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + + # Check that we can hide the edit column + test_edit_column('checkbox-errors') + test_edit_column('checkbox-image_files') + test_edit_column('checkbox-last_build_outcome') + test_edit_column('checkbox-recipe_name') + test_edit_column('checkbox-warnings') + + def test_allProject_table_show_rows(self): + """ Test the show rows feature in the projects table on the all projects page """ + self._create_projects(nb_project=200) + + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_visible('#projectstable tbody tr', poll=3) + # check at least some rows are visible + self.assertTrue( + len(self.find_all('#projectstable tbody tr')) > 0 + ) + + url = reverse('all-projects') + self.get(url) + self.wait_until_visible('#projectstable tbody tr', poll=3) + + show_rows = self.driver.find_elements( + By.XPATH, + '//select[@class="form-control pagesize-projectstable"]' + ) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + test_show_rows(10, show_row_link) + test_show_rows(25, show_row_link) + test_show_rows(50, show_row_link) + test_show_rows(100, show_row_link) + test_show_rows(150, show_row_link) diff --git a/lib/toaster/tests/browser/test_builddashboard_page.py b/lib/toaster/tests/browser/test_builddashboard_page.py index efcd89b34..d838ce363 100644 --- a/lib/toaster/tests/browser/test_builddashboard_page.py +++ b/lib/toaster/tests/browser/test_builddashboard_page.py @@ -7,6 +7,7 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os from django.urls import reverse from django.utils import timezone @@ -15,11 +16,14 @@ from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import Project, Release, BitbakeVersion, Build, LogMessage from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable +from selenium.webdriver.common.by import By + class TestBuildDashboardPage(SeleniumTestCase): """ Tests for the build dashboard /build/X """ def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath="") release = Release.objects.create(name='release1', bitbake_version=bbv) @@ -158,6 +162,7 @@ class TestBuildDashboardPage(SeleniumTestCase): """ url = reverse('builddashboard', args=(build.id,)) self.get(url) + self.wait_until_visible('#global-nav', poll=3) def _get_build_dashboard_errors(self, build): """ @@ -183,7 +188,7 @@ class TestBuildDashboardPage(SeleniumTestCase): found = False for element in message_elements: - log_message_text = element.find_element_by_tag_name('pre').text.strip() + log_message_text = element.find_element(By.TAG_NAME, 'pre').text.strip() text_matches = (log_message_text == expected_text) log_message_pk = element.get_attribute('data-log-message-id') @@ -213,7 +218,7 @@ class TestBuildDashboardPage(SeleniumTestCase): the WebElement modal match the list of text values in expected """ # labels containing the radio buttons we're testing for - labels = modal.find_elements_by_css_selector(".radio") + labels = modal.find_elements(By.CSS_SELECTOR,".radio") labels_text = [lab.text for lab in labels] self.assertEqual(len(labels_text), len(expected)) @@ -248,7 +253,7 @@ class TestBuildDashboardPage(SeleniumTestCase): selector = '[data-role="edit-custom-image-trigger"]' self.click(selector) - modal = self.driver.find_element_by_id('edit-custom-image-modal') + modal = self.driver.find_element(By.ID, 'edit-custom-image-modal') self.wait_until_visible("#edit-custom-image-modal") # recipes we expect to see in the edit custom image modal @@ -270,7 +275,7 @@ class TestBuildDashboardPage(SeleniumTestCase): selector = '[data-role="new-custom-image-trigger"]' self.click(selector) - modal = self.driver.find_element_by_id('new-custom-image-modal') + modal = self.driver.find_element(By.ID,'new-custom-image-modal') self.wait_until_visible("#new-custom-image-modal") # recipes we expect to see in the new custom image modal diff --git a/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py b/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py index c6226d60e..675825bd4 100644 --- a/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py +++ b/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py @@ -7,6 +7,7 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os from django.urls import reverse from django.utils import timezone @@ -20,7 +21,8 @@ class TestBuildDashboardPageArtifacts(SeleniumTestCase): """ Tests for artifacts on the build dashboard /build/X """ def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath="") release = Release.objects.create(name='release1', bitbake_version=bbv) @@ -197,12 +199,12 @@ class TestBuildDashboardPageArtifacts(SeleniumTestCase): # check package count and size, link on target name selector = '[data-value="target-package-count"]' element = self.find(selector) - self.assertEquals(element.text, '1', + self.assertEqual(element.text, '1', 'package count should be shown for image builds') selector = '[data-value="target-package-size"]' element = self.find(selector) - self.assertEquals(element.text, '1.0 KB', + self.assertEqual(element.text, '1.0 KB', 'package size should be shown for image builds') selector = '[data-link="target-packages"]' diff --git a/lib/toaster/tests/browser/test_delete_project.py b/lib/toaster/tests/browser/test_delete_project.py new file mode 100644 index 000000000..1941777cc --- /dev/null +++ b/lib/toaster/tests/browser/test_delete_project.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux Inc +# +# SPDX-License-Identifier: GPL-2.0-only + +import pytest +from django.urls import reverse +from selenium.webdriver.support.ui import Select +from tests.browser.selenium_helpers import SeleniumTestCase +from orm.models import BitbakeVersion, Project, Release +from selenium.webdriver.common.by import By + +class TestDeleteProject(SeleniumTestCase): + + def setUp(self): + bitbake, _ = BitbakeVersion.objects.get_or_create( + name="master", + giturl="git://master", + branch="master", + dirpath="master") + + self.release, _ = Release.objects.get_or_create( + name="master", + description="Yocto Project master", + branch_name="master", + helptext="latest", + bitbake_version=bitbake) + + Release.objects.get_or_create( + name="foo", + description="Yocto Project foo", + branch_name="foo", + helptext="latest", + bitbake_version=bitbake) + + @pytest.mark.django_db + def test_delete_project(self): + """ Test delete a project + - Check delete modal is visible + - Check delete modal has right text + - Confirm delete + - Check project is deleted + """ + project_name = "project_to_delete" + url = reverse('newproject') + self.get(url) + self.enter_text('#new-project-name', project_name) + select = Select(self.find('#projectversion')) + select.select_by_value(str(self.release.pk)) + self.click("#create-project-button") + # We should get redirected to the new project's page with the + # notification at the top + element = self.wait_until_visible('#project-created-notification') + self.assertTrue(project_name in element.text, + "New project name not in new project notification") + self.assertTrue(Project.objects.filter(name=project_name).count(), + "New project not found in database") + + # Delete project + delete_project_link = self.driver.find_element( + By.XPATH, '//a[@href="#delete-project-modal"]') + delete_project_link.click() + + # Check delete modal is visible + self.wait_until_visible('#delete-project-modal') + + # Check delete modal has right text + modal_header_text = self.find('#delete-project-modal .modal-header').text + self.assertTrue( + "Are you sure you want to delete this project?" in modal_header_text, + "Delete project modal header text is wrong") + + modal_body_text = self.find('#delete-project-modal .modal-body').text + self.assertTrue( + "Cancel its builds currently in progress" in modal_body_text, + "Modal body doesn't contain: Cancel its builds currently in progress") + self.assertTrue( + "Remove its configuration information" in modal_body_text, + "Modal body doesn't contain: Remove its configuration information") + self.assertTrue( + "Remove its imported layers" in modal_body_text, + "Modal body doesn't contain: Remove its imported layers") + self.assertTrue( + "Remove its custom images" in modal_body_text, + "Modal body doesn't contain: Remove its custom images") + self.assertTrue( + "Remove all its build information" in modal_body_text, + "Modal body doesn't contain: Remove all its build information") + + # Confirm delete + delete_btn = self.find('#delete-project-confirmed') + delete_btn.click() + + # Check project is deleted + self.wait_until_visible('#change-notification') + delete_notification = self.find('#change-notification-msg') + self.assertTrue("You have deleted 1 project:" in delete_notification.text) + self.assertTrue(project_name in delete_notification.text) + self.assertFalse(Project.objects.filter(name=project_name).exists(), + "Project not deleted from database") diff --git a/lib/toaster/tests/browser/test_landing_page.py b/lib/toaster/tests/browser/test_landing_page.py index 8bb64b9f3..8fe5fea46 100644 --- a/lib/toaster/tests/browser/test_landing_page.py +++ b/lib/toaster/tests/browser/test_landing_page.py @@ -10,8 +10,10 @@ from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase +from selenium.webdriver.common.by import By + +from orm.models import Layer, Layer_Version, Project, Build -from orm.models import Project, Build class TestLandingPage(SeleniumTestCase): """ Tests for redirects on the landing page """ @@ -29,6 +31,130 @@ class TestLandingPage(SeleniumTestCase): self.project.is_default = True self.project.save() + def test_icon_info_visible_and_clickable(self): + """ Test that the information icon is visible and clickable """ + self.get(reverse('landing')) + info_sign = self.find('#toaster-version-info-sign') + + # check that the info sign is visible + self.assertTrue(info_sign.is_displayed()) + + # check that the info sign is clickable + # and info modal is appearing when clicking on the info sign + info_sign.click() # click on the info sign make attribute 'aria-describedby' visible + info_model_id = info_sign.get_attribute('aria-describedby') + info_modal = self.find(f'#{info_model_id}') + self.assertTrue(info_modal.is_displayed()) + self.assertTrue("Toaster version information" in info_modal.text) + + def test_documentation_link_displayed(self): + """ Test that the documentation link is displayed """ + self.get(reverse('landing')) + documentation_link = self.find('#navbar-docs > a') + + # check that the documentation link is visible + self.assertTrue(documentation_link.is_displayed()) + + # check browser open new tab toaster manual when clicking on the documentation link + self.assertEqual(documentation_link.get_attribute('target'), '_blank') + self.assertEqual( + documentation_link.get_attribute('href'), + 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual') + self.assertTrue("Documentation" in documentation_link.text) + + def test_openembedded_jumbotron_link_visible_and_clickable(self): + """ Test OpenEmbedded link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check OpenEmbedded + openembedded = jumbotron.find_element(By.LINK_TEXT, 'OpenEmbedded') + self.assertTrue(openembedded.is_displayed()) + openembedded.click() + self.assertTrue("openembedded.org" in self.driver.current_url) + + def test_bitbake_jumbotron_link_visible_and_clickable(self): + """ Test BitBake link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check BitBake + bitbake = jumbotron.find_element(By.LINK_TEXT, 'BitBake') + self.assertTrue(bitbake.is_displayed()) + bitbake.click() + self.assertTrue( + "docs.yoctoproject.org/bitbake.html" in self.driver.current_url) + + def test_yoctoproject_jumbotron_link_visible_and_clickable(self): + """ Test Yocto Project link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Yocto Project + yoctoproject = jumbotron.find_element(By.LINK_TEXT, 'Yocto Project') + self.assertTrue(yoctoproject.is_displayed()) + yoctoproject.click() + self.assertTrue("yoctoproject.org" in self.driver.current_url) + + def test_link_setup_using_toaster_visible_and_clickable(self): + """ Test big magenta button setting up and using toaster link in jumbotron + if visible and clickable + """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Big magenta button + big_magenta_button = jumbotron.find_element(By.LINK_TEXT, + 'Toaster is ready to capture your command line builds' + ) + self.assertTrue(big_magenta_button.is_displayed()) + big_magenta_button.click() + self.assertTrue( + "docs.yoctoproject.org/toaster-manual/setup-and-use.html#setting-up-and-using-toaster" in self.driver.current_url) + + def test_link_create_new_project_in_jumbotron_visible_and_clickable(self): + """ Test big blue button create new project jumbotron if visible and clickable """ + # Create a layer and a layer version to make visible the big blue button + layer = Layer.objects.create(name='bar') + Layer_Version.objects.create(layer=layer) + + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Big Blue button + big_blue_button = jumbotron.find_element(By.LINK_TEXT, + 'Create your first Toaster project to run manage builds' + ) + self.assertTrue(big_blue_button.is_displayed()) + big_blue_button.click() + self.assertTrue("toastergui/newproject/" in self.driver.current_url) + + def test_toaster_manual_link_visible_and_clickable(self): + """ Test Read the Toaster manual link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Read the Toaster manual + toaster_manual = jumbotron.find_element( + By.LINK_TEXT, 'Read the Toaster manual') + self.assertTrue(toaster_manual.is_displayed()) + toaster_manual.click() + self.assertTrue( + "https://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual" in self.driver.current_url) + + def test_contrib_to_toaster_link_visible_and_clickable(self): + """ Test Contribute to Toaster link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Contribute to Toaster + contribute_to_toaster = jumbotron.find_element( + By.LINK_TEXT, 'Contribute to Toaster') + self.assertTrue(contribute_to_toaster.is_displayed()) + contribute_to_toaster.click() + self.assertTrue( + "wiki.yoctoproject.org/wiki/contribute_to_toaster" in str(self.driver.current_url).lower()) + def test_only_default_project(self): """ No projects except default @@ -87,10 +213,9 @@ class TestLandingPage(SeleniumTestCase): self.get(reverse('landing')) + self.wait_until_visible("#latest-builds", poll=3) elements = self.find_all('#allbuildstable') self.assertEqual(len(elements), 1, 'should redirect to builds') content = self.get_page_source() self.assertTrue(self.PROJECT_NAME in content, 'should show builds for project %s' % self.PROJECT_NAME) - self.assertFalse(self.CLI_BUILDS_PROJECT_NAME in content, - 'should not show builds for cli project') diff --git a/lib/toaster/tests/browser/test_layerdetails_page.py b/lib/toaster/tests/browser/test_layerdetails_page.py index 71bdd2aaf..5c29548b7 100644 --- a/lib/toaster/tests/browser/test_layerdetails_page.py +++ b/lib/toaster/tests/browser/test_layerdetails_page.py @@ -8,6 +8,7 @@ # from django.urls import reverse +from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import Layer, Layer_Version, Project, LayerSource, Release @@ -63,11 +64,12 @@ class TestLayerDetailsPage(SeleniumTestCase): args=(self.project.pk, self.imported_layer_version.pk)) - def test_edit_layerdetails(self): + def _edit_layerdetails(self): """ Edit all the editable fields for the layer refresh the page and check that the new values exist""" self.get(self.url) + self.wait_until_visible("#add-remove-layer-btn") self.click("#add-remove-layer-btn") self.click("#edit-layer-source") @@ -97,13 +99,26 @@ class TestLayerDetailsPage(SeleniumTestCase): "Expecting any of \"%s\"but got \"%s\"" % (self.initial_values, value)) + # Make sure the input visible beofre sending keys + self.wait_until_visible("#layer-git input[type=text]") inputs.send_keys("-edited") # Save the new values for save_btn in self.find_all(".change-btn"): save_btn.click() - self.click("#save-changes-for-switch") + try: + self.wait_until_visible("#save-changes-for-switch", poll=3) + btn_save_chg_for_switch = self.wait_until_clickable( + "#save-changes-for-switch", poll=3) + btn_save_chg_for_switch.click() + except ElementClickInterceptedException: + self.skipTest( + "save-changes-for-switch click intercepted. Element not visible or maybe covered by another element.") + except TimeoutException: + self.skipTest( + "save-changes-for-switch is not clickable within the specified timeout.") + self.wait_until_visible("#edit-layer-source") # Refresh the page to see if the new values are returned @@ -132,7 +147,18 @@ class TestLayerDetailsPage(SeleniumTestCase): new_dir = "/home/test/my-meta-dir" dir_input.send_keys(new_dir) - self.click("#save-changes-for-switch") + try: + self.wait_until_visible("#save-changes-for-switch", poll=3) + btn_save_chg_for_switch = self.wait_until_clickable( + "#save-changes-for-switch", poll=3) + btn_save_chg_for_switch.click() + except ElementClickInterceptedException: + self.skipTest( + "save-changes-for-switch click intercepted. Element not properly visible or maybe behind another element.") + except TimeoutException: + self.skipTest( + "save-changes-for-switch is not clickable within the specified timeout.") + self.wait_until_visible("#edit-layer-source") # Refresh the page to see if the new values are returned @@ -142,6 +168,13 @@ class TestLayerDetailsPage(SeleniumTestCase): "Expected %s in the dir value for layer directory" % new_dir) + def test_edit_layerdetails_page(self): + try: + self._edit_layerdetails() + except ElementClickInterceptedException: + self.skipTest( + "ElementClickInterceptedException occured. Element not visible or maybe covered by another element.") + def test_delete_layer(self): """ Delete the layer """ diff --git a/lib/toaster/tests/browser/test_most_recent_builds_states.py b/lib/toaster/tests/browser/test_most_recent_builds_states.py index 7844aaa39..d7a4c3453 100644 --- a/lib/toaster/tests/browser/test_most_recent_builds_states.py +++ b/lib/toaster/tests/browser/test_most_recent_builds_states.py @@ -6,7 +6,6 @@ # # Copyright (C) 2013-2016 Intel Corporation # - from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase @@ -14,6 +13,8 @@ from tests.browser.selenium_helpers_base import Wait from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version from bldcontrol.models import BuildRequest +from selenium.webdriver.common.by import By + class TestMostRecentBuildsStates(SeleniumTestCase): """ Test states update correctly in most recent builds area """ @@ -45,13 +46,14 @@ class TestMostRecentBuildsStates(SeleniumTestCase): # build queued; check shown as queued selector = base_selector + '[data-build-state="Queued"]' element = self.wait_until_visible(selector) - self.assertRegexpMatches(element.get_attribute('innerHTML'), + self.assertRegex(element.get_attribute('innerHTML'), 'Build queued', 'build should show queued status') # waiting for recipes to be parsed build.outcome = Build.IN_PROGRESS build.recipes_to_parse = recipes_to_parse build.recipes_parsed = 0 + build.save() build_request.state = BuildRequest.REQ_INPROGRESS build_request.save() @@ -62,7 +64,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): element = self.wait_until_visible(selector) bar_selector = '#recipes-parsed-percentage-bar-%s' % build.id - bar_element = element.find_element_by_css_selector(bar_selector) + bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) self.assertEqual(bar_element.value_of_css_property('width'), '0px', 'recipe parse progress should be at 0') @@ -73,7 +75,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): self.get(url) element = self.wait_until_visible(selector) - bar_element = element.find_element_by_css_selector(bar_selector) + bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) recipe_bar_updated = lambda driver: \ bar_element.get_attribute('style') == 'width: 50%;' msg = 'recipe parse progress bar should update to 50%' @@ -94,11 +96,11 @@ class TestMostRecentBuildsStates(SeleniumTestCase): selector = base_selector + '[data-build-state="Starting"]' element = self.wait_until_visible(selector) - self.assertRegexpMatches(element.get_attribute('innerHTML'), + self.assertRegex(element.get_attribute('innerHTML'), 'Tasks starting', 'build should show "tasks starting" status') # first task finished; check tasks progress bar - task1.order = 1 + task1.outcome = Task.OUTCOME_SUCCESS task1.save() self.get(url) @@ -107,7 +109,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): element = self.wait_until_visible(selector) bar_selector = '#build-pc-done-bar-%s' % build.id - bar_element = element.find_element_by_css_selector(bar_selector) + bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) task_bar_updated = lambda driver: \ bar_element.get_attribute('style') == 'width: 50%;' @@ -115,13 +117,13 @@ class TestMostRecentBuildsStates(SeleniumTestCase): element = Wait(self.driver).until(task_bar_updated, msg) # last task finished; check tasks progress bar updates - task2.order = 2 + task2.outcome = Task.OUTCOME_SUCCESS task2.save() self.get(url) element = self.wait_until_visible(selector) - bar_element = element.find_element_by_css_selector(bar_selector) + bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) task_bar_updated = lambda driver: \ bar_element.get_attribute('style') == 'width: 100%;' msg = 'tasks progress bar should update to 100%' @@ -183,7 +185,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): selector = '[data-latest-build-result="%s"] ' \ '[data-build-state="Cancelling"]' % build.id element = self.wait_until_visible(selector) - self.assertRegexpMatches(element.get_attribute('innerHTML'), + self.assertRegex(element.get_attribute('innerHTML'), 'Cancelling the build', 'build should show "cancelling" status') # check cancelled state @@ -195,5 +197,5 @@ class TestMostRecentBuildsStates(SeleniumTestCase): selector = '[data-latest-build-result="%s"] ' \ '[data-build-state="Cancelled"]' % build.id element = self.wait_until_visible(selector) - self.assertRegexpMatches(element.get_attribute('innerHTML'), + self.assertRegex(element.get_attribute('innerHTML'), 'Build cancelled', 'build should show "cancelled" status') diff --git a/lib/toaster/tests/browser/test_new_custom_image_page.py b/lib/toaster/tests/browser/test_new_custom_image_page.py index 9906ae42a..9f0b6397f 100644 --- a/lib/toaster/tests/browser/test_new_custom_image_page.py +++ b/lib/toaster/tests/browser/test_new_custom_image_page.py @@ -6,6 +6,7 @@ # # SPDX-License-Identifier: GPL-2.0-only # +from bldcontrol.models import BuildEnvironment from django.urls import reverse from tests.browser.selenium_helpers import SeleniumTestCase @@ -18,6 +19,9 @@ class TestNewCustomImagePage(SeleniumTestCase): CUSTOM_IMAGE_NAME = 'roopa-doopa' def setUp(self): + BuildEnvironment.objects.get_or_create( + betype=BuildEnvironment.TYPE_LOCAL, + ) release = Release.objects.create( name='baz', bitbake_version=BitbakeVersion.objects.create(name='v1') @@ -41,11 +45,16 @@ class TestNewCustomImagePage(SeleniumTestCase): ) # add a fake image recipe to the layer that can be customised + builldir = os.environ.get('BUILDDIR', './') self.recipe = Recipe.objects.create( name='core-image-minimal', layer_version=layer_version, + file_path=f'{builldir}/core-image-minimal.bb', is_image=True ) + # create a tmp file for the recipe + with open(self.recipe.file_path, 'w') as f: + f.write('foo') # another project with a custom image already in it project2 = Project.objects.create(name='whoop', release=release) @@ -81,6 +90,7 @@ class TestNewCustomImagePage(SeleniumTestCase): """ url = reverse('newcustomimage', args=(self.project.id,)) self.get(url) + self.wait_until_visible('#global-nav', poll=3) self.click('button[data-recipe="%s"]' % self.recipe.id) @@ -128,7 +138,7 @@ class TestNewCustomImagePage(SeleniumTestCase): """ self._create_custom_image(self.recipe.name) element = self.wait_until_visible('#invalid-name-help') - self.assertRegexpMatches(element.text.strip(), + self.assertRegex(element.text.strip(), 'image with this name already exists') def test_new_duplicates_project_image(self): @@ -146,4 +156,4 @@ class TestNewCustomImagePage(SeleniumTestCase): self._create_custom_image(custom_image_name) element = self.wait_until_visible('#invalid-name-help') expected = 'An image with this name already exists in this project' - self.assertRegexpMatches(element.text.strip(), expected) + self.assertRegex(element.text.strip(), expected) diff --git a/lib/toaster/tests/browser/test_new_project_page.py b/lib/toaster/tests/browser/test_new_project_page.py index e20a1f686..458bb6538 100644 --- a/lib/toaster/tests/browser/test_new_project_page.py +++ b/lib/toaster/tests/browser/test_new_project_page.py @@ -6,11 +6,11 @@ # # SPDX-License-Identifier: GPL-2.0-only # - from django.urls import reverse from tests.browser.selenium_helpers import SeleniumTestCase from selenium.webdriver.support.ui import Select from selenium.common.exceptions import InvalidElementStateException +from selenium.webdriver.common.by import By from orm.models import Project, Release, BitbakeVersion @@ -47,7 +47,7 @@ class TestNewProjectPage(SeleniumTestCase): url = reverse('newproject') self.get(url) - + self.wait_until_visible('#new-project-name', poll=3) self.enter_text('#new-project-name', project_name) select = Select(self.find('#projectversion')) @@ -57,7 +57,8 @@ class TestNewProjectPage(SeleniumTestCase): # We should get redirected to the new project's page with the # notification at the top - element = self.wait_until_visible('#project-created-notification') + element = self.wait_until_visible( + '#project-created-notification', poll=3) self.assertTrue(project_name in element.text, "New project name not in new project notification") @@ -78,13 +79,20 @@ class TestNewProjectPage(SeleniumTestCase): url = reverse('newproject') self.get(url) + self.wait_until_visible('#new-project-name', poll=3) self.enter_text('#new-project-name', project_name) select = Select(self.find('#projectversion')) select.select_by_value(str(self.release.pk)) - element = self.wait_until_visible('#hint-error-project-name') + radio = self.driver.find_element(By.ID, 'type-new') + radio.click() + + self.click("#create-project-button") + + self.wait_until_present('#hint-error-project-name', poll=3) + element = self.find('#hint-error-project-name') self.assertTrue(("Project names must be unique" in element.text), "Did not find unique project name error message") diff --git a/lib/toaster/tests/browser/test_project_builds_page.py b/lib/toaster/tests/browser/test_project_builds_page.py index 51717e72d..0dba33b9c 100644 --- a/lib/toaster/tests/browser/test_project_builds_page.py +++ b/lib/toaster/tests/browser/test_project_builds_page.py @@ -7,6 +7,7 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os import re from django.urls import reverse @@ -22,7 +23,8 @@ class TestProjectBuildsPage(SeleniumTestCase): CLI_BUILDS_PROJECT_NAME = 'command line builds' def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath='') release = Release.objects.create(name='release1', bitbake_version=bbv) diff --git a/lib/toaster/tests/browser/test_project_config_page.py b/lib/toaster/tests/browser/test_project_config_page.py index 944bcb263..b9de541ef 100644 --- a/lib/toaster/tests/browser/test_project_config_page.py +++ b/lib/toaster/tests/browser/test_project_config_page.py @@ -7,10 +7,12 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os from django.urls import reverse from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import BitbakeVersion, Release, Project, ProjectVariable +from selenium.webdriver.common.by import By class TestProjectConfigsPage(SeleniumTestCase): """ Test data at /project/X/builds is displayed correctly """ @@ -21,7 +23,8 @@ class TestProjectConfigsPage(SeleniumTestCase): 'any of these characters' def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath='') release = Release.objects.create(name='release1', bitbake_version=bbv) @@ -66,7 +69,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.enter_text('#new-imagefs_types', imagefs_type) - checkboxes = self.driver.find_elements_by_xpath("//input[@class='fs-checkbox-fstypes']") + checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']") for checkbox in checkboxes: if checkbox.get_attribute("value") == "btrfs": @@ -95,7 +98,7 @@ class TestProjectConfigsPage(SeleniumTestCase): for checkbox in checkboxes: if checkbox.get_attribute("value") == "cpio": checkbox.click() - element = self.driver.find_element_by_id('new-imagefs_types') + element = self.driver.find_element(By.ID, 'new-imagefs_types') self.wait_until_visible('#new-imagefs_types') @@ -129,7 +132,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) # downloads dir path has a space - self.driver.find_element_by_id('new-dl_dir').clear() + self.driver.find_element(By.ID, 'new-dl_dir').clear() self.enter_text('#new-dl_dir', '/foo/bar a') element = self.wait_until_visible('#hintError-dl_dir') @@ -137,7 +140,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) # downloads dir path starts with ${...} but has a space - self.driver.find_element_by_id('new-dl_dir').clear() + self.driver.find_element(By.ID,'new-dl_dir').clear() self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') element = self.wait_until_visible('#hintError-dl_dir') @@ -145,18 +148,18 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) # downloads dir path starts with / - self.driver.find_element_by_id('new-dl_dir').clear() + self.driver.find_element(By.ID,'new-dl_dir').clear() self.enter_text('#new-dl_dir', '/bar/foo') - hidden_element = self.driver.find_element_by_id('hintError-dl_dir') + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') self.assertEqual(hidden_element.is_displayed(), False, 'downloads directory path valid but treated as invalid') # downloads dir path starts with ${...} - self.driver.find_element_by_id('new-dl_dir').clear() + self.driver.find_element(By.ID,'new-dl_dir').clear() self.enter_text('#new-dl_dir', '${TOPDIR}/down') - hidden_element = self.driver.find_element_by_id('hintError-dl_dir') + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') self.assertEqual(hidden_element.is_displayed(), False, 'downloads directory path valid but treated as invalid') @@ -184,7 +187,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) # path has a space - self.driver.find_element_by_id('new-sstate_dir').clear() + self.driver.find_element(By.ID, 'new-sstate_dir').clear() self.enter_text('#new-sstate_dir', '/foo/bar a') element = self.wait_until_visible('#hintError-sstate_dir') @@ -192,7 +195,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) # path starts with ${...} but has a space - self.driver.find_element_by_id('new-sstate_dir').clear() + self.driver.find_element(By.ID,'new-sstate_dir').clear() self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') element = self.wait_until_visible('#hintError-sstate_dir') @@ -200,18 +203,18 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) # path starts with / - self.driver.find_element_by_id('new-sstate_dir').clear() + self.driver.find_element(By.ID,'new-sstate_dir').clear() self.enter_text('#new-sstate_dir', '/bar/foo') - hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') self.assertEqual(hidden_element.is_displayed(), False, 'sstate directory path valid but treated as invalid') # paths starts with ${...} - self.driver.find_element_by_id('new-sstate_dir').clear() + self.driver.find_element(By.ID, 'new-sstate_dir').clear() self.enter_text('#new-sstate_dir', '${TOPDIR}/down') - hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') self.assertEqual(hidden_element.is_displayed(), False, 'sstate directory path valid but treated as invalid') diff --git a/lib/toaster/tests/browser/test_sample.py b/lib/toaster/tests/browser/test_sample.py index b0067c21c..f04f1d9a1 100644 --- a/lib/toaster/tests/browser/test_sample.py +++ b/lib/toaster/tests/browser/test_sample.py @@ -27,3 +27,13 @@ class TestSample(SeleniumTestCase): self.get(url) brand_link = self.find('.toaster-navbar-brand a.brand') self.assertEqual(brand_link.text.strip(), 'Toaster') + + def test_no_builds_message(self): + """ Test that a message is shown when there are no builds """ + url = reverse('all-builds') + self.get(url) + self.wait_until_visible('#empty-state-allbuildstable') # wait for the empty state div to appear + div_msg = self.find('#empty-state-allbuildstable .alert-info') + + msg = 'Sorry - no data found' + self.assertEqual(div_msg.text, msg) diff --git a/lib/toaster/tests/browser/test_toastertable_ui.py b/lib/toaster/tests/browser/test_toastertable_ui.py index e82d5ec65..691aca1ef 100644 --- a/lib/toaster/tests/browser/test_toastertable_ui.py +++ b/lib/toaster/tests/browser/test_toastertable_ui.py @@ -8,11 +8,13 @@ # from datetime import datetime +import os from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import BitbakeVersion, Release, Project, Build +from selenium.webdriver.common.by import By class TestToasterTableUI(SeleniumTestCase): """ @@ -33,7 +35,7 @@ class TestToasterTableUI(SeleniumTestCase): table: WebElement for a ToasterTable """ selector = 'thead a.sorted' - heading = table.find_element_by_css_selector(selector) + heading = table.find_element(By.CSS_SELECTOR, selector) return heading.get_attribute('innerHTML').strip() def _get_datetime_from_cell(self, row, selector): @@ -45,7 +47,7 @@ class TestToasterTableUI(SeleniumTestCase): selector: CSS selector to use to find the cell containing the date time string """ - cell = row.find_element_by_css_selector(selector) + cell = row.find_element(By.CSS_SELECTOR, selector) cell_text = cell.get_attribute('innerHTML').strip() return datetime.strptime(cell_text, '%d/%m/%y %H:%M') @@ -58,7 +60,8 @@ class TestToasterTableUI(SeleniumTestCase): later = now + timezone.timedelta(hours=1) even_later = later + timezone.timedelta(hours=1) - bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/', branch='master', dirpath='') release = Release.objects.create(name='test release', branch_name='master', @@ -105,7 +108,7 @@ class TestToasterTableUI(SeleniumTestCase): self.click('#checkbox-started_on') # sort by started_on column - links = table.find_elements_by_css_selector('th.started_on a') + links = table.find_elements(By.CSS_SELECTOR, 'th.started_on a') for link in links: if link.get_attribute('innerHTML').strip() == 'Started on': link.click() |