blob: ee1f5c4bb8bf8d01cf6b42ad3654c53f5f609e90 [file] [log] [blame]
#! /usr/bin/env python3 #
# BitBake Toaster UI tests implementation
#
# Copyright (C) 2023 Savoir-faire Linux
#
# SPDX-License-Identifier: GPL-2.0-only
#
import string
import random
import pytest
from django.urls import reverse
from selenium.webdriver import Keys
from selenium.webdriver.support.select import Select
from selenium.common.exceptions import NoSuchElementException, TimeoutException
from orm.models import Project
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
from selenium.webdriver.common.by import By
from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
@pytest.mark.django_db
@pytest.mark.order("last")
class TestProjectConfigTab(SeleniumFunctionalTestCase):
PROJECT_NAME = 'TestProjectConfigTab'
project_id = None
def _create_project(self, project_name, **kwargs):
""" Create/Test new project using:
- Project Name: Any string
- Release: Any string
- Merge Toaster settings: True or False
"""
release = kwargs.get('release', '3')
self.get(reverse('newproject'))
self.wait_until_visible('#new-project-name')
self.find("#new-project-name").send_keys(project_name)
select = Select(self.find("#projectversion"))
select.select_by_value(release)
# check merge toaster settings
checkbox = self.find('.checkbox-mergeattr')
if not checkbox.is_selected():
checkbox.click()
if self.PROJECT_NAME != 'TestProjectConfigTab':
# Reset project name if it's not the default one
self.PROJECT_NAME = 'TestProjectConfigTab'
self.find("#create-project-button").click()
try:
self.wait_until_visible('#hint-error-project-name', poll=3)
url = reverse('project', args=(TestProjectConfigTab.project_id, ))
self.get(url)
self.wait_until_visible('#config-nav', poll=3)
except TimeoutException:
self.wait_until_visible('#config-nav', poll=3)
def _random_string(self, length):
return ''.join(
random.choice(string.ascii_letters) for _ in range(length)
)
def _navigate_to_project_page(self):
# Navigate to project page
if TestProjectConfigTab.project_id is None:
self._create_project(project_name=self._random_string(10))
current_url = self.driver.current_url
TestProjectConfigTab.project_id = get_projectId_from_url(
current_url)
else:
url = reverse('project', args=(TestProjectConfigTab.project_id,))
self.get(url)
self.wait_until_visible('#config-nav')
def _create_builds(self):
# check search box can be use to build recipes
search_box = self.find('#build-input')
search_box.send_keys('foo')
self.find('#build-button').click()
self.wait_until_present('#latest-builds')
# loop until reach the parsing state
wait_until_build(self, 'queued cloning starting parsing failed')
lastest_builds = self.driver.find_elements(
By.XPATH,
'//div[@id="latest-builds"]/div',
)
last_build = lastest_builds[0]
self.assertTrue(
'foo' in str(last_build.text)
)
last_build = lastest_builds[0]
try:
cancel_button = last_build.find_element(
By.XPATH,
'//span[@class="cancel-build-btn pull-right alert-link"]',
)
cancel_button.click()
except NoSuchElementException:
# Skip if the build is already cancelled
pass
wait_until_build_cancelled(self)
def _get_tabs(self):
# tabs links list
return self.driver.find_elements(
By.XPATH,
'//div[@id="project-topbar"]//li'
)
def _get_config_nav_item(self, index):
config_nav = self.find('#config-nav')
return config_nav.find_elements(By.TAG_NAME, 'li')[index]
def test_project_config_nav(self):
""" Test project config tab navigation:
- Check if the menu is displayed and contains the right elements:
- Configuration
- COMPATIBLE METADATA
- Custom images
- Image recipes
- Software recipes
- Machines
- Layers
- Distro
- EXTRA CONFIGURATION
- Bitbake variables
- Actions
- Delete project
"""
self._navigate_to_project_page()
def _get_config_nav_item(index):
config_nav = self.find('#config-nav')
return config_nav.find_elements(By.TAG_NAME, 'li')[index]
def check_config_nav_item(index, item_name, url):
item = _get_config_nav_item(index)
self.assertTrue(item_name in item.text)
self.assertTrue(item.get_attribute('class') == 'active')
self.assertTrue(url in self.driver.current_url)
# check if the menu contains the right elements
# COMPATIBLE METADATA
compatible_metadata = _get_config_nav_item(1)
self.assertTrue(
"compatible metadata" in compatible_metadata.text.lower()
)
# EXTRA CONFIGURATION
extra_configuration = _get_config_nav_item(8)
self.assertTrue(
"extra configuration" in extra_configuration.text.lower()
)
# Actions
actions = _get_config_nav_item(10)
self.assertTrue("actions" in str(actions.text).lower())
conf_nav_list = [
# config
[0, 'Configuration',
f"/toastergui/project/{TestProjectConfigTab.project_id}"],
# custom images
[2, 'Custom images',
f"/toastergui/project/{TestProjectConfigTab.project_id}/customimages"],
# image recipes
[3, 'Image recipes',
f"/toastergui/project/{TestProjectConfigTab.project_id}/images"],
# software recipes
[4, 'Software recipes',
f"/toastergui/project/{TestProjectConfigTab.project_id}/softwarerecipes"],
# machines
[5, 'Machines',
f"/toastergui/project/{TestProjectConfigTab.project_id}/machines"],
# layers
[6, 'Layers',
f"/toastergui/project/{TestProjectConfigTab.project_id}/layers"],
# distro
[7, 'Distros',
f"/toastergui/project/{TestProjectConfigTab.project_id}/distros"],
# [9, 'BitBake variables', f"/toastergui/project/{TestProjectConfigTab.project_id}/configuration"], # bitbake variables
]
for index, item_name, url in conf_nav_list:
item = _get_config_nav_item(index)
if item.get_attribute('class') != 'active':
item.click()
check_config_nav_item(index, item_name, url)
def test_image_recipe_editColumn(self):
""" Test the edit column feature in image recipe table on project page """
def test_edit_column(check_box_id):
# Check that we can hide/show table column
check_box = self.find(f'#{check_box_id}')
th_class = str(check_box_id).replace('checkbox-', '')
if check_box.is_selected():
# check if column is visible in table
self.assertTrue(
self.find(
f'#imagerecipestable thead th.{th_class}'
).is_displayed(),
f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
)
check_box.click()
# check if column is hidden in table
self.assertFalse(
self.find(
f'#imagerecipestable thead th.{th_class}'
).is_displayed(),
f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
)
else:
# check if column is hidden in table
self.assertFalse(
self.find(
f'#imagerecipestable thead th.{th_class}'
).is_displayed(),
f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
)
check_box.click()
# check if column is visible in table
self.assertTrue(
self.find(
f'#imagerecipestable thead th.{th_class}'
).is_displayed(),
f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
)
self._navigate_to_project_page()
# navigate to project image recipe page
recipe_image_page_link = self._get_config_nav_item(3)
recipe_image_page_link.click()
self.wait_until_present('#imagerecipestable tbody tr')
# Check edit column
edit_column = self.find('#edit-columns-button')
self.assertTrue(edit_column.is_displayed())
edit_column.click()
# Check dropdown is visible
self.wait_until_visible('ul.dropdown-menu.editcol')
# Check that we can hide the edit column
test_edit_column('checkbox-get_description_or_summary')
test_edit_column('checkbox-layer_version__get_vcs_reference')
test_edit_column('checkbox-layer_version__layer__name')
test_edit_column('checkbox-license')
test_edit_column('checkbox-recipe-file')
test_edit_column('checkbox-section')
test_edit_column('checkbox-version')
def test_image_recipe_show_rows(self):
""" Test the show rows feature in image recipe table on project page """
def test_show_rows(row_to_show, show_row_link):
# Check that we can show rows == row_to_show
show_row_link.select_by_value(str(row_to_show))
self.wait_until_visible('#imagerecipestable tbody tr')
self.assertTrue(
len(self.find_all('#imagerecipestable tbody tr')) == row_to_show
)
self._navigate_to_project_page()
# navigate to project image recipe page
recipe_image_page_link = self._get_config_nav_item(3)
recipe_image_page_link.click()
self.wait_until_present('#imagerecipestable tbody tr')
show_rows = self.driver.find_elements(
By.XPATH,
'//select[@class="form-control pagesize-imagerecipestable"]'
)
# Check show rows
for show_row_link in show_rows:
show_row_link = Select(show_row_link)
test_show_rows(10, show_row_link)
test_show_rows(25, show_row_link)
test_show_rows(50, show_row_link)
test_show_rows(100, show_row_link)
test_show_rows(150, show_row_link)
def test_project_config_tab_right_section(self):
""" Test project config tab right section contains five blocks:
- Machine:
- check 'Machine' is displayed
- check can change Machine
- Distro:
- check 'Distro' is displayed
- check can change Distro
- Most built recipes:
- check 'Most built recipes' is displayed
- check can select a recipe and build it
- Project release:
- check 'Project release' is displayed
- check project has right release displayed
- Layers:
- check can add a layer if exists
- check at least three layers are displayed
- openembedded-core
- meta-poky
- meta-yocto-bsp
"""
# Create a new project for this test
project_name = self._random_string(10)
self._create_project(project_name=project_name)
# check if the menu is displayed
self.wait_until_visible('#project-page')
block_l = self.driver.find_element(
By.XPATH, '//*[@id="project-page"]/div[2]')
project_release = self.driver.find_element(
By.XPATH, '//*[@id="project-page"]/div[1]/div[4]')
layers = block_l.find_element(By.ID, 'layer-container')
def check_machine_distro(self, item_name, new_item_name, block_id):
block = self.find(f'#{block_id}')
title = block.find_element(By.TAG_NAME, 'h3')
self.assertTrue(item_name.capitalize() in title.text)
edit_btn = self.find(f'#change-{item_name}-toggle')
edit_btn.click()
self.wait_until_visible(f'#{item_name}-change-input')
name_input = self.find(f'#{item_name}-change-input')
name_input.clear()
name_input.send_keys(new_item_name)
change_btn = self.find(f'#{item_name}-change-btn')
change_btn.click()
self.wait_until_visible(f'#project-{item_name}-name')
project_name = self.find(f'#project-{item_name}-name')
self.assertTrue(new_item_name in project_name.text)
# check change notificaiton is displayed
change_notification = self.find('#change-notification')
self.assertTrue(
f'You have changed the {item_name} to: {new_item_name}' in change_notification.text
)
# Machine
check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section')
# Distro
check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section')
# Project release
title = project_release.find_element(By.TAG_NAME, 'h3')
self.assertTrue("Project release" in title.text)
self.assertTrue(
"Yocto Project master" in self.find('#project-release-title').text
)
# Layers
title = layers.find_element(By.TAG_NAME, 'h3')
self.assertTrue("Layers" in title.text)
# check at least three layers are displayed
# openembedded-core
# meta-poky
# meta-yocto-bsp
layers_list = layers.find_element(By.ID, 'layers-in-project-list')
layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
# remove all layers except the first three layers
for i in range(3, len(layers_list_items)):
layers_list_items[i].find_element(By.TAG_NAME, 'span').click()
# check can add a layer if exists
add_layer_input = layers.find_element(By.ID, 'layer-add-input')
add_layer_input.send_keys('meta-oe')
self.wait_until_visible('#layer-container > form > div > span > div')
dropdown_item = self.driver.find_element(
By.XPATH,
'//*[@id="layer-container"]/form/div/span/div'
)
dropdown_item.click()
add_layer_btn = layers.find_element(By.ID, 'add-layer-btn')
add_layer_btn.click()
self.wait_until_visible('#layers-in-project-list')
# check layer is added
layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
self.assertTrue(len(layers_list_items) == 4)
def test_most_build_recipes(self):
""" Test most build recipes block contains"""
def rebuild_from_most_build_recipes(recipe_list_items):
checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input')
checkbox.click()
build_btn = self.find('#freq-build-btn')
build_btn.click()
self.wait_until_visible('#latest-builds')
wait_until_build(self, 'queued cloning starting parsing failed')
lastest_builds = self.driver.find_elements(
By.XPATH,
'//div[@id="latest-builds"]/div'
)
self.assertTrue(len(lastest_builds) >= 2)
last_build = lastest_builds[0]
try:
cancel_button = last_build.find_element(
By.XPATH,
'//span[@class="cancel-build-btn pull-right alert-link"]',
)
cancel_button.click()
except NoSuchElementException:
# Skip if the build is already cancelled
pass
wait_until_build_cancelled(self)
# Create a new project for remaining asserts
project_name = self._random_string(10)
self._create_project(project_name=project_name, release='2')
current_url = self.driver.current_url
TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
url = current_url.split('?')[0]
# Create a new builds
self._create_builds()
# back to project page
self.driver.get(url)
self.wait_until_visible('#project-page', poll=3)
# Most built recipes
most_built_recipes = self.driver.find_element(
By.XPATH, '//*[@id="project-page"]/div[1]/div[3]')
title = most_built_recipes.find_element(By.TAG_NAME, 'h3')
self.assertTrue("Most built recipes" in title.text)
# check can select a recipe and build it
self.wait_until_visible('#freq-build-list', poll=3)
recipe_list = self.find('#freq-build-list')
recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li')
self.assertTrue(
len(recipe_list_items) > 0,
msg="Any recipes found in the most built recipes list",
)
rebuild_from_most_build_recipes(recipe_list_items)
TestProjectConfigTab.project_id = None # reset project id
def test_project_page_tab_importlayer(self):
""" Test project page tab import layer """
self._navigate_to_project_page()
# navigate to "Import layers" tab
import_layers_tab = self._get_tabs()[2]
import_layers_tab.find_element(By.TAG_NAME, 'a').click()
self.wait_until_visible('#layer-git-repo-url')
# Check git repo radio button
git_repo_radio = self.find('#git-repo-radio')
git_repo_radio.click()
# Set git repo url
input_repo_url = self.find('#layer-git-repo-url')
input_repo_url.send_keys('git://git.yoctoproject.org/meta-fake')
# Blur the input to trigger the validation
input_repo_url.send_keys(Keys.TAB)
# Check name is set
input_layer_name = self.find('#import-layer-name')
self.assertTrue(input_layer_name.get_attribute('value') == 'meta-fake')
# Set branch
input_branch = self.find('#layer-git-ref')
input_branch.send_keys('master')
# Import layer
self.find('#import-and-add-btn').click()
# Check layer is added
self.wait_until_visible('#layer-container')
block_l = self.driver.find_element(
By.XPATH, '//*[@id="project-page"]/div[2]')
layers = block_l.find_element(By.ID, 'layer-container')
layers_list = layers.find_element(By.ID, 'layers-in-project-list')
layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
self.assertTrue(
'meta-fake' in str(layers_list_items[-1].text)
)
def test_project_page_custom_image_no_image(self):
""" Test project page tab "New custom image" when no custom image """
project_name = self._random_string(10)
self._create_project(project_name=project_name)
current_url = self.driver.current_url
TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
# navigate to "Custom image" tab
custom_image_section = self._get_config_nav_item(2)
custom_image_section.click()
self.wait_until_visible('#empty-state-customimagestable')
# Check message when no custom image
self.assertTrue(
"You have not created any custom images yet." in str(
self.find('#empty-state-customimagestable').text
)
)
div_empty_msg = self.find('#empty-state-customimagestable')
link_create_custom_image = div_empty_msg.find_element(
By.TAG_NAME, 'a')
self.assertTrue(TestProjectConfigTab.project_id is not None)
self.assertTrue(
f"/toastergui/project/{TestProjectConfigTab.project_id}/newcustomimage" in str(
link_create_custom_image.get_attribute('href')
)
)
self.assertTrue(
"Create your first custom image" in str(
link_create_custom_image.text
)
)
TestProjectConfigTab.project_id = None # reset project id
def test_project_page_image_recipe(self):
""" Test project page section images
- Check image recipes are displayed
- Check search input
- Check image recipe build button works
- Check image recipe table features(show/hide column, pagination)
"""
self._navigate_to_project_page()
# navigate to "Images section"
images_section = self._get_config_nav_item(3)
images_section.click()
self.wait_until_visible('#imagerecipestable')
rows = self.find_all('#imagerecipestable tbody tr')
self.assertTrue(len(rows) > 0)
# Test search input
self.wait_until_visible('#search-input-imagerecipestable')
recipe_input = self.find('#search-input-imagerecipestable')
recipe_input.send_keys('core-image-minimal')
self.find('#search-submit-imagerecipestable').click()
self.wait_until_visible('#imagerecipestable tbody tr')
rows = self.find_all('#imagerecipestable tbody tr')
self.assertTrue(len(rows) > 0)