Ticket #2879: 2879.selenium-support.diff

File 2879.selenium-support.diff, 21.5 KB (added by Julien Phalip, 12 years ago)
  • django/conf/global_settings.py

    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index 6b09be2..51f97cf 100644
    a b DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFil  
    560560# The name of the class to use to run the test suite
    561561TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'
    562562
     563# Selenium settings
     564SELENIUM_SERVER_ADDRESS = 'localhost'
     565SELENIUM_SERVER_PORT = 4444
     566SELENIUM_BROWSER = '*firefox'
     567
    563568############
    564569# FIXTURES #
    565570############
  • django/test/testcases.py

    diff --git a/django/test/testcases.py b/django/test/testcases.py
    index ee22ac2..a86a16e 100644
    a b  
    11from __future__ import with_statement
    22
     3import httplib
    34import re
    45import sys
    56from functools import wraps
    67from urlparse import urlsplit, urlunsplit
    78from xml.dom.minidom import parseString, Node
     9import socket
     10import threading
    811
    912from django.conf import settings
    1013from django.core import mail
    1114from django.core.exceptions import ValidationError
     15from django.core.handlers.wsgi import WSGIHandler
    1216from django.core.management import call_command
    1317from django.core.signals import request_started
     18from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
     19    AdminMediaHandler)
    1420from django.core.urlresolvers import clear_url_caches
    1521from django.core.validators import EMPTY_VALUES
    1622from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
    from django.test.utils import (get_warnings_state, restore_warnings_state,  
    2329    override_settings)
    2430from django.utils import simplejson, unittest as ut2
    2531from django.utils.encoding import smart_str
     32from django.utils.unittest.case import skipUnless
    2633
    2734__all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
    2835           'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature')
    def skipUnlessDBFeature(feature):  
    732739    """
    733740    return _deferredSkip(lambda: not getattr(connection.features, feature),
    734741                         "Database doesn't support feature %s" % feature)
     742
     743
     744
     745
     746
     747
     748class QuietWSGIRequestHandler(WSGIRequestHandler):
     749    """
     750    Just a regular WSGIRequestHandler except it doesn't log to the standard
     751    output any of the requests received, so as to not clutter the output for
     752    the tests' results.
     753    """
     754    def log_message(*args):
     755        pass
     756
     757
     758class LiveServerThread(threading.Thread):
     759    """
     760    Thread for running a live http server while the tests are running.
     761    """
     762
     763    def __init__(self, address, port, fixtures):
     764        self.address = address
     765        self.port = port
     766        self.fixtures = fixtures
     767        self.please_stop = threading.Event()
     768        self.is_ready = threading.Event()
     769        super(LiveServerThread, self).__init__()
     770
     771    def run(self):
     772        """
     773        Sets up live server and database and loops over handling http requests.
     774        """
     775        # Instantiate the server and prepare it so also serve the admin media
     776        httpd = WSGIServer((self.address, self.port), QuietWSGIRequestHandler)
     777        handler = AdminMediaHandler(WSGIHandler())
     778        httpd.set_app(handler)
     779
     780        # If the database is in memory we must reload the data in this new
     781        # thread.
     782        if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3' or
     783            settings.DATABASES['default']['TEST_NAME']):
     784            connection.creation.create_test_db(0)
     785            # Import the fixtures into the test database
     786            if hasattr(self, 'fixtures'):
     787                call_command('loaddata', *self.fixtures, **{'verbosity': 0})
     788
     789        # Serve requests
     790        self.is_ready.set()
     791        while not self.please_stop.isSet():
     792            httpd.handle_request()
     793        httpd.server_close()
     794
     795class LiveServerTestCase(TestCase):
     796    """
     797    Does basically the same as TestCase but also launches a live http server in
     798    a separate thread so that the tests may use another testing framework, such
     799    as Selenium for example, instead of the built-in dummy client.
     800    """
     801
     802    django_server_address = '127.0.0.1' #TODO: Should those be settings instead
     803    django_server_port = 8000           # of class attributes?
     804    fixtures = []
     805
     806    def setUp(self):
     807        # Launch the Django live server's thread
     808        self.server_thread = LiveServerThread(self.django_server_address,
     809            self.django_server_port, fixtures=self.fixtures)
     810        self.server_thread.start()
     811        super(LiveServerTestCase, self).setUp()
     812
     813    def tearDown(self):
     814        # Kindly ask the Django live server to stop
     815        self.server_thread.please_stop.set()
     816        # Send one last dummy request to unlock the live server thread
     817        conn = httplib.HTTPConnection(self.django_server_address,
     818            self.django_server_port)
     819        conn.request("GET", '/', '', {})
     820        self.server_thread.join()
     821        super(LiveServerTestCase, self).tearDown()
     822
     823
     824
     825
     826try:
     827    # Check if the 'selenium' package is installed
     828    from selenium import selenium
     829    selenium_installed = True
     830except ImportError:
     831    selenium_installed = False
     832
     833# Check if the Selenium server is running
     834try:
     835    conn = httplib.HTTPConnection(settings.SELENIUM_SERVER_ADDRESS,
     836        settings.SELENIUM_SERVER_PORT)
     837    try:
     838        conn.request("GET", "/selenium-server/driver/", '', {})
     839    finally:
     840        conn.close()
     841    selenium_server_running = True
     842except socket.error:
     843    selenium_server_running = False
     844
     845class SeleniumTestCase(LiveServerTestCase):
     846    """
     847    Does basically the same as TestServerTestCase but also connects to the
     848    Selenium server. The selenium client is then available with
     849    'self.selenium'. The requirements are to have the 'selenium' installed in
     850    the python path and to have the Selenium server running. If those
     851    requirements are not filled then the tests will be skipped.
     852    """
     853
     854    def setUp(self):
     855        super(SeleniumTestCase, self).setUp()
     856        # Launch the Selenium server
     857        selenium_browser_url = 'http://%s:%s' % (
     858            self.django_server_address, self.django_server_port)
     859        self.selenium = selenium(
     860            settings.SELENIUM_SERVER_ADDRESS, settings.SELENIUM_SERVER_PORT,
     861            settings.SELENIUM_BROWSER, selenium_browser_url)
     862        self.selenium.start()
     863        # Wait for the Django server to be ready
     864        self.server_thread.is_ready.wait(timeout=5)
     865
     866    def tearDown(self):
     867        self.selenium.stop()
     868        super(SeleniumTestCase, self).tearDown()
     869
     870SeleniumTestCase = skipUnless(selenium_installed,
     871    'The \'selenium\' package isn\'t installed')(SeleniumTestCase)
     872SeleniumTestCase = skipUnless(selenium_server_running,
     873    'Can\'t connect to the Selenium server using address %s and port %s' % (
     874    settings.SELENIUM_SERVER_ADDRESS, settings.SELENIUM_SERVER_PORT)
     875)(SeleniumTestCase)
     876 No newline at end of file
  • docs/internals/contributing/writing-code/unit-tests.txt

    diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt
    index 5ec09fe..4a50a77 100644
    a b Going beyond that, you can specify an individual test method like this:  
    122122
    123123    ./runtests.py --settings=path.to.settings i18n.TranslationTests.test_lazy_objects
    124124
     125Running the Selenium tests
     126~~~~~~~~~~~~~~~~~~~~~~~~~~
     127
     128Some admin tests require Selenium to work via a real Web browser. To allow
     129those tests to run and not be skipped, you must install the selenium_ package
     130into the Python path and download the `Selenium server (>2.11.0)`_. The
     131Selenium server must then be started with the following command:
     132
     133.. code-block:: bash
     134
     135    java -jar selenium-server-standalone-2.11.0.jar
     136
     137You may then run the tests normally, for example:
     138
     139.. code-block:: bash
     140
     141    ./runtests.py --settings=test_sqlite admin_inlines
     142
    125143Running all the tests
    126144~~~~~~~~~~~~~~~~~~~~~
    127145
    dependencies:  
    135153*  setuptools_
    136154*  memcached_, plus a :ref:`supported Python binding <memcached>`
    137155*  gettext_ (:ref:`gettext_on_windows`)
     156*  selenium_ plus the `Selenium server (>2.11.0)`_
    138157
    139158If you want to test the memcached cache backend, you'll also need to define
    140159a :setting:`CACHES` setting that points at your memcached instance.
    associated tests will be skipped.  
    149168.. _setuptools: http://pypi.python.org/pypi/setuptools/
    150169.. _memcached: http://www.danga.com/memcached/
    151170.. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html
     171.. _selenium: http://pypi.python.org/pypi/selenium
     172.. _Selenium server (>2.11.0): http://seleniumhq.org/download/
    152173
    153174.. _contrib-apps:
    154175
  • docs/ref/settings.txt

    diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
    index 20366e3..b7151a6 100644
    a b default port. Not used with SQLite.  
    498498
    499499.. setting:: USER
    500500
     501SELENIUM_SERVER_ADDRESS
     502~~~~~~~~~~~~~~~~~~~~~~~
     503
     504Default: ``localhost``
     505
     506Address where the Selenium server can be accessed.
     507
     508SELENIUM_SERVER_PORT
     509~~~~~~~~~~~~~~~~~~~~
     510
     511Default: ``4444``
     512
     513Port where the Selenium server can be accessed.
     514
     515SELENIUM_BROWSER
     516~~~~~~~~~~~~~~~~
     517
     518Default: ``'*firefox'``
     519
     520Browser to be used when running Selenium tests. Note that the prefixing star
     521('``*``') is required. Possible values include:
     522
     523*  ``'*firefox'``
     524*  ``'*googlechrome'``
     525*  ``'*safari'``
     526*  ``'*mock'``
     527*  ``'*firefoxproxy'``
     528*  ``'*pifirefox'``
     529*  ``'*chrome'``
     530*  ``'*iexploreproxy'``
     531*  ``'*iexplore'``
     532*  ``'*safariproxy'``
     533*  ``'*konqueror'``
     534*  ``'*firefox2'``
     535*  ``'*firefox3'``
     536*  ``'*firefoxchrome'``
     537*  ``'*piiexplore'``
     538*  ``'*opera'``
     539*  ``'*iehta'``
     540*  ``'*custom'``
     541
    501542USER
    502543~~~~
    503544
  • docs/topics/testing.txt

    diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
    index dc5bf7e..47638f3 100644
    a b Some of the things you can do with the test client are:  
    580580* Test that a given request is rendered by a given Django template, with
    581581  a template context that contains certain values.
    582582
    583 Note that the test client is not intended to be a replacement for Twill_,
     583Note that the test client is not intended to be a replacement for Windmill_,
    584584Selenium_, or other "in-browser" frameworks. Django's test client has
    585585a different focus. In short:
    586586
    587587* Use Django's test client to establish that the correct view is being
    588588  called and that the view is collecting the correct context data.
    589589
    590 * Use in-browser frameworks such as Twill and Selenium to test *rendered*
    591   HTML and the *behavior* of Web pages, namely JavaScript functionality.
     590* Use in-browser frameworks such as Windmill_ and Selenium_ to test *rendered*
     591  HTML and the *behavior* of Web pages, namely JavaScript functionality. Django
     592  also provides special support for those frameworks; see the sections on
     593  :class:`~django.test.testcases.LiveServerTestCase` and
     594  :class:`~django.test.testcases.SeleniumTestCase`.
    592595
    593596A comprehensive test suite should use a combination of both test types.
    594597
    595 .. _Twill: http://twill.idyll.org/
    596 .. _Selenium: http://seleniumhq.org/
    597 
    598598Overview and a quick example
    599599~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    600600
    set up, execute and tear down the test suite.  
    18281828    those options will be added to the list of command-line options that
    18291829    the :djadmin:`test` command can use.
    18301830
     1831Live test server
     1832----------------
     1833
     1834.. currentmodule:: django.test.testcases
     1835
     1836.. versionadded::1.4
     1837
     1838.. class:: LiveServerTestCase()
     1839
     1840**This is a stub**
     1841
     1842``LiveServerTestCase`` does basically the same as ``TestCase`` but also
     1843launches a live http server in a separate thread so that the tests may use
     1844another testing framework, such as Selenium_ or Windmill_ for example, instead
     1845of the built-in :ref:`dummy client <test-client>`.
     1846
     1847Selenium
     1848--------
     1849
     1850.. versionadded::1.4
     1851
     1852.. class:: SeleniumTestCase()
     1853
     1854**This is a stub**
     1855
     1856Django provides out-of-the box support for Selenium_, as Django itself uses it
     1857in its own test suite for the admin.
     1858
     1859TODO:
     1860
     1861*  settings
     1862*  requirements
     1863*  code sample
     1864
     1865.. _Windmill: http://www.getwindmill.com/
     1866.. _Selenium: http://seleniumhq.org/
    18311867
    18321868Attributes
    18331869~~~~~~~~~~
  • tests/regressiontests/admin_inlines/tests.py

    diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py
    index c2e3bbc..629e2c3 100644
    a b from django.test import TestCase  
    77
    88# local test models
    99from .admin import InnerInline
     10from django.test.testcases import SeleniumTestCase
    1011from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
    1112    OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book)
    1213
    class TestInlinePermissions(TestCase):  
    380381        self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"')
    381382        self.assertContains(response, '<input type="hidden" name="inner2_set-0-id" value="%i"' % self.inner2_id)
    382383        self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
     384
     385
     386class AdminInlinesSeleniumTests(SeleniumTestCase):
     387    fixtures = ['admin-views-users.xml']
     388    urls = "regressiontests.admin_inlines.urls"
     389
     390    def admin_login(self, username, password):
     391        """
     392        Helper function to log into the admin.
     393        """
     394        self.selenium.open('/admin/')
     395        self.selenium.type('username', username)
     396        self.selenium.type('password', password)
     397        self.selenium.click("//input[@value='Log in']")
     398        self.selenium.wait_for_page_to_load(3000)
     399
     400    def test_add_inlines(self):
     401        """
     402        Ensure that the "Add another XXX" link correctly adds items to the
     403        inline form.
     404        """
     405        self.admin_login(username='super', password='secret')
     406        self.selenium.open('/admin/admin_inlines/titlecollection/add/')
     407
     408        # Check that there's only one inline to start with and that it has the
     409        # correct ID.
     410        self.failUnlessEqual(self.selenium.get_css_count(
     411            'css=#title_set-group table tr.dynamic-title_set'), 1)
     412        self.failUnless(self.selenium.get_attribute(
     413            'css=.dynamic-title_set:nth-of-type(1)@id'), 'title_set-0')
     414        self.failUnless(self.selenium.is_element_present(
     415            'css=form#titlecollection_form tr.dynamic-title_set#title_set-0 input[name=title_set-0-title1]'))
     416        self.failUnless(self.selenium.is_element_present(
     417            'css=form#titlecollection_form tr.dynamic-title_set#title_set-0 input[name=title_set-0-title2]'))
     418
     419        # Add an inline
     420        self.selenium.click("link=Add another Title")
     421
     422        # Check that the inline has been added, that it has the right id, and
     423        # that it contains the right fields.
     424        self.failUnlessEqual(self.selenium.get_css_count(
     425            'css=#title_set-group table tr.dynamic-title_set'), 2)
     426        self.failUnless(self.selenium.get_attribute(
     427            'css=.dynamic-title_set:nth-of-type(2)@id'), 'title_set-1')
     428        self.failUnless(self.selenium.is_element_present(
     429            'css=form#titlecollection_form tr.dynamic-title_set#title_set-1 input[name=title_set-1-title1]'))
     430        self.failUnless(self.selenium.is_element_present(
     431            'css=form#titlecollection_form tr.dynamic-title_set#title_set-1 input[name=title_set-1-title2]'))
     432
     433        # Let's add another one to be sure
     434        self.selenium.click("link=Add another Title")
     435        self.failUnlessEqual(self.selenium.get_css_count(
     436            'css=#title_set-group table tr.dynamic-title_set'), 3)
     437        self.failUnless(self.selenium.get_attribute(
     438            'css=.dynamic-title_set:nth-of-type(3)@id'), 'title_set-2')
     439        self.failUnless(self.selenium.is_element_present(
     440            'css=form#titlecollection_form tr.dynamic-title_set#title_set-2 input[name=title_set-2-title1]'))
     441        self.failUnless(self.selenium.is_element_present(
     442            'css=form#titlecollection_form tr.dynamic-title_set#title_set-2 input[name=title_set-2-title2]'))
     443
     444    def test_delete_inlines(self):
     445        self.admin_login(username='super', password='secret')
     446        self.selenium.open('/admin/admin_inlines/titlecollection/add/')
     447
     448        # Add a few inlines
     449        self.selenium.click("link=Add another Title")
     450        self.selenium.click("link=Add another Title")
     451        self.selenium.click("link=Add another Title")
     452        self.selenium.click("link=Add another Title")
     453        self.failUnlessEqual(self.selenium.get_css_count(
     454            'css=#title_set-group table tr.dynamic-title_set'), 5)
     455        self.failUnless(self.selenium.is_element_present(
     456            'css=form#titlecollection_form tr.dynamic-title_set#title_set-0'))
     457        self.failUnless(self.selenium.is_element_present(
     458            'css=form#titlecollection_form tr.dynamic-title_set#title_set-1'))
     459        self.failUnless(self.selenium.is_element_present(
     460            'css=form#titlecollection_form tr.dynamic-title_set#title_set-2'))
     461        self.failUnless(self.selenium.is_element_present(
     462            'css=form#titlecollection_form tr.dynamic-title_set#title_set-3'))
     463        self.failUnless(self.selenium.is_element_present(
     464            'css=form#titlecollection_form tr.dynamic-title_set#title_set-4'))
     465
     466        # Click on a few delete buttons
     467        self.selenium.click(
     468            'css=form#titlecollection_form tr.dynamic-title_set#title_set-1 td.delete a')
     469        self.selenium.click(
     470            'css=form#titlecollection_form tr.dynamic-title_set#title_set-2 td.delete a')
     471        # Verify that they're gone and that the IDs have been re-sequenced
     472        self.failUnlessEqual(self.selenium.get_css_count(
     473            'css=#title_set-group table tr.dynamic-title_set'), 3)
     474        self.failUnless(self.selenium.is_element_present(
     475            'css=form#titlecollection_form tr.dynamic-title_set#title_set-0'))
     476        self.failUnless(self.selenium.is_element_present(
     477            'css=form#titlecollection_form tr.dynamic-title_set#title_set-1'))
     478        self.failUnless(self.selenium.is_element_present(
     479            'css=form#titlecollection_form tr.dynamic-title_set#title_set-2'))
     480 No newline at end of file
  • tests/regressiontests/admin_widgets/tests.py

    diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
    index 08a1a59..f163dc0 100644
    a b from django.core.files.storage import default_storage  
    1111from django.core.files.uploadedfile import SimpleUploadedFile
    1212from django.db.models import DateField
    1313from django.test import TestCase as DjangoTestCase
     14from django.test.testcases import SeleniumTestCase
    1415from django.utils import translation
    1516from django.utils.html import conditional_escape
    1617from django.utils.unittest import TestCase
    class RelatedFieldWidgetWrapperTests(DjangoTestCase):  
    372373        # Used to fail with a name error.
    373374        w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
    374375        self.assertFalse(w.can_add_related)
     376
     377
     378class AdminWidgetsSeleniumTests(SeleniumTestCase):
     379    fixtures = ['admin-widgets-users.xml']
     380    urls = "regressiontests.admin_widgets.urls"
     381
     382    def admin_login(self, username, password):
     383        """
     384        Helper function to log into the admin.
     385        """
     386        self.selenium.open('/')
     387        self.selenium.type('username', username)
     388        self.selenium.type('password', password)
     389        self.selenium.click("//input[@value='Log in']")
     390        self.selenium.wait_for_page_to_load(3000)
     391
     392    def get_css_value(self, selector, attribute):
     393        """
     394        Helper function that returns the value for the CSS attribute of an
     395        DOM element specified by the given selector.
     396        """
     397        return self.selenium.get_eval(
     398            'selenium.browserbot.getCurrentWindow().django'
     399            '.jQuery("%s").css("%s")' % (selector, attribute))
     400
     401    def test_show_hide_date_time_picker_widgets(self):
     402        """
     403        Ensure that pressing the ESC key closes the date and time picker
     404        widgets.
     405        Refs #17064.
     406        """
     407        self.admin_login(username='super', password='secret')
     408        # Open a page that has a date and time picker widgets
     409        self.selenium.open('/admin_widgets/member/add/')
     410
     411        # First, with the date picker widget ---------------------------------
     412        # Check that the date picker is hidden
     413        self.assertEqual(
     414            self.get_css_value('#calendarbox0', 'display'), 'none')
     415        # Click the calendar icon
     416        self.selenium.click('id=calendarlink0')
     417        # Check that the date picker is visible
     418        self.assertEqual(
     419            self.get_css_value('#calendarbox0', 'display'), 'block')
     420        # Press the ESC key
     421        self.selenium.key_up('css=html', '27')
     422        # Check that the date picker is hidden again
     423        self.assertEqual(
     424            self.get_css_value('#calendarbox0', 'display'), 'none')
     425
     426        # Then, with the time picker widget ----------------------------------
     427        # Check that the time picker is hidden
     428        self.assertEqual(
     429            self.get_css_value('#clockbox0', 'display'), 'none')
     430        # Click the time icon
     431        self.selenium.click('id=clocklink0')
     432        # Check that the time picker is visible
     433        self.assertEqual(
     434            self.get_css_value('#clockbox0', 'display'), 'block')
     435        # Press the ESC key
     436        self.selenium.key_up('css=html', '27')
     437        # Check that the time picker is hidden again
     438        self.assertEqual(
     439            self.get_css_value('#clockbox0', 'display'), 'none')
     440 No newline at end of file
Back to Top