1#! /usr/bin/env python3
2#
3# BitBake Toaster Implementation
4#
5# Copyright (C) 2013-2016 Intel Corporation
6#
7# SPDX-License-Identifier: GPL-2.0-only
8#
9
10import os
11import re
12
13from django.urls import reverse
14from django.utils import timezone
15from selenium.webdriver.support.select import Select
16from tests.browser.selenium_helpers import SeleniumTestCase
17
18from orm.models import BitbakeVersion, Release, Project, Build
19from orm.models import ProjectVariable
20
21from selenium.webdriver.common.by import By
22
23
24class TestAllProjectsPage(SeleniumTestCase):
25    """ Browser tests for projects page /projects/ """
26
27    PROJECT_NAME = 'test project'
28    CLI_BUILDS_PROJECT_NAME = 'command line builds'
29    MACHINE_NAME = 'delorean'
30
31    def setUp(self):
32        """ Add default project manually """
33        project = Project.objects.create_project(
34            self.CLI_BUILDS_PROJECT_NAME, None)
35        self.default_project = project
36        self.default_project.is_default = True
37        self.default_project.save()
38
39        # this project is only set for some of the tests
40        self.project = None
41
42        self.release = None
43
44    def _create_projects(self, nb_project=10):
45        projects = []
46        for i in range(1, nb_project + 1):
47            projects.append(
48                Project(
49                    name='test project {}'.format(i),
50                    release=self.release,
51                )
52            )
53        Project.objects.bulk_create(projects)
54
55    def _add_build_to_default_project(self):
56        """ Add a build to the default project (not used in all tests) """
57        now = timezone.now()
58        build = Build.objects.create(project=self.default_project,
59                                     started_on=now,
60                                     completed_on=now)
61        build.save()
62
63    def _add_non_default_project(self):
64        """ Add another project """
65        builldir = os.environ.get('BUILDDIR', './')
66        bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/',
67                                            branch='master', dirpath='')
68        self.release = Release.objects.create(name='test release',
69                                              branch_name='master',
70                                              bitbake_version=bbv)
71        self.project = Project.objects.create_project(
72            self.PROJECT_NAME, self.release)
73        self.project.is_default = False
74        self.project.save()
75
76        # fake the MACHINE variable
77        project_var = ProjectVariable.objects.create(project=self.project,
78                                                     name='MACHINE',
79                                                     value=self.MACHINE_NAME)
80        project_var.save()
81
82    def _get_row_for_project(self, project_name):
83        """ Get the HTML row for a project, or None if not found """
84        self.wait_until_visible('#projectstable tbody tr', poll=3)
85        rows = self.find_all('#projectstable tbody tr')
86
87        # find the row with a project name matching the one supplied
88        found_row = None
89        for row in rows:
90            if re.search(project_name, row.get_attribute('innerHTML')):
91                found_row = row
92                break
93
94        return found_row
95
96    def test_default_project_hidden(self):
97        """
98        The default project should be hidden if it has no builds
99        and we should see the "no results" area
100        """
101        url = reverse('all-projects')
102        self.get(url)
103        self.wait_until_visible('#empty-state-projectstable')
104
105        rows = self.find_all('#projectstable tbody tr')
106        self.assertEqual(len(rows), 0, 'should be no projects displayed')
107
108    def test_default_project_has_build(self):
109        """ The default project should be shown if it has builds """
110        self._add_build_to_default_project()
111
112        url = reverse('all-projects')
113        self.get(url)
114
115        default_project_row = self._get_row_for_project(
116            self.default_project.name)
117
118        self.assertNotEqual(default_project_row, None,
119                            'default project "cli builds" should be in page')
120
121    def test_default_project_release(self):
122        """
123        The release for the default project should display as
124        'Not applicable'
125        """
126        # need a build, otherwise project doesn't display at all
127        self._add_build_to_default_project()
128
129        # another project to test, which should show release
130        self._add_non_default_project()
131
132        self.get(reverse('all-projects'))
133        self.wait_until_visible("#projectstable tr")
134
135        # find the row for the default project
136        default_project_row = self._get_row_for_project(
137            self.default_project.name)
138
139        # check the release text for the default project
140        selector = 'span[data-project-field="release"] span.text-muted'
141        element = default_project_row.find_element(By.CSS_SELECTOR, selector)
142        text = element.text.strip()
143        self.assertEqual(text, 'Not applicable',
144                         'release should be "not applicable" for default project')
145
146        # find the row for the default project
147        other_project_row = self._get_row_for_project(self.project.name)
148
149        # check the link in the release cell for the other project
150        selector = 'span[data-project-field="release"]'
151        element = other_project_row.find_element(By.CSS_SELECTOR, selector)
152        text = element.text.strip()
153        self.assertEqual(text, self.release.name,
154                         'release name should be shown for non-default project')
155
156    def test_default_project_machine(self):
157        """
158        The machine for the default project should display as
159        'Not applicable'
160        """
161        # need a build, otherwise project doesn't display at all
162        self._add_build_to_default_project()
163
164        # another project to test, which should show machine
165        self._add_non_default_project()
166
167        self.get(reverse('all-projects'))
168
169        self.wait_until_visible("#projectstable tr")
170
171        # find the row for the default project
172        default_project_row = self._get_row_for_project(
173            self.default_project.name)
174
175        # check the machine cell for the default project
176        selector = 'span[data-project-field="machine"] span.text-muted'
177        element = default_project_row.find_element(By.CSS_SELECTOR, selector)
178        text = element.text.strip()
179        self.assertEqual(text, 'Not applicable',
180                         'machine should be not applicable for default project')
181
182        # find the row for the default project
183        other_project_row = self._get_row_for_project(self.project.name)
184
185        # check the link in the machine cell for the other project
186        selector = 'span[data-project-field="machine"]'
187        element = other_project_row.find_element(By.CSS_SELECTOR, selector)
188        text = element.text.strip()
189        self.assertEqual(text, self.MACHINE_NAME,
190                         'machine name should be shown for non-default project')
191
192    def test_project_page_links(self):
193        """
194        Test that links for the default project point to the builds
195        page /projects/X/builds for that project, and that links for
196        other projects point to their configuration pages /projects/X/
197        """
198
199        # need a build, otherwise project doesn't display at all
200        self._add_build_to_default_project()
201
202        # another project to test
203        self._add_non_default_project()
204
205        self.get(reverse('all-projects'))
206
207        # find the row for the default project
208        default_project_row = self._get_row_for_project(
209            self.default_project.name)
210
211        # check the link on the name field
212        selector = 'span[data-project-field="name"] a'
213        element = default_project_row.find_element(By.CSS_SELECTOR, selector)
214        link_url = element.get_attribute('href').strip()
215        expected_url = reverse(
216            'projectbuilds', args=(self.default_project.id,))
217        msg = 'link on default project name should point to builds but was %s' % link_url
218        self.assertTrue(link_url.endswith(expected_url), msg)
219
220        # find the row for the other project
221        other_project_row = self._get_row_for_project(self.project.name)
222
223        # check the link for the other project
224        selector = 'span[data-project-field="name"] a'
225        element = other_project_row.find_element(By.CSS_SELECTOR, selector)
226        link_url = element.get_attribute('href').strip()
227        expected_url = reverse('project', args=(self.project.id,))
228        msg = 'link on project name should point to configuration but was %s' % link_url
229        self.assertTrue(link_url.endswith(expected_url), msg)
230
231    def test_allProject_table_search_box(self):
232        """ Test the search box in the all project table on the all projects page """
233        self._create_projects()
234
235        url = reverse('all-projects')
236        self.get(url)
237
238        # Chseck search box is present and works
239        self.wait_until_visible('#projectstable tbody tr', poll=3)
240        search_box = self.find('#search-input-projectstable')
241        self.assertTrue(search_box.is_displayed())
242
243        # Check that we can search for a project by project name
244        search_box.send_keys('test project 10')
245        search_btn = self.find('#search-submit-projectstable')
246        search_btn.click()
247        self.wait_until_visible('#projectstable tbody tr', poll=3)
248        rows = self.find_all('#projectstable tbody tr')
249        self.assertTrue(len(rows) == 1)
250
251    def test_allProject_table_editColumn(self):
252        """ Test the edit column feature in the projects table on the all projects page """
253        self._create_projects()
254
255        def test_edit_column(check_box_id):
256            # Check that we can hide/show table column
257            check_box = self.find(f'#{check_box_id}')
258            th_class = str(check_box_id).replace('checkbox-', '')
259            if check_box.is_selected():
260                # check if column is visible in table
261                self.assertTrue(
262                    self.find(
263                        f'#projectstable thead th.{th_class}'
264                    ).is_displayed(),
265                    f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
266                )
267                check_box.click()
268                # check if column is hidden in table
269                self.assertFalse(
270                    self.find(
271                        f'#projectstable thead th.{th_class}'
272                    ).is_displayed(),
273                    f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
274                )
275            else:
276                # check if column is hidden in table
277                self.assertFalse(
278                    self.find(
279                        f'#projectstable thead th.{th_class}'
280                    ).is_displayed(),
281                    f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
282                )
283                check_box.click()
284                # check if column is visible in table
285                self.assertTrue(
286                    self.find(
287                        f'#projectstable thead th.{th_class}'
288                    ).is_displayed(),
289                    f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
290                )
291        url = reverse('all-projects')
292        self.get(url)
293        self.wait_until_visible('#projectstable tbody tr', poll=3)
294
295        # Check edit column
296        edit_column = self.find('#edit-columns-button')
297        self.assertTrue(edit_column.is_displayed())
298        edit_column.click()
299        # Check dropdown is visible
300        self.wait_until_visible('ul.dropdown-menu.editcol')
301
302        # Check that we can hide the edit column
303        test_edit_column('checkbox-errors')
304        test_edit_column('checkbox-image_files')
305        test_edit_column('checkbox-last_build_outcome')
306        test_edit_column('checkbox-recipe_name')
307        test_edit_column('checkbox-warnings')
308
309    def test_allProject_table_show_rows(self):
310        """ Test the show rows feature in the projects table on the all projects page """
311        self._create_projects(nb_project=200)
312
313        def test_show_rows(row_to_show, show_row_link):
314            # Check that we can show rows == row_to_show
315            show_row_link.select_by_value(str(row_to_show))
316            self.wait_until_visible('#projectstable tbody tr', poll=3)
317            # check at least some rows are visible
318            self.assertTrue(
319                len(self.find_all('#projectstable tbody tr')) > 0
320            )
321
322        url = reverse('all-projects')
323        self.get(url)
324        self.wait_until_visible('#projectstable tbody tr', poll=3)
325
326        show_rows = self.driver.find_elements(
327            By.XPATH,
328            '//select[@class="form-control pagesize-projectstable"]'
329        )
330        # Check show rows
331        for show_row_link in show_rows:
332            show_row_link = Select(show_row_link)
333            test_show_rows(10, show_row_link)
334            test_show_rows(25, show_row_link)
335            test_show_rows(50, show_row_link)
336            test_show_rows(100, show_row_link)
337            test_show_rows(150, show_row_link)
338