diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 0aee63d..da9d030 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -565,6 +565,10 @@ DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFil
 # The name of the class to use to run the test suite
 TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'
 
+# For the live test server (e.g. used for running Selenium tests)
+LIVE_TEST_SERVER_HOST = 'localhost'
+LIVE_TEST_SERVER_PORT = 8081
+
 ############
 # FIXTURES #
 ############
diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py
new file mode 100644
index 0000000..8a09604
--- /dev/null
+++ b/django/contrib/admin/tests.py
@@ -0,0 +1,44 @@
+from django.test import LiveServerTestCase
+from django.utils.importlib import import_module
+from django.utils.unittest import SkipTest
+
+class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
+
+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
+
+    def setUp(self):
+        try:
+            # Import and start the WebDriver class.
+            module, attr = self.webdriver_class.rsplit('.', 1)
+            mod = import_module(module)
+            WebDriver = getattr(mod, attr)
+            self.selenium = WebDriver()
+        except Exception:
+            raise SkipTest('Selenium webdriver "%s" not installed or not '
+                           'operational.' % self.webdriver_class)
+        super(AdminSeleniumWebDriverTestCase, self).setUp()
+
+    def tearDown(self):
+        super(AdminSeleniumWebDriverTestCase, self).tearDown()
+        if hasattr(self, 'selenium'):
+            self.selenium.quit()
+
+    def admin_login(self, username, password, login_url='/admin/'):
+        """
+        Helper function to log into the admin.
+        """
+        self.selenium.get('%s%s' % (self.live_server_url, login_url))
+        username_input = self.selenium.find_element_by_name("username")
+        username_input.send_keys(username)
+        password_input = self.selenium.find_element_by_name("password")
+        password_input.send_keys(password)
+        self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
+
+    def get_css_value(self, selector, attribute):
+        """
+        Helper function that returns the value for the CSS attribute of an
+        DOM element specified by the given selector. Uses the jQuery that ships
+        with Django.
+        """
+        return self.selenium.execute_script(
+            'return django.jQuery("%s").css("%s")' % (selector, attribute))
\ No newline at end of file
diff --git a/django/test/__init__.py b/django/test/__init__.py
index a3a03e3..21a4841 100644
--- a/django/test/__init__.py
+++ b/django/test/__init__.py
@@ -4,5 +4,6 @@ Django Unit Test and Doctest framework.
 
 from django.test.client import Client, RequestFactory
 from django.test.testcases import (TestCase, TransactionTestCase,
-        SimpleTestCase, skipIfDBFeature, skipUnlessDBFeature)
+    SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
+    skipUnlessDBFeature)
 from django.test.utils import Approximate
diff --git a/django/test/testcases.py b/django/test/testcases.py
index ee22ac2..4e69b48 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -5,12 +5,19 @@ import sys
 from functools import wraps
 from urlparse import urlsplit, urlunsplit
 from xml.dom.minidom import parseString, Node
+import select
+import socket
+import threading
 
 from django.conf import settings
+from django.contrib.staticfiles.handlers import StaticFilesHandler
 from django.core import mail
 from django.core.exceptions import ValidationError
+from django.core.handlers.wsgi import WSGIHandler
 from django.core.management import call_command
 from django.core.signals import request_started
+from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
+    WSGIServerException)
 from django.core.urlresolvers import clear_url_caches
 from django.core.validators import EMPTY_VALUES
 from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
@@ -23,6 +30,7 @@ from django.test.utils import (get_warnings_state, restore_warnings_state,
     override_settings)
 from django.utils import simplejson, unittest as ut2
 from django.utils.encoding import smart_str
+from django.views.static import serve
 
 __all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
            'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature')
