#! /usr/bin/env python3 #
# BitBake Toaster UI tests implementation
#
# Copyright (C) 2023 Savoir-faire Linux
#
# SPDX-License-Identifier: GPL-2.0-only
#

import os
import string
import time
from unittest import skip
import pytest
from django.urls import reverse
from django.utils import timezone
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.select import Select
from selenium.common.exceptions import TimeoutException
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
from orm.models import Build, Project, Target
from selenium.webdriver.common.by import By

from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled

class TestProjectPageBase(SeleniumFunctionalTestCase):
    project_id = None
    PROJECT_NAME = 'TestProjectPage'

    def _navigate_to_project_page(self):
        # Navigate to project page
        if TestProjectPageBase.project_id is None:
            TestProjectPageBase.project_id = self.create_new_project(self.PROJECT_NAME, '3', None, True)

        url = reverse('project', args=(TestProjectPageBase.project_id,))
        self.get(url)
        self.wait_until_visible('#config-nav')

    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()
        self.project1_build_success = {
            'project': Project.objects.get(id=TestProjectPageBase.project_id),
            'started_on': now,
            'completed_on': now,
            'outcome': Build.SUCCEEDED
        }

        self.project1_build_failure = {
            'project': Project.objects.get(id=TestProjectPageBase.project_id),
            '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 _mixin_test_table_edit_column(
            self,
            table_id,
            edit_btn_id,
            list_check_box_id: list
    ):
        # Check edit column
        finder = lambda driver: self.find(f'#{edit_btn_id}')
        edit_column = self.wait_until_element_clickable(finder)
        self.assertTrue(edit_column.is_displayed())
        edit_column.click()
        # Check dropdown is visible
        self.wait_until_visible('ul.dropdown-menu.editcol')
        for check_box_id in list_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'#{table_id} 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'#{table_id} 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'#{table_id} 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'#{table_id} thead th.{th_class}'
                    ).is_displayed(),
                    f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
                )

    def _get_config_nav_item(self, index):
        config_nav = self.find('#config-nav')
        return config_nav.find_elements(By.TAG_NAME, 'li')[index]

    def _navigate_to_config_nav(self, nav_id, nav_index):
        # navigate to the project page
        self._navigate_to_project_page()
        # click on "Software recipe" tab
        soft_recipe = self._get_config_nav_item(nav_index)
        soft_recipe.click()
        self.wait_until_visible(f'#{nav_id}')

    def _mixin_test_table_show_rows(self, table_selector, **kwargs):
        """ Test the show rows feature in the builds table on the all builds 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(f'#{table_selector} tbody tr')
            # check at least some rows are visible
            self.assertTrue(
                len(self.find_all(f'#{table_selector} tbody tr')) > 0
            )
        self.wait_until_present(f'#{table_selector} tbody tr')
        show_rows = self.driver.find_elements(
            By.XPATH,
            f'//select[@class="form-control pagesize-{table_selector}"]'
        )
        rows_to_show = [10, 25, 50, 100, 150]
        to_skip = kwargs.get('to_skip', [])
        # Check show rows
        for show_row_link in show_rows:
            show_row_link = Select(show_row_link)
            for row_to_show in rows_to_show:
                if row_to_show not in to_skip:
                    test_show_rows(row_to_show, show_row_link)

    def _mixin_test_table_search_input(self, **kwargs):
        input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values()
        # Test search input
        self.wait_until_visible(f'#{input_selector}')
        recipe_input = self.find(f'#{input_selector}')
        recipe_input.send_keys(input_text)
        self.find(f'#{searchBtn_selector}').click()
        self.wait_until_visible(f'#{table_selector} tbody tr')
        rows = self.find_all(f'#{table_selector} tbody tr')
        self.assertTrue(len(rows) > 0)

class TestProjectPage(TestProjectPageBase):

    def test_page_header_on_project_page(self):
        """ Check page header in project page:
          - AT LEFT -> Logo of Yocto project, displayed, clickable
          - "Toaster"+" Information icon", displayed, clickable
          - "Server Icon" + "All builds", displayed, clickable
          - "Directory Icon" + "All projects", displayed, clickable
          - "Book Icon" + "Documentation", displayed, clickable
          - AT RIGHT -> button "New project", displayed, clickable
        """
        # navigate to the project page
        self._navigate_to_project_page()

        # check page header
        # AT LEFT -> Logo of Yocto project
        logo = self.driver.find_element(
            By.XPATH,
            "//div[@class='toaster-navbar-brand']",
        )
        logo_img = logo.find_element(By.TAG_NAME, 'img')
        self.assertTrue(logo_img.is_displayed(),
                        'Logo of Yocto project not found')
        self.assertIn(
            '/static/img/logo.png', str(logo_img.get_attribute('src')),
            'Logo of Yocto project not found'
        )
        # "Toaster"+" Information icon", clickable
        toaster = self.driver.find_element(
            By.XPATH,
            "//div[@class='toaster-navbar-brand']//a[@class='brand']",
        )
        self.assertTrue(toaster.is_displayed(), 'Toaster not found')
        self.assertEqual(toaster.text, 'Toaster')
        info_sign = self.find('.glyphicon-info-sign')
        self.assertTrue(info_sign.is_displayed())

        # "Server Icon" + "All builds"
        all_builds = self.find('#navbar-all-builds')
        all_builds_link = all_builds.find_element(By.TAG_NAME, 'a')
        self.assertIn("All builds", all_builds_link.text)
        self.assertIn(
            '/toastergui/builds/', str(all_builds_link.get_attribute('href'))
        )
        server_icon = all_builds.find_element(By.TAG_NAME, 'i')
        self.assertEqual(
            server_icon.get_attribute('class'), 'glyphicon glyphicon-tasks'
        )
        self.assertTrue(server_icon.is_displayed())

        # "Directory Icon" + "All projects"
        all_projects = self.find('#navbar-all-projects')
        all_projects_link = all_projects.find_element(By.TAG_NAME, 'a')
        self.assertIn("All projects", all_projects_link.text)
        self.assertIn(
            '/toastergui/projects/', str(all_projects_link.get_attribute(
                'href'))
        )
        dir_icon = all_projects.find_element(By.TAG_NAME, 'i')
        self.assertEqual(
            dir_icon.get_attribute('class'), 'icon-folder-open'
        )
        self.assertTrue(dir_icon.is_displayed())

        # "Book Icon" + "Documentation"
        toaster_docs_link = self.find('#navbar-docs')
        toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME,
                                                                'a')
        self.assertIn("Documentation", toaster_docs_link_link.text)
        self.assertEqual(
            toaster_docs_link_link.get_attribute('href'), 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual'
        )
        book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i')
        self.assertEqual(
            book_icon.get_attribute('class'), 'glyphicon glyphicon-book'
        )
        self.assertTrue(book_icon.is_displayed())

        # AT RIGHT -> button "New project"
        new_project_button = self.find('#new-project-button')
        self.assertTrue(new_project_button.is_displayed())
        self.assertEqual(new_project_button.text, 'New project')
        new_project_button.click()
        self.assertIn(
            '/toastergui/newproject/', str(self.driver.current_url)
        )

    def test_edit_project_name(self):
        """ Test edit project name:
          - Click on "Edit" icon button
          - Change project name
          - Click on "Save" button
          - Check project name is changed
        """
        # navigate to the project page
        self._navigate_to_project_page()

        # click on "Edit" icon button
        self.wait_until_visible('#project-name-container')
        finder = lambda driver: self.find('#project-change-form-toggle')
        edit_button = self.wait_until_element_clickable(finder)
        edit_button.click()
        project_name_input = self.find('#project-name-change-input')
        self.assertTrue(project_name_input.is_displayed())
        project_name_input.clear()
        project_name_input.send_keys('New Name')
        self.find('#project-name-change-btn').click()

        # check project name is changed
        self.wait_until_visible('#project-name-container')
        self.assertIn(
            'New Name', str(self.find('#project-name-container').text)
        )

    def test_project_page_tabs(self):
        """ Test project tabs:
          - "configuration" tab
          - "Builds" tab
          - "Import layers" tab
          - "New custom image" tab
          Check search box used to build recipes
        """
        # navigate to the project page
        self._navigate_to_project_page()

        # check "configuration" tab
        self.wait_until_visible('#topbar-configuration-tab')
        config_tab = self.find('#topbar-configuration-tab')
        self.assertEqual(config_tab.get_attribute('class'), 'active')
        self.assertIn('Configuration', str(config_tab.text))
        self.assertIn(
            f"/toastergui/project/{TestProjectPageBase.project_id}", str(self.driver.current_url)
        )

        def get_tabs():
            # tabs links list
            return self.driver.find_elements(
                By.XPATH,
                '//div[@id="project-topbar"]//li'
            )

        def check_tab_link(tab_index, tab_name, url):
            tab = get_tabs()[tab_index]
            tab_link = tab.find_element(By.TAG_NAME, 'a')
            self.assertIn(url, tab_link.get_attribute('href'))
            self.assertIn(tab_name, tab_link.text)
            self.assertEqual(tab.get_attribute('class'), 'active')

        # check "Builds" tab
        builds_tab = get_tabs()[1]
        builds_tab.find_element(By.TAG_NAME, 'a').click()
        check_tab_link(
            1,
            'Builds',
            f"/toastergui/project/{TestProjectPageBase.project_id}/builds"
        )

        # check "Import layers" tab
        import_layers_tab = get_tabs()[2]
        import_layers_tab.find_element(By.TAG_NAME, 'a').click()
        check_tab_link(
            2,
            'Import layer',
            f"/toastergui/project/{TestProjectPageBase.project_id}/importlayer"
        )

        # check "New custom image" tab
        new_custom_image_tab = get_tabs()[3]
        new_custom_image_tab.find_element(By.TAG_NAME, 'a').click()
        check_tab_link(
            3,
            'New custom image',
            f"/toastergui/project/{TestProjectPageBase.project_id}/newcustomimage"
        )

        # 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()
        self.wait_until_visible('#latest-builds')
        buildtext = "Loading"
        while "Loading" in buildtext:
            time.sleep(1)
            lastest_builds = self.driver.find_elements(
                By.XPATH,
                '//div[@id="latest-builds"]',
            )
            last_build = lastest_builds[0]
            buildtext = last_build.text
        self.assertIn(
            'core-image-minimal', str(last_build.text)
        )

    def test_softwareRecipe_page(self):
        """ Test software recipe page
            - Check title "Compatible software recipes" is displayed
            - Check search input
            - Check "build recipe" button works
            - Check software recipe table feature(show/hide column, pagination)
        """
        self._navigate_to_config_nav('softwarerecipestable', 4)
        # check title "Compatible software recipes" is displayed
        self.assertIn("Compatible software recipes", self.get_page_source())
        # Test search input
        self._mixin_test_table_search_input(
            input_selector='search-input-softwarerecipestable',
            input_text='busybox',
            searchBtn_selector='search-submit-softwarerecipestable',
            table_selector='softwarerecipestable'
        )
        # check "build recipe" button works
        finder = lambda driver: self.find_all('#softwarerecipestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a')
        build_btn = self.wait_until_element_clickable(finder)
        build_btn.click()
        build_state = 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) > 0)
        # Find the latest builds, the last build and then the cancel button
 
        finder = lambda driver: driver.find_elements(By.XPATH, '//div[@id="latest-builds"]/div')[0].find_element(By.XPATH, '//span[@class="cancel-build-btn pull-right alert-link"]')
        cancel_button = self.wait_until_element_clickable(finder)
        cancel_button.click()
        if 'starting' not in build_state:  # change build state when cancelled in starting state
            wait_until_build_cancelled(self)

        # check software recipe table feature(show/hide column, pagination)
        self._navigate_to_config_nav('softwarerecipestable', 4)
        column_list = [
            'get_description_or_summary',
            'layer_version__get_vcs_reference',
            'layer_version__layer__name',
            'license',
            'recipe-file',
            'section',
            'version',
        ]
        self._mixin_test_table_edit_column(
            'softwarerecipestable',
            'edit-columns-button',
            [f'checkbox-{column}' for column in column_list]
        )
        self._navigate_to_config_nav('softwarerecipestable', 4)
        # check show rows(pagination)
        self._mixin_test_table_show_rows(
            table_selector='softwarerecipestable',
            to_skip=[150],
        )

    def test_machines_page(self):
        """ Test Machine page
            - Check if title "Compatible machines" is displayed
            - Check search input
            - Check "Select machine" button works
            - Check "Add layer" button works
            - Check Machine table feature(show/hide column, pagination)
        """
        self._navigate_to_config_nav('machinestable', 5)
        # check title "Compatible software recipes" is displayed
        self.assertIn("Compatible machines", self.get_page_source())
        # Test search input
        self._mixin_test_table_search_input(
            input_selector='search-input-machinestable',
            input_text='qemux86-64',
            searchBtn_selector='search-submit-machinestable',
            table_selector='machinestable'
        )
        # check "Select machine" button works
        finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]')
        select_btn = self.wait_until_element_clickable(finder)
        select_btn.click()
        self.wait_until_visible('#project-machine-name')
        project_machine_name = self.find('#project-machine-name')
        self.assertIn(
            'qemux86-64', project_machine_name.text
        )
        # check "Add layer" button works
        self._navigate_to_config_nav('machinestable', 5)
        # Search for a machine whit layer not in project
        self._mixin_test_table_search_input(
            input_selector='search-input-machinestable',
            input_text='qemux86-64-tpm2',
            searchBtn_selector='search-submit-machinestable',
            table_selector='machinestable'
        )

        self.wait_until_visible('#machinestable tbody tr')
        # Locate a machine to add button
        finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]')
        add_btn = self.wait_until_element_clickable(finder)
        add_btn.click()
        self.wait_until_visible('#change-notification')
        change_notification = self.find('#change-notification')
        self.assertIn(
            f'You have added 1 layer to your project', str(change_notification.text)
        )

        finder = lambda driver: self.find('#hide-alert')
        hide_button = self.wait_until_element_clickable(finder)
        hide_button.click()
        self.wait_until_not_visible('#change-notification')

        # check Machine table feature(show/hide column, pagination)
        self._navigate_to_config_nav('machinestable', 5)
        column_list = [
            'description',
            'layer_version__get_vcs_reference',
            'layer_version__layer__name',
            'machinefile',
        ]
        self._mixin_test_table_edit_column(
            'machinestable',
            'edit-columns-button',
            [f'checkbox-{column}' for column in column_list]
        )
        self._navigate_to_config_nav('machinestable', 5)
        # check show rows(pagination)
        self._mixin_test_table_show_rows(
            table_selector='machinestable',
            to_skip=[150],
        )

    def test_layers_page(self):
        """ Test layers page
            - Check if title "Compatible layerss" is displayed
            - Check search input
            - Check "Add layer" button works
            - Check "Remove layer" button works
            - Check layers table feature(show/hide column, pagination)
        """
        self._navigate_to_config_nav('layerstable', 6)
        # check title "Compatible layers" is displayed
        self.assertIn("Compatible layers", self.get_page_source())
        # Test search input
        input_text='meta-tanowrt'
        self._mixin_test_table_search_input(
            input_selector='search-input-layerstable',
            input_text=input_text,
            searchBtn_selector='search-submit-layerstable',
            table_selector='layerstable'
        )
        # check "Add layer" button works
        self.wait_until_visible('#layerstable tbody tr')
        finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="add"]')
        add_btn = self.wait_until_element_clickable(finder)
        add_btn.click()
        # check modal is displayed
        self.wait_until_visible('#dependencies-modal')
        list_dependencies = self.find_all('#dependencies-list li')
        # click on add-layers button
        finder = lambda driver: self.driver.find_element(By.XPATH, '//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]')
        add_layers_btn = self.wait_until_element_clickable(finder)
        add_layers_btn.click()
        self.wait_until_visible('#change-notification')
        change_notification = self.find('#change-notification')
        self.assertIn(
            f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies', str(change_notification.text)
        )

        finder = lambda driver: self.find('#hide-alert')
        hide_button = self.wait_until_element_clickable(finder)
        hide_button.click()
        self.wait_until_not_visible('#change-notification')

        # check "Remove layer" button works
        self.wait_until_visible('#layerstable tbody tr')
        finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="remove"]')
        remove_btn = self.wait_until_element_clickable(finder)
        remove_btn.click()
        self.wait_until_visible('#change-notification')
        change_notification = self.find('#change-notification')
        self.assertIn(
            f'You have removed 1 layer from your project: {input_text}', str(change_notification.text)
        )

        finder = lambda driver: self.find('#hide-alert')
        hide_button = self.wait_until_element_clickable(finder)
        hide_button.click()
        self.wait_until_not_visible('#change-notification')

        # check layers table feature(show/hide column, pagination)
        self._navigate_to_config_nav('layerstable', 6)
        column_list = [
            'dependencies',
            'revision',
            'layer__vcs_url',
            'git_subdir',
            'layer__summary',
        ]
        self._mixin_test_table_edit_column(
            'layerstable',
            'edit-columns-button',
            [f'checkbox-{column}' for column in column_list]
        )
        self._navigate_to_config_nav('layerstable', 6)
        # check show rows(pagination)
        self._mixin_test_table_show_rows(
            table_selector='layerstable',
            to_skip=[150],
        )

    def test_distro_page(self):
        """ Test distros page
            - Check if title "Compatible distros" is displayed
            - Check search input
            - Check "Add layer" button works
            - Check distro table feature(show/hide column, pagination)
        """
        self._navigate_to_config_nav('distrostable', 7)
        # check title "Compatible distros" is displayed
        self.assertIn("Compatible Distros", self.get_page_source())
        # Test search input
        input_text='poky-altcfg'
        self._mixin_test_table_search_input(
            input_selector='search-input-distrostable',
            input_text=input_text,
            searchBtn_selector='search-submit-distrostable',
            table_selector='distrostable'
        )
        # check "Add distro" button works
        self.wait_until_visible(".add-del-layers")
        finder = lambda driver: self.find_all('#distrostable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]')
        add_btn = self.wait_until_element_clickable(finder)
        add_btn.click()
        self.wait_until_visible('#change-notification')
        change_notification = self.find('#change-notification')
        self.assertIn(
            f'You have changed the distro to: {input_text}', str(change_notification.text)
        )
        # check distro table feature(show/hide column, pagination)
        self._navigate_to_config_nav('distrostable', 7)
        column_list = [
            'description',
            'templatefile',
            'layer_version__get_vcs_reference',
            'layer_version__layer__name',
        ]
        self._mixin_test_table_edit_column(
            'distrostable',
            'edit-columns-button',
            [f'checkbox-{column}' for column in column_list]
        )
        self._navigate_to_config_nav('distrostable', 7)
        # check show rows(pagination)
        self._mixin_test_table_show_rows(
            table_selector='distrostable',
            to_skip=[150],
        )

    def test_single_layer_page(self):
        """ Test layer details page using meta-poky as an example (assumes is added to start with)
            - Check if title is displayed
            - Check add/remove layer button works
            - Check tabs(layers, recipes, machines) are displayed
            - Check left section is displayed
                - Check layer name
                - Check layer summary
                - Check layer description
        """
        self._navigate_to_config_nav('layerstable', 6)
        layer_link = self.driver.find_element(By.XPATH, '//tr/td[@class="layer__name"]/a[contains(text(),"meta-poky")]')
        layer_link.click()
        self.wait_until_visible('.page-header')
        # check title is displayed
        self.assertTrue(self.find('.page-header h1').is_displayed())

        # check remove layer button works
        finder = lambda driver: self.find('#add-remove-layer-btn')
        remove_layer_btn = self.wait_until_element_clickable(finder)
        remove_layer_btn.click()
        self.wait_until_visible('#change-notification')
        change_notification = self.find('#change-notification')
        self.assertIn(
            f'You have removed 1 layer from your project', str(change_notification.text)
        )
        finder = lambda driver: self.find('#hide-alert')
        hide_button = self.wait_until_element_clickable(finder)
        hide_button.click()
        # check add layer button works
        self.wait_until_not_visible('#change-notification')
        finder = lambda driver: self.find('#add-remove-layer-btn')
        add_layer_btn = self.wait_until_element_clickable(finder)
        add_layer_btn.click()
        self.wait_until_visible('#change-notification')
        change_notification = self.find('#change-notification')
        self.assertIn(
            f'You have added 1 layer to your project', str(change_notification.text)
        )
        finder = lambda driver: self.find('#hide-alert')
        hide_button = self.wait_until_element_clickable(finder)
        hide_button.click()
        self.wait_until_not_visible('#change-notification')
        # check tabs(layers, recipes, machines) are displayed
        tabs = self.find_all('.nav-tabs li')
        self.assertEqual(len(tabs), 3)
        # Check first tab
        tabs[0].click()
        self.assertIn(
            'active', str(self.find('#information').get_attribute('class'))
        )
        # Check second tab (recipes)
        self.wait_until_visible('.nav-tabs')
        # Ensure page is scrolled to the top
        self.driver.execute_script('window.scrollTo({behavior: "instant", top: 0, left: 0})')
        tabs[1].click()
        self.assertIn(
            'active', str(self.find('#recipes').get_attribute('class'))
        )
        # Check third tab (machines)
        self.wait_until_visible('.nav-tabs')
        # Ensure page is scrolled to the top
        self.driver.execute_script('window.scrollTo({behavior: "instant", top: 0, left: 0})')
        tabs[2].click()
        self.assertIn(
            'active', str(self.find('#machines').get_attribute('class'))
        )
        # Check left section is displayed
        section = self.find('.well')
        # Check layer name
        self.assertTrue(
            section.find_element(By.XPATH, '//h2[1]').is_displayed()
        )
        # Check layer summary
        self.assertIn("Summary", section.text)
        # Check layer description
        self.assertIn("Description", section.text)

@pytest.mark.django_db
@pytest.mark.order("last")
class TestProjectPageRecipes(TestProjectPageBase):

    def test_single_recipe_page(self):
        """ Test recipe page
            - Check if title is displayed
            - Check add recipe layer displayed
            - Check left section is displayed
                - Check recipe: name, summary, description, Version, Section,
                License, Approx. packages included, Approx. size, Recipe file
        """
        # Use a recipe which is likely to exist in the layer index but not enabled
        # in poky out the box - xen-image-minimal from meta-virtualization
        self._navigate_to_project_page()
        prj = Project.objects.get(pk=TestProjectPageBase.project_id)
        recipe_id = prj.get_all_compatible_recipes().get(name="xen-image-minimal").pk
        url = reverse("recipedetails", args=(TestProjectPageBase.project_id, recipe_id))
        self.get(url)
        self.wait_until_visible('.page-header')
        # check title is displayed
        self.assertTrue(self.find('.page-header h1').is_displayed())
        # check add recipe layer displayed
        add_recipe_layer_btn = self.find('#add-layer-btn')
        self.assertTrue(add_recipe_layer_btn.is_displayed())
        # check left section is displayed
        section = self.find('.well')
        # Check recipe name
        self.assertTrue(
            section.find_element(By.XPATH, '//h2[1]').is_displayed()
        )
        # Check recipe sections details info are displayed
        self.assertIn("Summary", section.text)
        self.assertIn("Description", section.text)
        self.assertIn("Version", section.text)
        self.assertIn("Section", section.text)
        self.assertIn("License", section.text)
        self.assertIn("Approx. packages included", section.text)
        self.assertIn("Approx. package size", section.text)
        self.assertIn("Recipe file", section.text)

    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)

        url = reverse('projectimagerecipes', args=(TestProjectPageBase.project_id,))
        self.get(url)
        self.wait_until_present('#imagerecipestable tbody tr')

        column_list = [
            'get_description_or_summary', 'layer_version__get_vcs_reference',
            'layer_version__layer__name', 'license', 'recipe-file', 'section',
            'version'
        ]

        # Check that we can hide the edit column
        self._mixin_test_table_edit_column(
            'imagerecipestable',
            'edit-columns-button',
            [f'checkbox-{column}' for column in column_list]
        )

