blob: 23012d786583f727acdb25ff9f8b449260d93722 [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
#
from time import sleep
import pytest
from django.utils import timezone
from django.urls import reverse
from selenium.webdriver import Keys
from selenium.webdriver.support.select import Select
from selenium.common.exceptions import NoSuchElementException
from orm.models import Build, Project, Target
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
from selenium.webdriver.common.by import By
@pytest.mark.django_db
class TestProjectConfigTab(SeleniumFunctionalTestCase):
def setUp(self):
self.recipe = None
super().setUp()
release = '3'
project_name = 'projectmaster'
self._create_test_new_project(
project_name,
release,
False,
)
def _create_test_new_project(
self,
project_name,
release,
merge_toaster_settings,
):
""" Create/Test new project using:
- Project Name: Any string
- Release: Any string
- Merge Toaster settings: True or False
"""
self.get(reverse('newproject'))
self.driver.find_element(By.ID,
"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 merge_toaster_settings:
if not checkbox.is_selected():
checkbox.click()
else:
if checkbox.is_selected():
checkbox.click()
self.driver.find_element(By.ID, "create-project-button").click()
@classmethod
def _wait_until_build(cls, state):
while True:
try:
last_build_state = cls.driver.find_element(
By.XPATH,
'//*[@id="latest-builds"]/div[1]//div[@class="build-state"]',
)
build_state = last_build_state.get_attribute(
'data-build-state')
state_text = state.lower().split()
if any(x in str(build_state).lower() for x in state_text):
break
except NoSuchElementException:
continue
sleep(1)
def _create_builds(self):
# check search box can be use to build recipes
search_box = self.find('#build-input')
search_box.send_keys('core-image-minimal')
self.find('#build-button').click()
sleep(1)
self.wait_until_visible('#latest-builds')
# loop until reach the parsing state
self._wait_until_build('parsing starting cloning')
lastest_builds = self.driver.find_elements(
By.XPATH,
'//div[@id="latest-builds"]/div',
)
last_build = lastest_builds[0]
self.assertTrue(
'core-image-minimal' in str(last_build.text)
)
cancel_button = last_build.find_element(
By.XPATH,
'//span[@class="cancel-build-btn pull-right alert-link"]',
)
cancel_button.click()
sleep(1)
self._wait_until_build('cancelled')
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 _get_create_builds(self, **kwargs):
""" Create a build and return the build object """
# parameters for builds to associate with the projects
now = timezone.now()
release = '3'
project_name = 'projectmaster'
self._create_test_new_project(
project_name+"2",
release,
False,
)
self.project1_build_success = {
'project': Project.objects.get(id=1),
'started_on': now,
'completed_on': now,
'outcome': Build.SUCCEEDED
}
self.project1_build_failure = {
'project': Project.objects.get(id=1),
'started_on': now,
'completed_on': now,
'outcome': Build.FAILED
}
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')
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')
return build1, build2
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
"""
# navigate to the project page
url = reverse("project", args=(1,))
self.get(url)
# check if the menu is displayed
self.wait_until_visible('#config-nav')
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 = [
[0, 'Configuration', f"/toastergui/project/1"], # config
[2, 'Custom images', f"/toastergui/project/1/customimages"], # custom images
[3, 'Image recipes', f"/toastergui/project/1/images"], # image recipes
[4, 'Software recipes', f"/toastergui/project/1/softwarerecipes"], # software recipes
[5, 'Machines', f"/toastergui/project/1/machines"], # machines
[6, 'Layers', f"/toastergui/project/1/layers"], # layers
[7, 'Distro', f"/toastergui/project/1/distro"], # distro
[9, 'BitBake variables', f"/toastergui/project/1/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_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
"""
# navigate to the project page
url = reverse("project", args=(1,))
self.get(url)
# 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]')
machine = self.find('#machine-section')
distro = self.find('#distro-section')
most_built_recipes = self.driver.find_element(
By.XPATH, '//*[@id="project-page"]/div[1]/div[3]')
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):
title = block.find_element(By.TAG_NAME, 'h3')
self.assertTrue(item_name.capitalize() in title.text)
edit_btn = block.find_element(By.ID, f'change-{item_name}-toggle')
edit_btn.click()
sleep(1)
name_input = block.find_element(By.ID, f'{item_name}-change-input')
name_input.clear()
name_input.send_keys(new_item_name)
change_btn = block.find_element(By.ID, f'{item_name}-change-btn')
change_btn.click()
sleep(1)
project_name = block.find_element(By.ID, 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)
# Distro
check_machine_distro(self, 'distro', 'poky-altcfg', distro)
# 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')
self.assertTrue(len(layers_list_items) == 3)
# 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()
sleep(1)
# check layer is added
layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
self.assertTrue(len(layers_list_items) == 4)
# Most built recipes
title = most_built_recipes.find_element(By.TAG_NAME, 'h3')
self.assertTrue("Most built recipes" in title.text)
# Create a new builds 5
self._create_builds()
# Refresh the page
self.get(url)
sleep(1) # wait for page to load
self.wait_until_visible('#project-page')
# check can select a recipe and build it
most_built_recipes = self.driver.find_element(
By.XPATH, '//*[@id="project-page"]/div[1]/div[3]')
recipe_list = most_built_recipes.find_element(By.ID, 'freq-build-list')
recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li')
self.assertTrue(
len(recipe_list_items) > 0,
msg="No recipes found in the most built recipes list",
)
checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input')
checkbox.click()
build_btn = self.find('#freq-build-btn')
build_btn.click()
sleep(1) # wait for page to load
self.wait_until_visible('#latest-builds')
self._wait_until_build('parsing starting cloning queueing')
lastest_builds = self.driver.find_elements(
By.XPATH,
'//div[@id="latest-builds"]/div'
)
last_build = lastest_builds[0]
cancel_button = last_build.find_element(
By.XPATH,
'//span[@class="cancel-build-btn pull-right alert-link"]',
)
cancel_button.click()
self.assertTrue(len(lastest_builds) == 2)
def test_project_page_tab_importlayer(self):
""" Test project page tab import layer """
# navigate to the project page
url = reverse("project", args=(1,))
self.get(url)
# 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 """
# navigate to the project page
url = reverse("project", args=(1,))
self.get(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(
f"/toastergui/project/1/newcustomimage" in str(
link_create_custom_image.get_attribute('href')
)
)
self.assertTrue(
"Create your first custom image" in str(
link_create_custom_image.text
)
)
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)
"""
# navigate to the project page
url = reverse("project", args=(1,))
self.get(url)
self.wait_until_visible('#config-nav')
# 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)
# Test build button
image_to_build = rows[0]
build_btn = image_to_build.find_element(
By.XPATH,
'//td[@class="add-del-layers"]'
)
build_btn.click()
self._wait_until_build('parsing starting cloning')
lastest_builds = self.driver.find_elements(
By.XPATH,
'//div[@id="latest-builds"]/div'
)
self.assertTrue(len(lastest_builds) > 0)
def test_image_recipe_editColumn(self):
""" Test the edit column feature in image recipe table on project 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'#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"
)
url = reverse('projectimagerecipes', args=(1,))
self.get(url)
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 """
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_present('#imagerecipestable tbody tr')
sleep(1)
self.assertTrue(
len(self.find_all('#imagerecipestable tbody tr')) == row_to_show
)
url = reverse('projectimagerecipes', args=(2,))
self.get(url)
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)