Code

Ticket #2879: 2879.selenium-support.9.diff

File 2879.selenium-support.9.diff, 42.8 KB (added by julien, 2 years ago)
Line 
1diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
2index 0aee63d..da9d030 100644
3--- a/django/conf/global_settings.py
4+++ b/django/conf/global_settings.py
5@@ -565,6 +565,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/contrib/admin/tests.py b/django/contrib/admin/tests.py
17new file mode 100644
18index 0000000..bd93899
19--- /dev/null
20+++ b/django/contrib/admin/tests.py
21@@ -0,0 +1,48 @@
22+import sys
23+
24+from django.test import LiveServerTestCase
25+from django.utils.importlib import import_module
26+from django.utils.unittest import SkipTest
27+
28+class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
29+
30+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
31+
32+    def setUp(self):
33+        if sys.version_info < (2, 6):
34+            raise SkipTest('Selenium Webdriver does not support Python < 2.6.')
35+        try:
36+            # Import and start the WebDriver class.
37+            module, attr = self.webdriver_class.rsplit('.', 1)
38+            mod = import_module(module)
39+            WebDriver = getattr(mod, attr)
40+            self.selenium = WebDriver()
41+        except Exception:
42+            raise SkipTest('Selenium webdriver "%s" not installed or not '
43+                           'operational.' % self.webdriver_class)
44+        super(AdminSeleniumWebDriverTestCase, self).setUp()
45+
46+    def tearDown(self):
47+        super(AdminSeleniumWebDriverTestCase, self).tearDown()
48+        if hasattr(self, 'selenium'):
49+            self.selenium.quit()
50+
51+    def admin_login(self, username, password, login_url='/admin/'):
52+        """
53+        Helper function to log into the admin.
54+        """
55+        self.selenium.get('%s%s' % (self.live_server_url, login_url))
56+        username_input = self.selenium.find_element_by_name("username")
57+        username_input.send_keys(username)
58+        password_input = self.selenium.find_element_by_name("password")
59+        password_input.send_keys(password)
60+        self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
61+
62+    def get_css_value(self, selector, attribute):
63+        """
64+        Helper function that returns the value for the CSS attribute of an
65+        DOM element specified by the given selector. Uses the jQuery that ships
66+        with Django.
67+        """
68+        return self.selenium.execute_script(
69+            'return django.jQuery("%s").css("%s")' % (selector, attribute))
70\ No newline at end of file
71diff --git a/django/db/__init__.py b/django/db/__init__.py
72index 8395468..e3fcbbd 100644
73--- a/django/db/__init__.py
74+++ b/django/db/__init__.py
75@@ -1,3 +1,4 @@
76+from threading import local
77 from django.conf import settings
78 from django.core import signals
79 from django.core.exceptions import ImproperlyConfigured
80@@ -24,7 +25,15 @@ router = ConnectionRouter(settings.DATABASE_ROUTERS)
81 # by the PostgreSQL backends.
82 # we load all these up for backwards compatibility, you should use
83 # connections['default'] instead.
84-connection = connections[DEFAULT_DB_ALIAS]
85+class DefaultConnectionProxy(local):
86+    """
87+    Thread-local proxy for the default connection.
88+    """
89+    def __getattr__(self, item):
90+        return getattr(connections[DEFAULT_DB_ALIAS], item)
91+    def __setattr__(self, name, value):
92+        return setattr(connections[DEFAULT_DB_ALIAS], name, value)
93+connection = DefaultConnectionProxy()
94 backend = load_backend(connection.settings_dict['ENGINE'])
95 
96 # Register an event that closes the database connection
97diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
98index f2bde84..0f4fc31 100644
99--- a/django/db/backends/__init__.py
100+++ b/django/db/backends/__init__.py
101@@ -2,7 +2,6 @@ try:
102     import thread
103 except ImportError:
104     import dummy_thread as thread
105-from threading import local
106 from contextlib import contextmanager
107 
108 from django.conf import settings
109@@ -13,7 +12,7 @@ from django.utils.importlib import import_module
110 from django.utils.timezone import is_aware
111 
112 
113-class BaseDatabaseWrapper(local):
114+class BaseDatabaseWrapper(object):
115     """
116     Represents a database connection.
117     """
118diff --git a/django/db/utils.py b/django/db/utils.py
119index f0c13e3..41ad6df 100644
120--- a/django/db/utils.py
121+++ b/django/db/utils.py
122@@ -1,4 +1,5 @@
123 import os
124+from threading import local
125 
126 from django.conf import settings
127 from django.core.exceptions import ImproperlyConfigured
128@@ -50,7 +51,7 @@ class ConnectionDoesNotExist(Exception):
129 class ConnectionHandler(object):
130     def __init__(self, databases):
131         self.databases = databases
132-        self._connections = {}
133+        self._connections = local()
134 
135     def ensure_defaults(self, alias):
136         """
137@@ -73,16 +74,19 @@ class ConnectionHandler(object):
138             conn.setdefault(setting, None)
139 
140     def __getitem__(self, alias):
141-        if alias in self._connections:
142-            return self._connections[alias]
143+        if hasattr(self._connections, alias):
144+            return getattr(self._connections, alias)
145 
146         self.ensure_defaults(alias)
147         db = self.databases[alias]
148         backend = load_backend(db['ENGINE'])
149         conn = backend.DatabaseWrapper(db, alias)
150-        self._connections[alias] = conn
151+        setattr(self._connections, alias, conn)
152         return conn
153 
154+    def __setitem__(self, key, value):
155+        setattr(self._connections, key, value)
156+
157     def __iter__(self):
158         return iter(self.databases)
159 
160diff --git a/django/test/__init__.py b/django/test/__init__.py
161index a3a03e3..21a4841 100644
162--- a/django/test/__init__.py
163+++ b/django/test/__init__.py
164@@ -4,5 +4,6 @@ Django Unit Test and Doctest framework.
165 
166 from django.test.client import Client, RequestFactory
167 from django.test.testcases import (TestCase, TransactionTestCase,
168-        SimpleTestCase, skipIfDBFeature, skipUnlessDBFeature)
169+    SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
170+    skipUnlessDBFeature)
171 from django.test.utils import Approximate
172diff --git a/django/test/testcases.py b/django/test/testcases.py
173index ee22ac2..c2344d4 100644
174--- a/django/test/testcases.py
175+++ b/django/test/testcases.py
176@@ -5,12 +5,19 @@ import sys
177 from functools import wraps
178 from urlparse import urlsplit, urlunsplit
179 from xml.dom.minidom import parseString, Node
180+import select
181+import socket
182+import threading
183 
184 from django.conf import settings
185+from django.contrib.staticfiles.handlers import StaticFilesHandler
186 from django.core import mail
187 from django.core.exceptions import ValidationError
188+from django.core.handlers.wsgi import WSGIHandler
189 from django.core.management import call_command
190 from django.core.signals import request_started
191+from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
192+    WSGIServerException)
193 from django.core.urlresolvers import clear_url_caches
194 from django.core.validators import EMPTY_VALUES
195 from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
196@@ -23,6 +30,7 @@ from django.test.utils import (get_warnings_state, restore_warnings_state,
197     override_settings)
198 from django.utils import simplejson, unittest as ut2
199 from django.utils.encoding import smart_str
200+from django.views.static import serve
201 
202 __all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
203            'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature')
204@@ -732,3 +740,175 @@ def skipUnlessDBFeature(feature):
205     """
206     return _deferredSkip(lambda: not getattr(connection.features, feature),
207                          "Database doesn't support feature %s" % feature)
208+
209+class QuietWSGIRequestHandler(WSGIRequestHandler):
210+    """
211+    Just a regular WSGIRequestHandler except it doesn't log to the standard
212+    output any of the requests received, so as to not clutter the output for
213+    the tests' results.
214+    """
215+    def log_message(*args):
216+        pass
217+
218+class StoppableWSGIServer(WSGIServer):
219+    """
220+    The code in this class is borrowed from the `SocketServer.BaseServer` class
221+    in Python 2.6. The important functionality here is that the server is non-
222+    blocking and that it can be shut down at any moment. This is made possible
223+    by the server regularly polling the socket and checking if it has been
224+    asked to stop.
225+    Note for the future: Once Django stops supporting Python 2.5, this class
226+    can be removed as `WSGIServer` will have this ability to shutdown on
227+    demand.
228+    """
229+
230+    def __init__(self, *args, **kwargs):
231+        super(StoppableWSGIServer, self).__init__(*args, **kwargs)
232+        self.__is_shut_down = threading.Event()
233+        self.__serving = False
234+
235+    def serve_forever(self, poll_interval=0.5):
236+        """Handle one request at a time until shutdown.
237+
238+        Polls for shutdown every poll_interval seconds.
239+        """
240+        self.__serving = True
241+        self.__is_shut_down.clear()
242+        while self.__serving:
243+            r, w, e = select.select([self], [], [], poll_interval)
244+            if r:
245+                self._handle_request_noblock()
246+        self.__is_shut_down.set()
247+
248+    def shutdown(self):
249+        """Stops the serve_forever loop.
250+
251+        Blocks until the loop has finished. This must be called while
252+        serve_forever() is running in another thread, or it will
253+        deadlock.
254+        """
255+        self.__serving = False
256+        self.__is_shut_down.wait()
257+
258+    def handle_request(self):
259+        """Handle one request, possibly blocking.
260+        """
261+        fd_sets = select.select([self], [], [], None)
262+        if not fd_sets[0]:
263+            return
264+        self._handle_request_noblock()
265+
266+    def _handle_request_noblock(self):
267+        """Handle one request, without blocking.
268+
269+        I assume that select.select has returned that the socket is
270+        readable before this function was called, so there should be
271+        no risk of blocking in get_request().
272+        """
273+        try:
274+            request, client_address = self.get_request()
275+        except socket.error:
276+            return
277+        if self.verify_request(request, client_address):
278+            try:
279+                self.process_request(request, client_address)
280+            except Exception:
281+                self.handle_error(request, client_address)
282+                self.close_request(request)
283+
284+class MediaFilesHandler(StaticFilesHandler):
285+    """
286+    Handler for serving the media files.
287+    """
288+
289+    def get_base_dir(self):
290+        return settings.MEDIA_ROOT
291+
292+    def get_base_url(self):
293+        return settings.MEDIA_URL
294+
295+    def serve(self, request):
296+        return serve(request, self.file_path(request.path),
297+            document_root=self.get_base_dir())
298+
299+class LiveServerThread(threading.Thread):
300+    """
301+    Thread for running a live http server while the tests are running.
302+    """
303+
304+    def __init__(self, address, port, connections_override=None):
305+        self.address = address
306+        self.port = port
307+        self.is_ready = threading.Event()
308+        self.error = None
309+        self.connections_override = connections_override
310+        super(LiveServerThread, self).__init__()
311+
312+    def run(self):
313+        """
314+        Sets up live server and database and loops over handling http requests.
315+        """
316+        if self.connections_override:
317+            from django.db import connections
318+            for alias, conn in self.connections_override.items():
319+                connections[alias] = conn
320+        try:
321+            # Instantiate and start the server
322+            self.httpd = StoppableWSGIServer(
323+                (self.address, self.port), QuietWSGIRequestHandler)
324+            handler = StaticFilesHandler(MediaFilesHandler(WSGIHandler()))
325+            self.httpd.set_app(handler)
326+            self.is_ready.set()
327+            self.httpd.serve_forever()
328+        except WSGIServerException, e:
329+            self.error = e
330+            self.is_ready.set()
331+
332+    def join(self, timeout=None):
333+        self.httpd.shutdown()
334+        self.httpd.server_close()
335+        super(LiveServerThread, self).join(timeout)
336+
337+class LiveServerTestCase(TransactionTestCase):
338+    """
339+    Does basically the same as TransactionTestCase but also launches a live
340+    http server in a separate thread so that the tests may use another testing
341+    framework, such as Selenium for example, instead of the built-in dummy
342+    client.
343+    Note that it inherits from TransactionTestCase instead of TestCase because
344+    the threads do not share the same connection's cursor (unless if using
345+    in-memory sqlite).
346+    """
347+
348+    @property
349+    def live_server_url(self):
350+        return 'http://%s:%s' % (settings.LIVE_TEST_SERVER_HOST,
351+                                 settings.LIVE_TEST_SERVER_PORT)
352+
353+    @classmethod
354+    def setUpClass(cls):
355+        connections_override = {}
356+        for conn in connections.all():
357+            if (conn.settings_dict['ENGINE'] == 'django.db.backends.sqlite3'
358+                and conn.settings_dict['NAME'] == ':memory:'):
359+                connections_override[conn.alias] = conn
360+
361+        # Launch the Django live server's thread
362+        cls.server_thread = LiveServerThread(
363+            settings.LIVE_TEST_SERVER_HOST,
364+            int(settings.LIVE_TEST_SERVER_PORT),
365+            connections_override)
366+        cls.server_thread.start()
367+
368+        # Wait for the Django server to be ready
369+        cls.server_thread.is_ready.wait()
370+        if cls.server_thread.error:
371+            raise cls.server_thread.error
372+
373+        super(LiveServerTestCase, cls).setUpClass()
374+
375+    @classmethod
376+    def tearDownClass(cls):
377+        # Terminate the Django server's thread
378+        cls.server_thread.join()
379+        super(LiveServerTestCase, cls).tearDownClass()
380diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt
381index 5ec09fe..baa20a1 100644
382--- a/docs/internals/contributing/writing-code/unit-tests.txt
383+++ b/docs/internals/contributing/writing-code/unit-tests.txt
384@@ -122,6 +122,19 @@ Going beyond that, you can specify an individual test method like this:
385 
386     ./runtests.py --settings=path.to.settings i18n.TranslationTests.test_lazy_objects
387 
388+Running the Selenium tests
389+~~~~~~~~~~~~~~~~~~~~~~~~~~
390+
391+Some admin tests require Selenium 2 to work via a real Web browser. To allow
392+those tests to run and not be skipped, you must install the selenium_ package
393+(version > 2.13) into your Python path.
394+
395+Then, run the tests normally, for example:
396+
397+.. code-block:: bash
398+
399+    ./runtests.py --settings=test_sqlite admin_inlines
400+
401 Running all the tests
402 ~~~~~~~~~~~~~~~~~~~~~
403 
404@@ -135,6 +148,7 @@ dependencies:
405 *  setuptools_
406 *  memcached_, plus a :ref:`supported Python binding <memcached>`
407 *  gettext_ (:ref:`gettext_on_windows`)
408+*  selenium_
409 
410 If you want to test the memcached cache backend, you'll also need to define
411 a :setting:`CACHES` setting that points at your memcached instance.
412@@ -149,6 +163,7 @@ associated tests will be skipped.
413 .. _setuptools: http://pypi.python.org/pypi/setuptools/
414 .. _memcached: http://www.danga.com/memcached/
415 .. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html
416+.. _selenium: http://pypi.python.org/pypi/selenium
417 
418 .. _contrib-apps:
419 
420diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
421index a35d99a..89f2c47 100644
422--- a/docs/ref/settings.txt
423+++ b/docs/ref/settings.txt
424@@ -195,6 +195,30 @@ all cache keys used by the Django server.
425 
426 See the :ref:`cache documentation <cache_key_prefixing>` for more information.
427 
428+.. setting:: LIVE_TEST_SERVER_HOST
429+
430+LIVE_TEST_SERVER_HOST
431+~~~~~~~~~~~~~~~~~~~~~
432+
433+Default: ``'localhost'``
434+
435+Controls the host address at which the live test server gets started when using
436+a :class:`~django.test.LiveServerTestCase`.
437+
438+See also: :setting:`LIVE_TEST_SERVER_PORT`
439+
440+.. setting:: LIVE_TEST_SERVER_PORT
441+
442+LIVE_TEST_SERVER_PORT
443+~~~~~~~~~~~~~~~~~~~~~
444+
445+Default: ``8081``
446+
447+Controls the port at which the live test server gets started when using
448+a :class:`~django.test.LiveServerTestCase`.
449+
450+See also: :setting:`LIVE_TEST_SERVER_HOST`
451+
452 .. setting:: CACHES-LOCATION
453 
454 LOCATION
455diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
456index 181b4ff..f5044b3 100644
457--- a/docs/topics/testing.txt
458+++ b/docs/topics/testing.txt
459@@ -580,21 +580,20 @@ Some of the things you can do with the test client are:
460 * Test that a given request is rendered by a given Django template, with
461   a template context that contains certain values.
462 
463-Note that the test client is not intended to be a replacement for Twill_,
464+Note that the test client is not intended to be a replacement for Windmill_,
465 Selenium_, or other "in-browser" frameworks. Django's test client has
466 a different focus. In short:
467 
468 * Use Django's test client to establish that the correct view is being
469   called and that the view is collecting the correct context data.
470 
471-* Use in-browser frameworks such as Twill and Selenium to test *rendered*
472-  HTML and the *behavior* of Web pages, namely JavaScript functionality.
473+* Use in-browser frameworks such as Windmill_ and Selenium_ to test *rendered*
474+  HTML and the *behavior* of Web pages, namely JavaScript functionality. Django
475+  also provides special support for those frameworks; see the section on
476+  :class:`~django.test.LiveServerTestCase` for more details.
477 
478 A comprehensive test suite should use a combination of both test types.
479 
480-.. _Twill: http://twill.idyll.org/
481-.. _Selenium: http://seleniumhq.org/
482-
483 Overview and a quick example
484 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
485 
486@@ -1830,11 +1829,104 @@ set up, execute and tear down the test suite.
487     those options will be added to the list of command-line options that
488     the :djadmin:`test` command can use.
489 
490+Live test server
491+----------------
492+
493+.. versionadded:: 1.4
494+
495+.. currentmodule:: django.test
496+
497+.. class:: LiveServerTestCase()
498+
499+``LiveServerTestCase`` does basically the same as
500+:class:`~django.test.TransactionTestCase` with one extra thing: it launches a
501+live Django server in the background on setup, and shuts it down on teardown.
502+This allows to use other automated test clients than the
503+:ref:`Django dummy client <test-client>` such as, for example, the Selenium_ or
504+Windmill_ clients, to execute a series of functional tests inside a browser and
505+simulate a real user's actions.
506+
507+You may control which host and port the live server will run at with
508+respectively the :setting:`LIVE_TEST_SERVER_HOST` and
509+:setting:`LIVE_TEST_SERVER_PORT` settings. The full server url can then be
510+accessed during the tests with ``self.live_server_url``.
511+
512+To demonstrate how to use ``LiveServerTestCase``, let's write a simple Selenium
513+test. First of all, you need to install the `selenium package`_ into your
514+Python path:
515+
516+.. code-block:: bash
517+
518+   pip install selenium
519+
520+Then, add the following code to one of your app's tests module (for example:
521+``myapp/tests.py``):
522+
523+.. code-block:: python
524+
525+    from django.test import LiveServerTestCase
526+    from selenium.webdriver.firefox.webdriver import WebDriver
527+
528+    class MySeleniumTests(LiveServerTestCase):
529+        fixtures = ['user-data.json']
530+
531+        def setUp(self):
532+            self.selenium = WebDriver()
533+            super(MySeleniumTests, self).setUp()
534+
535+        def tearDown(self):
536+            super(MySeleniumTests, self).tearDown()
537+            self.selenium.quit()
538+
539+        def test_login(self):
540+            self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
541+            username_input = self.selenium.find_element_by_name("username")
542+            username_input.send_keys('myuser')
543+            password_input = self.selenium.find_element_by_name("password")
544+            password_input.send_keys('secret')
545+            self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
546+
547+Finally, you may run the test as follows:
548+
549+.. code-block:: bash
550+
551+    ./manage.py test myapp.MySeleniumTests.test_login
552+
553+This example will automatically open Firefox then go to the login page, enter
554+the credentials and press the "Log in" button. Selenium offers other drivers in
555+case you do not have Firefox installed or wish to use another browser. The
556+example above is just a tiny fraction of what the Selenium client can do; check
557+out the `full reference`_ for more details.
558+
559+.. _Windmill: http://www.getwindmill.com/
560+.. _Selenium: http://seleniumhq.org/
561+.. _selenium package: http://pypi.python.org/pypi/selenium
562+.. _full reference: http://readthedocs.org/docs/selenium-python/en/latest/api.html
563+.. _Firefox: http://www.mozilla.com/firefox/
564+
565+.. note::
566+
567+    When running tests with an in-memory SQLite database, the same database
568+    connection needs to be shared between the thread in which the tested code
569+    is run and the thread in which the test case is run. By default, pysqlite
570+    forbids connections being shared between multiple threads. To allow this
571+    behavior you need to set the ``check_same_thread`` option in the
572+    :setting:`DATABASES` setting to ``False``.
573+
574+    .. code-block:: python
575+
576+        DATABASES = {
577+            'default': {
578+                'ENGINE': 'django.db.backends.sqlite3',
579+                'OPTIONS': {
580+                    'check_same_thread': False,
581+                }
582+            }
583+        }
584 
585 Attributes
586 ~~~~~~~~~~
587 
588-
589 .. attribute:: DjangoTestSuiteRunner.option_list
590 
591     .. versionadded:: 1.4
592diff --git a/tests/regressiontests/admin_inlines/admin.py b/tests/regressiontests/admin_inlines/admin.py
593index 4edd361..508f302 100644
594--- a/tests/regressiontests/admin_inlines/admin.py
595+++ b/tests/regressiontests/admin_inlines/admin.py
596@@ -109,6 +109,10 @@ class SottoCapoInline(admin.TabularInline):
597     model = SottoCapo
598 
599 
600+class ProfileInline(admin.TabularInline):
601+    model = Profile
602+    extra = 1
603+
604 site.register(TitleCollection, inlines=[TitleInline])
605 # Test bug #12561 and #12778
606 # only ModelAdmin media
607@@ -124,3 +128,4 @@ site.register(Fashionista, inlines=[InlineWeakness])
608 site.register(Holder4, Holder4Admin)
609 site.register(Author, AuthorAdmin)
610 site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline])
611+site.register(ProfileCollection, inlines=[ProfileInline])
612\ No newline at end of file
613diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py
614index 748280d..f2add00 100644
615--- a/tests/regressiontests/admin_inlines/models.py
616+++ b/tests/regressiontests/admin_inlines/models.py
617@@ -136,3 +136,13 @@ class Consigliere(models.Model):
618 class SottoCapo(models.Model):
619     name = models.CharField(max_length=100)
620     capo_famiglia = models.ForeignKey(CapoFamiglia, related_name='+')
621+
622+# Other models
623+
624+class ProfileCollection(models.Model):
625+    pass
626+
627+class Profile(models.Model):
628+    collection = models.ForeignKey(ProfileCollection, blank=True, null=True)
629+    first_name = models.CharField(max_length=100)
630+    last_name = models.CharField(max_length=100)
631\ No newline at end of file
632diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py
633index c2e3bbc..7b417d8 100644
634--- a/tests/regressiontests/admin_inlines/tests.py
635+++ b/tests/regressiontests/admin_inlines/tests.py
636@@ -1,5 +1,6 @@
637 from __future__ import absolute_import
638 
639+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
640 from django.contrib.admin.helpers import InlineAdminForm
641 from django.contrib.auth.models import User, Permission
642 from django.contrib.contenttypes.models import ContentType
643@@ -8,7 +9,8 @@ from django.test import TestCase
644 # local test models
645 from .admin import InnerInline
646 from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
647-    OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book)
648+    OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
649+    ProfileCollection)
650 
651 
652 class TestInline(TestCase):
653@@ -380,3 +382,105 @@ class TestInlinePermissions(TestCase):
654         self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"')
655         self.assertContains(response, '<input type="hidden" name="inner2_set-0-id" value="%i"' % self.inner2_id)
656         self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
657+
658+class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
659+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
660+    fixtures = ['admin-views-users.xml']
661+    urls = "regressiontests.admin_inlines.urls"
662+
663+    def test_add_inlines(self):
664+        """
665+        Ensure that the "Add another XXX" link correctly adds items to the
666+        inline form.
667+        """
668+        self.admin_login(username='super', password='secret')
669+        self.selenium.get('%s%s' % (self.live_server_url,
670+            '/admin/admin_inlines/profilecollection/add/'))
671+
672+        # Check that there's only one inline to start with and that it has the
673+        # correct ID.
674+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
675+            '#profile_set-group table tr.dynamic-profile_set')), 1)
676+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
677+            '.dynamic-profile_set:nth-of-type(1)').get_attribute('id'),
678+            'profile_set-0')
679+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
680+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-0 input[name=profile_set-0-first_name]')), 1)
681+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
682+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-0 input[name=profile_set-0-last_name]')), 1)
683+
684+        # Add an inline
685+        self.selenium.find_element_by_link_text('Add another Profile').click()
686+
687+        # Check that the inline has been added, that it has the right id, and
688+        # that it contains the right fields.
689+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
690+            '#profile_set-group table tr.dynamic-profile_set')), 2)
691+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
692+            '.dynamic-profile_set:nth-of-type(2)').get_attribute('id'), 'profile_set-1')
693+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
694+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 input[name=profile_set-1-first_name]')), 1)
695+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
696+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 input[name=profile_set-1-last_name]')), 1)
697+
698+        # Let's add another one to be sure
699+        self.selenium.find_element_by_link_text('Add another Profile').click()
700+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
701+            '#profile_set-group table tr.dynamic-profile_set')), 3)
702+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
703+            '.dynamic-profile_set:nth-of-type(3)').get_attribute('id'), 'profile_set-2')
704+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
705+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 input[name=profile_set-2-first_name]')), 1)
706+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
707+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 input[name=profile_set-2-last_name]')), 1)
708+
709+        # Enter some data and click 'Save'
710+        self.selenium.find_element_by_name('profile_set-0-first_name').send_keys('0 first name 1')
711+        self.selenium.find_element_by_name('profile_set-0-last_name').send_keys('0 last name 2')
712+        self.selenium.find_element_by_name('profile_set-1-first_name').send_keys('1 first name 1')
713+        self.selenium.find_element_by_name('profile_set-1-last_name').send_keys('1 last name 2')
714+        self.selenium.find_element_by_name('profile_set-2-first_name').send_keys('2 first name 1')
715+        self.selenium.find_element_by_name('profile_set-2-last_name').send_keys('2 last name 2')
716+        self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
717+
718+        # Check that the objects have been created in the database
719+        self.assertEqual(ProfileCollection.objects.all().count(), 1)
720+        self.assertEqual(Profile.objects.all().count(), 3)
721+
722+    def test_delete_inlines(self):
723+        self.admin_login(username='super', password='secret')
724+        self.selenium.get('%s%s' % (self.live_server_url,
725+            '/admin/admin_inlines/profilecollection/add/'))
726+
727+        # Add a few inlines
728+        self.selenium.find_element_by_link_text('Add another Profile').click()
729+        self.selenium.find_element_by_link_text('Add another Profile').click()
730+        self.selenium.find_element_by_link_text('Add another Profile').click()
731+        self.selenium.find_element_by_link_text('Add another Profile').click()
732+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
733+            '#profile_set-group table tr.dynamic-profile_set')), 5)
734+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
735+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1)
736+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
737+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)
738+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
739+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1)
740+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
741+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-3')), 1)
742+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
743+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-4')), 1)
744+
745+        # Click on a few delete buttons
746+        self.selenium.find_element_by_css_selector(
747+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 td.delete a').click()
748+        self.selenium.find_element_by_css_selector(
749+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 td.delete a').click()
750+        # Verify that they're gone and that the IDs have been re-sequenced
751+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
752+            '#profile_set-group table tr.dynamic-profile_set')), 3)
753+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
754+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1)
755+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
756+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)
757+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
758+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1)
759diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
760index 37fa7bc..e28df32 100644
761--- a/tests/regressiontests/admin_widgets/tests.py
762+++ b/tests/regressiontests/admin_widgets/tests.py
763@@ -7,6 +7,7 @@ from django import forms
764 from django.conf import settings
765 from django.contrib import admin
766 from django.contrib.admin import widgets
767+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
768 from django.core.files.storage import default_storage
769 from django.core.files.uploadedfile import SimpleUploadedFile
770 from django.db.models import DateField
771@@ -407,3 +408,52 @@ class RelatedFieldWidgetWrapperTests(DjangoTestCase):
772         # Used to fail with a name error.
773         w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
774         self.assertFalse(w.can_add_related)
775+
776+
777+class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
778+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
779+    fixtures = ['admin-widgets-users.xml']
780+    urls = "regressiontests.admin_widgets.urls"
781+
782+    def test_show_hide_date_time_picker_widgets(self):
783+        """
784+        Ensure that pressing the ESC key closes the date and time picker
785+        widgets.
786+        Refs #17064.
787+        """
788+        from selenium.webdriver.common.keys import Keys
789+
790+        self.admin_login(username='super', password='secret', login_url='/')
791+        # Open a page that has a date and time picker widgets
792+        self.selenium.get('%s%s' % (self.live_server_url,
793+            '/admin_widgets/member/add/'))
794+
795+        # First, with the date picker widget ---------------------------------
796+        # Check that the date picker is hidden
797+        self.assertEqual(
798+            self.get_css_value('#calendarbox0', 'display'), 'none')
799+        # Click the calendar icon
800+        self.selenium.find_element_by_id('calendarlink0').click()
801+        # Check that the date picker is visible
802+        self.assertEqual(
803+            self.get_css_value('#calendarbox0', 'display'), 'block')
804+        # Press the ESC key
805+        self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
806+        # Check that the date picker is hidden again
807+        self.assertEqual(
808+            self.get_css_value('#calendarbox0', 'display'), 'none')
809+
810+        # Then, with the time picker widget ----------------------------------
811+        # Check that the time picker is hidden
812+        self.assertEqual(
813+            self.get_css_value('#clockbox0', 'display'), 'none')
814+        # Click the time icon
815+        self.selenium.find_element_by_id('clocklink0').click()
816+        # Check that the time picker is visible
817+        self.assertEqual(
818+            self.get_css_value('#clockbox0', 'display'), 'block')
819+        # Press the ESC key
820+        self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
821+        # Check that the time picker is hidden again
822+        self.assertEqual(
823+            self.get_css_value('#clockbox0', 'display'), 'none')
824diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py
825index 936f010..be83a9a 100644
826--- a/tests/regressiontests/backends/tests.py
827+++ b/tests/regressiontests/backends/tests.py
828@@ -3,6 +3,7 @@
829 from __future__ import with_statement, absolute_import
830 
831 import datetime
832+import threading
833 
834 from django.conf import settings
835 from django.core.management.color import no_style
836@@ -446,3 +447,42 @@ class FkConstraintsTests(TransactionTestCase):
837                         connection.check_constraints()
838             finally:
839                 transaction.rollback()
840+
841+class ThreadTests(TestCase):
842+
843+    def test_default_connection_thread_local(self):
844+        """
845+        Ensure that the default connection (i.e. django.db.connection) is
846+        different for each thread.
847+        Refs #17258.
848+        """
849+        connections_set = set()
850+        connection.cursor()
851+        connections_set.add(connection.connection)
852+        def runner():
853+            from django.db import connection
854+            connection.cursor()
855+            connections_set.add(connection.connection)
856+        for x in xrange(2):
857+            t = threading.Thread(target=runner)
858+            t.start()
859+            t.join()
860+        self.assertEquals(len(connections_set), 3)
861+
862+    def test_connections_thread_local(self):
863+        """
864+        Ensure that the connections are different for each thread.
865+        Refs #17258.
866+        """
867+        connections_set = set()
868+        for conn in connections.all():
869+            connections_set.add(conn)
870+        def runner():
871+            from django.db import connections
872+            for conn in connections.all():
873+                connections_set.add(conn)
874+        for x in xrange(2):
875+            t = threading.Thread(target=runner)
876+            t.start()
877+            t.join()
878+        self.assertEquals(len(connections_set), 6)
879\ No newline at end of file
880diff --git a/tests/regressiontests/servers/fixtures/testdata.json b/tests/regressiontests/servers/fixtures/testdata.json
881new file mode 100644
882index 0000000..d81b225
883--- /dev/null
884+++ b/tests/regressiontests/servers/fixtures/testdata.json
885@@ -0,0 +1,16 @@
886+[
887+  {
888+    "pk": 1,
889+    "model": "servers.person",
890+    "fields": {
891+      "name": "jane"
892+    }
893+  },
894+  {
895+    "pk": 2,
896+    "model": "servers.person",
897+    "fields": {
898+      "name": "robert"
899+    }
900+  }
901+]
902\ No newline at end of file
903diff --git a/tests/regressiontests/servers/media/example_media_file.txt b/tests/regressiontests/servers/media/example_media_file.txt
904new file mode 100644
905index 0000000..dd2dda9
906--- /dev/null
907+++ b/tests/regressiontests/servers/media/example_media_file.txt
908@@ -0,0 +1 @@
909+example media file
910diff --git a/tests/regressiontests/servers/models.py b/tests/regressiontests/servers/models.py
911index e69de29..6e1414a 100644
912--- a/tests/regressiontests/servers/models.py
913+++ b/tests/regressiontests/servers/models.py
914@@ -0,0 +1,5 @@
915+from django.db import models
916+
917+
918+class Person(models.Model):
919+    name = models.CharField(max_length=256)
920diff --git a/tests/regressiontests/servers/static/example_file.txt b/tests/regressiontests/servers/static/example_file.txt
921new file mode 100644
922index 0000000..5f1cfce
923--- /dev/null
924+++ b/tests/regressiontests/servers/static/example_file.txt
925@@ -0,0 +1 @@
926+example file
927diff --git a/tests/regressiontests/servers/tests.py b/tests/regressiontests/servers/tests.py
928index b9d24ba..04450a9 100644
929--- a/tests/regressiontests/servers/tests.py
930+++ b/tests/regressiontests/servers/tests.py
931@@ -3,13 +3,16 @@ Tests for django.core.servers.
932 """
933 import os
934 from urlparse import urljoin
935+import urllib2
936 
937 import django
938 from django.conf import settings
939-from django.test import TestCase
940+from django.test import TestCase, LiveServerTestCase
941 from django.core.handlers.wsgi import WSGIHandler
942 from django.core.servers.basehttp import AdminMediaHandler
943+from django.test.utils import override_settings
944 
945+from .models import Person
946 
947 class AdminMediaHandlerTests(TestCase):
948 
949@@ -68,3 +71,86 @@ class AdminMediaHandlerTests(TestCase):
950                 continue
951             self.fail('URL: %s should have caused a ValueError exception.'
952                       % url)
953+
954+
955+TEST_ROOT = os.path.dirname(__file__)
956+TEST_SETTINGS = {
957+    'MEDIA_URL': '/media/',
958+    'MEDIA_ROOT': os.path.join(TEST_ROOT, 'media'),
959+    'STATIC_URL': '/static/',
960+    'STATIC_ROOT': os.path.join(TEST_ROOT, 'static'),
961+}
962+
963+
964+class LiveServerBase(LiveServerTestCase):
965+    urls = 'regressiontests.servers.urls'
966+    fixtures = ['testdata.json']
967+
968+    @classmethod
969+    def setUpClass(cls):
970+        cls.settings_override = override_settings(**TEST_SETTINGS)
971+        cls.settings_override.enable()
972+        super(LiveServerBase, cls).setUpClass()
973+
974+    @classmethod
975+    def tearDownClass(cls):
976+        cls.settings_override.disable()
977+        super(LiveServerBase, cls).tearDownClass()
978+
979+    def urlopen(self, url):
980+        base = 'http://%s:%s' % (settings.LIVE_TEST_SERVER_HOST,
981+                                 settings.LIVE_TEST_SERVER_PORT)
982+        return urllib2.urlopen(base + url)
983+
984+
985+class LiveServerViews(LiveServerBase):
986+    def test_404(self):
987+        """
988+        Ensure that the LiveServerTestCase serves 404s.
989+        """
990+        try:
991+            self.urlopen('/')
992+        except urllib2.HTTPError, err:
993+            self.assertEquals(err.code, 404, 'Expected 404 response')
994+        else:
995+            self.fail('Expected 404 response')
996+
997+    def test_view(self):
998+        """
999+        Ensure that the LiveServerTestCase serves views.
1000+        """
1001+        f = self.urlopen('/example_view/')
1002+        self.assertEquals(f.read(), 'example view')
1003+
1004+    def test_static_files(self):
1005+        """
1006+        Ensure that the LiveServerTestCase serves static files.
1007+        """
1008+        f = self.urlopen('/static/example_file.txt')
1009+        self.assertEquals(f.read(), 'example file\n')
1010+
1011+    def test_media_files(self):
1012+        """
1013+        Ensure that the LiveServerTestCase serves media files.
1014+        """
1015+        f = self.urlopen('/media/example_media_file.txt')
1016+        self.assertEquals(f.read(), 'example media file\n')
1017+
1018+
1019+class LiveServerDatabase(LiveServerBase):
1020+
1021+    def test_fixtures_loaded(self):
1022+        """
1023+        Ensure that fixtures are properly loaded and visible to the
1024+        live server thread.
1025+        """
1026+        f = self.urlopen('/model_view/')
1027+        self.assertEquals(f.read().splitlines(), ['jane', 'robert'])
1028+
1029+    def test_database_writes(self):
1030+        """
1031+        Ensure that data written to the database by a view can be read.
1032+        """
1033+        self.urlopen('/create_model_instance/')
1034+        names = [person.name for person in Person.objects.all()]
1035+        self.assertEquals(names, ['jane', 'robert', 'emily'])
1036diff --git a/tests/regressiontests/servers/urls.py b/tests/regressiontests/servers/urls.py
1037new file mode 100644
1038index 0000000..83f757f
1039--- /dev/null
1040+++ b/tests/regressiontests/servers/urls.py
1041@@ -0,0 +1,12 @@
1042+from __future__ import absolute_import
1043+
1044+from django.conf.urls import patterns, url
1045+
1046+from . import views
1047+
1048+
1049+urlpatterns = patterns('',
1050+    url(r'^example_view/$', views.example_view),
1051+    url(r'^model_view/$', views.model_view),
1052+    url(r'^create_model_instance/$', views.create_model_instance),
1053+)
1054diff --git a/tests/regressiontests/servers/views.py b/tests/regressiontests/servers/views.py
1055new file mode 100644
1056index 0000000..42fafe0
1057--- /dev/null
1058+++ b/tests/regressiontests/servers/views.py
1059@@ -0,0 +1,17 @@
1060+from django.http import HttpResponse
1061+from .models import Person
1062+
1063+
1064+def example_view(request):
1065+    return HttpResponse('example view')
1066+
1067+
1068+def model_view(request):
1069+    people = Person.objects.all()
1070+    return HttpResponse('\n'.join([person.name for person in people]))
1071+
1072+
1073+def create_model_instance(request):
1074+    person = Person(name='emily')
1075+    person.save()
1076+    return HttpResponse('')
1077diff --git a/tests/test_sqlite.py b/tests/test_sqlite.py
1078index de8bf93..5af754c 100644
1079--- a/tests/test_sqlite.py
1080+++ b/tests/test_sqlite.py
1081@@ -1,11 +1,13 @@
1082 # This is an example test settings file for use with the Django test suite.
1083 #
1084 # The 'sqlite3' backend requires only the ENGINE setting (an in-
1085-# memory database will be used). All other backends will require a
1086-# NAME and potentially authentication information. See the
1087-# following section in the docs for more information:
1088+# memory database will be used) and the 'check_same_thread' option set to False
1089+# so that the in-memory database is shared between threads (which is required
1090+# for running Selenium tests). All other backends will require a NAME and
1091+# potentially authentication information. See the following section in the docs
1092+# for more information:
1093 #
1094-# http://docs.djangoproject.com/en/dev/internals/contributing/#unit-tests
1095+# https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/
1096 #
1097 # The different databases that Django supports behave differently in certain
1098 # situations, so it is recommended to run the test suite against as many
1099@@ -14,9 +16,15 @@
1100 
1101 DATABASES = {
1102     'default': {
1103-        'ENGINE': 'django.db.backends.sqlite3'
1104+        'ENGINE': 'django.db.backends.sqlite3',
1105+        'OPTIONS': {
1106+            'check_same_thread': False,
1107+        },
1108     },
1109     'other': {
1110         'ENGINE': 'django.db.backends.sqlite3',
1111+        'OPTIONS': {
1112+            'check_same_thread': False,
1113+        },
1114     }
1115 }