summaryrefslogtreecommitdiffstats
path: root/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'import-layers/yocto-poky/bitbake/lib/toaster/tests/browser')
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README41
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/__init__.py0
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py204
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py143
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py214
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py251
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py57
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_landing_page.py108
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py160
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py168
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py59
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py41
12 files changed, 1446 insertions, 0 deletions
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README
new file mode 100644
index 000000000..63e8169c1
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README
@@ -0,0 +1,41 @@
+# Running Toaster's browser-based test suite
+
+These tests require Selenium to be installed in your Python environment.
+
+The simplest way to install this is via pip:
+
+ pip install selenium
+
+Alternatively, if you used pip to install the libraries required by Toaster,
+selenium will already be installed.
+
+To run tests against Chrome:
+
+* Download chromedriver for your host OS from
+ https://code.google.com/p/chromedriver/downloads/list
+* On *nix systems, put chromedriver on PATH
+* On Windows, put chromedriver.exe in the same directory as chrome.exe
+
+To run tests against PhantomJS (headless):
+
+* Download and install PhantomJS:
+ http://phantomjs.org/download.html
+* On *nix systems, put phantomjs on PATH
+* Not tested on Windows
+
+Firefox should work without requiring additional software to be installed.
+
+The test case will instantiate a Selenium driver set by the
+TOASTER_TESTS_BROWSER environment variable, or Chrome if this is not specified.
+
+Available drivers:
+
+* chrome (default)
+* firefox
+* ie
+* phantomjs
+
+e.g. to run the test suite with phantomjs where you have phantomjs installed
+in /home/me/apps/phantomjs:
+
+PATH=/home/me/apps/phantomjs/bin:$PATH TOASTER_TESTS_BROWSER=phantomjs manage.py test tests.browser
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/__init__.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/__init__.py
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py
new file mode 100644
index 000000000..56dbe2b34
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py
@@ -0,0 +1,204 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are
+# modified from Patchwork, released under the same licence terms as Toaster:
+# https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py
+
+"""
+Helper methods for creating Toaster Selenium tests which run within
+the context of Django unit tests.
+"""
+
+import os
+import time
+
+from django.contrib.staticfiles.testing import StaticLiveServerTestCase
+from selenium import webdriver
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.common.exceptions import NoSuchElementException, \
+ StaleElementReferenceException, TimeoutException
+
+def create_selenium_driver(browser='chrome'):
+ # set default browser string based on env (if available)
+ env_browser = os.environ.get('TOASTER_TESTS_BROWSER')
+ if env_browser:
+ browser = env_browser
+
+ if browser == 'chrome':
+ return webdriver.Chrome(
+ service_args=["--verbose", "--log-path=selenium.log"]
+ )
+ elif browser == 'firefox':
+ return webdriver.Firefox()
+ elif browser == 'ie':
+ return webdriver.Ie()
+ elif browser == 'phantomjs':
+ return webdriver.PhantomJS()
+ else:
+ msg = 'Selenium driver for browser %s is not available' % browser
+ raise RuntimeError(msg)
+
+class Wait(WebDriverWait):
+ """
+ Subclass of WebDriverWait with predetermined timeout and poll
+ frequency. Also deals with a wider variety of exceptions.
+ """
+ _TIMEOUT = 10
+ _POLL_FREQUENCY = 0.5
+
+ def __init__(self, driver):
+ super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY)
+
+ def until(self, method, message=''):
+ """
+ Calls the method provided with the driver as an argument until the
+ return value is not False.
+ """
+
+ end_time = time.time() + self._timeout
+ while True:
+ try:
+ value = method(self._driver)
+ if value:
+ return value
+ except NoSuchElementException:
+ pass
+ except StaleElementReferenceException:
+ pass
+
+ time.sleep(self._poll)
+ if time.time() > end_time:
+ break
+
+ raise TimeoutException(message)
+
+ def until_not(self, method, message=''):
+ """
+ Calls the method provided with the driver as an argument until the
+ return value is False.
+ """
+
+ end_time = time.time() + self._timeout
+ while True:
+ try:
+ value = method(self._driver)
+ if not value:
+ return value
+ except NoSuchElementException:
+ return True
+ except StaleElementReferenceException:
+ pass
+
+ time.sleep(self._poll)
+ if time.time() > end_time:
+ break
+
+ raise TimeoutException(message)
+
+class SeleniumTestCase(StaticLiveServerTestCase):
+ """
+ NB StaticLiveServerTestCase is used as the base test case so that
+ static files are served correctly in a Selenium test run context; see
+ https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ """ Create a webdriver driver at the class level """
+
+ super(SeleniumTestCase, cls).setUpClass()
+
+ # instantiate the Selenium webdriver once for all the test methods
+ # in this test case
+ cls.driver = create_selenium_driver()
+
+ @classmethod
+ def tearDownClass(cls):
+ """ Clean up webdriver driver """
+
+ cls.driver.quit()
+ super(SeleniumTestCase, cls).tearDownClass()
+
+ def get(self, url):
+ """
+ Selenium requires absolute URLs, so convert Django URLs returned
+ by resolve() or similar to absolute ones and get using the
+ webdriver instance.
+
+ url: a relative URL
+ """
+ abs_url = '%s%s' % (self.live_server_url, url)
+ self.driver.get(abs_url)
+
+ def find(self, selector):
+ """ Find single element by CSS selector """
+ return self.driver.find_element_by_css_selector(selector)
+
+ def find_all(self, selector):
+ """ Find all elements matching CSS selector """
+ return self.driver.find_elements_by_css_selector(selector)
+
+ def focused_element(self):
+ """ Return the element which currently has focus on the page """
+ return self.driver.switch_to.active_element
+
+ def wait_until_present(self, selector):
+ """ Wait until element matching CSS selector is on the page """
+ is_present = lambda driver: self.find(selector)
+ msg = 'An element matching "%s" should be on the page' % selector
+ element = Wait(self.driver).until(is_present, msg)
+ return element
+
+ def wait_until_visible(self, selector):
+ """ Wait until element matching CSS selector is visible on the page """
+ is_visible = lambda driver: self.find(selector).is_displayed()
+ msg = 'An element matching "%s" should be visible' % selector
+ Wait(self.driver).until(is_visible, msg)
+ return self.find(selector)
+
+ def wait_until_focused(self, selector):
+ """ Wait until element matching CSS selector has focus """
+ is_focused = \
+ lambda driver: self.find(selector) == self.focused_element()
+ msg = 'An element matching "%s" should be focused' % selector
+ Wait(self.driver).until(is_focused, msg)
+ return self.find(selector)
+
+ def enter_text(self, selector, value):
+ """ Insert text into element matching selector """
+ # note that keyup events don't occur until the element is clicked
+ # (in the case of <input type="text"...>, for example), so simulate
+ # user clicking the element before inserting text into it
+ field = self.click(selector)
+
+ field.send_keys(value)
+ return field
+
+ def click(self, selector):
+ """ Click on element which matches CSS selector """
+ element = self.wait_until_visible(selector)
+ element.click()
+ return element
+
+ def get_page_source(self):
+ """ Get raw HTML for the current page """
+ return self.driver.page_source
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
new file mode 100644
index 000000000..e4223f482
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
@@ -0,0 +1,143 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import re
+
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+from tests.browser.selenium_helpers import SeleniumTestCase
+
+from orm.models import BitbakeVersion, Release, Project, Build, Target
+
+class TestAllBuildsPage(SeleniumTestCase):
+ """ Tests for all builds page /builds/ """
+
+ PROJECT_NAME = 'test project'
+ CLI_BUILDS_PROJECT_NAME = 'command line builds'
+
+ def setUp(self):
+ bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
+ branch='master', dirpath='')
+ release = Release.objects.create(name='release1',
+ bitbake_version=bbv)
+ self.project1 = Project.objects.create_project(name=self.PROJECT_NAME,
+ release=release)
+ self.default_project = Project.objects.create_project(
+ name=self.CLI_BUILDS_PROJECT_NAME,
+ release=release
+ )
+ self.default_project.is_default = True
+ self.default_project.save()
+
+ # parameters for builds to associate with the projects
+ now = timezone.now()
+
+ self.project1_build_success = {
+ 'project': self.project1,
+ 'started_on': now,
+ 'completed_on': now,
+ 'outcome': Build.SUCCEEDED
+ }
+
+ self.default_project_build_success = {
+ 'project': self.default_project,
+ 'started_on': now,
+ 'completed_on': now,
+ 'outcome': Build.SUCCEEDED
+ }
+
+ def test_show_tasks_with_suffix(self):
+ """ Task should be shown as suffix on build name """
+ build = Build.objects.create(**self.project1_build_success)
+ target = 'bash'
+ task = 'clean'
+ Target.objects.create(build=build, target=target, task=task)
+
+ url = reverse('all-builds')
+ self.get(url)
+ self.wait_until_present('td[class="target"]')
+
+ cell = self.find('td[class="target"]')
+ content = cell.get_attribute('innerHTML')
+ expected_text = '%s:%s' % (target, task)
+
+ self.assertTrue(re.search(expected_text, content),
+ '"target" cell should contain text %s' % expected_text)
+
+ def test_rebuild_buttons(self):
+ """
+ Test 'Rebuild' buttons in recent builds section
+
+ 'Rebuild' button should not be shown for command-line builds,
+ but should be shown for other builds
+ """
+ build1 = Build.objects.create(**self.project1_build_success)
+ default_build = Build.objects.create(**self.default_project_build_success)
+
+ url = reverse('all-builds')
+ self.get(url)
+
+ # shouldn't see a run again button for command-line builds
+ selector = 'div[data-latest-build-result="%s"] button' % default_build.id
+ run_again_button = self.find_all(selector)
+ self.assertEqual(len(run_again_button), 0,
+ 'should not see a run again button for cli builds')
+
+ # should see a run again button for non-command-line builds
+ selector = 'div[data-latest-build-result="%s"] button' % build1.id
+ run_again_button = self.find_all(selector)
+ self.assertEqual(len(run_again_button), 1,
+ 'should see a run again button for non-cli builds')
+
+ def test_tooltips_on_project_name(self):
+ """
+ Test tooltips shown next to project name in the main table
+
+ A tooltip should be present next to the command line
+ builds project name in the all builds page, but not for
+ other projects
+ """
+ Build.objects.create(**self.project1_build_success)
+ Build.objects.create(**self.default_project_build_success)
+
+ url = reverse('all-builds')
+ self.get(url)
+
+ # get the project name cells from the table
+ cells = self.find_all('#allbuildstable td[class="project"]')
+
+ selector = 'i.get-help'
+
+ for cell in cells:
+ content = cell.get_attribute('innerHTML')
+ help_icons = cell.find_elements_by_css_selector(selector)
+
+ if re.search(self.PROJECT_NAME, content):
+ # no help icon next to non-cli project name
+ msg = 'should not be a help icon for non-cli builds name'
+ self.assertEqual(len(help_icons), 0, msg)
+ elif re.search(self.CLI_BUILDS_PROJECT_NAME, content):
+ # help icon next to cli project name
+ msg = 'should be a help icon for cli builds name'
+ self.assertEqual(len(help_icons), 1, msg)
+ else:
+ msg = 'found unexpected project name cell in all builds table'
+ self.fail(msg)
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
new file mode 100644
index 000000000..ed8e620db
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
@@ -0,0 +1,214 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import re
+
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+from tests.browser.selenium_helpers import SeleniumTestCase
+
+from orm.models import BitbakeVersion, Release, Project, Build
+from orm.models import ProjectVariable
+
+class TestAllProjectsPage(SeleniumTestCase):
+ """ Browser tests for projects page /projects/ """
+
+ PROJECT_NAME = 'test project'
+ CLI_BUILDS_PROJECT_NAME = 'command line builds'
+ MACHINE_NAME = 'delorean'
+
+ def setUp(self):
+ """ Add default project manually """
+ project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None)
+ self.default_project = project
+ self.default_project.is_default = True
+ self.default_project.save()
+
+ # this project is only set for some of the tests
+ self.project = None
+
+ self.release = None
+
+ def _add_build_to_default_project(self):
+ """ Add a build to the default project (not used in all tests) """
+ now = timezone.now()
+ build = Build.objects.create(project=self.default_project,
+ started_on=now,
+ completed_on=now)
+ build.save()
+
+ def _add_non_default_project(self):
+ """ Add another project """
+ bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/',
+ branch='master', dirpath='')
+ self.release = Release.objects.create(name='test release',
+ branch_name='master',
+ bitbake_version=bbv)
+ self.project = Project.objects.create_project(self.PROJECT_NAME, self.release)
+ self.project.is_default = False
+ self.project.save()
+
+ # fake the MACHINE variable
+ project_var = ProjectVariable.objects.create(project=self.project,
+ name='MACHINE',
+ value=self.MACHINE_NAME)
+ project_var.save()
+
+ def _get_row_for_project(self, project_name):
+ """ Get the HTML row for a project, or None if not found """
+ self.wait_until_present('#projectstable tbody tr')
+ rows = self.find_all('#projectstable tbody tr')
+
+ # find the row with a project name matching the one supplied
+ found_row = None
+ for row in rows:
+ if re.search(project_name, row.get_attribute('innerHTML')):
+ found_row = row
+ break
+
+ return found_row
+
+ def test_default_project_hidden(self):
+ """
+ The default project should be hidden if it has no builds
+ and we should see the "no results" area
+ """
+ url = reverse('all-projects')
+ self.get(url)
+ self.wait_until_visible('#no-results-projectstable')
+
+ rows = self.find_all('#projectstable tbody tr')
+ self.assertEqual(len(rows), 0, 'should be no projects displayed')
+
+ def test_default_project_has_build(self):
+ """ The default project should be shown if it has builds """
+ self._add_build_to_default_project()
+
+ url = reverse('all-projects')
+ self.get(url)
+
+ default_project_row = self._get_row_for_project(self.default_project.name)
+
+ self.assertNotEqual(default_project_row, None,
+ 'default project "cli builds" should be in page')
+
+ def test_default_project_release(self):
+ """
+ The release for the default project should display as
+ 'Not applicable'
+ """
+ # need a build, otherwise project doesn't display at all
+ self._add_build_to_default_project()
+
+ # another project to test, which should show release
+ self._add_non_default_project()
+
+ self.get(reverse('all-projects'))
+
+ # find the row for the default project
+ default_project_row = self._get_row_for_project(self.default_project.name)
+
+ # check the release text for the default project
+ selector = 'span[data-project-field="release"] span.muted'
+ element = default_project_row.find_element_by_css_selector(selector)
+ text = element.text.strip()
+ self.assertEqual(text, 'Not applicable',
+ 'release should be "not applicable" for default project')
+
+ # find the row for the default project
+ other_project_row = self._get_row_for_project(self.project.name)
+
+ # check the link in the release cell for the other project
+ selector = 'span[data-project-field="release"] a'
+ element = other_project_row.find_element_by_css_selector(selector)
+ text = element.text.strip()
+ self.assertEqual(text, self.release.name,
+ 'release name should be shown for non-default project')
+
+ def test_default_project_machine(self):
+ """
+ The machine for the default project should display as
+ 'Not applicable'
+ """
+ # need a build, otherwise project doesn't display at all
+ self._add_build_to_default_project()
+
+ # another project to test, which should show machine
+ self._add_non_default_project()
+
+ self.get(reverse('all-projects'))
+
+ # find the row for the default project
+ default_project_row = self._get_row_for_project(self.default_project.name)
+
+ # check the machine cell for the default project
+ selector = 'span[data-project-field="machine"] span.muted'
+ element = default_project_row.find_element_by_css_selector(selector)
+ text = element.text.strip()
+ self.assertEqual(text, 'Not applicable',
+ 'machine should be not applicable for default project')
+
+ # find the row for the default project
+ other_project_row = self._get_row_for_project(self.project.name)
+
+ # check the link in the machine cell for the other project
+ selector = 'span[data-project-field="machine"] a'
+ element = other_project_row.find_element_by_css_selector(selector)
+ text = element.text.strip()
+ self.assertEqual(text, self.MACHINE_NAME,
+ 'machine name should be shown for non-default project')
+
+ def test_project_page_links(self):
+ """
+ Test that links for the default project point to the builds
+ page /projects/X/builds for that project, and that links for
+ other projects point to their configuration pages /projects/X/
+ """
+
+ # need a build, otherwise project doesn't display at all
+ self._add_build_to_default_project()
+
+ # another project to test
+ self._add_non_default_project()
+
+ self.get(reverse('all-projects'))
+
+ # find the row for the default project
+ default_project_row = self._get_row_for_project(self.default_project.name)
+
+ # check the link on the name field
+ selector = 'span[data-project-field="name"] a'
+ element = default_project_row.find_element_by_css_selector(selector)
+ link_url = element.get_attribute('href').strip()
+ expected_url = reverse('projectbuilds', args=(self.default_project.id,))
+ msg = 'link on default project name should point to builds but was %s' % link_url
+ self.assertTrue(link_url.endswith(expected_url), msg)
+
+ # find the row for the other project
+ other_project_row = self._get_row_for_project(self.project.name)
+
+ # check the link for the other project
+ selector = 'span[data-project-field="name"] a'
+ element = other_project_row.find_element_by_css_selector(selector)
+ link_url = element.get_attribute('href').strip()
+ expected_url = reverse('project', args=(self.project.id,))
+ msg = 'link on project name should point to configuration but was %s' % link_url
+ self.assertTrue(link_url.endswith(expected_url), msg)
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
new file mode 100644
index 000000000..5e0874947
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
@@ -0,0 +1,251 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+
+from selenium_helpers import SeleniumTestCase
+
+from orm.models import Project, Release, BitbakeVersion, Build, LogMessage
+from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe
+
+class TestBuildDashboardPage(SeleniumTestCase):
+ """ Tests for the build dashboard /build/X """
+
+ def setUp(self):
+ bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
+ branch='master', dirpath="")
+ release = Release.objects.create(name='release1',
+ bitbake_version=bbv)
+ project = Project.objects.create_project(name='test project',
+ release=release)
+
+ now = timezone.now()
+
+ self.build1 = Build.objects.create(project=project,
+ started_on=now,
+ completed_on=now)
+
+ self.build2 = Build.objects.create(project=project,
+ started_on=now,
+ completed_on=now)
+
+ # exception
+ msg1 = 'an exception was thrown'
+ self.exception_message = LogMessage.objects.create(
+ build=self.build1,
+ level=LogMessage.EXCEPTION,
+ message=msg1
+ )
+
+ # critical
+ msg2 = 'a critical error occurred'
+ self.critical_message = LogMessage.objects.create(
+ build=self.build1,
+ level=LogMessage.CRITICAL,
+ message=msg2
+ )
+
+ # recipes related to the build, for testing the edit custom image/new
+ # custom image buttons
+ layer = Layer.objects.create(name='alayer')
+ layer_version = Layer_Version.objects.create(
+ layer=layer, build=self.build1
+ )
+
+ # image recipes
+ self.image_recipe1 = Recipe.objects.create(
+ name='recipeA',
+ layer_version=layer_version,
+ file_path='/foo/recipeA.bb',
+ is_image=True
+ )
+ self.image_recipe2 = Recipe.objects.create(
+ name='recipeB',
+ layer_version=layer_version,
+ file_path='/foo/recipeB.bb',
+ is_image=True
+ )
+
+ # custom image recipes for this project
+ self.custom_image_recipe1 = CustomImageRecipe.objects.create(
+ name='customRecipeY',
+ project=project,
+ layer_version=layer_version,
+ file_path='/foo/customRecipeY.bb',
+ base_recipe=self.image_recipe1,
+ is_image=True
+ )
+ self.custom_image_recipe2 = CustomImageRecipe.objects.create(
+ name='customRecipeZ',
+ project=project,
+ layer_version=layer_version,
+ file_path='/foo/customRecipeZ.bb',
+ base_recipe=self.image_recipe2,
+ is_image=True
+ )
+
+ # custom image recipe for a different project (to test filtering
+ # of image recipes and custom image recipes is correct: this shouldn't
+ # show up in either query against self.build1)
+ self.custom_image_recipe3 = CustomImageRecipe.objects.create(
+ name='customRecipeOmega',
+ project=Project.objects.create(name='baz', release=release),
+ layer_version=Layer_Version.objects.create(
+ layer=layer, build=self.build2
+ ),
+ file_path='/foo/customRecipeOmega.bb',
+ base_recipe=self.image_recipe2,
+ is_image=True
+ )
+
+ # another non-image recipe (to test filtering of image recipes and
+ # custom image recipes is correct: this shouldn't show up in either
+ # for any build)
+ self.non_image_recipe = Recipe.objects.create(
+ name='nonImageRecipe',
+ layer_version=layer_version,
+ file_path='/foo/nonImageRecipe.bb',
+ is_image=False
+ )
+
+ def _get_build_dashboard(self, build):
+ """
+ Navigate to the build dashboard for build
+ """
+ url = reverse('builddashboard', args=(build.id,))
+ self.get(url)
+
+ def _get_build_dashboard_errors(self, build):
+ """
+ Get a list of HTML fragments representing the errors on the
+ dashboard for the Build object build
+ """
+ self._get_build_dashboard(build)
+ return self.find_all('#errors div.alert-error')
+
+ def _check_for_log_message(self, build, log_message):
+ """
+ Check whether the LogMessage instance <log_message> is
+ represented as an HTML error in the dashboard page for the Build object
+ build
+ """
+ errors = self._get_build_dashboard_errors(build)
+ self.assertEqual(len(errors), 2)
+
+ expected_text = log_message.message
+ expected_id = str(log_message.id)
+
+ found = False
+ for error in errors:
+ error_text = error.find_element_by_tag_name('pre').text
+ text_matches = (error_text == expected_text)
+
+ error_id = error.get_attribute('data-error')
+ id_matches = (error_id == expected_id)
+
+ if text_matches and id_matches:
+ found = True
+ break
+
+ template_vars = (expected_text, error_text,
+ expected_id, error_id)
+ assertion_error_msg = 'exception not found as error: ' \
+ 'expected text "%s" and got "%s"; ' \
+ 'expected ID %s and got %s' % template_vars
+ self.assertTrue(found, assertion_error_msg)
+
+ def _check_labels_in_modal(self, modal, expected):
+ """
+ Check that the text values of the <label> elements inside
+ the WebElement modal match the list of text values in expected
+ """
+ # labels containing the radio buttons we're testing for
+ labels = modal.find_elements_by_tag_name('label')
+
+ # because the label content has the structure
+ # label text
+ # <input...>
+ # we have to regex on its innerHTML, as we can't just retrieve the
+ # "label text" on its own via the Selenium API
+ labels_text = sorted(map(
+ lambda label: label.get_attribute('innerHTML'), labels
+ ))
+
+ expected = sorted(expected)
+
+ self.assertEqual(len(labels_text), len(expected))
+
+ for idx, label_text in enumerate(labels_text):
+ self.assertRegexpMatches(label_text, expected[idx])
+
+ def test_exceptions_show_as_errors(self):
+ """
+ LogMessages with level EXCEPTION should display in the errors
+ section of the page
+ """
+ self._check_for_log_message(self.build1, self.exception_message)
+
+ def test_criticals_show_as_errors(self):
+ """
+ LogMessages with level CRITICAL should display in the errors
+ section of the page
+ """
+ self._check_for_log_message(self.build1, self.critical_message)
+
+ def test_edit_custom_image_button(self):
+ """
+ A build which built two custom images should present a modal which lets
+ the user choose one of them to edit
+ """
+ self._get_build_dashboard(self.build1)
+ modal = self.driver.find_element_by_id('edit-custom-image-modal')
+
+ # recipes we expect to see in the edit custom image modal
+ expected_recipes = [
+ self.custom_image_recipe1.name,
+ self.custom_image_recipe2.name
+ ]
+
+ self._check_labels_in_modal(modal, expected_recipes)
+
+ def test_new_custom_image_button(self):
+ """
+ Check that a build with multiple images and custom images presents
+ all of them as options for creating a new custom image from
+ """
+ self._get_build_dashboard(self.build1)
+
+ # click the "new custom image" button, which populates the modal
+ selector = '[data-role="new-custom-image-trigger"] button'
+ self.click(selector)
+
+ modal = self.driver.find_element_by_id('new-custom-image-modal')
+
+ # recipes we expect to see in the new custom image modal
+ expected_recipes = [
+ self.image_recipe1.name,
+ self.image_recipe2.name,
+ self.custom_image_recipe1.name,
+ self.custom_image_recipe2.name
+ ]
+
+ self._check_labels_in_modal(modal, expected_recipes)
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py
new file mode 100644
index 000000000..e63da8e7a
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py
@@ -0,0 +1,57 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Run the js unit tests
+"""
+
+from django.core.urlresolvers import reverse
+from tests.browser.selenium_helpers import SeleniumTestCase
+import logging
+
+logger = logging.getLogger("toaster")
+
+
+class TestJsUnitTests(SeleniumTestCase):
+ """ Test landing page shows the Toaster brand """
+
+ fixtures = ['toastergui-unittest-data']
+
+ def test_that_js_unit_tests_pass(self):
+ url = reverse('js-unit-tests')
+ self.get(url)
+ self.wait_until_present('#tests-failed')
+
+ failed = self.find("#tests-failed").text
+ passed = self.find("#tests-passed").text
+ total = self.find("#tests-total").text
+
+ logger.info("Js unit tests completed %s out of %s passed, %s failed",
+ passed,
+ total,
+ failed)
+
+ failed_tests = self.find_all("li .fail .test-message")
+ for fail in failed_tests:
+ logger.error("JS unit test failed: %s" % fail.text)
+
+ self.assertEqual(failed, '0',
+ "%s JS unit tests failed" % failed)
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_landing_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_landing_page.py
new file mode 100644
index 000000000..4d4cd660f
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_landing_page.py
@@ -0,0 +1,108 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+from tests.browser.selenium_helpers import SeleniumTestCase
+
+from orm.models import Project, Build
+
+class TestLandingPage(SeleniumTestCase):
+ """ Tests for redirects on the landing page """
+
+ PROJECT_NAME = 'test project'
+ LANDING_PAGE_TITLE = 'This is Toaster'
+ CLI_BUILDS_PROJECT_NAME = 'command line builds'
+
+ def setUp(self):
+ """ Add default project manually """
+ self.project = Project.objects.create_project(
+ self.CLI_BUILDS_PROJECT_NAME,
+ None
+ )
+ self.project.is_default = True
+ self.project.save()
+
+ def test_only_default_project(self):
+ """
+ No projects except default
+ => should see the landing page
+ """
+ self.get(reverse('landing'))
+ self.assertTrue(self.LANDING_PAGE_TITLE in self.get_page_source())
+
+ def test_default_project_has_build(self):
+ """
+ Default project has a build, no other projects
+ => should see the builds page
+ """
+ now = timezone.now()
+ build = Build.objects.create(project=self.project,
+ started_on=now,
+ completed_on=now)
+ build.save()
+
+ self.get(reverse('landing'))
+
+ elements = self.find_all('#allbuildstable')
+ self.assertEqual(len(elements), 1, 'should redirect to builds')
+ content = self.get_page_source()
+ self.assertFalse(self.PROJECT_NAME in content,
+ 'should not show builds for project %s' % self.PROJECT_NAME)
+ self.assertTrue(self.CLI_BUILDS_PROJECT_NAME in content,
+ 'should show builds for cli project')
+
+ def test_user_project_exists(self):
+ """
+ User has added a project (without builds)
+ => should see the projects page
+ """
+ user_project = Project.objects.create_project('foo', None)
+ user_project.save()
+
+ self.get(reverse('landing'))
+
+ elements = self.find_all('#projectstable')
+ self.assertEqual(len(elements), 1, 'should redirect to projects')
+
+ def test_user_project_has_build(self):
+ """
+ User has added a project (with builds), command line builds doesn't
+ => should see the builds page
+ """
+ user_project = Project.objects.create_project(self.PROJECT_NAME, None)
+ user_project.save()
+
+ now = timezone.now()
+ build = Build.objects.create(project=user_project,
+ started_on=now,
+ completed_on=now)
+ build.save()
+
+ self.get(reverse('landing'))
+
+ elements = self.find_all('#allbuildstable')
+ self.assertEqual(len(elements), 1, 'should redirect to builds')
+ content = self.get_page_source()
+ self.assertTrue(self.PROJECT_NAME in content,
+ 'should show builds for project %s' % self.PROJECT_NAME)
+ self.assertFalse(self.CLI_BUILDS_PROJECT_NAME in content,
+ 'should not show builds for cli project')
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
new file mode 100644
index 000000000..8906cb27d
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
@@ -0,0 +1,160 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers import reverse
+from tests.browser.selenium_helpers import SeleniumTestCase
+
+from orm.models import BitbakeVersion, Release, Project, ProjectLayer, Layer
+from orm.models import Layer_Version, Recipe, CustomImageRecipe
+
+class TestNewCustomImagePage(SeleniumTestCase):
+ CUSTOM_IMAGE_NAME = 'roopa-doopa'
+
+ def setUp(self):
+ release = Release.objects.create(
+ name='baz',
+ bitbake_version=BitbakeVersion.objects.create(name='v1')
+ )
+
+ # project to add new custom images to
+ self.project = Project.objects.create(name='foo', release=release)
+
+ # layer associated with the project
+ layer = Layer.objects.create(name='bar')
+ layer_version = Layer_Version.objects.create(
+ layer=layer,
+ project=self.project
+ )
+
+ # properly add the layer to the project
+ ProjectLayer.objects.create(
+ project=self.project,
+ layercommit=layer_version,
+ optional=False
+ )
+
+ # add a fake image recipe to the layer that can be customised
+ self.recipe = Recipe.objects.create(
+ name='core-image-minimal',
+ layer_version=layer_version,
+ is_image=True
+ )
+
+ # another project with a custom image already in it
+ project2 = Project.objects.create(name='whoop', release=release)
+ layer_version2 = Layer_Version.objects.create(
+ layer=layer,
+ project=project2
+ )
+ ProjectLayer.objects.create(
+ project=project2,
+ layercommit=layer_version2,
+ optional=False
+ )
+ recipe2 = Recipe.objects.create(
+ name='core-image-minimal',
+ layer_version=layer_version2,
+ is_image=True
+ )
+ CustomImageRecipe.objects.create(
+ name=self.CUSTOM_IMAGE_NAME,
+ base_recipe=recipe2,
+ layer_version=layer_version2,
+ file_path='/1/2',
+ project=project2
+ )
+
+ def _create_custom_image(self, new_custom_image_name):
+ """
+ 1. Go to the 'new custom image' page
+ 2. Click the button for the fake core-image-minimal
+ 3. Wait for the dialog box for setting the name of the new custom
+ image
+ 4. Insert new_custom_image_name into that dialog's text box
+ """
+ url = reverse('newcustomimage', args=(self.project.id,))
+ self.get(url)
+
+ self.click('button[data-recipe="%s"]' % self.recipe.id)
+
+ selector = '#new-custom-image-modal input[type="text"]'
+ self.enter_text(selector, new_custom_image_name)
+
+ self.click('#create-new-custom-image-btn')
+
+ def _check_for_custom_image(self, image_name):
+ """
+ Fetch the list of custom images for the project and check the
+ image with name image_name is listed there
+ """
+ url = reverse('projectcustomimages', args=(self.project.id,))
+ self.get(url)
+
+ self.wait_until_visible('#customimagestable')
+
+ element = self.find('#customimagestable td[class="name"] a')
+ msg = 'should be a custom image link with text %s' % image_name
+ self.assertEqual(element.text.strip(), image_name, msg)
+
+ def test_new_image(self):
+ """
+ Should be able to create a new custom image
+ """
+ custom_image_name = 'boo-image'
+ self._create_custom_image(custom_image_name)
+ self.wait_until_visible('#image-created-notification')
+ self._check_for_custom_image(custom_image_name)
+
+ def test_new_duplicates_other_project_image(self):
+ """
+ Should be able to create a new custom image if its name is the same
+ as a custom image in another project
+ """
+ self._create_custom_image(self.CUSTOM_IMAGE_NAME)
+ self.wait_until_visible('#image-created-notification')
+ self._check_for_custom_image(self.CUSTOM_IMAGE_NAME)
+
+ def test_new_duplicates_non_image_recipe(self):
+ """
+ Should not be able to create a new custom image whose name is the
+ same as an existing non-image recipe
+ """
+ self._create_custom_image(self.recipe.name)
+ element = self.wait_until_visible('#invalid-name-help')
+ self.assertRegexpMatches(element.text.strip(),
+ 'recipe with this name already exists')
+
+ def test_new_duplicates_project_image(self):
+ """
+ Should not be able to create a new custom image whose name is the same
+ as a custom image in this project
+ """
+ # create the image
+ custom_image_name = 'doh-image'
+ self._create_custom_image(custom_image_name)
+ self.wait_until_visible('#image-created-notification')
+ self._check_for_custom_image(custom_image_name)
+
+ # try to create an image with the same name
+ self._create_custom_image(custom_image_name)
+ element = self.wait_until_visible('#invalid-name-help')
+ expected = 'An image with this name already exists in this project'
+ self.assertRegexpMatches(element.text.strip(), expected)
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py
new file mode 100644
index 000000000..9fe91ab06
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py
@@ -0,0 +1,168 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import re
+
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+from tests.browser.selenium_helpers import SeleniumTestCase
+
+from orm.models import BitbakeVersion, Release, Project, Build, Target
+
+class TestProjectBuildsPage(SeleniumTestCase):
+ """ Test data at /project/X/builds is displayed correctly """
+
+ PROJECT_NAME = 'test project'
+ CLI_BUILDS_PROJECT_NAME = 'command line builds'
+
+ def setUp(self):
+ bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
+ branch='master', dirpath='')
+ release = Release.objects.create(name='release1',
+ bitbake_version=bbv)
+ self.project1 = Project.objects.create_project(name=self.PROJECT_NAME,
+ release=release)
+ self.project1.save()
+
+ self.project2 = Project.objects.create_project(name=self.PROJECT_NAME,
+ release=release)
+ self.project2.save()
+
+ self.default_project = Project.objects.create_project(
+ name=self.CLI_BUILDS_PROJECT_NAME,
+ release=release
+ )
+ self.default_project.is_default = True
+ self.default_project.save()
+
+ # parameters for builds to associate with the projects
+ now = timezone.now()
+
+ self.project1_build_success = {
+ 'project': self.project1,
+ 'started_on': now,
+ 'completed_on': now,
+ 'outcome': Build.SUCCEEDED
+ }
+
+ self.project1_build_in_progress = {
+ 'project': self.project1,
+ 'started_on': now,
+ 'completed_on': now,
+ 'outcome': Build.IN_PROGRESS
+ }
+
+ self.project2_build_success = {
+ 'project': self.project2,
+ 'started_on': now,
+ 'completed_on': now,
+ 'outcome': Build.SUCCEEDED
+ }
+
+ self.project2_build_in_progress = {
+ 'project': self.project2,
+ 'started_on': now,
+ 'completed_on': now,
+ 'outcome': Build.IN_PROGRESS
+ }
+
+ def _get_rows_for_project(self, project_id):
+ """
+ Helper to retrieve HTML rows for a project's builds,
+ as shown in the main table of the page
+ """
+ url = reverse('projectbuilds', args=(project_id,))
+ self.get(url)
+ self.wait_until_present('#projectbuildstable tbody tr')
+ return self.find_all('#projectbuildstable tbody tr')
+
+ def test_show_builds_for_project(self):
+ """ Builds for a project should be displayed in the main table """
+ Build.objects.create(**self.project1_build_success)
+ Build.objects.create(**self.project1_build_success)
+ build_rows = self._get_rows_for_project(self.project1.id)
+ self.assertEqual(len(build_rows), 2)
+
+ def test_show_builds_project_only(self):
+ """ Builds for other projects should be excluded """
+ Build.objects.create(**self.project1_build_success)
+ Build.objects.create(**self.project1_build_success)
+ Build.objects.create(**self.project1_build_success)
+
+ # shouldn't see these two
+ Build.objects.create(**self.project2_build_success)
+ Build.objects.create(**self.project2_build_in_progress)
+
+ build_rows = self._get_rows_for_project(self.project1.id)
+ self.assertEqual(len(build_rows), 3)
+
+ def test_builds_exclude_in_progress(self):
+ """ "in progress" builds should not be shown in main table """
+ Build.objects.create(**self.project1_build_success)
+ Build.objects.create(**self.project1_build_success)
+
+ # shouldn't see this one
+ Build.objects.create(**self.project1_build_in_progress)
+
+ # shouldn't see these two either, as they belong to a different project
+ Build.objects.create(**self.project2_build_success)
+ Build.objects.create(**self.project2_build_in_progress)
+
+ build_rows = self._get_rows_for_project(self.project1.id)
+ self.assertEqual(len(build_rows), 2)
+
+ def test_show_tasks_with_suffix(self):
+ """ Task should be shown as suffixes on build names """
+ build = Build.objects.create(**self.project1_build_success)
+ target = 'bash'
+ task = 'clean'
+ Target.objects.create(build=build, target=target, task=task)
+
+ url = reverse('projectbuilds', args=(self.project1.id,))
+ self.get(url)
+ self.wait_until_present('td[class="target"]')
+
+ cell = self.find('td[class="target"]')
+ content = cell.get_attribute('innerHTML')
+ expected_text = '%s:%s' % (target, task)
+
+ self.assertTrue(re.search(expected_text, content),
+ '"target" cell should contain text %s' % expected_text)
+
+ def test_cli_builds_hides_tabs(self):
+ """
+ Display for command line builds should hide tabs
+ """
+ url = reverse('projectbuilds', args=(self.default_project.id,))
+ self.get(url)
+ tabs = self.find_all('#project-topbar')
+ self.assertEqual(len(tabs), 0,
+ 'should be no top bar shown for command line builds')
+
+ def test_non_cli_builds_has_tabs(self):
+ """
+ Non-command-line builds projects should show the tabs
+ """
+ url = reverse('projectbuilds', args=(self.project1.id,))
+ self.get(url)
+ tabs = self.find_all('#project-topbar')
+ self.assertEqual(len(tabs), 1,
+ 'should be a top bar shown for non-command-line builds')
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py
new file mode 100644
index 000000000..786bef1c6
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py
@@ -0,0 +1,59 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+from tests.browser.selenium_helpers import SeleniumTestCase
+
+from orm.models import Build, Project
+
+class TestProjectPage(SeleniumTestCase):
+ """ Test project data at /project/X/ is displayed correctly """
+
+ CLI_BUILDS_PROJECT_NAME = 'Command line builds'
+
+ def test_cli_builds_in_progress(self):
+ """
+ In progress builds should not cause an error to be thrown
+ when navigating to "command line builds" project page;
+ see https://bugzilla.yoctoproject.org/show_bug.cgi?id=8277
+ """
+
+ # add the "command line builds" default project; this mirrors what
+ # we do with get_or_create_default_project()
+ default_project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None)
+ default_project.is_default = True
+ default_project.save()
+
+ # add an "in progress" build for the default project
+ now = timezone.now()
+ Build.objects.create(project=default_project,
+ started_on=now,
+ completed_on=now,
+ outcome=Build.IN_PROGRESS)
+
+ # navigate to the project page for the default project
+ url = reverse("project", args=(default_project.id,))
+ self.get(url)
+
+ # check that we get a project page with the correct heading
+ project_name = self.find('#project-name').text.strip()
+ self.assertEqual(project_name, self.CLI_BUILDS_PROJECT_NAME)
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py
new file mode 100644
index 000000000..7bb8b97e8
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py
@@ -0,0 +1,41 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+A small example test demonstrating the basics of writing a test with
+Toaster's SeleniumTestCase; this just fetches the Toaster home page
+and checks it has the word "Toaster" in the brand link
+
+New test files should follow this structure, should be named "test_*.py",
+and should be in the same directory as this sample.
+"""
+
+from django.core.urlresolvers import reverse
+from tests.browser.selenium_helpers import SeleniumTestCase
+
+class TestSample(SeleniumTestCase):
+ """ Test landing page shows the Toaster brand """
+
+ def test_landing_page_has_brand(self):
+ url = reverse('landing')
+ self.get(url)
+ brand_link = self.find('span.brand a')
+ self.assertEqual(brand_link.text.strip(), 'Toaster')
OpenPOWER on IntegriCloud