]> git.ipfire.org Git - thirdparty/patchwork.git/commitdiff
tests: Add a couple of Selenium tests
authorDamien Lespiau <damien.lespiau@intel.com>
Tue, 8 Sep 2015 18:00:11 +0000 (19:00 +0100)
committerStephen Finucane <stephen.finucane@intel.com>
Thu, 5 Nov 2015 04:22:55 +0000 (04:22 +0000)
While developing the new series UI, several bug crept in but weren't
discovered until later. All because we don't have in-browser tests to go
along the lower level tests we already have.

In particular, behaviours that need javascript to run cannot be tested
outside of a full environment with the pages being served to an actual
browser.

This commit introduces selenium to the test suite and starts with 2
simple tests to give a taste of what it looks like.

test_default_focus: make sure we do focus the username field on the
                    login page

test_login: shows how to chain actions to test the full login phase.
            This is quite similar the lower level test, except it also
            checks we display the username once logged in.

v2: Use LiveServerTestCase for django pre-1.7
v3: Propagate the DISPLAY environment variable to have an X display
    specified for the browser
v4: Log execution of the chrome driver, useful for debugging
v5: Rebase on top of upstream
v6: Pass environmental variables to tox and ensure PEP8 compliance

Signed-off-by: Damien Lespiau <damien.lespiau@intel.com>
Signed-off-by: Stephen Finucane <stephen.finucane@intel.com>
.gitignore
docs/requirements-dev.txt
patchwork/tests/browser.py [new file with mode: 0644]
patchwork/tests/test_user_browser.py [new file with mode: 0644]
tox.ini

index b718f0a0bf60746e2564355ddec44efc106d9639..94481ade244c0b3f097a7a6157e347c6a8d4b403 100644 (file)
@@ -28,3 +28,7 @@ cscope.*
 
 *.orig
 *.rej
+
+# test artifacts
+/selenium.log
+/selenium_screenshots
index eee64632c92e77b42aaca67e259d84e192ea16ec..12b8bef3365458ddf9ce5050ad35f3150fe3ff27 100644 (file)
@@ -1 +1,2 @@
 -r requirements-base.txt
+selenium
diff --git a/patchwork/tests/browser.py b/patchwork/tests/browser.py
new file mode 100644 (file)
index 0000000..1845000
--- /dev/null
@@ -0,0 +1,165 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2015 Intel Corporation
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork 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 Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import errno
+import os
+import time
+
+try:  # django 1.7+
+    from django.contrib.staticfiles.testing import StaticLiveServerTestCase
+except:
+    from django.test import LiveServerTestCase as StaticLiveServerTestCase
+from selenium.common.exceptions import (
+        NoSuchElementException, StaleElementReferenceException,
+        TimeoutException)
+from selenium import webdriver
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+class Wait(WebDriverWait):
+    """Subclass of WebDriverWait.
+
+    Includes a 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=''):
+        """Call method with driver until it returns True."""
+        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=''):
+        """Call method with driver until it returns True."""
+        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)
+
+
+def mkdir(path):
+    try:
+        os.makedirs(path)
+    except OSError as error:
+        if error.errno == errno.EEXIST and os.path.isdir(path):
+            pass
+        else:
+            raise
+
+
+class SeleniumTestCase(StaticLiveServerTestCase):
+    # TODO(stephenfin): This should handle non-UNIX paths
+    _SCREENSHOT_DIR = os.path.dirname(__file__) + '/../../selenium_screenshots'
+
+    def setUp(self):
+        super(SeleniumTestCase, self).setUp()
+
+        self.browser = os.getenv('SELENIUM_BROWSER', 'chrome')
+        if self.browser == 'firefox':
+            self.selenium = webdriver.Firefox()
+        if self.browser == 'chrome':
+            self.selenium = webdriver.Chrome(
+                service_args=['--verbose', '--log-path=selenium.log']
+            )
+
+        mkdir(self._SCREENSHOT_DIR)
+        self._screenshot_number = 1
+
+    def tearDown(self):
+        self.selenium.quit()
+        super(SeleniumTestCase, self).tearDown()
+
+    def screenshot(self):
+        name = '%s_%d.png' % (self._testMethodName, self._screenshot_number)
+        path = os.path.join(self._SCREENSHOT_DIR, name)
+        self.selenium.get_screenshot_as_file(path)
+        self._screenshot_number += 1
+
+    def get(self, relative_url):
+        self.selenium.get('%s%s' % (self.live_server_url, relative_url))
+        self.screenshot()
+
+    def find(self, selector):
+        return self.selenium.find_element_by_css_selector(selector)
+
+    def focused_element(self):
+        return self.selenium.switch_to.active_element
+
+    def wait_until_present(self, name):
+        is_present = lambda driver: driver.find_element_by_name(name)
+        msg = "An element named '%s' should be on the page" % name
+        element = Wait(self.selenium).until(is_present, msg)
+        self.screenshot()
+        return element
+
+    def wait_until_visible(self, selector):
+        is_visible = lambda driver: self.find(selector).is_displayed()
+        msg = "The element matching '%s' should be visible" % selector
+        Wait(self.selenium).until(is_visible, msg)
+        self.screenshot()
+        return self.find(selector)
+
+    def wait_until_focused(self, selector):
+        is_focused = lambda driver: (
+            self.find(selector) == self.focused_element())
+        msg = "The element matching '%s' should be focused" % selector
+        Wait(self.selenium).until(is_focused, msg)
+        self.screenshot()
+        return self.find(selector)
+
+    def enter_text(self, name, value):
+        field = self.wait_until_present(name)
+        field.send_keys(value)
+        return field
+
+    def click(self, selector):
+        element = self.wait_until_visible(selector)
+        element.click()
+        return element
diff --git a/patchwork/tests/test_user_browser.py b/patchwork/tests/test_user_browser.py
new file mode 100644 (file)
index 0000000..2b9ed2e
--- /dev/null
@@ -0,0 +1,38 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2015 Intel Corporation
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork 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 Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from patchwork.tests.browser import SeleniumTestCase
+from patchwork.tests.test_user import TestUser
+
+class LoginTestCase(SeleniumTestCase):
+    def setUp(self):
+        super(LoginTestCase, self).setUp()
+        self.user = TestUser()
+
+    def test_default_focus(self):
+        self.get('/user/login/')
+        self.wait_until_focused('#id_username')
+
+    def test_login(self):
+        self.get('/user/login/')
+        self.enter_text('username', self.user.username)
+        self.enter_text('password', self.user.password)
+        self.click('input[value="Login"]')
+        dropdown = self.wait_until_visible('a.dropdown-toggle strong')
+        self.assertEquals(dropdown.text, 'testuser')
diff --git a/tox.ini b/tox.ini
index e44c884bb0161ac20bfa3159983a4e0f39e058bd..8647cf37c34b5a727723ae21278a8b71448762a7 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -19,6 +19,7 @@ commands =
 passenv =
     http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
     PW_TEST_DB_USER PW_TEST_DB_PASS
+    DISPLAY SELENIUM_BROWSER
 
 [testenv:pep8]
 basepython = python2.7