Code

Ticket #2879: 2879.selenium-support-extra-tests.diff

File 2879.selenium-support-extra-tests.diff, 30.6 KB (added by tomchristie, 3 years ago)
Line 
1diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
2index 6b09be2..373d280 100644
3--- a/django/conf/global_settings.py
4+++ b/django/conf/global_settings.py
5@@ -560,6 +560,10 @@ DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFil
6 # The name of the class to use to run the test suite
7 TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'
8 
9+# For the live test server (e.g. used for running Selenium tests)
10+LIVE_TEST_SERVER_HOST = 'localhost'
11+LIVE_TEST_SERVER_PORT = 8081
12+
13 ############
14 # FIXTURES #
15 ############
16diff --git a/django/test/__init__.py b/django/test/__init__.py
17index a3a03e3..21a4841 100644
18--- a/django/test/__init__.py
19+++ b/django/test/__init__.py
20@@ -4,5 +4,6 @@ Django Unit Test and Doctest framework.
21 
22 from django.test.client import Client, RequestFactory
23 from django.test.testcases import (TestCase, TransactionTestCase,
24-        SimpleTestCase, skipIfDBFeature, skipUnlessDBFeature)
25+    SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
26+    skipUnlessDBFeature)
27 from django.test.utils import Approximate
28diff --git a/django/test/testcases.py b/django/test/testcases.py
29index ee22ac2..049da26 100644
30--- a/django/test/testcases.py
31+++ b/django/test/testcases.py
32@@ -5,12 +5,19 @@ import sys
33 from functools import wraps
34 from urlparse import urlsplit, urlunsplit
35 from xml.dom.minidom import parseString, Node
36+import select
37+import socket
38+import threading
39 
40 from django.conf import settings
41+from django.contrib.staticfiles.handlers import StaticFilesHandler
42 from django.core import mail
43 from django.core.exceptions import ValidationError
44+from django.core.handlers.wsgi import WSGIHandler
45 from django.core.management import call_command
46 from django.core.signals import request_started
47+from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
48+    WSGIServerException)
49 from django.core.urlresolvers import clear_url_caches
50 from django.core.validators import EMPTY_VALUES
51 from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
52@@ -364,7 +371,7 @@ class TransactionTestCase(SimpleTestCase):
53         for db in databases:
54             call_command('flush', verbosity=0, interactive=False, database=db)
55 
56-            if hasattr(self, 'fixtures'):
57+            if getattr(self, 'fixtures', None):
58                 # We have to use this slightly awkward syntax due to the fact
59                 # that we're using *args and **kwargs together.
60                 call_command('loaddata', *self.fixtures,
61@@ -679,7 +686,7 @@ class TestCase(TransactionTestCase):
62         Site.objects.clear_cache()
63 
64         for db in databases:
65-            if hasattr(self, 'fixtures'):
66+            if getattr(self, 'fixtures', None):
67                 call_command('loaddata', *self.fixtures,
68                              **{
69                                 'verbosity': 0,
70@@ -732,3 +739,154 @@ def skipUnlessDBFeature(feature):
71     """
72     return _deferredSkip(lambda: not getattr(connection.features, feature),
73                          "Database doesn't support feature %s" % feature)
74+
75+class QuietWSGIRequestHandler(WSGIRequestHandler):
76+    """
77+    Just a regular WSGIRequestHandler except it doesn't log to the standard
78+    output any of the requests received, so as to not clutter the output for
79+    the tests' results.
80+    """
81+    def log_message(*args):
82+        pass
83+
84+class StoppableWSGIServer(WSGIServer):
85+    """
86+    The code in this class is borrowed from the `SocketServer.BaseServer` class
87+    in Python 2.6. The important functionality here is that the server is non-
88+    blocking and that it can be shut down at any moment. This is made possible
89+    by the server regularly polling the socket and checking if it has been
90+    asked to stop.
91+    Note for the future: Once Django stops supporting Python 2.5, this class
92+    can be removed as `WSGIServer` will have this ability to shutdown on
93+    demand.
94+    """
95+
96+    def __init__(self, *args, **kwargs):
97+        super(StoppableWSGIServer, self).__init__(*args, **kwargs)
98+        self.__is_shut_down = threading.Event()
99+        self.__serving = False
100+
101+    def serve_forever(self, poll_interval=0.5):
102+        """Handle one request at a time until shutdown.
103+
104+        Polls for shutdown every poll_interval seconds.
105+        """
106+        self.__serving = True
107+        self.__is_shut_down.clear()
108+        while self.__serving:
109+            r, w, e = select.select([self], [], [], poll_interval)
110+            if r:
111+                self._handle_request_noblock()
112+        self.__is_shut_down.set()
113+
114+    def shutdown(self):
115+        """Stops the serve_forever loop.
116+
117+        Blocks until the loop has finished. This must be called while
118+        serve_forever() is running in another thread, or it will
119+        deadlock.
120+        """
121+        self.__serving = False
122+        self.__is_shut_down.wait()
123+
124+    def handle_request(self):
125+        """Handle one request, possibly blocking.
126+        """
127+        fd_sets = select.select([self], [], [], None)
128+        if not fd_sets[0]:
129+            return
130+        self._handle_request_noblock()
131+
132+    def _handle_request_noblock(self):
133+        """Handle one request, without blocking.
134+
135+        I assume that select.select has returned that the socket is
136+        readable before this function was called, so there should be
137+        no risk of blocking in get_request().
138+        """
139+        try:
140+            request, client_address = self.get_request()
141+        except socket.error:
142+            return
143+        if self.verify_request(request, client_address):
144+            try:
145+                self.process_request(request, client_address)
146+            except Exception:
147+                self.handle_error(request, client_address)
148+                self.close_request(request)
149+
150+class LiveServerThread(threading.Thread):
151+    """
152+    Thread for running a live http server while the tests are running.
153+    """
154+
155+    def __init__(self, address, port, fixtures=None):
156+        self.address = address
157+        self.port = port
158+        self.fixtures = fixtures
159+        self.is_ready = threading.Event()
160+        self.error = None
161+        super(LiveServerThread, self).__init__()
162+
163+    def run(self):
164+        """
165+        Sets up live server and database and loops over handling http requests.
166+        """
167+        try:
168+            # Instantiate and start the server
169+            self.httpd = StoppableWSGIServer(
170+                (self.address, self.port), QuietWSGIRequestHandler)
171+            handler = StaticFilesHandler(WSGIHandler())
172+            self.httpd.set_app(handler)
173+
174+            # If the database is in memory we must reload the data in this new
175+            # thread.
176+            if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3' or
177+                settings.DATABASES['default']['TEST_NAME']):
178+                connection.creation.create_test_db(0)
179+                # Import the fixtures into the test database
180+                if self.fixtures:
181+                    call_command('loaddata', *self.fixtures,
182+                        **{'verbosity': 0})
183+            self.is_ready.set()
184+            self.httpd.serve_forever()
185+        except WSGIServerException, e:
186+            self.error = e
187+            self.is_ready.set()
188+
189+    def join(self, timeout=None):
190+        self.httpd.shutdown()
191+        self.httpd.server_close()
192+        super(LiveServerThread, self).join(timeout)
193+
194+class LiveServerTestCase(TestCase):
195+    """
196+    Does basically the same as TestCase but also launches a live http server in
197+    a separate thread so that the tests may use another testing framework, such
198+    as Selenium for example, instead of the built-in dummy client.
199+    """
200+
201+    @property
202+    def live_server_url(self):
203+        return 'http://%s:%s' % (settings.LIVE_TEST_SERVER_HOST,
204+                                 settings.LIVE_TEST_SERVER_PORT)
205+
206+    def setUp(self):
207+        # Launch the Django live server's thread
208+        self.server_thread = LiveServerThread(
209+            settings.LIVE_TEST_SERVER_HOST,
210+            int(settings.LIVE_TEST_SERVER_PORT),
211+            fixtures=getattr(self, 'fixtures', None))
212+        self.server_thread.start()
213+
214+        # Wait for the Django server to be ready
215+        self.server_thread.is_ready.wait()
216+        if self.server_thread.error:
217+            raise self.server_thread.error
218+
219+        super(LiveServerTestCase, self).setUp()
220+
221+    def tearDown(self):
222+        # Terminate the Django server's thread
223+        self.server_thread.join()
224+        super(LiveServerTestCase, self).tearDown()
225diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt
226index 5ec09fe..baa20a1 100644
227--- a/docs/internals/contributing/writing-code/unit-tests.txt
228+++ b/docs/internals/contributing/writing-code/unit-tests.txt
229@@ -122,6 +122,19 @@ Going beyond that, you can specify an individual test method like this:
230 
231     ./runtests.py --settings=path.to.settings i18n.TranslationTests.test_lazy_objects
232 
233+Running the Selenium tests
234+~~~~~~~~~~~~~~~~~~~~~~~~~~
235+
236+Some admin tests require Selenium 2 to work via a real Web browser. To allow
237+those tests to run and not be skipped, you must install the selenium_ package
238+(version > 2.13) into your Python path.
239+
240+Then, run the tests normally, for example:
241+
242+.. code-block:: bash
243+
244+    ./runtests.py --settings=test_sqlite admin_inlines
245+
246 Running all the tests
247 ~~~~~~~~~~~~~~~~~~~~~
248 
249@@ -135,6 +148,7 @@ dependencies:
250 *  setuptools_
251 *  memcached_, plus a :ref:`supported Python binding <memcached>`
252 *  gettext_ (:ref:`gettext_on_windows`)
253+*  selenium_
254 
255 If you want to test the memcached cache backend, you'll also need to define
256 a :setting:`CACHES` setting that points at your memcached instance.
257@@ -149,6 +163,7 @@ associated tests will be skipped.
258 .. _setuptools: http://pypi.python.org/pypi/setuptools/
259 .. _memcached: http://www.danga.com/memcached/
260 .. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html
261+.. _selenium: http://pypi.python.org/pypi/selenium
262 
263 .. _contrib-apps:
264 
265diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
266index 20366e3..2a794ce 100644
267--- a/docs/ref/settings.txt
268+++ b/docs/ref/settings.txt
269@@ -195,6 +195,30 @@ all cache keys used by the Django server.
270 
271 See the :ref:`cache documentation <cache_key_prefixing>` for more information.
272 
273+.. setting:: LIVE_TEST_SERVER_HOST
274+
275+LIVE_TEST_SERVER_HOST
276+~~~~~~~~~~~~~~~~~~~~~
277+
278+Default: ``'localhost'``
279+
280+Controls the host address at which the live test server gets started when using
281+a :class:`~django.test.LiveServerTestCase`.
282+
283+See also: :setting:`LIVE_TEST_SERVER_PORT`
284+
285+.. setting:: LIVE_TEST_SERVER_PORT
286+
287+LIVE_TEST_SERVER_PORT
288+~~~~~~~~~~~~~~~~~~~~~
289+
290+Default: ``8081``
291+
292+Controls the port at which the live test server gets started when using
293+a :class:`~django.test.LiveServerTestCase`.
294+
295+See also: :setting:`LIVE_TEST_SERVER_HOST`
296+
297 .. setting:: CACHES-LOCATION
298 
299 LOCATION
300diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
301index 181b4ff..e5f8dcc 100644
302--- a/docs/topics/testing.txt
303+++ b/docs/topics/testing.txt
304@@ -580,21 +580,20 @@ Some of the things you can do with the test client are:
305 * Test that a given request is rendered by a given Django template, with
306   a template context that contains certain values.
307 
308-Note that the test client is not intended to be a replacement for Twill_,
309+Note that the test client is not intended to be a replacement for Windmill_,
310 Selenium_, or other "in-browser" frameworks. Django's test client has
311 a different focus. In short:
312 
313 * Use Django's test client to establish that the correct view is being
314   called and that the view is collecting the correct context data.
315 
316-* Use in-browser frameworks such as Twill and Selenium to test *rendered*
317-  HTML and the *behavior* of Web pages, namely JavaScript functionality.
318+* Use in-browser frameworks such as Windmill_ and Selenium_ to test *rendered*
319+  HTML and the *behavior* of Web pages, namely JavaScript functionality. Django
320+  also provides special support for those frameworks; see the section on
321+  :class:`~django.test.LiveServerTestCase` for more details.
322 
323 A comprehensive test suite should use a combination of both test types.
324 
325-.. _Twill: http://twill.idyll.org/
326-.. _Selenium: http://seleniumhq.org/
327-
328 Overview and a quick example
329 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
330 
331@@ -1830,11 +1829,84 @@ set up, execute and tear down the test suite.
332     those options will be added to the list of command-line options that
333     the :djadmin:`test` command can use.
334 
335+Live test server
336+----------------
337+
338+.. versionadded:: 1.4
339+
340+.. currentmodule:: django.test
341+
342+.. class:: LiveServerTestCase()
343+
344+``LiveServerTestCase`` does basically the same as
345+:class:`~django.test.TestCase` with one extra thing: it launches a live Django
346+server in the background on setup, and shuts it down on teardown. This allows
347+to use other automated test clients than the
348+:ref:`Django dummy client <test-client>` such as, for example, the Selenium_ or
349+Windmill_ clients, to execute a series of functional tests inside a browser and
350+simulate a real user's actions.
351+
352+You may control which host and port the live server will run at with
353+respectively the :setting:`LIVE_TEST_SERVER_HOST` and
354+:setting:`LIVE_TEST_SERVER_PORT` settings. The full server url can then be
355+accessed during the tests with ``self.live_server_url``.
356+
357+To demonstrate how to use ``LiveServerTestCase``, let's write a simple Selenium
358+test. First of all, you need to install the `selenium package`_ into your
359+Python path:
360+
361+.. code-block:: bash
362+
363+   pip install selenium
364+
365+Then, add the following code to one of your app's tests module (for example:
366+``myapp/tests.py``):
367+
368+.. code-block:: python
369+
370+    from django.test import LiveServerTestCase
371+    from selenium.webdriver.firefox.webdriver import WebDriver
372+
373+    class MySeleniumTests(LiveServerTestCase):
374+        fixtures = ['user-data.json']
375+
376+        def setUp(self):
377+            self.selenium = WebDriver()
378+            super(MySeleniumTests, self).setUp()
379+
380+        def tearDown(self):
381+            super(MySeleniumTests, self).tearDown()
382+            self.selenium.quit()
383+
384+        def test_login(self):
385+            self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
386+            username_input = self.selenium.find_element_by_name("username")
387+            username_input.send_keys('myuser')
388+            password_input = self.selenium.find_element_by_name("password")
389+            password_input.send_keys('secret')
390+            self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
391+
392+Finally, you may run the test as follows:
393+
394+.. code-block:: bash
395+
396+    ./manage.py test myapp.MySeleniumTests.test_login
397+
398+This example will automatically open Firefox then go to the login page, enter
399+the credentials and press the "Log in" button. Selenium offers other drivers in
400+case you do not have Firefox installed or wish to use another browser. The
401+example above is just a tiny fraction of what the Selenium client can do; check
402+out the `full reference`_ for more details.
403+
404+.. _Windmill: http://www.getwindmill.com/
405+.. _Selenium: http://seleniumhq.org/
406+.. _selenium package: http://pypi.python.org/pypi/selenium
407+.. _full reference: http://readthedocs.org/docs/selenium-python/en/latest/api.html
408+.. _Firefox: http://www.mozilla.com/firefox/
409 
410 Attributes
411 ~~~~~~~~~~
412 
413-
414 .. attribute:: DjangoTestSuiteRunner.option_list
415 
416     .. versionadded:: 1.4
417diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py
418index df872cd..351b6e9 100644
419--- a/tests/modeltests/test_client/models.py
420+++ b/tests/modeltests/test_client/models.py
421@@ -489,6 +489,7 @@ class CSRFEnabledClientTests(TestCase):
422 class CustomTestClient(Client):
423     i_am_customized = "Yes"
424 
425+
426 class CustomTestClientTest(TestCase):
427     client_class = CustomTestClient
428 
429diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py
430index c2e3bbc..d4f8f67 100644
431--- a/tests/regressiontests/admin_inlines/tests.py
432+++ b/tests/regressiontests/admin_inlines/tests.py
433@@ -1,5 +1,6 @@
434 from __future__ import absolute_import
435 
436+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
437 from django.contrib.admin.helpers import InlineAdminForm
438 from django.contrib.auth.models import User, Permission
439 from django.contrib.contenttypes.models import ContentType
440@@ -380,3 +381,95 @@ class TestInlinePermissions(TestCase):
441         self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"')
442         self.assertContains(response, '<input type="hidden" name="inner2_set-0-id" value="%i"' % self.inner2_id)
443         self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
444+
445+class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
446+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
447+    fixtures = ['admin-views-users.xml']
448+    urls = "regressiontests.admin_inlines.urls"
449+
450+    def test_add_inlines(self):
451+        """
452+        Ensure that the "Add another XXX" link correctly adds items to the
453+        inline form.
454+        """
455+        self.admin_login(username='super', password='secret')
456+        self.selenium.get('%s%s' % (self.live_server_url,
457+            '/admin/admin_inlines/titlecollection/add/'))
458+
459+        # Check that there's only one inline to start with and that it has the
460+        # correct ID.
461+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
462+            '#title_set-group table tr.dynamic-title_set')), 1)
463+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
464+            '.dynamic-title_set:nth-of-type(1)').get_attribute('id'),
465+            'title_set-0')
466+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
467+            'form#titlecollection_form tr.dynamic-title_set#title_set-0 input[name=title_set-0-title1]')), 1)
468+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
469+            'form#titlecollection_form tr.dynamic-title_set#title_set-0 input[name=title_set-0-title2]')), 1)
470+
471+        # Add an inline
472+        self.selenium.find_element_by_link_text('Add another Title').click()
473+
474+        # Check that the inline has been added, that it has the right id, and
475+        # that it contains the right fields.
476+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
477+            '#title_set-group table tr.dynamic-title_set')), 2)
478+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
479+            '.dynamic-title_set:nth-of-type(2)').get_attribute('id'), 'title_set-1')
480+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
481+            'form#titlecollection_form tr.dynamic-title_set#title_set-1 input[name=title_set-1-title1]')), 1)
482+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
483+            'form#titlecollection_form tr.dynamic-title_set#title_set-1 input[name=title_set-1-title2]')), 1)
484+
485+        # Let's add another one to be sure
486+        self.selenium.find_element_by_link_text('Add another Title').click()
487+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
488+            '#title_set-group table tr.dynamic-title_set')), 3)
489+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
490+            '.dynamic-title_set:nth-of-type(3)').get_attribute('id'), 'title_set-2')
491+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
492+            'form#titlecollection_form tr.dynamic-title_set#title_set-2 input[name=title_set-2-title1]')), 1)
493+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
494+            'form#titlecollection_form tr.dynamic-title_set#title_set-2 input[name=title_set-2-title2]')), 1)
495+
496+    def test_delete_inlines(self):
497+        self.admin_login(username='super', password='secret')
498+        self.selenium.get('%s%s' % (self.live_server_url,
499+            '/admin/admin_inlines/titlecollection/add/'))
500+
501+        # Add a few inlines
502+        self.selenium.find_element_by_link_text('Add another Title').click()
503+        self.selenium.find_element_by_link_text('Add another Title').click()
504+        self.selenium.find_element_by_link_text('Add another Title').click()
505+        self.selenium.find_element_by_link_text('Add another Title').click()
506+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
507+            '#title_set-group table tr.dynamic-title_set')), 5)
508+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
509+            'form#titlecollection_form tr.dynamic-title_set#title_set-0')), 1)
510+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
511+            'form#titlecollection_form tr.dynamic-title_set#title_set-1')), 1)
512+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
513+            'form#titlecollection_form tr.dynamic-title_set#title_set-2')), 1)
514+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
515+            'form#titlecollection_form tr.dynamic-title_set#title_set-3')), 1)
516+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
517+            'form#titlecollection_form tr.dynamic-title_set#title_set-4')), 1)
518+
519+        # Click on a few delete buttons
520+        self.selenium.find_element_by_css_selector(
521+            'form#titlecollection_form tr.dynamic-title_set#title_set-1 td.delete a').click()
522+        self.selenium.find_element_by_css_selector(
523+            'form#titlecollection_form tr.dynamic-title_set#title_set-2 td.delete a').click()
524+        # Verify that they're gone and that the IDs have been re-sequenced
525+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
526+            '#title_set-group table tr.dynamic-title_set')), 3)
527+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
528+            'form#titlecollection_form tr.dynamic-title_set#title_set-0')), 1)
529+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
530+            'form#titlecollection_form tr.dynamic-title_set#title_set-1')), 1)
531+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
532+            'form#titlecollection_form tr.dynamic-title_set#title_set-2')), 1)
533+
534+class SeleniumInternetExplorerTests(SeleniumFirefoxTests):
535+    webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
536\ No newline at end of file
537diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
538index 08a1a59..a4b2bf4 100644
539--- a/tests/regressiontests/admin_widgets/tests.py
540+++ b/tests/regressiontests/admin_widgets/tests.py
541@@ -7,6 +7,7 @@ from django import forms
542 from django.conf import settings
543 from django.contrib import admin
544 from django.contrib.admin import widgets
545+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
546 from django.core.files.storage import default_storage
547 from django.core.files.uploadedfile import SimpleUploadedFile
548 from django.db.models import DateField
549@@ -372,3 +373,55 @@ class RelatedFieldWidgetWrapperTests(DjangoTestCase):
550         # Used to fail with a name error.
551         w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
552         self.assertFalse(w.can_add_related)
553+
554+
555+class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
556+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
557+    fixtures = ['admin-widgets-users.xml']
558+    urls = "regressiontests.admin_widgets.urls"
559+
560+    def test_show_hide_date_time_picker_widgets(self):
561+        """
562+        Ensure that pressing the ESC key closes the date and time picker
563+        widgets.
564+        Refs #17064.
565+        """
566+        from selenium.webdriver.common.keys import Keys
567+
568+        self.admin_login(username='super', password='secret', login_url='/')
569+        # Open a page that has a date and time picker widgets
570+        self.selenium.get('%s%s' % (self.live_server_url,
571+            '/admin_widgets/member/add/'))
572+
573+        # First, with the date picker widget ---------------------------------
574+        # Check that the date picker is hidden
575+        self.assertEqual(
576+            self.get_css_value('#calendarbox0', 'display'), 'none')
577+        # Click the calendar icon
578+        self.selenium.find_element_by_id('calendarlink0').click()
579+        # Check that the date picker is visible
580+        self.assertEqual(
581+            self.get_css_value('#calendarbox0', 'display'), 'block')
582+        # Press the ESC key
583+        self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
584+        # Check that the date picker is hidden again
585+        self.assertEqual(
586+            self.get_css_value('#calendarbox0', 'display'), 'none')
587+
588+        # Then, with the time picker widget ----------------------------------
589+        # Check that the time picker is hidden
590+        self.assertEqual(
591+            self.get_css_value('#clockbox0', 'display'), 'none')
592+        # Click the time icon
593+        self.selenium.find_element_by_id('clocklink0').click()
594+        # Check that the time picker is visible
595+        self.assertEqual(
596+            self.get_css_value('#clockbox0', 'display'), 'block')
597+        # Press the ESC key
598+        self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
599+        # Check that the time picker is hidden again
600+        self.assertEqual(
601+            self.get_css_value('#clockbox0', 'display'), 'none')
602+
603+class SeleniumInternetExplorerTests(SeleniumFirefoxTests):
604+    webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
605\ No newline at end of file
606diff --git a/tests/regressiontests/live_server_tests/models.py b/tests/regressiontests/live_server_tests/models.py
607index e69de29..6e1414a 100644
608--- a/tests/regressiontests/live_server_tests/models.py
609+++ b/tests/regressiontests/live_server_tests/models.py
610@@ -0,0 +1,5 @@
611+from django.db import models
612+
613+
614+class Person(models.Model):
615+    name = models.CharField(max_length=256)
616diff --git a/tests/regressiontests/live_server_tests/tests.py b/tests/regressiontests/live_server_tests/tests.py
617index e69de29..5e431b9 100644
618--- a/tests/regressiontests/live_server_tests/tests.py
619+++ b/tests/regressiontests/live_server_tests/tests.py
620@@ -0,0 +1,106 @@
621+import os
622+import urllib2
623+
624+from django.conf import settings
625+from django.test import LiveServerTestCase
626+from django.test.utils import override_settings
627+
628+from .models import Person
629+
630+
631+TEST_ROOT = os.path.dirname(__file__)
632+TEST_SETTINGS = {
633+    'MEDIA_URL': '/media/',
634+    'MEDIA_ROOT': os.path.join(TEST_ROOT, 'media'),
635+}
636+
637+
638+class LiveServerTestBase(LiveServerTestCase):
639+    urls = 'regressiontests.live_server_tests.urls'
640+    fixtures = ['testdata.json']
641+
642+    def urlopen(self, url):
643+        base = 'http://%s:%s' % (settings.LIVE_TEST_SERVER_HOST,
644+                                 settings.LIVE_TEST_SERVER_PORT)
645+        return urllib2.urlopen(base + url)
646+
647+
648+class TestViews(LiveServerTestBase):
649+    def test_404(self):
650+        """
651+        Ensure that the LiveServerTestCase serves 404s.
652+        """
653+        try:
654+            self.urlopen('/')
655+        except urllib2.HTTPError, err:
656+            self.assertEquals(err.code, 404, 'Expected 404 response')
657+        else:
658+            self.fail('Expected 404 response')
659+
660+    def test_view(self):
661+        """
662+        Ensure that the LiveServerTestCase serves views.
663+        """
664+        f = self.urlopen('/example_view/')
665+        self.assertEquals(f.read(), 'example view')
666+
667+    def test_static_file(self):
668+        """
669+        Ensure that the LiveServerTestCase serves static files.
670+        """
671+        f = self.urlopen('/static/example_file.txt')
672+        self.assertEquals(f.read(), 'example file\n')
673+
674+    def test_media_files(self):
675+        """
676+        Ensure that the LiveServerTestCase serves media files.
677+        """
678+        f = self.urlopen('/media/example_media_file.txt')
679+        self.assertEquals(f.read(), 'example media file\n')
680+
681+TestViews = override_settings(**TEST_SETTINGS)(TestViews)
682+
683+
684+class TestDatabase(LiveServerTestBase):
685+    def test_fixtures_loaded(self):
686+        """
687+        Ensure that fixtures are properly loaded and visible to the
688+        live server thread.
689+        """
690+        f = self.urlopen('/model_view/')
691+        self.assertEquals(f.read().splitlines(), ['jane', 'robert'])
692+
693+    def test_database_writes(self):
694+        """
695+        Ensure that data written to the database by a view can be read.
696+        """
697+        self.urlopen('/create_model_instance/')
698+        names = [person.name for person in Person.objects.all()]
699+        self.assertEquals(names, ['jane', 'robert', 'emily'])
700+
701+
702+class TestDatabaseIsolation(LiveServerTestBase):
703+    urls = 'regressiontests.live_server_tests.urls'
704+    fixtures = ['testdata.json']
705+
706+    def urlopen(self, url):
707+        return urllib2.urlopen('http://localhost:8081' + url)
708+
709+    def test_data_seperation_1(self):
710+        """
711+        Ensure destructive operations on the database are isolated between
712+        tests.  Both test_data_seperation_1 and test_data_seperation_2
713+        must run.
714+        """
715+        f = self.urlopen('/model_view/')
716+        self.assertEquals(f.read().splitlines(), ['jane', 'robert'])
717+        self.urlopen('/create_model_instance/')
718+        f = self.urlopen('/model_view/')
719+        self.assertEquals(f.read().splitlines(), ['jane', 'robert', 'emily'])
720+
721+    def test_data_seperation_2(self):
722+        f = self.urlopen('/model_view/')
723+        self.assertEquals(f.read().splitlines(), ['jane', 'robert'])
724+        self.urlopen('/create_model_instance/')
725+        f = self.urlopen('/model_view/')
726+        self.assertEquals(f.read().splitlines(), ['jane', 'robert', 'emily'])
727diff --git a/tests/regressiontests/live_server_tests/urls.py b/tests/regressiontests/live_server_tests/urls.py
728index e69de29..83f757f 100644
729--- a/tests/regressiontests/live_server_tests/urls.py
730+++ b/tests/regressiontests/live_server_tests/urls.py
731@@ -0,0 +1,12 @@
732+from __future__ import absolute_import
733+
734+from django.conf.urls import patterns, url
735+
736+from . import views
737+
738+
739+urlpatterns = patterns('',
740+    url(r'^example_view/$', views.example_view),
741+    url(r'^model_view/$', views.model_view),
742+    url(r'^create_model_instance/$', views.create_model_instance),
743+)
744diff --git a/tests/regressiontests/live_server_tests/views.py b/tests/regressiontests/live_server_tests/views.py
745index e69de29..42fafe0 100644
746--- a/tests/regressiontests/live_server_tests/views.py
747+++ b/tests/regressiontests/live_server_tests/views.py
748@@ -0,0 +1,17 @@
749+from django.http import HttpResponse
750+from .models import Person
751+
752+
753+def example_view(request):
754+    return HttpResponse('example view')
755+
756+
757+def model_view(request):
758+    people = Person.objects.all()
759+    return HttpResponse('\n'.join([person.name for person in people]))
760+
761+
762+def create_model_instance(request):
763+    person = Person(name='emily')
764+    person.save()
765+    return HttpResponse('')