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

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

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

    diff --git a/django/test/testcases.py b/django/test/testcases.py
    index ee22ac2..871b7de 100644
    a b  
    11from __future__ import with_statement
    22
     3import httplib
    34import re
    45import sys
    56from functools import wraps
    67from urlparse import urlsplit, urlunsplit
    78from xml.dom.minidom import parseString, Node
     9import select
     10import socket
     11import threading
    812
    913from django.conf import settings
     14from django.contrib.staticfiles.handlers import StaticFilesHandler
    1015from django.core import mail
    1116from django.core.exceptions import ValidationError
     17from django.core.handlers.wsgi import WSGIHandler
    1218from django.core.management import call_command
    1319from django.core.signals import request_started
     20from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
     21    WSGIServerException)
    1422from django.core.urlresolvers import clear_url_caches
    1523from django.core.validators import EMPTY_VALUES
    1624from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
    from django.test.utils import (get_warnings_state, restore_warnings_state,  
    2331    override_settings)
    2432from django.utils import simplejson, unittest as ut2
    2533from django.utils.encoding import smart_str
     34from django.utils.unittest.case import skipUnless
    2635
    2736__all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
    2837           'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature')
    def skipUnlessDBFeature(feature):  
    732741    """
    733742    return _deferredSkip(lambda: not getattr(connection.features, feature),
    734743                         "Database doesn't support feature %s" % feature)
     744
     745class QuietWSGIRequestHandler(WSGIRequestHandler):
     746    """
     747    Just a regular WSGIRequestHandler except it doesn't log to the standard
     748    output any of the requests received, so as to not clutter the output for
     749    the tests' results.
     750    """
     751    def log_message(*args):
     752        pass
     753
     754class NonBlockingWSGIServer(WSGIServer):
     755    """
     756    The code in this class is borrowed from the `SocketServer.BaseServer` class
     757    in Python 2.6. The important functionality here is that the server is non-
     758    blocking and that it can be shut down at any moment. This is made possible
     759    by the server regularly polling the socket and checking if it has been
     760    asked to stop.
     761    Note for the future: Once Django stops supporting Python 2.5, this class
     762    can be removed as `WSGIServer` will have this ability to shutdown on
     763    demand.
     764    """
     765
     766    def __init__(self, *args, **kwargs):
     767        super(NonBlockingWSGIServer, self).__init__(*args, **kwargs)
     768        self.__is_shut_down = threading.Event()
     769        self.__serving = False
     770
     771    def serve_forever(self, poll_interval=0.5):
     772        """Handle one request at a time until shutdown.
     773
     774        Polls for shutdown every poll_interval seconds.
     775        """
     776        self.__serving = True
     777        self.__is_shut_down.clear()
     778        while self.__serving:
     779            r, w, e = select.select([self], [], [], poll_interval)
     780            if r:
     781                self._handle_request_noblock()
     782        self.__is_shut_down.set()
     783
     784    def shutdown(self):
     785        """Stops the serve_forever loop.
     786
     787        Blocks until the loop has finished. This must be called while
     788        serve_forever() is running in another thread, or it will
     789        deadlock.
     790        """
     791        self.__serving = False
     792        self.__is_shut_down.wait()
     793
     794    def handle_request(self):
     795        """Handle one request, possibly blocking.
     796        """
     797        fd_sets = select.select([self], [], [], None)
     798        if not fd_sets[0]:
     799            return
     800        self._handle_request_noblock()
     801
     802    def _handle_request_noblock(self):
     803        """Handle one request, without blocking.
     804
     805        I assume that select.select has returned that the socket is
     806        readable before this function was called, so there should be
     807        no risk of blocking in get_request().
     808        """
     809        try:
     810            request, client_address = self.get_request()
     811        except socket.error:
     812            return
     813        if self.verify_request(request, client_address):
     814            try:
     815                self.process_request(request, client_address)
     816            except Exception:
     817                self.handle_error(request, client_address)
     818                self.close_request(request)
     819
     820class LiveServerThread(threading.Thread):
     821    """
     822    Thread for running a live http server while the tests are running.
     823    """
     824
     825    def __init__(self, address, port, fixtures):
     826        self.address = address
     827        self.port = port
     828        self.fixtures = fixtures
     829        self.is_ready = threading.Event()
     830        self.error = None
     831        super(LiveServerThread, self).__init__()
     832
     833    def run(self):
     834        """
     835        Sets up live server and database and loops over handling http requests.
     836        """
     837        try:
     838            # Instantiate and start the server
     839            self.httpd = NonBlockingWSGIServer(
     840                (self.address, self.port), QuietWSGIRequestHandler)
     841            handler = StaticFilesHandler(WSGIHandler())
     842            self.httpd.set_app(handler)
     843
     844            # If the database is in memory we must reload the data in this new
     845            # thread.
     846            if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3' or
     847                settings.DATABASES['default']['TEST_NAME']):
     848                connection.creation.create_test_db(0)
     849                # Import the fixtures into the test database
     850                if hasattr(self, 'fixtures'):
     851                    call_command('loaddata', *self.fixtures,
     852                        **{'verbosity': 0})
     853            self.is_ready.set()
     854            self.httpd.serve_forever()
     855        except WSGIServerException, e:
     856            self.error = e
     857            self.is_ready.set()
     858
     859    def join(self, timeout=None):
     860        self.httpd.shutdown()
     861        super(LiveServerThread, self).join(timeout)
     862
     863class LiveServerTestCase(TestCase):
     864    """
     865    Does basically the same as TestCase but also launches a live http server in
     866    a separate thread so that the tests may use another testing framework, such
     867    as Selenium for example, instead of the built-in dummy client.
     868    """
     869
     870    django_server_address = '127.0.0.1' # TODO: Should those be settings instead
     871    django_server_port = 8000           # of class attributes?
     872    fixtures = []
     873
     874    def setUp(self):
     875        # Launch the Django live server's thread
     876        self.server_thread = LiveServerThread(self.django_server_address,
     877            int(self.django_server_port), fixtures=self.fixtures)
     878        self.server_thread.start()
     879        super(LiveServerTestCase, self).setUp()
     880
     881    def tearDown(self):
     882        # Terminate the Django server's thread
     883        self.server_thread.join()
     884        super(LiveServerTestCase, self).tearDown()
     885
     886try:
     887    # Check if the 'selenium' package is installed
     888    from selenium import selenium
     889    selenium_installed = True
     890except ImportError:
     891    selenium_installed = False
     892
     893# Check if the Selenium server is running
     894try:
     895    conn = httplib.HTTPConnection(settings.SELENIUM_SERVER_ADDRESS,
     896        settings.SELENIUM_SERVER_PORT)
     897    try:
     898        conn.request("GET", "/selenium-server/driver/", '', {})
     899    finally:
     900        conn.close()
     901    selenium_server_running = True
     902except socket.error:
     903    selenium_server_running = False
     904
     905class SeleniumTestCase(LiveServerTestCase):
     906    """
     907    Does basically the same as TestServerTestCase but also connects to the
     908    Selenium server. The selenium client is then available with
     909    'self.selenium'. The requirements are to have the 'selenium' installed in
     910    the python path and to have the Selenium server running. If those
     911    requirements are not filled then the tests will be skipped.
     912    """
     913
     914    def setUp(self):
     915        super(SeleniumTestCase, self).setUp()
     916        # Launch the Selenium server
     917        selenium_browser_url = 'http://%s:%s' % (
     918            self.django_server_address, self.django_server_port)
     919        self.selenium = selenium(
     920            settings.SELENIUM_SERVER_ADDRESS,
     921            int(settings.SELENIUM_SERVER_PORT),
     922            settings.SELENIUM_BROWSER,
     923            selenium_browser_url)
     924        self.selenium.start()
     925        # Wait for the Django server to be ready
     926        self.server_thread.is_ready.wait()
     927        if self.server_thread.error:
     928            raise self.server_thread.error
     929
     930    def tearDown(self):
     931        self.selenium.stop()
     932        super(SeleniumTestCase, self).tearDown()
     933
     934SeleniumTestCase = skipUnless(selenium_installed,
     935    'The \'selenium\' package isn\'t installed')(SeleniumTestCase)
     936SeleniumTestCase = skipUnless(selenium_server_running,
     937    'Can\'t connect to the Selenium server using address %s and port %s' % (
     938    settings.SELENIUM_SERVER_ADDRESS, settings.SELENIUM_SERVER_PORT)
     939)(SeleniumTestCase)
     940 No newline at end of file
  • docs/internals/contributing/writing-code/unit-tests.txt

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

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

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

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

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