@@ -732,3 +740,165 @@ def skipUnlessDBFeature(feature):
     """
     return _deferredSkip(lambda: not getattr(connection.features, feature),
                          "Database doesn't support feature %s" % feature)
+
+class QuietWSGIRequestHandler(WSGIRequestHandler):
+    """
+    Just a regular WSGIRequestHandler except it doesn't log to the standard
+    output any of the requests received, so as to not clutter the output for
+    the tests' results.
+    """
+    def log_message(*args):
+        pass
+
+class StoppableWSGIServer(WSGIServer):
+    """
+    The code in this class is borrowed from the `SocketServer.BaseServer` class
+    in Python 2.6. The important functionality here is that the server is non-
+    blocking and that it can be shut down at any moment. This is made possible
+    by the server regularly polling the socket and checking if it has been
+    asked to stop.
+    Note for the future: Once Django stops supporting Python 2.5, this class
+    can be removed as `WSGIServer` will have this ability to shutdown on
+    demand.
+    """
+
+    def __init__(self, *args, **kwargs):
+        super(StoppableWSGIServer, self).__init__(*args, **kwargs)
+        self.__is_shut_down = threading.Event()
+        self.__serving = False
+
+    def serve_forever(self, poll_interval=0.5):
+        """Handle one request at a time until shutdown.
+
+        Polls for shutdown every poll_interval seconds.
+        """
+        self.__serving = True
+        self.__is_shut_down.clear()
+        while self.__serving:
+            r, w, e = select.select([self], [], [], poll_interval)
+            if r:
+                self._handle_request_noblock()
+        self.__is_shut_down.set()
+
+    def shutdown(self):
+        """Stops the serve_forever loop.
+
+        Blocks until the loop has finished. This must be called while
+        serve_forever() is running in another thread, or it will
+        deadlock.
+        """
+        self.__serving = False
+        self.__is_shut_down.wait()
+
+    def handle_request(self):
+        """Handle one request, possibly blocking.
+        """
+        fd_sets = select.select([self], [], [], None)
+        if not fd_sets[0]:
+            return
+        self._handle_request_noblock()
+
+    def _handle_request_noblock(self):
+        """Handle one request, without blocking.
+
+        I assume that select.select has returned that the socket is
+        readable before this function was called, so there should be
+        no risk of blocking in get_request().
+        """
+        try:
+            request, client_address = self.get_request()
+        except socket.error:
+            return
+        if self.verify_request(request, client_address):
+            try:
+                self.process_request(request, client_address)
+            except Exception:
+                self.handle_error(request, client_address)
+                self.close_request(request)
+
+class MediaFilesHandler(StaticFilesHandler):
+    """
+    Handler for serving the media files.
+    """
+
+    def get_base_dir(self):
+        return settings.MEDIA_ROOT
+
+    def get_base_url(self):
+        return settings.MEDIA_URL
+
+    def serve(self, request):
+        return serve(request, self.file_path(request.path),
+            document_root=self.get_base_dir())
+
+class LiveServerThread(threading.Thread):
+    """
+    Thread for running a live http server while the tests are running.
+    """
+
+    def __init__(self, address, port):
+        self.address = address
+        self.port = port
+        self.is_ready = threading.Event()
+        self.error = None
+        super(LiveServerThread, self).__init__()
+
+    def run(self):
+        """
+        Sets up live server and database and loops over handling http requests.
+        """
+        try:
+            # Instantiate and start the server
+            self.httpd = StoppableWSGIServer(
+                (self.address, self.port), QuietWSGIRequestHandler)
+            handler = StaticFilesHandler(MediaFilesHandler(WSGIHandler()))
+            self.httpd.set_app(handler)
+            self.is_ready.set()
+            self.httpd.serve_forever()
+        except WSGIServerException, e:
+            self.error = e
+            self.is_ready.set()
+
+    def join(self, timeout=None):
+        self.httpd.shutdown()
+        self.httpd.server_close()
+        super(LiveServerThread, self).join(timeout)
+
+class LiveServerTestCase(TransactionTestCase):
+    """
+    Does basically the same as TransactionTestCase but also launches a live
+    http server in a separate thread so that the tests may use another testing
+    framework, such as Selenium for example, instead of the built-in dummy
+    client.
+    """
+
+    @property
+    def live_server_url(self):
+        return 'http://%s:%s' % (settings.LIVE_TEST_SERVER_HOST,
+                                 settings.LIVE_TEST_SERVER_PORT)
+
+    def setUp(self):
+        for conn in connections.all():
+            if (conn.settings_dict['ENGINE'] == 'django.db.backends.sqlite3'
+                and conn.settings_dict['NAME'] == ':memory:'):
+                raise ut2.SkipTest('In-memory sqlite databases are not '
+                    'supported by LiveServerTestCase because the memory '
+                    'cannot be shared between threads.')
+
+        # Launch the Django live server's thread
+        self.server_thread = LiveServerThread(
+            settings.LIVE_TEST_SERVER_HOST,
+            int(settings.LIVE_TEST_SERVER_PORT))
+        self.server_thread.start()
+
+        # Wait for the Django server to be ready
+        self.server_thread.is_ready.wait()
+        if self.server_thread.error:
+            raise self.server_thread.error
+
+        super(LiveServerTestCase, self).setUp()
+
+    def tearDown(self):
+        # Terminate the Django server's thread
+        self.server_thread.join()
+        super(LiveServerTestCase, self).tearDown()
diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt
index 5ec09fe..baa20a1 100644
--- a/docs/internals/contributing/writing-code/unit-tests.txt
+++ b/docs/internals/contributing/writing-code/unit-tests.txt
@@ -122,6 +122,19 @@ Going beyond that, you can specify an individual test method like this:
 
     ./runtests.py --settings=path.to.settings i18n.TranslationTests.test_lazy_objects
 
+Running the Selenium tests
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Some admin tests require Selenium 2 to work via a real Web browser. To allow
+those tests to run and not be skipped, you must install the selenium_ package
+(version > 2.13) into your Python path.
+
+Then, run the tests normally, for example:
+
+.. code-block:: bash
+
+    ./runtests.py --settings=test_sqlite admin_inlines
+
 Running all the tests
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -135,6 +148,7 @@ dependencies:
 *  setuptools_
 *  memcached_, plus a :ref:`supported Python binding <memcached>`
 *  gettext_ (:ref:`gettext_on_windows`)
+*  selenium_
 
 If you want to test the memcached cache backend, you'll also need to define
 a :setting:`CACHES` setting that points at your memcached instance.
@@ -149,6 +163,7 @@ associated tests will be skipped.
 .. _setuptools: http://pypi.python.org/pypi/setuptools/
 .. _memcached: http://www.danga.com/memcached/
 .. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html
+.. _selenium: http://pypi.python.org/pypi/selenium
 
 .. _contrib-apps:
 
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index a35d99a..89f2c47 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -195,6 +195,30 @@ all cache keys used by the Django server.
 
 See the :ref:`cache documentation <cache_key_prefixing>` for more information.
 
+.. setting:: LIVE_TEST_SERVER_HOST
+
+LIVE_TEST_SERVER_HOST
+~~~~~~~~~~~~~~~~~~~~~
+
+Default: ``'localhost'``
+
+Controls the host address at which the live test server gets started when using
+a :class:`~django.test.LiveServerTestCase`.
+
+See also: :setting:`LIVE_TEST_SERVER_PORT`
+
+.. setting:: LIVE_TEST_SERVER_PORT
+
+LIVE_TEST_SERVER_PORT
+~~~~~~~~~~~~~~~~~~~~~
+
+Default: ``8081``
+
+Controls the port at which the live test server gets started when using
+a :class:`~django.test.LiveServerTestCase`.
+
+See also: :setting:`LIVE_TEST_SERVER_HOST`
+
 .. setting:: CACHES-LOCATION
 
 LOCATION
diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
index 181b4ff..fdf4e81 100644
--- a/docs/topics/testing.txt
+++ b/docs/topics/testing.txt
@@ -580,21 +580,20 @@ Some of the things you can do with the test client are:
 * Test that a given request is rendered by a given Django template, with
   a template context that contains certain values.
 
-Note that the test client is not intended to be a replacement for Twill_,
+Note that the test client is not intended to be a replacement for Windmill_,
 Selenium_, or other "in-browser" frameworks. Django's test client has
 a different focus. In short:
 
 * Use Django's test client to establish that the correct view is being
   called and that the view is collecting the correct context data.
 
-* Use in-browser frameworks such as Twill and Selenium to test *rendered*
-  HTML and the *behavior* of Web pages, namely JavaScript functionality.
+* Use in-browser frameworks such as Windmill_ and Selenium_ to test *rendered*
+  HTML and the *behavior* of Web pages, namely JavaScript functionality. Django
+  also provides special support for those frameworks; see the section on
+  :class:`~django.test.LiveServerTestCase` for more details.
 
 A comprehensive test suite should use a combination of both test types.
 
-.. _Twill: http://twill.idyll.org/
-.. _Selenium: http://seleniumhq.org/
-
 Overview and a quick example
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1830,11 +1829,84 @@ set up, execute and tear down the test suite.
     those options will be added to the list of command-line options that
     the :djadmin:`test` command can use.
 
+Live test server
+----------------
+
+.. versionadded:: 1.4
+
+.. currentmodule:: django.test
+
+.. class:: LiveServerTestCase()
+
+``LiveServerTestCase`` does basically the same as
+:class:`~django.test.TransactionTestCase` with one extra thing: it launches a
+live Django server in the background on setup, and shuts it down on teardown.
+This allows to use other automated test clients than the
+:ref:`Django dummy client <test-client>` such as, for example, the Selenium_ or
+Windmill_ clients, to execute a series of functional tests inside a browser and
+simulate a real user's actions.
+
+You may control which host and port the live server will run at with
+respectively the :setting:`LIVE_TEST_SERVER_HOST` and
+:setting:`LIVE_TEST_SERVER_PORT` settings. The full server url can then be
+accessed during the tests with ``self.live_server_url``.
+
+To demonstrate how to use ``LiveServerTestCase``, let's write a simple Selenium
+test. First of all, you need to install the `selenium package`_ into your
+Python path:
+
+.. code-block:: bash
+
+   pip install selenium
+
+Then, add the following code to one of your app's tests module (for example:
+``myapp/tests.py``):
+
+.. code-block:: python
+
+    from django.test import LiveServerTestCase
+    from selenium.webdriver.firefox.webdriver import WebDriver
+
+    class MySeleniumTests(LiveServerTestCase):
+        fixtures = ['user-data.json']
+
+        def setUp(self):
+            self.selenium = WebDriver()
+            super(MySeleniumTests, self).setUp()
+
+        def tearDown(self):
+            super(MySeleniumTests, self).tearDown()
+            self.selenium.quit()
+
+        def test_login(self):
+            self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
+            username_input = self.selenium.find_element_by_name("username")
+            username_input.send_keys('myuser')
+            password_input = self.selenium.find_element_by_name("password")
+            password_input.send_keys('secret')
+            self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
+
+Finally, you may run the test as follows:
+
+.. code-block:: bash
+
+    ./manage.py test myapp.MySeleniumTests.test_login
+
+This example will automatically open Firefox then go to the login page, enter
+the credentials and press the "Log in" button. Selenium offers other drivers in
+case you do not have Firefox installed or wish to use another browser. The
+example above is just a tiny fraction of what the Selenium client can do; check
+out the `full reference`_ for more details.
+
+.. _Windmill: http://www.getwindmill.com/
+.. _Selenium: http://seleniumhq.org/
+.. _selenium package: http://pypi.python.org/pypi/selenium
+.. _full reference: http://readthedocs.org/docs/selenium-python/en/latest/api.html
+.. _Firefox: http://www.mozilla.com/firefox/
 
 Attributes
 ~~~~~~~~~~
 
-
 .. attribute:: DjangoTestSuiteRunner.option_list
 
     .. versionadded:: 1.4
diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py
index c2e3bbc..d4f8f67 100644
--- a/tests/regressiontests/admin_inlines/tests.py
+++ b/tests/regressiontests/admin_inlines/tests.py
@@ -1,5 +1,6 @@
 from __future__ import absolute_import
 
+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
 from django.contrib.admin.helpers import InlineAdminForm
 from django.contrib.auth.models import User, Permission
 from django.contrib.contenttypes.models import ContentType
@@ -380,3 +381,95 @@ class TestInlinePermissions(TestCase):
         self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"')
         self.assertContains(response, '<input type="hidden" name="inner2_set-0-id" value="%i"' % self.inner2_id)
         self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
+
+class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
+    fixtures = ['admin-views-users.xml']
+    urls = "regressiontests.admin_inlines.urls"
+
+    def test_add_inlines(self):
+        """
+        Ensure that the "Add another XXX" link correctly adds items to the
+        inline form.
+        """
+        self.admin_login(username='super', password='secret')
+        self.selenium.get('%s%s' % (self.live_server_url,
+            '/admin/admin_inlines/titlecollection/add/'))
+
+        # Check that there's only one inline to start with and that it has the
+        # correct ID.
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            '#title_set-group table tr.dynamic-title_set')), 1)
+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
+            '.dynamic-title_set:nth-of-type(1)').get_attribute('id'),
+            'title_set-0')
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-0 input[name=title_set-0-title1]')), 1)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-0 input[name=title_set-0-title2]')), 1)
+
+        # Add an inline
+        self.selenium.find_element_by_link_text('Add another Title').click()
+
+        # Check that the inline has been added, that it has the right id, and
+        # that it contains the right fields.
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            '#title_set-group table tr.dynamic-title_set')), 2)
+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
+            '.dynamic-title_set:nth-of-type(2)').get_attribute('id'), 'title_set-1')
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-1 input[name=title_set-1-title1]')), 1)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-1 input[name=title_set-1-title2]')), 1)
+
+        # Let's add another one to be sure
+        self.selenium.find_element_by_link_text('Add another Title').click()
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            '#title_set-group table tr.dynamic-title_set')), 3)
+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
+            '.dynamic-title_set:nth-of-type(3)').get_attribute('id'), 'title_set-2')
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-2 input[name=title_set-2-title1]')), 1)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-2 input[name=title_set-2-title2]')), 1)
+
+    def test_delete_inlines(self):
+        self.admin_login(username='super', password='secret')
+        self.selenium.get('%s%s' % (self.live_server_url,
+            '/admin/admin_inlines/titlecollection/add/'))
+
+        # Add a few inlines
+        self.selenium.find_element_by_link_text('Add another Title').click()
+        self.selenium.find_element_by_link_text('Add another Title').click()
+        self.selenium.find_element_by_link_text('Add another Title').click()
+        self.selenium.find_element_by_link_text('Add another Title').click()
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            '#title_set-group table tr.dynamic-title_set')), 5)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-0')), 1)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-1')), 1)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-2')), 1)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-3')), 1)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-4')), 1)
+
+        # Click on a few delete buttons
+        self.selenium.find_element_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-1 td.delete a').click()
+        self.selenium.find_element_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-2 td.delete a').click()
+        # Verify that they're gone and that the IDs have been re-sequenced
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            '#title_set-group table tr.dynamic-title_set')), 3)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-0')), 1)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-1')), 1)
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
+            'form#titlecollection_form tr.dynamic-title_set#title_set-2')), 1)
+
+class SeleniumInternetExplorerTests(SeleniumFirefoxTests):
+    webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
\ No newline at end of file
diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
index 37fa7bc..ecf3165 100644
--- a/tests/regressiontests/admin_widgets/tests.py
+++ b/tests/regressiontests/admin_widgets/tests.py
@@ -7,6 +7,7 @@ from django import forms
 from django.conf import settings
 from django.contrib import admin
 from django.contrib.admin import widgets
+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
 from django.core.files.storage import default_storage
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.db.models import DateField
@@ -407,3 +408,55 @@ class RelatedFieldWidgetWrapperTests(DjangoTestCase):
         # Used to fail with a name error.
         w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
         self.assertFalse(w.can_add_related)
+
+
+class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
+    fixtures = ['admin-widgets-users.xml']
+    urls = "regressiontests.admin_widgets.urls"
+
+    def test_show_hide_date_time_picker_widgets(self):
+        """
+        Ensure that pressing the ESC key closes the date and time picker
+        widgets.
+        Refs #17064.
+        """
+        from selenium.webdriver.common.keys import Keys
+
+        self.admin_login(username='super', password='secret', login_url='/')
+        # Open a page that has a date and time picker widgets
+        self.selenium.get('%s%s' % (self.live_server_url,
+            '/admin_widgets/member/add/'))
+
+        # First, with the date picker widget ---------------------------------
+        # Check that the date picker is hidden
+        self.assertEqual(
+            self.get_css_value('#calendarbox0', 'display'), 'none')
+        # Click the calendar icon
+        self.selenium.find_element_by_id('calendarlink0').click()
+        # Check that the date picker is visible
+        self.assertEqual(
+            self.get_css_value('#calendarbox0', 'display'), 'block')
+        # Press the ESC key
+        self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
+        # Check that the date picker is hidden again
+        self.assertEqual(
+            self.get_css_value('#calendarbox0', 'display'), 'none')
+
+        # Then, with the time picker widget ----------------------------------
+        # Check that the time picker is hidden
+        self.assertEqual(
+            self.get_css_value('#clockbox0', 'display'), 'none')
+        # Click the time icon
+        self.selenium.find_element_by_id('clocklink0').click()
+        # Check that the time picker is visible
+        self.assertEqual(
+            self.get_css_value('#clockbox0', 'display'), 'block')
+        # Press the ESC key
+        self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
+        # Check that the time picker is hidden again
+        self.assertEqual(
+            self.get_css_value('#clockbox0', 'display'), 'none')
+
+class SeleniumInternetExplorerTests(SeleniumFirefoxTests):
+    webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
\ No newline at end of file
diff --git a/tests/regressiontests/live_server/__init__.py b/tests/regressiontests/live_server/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/regressiontests/live_server/fixtures/testdata.json b/tests/regressiontests/live_server/fixtures/testdata.json
new file mode 100644
index 0000000..e987a04
--- /dev/null
+++ b/tests/regressiontests/live_server/fixtures/testdata.json
@@ -0,0 +1,16 @@
+[
+  {
+    "pk": 1,
+    "model": "live_server.person",
+    "fields": {
+      "name": "jane"
+    }
+  },
+  {
+    "pk": 2,
+    "model": "live_server.person",
+    "fields": {
+      "name": "robert"
+    }
+  }
+]
\ No newline at end of file
diff --git a/tests/regressiontests/live_server/media/example_media_file.txt b/tests/regressiontests/live_server/media/example_media_file.txt
new file mode 100644
index 0000000..dd2dda9
--- /dev/null
+++ b/tests/regressiontests/live_server/media/example_media_file.txt
@@ -0,0 +1 @@
+example media file
diff --git a/tests/regressiontests/live_server/models.py b/tests/regressiontests/live_server/models.py
new file mode 100644
index 0000000..6e1414a
--- /dev/null
+++ b/tests/regressiontests/live_server/models.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+
+class Person(models.Model):
+    name = models.CharField(max_length=256)
diff --git a/tests/regressiontests/live_server/static/example_file.txt b/tests/regressiontests/live_server/static/example_file.txt
new file mode 100644
index 0000000..5f1cfce
--- /dev/null
+++ b/tests/regressiontests/live_server/static/example_file.txt
@@ -0,0 +1 @@
+example file
diff --git a/tests/regressiontests/live_server/tests.py b/tests/regressiontests/live_server/tests.py
new file mode 100644
index 0000000..67acf5d
--- /dev/null
+++ b/tests/regressiontests/live_server/tests.py
@@ -0,0 +1,83 @@
+import os
+import urllib2
+
+from django.conf import settings
+from django.test import LiveServerTestCase
+from django.test.utils import override_settings
+
+from .models import Person
+
+
+TEST_ROOT = os.path.dirname(__file__)
+TEST_SETTINGS = {
+    'MEDIA_URL': '/media/',
+    'MEDIA_ROOT': os.path.join(TEST_ROOT, 'media'),
+    'STATIC_URL': '/static/',
+    'STATIC_ROOT': os.path.join(TEST_ROOT, 'static'),
+}
+
+
+class LiveServerTestBase(LiveServerTestCase):
+    urls = 'regressiontests.live_server.urls'
+    fixtures = ['testdata.json']
+
+    def urlopen(self, url):
+        base = 'http://%s:%s' % (settings.LIVE_TEST_SERVER_HOST,
+                                 settings.LIVE_TEST_SERVER_PORT)
+        return urllib2.urlopen(base + url)
+
+
+class TestViews(LiveServerTestBase):
+    def test_404(self):
+        """
+        Ensure that the LiveServerTestCase serves 404s.
+        """
+        try:
+            self.urlopen('/')
+        except urllib2.HTTPError, err:
+            self.assertEquals(err.code, 404, 'Expected 404 response')
+        else:
+            self.fail('Expected 404 response')
+
+    def test_view(self):
+        """
+        Ensure that the LiveServerTestCase serves views.
+        """
+        f = self.urlopen('/example_view/')
+        self.assertEquals(f.read(), 'example view')
+
+    def test_static_file(self):
+        """
+        Ensure that the LiveServerTestCase serves static files.
+        """
+        f = self.urlopen('/static/example_file.txt')
+        self.assertEquals(f.read(), 'example file\n')
+
+    def test_media_files(self):
+        """
+        Ensure that the LiveServerTestCase serves media files.
+        """
+        f = self.urlopen('/media/example_media_file.txt')
+        self.assertEquals(f.read(), 'example media file\n')
+
+TestViews = override_settings(**TEST_SETTINGS)(TestViews)
+
+
+class TestDatabase(LiveServerTestBase):
+
+    def test_fixtures_loaded(self):
+        """
+        Ensure that fixtures are properly loaded and visible to the
+        live server thread.
+        """
+        import pdb; pdb.set_trace()
+        f = self.urlopen('/model_view/')
+        self.assertEquals(f.read().splitlines(), ['jane', 'robert'])
+
+    def test_database_writes(self):
+        """
+        Ensure that data written to the database by a view can be read.
+        """
+        self.urlopen('/create_model_instance/')
+        names = [person.name for person in Person.objects.all()]
+        self.assertEquals(names, ['jane', 'robert', 'emily'])
diff --git a/tests/regressiontests/live_server/urls.py b/tests/regressiontests/live_server/urls.py
new file mode 100644
index 0000000..83f757f
--- /dev/null
+++ b/tests/regressiontests/live_server/urls.py
@@ -0,0 +1,12 @@
+from __future__ import absolute_import
+
+from django.conf.urls import patterns, url
+
+from . import views
+
+
+urlpatterns = patterns('',
+    url(r'^example_view/$', views.example_view),
+    url(r'^model_view/$', views.model_view),
+    url(r'^create_model_instance/$', views.create_model_instance),
+)
diff --git a/tests/regressiontests/live_server/views.py b/tests/regressiontests/live_server/views.py
new file mode 100644
index 0000000..bfc7513
--- /dev/null
+++ b/tests/regressiontests/live_server/views.py
@@ -0,0 +1,18 @@
+from django.http import HttpResponse
+from .models import Person
+
+
+def example_view(request):
+    return HttpResponse('example view')
+
+
+def model_view(request):
+    people = Person.objects.all()
+    import pdb; pdb.set_trace()
+    return HttpResponse('\n'.join([person.name for person in people]))
+
+
+def create_model_instance(request):
+    person = Person(name='emily')
+    person.save()
+    return HttpResponse('')
