xref: /openbmc/openbmc/poky/bitbake/lib/toaster/tests/functional/test_project_page.py (revision 8460358c3d24c71d9d38fd126c745854a6301564)
1#! /usr/bin/env python3 #
2# BitBake Toaster UI tests implementation
3#
4# Copyright (C) 2023 Savoir-faire Linux
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import os
10import string
11import time
12from unittest import skip
13import pytest
14from django.urls import reverse
15from django.utils import timezone
16from selenium.webdriver.common.keys import Keys
17from selenium.webdriver.support.select import Select
18from selenium.common.exceptions import TimeoutException
19from tests.functional.functional_helpers import SeleniumFunctionalTestCase
20from orm.models import Build, Project, Target
21from selenium.webdriver.common.by import By
22
23from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
24
25class TestProjectPageBase(SeleniumFunctionalTestCase):
26    project_id = None
27    PROJECT_NAME = 'TestProjectPage'
28
29    def _navigate_to_project_page(self):
30        # Navigate to project page
31        if TestProjectPageBase.project_id is None:
32            TestProjectPageBase.project_id = self.create_new_project(self.PROJECT_NAME, '3', None, True)
33
34        url = reverse('project', args=(TestProjectPageBase.project_id,))
35        self.get(url)
36        self.wait_until_visible('#config-nav')
37
38    def _get_create_builds(self, **kwargs):
39        """ Create a build and return the build object """
40        # parameters for builds to associate with the projects
41        now = timezone.now()
42        self.project1_build_success = {
43            'project': Project.objects.get(id=TestProjectPageBase.project_id),
44            'started_on': now,
45            'completed_on': now,
46            'outcome': Build.SUCCEEDED
47        }
48
49        self.project1_build_failure = {
50            'project': Project.objects.get(id=TestProjectPageBase.project_id),
51            'started_on': now,
52            'completed_on': now,
53            'outcome': Build.FAILED
54        }
55        build1 = Build.objects.create(**self.project1_build_success)
56        build2 = Build.objects.create(**self.project1_build_failure)
57
58        # add some targets to these builds so they have recipe links
59        # (and so we can find the row in the ToasterTable corresponding to
60        # a particular build)
61        Target.objects.create(build=build1, target='foo')
62        Target.objects.create(build=build2, target='bar')
63
64        if kwargs:
65            # Create kwargs.get('success') builds with success status with target
66            # and kwargs.get('failure') builds with failure status with target
67            for i in range(kwargs.get('success', 0)):
68                now = timezone.now()
69                self.project1_build_success['started_on'] = now
70                self.project1_build_success[
71                    'completed_on'] = now - timezone.timedelta(days=i)
72                build = Build.objects.create(**self.project1_build_success)
73                Target.objects.create(build=build,
74                                      target=f'{i}_success_recipe',
75                                      task=f'{i}_success_task')
76
77            for i in range(kwargs.get('failure', 0)):
78                now = timezone.now()
79                self.project1_build_failure['started_on'] = now
80                self.project1_build_failure[
81                    'completed_on'] = now - timezone.timedelta(days=i)
82                build = Build.objects.create(**self.project1_build_failure)
83                Target.objects.create(build=build,
84                                      target=f'{i}_fail_recipe',
85                                      task=f'{i}_fail_task')
86        return build1, build2
87
88    def _mixin_test_table_edit_column(
89            self,
90            table_id,
91            edit_btn_id,
92            list_check_box_id: list
93    ):
94        # Check edit column
95        finder = lambda driver: self.find(f'#{edit_btn_id}')
96        edit_column = self.wait_until_element_clickable(finder)
97        self.assertTrue(edit_column.is_displayed())
98        edit_column.click()
99        # Check dropdown is visible
100        self.wait_until_visible('ul.dropdown-menu.editcol')
101        for check_box_id in list_check_box_id:
102            # Check that we can hide/show table column
103            check_box = self.find(f'#{check_box_id}')
104            th_class = str(check_box_id).replace('checkbox-', '')
105            if check_box.is_selected():
106                # check if column is visible in table
107                self.assertTrue(
108                    self.find(
109                        f'#{table_id} thead th.{th_class}'
110                    ).is_displayed(),
111                    f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
112                )
113                check_box.click()
114                # check if column is hidden in table
115                self.assertFalse(
116                    self.find(
117                        f'#{table_id} thead th.{th_class}'
118                    ).is_displayed(),
119                    f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
120                )
121            else:
122                # check if column is hidden in table
123                self.assertFalse(
124                    self.find(
125                        f'#{table_id} thead th.{th_class}'
126                    ).is_displayed(),
127                    f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
128                )
129                check_box.click()
130                # check if column is visible in table
131                self.assertTrue(
132                    self.find(
133                        f'#{table_id} thead th.{th_class}'
134                    ).is_displayed(),
135                    f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
136                )
137
138    def _get_config_nav_item(self, index):
139        config_nav = self.find('#config-nav')
140        return config_nav.find_elements(By.TAG_NAME, 'li')[index]
141
142    def _navigate_to_config_nav(self, nav_id, nav_index):
143        # navigate to the project page
144        self._navigate_to_project_page()
145        # click on "Software recipe" tab
146        soft_recipe = self._get_config_nav_item(nav_index)
147        soft_recipe.click()
148        self.wait_until_visible(f'#{nav_id}')
149
150    def _mixin_test_table_show_rows(self, table_selector, **kwargs):
151        """ Test the show rows feature in the builds table on the all builds page """
152        def test_show_rows(row_to_show, show_row_link):
153            # Check that we can show rows == row_to_show
154            show_row_link.select_by_value(str(row_to_show))
155            self.wait_until_visible(f'#{table_selector} tbody tr')
156            # check at least some rows are visible
157            self.assertTrue(
158                len(self.find_all(f'#{table_selector} tbody tr')) > 0
159            )
160        self.wait_until_present(f'#{table_selector} tbody tr')
161        show_rows = self.driver.find_elements(
162            By.XPATH,
163            f'//select[@class="form-control pagesize-{table_selector}"]'
164        )
165        rows_to_show = [10, 25, 50, 100, 150]
166        to_skip = kwargs.get('to_skip', [])
167        # Check show rows
168        for show_row_link in show_rows:
169            show_row_link = Select(show_row_link)
170            for row_to_show in rows_to_show:
171                if row_to_show not in to_skip:
172                    test_show_rows(row_to_show, show_row_link)
173
174    def _mixin_test_table_search_input(self, **kwargs):
175        input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values()
176        # Test search input
177        self.wait_until_visible(f'#{input_selector}')
178        recipe_input = self.find(f'#{input_selector}')
179        recipe_input.send_keys(input_text)
180        self.find(f'#{searchBtn_selector}').click()
181        self.wait_until_visible(f'#{table_selector} tbody tr')
182        rows = self.find_all(f'#{table_selector} tbody tr')
183        self.assertTrue(len(rows) > 0)
184
185class TestProjectPage(TestProjectPageBase):
186
187    def test_page_header_on_project_page(self):
188        """ Check page header in project page:
189          - AT LEFT -> Logo of Yocto project, displayed, clickable
190          - "Toaster"+" Information icon", displayed, clickable
191          - "Server Icon" + "All builds", displayed, clickable
192          - "Directory Icon" + "All projects", displayed, clickable
193          - "Book Icon" + "Documentation", displayed, clickable
194          - AT RIGHT -> button "New project", displayed, clickable
195        """
196        # navigate to the project page
197        self._navigate_to_project_page()
198
199        # check page header
200        # AT LEFT -> Logo of Yocto project
201        logo = self.driver.find_element(
202            By.XPATH,
203            "//div[@class='toaster-navbar-brand']",
204        )
205        logo_img = logo.find_element(By.TAG_NAME, 'img')
206        self.assertTrue(logo_img.is_displayed(),
207                        'Logo of Yocto project not found')
208        self.assertIn(
209            '/static/img/logo.png', str(logo_img.get_attribute('src')),
210            'Logo of Yocto project not found'
211        )
212        # "Toaster"+" Information icon", clickable
213        toaster = self.driver.find_element(
214            By.XPATH,
215            "//div[@class='toaster-navbar-brand']//a[@class='brand']",
216        )
217        self.assertTrue(toaster.is_displayed(), 'Toaster not found')
218        self.assertEqual(toaster.text, 'Toaster')
219        info_sign = self.find('.glyphicon-info-sign')
220        self.assertTrue(info_sign.is_displayed())
221
222        # "Server Icon" + "All builds"
223        all_builds = self.find('#navbar-all-builds')
224        all_builds_link = all_builds.find_element(By.TAG_NAME, 'a')
225        self.assertIn("All builds", all_builds_link.text)
226        self.assertIn(
227            '/toastergui/builds/', str(all_builds_link.get_attribute('href'))
228        )
229        server_icon = all_builds.find_element(By.TAG_NAME, 'i')
230        self.assertEqual(
231            server_icon.get_attribute('class'), 'glyphicon glyphicon-tasks'
232        )
233        self.assertTrue(server_icon.is_displayed())
234
235        # "Directory Icon" + "All projects"
236        all_projects = self.find('#navbar-all-projects')
237        all_projects_link = all_projects.find_element(By.TAG_NAME, 'a')
238        self.assertIn("All projects", all_projects_link.text)
239        self.assertIn(
240            '/toastergui/projects/', str(all_projects_link.get_attribute(
241                'href'))
242        )
243        dir_icon = all_projects.find_element(By.TAG_NAME, 'i')
244        self.assertEqual(
245            dir_icon.get_attribute('class'), 'icon-folder-open'
246        )
247        self.assertTrue(dir_icon.is_displayed())
248
249        # "Book Icon" + "Documentation"
250        toaster_docs_link = self.find('#navbar-docs')
251        toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME,
252                                                                'a')
253        self.assertIn("Documentation", toaster_docs_link_link.text)
254        self.assertEqual(
255            toaster_docs_link_link.get_attribute('href'), 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual'
256        )
257        book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i')
258        self.assertEqual(
259            book_icon.get_attribute('class'), 'glyphicon glyphicon-book'
260        )
261        self.assertTrue(book_icon.is_displayed())
262
263        # AT RIGHT -> button "New project"
264        new_project_button = self.find('#new-project-button')
265        self.assertTrue(new_project_button.is_displayed())
266        self.assertEqual(new_project_button.text, 'New project')
267        new_project_button.click()
268        self.assertIn(
269            '/toastergui/newproject/', str(self.driver.current_url)
270        )
271
272    def test_edit_project_name(self):
273        """ Test edit project name:
274          - Click on "Edit" icon button
275          - Change project name
276          - Click on "Save" button
277          - Check project name is changed
278        """
279        # navigate to the project page
280        self._navigate_to_project_page()
281
282        # click on "Edit" icon button
283        self.wait_until_visible('#project-name-container')
284        finder = lambda driver: self.find('#project-change-form-toggle')
285        edit_button = self.wait_until_element_clickable(finder)
286        edit_button.click()
287        project_name_input = self.find('#project-name-change-input')
288        self.assertTrue(project_name_input.is_displayed())
289        project_name_input.clear()
290        project_name_input.send_keys('New Name')
291        self.find('#project-name-change-btn').click()
292
293        # check project name is changed
294        self.wait_until_visible('#project-name-container')
295        self.assertIn(
296            'New Name', str(self.find('#project-name-container').text)
297        )
298
299    def test_project_page_tabs(self):
300        """ Test project tabs:
301          - "configuration" tab
302          - "Builds" tab
303          - "Import layers" tab
304          - "New custom image" tab
305          Check search box used to build recipes
306        """
307        # navigate to the project page
308        self._navigate_to_project_page()
309
310        # check "configuration" tab
311        self.wait_until_visible('#topbar-configuration-tab')
312        config_tab = self.find('#topbar-configuration-tab')
313        self.assertEqual(config_tab.get_attribute('class'), 'active')
314        self.assertIn('Configuration', str(config_tab.text))
315        self.assertIn(
316            f"/toastergui/project/{TestProjectPageBase.project_id}", str(self.driver.current_url)
317        )
318
319        def get_tabs():
320            # tabs links list
321            return self.driver.find_elements(
322                By.XPATH,
323                '//div[@id="project-topbar"]//li'
324            )
325
326        def check_tab_link(tab_index, tab_name, url):
327            tab = get_tabs()[tab_index]
328            tab_link = tab.find_element(By.TAG_NAME, 'a')
329            self.assertIn(url, tab_link.get_attribute('href'))
330            self.assertIn(tab_name, tab_link.text)
331            self.assertEqual(tab.get_attribute('class'), 'active')
332
333        # check "Builds" tab
334        builds_tab = get_tabs()[1]
335        builds_tab.find_element(By.TAG_NAME, 'a').click()
336        check_tab_link(
337            1,
338            'Builds',
339            f"/toastergui/project/{TestProjectPageBase.project_id}/builds"
340        )
341
342        # check "Import layers" tab
343        import_layers_tab = get_tabs()[2]
344        import_layers_tab.find_element(By.TAG_NAME, 'a').click()
345        check_tab_link(
346            2,
347            'Import layer',
348            f"/toastergui/project/{TestProjectPageBase.project_id}/importlayer"
349        )
350
351        # check "New custom image" tab
352        new_custom_image_tab = get_tabs()[3]
353        new_custom_image_tab.find_element(By.TAG_NAME, 'a').click()
354        check_tab_link(
355            3,
356            'New custom image',
357            f"/toastergui/project/{TestProjectPageBase.project_id}/newcustomimage"
358        )
359
360        # check search box can be use to build recipes
361        search_box = self.find('#build-input')
362        search_box.send_keys('core-image-minimal')
363        self.find('#build-button').click()
364        self.wait_until_visible('#latest-builds')
365        buildtext = "Loading"
366        while "Loading" in buildtext:
367            time.sleep(1)
368            lastest_builds = self.driver.find_elements(
369                By.XPATH,
370                '//div[@id="latest-builds"]',
371            )
372            last_build = lastest_builds[0]
373            buildtext = last_build.text
374        self.assertIn(
375            'core-image-minimal', str(last_build.text)
376        )
377
378    def test_softwareRecipe_page(self):
379        """ Test software recipe page
380            - Check title "Compatible software recipes" is displayed
381            - Check search input
382            - Check "build recipe" button works
383            - Check software recipe table feature(show/hide column, pagination)
384        """
385        self._navigate_to_config_nav('softwarerecipestable', 4)
386        # check title "Compatible software recipes" is displayed
387        self.assertIn("Compatible software recipes", self.get_page_source())
388        # Test search input
389        self._mixin_test_table_search_input(
390            input_selector='search-input-softwarerecipestable',
391            input_text='busybox',
392            searchBtn_selector='search-submit-softwarerecipestable',
393            table_selector='softwarerecipestable'
394        )
395        # check "build recipe" button works
396        finder = lambda driver: self.find_all('#softwarerecipestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a')
397        build_btn = self.wait_until_element_clickable(finder)
398        build_btn.click()
399        build_state = wait_until_build(self, 'queued cloning starting parsing failed')
400        lastest_builds = self.driver.find_elements(
401            By.XPATH,
402            '//div[@id="latest-builds"]/div'
403        )
404        self.assertTrue(len(lastest_builds) > 0)
405        # Find the latest builds, the last build and then the cancel button
406
407        finder = lambda driver: driver.find_elements(By.XPATH, '//div[@id="latest-builds"]/div')[0].find_element(By.XPATH, '//span[@class="cancel-build-btn pull-right alert-link"]')
408        cancel_button = self.wait_until_element_clickable(finder)
409        cancel_button.click()
410        if 'starting' not in build_state:  # change build state when cancelled in starting state
411            wait_until_build_cancelled(self)
412
413        # check software recipe table feature(show/hide column, pagination)
414        self._navigate_to_config_nav('softwarerecipestable', 4)
415        column_list = [
416            'get_description_or_summary',
417            'layer_version__get_vcs_reference',
418            'layer_version__layer__name',
419            'license',
420            'recipe-file',
421            'section',
422            'version',
423        ]
424        self._mixin_test_table_edit_column(
425            'softwarerecipestable',
426            'edit-columns-button',
427            [f'checkbox-{column}' for column in column_list]
428        )
429        self._navigate_to_config_nav('softwarerecipestable', 4)
430        # check show rows(pagination)
431        self._mixin_test_table_show_rows(
432            table_selector='softwarerecipestable',
433            to_skip=[150],
434        )
435
436    def test_machines_page(self):
437        """ Test Machine page
438            - Check if title "Compatible machines" is displayed
439            - Check search input
440            - Check "Select machine" button works
441            - Check "Add layer" button works
442            - Check Machine table feature(show/hide column, pagination)
443        """
444        self._navigate_to_config_nav('machinestable', 5)
445        # check title "Compatible software recipes" is displayed
446        self.assertIn("Compatible machines", self.get_page_source())
447        # Test search input
448        self._mixin_test_table_search_input(
449            input_selector='search-input-machinestable',
450            input_text='qemux86-64',
451            searchBtn_selector='search-submit-machinestable',
452            table_selector='machinestable'
453        )
454        # check "Select machine" button works
455        finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]')
456        select_btn = self.wait_until_element_clickable(finder)
457        select_btn.click()
458        self.wait_until_visible('#project-machine-name')
459        project_machine_name = self.find('#project-machine-name')
460        self.assertIn(
461            'qemux86-64', project_machine_name.text
462        )
463        # check "Add layer" button works
464        self._navigate_to_config_nav('machinestable', 5)
465        # Search for a machine whit layer not in project
466        self._mixin_test_table_search_input(
467            input_selector='search-input-machinestable',
468            input_text='qemux86-64-tpm2',
469            searchBtn_selector='search-submit-machinestable',
470            table_selector='machinestable'
471        )
472
473        self.wait_until_visible('#machinestable tbody tr')
474        # Locate a machine to add button
475        finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]')
476        add_btn = self.wait_until_element_clickable(finder)
477        add_btn.click()
478        self.wait_until_visible('#change-notification')
479        change_notification = self.find('#change-notification')
480        self.assertIn(
481            f'You have added 1 layer to your project', str(change_notification.text)
482        )
483
484        finder = lambda driver: self.find('#hide-alert')
485        hide_button = self.wait_until_element_clickable(finder)
486        hide_button.click()
487        self.wait_until_not_visible('#change-notification')
488
489        # check Machine table feature(show/hide column, pagination)
490        self._navigate_to_config_nav('machinestable', 5)
491        column_list = [
492            'description',
493            'layer_version__get_vcs_reference',
494            'layer_version__layer__name',
495            'machinefile',
496        ]
497        self._mixin_test_table_edit_column(
498            'machinestable',
499            'edit-columns-button',
500            [f'checkbox-{column}' for column in column_list]
501        )
502        self._navigate_to_config_nav('machinestable', 5)
503        # check show rows(pagination)
504        self._mixin_test_table_show_rows(
505            table_selector='machinestable',
506            to_skip=[150],
507        )
508
509    def test_layers_page(self):
510        """ Test layers page
511            - Check if title "Compatible layerss" is displayed
512            - Check search input
513            - Check "Add layer" button works
514            - Check "Remove layer" button works
515            - Check layers table feature(show/hide column, pagination)
516        """
517        self._navigate_to_config_nav('layerstable', 6)
518        # check title "Compatible layers" is displayed
519        self.assertIn("Compatible layers", self.get_page_source())
520        # Test search input
521        input_text='meta-tanowrt'
522        self._mixin_test_table_search_input(
523            input_selector='search-input-layerstable',
524            input_text=input_text,
525            searchBtn_selector='search-submit-layerstable',
526            table_selector='layerstable'
527        )
528        # check "Add layer" button works
529        self.wait_until_visible('#layerstable tbody tr')
530        finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="add"]')
531        add_btn = self.wait_until_element_clickable(finder)
532        add_btn.click()
533        # check modal is displayed
534        self.wait_until_visible('#dependencies-modal')
535        list_dependencies = self.find_all('#dependencies-list li')
536        # click on add-layers button
537        finder = lambda driver: self.driver.find_element(By.XPATH, '//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]')
538        add_layers_btn = self.wait_until_element_clickable(finder)
539        add_layers_btn.click()
540        self.wait_until_visible('#change-notification')
541        change_notification = self.find('#change-notification')
542        self.assertIn(
543            f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies', str(change_notification.text)
544        )
545
546        finder = lambda driver: self.find('#hide-alert')
547        hide_button = self.wait_until_element_clickable(finder)
548        hide_button.click()
549        self.wait_until_not_visible('#change-notification')
550
551        # check "Remove layer" button works
552        self.wait_until_visible('#layerstable tbody tr')
553        finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="remove"]')
554        remove_btn = self.wait_until_element_clickable(finder)
555        remove_btn.click()
556        self.wait_until_visible('#change-notification')
557        change_notification = self.find('#change-notification')
558        self.assertIn(
559            f'You have removed 1 layer from your project: {input_text}', str(change_notification.text)
560        )
561
562        finder = lambda driver: self.find('#hide-alert')
563        hide_button = self.wait_until_element_clickable(finder)
564        hide_button.click()
565        self.wait_until_not_visible('#change-notification')
566
567        # check layers table feature(show/hide column, pagination)
568        self._navigate_to_config_nav('layerstable', 6)
569        column_list = [
570            'dependencies',
571            'revision',
572            'layer__vcs_url',
573            'git_subdir',
574            'layer__summary',
575        ]
576        self._mixin_test_table_edit_column(
577            'layerstable',
578            'edit-columns-button',
579            [f'checkbox-{column}' for column in column_list]
580        )
581        self._navigate_to_config_nav('layerstable', 6)
582        # check show rows(pagination)
583        self._mixin_test_table_show_rows(
584            table_selector='layerstable',
585            to_skip=[150],
586        )
587
588    def test_distro_page(self):
589        """ Test distros page
590            - Check if title "Compatible distros" is displayed
591            - Check search input
592            - Check "Add layer" button works
593            - Check distro table feature(show/hide column, pagination)
594        """
595        self._navigate_to_config_nav('distrostable', 7)
596        # check title "Compatible distros" is displayed
597        self.assertIn("Compatible Distros", self.get_page_source())
598        # Test search input
599        input_text='poky-altcfg'
600        self._mixin_test_table_search_input(
601            input_selector='search-input-distrostable',
602            input_text=input_text,
603            searchBtn_selector='search-submit-distrostable',
604            table_selector='distrostable'
605        )
606        # check "Add distro" button works
607        self.wait_until_visible(".add-del-layers")
608        finder = lambda driver: self.find_all('#distrostable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]')
609        add_btn = self.wait_until_element_clickable(finder)
610        add_btn.click()
611        self.wait_until_visible('#change-notification')
612        change_notification = self.find('#change-notification')
613        self.assertIn(
614            f'You have changed the distro to: {input_text}', str(change_notification.text)
615        )
616        # check distro table feature(show/hide column, pagination)
617        self._navigate_to_config_nav('distrostable', 7)
618        column_list = [
619            'description',
620            'templatefile',
621            'layer_version__get_vcs_reference',
622            'layer_version__layer__name',
623        ]
624        self._mixin_test_table_edit_column(
625            'distrostable',
626            'edit-columns-button',
627            [f'checkbox-{column}' for column in column_list]
628        )
629        self._navigate_to_config_nav('distrostable', 7)
630        # check show rows(pagination)
631        self._mixin_test_table_show_rows(
632            table_selector='distrostable',
633            to_skip=[150],
634        )
635
636    def test_single_layer_page(self):
637        """ Test layer details page using meta-poky as an example (assumes is added to start with)
638            - Check if title is displayed
639            - Check add/remove layer button works
640            - Check tabs(layers, recipes, machines) are displayed
641            - Check left section is displayed
642                - Check layer name
643                - Check layer summary
644                - Check layer description
645        """
646        self._navigate_to_config_nav('layerstable', 6)
647        layer_link = self.driver.find_element(By.XPATH, '//tr/td[@class="layer__name"]/a[contains(text(),"meta-poky")]')
648        layer_link.click()
649        self.wait_until_visible('.page-header')
650        # check title is displayed
651        self.assertTrue(self.find('.page-header h1').is_displayed())
652
653        # check remove layer button works
654        finder = lambda driver: self.find('#add-remove-layer-btn')
655        remove_layer_btn = self.wait_until_element_clickable(finder)
656        remove_layer_btn.click()
657        self.wait_until_visible('#change-notification')
658        change_notification = self.find('#change-notification')
659        self.assertIn(
660            f'You have removed 1 layer from your project', str(change_notification.text)
661        )
662        finder = lambda driver: self.find('#hide-alert')
663        hide_button = self.wait_until_element_clickable(finder)
664        hide_button.click()
665        # check add layer button works
666        self.wait_until_not_visible('#change-notification')
667        finder = lambda driver: self.find('#add-remove-layer-btn')
668        add_layer_btn = self.wait_until_element_clickable(finder)
669        add_layer_btn.click()
670        self.wait_until_visible('#change-notification')
671        change_notification = self.find('#change-notification')
672        self.assertIn(
673            f'You have added 1 layer to your project', str(change_notification.text)
674        )
675        finder = lambda driver: self.find('#hide-alert')
676        hide_button = self.wait_until_element_clickable(finder)
677        hide_button.click()
678        self.wait_until_not_visible('#change-notification')
679        # check tabs(layers, recipes, machines) are displayed
680        tabs = self.find_all('.nav-tabs li')
681        self.assertEqual(len(tabs), 3)
682        # Check first tab
683        tabs[0].click()
684        self.assertIn(
685            'active', str(self.find('#information').get_attribute('class'))
686        )
687        # Check second tab (recipes)
688        # Ensure page is scrolled to the top
689        self.driver.find_element(By.XPATH, '//body').send_keys(Keys.CONTROL + Keys.HOME)
690        self.wait_until_visible('.nav-tabs')
691        tabs[1].click()
692        self.assertIn(
693            'active', str(self.find('#recipes').get_attribute('class'))
694        )
695        # Check third tab (machines)
696        # Ensure page is scrolled to the top
697        self.driver.find_element(By.XPATH, '//body').send_keys(Keys.CONTROL + Keys.HOME)
698        self.wait_until_visible('.nav-tabs')
699        tabs[2].click()
700        self.assertIn(
701            'active', str(self.find('#machines').get_attribute('class'))
702        )
703        # Check left section is displayed
704        section = self.find('.well')
705        # Check layer name
706        self.assertTrue(
707            section.find_element(By.XPATH, '//h2[1]').is_displayed()
708        )
709        # Check layer summary
710        self.assertIn("Summary", section.text)
711        # Check layer description
712        self.assertIn("Description", section.text)
713
714@pytest.mark.django_db
715@pytest.mark.order("last")
716class TestProjectPageRecipes(TestProjectPageBase):
717
718    def test_single_recipe_page(self):
719        """ Test recipe page
720            - Check if title is displayed
721            - Check add recipe layer displayed
722            - Check left section is displayed
723                - Check recipe: name, summary, description, Version, Section,
724                License, Approx. packages included, Approx. size, Recipe file
725        """
726        # Use a recipe which is likely to exist in the layer index but not enabled
727        # in poky out the box - xen-image-minimal from meta-virtualization
728        self._navigate_to_project_page()
729        prj = Project.objects.get(pk=TestProjectPageBase.project_id)
730        recipe_id = prj.get_all_compatible_recipes().get(name="xen-image-minimal").pk
731        url = reverse("recipedetails", args=(TestProjectPageBase.project_id, recipe_id))
732        self.get(url)
733        self.wait_until_visible('.page-header')
734        # check title is displayed
735        self.assertTrue(self.find('.page-header h1').is_displayed())
736        # check add recipe layer displayed
737        add_recipe_layer_btn = self.find('#add-layer-btn')
738        self.assertTrue(add_recipe_layer_btn.is_displayed())
739        # check left section is displayed
740        section = self.find('.well')
741        # Check recipe name
742        self.assertTrue(
743            section.find_element(By.XPATH, '//h2[1]').is_displayed()
744        )
745        # Check recipe sections details info are displayed
746        self.assertIn("Summary", section.text)
747        self.assertIn("Description", section.text)
748        self.assertIn("Version", section.text)
749        self.assertIn("Section", section.text)
750        self.assertIn("License", section.text)
751        self.assertIn("Approx. packages included", section.text)
752        self.assertIn("Approx. package size", section.text)
753        self.assertIn("Recipe file", section.text)
754
755    def test_image_recipe_editColumn(self):
756        """ Test the edit column feature in image recipe table on project page """
757        self._get_create_builds(success=10, failure=10)
758
759        url = reverse('projectimagerecipes', args=(TestProjectPageBase.project_id,))
760        self.get(url)
761        self.wait_until_present('#imagerecipestable tbody tr')
762
763        column_list = [
764            'get_description_or_summary', 'layer_version__get_vcs_reference',
765            'layer_version__layer__name', 'license', 'recipe-file', 'section',
766            'version'
767        ]
768
769        # Check that we can hide the edit column
770        self._mixin_test_table_edit_column(
771            'imagerecipestable',
772            'edit-columns-button',
773            [f'checkbox-{column}' for column in column_list]
774        )
775
776