zope.testbrowser-4.0.4/COPYRIGHT.rst0000644000000000000000000000004012225722334015302 0ustar 00000000000000Zope Foundation and Contributorszope.testbrowser-4.0.4/bootstrap.py0000644000000000000000000002443512225722334015605 0ustar 00000000000000############################################################################## # # Copyright (c) 2006 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Bootstrap a buildout-based project Simply run this script in a directory containing a buildout.cfg. The script accepts buildout command-line options, so you can use the -c option to specify an alternate configuration file. """ import os, shutil, sys, tempfile, urllib, urllib2, subprocess from optparse import OptionParser if sys.platform == 'win32': def quote(c): if ' ' in c: return '"%s"' % c # work around spawn lamosity on windows else: return c else: quote = str # See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. stdout, stderr = subprocess.Popen( [sys.executable, '-Sc', 'try:\n' ' import ConfigParser\n' 'except ImportError:\n' ' print 1\n' 'else:\n' ' print 0\n'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() has_broken_dash_S = bool(int(stdout.strip())) # In order to be more robust in the face of system Pythons, we want to # run without site-packages loaded. This is somewhat tricky, in # particular because Python 2.6's distutils imports site, so starting # with the -S flag is not sufficient. However, we'll start with that: if not has_broken_dash_S and 'site' in sys.modules: # We will restart with python -S. args = sys.argv[:] args[0:0] = [sys.executable, '-S'] args = map(quote, args) os.execv(sys.executable, args) # Now we are running with -S. We'll get the clean sys.path, import site # because distutils will do it later, and then reset the path and clean # out any namespace packages from site-packages that might have been # loaded by .pth files. clean_path = sys.path[:] import site # imported because of its side effects sys.path[:] = clean_path for k, v in sys.modules.items(): if k in ('setuptools', 'pkg_resources') or ( hasattr(v, '__path__') and len(v.__path__) == 1 and not os.path.exists(os.path.join(v.__path__[0], '__init__.py'))): # This is a namespace package. Remove it. sys.modules.pop(k) is_jython = sys.platform.startswith('java') setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' distribute_source = 'http://python-distribute.org/distribute_setup.py' # parsing arguments def normalize_to_url(option, opt_str, value, parser): if value: if '://' not in value: # It doesn't smell like a URL. value = 'file://%s' % ( urllib.pathname2url( os.path.abspath(os.path.expanduser(value))),) if opt_str == '--download-base' and not value.endswith('/'): # Download base needs a trailing slash to make the world happy. value += '/' else: value = None name = opt_str[2:].replace('-', '_') setattr(parser.values, name, value) usage = '''\ [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] Bootstraps a buildout-based project. Simply run this script in a directory containing a buildout.cfg, using the Python that you want bin/buildout to use. Note that by using --setup-source and --download-base to point to local resources, you can keep this script from going over the network. ''' parser = OptionParser(usage=usage) parser.add_option("-v", "--version", dest="version", help="use a specific zc.buildout version") parser.add_option("-d", "--distribute", action="store_true", dest="use_distribute", default=False, help="Use Distribute rather than Setuptools.") parser.add_option("--setup-source", action="callback", dest="setup_source", callback=normalize_to_url, nargs=1, type="string", help=("Specify a URL or file location for the setup file. " "If you use Setuptools, this will default to " + setuptools_source + "; if you use Distribute, this " "will default to " + distribute_source + ".")) parser.add_option("--download-base", action="callback", dest="download_base", callback=normalize_to_url, nargs=1, type="string", help=("Specify a URL or directory for downloading " "zc.buildout and either Setuptools or Distribute. " "Defaults to PyPI.")) parser.add_option("--eggs", help=("Specify a directory for storing eggs. Defaults to " "a temporary directory that is deleted when the " "bootstrap script completes.")) parser.add_option("-t", "--accept-buildout-test-releases", dest='accept_buildout_test_releases', action="store_true", default=False, help=("Normally, if you do not specify a --version, the " "bootstrap script and buildout gets the newest " "*final* versions of zc.buildout and its recipes and " "extensions for you. If you use this flag, " "bootstrap and buildout will get the newest releases " "even if they are alphas or betas.")) parser.add_option("-c", None, action="store", dest="config_file", help=("Specify the path to the buildout configuration " "file to be used.")) options, args = parser.parse_args() if options.eggs: eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) else: eggs_dir = tempfile.mkdtemp() if options.setup_source is None: if options.use_distribute: options.setup_source = distribute_source else: options.setup_source = setuptools_source if options.accept_buildout_test_releases: args.insert(0, 'buildout:accept-buildout-test-releases=true') try: import pkg_resources import setuptools # A flag. Sometimes pkg_resources is installed alone. if not hasattr(pkg_resources, '_distribute'): raise ImportError except ImportError: ez_code = urllib2.urlopen( options.setup_source).read().replace('\r\n', '\n') ez = {} exec ez_code in ez setup_args = dict(to_dir=eggs_dir, download_delay=0) if options.download_base: setup_args['download_base'] = options.download_base if options.use_distribute: setup_args['no_fake'] = True if sys.version_info[:2] == (2, 4): setup_args['version'] = '0.6.32' ez['use_setuptools'](**setup_args) if 'pkg_resources' in sys.modules: reload(sys.modules['pkg_resources']) import pkg_resources # This does not (always?) update the default working set. We will # do it. for path in sys.path: if path not in pkg_resources.working_set.entries: pkg_resources.working_set.add_entry(path) cmd = [quote(sys.executable), '-c', quote('from setuptools.command.easy_install import main; main()'), '-mqNxd', quote(eggs_dir)] if not has_broken_dash_S: cmd.insert(1, '-S') find_links = options.download_base if not find_links: find_links = os.environ.get('bootstrap-testing-find-links') if not find_links and options.accept_buildout_test_releases: find_links = 'http://downloads.buildout.org/' if find_links: cmd.extend(['-f', quote(find_links)]) if options.use_distribute: setup_requirement = 'distribute' else: setup_requirement = 'setuptools' ws = pkg_resources.working_set setup_requirement_path = ws.find( pkg_resources.Requirement.parse(setup_requirement)).location env = dict( os.environ, PYTHONPATH=setup_requirement_path) requirement = 'zc.buildout' version = options.version if version is None and not options.accept_buildout_test_releases: # Figure out the most recent final version of zc.buildout. import setuptools.package_index _final_parts = '*final-', '*final' def _final_version(parsed_version): for part in parsed_version: if (part[:1] == '*') and (part not in _final_parts): return False return True index = setuptools.package_index.PackageIndex( search_path=[setup_requirement_path]) if find_links: index.add_find_links((find_links,)) req = pkg_resources.Requirement.parse(requirement) if index.obtain(req) is not None: best = [] bestv = None for dist in index[req.project_name]: distv = dist.parsed_version if distv >= pkg_resources.parse_version('2dev'): continue if _final_version(distv): if bestv is None or distv > bestv: best = [dist] bestv = distv elif distv == bestv: best.append(dist) if best: best.sort() version = best[-1].version if version: requirement += '=='+version else: requirement += '<2dev' cmd.append(requirement) if is_jython: import subprocess exitcode = subprocess.Popen(cmd, env=env).wait() else: # Windows prefers this, apparently; otherwise we would prefer subprocess exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) if exitcode != 0: sys.stdout.flush() sys.stderr.flush() print ("An error occurred when trying to install zc.buildout. " "Look above this message for any errors that " "were output by easy_install.") sys.exit(exitcode) ws.add_entry(eggs_dir) ws.require(requirement) import zc.buildout.buildout # If there isn't already a command in the args, add bootstrap if not [a for a in args if '=' not in a]: args.append('bootstrap') # if -c was provided, we push it back into args for buildout's main function if options.config_file is not None: args[0:0] = ['-c', options.config_file] zc.buildout.buildout.main(args) if not options.eggs: # clean up temporary egg directory shutil.rmtree(eggs_dir) zope.testbrowser-4.0.4/MANIFEST.in0000644000000000000000000000025112225722334014742 0ustar 00000000000000include *.py include *.rst include .travis.yml include tox.ini include buildout.cfg recursive-include src *.gif recursive-include src *.html recursive-include src *.txt zope.testbrowser-4.0.4/README.rst0000644000000000000000000000065112225722334014677 0ustar 00000000000000|buildstatus|_ ``zope.testbrowser`` provides an easy-to-use programmable web browser with special focus on testing. It is used in Zope, but it's not Zope specific at all. For instance, it can be used to test or otherwise interact with any web site. .. |buildstatus| image:: https://api.travis-ci.org/zopefoundation/zope.testbrowser.png?branch=master .. _buildstatus: https://travis-ci.org/zopefoundation/zope.testbrowser zope.testbrowser-4.0.4/CHANGES.rst0000644000000000000000000001636312225722334015021 0ustar 00000000000000======= CHANGES ======= 4.0.4 (2013-10-11) ------------------ - Removed the 'WebTest <= 1.3.4' version pin, fixed tests to work with modern WebTest versions (https://github.com/zopefoundation/zope.testbrowser/issues/10). 4.0.3 (2013-09-04) ------------------ - pinning version 'WebTest <= 1.3.4', because of some incompatibility and test failures - Make zope.testbrowser installable via pip (https://github.com/zopefoundation/zope.testbrowser/issues/6). - When ``Browser.handleErrors`` is False, also add ``x-wsgiorg.throw_errors`` to the environment. http://wsgi.org/wsgi/Specifications/throw_errors - Prevent WebTest from always sending ``paste.throw_errors=True`` in the environment by setting it to ``None`` when ``Browser.handleErrors`` is ``True``. This makes it easier to test error pages. - Made Browser.submit() handle ``raiseHttpErrors`` (https://github.com/zopefoundation/zope.testbrowser/pull/4). - More friendly error messages from getControl() et al: - when you specify an index that is out of bounds, show the available choices - when you fail to find anything, show all the available items 4.0.2 (2011-05-25) ------------------ - Remove test dependency on zope.pagetemplate. 4.0.1 (2011-05-04) ------------------ - Added a hint in documentation how to use ``zope.testbrowser.wsgi.Browser`` to test a Zope 2/Zope 3/Bluebream WSGI application. 4.0.0 (2011-03-14) ------------------ - LP #721252: AmbiguityError now shows all matching controls. - Integrate with WebTest. ``zope.testbrowser.wsgi.Browser`` is a ``Browser`` implementation that uses ``webtest.TestApp`` to drive a WSGI application. This this replaces the wsgi_intercept support added in 3.11. - Re-write the test application as a pure WSGI application using WebOb. Run the existing tests using the WebTest based Browser - Move zope.app.testing based Browser into ``zope.app.testing`` (leaving backwards compatibility imports in-place). Released in ``zope.app.testing`` 3.9.0. 3.11.1 (2011-01-24) ------------------- - Fixing brown bag release 3.11.0. 3.11.0 (2011-01-24) ------------------- - Added `wsgi_intercept` support (came from ``zope.app.wsgi.testlayer``). 3.10.4 (2011-01-14) ------------------- - Move the over-the-wire.txt doctest out of the TestBrowserLayer as it doesn't need or use it. - Fix test compatibility with zope.app.testing 3.8.1. 3.10.3 (2010-10-15) ------------------- - Fixed backwards compatibility with ``zope.app.wsgi.testlayer``. 3.10.2 (2010-10-15) ------------------- - Fixed Python 2.7 compatibility in Browser.handleErrors. 3.10.1 (2010-09-21) ------------------- - Fixed a bug that caused the ``Browser`` to keep it's previous ``contents`` The places are: - Link.click() - SubmitControl.click() - ImageControl.click() - Form.submit() - Also adjusted exception messages at the above places to match pre version 3.4.1 messages. 3.10.0 (2010-09-14) ------------------- - LP #98437: use mechanize's built-in ``submit()`` to submit forms, allowing mechanize to set the "Referer:" (sic) header appropriately. - Fixed tests to run with ``zope.app.testing`` 3.8 and above. 3.9.0 (2010-05-17) ------------------ - LP #568806: Update dependency ``mechanize >= 0.2.0``, which now includes the ``ClientForm`` APIs. Remove use of ``urllib2`` APIs (incompatible with ``mechanize 0.2.0``) in favor of ``mechanize`` equivalents. Thanks to John J. Lee for the patch. - Use stdlib ``doctest`` module, instead of ``zope.testing.doctest``. - **Caution:** This version is no longer fully compatible with Python 2.4: ``handleErrors = False`` no longer works. 3.8.1 (2010-04-19) ------------------ - Pinned dependency on mechanize to prevent use of the upcoming 0.2.0 release before we have time to adjust to its API changes. - LP #98396: testbrowser resolves relative URLs incorrectly. 3.8.0 (2010-03-05) ------------------ - Added ``follow`` convenience method which gets and follows a link. 3.7.0 (2009-12-17) ------------------ - Moved zope.app.testing dependency into the scope of the PublisherConnection class. Zope2 specifies its own PublisherConnection which isn't dependent on zope.app.testing. - Fixed LP #419119: return None when the browser has no contents instead of raising an exception. 3.7.0a1 (2009-08-29) -------------------- - Remove dependency on zope.app.publisher in favor of zope.browserpage, zope.browserresource and zope.ptresource. - Remove dependencies on zope.app.principalannotation and zope.securitypolicy by using the simple PermissiveSecurityPolicy. We aren't testing security in our tests. - Replaced the testing dependency on zope.app.zcmlfiles with explicit dependencies of a minimal set of packages. - Remove unneeded zope.app.authentication from ftesting.zcml. - Test dependency on zope.securitypolicy instead of its app variant. 3.6.0a2 (2009-01-31) -------------------- - Test dependency on zope.site.folder instead of zope.app.folder. - Remove useless test dependency in zope.app.component. 3.6.0a1 (2009-01-08) -------------------- - Author e-mail to zope-dev rather than zope3-dev. - New lines are no longer stripped in XML and HTML code contained in a textarea; fix requires ClientForm >= 0.2.10 (LP #268139). - Added ``cookies`` attribute to browser for easy manipulation of browser cookies. See brief example in main documentation, plus new ``cookies.txt`` documentation. 3.5.1 (2008-10-10) ------------------ - Provide a work around for a mechanize/urllib2 bug on Python 2.6 missing 'timeout' attribute on 'Request' base class. - Provide a work around for a mechanize/urllib2 bug in creating request objects that won't handle fragment URLs correctly. 3.5.0 (2008-03-30) ------------------ - Added a zope.testbrowser.testing.Browser.post method that allows tests to supply a body and a content type. This is handy for testing Ajax requests with non-form input (e.g. JSON). - Remove vendor import of mechanize. - Fix bug that caused HTTP exception tracebacks to differ between version 3.4.0 and 3.4.1. - Workaround for bug in Python Cookie.SimpleCookie when handling unicode strings. - Fix bug introduced in 3.4.1 that created incompatible tracebacks in doctests. This necessitated adding a patched mechanize to the source tree; patches have been sent to the mechanize project. - Fix https://bugs.launchpad.net/bugs/149517 by adding zope.interface and zope.schema as real dependencies - Fix browser.getLink documentation that was not updated since the last API modification. - Move tests for fixed bugs to a separate file. - Removed non-functional and undocumented code intended to help test servers using virtual hosting. 3.4.2 (2007-10-31) ------------------ - Resolve ``ZopeSecurityPolicy`` deprecation warning. 3.4.1 (2007-09-01) ------------------ * Updated to mechanize 0.1.7b and ClientForm 0.2.7. These are now pulled in via egg dependencies. * ``zope.testbrowser`` now works on Python 2.5. 3.4.0 (2007-06-04) ------------------ * Added the ability to suppress raising exceptions on HTTP errors (``raiseHttpErrors`` attribute). * Made the tests more resilient to HTTP header formatting changes with the REnormalizer. 3.4.0a1 (2007-04-22) -------------------- Initial release as a separate project, corresponds to zope.testbrowser from Zope 3.4.0a1 zope.testbrowser-4.0.4/tox.ini0000644000000000000000000000016412225722334014522 0ustar 00000000000000[tox] envlist = py26,py27 [testenv] deps = zope.testing WebTest commands = python setup.py test -q zope.testbrowser-4.0.4/setup.py0000644000000000000000000000470312225722334014724 0ustar 00000000000000############################################################################## # # Copyright (c) 2006 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Setup for zope.testbrowser package """ import os from setuptools import setup, find_packages long_description = ( '.. contents::\n\n' + open('README.rst').read() + '\n\n' + open(os.path.join('src', 'zope', 'testbrowser', 'README.txt')).read() + '\n\n' + open('CHANGES.rst').read() ) tests_require = [ 'zope.testing', 'WebTest', ] setup( name='zope.testbrowser', version='4.0.4', url='http://pypi.python.org/pypi/zope.testbrowser', license='ZPL 2.1', description='Programmable browser for functional black-box tests', author='Zope Corporation and Contributors', author_email='zope-dev@zope.org', long_description=long_description, classifiers=[ 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: Zope Public License', 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Software Development :: Testing', 'Topic :: Internet :: WWW/HTTP', ], packages=find_packages('src'), package_dir={'': 'src'}, namespace_packages=['zope'], test_suite='zope.testbrowser.tests', tests_require=tests_require, install_requires=[ # mechanize 0.2.0 folds in ClientForm, makes incompatible API changes 'mechanize>=0.2.0', 'setuptools', 'zope.interface', 'zope.schema', 'pytz > dev', ], extras_require={ 'test': tests_require, 'test_bbb': [ 'zope.testbrowser [test,zope-functional-testing]', ], 'zope-functional-testing': [ 'zope.app.testing >= 3.9.0dev', ], 'wsgi': [ 'WebTest', ] }, include_package_data=True, zip_safe=False, ) zope.testbrowser-4.0.4/setup.cfg0000644000000000000000000000007312225722370015027 0ustar 00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 zope.testbrowser-4.0.4/PKG-INFO0000644000000000000000000020555212225722370014314 0ustar 00000000000000Metadata-Version: 1.1 Name: zope.testbrowser Version: 4.0.4 Summary: Programmable browser for functional black-box tests Home-page: http://pypi.python.org/pypi/zope.testbrowser Author: Zope Corporation and Contributors Author-email: zope-dev@zope.org License: ZPL 2.1 Description: .. contents:: |buildstatus|_ ``zope.testbrowser`` provides an easy-to-use programmable web browser with special focus on testing. It is used in Zope, but it's not Zope specific at all. For instance, it can be used to test or otherwise interact with any web site. .. |buildstatus| image:: https://api.travis-ci.org/zopefoundation/zope.testbrowser.png?branch=master .. _buildstatus: https://travis-ci.org/zopefoundation/zope.testbrowser ====================== Detailed Documentation ====================== Different Browsers ------------------ HTTP Browser ~~~~~~~~~~~~ The ``zope.testbrowser.browser`` module exposes a ``Browser`` class that simulates a web browser similar to Mozilla Firefox or IE. >>> from zope.testbrowser.browser import Browser >>> browser = Browser() This version of the browser object can be used to access any web site just as you would do using a normal web browser. WSGI Test Browser ~~~~~~~~~~~~~~~~~ General usage +++++++++++++ There is also a special version of the ``Browser`` class which uses `WebTest`_ and can be used to do functional testing of WSGI applications. It can be imported from ``zope.testbrowser.wsgi``: >>> from zope.testbrowser.wsgi import Browser >>> from zope.testbrowser.tests.test_wsgi import demo_app >>> browser = Browser('http://localhost/', wsgi_app=demo_app) >>> print browser.contents Hello world! ... .. _`WebTest`: http://pypi.python.org/pypi/WebTest To use this browser you have to: * use the `wsgi` extra of the ``zope.testbrowser`` egg, You can also use it with zope layers by: * write a subclass of ``zope.testbrowser.wsgi.Layer`` and override the ``make_wsgi_app`` method, * use an instance of the class as the test layer of your test. Example: >>> import zope.testbrowser.wsgi >>> class SimpleLayer(zope.testbrowser.wsgi.Layer): ... def make_wsgi_app(self): ... return simple_app Where ``simple_app`` is the callable of your WSGI application. Testing a Zope 2/Zope 3/Bluebream WSGI application ++++++++++++++++++++++++++++++++++++++++++++++++++ When testing a Zope 2/Zope 3/Bluebream WSGI application you should wrap your WSGI application under test into ``zope.testbrowser.wsgi.AuthorizationMiddleware`` as all these application servers expect basic authentication headers to be base64 encoded. This middleware handles this for you. Example when using the layer: >>> import zope.testbrowser.wsgi >>> class ZopeSimpleLayer(zope.testbrowser.wsgi.Layer): ... def make_wsgi_app(self): ... return zope.testbrowser.wsgi.AuthorizationMiddleware(simple_app) There is also a BrowserLayer in `zope.app.wsgi.testlayer`_ which does this for you and includes a ``TransactionMiddleware``, too, which could be handy when testing a ZODB based application. .. _`zope.app.wsgi.testlayer` : http://pypi.python.org/pypi/zope.app.wsgi Bowser Usage ------------ We will test this browser against a WSGI test application: >>> from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication >>> wsgi_app = WSGITestApplication() An initial page to load can be passed to the ``Browser`` constructor: >>> browser = Browser('http://localhost/@@/testbrowser/simple.html', wsgi_app=wsgi_app) >>> browser.url 'http://localhost/@@/testbrowser/simple.html' The browser can send arbitrary headers; this is helpful for setting the "Authorization" header or a language value, so that your tests format values the way you expect in your tests, if you rely on zope.i18n locale-based formatting or a similar approach. >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw') >>> browser.addHeader('Accept-Language', 'en-US') An existing browser instance can also `open` web pages: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.url 'http://localhost/@@/testbrowser/simple.html' Once you have opened a web page initially, best practice for writing testbrowser doctests suggests using 'click' to navigate further (as discussed below), except in unusual circumstances. The test browser complies with the IBrowser interface; see ``zope.testbrowser.interfaces`` for full details on the interface. >>> from zope.testbrowser import interfaces >>> from zope.interface.verify import verifyObject >>> verifyObject(interfaces.IBrowser, browser) True Page Contents ------------- The contents of the current page are available: >>> print browser.contents Simple Page

Simple Page

Making assertions about page contents is easy. >>> '

Simple Page

' in browser.contents True Utilizing the doctest facilities, it also possible to do: >>> browser.contents '...

Simple Page

...' Note: Unfortunately, ellipsis (...) cannot be used at the beginning of the output (this is a limitation of doctest). Checking for HTML ----------------- Not all URLs return HTML. Of course our simple page does: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.isHtml True But if we load an image (or other binary file), we do not get HTML: >>> browser.open('http://localhost/@@/testbrowser/zope3logo.gif') >>> browser.isHtml False HTML Page Title ---------------- Another useful helper property is the title: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.title 'Simple Page' If a page does not provide a title, it is simply ``None``: >>> browser.open('http://localhost/@@/testbrowser/notitle.html') >>> browser.title However, if the output is not HTML, then an error will occur trying to access the title: >>> browser.open('http://localhost/@@/testbrowser/zope3logo.gif') >>> browser.title Traceback (most recent call last): ... BrowserStateError: not viewing HTML Headers ------- As you can see, the `contents` of the browser does not return any HTTP headers. The headers are accessible via a separate attribute, which is an ``httplib.HTTPMessage`` instance (httplib is a part of Python's standard library): >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.headers The headers can be accessed as a string: >>> print browser.headers Status: 200 OK Content-Length: 123 Content-Type: text/html;charset=utf-8 Or as a mapping: >>> browser.headers['content-type'] 'text/html;charset=utf-8' Cookies ------- When a Set-Cookie header is available, it can be found in the headers, as seen above. Here, we use a view that will make the server set cookies with the values we provide. >>> browser.open('http://localhost/set_cookie.html?name=foo&value=bar') >>> browser.headers['set-cookie'].replace(';', '') 'foo=bar' It is also available in the browser's ``cookies`` attribute. This is an extended mapping interface that allows getting, setting, and deleting the cookies that the browser is remembering *for the current url*. Here are a few examples. >>> browser.cookies['foo'] 'bar' >>> browser.cookies.keys() ['foo'] >>> browser.cookies.values() ['bar'] >>> browser.cookies.items() [('foo', 'bar')] >>> 'foo' in browser.cookies True >>> 'bar' in browser.cookies False >>> len(browser.cookies) 1 >>> print(dict(browser.cookies)) {'foo': 'bar'} >>> browser.cookies['sha'] = 'zam' >>> len(browser.cookies) 2 >>> sorted(browser.cookies.items()) [('foo', 'bar'), ('sha', 'zam')] >>> browser.open('http://localhost/get_cookie.html') >>> print browser.headers.get('set-cookie') None >>> print browser.contents # server got the cookie change foo: bar sha: zam >>> sorted(browser.cookies.items()) [('foo', 'bar'), ('sha', 'zam')] >>> browser.cookies.clearAll() >>> len(browser.cookies) 0 Many more examples, and a discussion of the additional methods available, can be found in cookies.txt. Navigation and Link Objects --------------------------- If you want to simulate clicking on a link, get the link and `click` on it. In the `navigate.html` file there are several links set up to demonstrate the capabilities of the link objects and their `click` method. The simplest way to get a link is via the anchor text. In other words the text you would see in a browser (text and url searches are substring searches): >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...Link Text...' >>> link = browser.getLink('Link Text') >>> link Link objects comply with the ILink interface. >>> verifyObject(interfaces.ILink, link) True Links expose several attributes for easy access. >>> link.text 'Link Text' >>> link.tag # links can also be image maps. 'a' >>> link.url # it's normalized 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' >>> link.attrs {'href': 'navigate.html?message=By+Link+Text'} Links can be "clicked" and the browser will navigate to the referenced URL. >>> link.click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' >>> browser.contents '...Message: By Link Text...' When finding a link by its text, whitespace is normalized. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...> Link Text \n with Whitespace\tNormalization (and parens) >> link = browser.getLink('Link Text with Whitespace Normalization ' ... '(and parens)') >>> link >>> link.text 'Link Text with Whitespace Normalization (and parens)' >>> link.click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text+with+Normalization' >>> browser.contents '...Message: By Link Text with Normalization...' When a link text matches more than one link, by default the first one is chosen. You can, however, specify the index of the link and thus retrieve a later matching link: >>> browser.getLink('Link Text') >>> browser.getLink('Link Text', index=1) Note that clicking a link object after its browser page has expired will generate an error. >>> link.click() Traceback (most recent call last): ... ExpiredError You can also find the link by its URL, >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...Using the URL...' >>> browser.getLink(url='?message=By+URL').click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+URL' >>> browser.contents '...Message: By URL...' or its id: >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...By Anchor Id...' >>> browser.getLink(id='anchorid').click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Id' >>> browser.contents '...Message: By Id...' You thought we were done here? Not so quickly. The `getLink` method also supports image maps, though not by specifying the coordinates, but using the area's id: >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> link = browser.getLink(id='zope3') >>> link.tag 'area' >>> link.click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=Zope+3+Name' >>> browser.contents '...Message: Zope 3 Name...' Getting a nonexistent link raises an exception. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.getLink('This does not exist') Traceback (most recent call last): ... LinkNotFoundError A convenience method is provided to follow links; this uses the same arguments as `getLink`, but clicks on the link instead of returning the link object. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...Link Text...' >>> browser.follow('Link Text') >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' >>> browser.contents '...Message: By Link Text...' >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.follow(url='?message=By+URL') >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+URL' >>> browser.contents '...Message: By URL...' >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.follow(id='zope3') >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=Zope+3+Name' >>> browser.contents '...Message: Zope 3 Name...' Attempting to follow links that don't exist raises the same exception as asking for the link object: >>> browser.follow('This does not exist') Traceback (most recent call last): ... LinkNotFoundError Other Navigation ---------------- Like in any normal browser, you can reload a page: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.url 'http://localhost/@@/testbrowser/simple.html' >>> browser.reload() >>> browser.url 'http://localhost/@@/testbrowser/simple.html' You can also go back: >>> browser.open('http://localhost/@@/testbrowser/notitle.html') >>> browser.url 'http://localhost/@@/testbrowser/notitle.html' >>> browser.goBack() >>> browser.url 'http://localhost/@@/testbrowser/simple.html' Controls -------- One of the most important features of the browser is the ability to inspect and fill in values for the controls of input forms. To do so, let's first open a page that has a bunch of controls: >>> browser.open('http://localhost/@@/testbrowser/controls.html') Obtaining a Control ~~~~~~~~~~~~~~~~~~~ You look up browser controls with the 'getControl' method. The default first argument is 'label', and looks up the form on the basis of any associated label. >>> control = browser.getControl('Text Control') >>> control >>> browser.getControl(label='Text Control') # equivalent If you request a control that doesn't exist, the code raises a LookupError: >>> browser.getControl('Does Not Exist') Traceback (most recent call last): ... LookupError: label 'Does Not Exist' available items: ... If you request a control with an ambiguous lookup, the code raises an AmbiguityError. >>> browser.getControl('Ambiguous Control') Traceback (most recent call last): ... AmbiguityError: label 'Ambiguous Control' matches: This is also true if an option in a control is ambiguous in relation to the control itself. >>> browser.getControl('Sub-control Ambiguity') Traceback (most recent call last): ... AmbiguityError: label 'Sub-control Ambiguity' matches: Ambiguous controls may be specified using an index value. We use the control's value attribute to show the two controls; this attribute is properly introduced below. >>> browser.getControl('Ambiguous Control', index=0) >>> browser.getControl('Ambiguous Control', index=0).value 'First' >>> browser.getControl('Ambiguous Control', index=1).value 'Second' >>> browser.getControl('Sub-control Ambiguity', index=0) >>> browser.getControl('Sub-control Ambiguity', index=1).optionValue 'ambiguous' >>> browser.getControl('Sub-control Ambiguity', index=2) Traceback (most recent call last): ... LookupError: label 'Sub-control Ambiguity' Index 2 out of range, available choices are 0...1 0: 1: Label searches are against stripped, whitespace-normalized, no-tag versions of the text. Text applied to searches is also stripped and whitespace normalized. The search finds results if the text search finds the whole words of your text in a label. Thus, for instance, a search for 'Add' will match the label 'Add a Client' but not 'Address'. Case is honored. >>> browser.getControl('Label Needs Whitespace Normalization') >>> browser.getControl('label needs whitespace normalization') Traceback (most recent call last): ... LookupError: label 'label needs whitespace normalization' ... >>> browser.getControl(' Label Needs Whitespace ') >>> browser.getControl('Whitespace') >>> browser.getControl('hitespace') Traceback (most recent call last): ... LookupError: label 'hitespace' ... >>> browser.getControl('[non word characters should not confuse]') Multiple labels can refer to the same control (simply because that is possible in the HTML 4.0 spec). >>> browser.getControl('Multiple labels really') >>> browser.getControl('really are possible') >>> browser.getControl('really') # OK: ambiguous labels, but not ambiguous control A label can be connected with a control using the 'for' attribute and also by containing a control. >>> browser.getControl( ... 'Labels can be connected by containing their respective fields') Get also accepts one other search argument, 'name'. Only one of 'label' and 'name' may be used at a time. The 'name' keyword searches form field names. >>> browser.getControl(name='text-value') >>> browser.getControl(name='ambiguous-control-name') Traceback (most recent call last): ... AmbiguityError: name 'ambiguous-control-name' matches: >>> browser.getControl(name='does-not-exist') Traceback (most recent call last): ... LookupError: name 'does-not-exist' available items: ... >>> browser.getControl(name='ambiguous-control-name', index=1).value 'Second' Combining 'label' and 'name' raises a ValueError, as does supplying neither of them. >>> browser.getControl(label='Ambiguous Control', name='ambiguous-control-name') Traceback (most recent call last): ... ValueError: Supply one and only one of "label" and "name" as arguments >>> browser.getControl() Traceback (most recent call last): ... ValueError: Supply one and only one of "label" and "name" as arguments Radio and checkbox fields are unusual in that their labels and names may point to different objects: names point to logical collections of radio buttons or checkboxes, but labels may only be used for individual choices within the logical collection. This means that obtaining a radio button by label gets a different object than obtaining the radio collection by name. Select options may also be searched by label. >>> browser.getControl(name='radio-value') >>> browser.getControl('Zwei') >>> browser.getControl('One') >>> browser.getControl('Tres') Characteristics of controls and subcontrols are discussed below. Control Objects ~~~~~~~~~~~~~~~ Controls provide IControl. >>> ctrl = browser.getControl('Text Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True They have several useful attributes: - the name as which the control is known to the form: >>> ctrl.name 'text-value' - the value of the control, which may also be set: >>> ctrl.value 'Some Text' >>> ctrl.value = 'More Text' >>> ctrl.value 'More Text' - the type of the control: >>> ctrl.type 'text' - a flag describing whether the control is disabled: >>> ctrl.disabled False - and a flag to tell us whether the control can have multiple values: >>> ctrl.multiple False Additionally, controllers for select, radio, and checkbox provide IListControl. These fields have four other attributes and an additional method: >>> ctrl = browser.getControl('Multiple Select Control') >>> ctrl >>> ctrl.disabled False >>> ctrl.multiple True >>> verifyObject(interfaces.IListControl, ctrl) True - 'options' lists all available value options. >>> ctrl.options ['1', '2', '3'] - 'displayOptions' lists all available options by label. The 'label' attribute on an option has precedence over its contents, which is why our last option is 'Third' in the display. >>> ctrl.displayOptions ['Un', 'Deux', 'Third'] - 'displayValue' lets you get and set the displayed values of the control of the select box, rather than the actual values. >>> ctrl.value [] >>> ctrl.displayValue [] >>> ctrl.displayValue = ['Un', 'Deux'] >>> ctrl.displayValue ['Un', 'Deux'] >>> ctrl.value ['1', '2'] - 'controls' gives you a list of the subcontrol objects in the control (subcontrols are discussed below). >>> ctrl.controls [, , ] - The 'getControl' method lets you get subcontrols by their label or their value. >>> ctrl.getControl('Un') >>> ctrl.getControl('Deux') >>> ctrl.getControl('Trois') # label attribute >>> ctrl.getControl('Third') # contents >>> browser.getControl('Third') # ambiguous in the browser, so useful Traceback (most recent call last): ... AmbiguityError: label 'Third' matches: Finally, submit controls provide ISubmitControl, and image controls provide IImageSubmitControl, which extents ISubmitControl. These both simply add a 'click' method. For image submit controls, you may also provide a coordinates argument, which is a tuple of (x, y). These submit the forms, and are demonstrated below as we examine each control individually. ItemControl Objects ~~~~~~~~~~~~~~~~~~~ As introduced briefly above, using labels to obtain elements of a logical radio button or checkbox collection returns item controls, which are parents. Manipulating the value of these controls affects the parent control. >>> browser.getControl(name='radio-value').value ['2'] >>> browser.getControl('Zwei').optionValue # read-only. '2' >>> browser.getControl('Zwei').selected True >>> verifyObject(interfaces.IItemControl, browser.getControl('Zwei')) True >>> browser.getControl('Ein').selected = True >>> browser.getControl('Ein').selected True >>> browser.getControl('Zwei').selected False >>> browser.getControl(name='radio-value').value ['1'] >>> browser.getControl('Ein').selected = False >>> browser.getControl(name='radio-value').value [] >>> browser.getControl('Zwei').selected = True Checkbox collections behave similarly, as shown below. Controls with subcontrols-- Various Controls ~~~~~~~~~~~~~~~~ The various types of controls are demonstrated here. - Text Control The text control we already introduced above. - Password Control >>> ctrl = browser.getControl('Password Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value 'Password' >>> ctrl.value = 'pass now' >>> ctrl.value 'pass now' >>> ctrl.disabled False >>> ctrl.multiple False - Hidden Control >>> ctrl = browser.getControl(name='hidden-value') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value 'Hidden' >>> ctrl.value = 'More Hidden' >>> ctrl.disabled False >>> ctrl.multiple False - Text Area Control >>> ctrl = browser.getControl('Text Area Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value ' Text inside\n area!\n ' >>> ctrl.value = 'A lot of\n text.' >>> ctrl.disabled False >>> ctrl.multiple False - File Control File controls are used when a form has a file-upload field. To specify data, call the add_file method, passing: - A file-like object - a content type, and - a file name >>> ctrl = browser.getControl('File Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value is None True >>> import cStringIO >>> ctrl.add_file(cStringIO.StringIO('File contents'), ... 'text/plain', 'test.txt') The file control (like the other controls) also knows if it is disabled or if it can have multiple values. >>> ctrl.disabled False >>> ctrl.multiple False - Selection Control (Single-Valued) >>> ctrl = browser.getControl('Single Select Control') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value ['1'] >>> ctrl.value = ['2'] >>> ctrl.disabled False >>> ctrl.multiple False >>> ctrl.options ['1', '2', '3'] >>> ctrl.displayOptions ['Uno', 'Dos', 'Third'] >>> ctrl.displayValue ['Dos'] >>> ctrl.displayValue = ['Tres'] >>> ctrl.displayValue ['Third'] >>> ctrl.displayValue = ['Dos'] >>> ctrl.displayValue ['Dos'] >>> ctrl.displayValue = ['Third'] >>> ctrl.displayValue ['Third'] >>> ctrl.value ['3'] - Selection Control (Multi-Valued) This was already demonstrated in the introduction to control objects above. - Checkbox Control (Single-Valued; Unvalued) >>> ctrl = browser.getControl(name='single-unvalued-checkbox-value') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value True >>> ctrl.value = False >>> ctrl.disabled False >>> ctrl.multiple True >>> ctrl.options [True] >>> ctrl.displayOptions ['Single Unvalued Checkbox'] >>> ctrl.displayValue [] >>> verifyObject( ... interfaces.IItemControl, ... browser.getControl('Single Unvalued Checkbox')) True >>> browser.getControl('Single Unvalued Checkbox').optionValue 'on' >>> browser.getControl('Single Unvalued Checkbox').selected False >>> ctrl.displayValue = ['Single Unvalued Checkbox'] >>> ctrl.displayValue ['Single Unvalued Checkbox'] >>> browser.getControl('Single Unvalued Checkbox').selected True >>> browser.getControl('Single Unvalued Checkbox').selected = False >>> browser.getControl('Single Unvalued Checkbox').selected False >>> ctrl.displayValue [] >>> browser.getControl( ... name='single-disabled-unvalued-checkbox-value').disabled True - Checkbox Control (Single-Valued, Valued) >>> ctrl = browser.getControl(name='single-valued-checkbox-value') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value ['1'] >>> ctrl.value = [] >>> ctrl.disabled False >>> ctrl.multiple True >>> ctrl.options ['1'] >>> ctrl.displayOptions ['Single Valued Checkbox'] >>> ctrl.displayValue [] >>> verifyObject( ... interfaces.IItemControl, ... browser.getControl('Single Valued Checkbox')) True >>> browser.getControl('Single Valued Checkbox').selected False >>> browser.getControl('Single Valued Checkbox').optionValue '1' >>> ctrl.displayValue = ['Single Valued Checkbox'] >>> ctrl.displayValue ['Single Valued Checkbox'] >>> browser.getControl('Single Valued Checkbox').selected True >>> browser.getControl('Single Valued Checkbox').selected = False >>> browser.getControl('Single Valued Checkbox').selected False >>> ctrl.displayValue [] - Checkbox Control (Multi-Valued) >>> ctrl = browser.getControl(name='multi-checkbox-value') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value ['1', '3'] >>> ctrl.value = ['1', '2'] >>> ctrl.disabled False >>> ctrl.multiple True >>> ctrl.options ['1', '2', '3'] >>> ctrl.displayOptions ['One', 'Two', 'Three'] >>> ctrl.displayValue ['One', 'Two'] >>> ctrl.displayValue = ['Two'] >>> ctrl.value ['2'] >>> browser.getControl('Two').optionValue '2' >>> browser.getControl('Two').selected True >>> verifyObject(interfaces.IItemControl, browser.getControl('Two')) True >>> browser.getControl('Three').selected = True >>> browser.getControl('Three').selected True >>> browser.getControl('Two').selected True >>> ctrl.value ['2', '3'] >>> browser.getControl('Two').selected = False >>> ctrl.value ['3'] >>> browser.getControl('Three').selected = False >>> ctrl.value [] - Radio Control This is how you get a radio button based control: >>> ctrl = browser.getControl(name='radio-value') This shows the existing value of the control, as it was in the HTML received from the server: >>> ctrl.value ['2'] We can then unselect it: >>> ctrl.value = [] >>> ctrl.value [] We can also reselect it: >>> ctrl.value = ['2'] >>> ctrl.value ['2'] displayValue shows the text the user would see next to the control: >>> ctrl.displayValue ['Zwei'] This is just unit testing: >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.disabled False >>> ctrl.multiple False >>> ctrl.options ['1', '2', '3'] >>> ctrl.displayOptions ['Ein', 'Zwei', 'Drei'] >>> ctrl.displayValue = ['Ein'] >>> ctrl.value ['1'] >>> ctrl.displayValue ['Ein'] The radio control subcontrols were illustrated above. - Image Control >>> ctrl = browser.getControl(name='image-value') >>> ctrl >>> verifyObject(interfaces.IImageSubmitControl, ctrl) True >>> ctrl.value '' >>> ctrl.disabled False >>> ctrl.multiple False - Submit Control >>> ctrl = browser.getControl(name='submit-value') >>> ctrl >>> browser.getControl('Submit This') # value of submit button is a label >>> browser.getControl('Standard Submit Control') # label tag is legal >>> browser.getControl('Submit') # multiple labels, but same control >>> verifyObject(interfaces.ISubmitControl, ctrl) True >>> ctrl.value 'Submit This' >>> ctrl.disabled False >>> ctrl.multiple False Using Submitting Controls ~~~~~~~~~~~~~~~~~~~~~~~~~ Both the submit and image type should be clickable and submit the form: >>> browser.getControl('Text Control').value = 'Other Text' >>> browser.getControl('Submit').click() >>> print browser.contents ... Other Text ... Submit This ... Note that if you click a submit object after the associated page has expired, you will get an error. >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> ctrl = browser.getControl('Submit') >>> ctrl.click() >>> ctrl.click() Traceback (most recent call last): ... ExpiredError All the above also holds true for the image control: >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> browser.getControl('Text Control').value = 'Other Text' >>> browser.getControl(name='image-value').click() >>> print browser.contents ... Other Text ... 1 1 ... >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> ctrl = browser.getControl(name='image-value') >>> ctrl.click() >>> ctrl.click() Traceback (most recent call last): ... ExpiredError But when sending an image, you can also specify the coordinate you clicked: >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> browser.getControl(name='image-value').click((50,25)) >>> print browser.contents ... 50 25 ... Pages Without Controls ~~~~~~~~~~~~~~~~~~~~~~ What would happen if we tried to look up a control on a page that has none? >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.getControl('anything') Traceback (most recent call last): ... LookupError: label 'anything' (there are no form items in the HTML) Forms ----- Because pages can have multiple forms with like-named controls, it is sometimes necessary to access forms by name or id. The browser's `forms` attribute can be used to do so. The key value is the form's name or id. If more than one form has the same name or id, the first one will be returned. >>> browser.open('http://localhost/@@/testbrowser/forms.html') >>> form = browser.getForm(name='one') Form instances conform to the IForm interface. >>> verifyObject(interfaces.IForm, form) True The form exposes several attributes related to forms: - The name of the form: >>> form.name 'one' - The id of the form: >>> form.id '1' - The action (target URL) when the form is submitted: >>> form.action 'http://localhost/@@/testbrowser/forms.html' - The method (HTTP verb) used to transmit the form data: >>> form.method 'GET' Besides those attributes, you have also a couple of methods. Like for the browser, you can get control objects, but limited to the current form... >>> form.getControl(name='text-value') ...and submit the form. >>> form.submit('Submit') >>> print browser.contents ... First Text ... Submitting also works without specifying a control, as shown below, which is it's primary reason for existing in competition with the control submission discussed above. Now let me show you briefly that looking up forms is sometimes important. In the `forms.html` template, we have four forms all having a text control named `text-value`. Now, if I use the browser's `get` method, >>> browser.getControl(name='text-value') Traceback (most recent call last): ... AmbiguityError: name 'text-value' matches: >>> browser.getControl('Text Control') Traceback (most recent call last): ... AmbiguityError: label 'Text Control' matches: I'll always get an ambiguous form field. I can use the index argument, or with the `getForm` method I can disambiguate by searching only within a given form: >>> form = browser.getForm('2') >>> form.getControl(name='text-value').value 'Second Text' >>> form.submit('Submit') >>> browser.contents '...Second Text...' >>> form = browser.getForm('2') >>> form.getControl('Submit').click() >>> browser.contents '...Second Text...' >>> browser.getForm('3').getControl('Text Control').value 'Third Text' The last form on the page does not have a name, an id, or a submit button. Working with it is still easy, thanks to a index attribute that guarantees order. (Forms without submit buttons are sometimes useful for JavaScript.) >>> form = browser.getForm(index=3) >>> form.submit() >>> browser.contents '...Fourth Text...Submitted without the submit button....' If a form is requested that does not exists, an exception will be raised. >>> form = browser.getForm('does-not-exist') Traceback (most recent call last): LookupError If the HTML page contains only one form, no arguments to `getForm` are needed: >>> oneform = Browser(wsgi_app=wsgi_app) >>> oneform.open('http://localhost/@@/testbrowser/oneform.html') >>> form = oneform.getForm() If the HTML page contains more than one form, `index` is needed to disambiguate if no other arguments are provided: >>> browser.getForm() Traceback (most recent call last): ValueError: if no other arguments are given, index is required. Submitting a posts body directly -------------------------------- In addition to the open method, zope.testbrowser.testing.Browser has a ``post`` method that allows a request body to be supplied. This method is particularly helpful when testing Ajax methods. Let's visit a page that echos some interesting values from it's request: >>> browser.open('http://localhost/echo.html') >>> print browser.contents HTTP_ACCEPT_LANGUAGE: en-US HTTP_CONNECTION: close HTTP_HOST: localhost HTTP_USER_AGENT: Python-urllib/2.4 PATH_INFO: /echo.html REQUEST_METHOD: GET Body: '' Now, we'll try a post. The post method takes a URL, a data string, and an optional content type. If we just pass a string, then a URL-encoded query string is assumed: >>> browser.post('http://localhost/echo.html', 'x=1&y=2') >>> print browser.contents CONTENT_LENGTH: 7 CONTENT_TYPE: application/x-www-form-urlencoded HTTP_ACCEPT_LANGUAGE: en-US HTTP_CONNECTION: close HTTP_HOST: localhost HTTP_USER_AGENT: Python-urllib/2.4 PATH_INFO: /echo.html REQUEST_METHOD: POST x: 1 y: 2 Body: '' The body is empty because it is consumed to get form data. We can pass a content-type explicitly: >>> browser.post('http://localhost/echo.html', ... '{"x":1,"y":2}', 'application/x-javascript') >>> print browser.contents CONTENT_LENGTH: 13 CONTENT_TYPE: application/x-javascript HTTP_ACCEPT_LANGUAGE: en-US HTTP_CONNECTION: close HTTP_HOST: localhost HTTP_USER_AGENT: Python-urllib/2.4 PATH_INFO: /echo.html REQUEST_METHOD: POST Body: '{"x":1,"y":2}' Here, the body is left in place because it isn't form data. Performance Testing ------------------- Browser objects keep up with how much time each request takes. This can be used to ensure a particular request's performance is within a tolerable range. Be very careful using raw seconds, cross-machine differences can be huge, pystones is usually a better choice. >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.lastRequestSeconds < 10 # really big number for safety True >>> browser.lastRequestPystones < 10000 # really big number for safety True Handling Errors --------------- Often WSGI middleware or the application itself gracefully handle application errors, such as invalid URLs: >>> browser.open('http://localhost/invalid') Traceback (most recent call last): ... HTTPError: HTTP Error 404: Not Found Note that the above error was thrown by ``mechanize`` and not by the application. For debugging purposes, however, it can be very useful to see the original exception caused by the application. In those cases you can set the ``handleErrors`` property of the browser to ``False``. It is defaulted to ``True``: >>> browser.handleErrors True So when we tell the application not to handle the errors, >>> browser.handleErrors = False we get a different, internal error from the application: >>> browser.open('http://localhost/invalid') Traceback (most recent call last): ... NotFound: /invalid NB: Setting the handleErrors attribute to False will only change anything if the WSGI application obeys the wsgi.handleErrors or paste.throw_errors WSGI environment variables. i.e. it does not catch and handle the original exception when these are set appropriately. When the testbrowser is raising HttpErrors, the errors still hit the test. Sometimes we don't want that to happen, in situations where there are edge cases that will cause the error to be predictably but infrequently raised. Time is a primary cause of this. To get around this, one can set the raiseHttpErrors to False. >>> browser.handleErrors = True >>> browser.raiseHttpErrors = False This will cause HttpErrors not to propagate. >>> browser.open('http://localhost/invalid') The headers are still there, though. >>> '404 Not Found' in str(browser.headers) True If we don't handle the errors, and allow internal ones to propagate, however, this flag doesn't affect things. >>> browser.handleErrors = False >>> browser.open('http://localhost/invalid') Traceback (most recent call last): ... NotFound: /invalid >>> browser.raiseHttpErrors = True Hand-Holding ------------ Instances of the various objects ensure that users don't set incorrect instance attributes accidentally. >>> browser.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Browser' object has no attribute 'nonexistant' >>> form.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Form' object has no attribute 'nonexistant' >>> control.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Control' object has no attribute 'nonexistant' >>> link.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Link' object has no attribute 'nonexistant' HTTPS support ------------- Depending on the scheme of the request the variable wsgi.url_scheme will be set correctly on the request: >>> browser.open('http://localhost/echo_one.html?var=wsgi.url_scheme') >>> print browser.contents 'http' >>> browser.open('https://localhost/echo_one.html?var=wsgi.url_scheme') >>> print browser.contents 'https' see http://www.python.org/dev/peps/pep-3333/ for details. ======= CHANGES ======= 4.0.4 (2013-10-11) ------------------ - Removed the 'WebTest <= 1.3.4' version pin, fixed tests to work with modern WebTest versions (https://github.com/zopefoundation/zope.testbrowser/issues/10). 4.0.3 (2013-09-04) ------------------ - pinning version 'WebTest <= 1.3.4', because of some incompatibility and test failures - Make zope.testbrowser installable via pip (https://github.com/zopefoundation/zope.testbrowser/issues/6). - When ``Browser.handleErrors`` is False, also add ``x-wsgiorg.throw_errors`` to the environment. http://wsgi.org/wsgi/Specifications/throw_errors - Prevent WebTest from always sending ``paste.throw_errors=True`` in the environment by setting it to ``None`` when ``Browser.handleErrors`` is ``True``. This makes it easier to test error pages. - Made Browser.submit() handle ``raiseHttpErrors`` (https://github.com/zopefoundation/zope.testbrowser/pull/4). - More friendly error messages from getControl() et al: - when you specify an index that is out of bounds, show the available choices - when you fail to find anything, show all the available items 4.0.2 (2011-05-25) ------------------ - Remove test dependency on zope.pagetemplate. 4.0.1 (2011-05-04) ------------------ - Added a hint in documentation how to use ``zope.testbrowser.wsgi.Browser`` to test a Zope 2/Zope 3/Bluebream WSGI application. 4.0.0 (2011-03-14) ------------------ - LP #721252: AmbiguityError now shows all matching controls. - Integrate with WebTest. ``zope.testbrowser.wsgi.Browser`` is a ``Browser`` implementation that uses ``webtest.TestApp`` to drive a WSGI application. This this replaces the wsgi_intercept support added in 3.11. - Re-write the test application as a pure WSGI application using WebOb. Run the existing tests using the WebTest based Browser - Move zope.app.testing based Browser into ``zope.app.testing`` (leaving backwards compatibility imports in-place). Released in ``zope.app.testing`` 3.9.0. 3.11.1 (2011-01-24) ------------------- - Fixing brown bag release 3.11.0. 3.11.0 (2011-01-24) ------------------- - Added `wsgi_intercept` support (came from ``zope.app.wsgi.testlayer``). 3.10.4 (2011-01-14) ------------------- - Move the over-the-wire.txt doctest out of the TestBrowserLayer as it doesn't need or use it. - Fix test compatibility with zope.app.testing 3.8.1. 3.10.3 (2010-10-15) ------------------- - Fixed backwards compatibility with ``zope.app.wsgi.testlayer``. 3.10.2 (2010-10-15) ------------------- - Fixed Python 2.7 compatibility in Browser.handleErrors. 3.10.1 (2010-09-21) ------------------- - Fixed a bug that caused the ``Browser`` to keep it's previous ``contents`` The places are: - Link.click() - SubmitControl.click() - ImageControl.click() - Form.submit() - Also adjusted exception messages at the above places to match pre version 3.4.1 messages. 3.10.0 (2010-09-14) ------------------- - LP #98437: use mechanize's built-in ``submit()`` to submit forms, allowing mechanize to set the "Referer:" (sic) header appropriately. - Fixed tests to run with ``zope.app.testing`` 3.8 and above. 3.9.0 (2010-05-17) ------------------ - LP #568806: Update dependency ``mechanize >= 0.2.0``, which now includes the ``ClientForm`` APIs. Remove use of ``urllib2`` APIs (incompatible with ``mechanize 0.2.0``) in favor of ``mechanize`` equivalents. Thanks to John J. Lee for the patch. - Use stdlib ``doctest`` module, instead of ``zope.testing.doctest``. - **Caution:** This version is no longer fully compatible with Python 2.4: ``handleErrors = False`` no longer works. 3.8.1 (2010-04-19) ------------------ - Pinned dependency on mechanize to prevent use of the upcoming 0.2.0 release before we have time to adjust to its API changes. - LP #98396: testbrowser resolves relative URLs incorrectly. 3.8.0 (2010-03-05) ------------------ - Added ``follow`` convenience method which gets and follows a link. 3.7.0 (2009-12-17) ------------------ - Moved zope.app.testing dependency into the scope of the PublisherConnection class. Zope2 specifies its own PublisherConnection which isn't dependent on zope.app.testing. - Fixed LP #419119: return None when the browser has no contents instead of raising an exception. 3.7.0a1 (2009-08-29) -------------------- - Remove dependency on zope.app.publisher in favor of zope.browserpage, zope.browserresource and zope.ptresource. - Remove dependencies on zope.app.principalannotation and zope.securitypolicy by using the simple PermissiveSecurityPolicy. We aren't testing security in our tests. - Replaced the testing dependency on zope.app.zcmlfiles with explicit dependencies of a minimal set of packages. - Remove unneeded zope.app.authentication from ftesting.zcml. - Test dependency on zope.securitypolicy instead of its app variant. 3.6.0a2 (2009-01-31) -------------------- - Test dependency on zope.site.folder instead of zope.app.folder. - Remove useless test dependency in zope.app.component. 3.6.0a1 (2009-01-08) -------------------- - Author e-mail to zope-dev rather than zope3-dev. - New lines are no longer stripped in XML and HTML code contained in a textarea; fix requires ClientForm >= 0.2.10 (LP #268139). - Added ``cookies`` attribute to browser for easy manipulation of browser cookies. See brief example in main documentation, plus new ``cookies.txt`` documentation. 3.5.1 (2008-10-10) ------------------ - Provide a work around for a mechanize/urllib2 bug on Python 2.6 missing 'timeout' attribute on 'Request' base class. - Provide a work around for a mechanize/urllib2 bug in creating request objects that won't handle fragment URLs correctly. 3.5.0 (2008-03-30) ------------------ - Added a zope.testbrowser.testing.Browser.post method that allows tests to supply a body and a content type. This is handy for testing Ajax requests with non-form input (e.g. JSON). - Remove vendor import of mechanize. - Fix bug that caused HTTP exception tracebacks to differ between version 3.4.0 and 3.4.1. - Workaround for bug in Python Cookie.SimpleCookie when handling unicode strings. - Fix bug introduced in 3.4.1 that created incompatible tracebacks in doctests. This necessitated adding a patched mechanize to the source tree; patches have been sent to the mechanize project. - Fix https://bugs.launchpad.net/bugs/149517 by adding zope.interface and zope.schema as real dependencies - Fix browser.getLink documentation that was not updated since the last API modification. - Move tests for fixed bugs to a separate file. - Removed non-functional and undocumented code intended to help test servers using virtual hosting. 3.4.2 (2007-10-31) ------------------ - Resolve ``ZopeSecurityPolicy`` deprecation warning. 3.4.1 (2007-09-01) ------------------ * Updated to mechanize 0.1.7b and ClientForm 0.2.7. These are now pulled in via egg dependencies. * ``zope.testbrowser`` now works on Python 2.5. 3.4.0 (2007-06-04) ------------------ * Added the ability to suppress raising exceptions on HTTP errors (``raiseHttpErrors`` attribute). * Made the tests more resilient to HTTP header formatting changes with the REnormalizer. 3.4.0a1 (2007-04-22) -------------------- Initial release as a separate project, corresponds to zope.testbrowser from Zope 3.4.0a1 Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Internet :: WWW/HTTP zope.testbrowser-4.0.4/buildout.cfg0000644000000000000000000000134012225722334015514 0ustar 00000000000000[buildout] develop = . parts = test test_bbb coverage-test coverage-report interpreter [test] recipe = zc.recipe.testrunner defaults = ['--tests-pattern', '^f?tests$'] eggs = zope.testbrowser [test] [test_bbb] recipe = zc.recipe.testrunner defaults = ['--tests-pattern', '^f?tests$'] eggs = zope.testbrowser [test,test_bbb] [coverage-test] recipe = zc.recipe.testrunner eggs = zope.testbrowser [test] defaults = ['--coverage', '${buildout:directory}/coverage'] [coverage-report] recipe = zc.recipe.egg eggs = z3c.coverage scripts = coveragereport arguments = ('${buildout:directory}/coverage', '${buildout:directory}/coverage/report') [interpreter] recipe = zc.recipe.egg eggs = zope.testbrowser interpreter = py zope.testbrowser-4.0.4/LICENSE.rst0000644000000000000000000000402612225722334015024 0ustar 00000000000000Zope Public License (ZPL) Version 2.1 A copyright notice accompanies this license document that identifies the copyright holders. This license has been certified as open source. It has also been designated as GPL compatible by the Free Software Foundation (FSF). Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. 4. The right to distribute this software or to use it for any purpose does not give you the right to use Servicemarks (sm) or Trademarks (tm) of the copyright holders. Use of them is covered by separate agreement with the copyright holders. 5. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. zope.testbrowser-4.0.4/.travis.yml0000644000000000000000000000024212225722334015315 0ustar 00000000000000language: python python: - 2.6 - 2.7 install: - pip install tox script: - tox -e py${TRAVIS_PYTHON_VERSION//[.]/} notifications: email: false zope.testbrowser-4.0.4/src/zope/__init__.py0000644000000000000000000000007012225722334017060 0ustar 00000000000000__import__('pkg_resources').declare_namespace(__name__) zope.testbrowser-4.0.4/src/zope/testbrowser/interfaces.py0000644000000000000000000003717712225722334022051 0ustar 00000000000000############################################################################## # # Copyright (c) 2005 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Browser-like functional doctest interfaces """ __docformat__ = "reStructuredText" import zope.interface import zope.schema import zope.interface.common.mapping class AlreadyExpiredError(ValueError): pass class ICookies(zope.interface.common.mapping.IExtendedReadMapping, zope.interface.common.mapping.IExtendedWriteMapping, zope.interface.common.mapping.IMapping): # NOT copy """A mapping of cookies for a given url""" url = zope.schema.URI( title=u"URL", description=u"The URL the mapping is currently exposing.", required=True) header = zope.schema.TextLine( title=u"Header", description=u"The current value for the Cookie header for the URL", required=True) def forURL(url): """Returns another ICookies instance for the given URL.""" def getinfo(name): """returns dict of settings for the given cookie name. This includes only the following cookie values: - name (str) - value (str), - port (int or None), - domain (str), - path (str or None), - secure (bool), and - expires (datetime.datetime with pytz.UTC timezone or None), - comment (str or None), - commenturl (str or None). (Method name is not camelCase because it is intended to feel like an extension to the mapping interface, which uses all lower case, e.g. iterkeys.) """ def iterinfo(name=None): """iterate over the information about all the cookies for the URL. Each result is a dictionary as described for ``getinfo``. If name is given, iterates over all cookies for given name. (Method name is not camelCase because it is intended to feel like an extension to the mapping interface, which uses all lower case, e.g. iterkeys.) """ def create(name, value, domain=None, expires=None, path=None, secure=None, comment=None, commenturl=None, port=None): """Create a new cookie with the given values. If cookie of the same name, domain, and path exists, raises a ValueError. Expires is a string or a datetime.datetime. timezone-naive datetimes are interpreted as in UTC. If expires is before now, raises AlreadyExpiredError. If the domain or path do not generally match the current URL, raises ValueError. """ def change(name, value=None, domain=None, expires=None, path=None, secure=None, comment=None, commenturl=None, port=None): """Change an attribute of an existing cookie. If cookie does not exist, raises a KeyError.""" def clearAll(): """Clear all cookies for the associated browser, irrespective of URL """ def clearAllSession(): """Clear session cookies for associated browser, irrespective of URL """ class IBrowser(zope.interface.Interface): """A Programmatic Web Browser.""" cookies = zope.schema.Field( title=u"Cookies", description=(u"An ICookies mapping for the browser's current URL."), required=True) url = zope.schema.URI( title=u"URL", description=u"The URL the browser is currently showing.", required=True) headers = zope.schema.Field( title=u"Headers", description=(u"Headers of the HTTP response; a " "``httplib.HTTPMessage``."), required=True) contents = zope.schema.Text( title=u"Contents", description=u"The complete response body of the HTTP request.", required=True) isHtml = zope.schema.Bool( title=u"Is HTML", description=u"Tells whether the output is HTML or not.", required=True) title = zope.schema.TextLine( title=u"Title", description=u"Title of the displayed page", required=False) handleErrors = zope.schema.Bool( title=u"Handle Errors", description=(u"Describes whether server-side errors will be handled " u"by the publisher. If set to ``False``, the error will " u"progress all the way to the test, which is good for " u"debugging."), default=True, required=True) def addHeader(key, value): """Adds a header to each HTTP request. Adding additional headers can be useful in many ways, from setting the credentials token to specifying the browser identification string. """ def open(url, data=None): """Open a URL in the browser. The URL must be fully qualified. However, note that the server name and port is arbitrary for Zope 3 functional tests, since the request is sent to the publisher directly. The ``data`` argument describes the data that will be sent as the body of the request. """ def reload(): """Reload the current page. Like a browser reload, if the past request included a form submission, the form data will be resubmitted.""" def goBack(count=1): """Go back in history by a certain amount of visisted pages. The ``count`` argument specifies how far to go back. It is set to 1 by default. """ def getLink(text=None, url=None, id=None, index=0): """Return an ILink from the page. The link is found by the arguments of the method. One or more may be used together. o ``text`` -- A regular expression trying to match the link's text, in other words everything between and or the value of the submit button. o ``url`` -- The URL the link is going to. This is either the ``href`` attribute of an anchor tag or the action of a form. o ``id`` -- The id attribute of the anchor tag submit button. o ``index`` -- When there's more than one link that matches the text/URL, you can specify which one you want. """ lastRequestSeconds = zope.schema.Field( title=u"Seconds to Process Last Request", description=( u"""Return how many seconds (or fractions) the last request took. The values returned have the same resolution as the results from ``time.clock``. """), required=True, readonly=True) lastRequestPystones = zope.schema.Field( title= u"Approximate System-Independent Effort of Last Request (Pystones)", description=( u"""Return how many pystones the last request took. This number is found by multiplying the number of pystones/second at which this system benchmarks and the result of ``lastRequestSeconds``. """), required=True, readonly=True) def getControl(label=None, name=None, index=None): """Get a control from the page. Only one of ``label`` and ``name`` may be provided. ``label`` searches form labels (including submit button values, per the HTML 4.0 spec), and ``name`` searches form field names. Label value is searched as case-sensitive whole words within the labels for each control--that is, a search for 'Add' will match 'Add a contact' but not 'Address'. A word is defined as one or more alphanumeric characters or the underline. If no values are found, the code raises a LookupError. If ``index`` is None (the default) and more than one field matches the search, the code raises an AmbiguityError. If an index is provided, it is used to choose the index from the ambiguous choices. If the index does not exist, the code raises a LookupError. """ def getForm(id=None, name=None, action=None, index=None): """Get a form from the page. Zero or one of ``id``, ``name``, and ``action`` may be provided. If none are provided the index alone is used to determine the return value. If no values are found, the code raises a LookupError. If ``index`` is None (the default) and more than one form matches the search, the code raises an AmbiguityError. If an index is provided, it is used to choose the index from the ambiguous choices. If the index does not exist, the code raises a LookupError. """ class ExpiredError(Exception): """The browser page to which this was attached is no longer active""" class IControl(zope.interface.Interface): """A control (input field) of a page.""" name = zope.schema.TextLine( title=u"Name", description=u"The name of the control.", required=True) value = zope.schema.Field( title=u"Value", description=u"The value of the control", default=None, required=True) type = zope.schema.Choice( title=u"Type", description=u"The type of the control", values=['text', 'password', 'hidden', 'submit', 'checkbox', 'select', 'radio', 'image', 'file'], required=True) disabled = zope.schema.Bool( title=u"Disabled", description=u"Describes whether a control is disabled.", default=False, required=False) multiple = zope.schema.Bool( title=u"Multiple", description=u"Describes whether this control can hold multiple values.", default=False, required=False) def clear(): """Clear the value of the control.""" class IListControl(IControl): """A radio button, checkbox, or select control""" options = zope.schema.List( title=u"Options", description=u"""\ A list of possible values for the control.""", required=True) displayOptions = zope.schema.List( # TODO: currently only implemented for select by mechanize title=u"Options", description=u"""\ A list of possible display values for the control.""", required=True) displayValue = zope.schema.Field( # TODO: currently only implemented for select by mechanize title=u"Value", description=u"The value of the control, as rendered by the display", default=None, required=True) def getControl(label=None, value=None, index=None): """return subcontrol for given label or value, disambiguated by index if given. Label value is searched as case-sensitive whole words within the labels for each item--that is, a search for 'Add' will match 'Add a contact' but not 'Address'. A word is defined as one or more alphanumeric characters or the underline.""" controls = zope.interface.Attribute( """a list of subcontrols for the control. mutating list has no effect on control (although subcontrols may be changed as usual).""") class ISubmitControl(IControl): def click(): "click the submit button" class IImageSubmitControl(ISubmitControl): def click(coord=(1,1,)): "click the submit button with optional coordinates" class IItemControl(zope.interface.Interface): """a radio button or checkbox within a larger multiple-choice control""" control = zope.schema.Object( title=u"Control", description=(u"The parent control element."), schema=IControl, required=True) disabled = zope.schema.Bool( title=u"Disabled", description=u"Describes whether a subcontrol is disabled.", default=False, required=False) selected = zope.schema.Bool( title=u"Selected", description=u"Whether the subcontrol is selected", default=None, required=True) optionValue = zope.schema.TextLine( title=u"Value", description=u"The value of the subcontrol", default=None, required=False) class ILink(zope.interface.Interface): def click(): """click the link, going to the URL referenced""" url = zope.schema.TextLine( title=u"URL", description=u"The normalized URL of the link", required=False) attrs = zope.schema.Dict( title=u'Attributes', description=u'The attributes of the link tag', required=False) text = zope.schema.TextLine( title=u'Text', description=u'The contained text of the link', required=False) tag = zope.schema.TextLine( title=u'Tag', description=u'The tag name of the link (a or area, typically)', required=True) class IForm(zope.interface.Interface): """An HTML form of the page.""" action = zope.schema.TextLine( title=u"Action", description=u"The action (or URI) that is opened upon submittance.", required=True) method = zope.schema.Choice( title=u"Method", description=u"The method used to submit the form.", values=['post', 'get', 'put'], required=True) enctype = zope.schema.TextLine( title=u"Encoding Type", description=u"The type of encoding used to encode the form data.", required=True) name = zope.schema.TextLine( title=u"Name", description=u"The value of the `name` attribute in the form tag, " u"if specified.", required=True) id = zope.schema.TextLine( title=u"Id", description=u"The value of the `id` attribute in the form tag, " u"if specified.", required=True) def getControl(label=None, name=None, index=None): """Get a control in the page. Only one of ``label`` and ``name`` may be provided. ``label`` searches form labels (including submit button values, per the HTML 4.0 spec), and ``name`` searches form field names. Label value is searched as case-sensitive whole words within the labels for each control--that is, a search for 'Add' will match 'Add a contact' but not 'Address'. A word is defined as one or more alphanumeric characters or the underline. If no values are found, the code raises a LookupError. If ``index`` is None (the default) and more than one field matches the search, the code raises an AmbiguityError. If an index is provided, it is used to choose the index from the ambiguous choices. If the index does not exist, the code raises a LookupError. """ def submit(label=None, name=None, index=None, coord=(1,1)): """Submit this form. The `label`, `name`, and `index` arguments select the submit button to use to submit the form. You may label or name, with index to disambiguate. Label value is searched as case-sensitive whole words within the labels for each control--that is, a search for 'Add' will match 'Add a contact' but not 'Address'. A word is defined as one or more alphanumeric characters or the underline. The control code works identically to 'get' except that searches are filtered to find only submit and image controls. """ zope.testbrowser-4.0.4/src/zope/testbrowser/browser.py0000644000000000000000000006725112225722334021405 0ustar 00000000000000############################################################################## # # Copyright (c) 2005 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Mechanize-based Functional Doctest interfaces """ __docformat__ = "reStructuredText" import cStringIO import re import sys import time import mechanize import zope.interface import zope.testbrowser.cookies import zope.testbrowser.interfaces RegexType = type(re.compile('')) _compress_re = re.compile(r"\s+") compressText = lambda text: _compress_re.sub(' ', text.strip()) def disambiguate(intermediate, msg, index, choice_repr=None, available=None): if intermediate: if index is None: if len(intermediate) > 1: if choice_repr: msg += ' matches:' + ''.join([ '\n %s' % choice_repr(choice) for choice in intermediate]) raise mechanize.AmbiguityError(msg) else: return intermediate[0] else: try: return intermediate[index] except IndexError: msg = '%s\nIndex %d out of range, available choices are 0...%d' % ( msg, index, len(intermediate) - 1) if choice_repr: msg += ''.join(['\n %d: %s' % (n, choice_repr(choice)) for n, choice in enumerate(intermediate)]) else: if available: msg += '\navailable items:' + ''.join([ '\n %s' % choice_repr(choice) for choice in available]) elif available is not None: # empty list msg += '\n(there are no form items in the HTML)' raise LookupError(msg) def control_form_tuple_repr((ctrl, form)): if isinstance(ctrl, mechanize._form.Control): # mechanize._form controls have a useful __str__ and a useless __repr__ return str(ctrl) else: # mechanize._form list control items have a useful __repr__ and a # too-terse __str__. return repr(ctrl) def controlFactory(control, form, browser): if isinstance(control, mechanize.Item): # it is a subcontrol return ItemControl(control, form, browser) else: t = control.type if t in ('checkbox', 'select', 'radio'): return ListControl(control, form, browser) elif t in ('submit', 'submitbutton'): return SubmitControl(control, form, browser) elif t=='image': return ImageControl(control, form, browser) else: return Control(control, form, browser) def any(items): return bool(sum([bool(i) for i in items])) def onlyOne(items, description): total = sum([bool(i) for i in items]) if total == 0 or total > 1: raise ValueError( "Supply one and only one of %s as arguments" % description) def zeroOrOne(items, description): if sum([bool(i) for i in items]) > 1: raise ValueError( "Supply no more than one of %s as arguments" % description) def fix_exception_name(e): # mechanize unceremoniously changed the repr of HTTPErrors, in # in order not to break existing doctests, we have to undo that if hasattr(e, '_exc_class_name'): name = e._exc_class_name name = name.rsplit('.', 1)[-1] e.__class__.__name__ = name class SetattrErrorsMixin(object): _enable_setattr_errors = False def __setattr__(self, name, value): if self._enable_setattr_errors: # cause an attribute error if the attribute doesn't already exist getattr(self, name) # set the value object.__setattr__(self, name, value) class PystoneTimer(object): start_time = 0 end_time = 0 _pystones_per_second = None @property def pystonesPerSecond(self): """How many pystones are equivalent to one second on this machine""" # deferred import as workaround for Zope 2 testrunner issue: # http://www.zope.org/Collectors/Zope/2268 from test import pystone if self._pystones_per_second == None: self._pystones_per_second = pystone.pystones(pystone.LOOPS/10)[1] return self._pystones_per_second def _getTime(self): if sys.platform.startswith('win'): # Windows' time.clock gives us high-resolution wall-time return time.clock() else: # everyone else uses time.time return time.time() def start(self): """Begin a timing period""" self.start_time = self._getTime() self.end_time = None def stop(self): """End a timing period""" self.end_time = self._getTime() @property def elapsedSeconds(self): """Elapsed time from calling `start` to calling `stop` or present time If `stop` has been called, the timing period stopped then, otherwise the end is the current time. """ if self.end_time is None: end_time = self._getTime() else: end_time = self.end_time return end_time - self.start_time @property def elapsedPystones(self): """Elapsed pystones in timing period See elapsed_seconds for definition of timing period. """ return self.elapsedSeconds * self.pystonesPerSecond class Browser(SetattrErrorsMixin): """A web user agent.""" zope.interface.implements(zope.testbrowser.interfaces.IBrowser) _contents = None _counter = 0 def __init__(self, url=None, mech_browser=None): if mech_browser is None: mech_browser = mechanize.Browser() self.mech_browser = mech_browser self.timer = PystoneTimer() self.raiseHttpErrors = True self.cookies = zope.testbrowser.cookies.Cookies(self.mech_browser) self._enable_setattr_errors = True if url is not None: self.open(url) @property def url(self): """See zope.testbrowser.interfaces.IBrowser""" return self.mech_browser.geturl() @property def isHtml(self): """See zope.testbrowser.interfaces.IBrowser""" return self.mech_browser.viewing_html() @property def title(self): """See zope.testbrowser.interfaces.IBrowser""" return self.mech_browser.title() @property def contents(self): """See zope.testbrowser.interfaces.IBrowser""" if self._contents is not None: return self._contents response = self.mech_browser.response() if response is None: return None old_location = response.tell() response.seek(0) self._contents = response.read() response.seek(old_location) return self._contents @property def headers(self): """See zope.testbrowser.interfaces.IBrowser""" return self.mech_browser.response().info() @apply def handleErrors(): """See zope.testbrowser.interfaces.IBrowser""" header_key = 'X-zope-handle-errors' def get(self): headers = self.mech_browser.addheaders value = dict(headers).get(header_key, True) return {'False': False}.get(value, True) def set(self, value): headers = self.mech_browser.addheaders current_value = get(self) if current_value == value: return # Remove the current header... for key, header_value in headers[:]: if key == header_key: headers.remove((key, header_value)) # ... Before adding the new one. headers.append((header_key, {False: 'False'}.get(value, 'True'))) return property(get, set) def open(self, url, data=None): """See zope.testbrowser.interfaces.IBrowser""" url = str(url) self._start_timer() try: try: try: self.mech_browser.open(url, data) except Exception, e: fix_exception_name(e) raise except mechanize.HTTPError, e: if e.code >= 200 and e.code <= 299: # 200s aren't really errors pass elif self.raiseHttpErrors: raise finally: self._stop_timer() self._changed() # if the headers don't have a status, I suppose there can't be an error if 'Status' in self.headers: code, msg = self.headers['Status'].split(' ', 1) code = int(code) if self.raiseHttpErrors and code >= 400: raise mechanize.HTTPError(url, code, msg, self.headers, fp=None) def post(self, url, data, content_type=None): if content_type is not None: data = {'body': data, 'content-type': content_type} return self.open(url, data) def _start_timer(self): self.timer.start() def _stop_timer(self): self.timer.stop() @property def lastRequestPystones(self): """See zope.testbrowser.interfaces.IBrowser""" return self.timer.elapsedPystones @property def lastRequestSeconds(self): """See zope.testbrowser.interfaces.IBrowser""" return self.timer.elapsedSeconds def reload(self): """See zope.testbrowser.interfaces.IBrowser""" self._start_timer() self.mech_browser.reload() self._stop_timer() self._changed() def goBack(self, count=1): """See zope.testbrowser.interfaces.IBrowser""" self._start_timer() self.mech_browser.back(count) self._stop_timer() self._changed() def addHeader(self, key, value): """See zope.testbrowser.interfaces.IBrowser""" if (self.mech_browser.request is not None and key.lower() in ('cookie', 'cookie2') and self.cookies.header): # to prevent unpleasant intermittent errors, only set cookies with # the browser headers OR the cookies mapping. raise ValueError('cookies are already set in `cookies` attribute') self.mech_browser.addheaders.append( (str(key), str(value)) ) def getLink(self, text=None, url=None, id=None, index=0): """See zope.testbrowser.interfaces.IBrowser""" if id is not None: def predicate(link): return dict(link.attrs).get('id') == id args = {'predicate': predicate} else: if isinstance(text, RegexType): text_regex = text elif text is not None: text_regex = re.compile(re.escape(text), re.DOTALL) else: text_regex = None if isinstance(url, RegexType): url_regex = url elif url is not None: url_regex = re.compile(re.escape(url), re.DOTALL) else: url_regex = None args = {'text_regex': text_regex, 'url_regex': url_regex} args['nr'] = index return Link(self.mech_browser.find_link(**args), self) def follow(self, *args, **kw): """Select a link and follow it.""" self.getLink(*args, **kw).click() def _findAllControls(self, forms, include_subcontrols=False): for f in forms: for control in f.controls: phantom = control.type in ('radio', 'checkbox') if not phantom: yield (control, f) if include_subcontrols and ( phantom or control.type=='select'): for i in control.items: yield (i, f) def _findByLabel(self, label, forms, include_subcontrols=False): # forms are iterable of mech_forms matches = re.compile(r'(^|\b|\W)%s(\b|\W|$)' % re.escape(compressText(label))).search found = [] for control, form in self._findAllControls(forms, include_subcontrols): for l in control.get_labels(): if matches(l.text): found.append((control, form)) break return found def _findByName(self, name, forms): found = [] for f in forms: for control in f.controls: if control.name==name: found.append((control, f)) return found def getControl(self, label=None, name=None, index=None): """See zope.testbrowser.interfaces.IBrowser""" intermediate, msg, available = self._get_all_controls( label, name, self.mech_browser.forms(), include_subcontrols=True) control, form = disambiguate(intermediate, msg, index, control_form_tuple_repr, available) return controlFactory(control, form, self) def _get_all_controls(self, label, name, forms, include_subcontrols=False): onlyOne([label, name], '"label" and "name"') forms = list(forms) # might be an iterator, and we need to iterate twice available = None if label is not None: res = self._findByLabel(label, forms, include_subcontrols) msg = 'label %r' % label elif name is not None: include_subcontrols = False res = self._findByName(name, forms) msg = 'name %r' % name if not res: available = list(self._findAllControls(forms, include_subcontrols)) return res, msg, available def getForm(self, id=None, name=None, action=None, index=None): zeroOrOne([id, name, action], '"id", "name", and "action"') matching_forms = [] for form in self.mech_browser.forms(): if ((id is not None and form.attrs.get('id') == id) or (name is not None and form.name == name) or (action is not None and re.search(action, str(form.action))) or id == name == action == None): matching_forms.append(form) if index is None and not any([id, name, action]): if len(matching_forms) == 1: index = 0 else: raise ValueError( 'if no other arguments are given, index is required.') form = disambiguate(matching_forms, '', index) self.mech_browser.form = form return Form(self, form) def _clickSubmit(self, form, control, coord): labels = control.get_labels() if labels: label = labels[0].text else: label = None try: self._start_timer() try: self.mech_browser.form = form self.mech_browser.submit(id=control.id, name=control.name, label=label, coord=coord) except Exception, e: fix_exception_name(e) raise finally: self._stop_timer() def _changed(self): self._counter += 1 self._contents = None class Link(SetattrErrorsMixin): zope.interface.implements(zope.testbrowser.interfaces.ILink) def __init__(self, link, browser): self.mech_link = link self.browser = browser self._browser_counter = self.browser._counter self._enable_setattr_errors = True def click(self): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError self.browser._start_timer() try: try: self.browser.mech_browser.follow_link(self.mech_link) except Exception, e: fix_exception_name(e) raise finally: self.browser._stop_timer() self.browser._changed() @property def url(self): return self.mech_link.absolute_url @property def text(self): return self.mech_link.text @property def tag(self): return self.mech_link.tag @property def attrs(self): return dict(self.mech_link.attrs) def __repr__(self): return "<%s text=%r url=%r>" % ( self.__class__.__name__, self.text, self.url) class Control(SetattrErrorsMixin): """A control of a form.""" zope.interface.implements(zope.testbrowser.interfaces.IControl) _enable_setattr_errors = False def __init__(self, control, form, browser): self.mech_control = control self.mech_form = form self.browser = browser self._browser_counter = self.browser._counter if self.mech_control.type == 'file': self.filename = None self.content_type = None # for some reason mechanize thinks we shouldn't be able to modify # hidden fields, but while testing it is sometimes very important if self.mech_control.type == 'hidden': self.mech_control.readonly = False # disable addition of further attributes self._enable_setattr_errors = True @property def disabled(self): return bool(getattr(self.mech_control, 'disabled', False)) @property def type(self): return getattr(self.mech_control, 'type', None) @property def name(self): return getattr(self.mech_control, 'name', None) @property def multiple(self): return bool(getattr(self.mech_control, 'multiple', False)) @apply def value(): """See zope.testbrowser.interfaces.IControl""" def fget(self): if (self.type == 'checkbox' and len(self.mech_control.items) == 1 and self.mech_control.items[0].name == 'on'): return self.mech_control.items[0].selected return self.mech_control.value def fset(self, value): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError if self.mech_control.type == 'file': self.mech_control.add_file(value, content_type=self.content_type, filename=self.filename) elif self.type == 'checkbox' and len(self.mech_control.items) == 1: self.mech_control.items[0].selected = bool(value) else: self.mech_control.value = value return property(fget, fset) def add_file(self, file, content_type, filename): if not self.mech_control.type == 'file': raise TypeError("Can't call add_file on %s controls" % self.mech_control.type) if isinstance(file, str): file = cStringIO.StringIO(file) self.mech_control.add_file(file, content_type, filename) def clear(self): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError self.mech_control.clear() def __repr__(self): return "<%s name=%r type=%r>" % ( self.__class__.__name__, self.name, self.type) class ListControl(Control): zope.interface.implements(zope.testbrowser.interfaces.IListControl) @apply def displayValue(): """See zope.testbrowser.interfaces.IListControl""" # not implemented for anything other than select; # would be nice if mechanize implemented for checkbox and radio. # attribute error for all others. def fget(self): return self.mech_control.get_value_by_label() def fset(self, value): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError self.mech_control.set_value_by_label(value) return property(fget, fset) @property def displayOptions(self): """See zope.testbrowser.interfaces.IListControl""" res = [] for item in self.mech_control.items: if not item.disabled: for label in item.get_labels(): if label.text: res.append(label.text) break else: res.append(None) return res @property def options(self): """See zope.testbrowser.interfaces.IListControl""" if (self.type == 'checkbox' and len(self.mech_control.items) == 1 and self.mech_control.items[0].name == 'on'): return [True] return [i.name for i in self.mech_control.items if not i.disabled] @property def disabled(self): if self.type == 'checkbox' and len(self.mech_control.items) == 1: return bool(getattr(self.mech_control.items[0], 'disabled', False)) return bool(getattr(self.mech_control, 'disabled', False)) @property def controls(self): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError res = [controlFactory(i, self.mech_form, self.browser) for i in self.mech_control.items] for s in res: s.__dict__['control'] = self return res def getControl(self, label=None, value=None, index=None): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError onlyOne([label, value], '"label" and "value"') if label is not None: options = self.mech_control.get_items(label=label) msg = 'label %r' % label elif value is not None: options = self.mech_control.get_items(name=value) msg = 'value %r' % value res = controlFactory( disambiguate(options, msg, index, control_form_tuple_repr), self.mech_form, self.browser) res.__dict__['control'] = self return res class SubmitControl(Control): zope.interface.implements(zope.testbrowser.interfaces.ISubmitControl) def click(self): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError try: self.browser._clickSubmit(self.mech_form, self.mech_control, (1,1)) finally: self.browser._changed() class ImageControl(Control): zope.interface.implements(zope.testbrowser.interfaces.IImageSubmitControl) def click(self, coord=(1,1)): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError try: self.browser._clickSubmit(self.mech_form, self.mech_control, coord) finally: self.browser._changed() class ItemControl(SetattrErrorsMixin): zope.interface.implements(zope.testbrowser.interfaces.IItemControl) def __init__(self, item, form, browser): self.mech_item = item self.mech_form = form self.browser = browser self._browser_counter = self.browser._counter self._enable_setattr_errors = True @property def control(self): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError res = controlFactory( self.mech_item._control, self.mech_form, self.browser) self.__dict__['control'] = res return res @property def disabled(self): return self.mech_item.disabled @apply def selected(): """See zope.testbrowser.interfaces.IControl""" def fget(self): return self.mech_item.selected def fset(self, value): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError self.mech_item.selected = value return property(fget, fset) @property def optionValue(self): return self.mech_item.attrs.get('value') def click(self): if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError self.mech_item.selected = not self.mech_item.selected def __repr__(self): return "<%s name=%r type=%r optionValue=%r selected=%r>" % ( self.__class__.__name__, self.mech_item._control.name, self.mech_item._control.type, self.optionValue, self.mech_item.selected) class Form(SetattrErrorsMixin): """HTML Form""" zope.interface.implements(zope.testbrowser.interfaces.IForm) def __init__(self, browser, form): """Initialize the Form browser - a Browser instance form - a mechanize.HTMLForm instance """ self.browser = browser self.mech_form = form self._browser_counter = self.browser._counter self._enable_setattr_errors = True @property def action(self): return self.mech_form.action @property def method(self): return self.mech_form.method @property def enctype(self): return self.mech_form.enctype @property def name(self): return self.mech_form.name @property def id(self): """See zope.testbrowser.interfaces.IForm""" return self.mech_form.attrs.get('id') def submit(self, label=None, name=None, index=None, coord=(1,1)): """See zope.testbrowser.interfaces.IForm""" if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError form = self.mech_form try: if label is not None or name is not None: intermediate, msg, available = self.browser._get_all_controls( label, name, (form,)) intermediate = [ (control, form) for (control, form) in intermediate if control.type in ('submit', 'submitbutton', 'image')] control, form = disambiguate(intermediate, msg, index, control_form_tuple_repr, available) self.browser._clickSubmit(form, control, coord) else: # JavaScript sort of submit if index is not None or coord != (1,1): raise ValueError( 'May not use index or coord without a control') request = self.mech_form._switch_click("request", mechanize.Request) self.browser._start_timer() try: try: self.browser.mech_browser.open(request) except mechanize.HTTPError, e: if self.browser.raiseHttpErrors: fix_exception_name(e) raise except Exception, e: fix_exception_name(e) raise finally: self.browser._stop_timer() finally: self.browser._changed() def getControl(self, label=None, name=None, index=None): """See zope.testbrowser.interfaces.IBrowser""" if self._browser_counter != self.browser._counter: raise zope.testbrowser.interfaces.ExpiredError intermediate, msg, available = self.browser._get_all_controls( label, name, (self.mech_form,), include_subcontrols=True) control, form = disambiguate(intermediate, msg, index, control_form_tuple_repr, available) return controlFactory(control, form, self.browser) zope.testbrowser-4.0.4/src/zope/testbrowser/connection.py0000644000000000000000000000660512225722334022055 0ustar 00000000000000############################################################################## # # Copyright (c) 2005 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Base classes sometimes useful to implement browsers """ import cStringIO import httplib import mechanize import socket import sys import zope.testbrowser.browser class Response(object): """``mechanize`` compatible response object.""" def __init__(self, content, headers, status, reason): self.content = content self.status = status self.reason = reason self.msg = httplib.HTTPMessage(cStringIO.StringIO(headers), 0) self.content_as_file = cStringIO.StringIO(self.content) def read(self, amt=None): return self.content_as_file.read(amt) def close(self): """To overcome changes in mechanize and socket in python2.5""" pass class HTTPHandler(mechanize.HTTPHandler): def _connect(self, *args, **kw): raise NotImplementedError("implement") def http_request(self, req): # look at data and set content type if req.has_data(): data = req.get_data() if isinstance(data, dict): req.add_data(data['body']) req.add_unredirected_header('Content-type', data['content-type']) return mechanize.HTTPHandler.do_request_(self, req) https_request = http_request def http_open(self, req): """Open an HTTP connection having a ``mechanize`` request.""" # Here we connect to the publisher. if sys.version_info > (2, 6) and not hasattr(req, 'timeout'): # Workaround mechanize incompatibility with Python # 2.6. See: LP #280334 req.timeout = socket._GLOBAL_DEFAULT_TIMEOUT return self.do_open(self._connect, req) https_open = http_open class MechanizeBrowser(mechanize.Browser): """Special ``mechanize`` browser using the Zope Publisher HTTP handler.""" default_schemes = ['http'] default_others = ['_http_error', '_http_default_error'] default_features = ['_redirect', '_cookies', '_referer', '_refresh', '_equiv', '_basicauth', '_digestauth'] def __init__(self, *args, **kws): inherited_handlers = ['_unknown', '_http_error', '_http_default_error', '_basicauth', '_digestauth', '_redirect', '_cookies', '_referer', '_refresh', '_equiv', '_gzip'] self.handler_classes = {"http": self._http_handler} for name in inherited_handlers: self.handler_classes[name] = mechanize.Browser.handler_classes[name] kws['request_class'] = kws.get('request_class', mechanize._request.Request) mechanize.Browser.__init__(self, *args, **kws) def _http_handler(self, *args, **kw): return NotImplementedError("Try return a sub-class of PublisherHTTPHandler here") zope.testbrowser-4.0.4/src/zope/testbrowser/cookies.txt0000644000000000000000000006233512225722334021543 0ustar 00000000000000======= Cookies ======= Getting started =============== The cookies mapping has an extended mapping interface that allows getting, setting, and deleting the cookies that the browser is remembering for the current url, or for an explicitly provided URL. >>> from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication >>> from zope.testbrowser.wsgi import Browser >>> wsgi_app = WSGITestApplication() >>> browser = Browser(wsgi_app=wsgi_app) Initially the browser does not point to a URL, and the cookies cannot be used. >>> len(browser.cookies) Traceback (most recent call last): ... RuntimeError: no request found >>> browser.cookies.keys() Traceback (most recent call last): ... RuntimeError: no request found Once you send the browser to a URL, the cookies attribute can be used. >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> len(browser.cookies) 0 >>> browser.cookies.keys() [] >>> browser.url 'http://localhost/@@/testbrowser/simple.html' >>> browser.cookies.url 'http://localhost/@@/testbrowser/simple.html' >>> import zope.testbrowser.interfaces >>> from zope.interface.verify import verifyObject >>> verifyObject(zope.testbrowser.interfaces.ICookies, browser.cookies) True Alternatively, you can use the ``forURL`` method to get another instance of the cookies mapping for the given URL. >>> len(browser.cookies.forURL('http://www.example.com')) 0 >>> browser.cookies.forURL('http://www.example.com').keys() [] >>> browser.cookies.forURL('http://www.example.com').url 'http://www.example.com' >>> browser.url 'http://localhost/@@/testbrowser/simple.html' >>> browser.cookies.url 'http://localhost/@@/testbrowser/simple.html' Here, we use a view that will make the server set cookies with the values we provide. >>> browser.open('http://localhost/set_cookie.html?name=foo&value=bar') >>> browser.headers['set-cookie'].replace(';', '') 'foo=bar' Basic Mapping Interface ======================= Now the cookies for localhost have a value. These are examples of just the basic accessor operators and methods. >>> browser.cookies['foo'] 'bar' >>> browser.cookies.keys() ['foo'] >>> browser.cookies.values() ['bar'] >>> browser.cookies.items() [('foo', 'bar')] >>> 'foo' in browser.cookies True >>> 'bar' in browser.cookies False >>> len(browser.cookies) 1 >>> print(dict(browser.cookies)) {'foo': 'bar'} As you would expect, the cookies attribute can also be used to examine cookies that have already been set in a previous request. To demonstrate this, we use another view that does not set cookies but reports on the cookies it receives from the browser. >>> browser.open('http://localhost/get_cookie.html') >>> print browser.headers.get('set-cookie') None >>> browser.contents 'foo: bar' >>> browser.cookies['foo'] 'bar' The standard mapping mutation methods and operators are also available, as seen here. >>> browser.cookies['sha'] = 'zam' >>> len(browser.cookies) 2 >>> import pprint >>> pprint.pprint(sorted(browser.cookies.items())) [('foo', 'bar'), ('sha', 'zam')] >>> browser.open('http://localhost/get_cookie.html') >>> print browser.headers.get('set-cookie') None >>> print browser.contents # server got the cookie change foo: bar sha: zam >>> browser.cookies.update({'va': 'voom', 'tweedle': 'dee'}) >>> pprint.pprint(sorted(browser.cookies.items())) [('foo', 'bar'), ('sha', 'zam'), ('tweedle', 'dee'), ('va', 'voom')] >>> browser.open('http://localhost/get_cookie.html') >>> print browser.headers.get('set-cookie') None >>> print browser.contents foo: bar sha: zam tweedle: dee va: voom >>> del browser.cookies['foo'] >>> del browser.cookies['tweedle'] >>> browser.open('http://localhost/get_cookie.html') >>> print browser.contents sha: zam va: voom Headers ======= You can see the Cookies header that will be sent to the browser in the ``header`` attribute and the repr and str. >>> browser.cookies.header 'sha=zam; va=voom' >>> browser.cookies # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE >>> str(browser.cookies) 'sha=zam; va=voom' Extended Mapping Interface ========================== ------------------------------------------ Read Methods: ``getinfo`` and ``iterinfo`` ------------------------------------------ ``getinfo`` ----------- The ``cookies`` mapping also has an extended interface to get and set extra information about each cookie. ``getinfo`` returns a dictionary. Here is the interface description. :: def getinfo(name): """returns dict of settings for the given cookie name. This includes only the following cookie values: - name (str) - value (str), - port (int or None), - domain (str), - path (str or None), - secure (bool), and - expires (datetime.datetime with pytz.UTC timezone or None), - comment (str or None), - commenturl (str or None). """ Here are some examples. >>> browser.open('http://localhost/set_cookie.html?name=foo&value=bar') >>> pprint.pprint(browser.cookies.getinfo('foo')) {'comment': None, 'commenturl': None, 'domain': 'localhost.local', 'expires': None, 'name': 'foo', 'path': '/', 'port': None, 'secure': False, 'value': 'bar'} >>> pprint.pprint(browser.cookies.getinfo('sha')) {'comment': None, 'commenturl': None, 'domain': 'localhost.local', 'expires': None, 'name': 'sha', 'path': '/', 'port': None, 'secure': False, 'value': 'zam'} >>> import datetime >>> expires = datetime.datetime(2030, 1, 1).strftime( ... '%a, %d %b %Y %H:%M:%S GMT') >>> browser.open( ... 'http://localhost/set_cookie.html?name=wow&value=wee&' ... 'expires=%s' % ... (expires,)) >>> pprint.pprint(browser.cookies.getinfo('wow')) {'comment': None, 'commenturl': None, 'domain': 'localhost.local', 'expires': datetime.datetime(2030, 1, 1, 0, 0, tzinfo=), 'name': 'wow', 'path': '/', 'port': None, 'secure': False, 'value': 'wee'} Max-age is converted to an "expires" value. >>> browser.open( ... 'http://localhost/set_cookie.html?name=max&value=min&' ... 'max-age=3000&&comment=silly+billy') >>> pprint.pprint(browser.cookies.getinfo('max')) # doctest: +ELLIPSIS {'comment': '"silly billy"', 'commenturl': None, 'domain': 'localhost.local', 'expires': datetime.datetime(..., tzinfo=), 'name': 'max', 'path': '/', 'port': None, 'secure': False, 'value': 'min'} ``iterinfo`` ------------ You can iterate over all of the information about the cookies for the current page using the ``iterinfo`` method. >>> pprint.pprint(sorted(browser.cookies.iterinfo(), ... key=lambda info: info['name'])) ... # doctest: +ELLIPSIS [{'comment': None, 'commenturl': None, 'domain': 'localhost.local', 'expires': None, 'name': 'foo', 'path': '/', 'port': None, 'secure': False, 'value': 'bar'}, {'comment': '"silly billy"', 'commenturl': None, 'domain': 'localhost.local', 'expires': datetime.datetime(..., tzinfo=), 'name': 'max', 'path': '/', 'port': None, 'secure': False, 'value': 'min'}, {'comment': None, 'commenturl': None, 'domain': 'localhost.local', 'expires': None, 'name': 'sha', 'path': '/', 'port': None, 'secure': False, 'value': 'zam'}, {'comment': None, 'commenturl': None, 'domain': 'localhost.local', 'expires': None, 'name': 'va', 'path': '/', 'port': None, 'secure': False, 'value': 'voom'}, {'comment': None, 'commenturl': None, 'domain': 'localhost.local', 'expires': datetime.datetime(2030, 1, 1, 0, 0, tzinfo=), 'name': 'wow', 'path': '/', 'port': None, 'secure': False, 'value': 'wee'}] Extended Examples ----------------- If you want to look at the cookies for another page, you can either navigate to the other page in the browser, or, as already mentioned, you can use the ``forURL`` method, which returns an ICookies instance for the new URL. >>> sorted(browser.cookies.forURL( ... 'http://localhost/inner/set_cookie.html').keys()) ['foo', 'max', 'sha', 'va', 'wow'] >>> extra_cookie = browser.cookies.forURL( ... 'http://localhost/inner/set_cookie.html') >>> extra_cookie['gew'] = 'gaw' >>> extra_cookie.getinfo('gew')['path'] '/inner' >>> sorted(extra_cookie.keys()) ['foo', 'gew', 'max', 'sha', 'va', 'wow'] >>> sorted(browser.cookies.keys()) ['foo', 'max', 'sha', 'va', 'wow'] >>> browser.open('http://localhost/inner/get_cookie.html') >>> print browser.contents # has gewgaw foo: bar gew: gaw max: min sha: zam va: voom wow: wee >>> browser.open('http://localhost/inner/path/get_cookie.html') >>> print browser.contents # has gewgaw foo: bar gew: gaw max: min sha: zam va: voom wow: wee >>> browser.open('http://localhost/get_cookie.html') >>> print browser.contents # NO gewgaw foo: bar max: min sha: zam va: voom wow: wee Here's an example of the server setting a cookie that is only available on an inner page. >>> browser.open( ... 'http://localhost/inner/path/set_cookie.html?name=big&value=kahuna' ... ) >>> browser.cookies['big'] 'kahuna' >>> browser.cookies.getinfo('big')['path'] '/inner/path' >>> browser.cookies.getinfo('gew')['path'] '/inner' >>> browser.cookies.getinfo('foo')['path'] '/' >>> print browser.cookies.forURL('http://localhost/').get('big') None ---------------------------------------- Write Methods: ``create`` and ``change`` ---------------------------------------- The basic mapping API only allows setting values. If a cookie already exists for the given name, it's value will be changed; or else a new cookie will be created for the current request's domain and a path of '/', set to last for only this browser session (a "session" cookie). To create or change cookies with different additional information, use the ``create`` and ``change`` methods, respectively. Here is an example of ``create``. >>> from pytz import UTC >>> browser.cookies.create( ... 'bling', value='blang', path='/inner', ... expires=datetime.datetime(2020, 1, 1, tzinfo=UTC), ... comment='follow swallow') >>> pprint.pprint(browser.cookies.getinfo('bling')) {'comment': 'follow%20swallow', 'commenturl': None, 'domain': 'localhost.local', 'expires': datetime.datetime(2020, 1, 1, 0, 0, tzinfo=), 'name': 'bling', 'path': '/inner', 'port': None, 'secure': False, 'value': 'blang'} In these further examples of ``create``, note that the testbrowser sends all domains to Zope, and both http and https. >>> browser.open('https://dev.example.com/inner/path/get_cookie.html') >>> browser.cookies.keys() # a different domain [] >>> browser.cookies.create('tweedle', 'dee') >>> pprint.pprint(browser.cookies.getinfo('tweedle')) {'comment': None, 'commenturl': None, 'domain': 'dev.example.com', 'expires': None, 'name': 'tweedle', 'path': '/inner/path', 'port': None, 'secure': False, 'value': 'dee'} >>> browser.cookies.create( ... 'boo', 'yah', domain='.example.com', path='/inner', secure=True) >>> pprint.pprint(browser.cookies.getinfo('boo')) {'comment': None, 'commenturl': None, 'domain': '.example.com', 'expires': None, 'name': 'boo', 'path': '/inner', 'port': None, 'secure': True, 'value': 'yah'} >>> sorted(browser.cookies.keys()) ['boo', 'tweedle'] >>> browser.open('https://dev.example.com/inner/path/get_cookie.html') >>> print browser.contents boo: yah tweedle: dee >>> browser.open( # not https, so not secure, so not 'boo' ... 'http://dev.example.com/inner/path/get_cookie.html') >>> sorted(browser.cookies.keys()) ['tweedle'] >>> print browser.contents tweedle: dee >>> browser.open( # not tweedle's domain ... 'https://prod.example.com/inner/path/get_cookie.html') >>> sorted(browser.cookies.keys()) ['boo'] >>> print browser.contents boo: yah >>> browser.open( # not tweedle's domain ... 'https://example.com/inner/path/get_cookie.html') >>> sorted(browser.cookies.keys()) ['boo'] >>> print browser.contents boo: yah >>> browser.open( # not tweedle's path ... 'https://dev.example.com/inner/get_cookie.html') >>> sorted(browser.cookies.keys()) ['boo'] >>> print browser.contents boo: yah Masking by Path --------------- The API allows creation of cookies that mask existing cookies, but it does not allow creating a cookie that will be immediately masked upon creation. Having multiple cookies with the same name for a given URL is rare, and is a pathological case for using a mapping API to work with cookies, but it is supported to some degree, as demonstrated below. Note that the Cookie RFCs (2109, 2965) specify that all matching cookies be sent to the server, but with an ordering so that more specific paths come first. We also prefer more specific domains, though the RFCs state that the ordering of cookies with the same path is indeterminate. The best-matching cookie is the one that the mapping API uses. Also note that ports, as sent by RFC 2965's Cookie2 and Set-Cookie2 headers, are parsed and stored by this API but are not used for filtering as of this writing. This is an example of making one cookie that masks another because of path. First, unless you pass an explicit path, you will be modifying the existing cookie. >>> browser.open('https://dev.example.com/inner/path/get_cookie.html') >>> print browser.contents boo: yah tweedle: dee >>> browser.cookies.getinfo('boo')['path'] '/inner' >>> browser.cookies['boo'] = 'hoo' >>> browser.cookies.getinfo('boo')['path'] '/inner' >>> browser.cookies.getinfo('boo')['secure'] True Now we mask the cookie, using the path. >>> browser.cookies.create('boo', 'boo', path='/inner/path') >>> browser.cookies['boo'] 'boo' >>> browser.cookies.getinfo('boo')['path'] '/inner/path' >>> browser.cookies.getinfo('boo')['secure'] False >>> browser.cookies['boo'] 'boo' >>> sorted(browser.cookies.keys()) ['boo', 'tweedle'] To identify the additional cookies, you can change the URL... >>> extra_cookies = browser.cookies.forURL( ... 'https://dev.example.com/inner/get_cookie.html') >>> extra_cookies['boo'] 'hoo' >>> extra_cookies.getinfo('boo')['path'] '/inner' >>> extra_cookies.getinfo('boo')['secure'] True ...or use ``iterinfo`` and pass in a name. >>> pprint.pprint(list(browser.cookies.iterinfo('boo'))) [{'comment': None, 'commenturl': None, 'domain': 'dev.example.com', 'expires': None, 'name': 'boo', 'path': '/inner/path', 'port': None, 'secure': False, 'value': 'boo'}, {'comment': None, 'commenturl': None, 'domain': '.example.com', 'expires': None, 'name': 'boo', 'path': '/inner', 'port': None, 'secure': True, 'value': 'hoo'}] An odd situation in this case is that deleting a cookie can sometimes reveal another one. >>> browser.open('https://dev.example.com/inner/path/get_cookie.html') >>> browser.cookies['boo'] 'boo' >>> del browser.cookies['boo'] >>> browser.cookies['boo'] 'hoo' Creating a cookie that will be immediately masked within the current url is not allowed. >>> browser.cookies.getinfo('tweedle')['path'] '/inner/path' >>> browser.cookies.create('tweedle', 'dum', path='/inner') ... # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ValueError: cannot set a cookie that will be hidden by another cookie for this url (https://dev.example.com/inner/path/get_cookie.html) >>> browser.cookies['tweedle'] 'dee' Masking by Domain ----------------- All of the same behavior is also true for domains. The only difference is a theoretical one: while the behavior of masking cookies via paths is defined by the relevant IRCs, it is not defined for domains. Here, we simply follow a "best match" policy. We initialize by setting some cookies for example.org. >>> browser.open('https://dev.example.org/get_cookie.html') >>> browser.cookies.keys() # a different domain [] >>> browser.cookies.create('tweedle', 'dee') >>> browser.cookies.create('boo', 'yah', domain='example.org', ... secure=True) Before we look at the examples, note that the default behavior of the cookies is to be liberal in the matching of domains. >>> browser.cookies.strict_domain_policy False According to the RFCs, a domain of 'example.com' can only be set implicitly from the server, and implies an exact match, so example.com URLs will get the cookie, but not *.example.com (i.e., dev.example.com). Real browsers vary in their behavior in this regard. The cookies collection, by default, has a looser interpretation of this, such that domains are always interpreted as effectively beginning with a ".", so dev.example.com will include a cookie from the "example.com" domain filter as if it were a ".example.com" filter. Here's an example. If we go to dev.example.org, we should only see the "tweedle" cookie if we are using strict rules. But right now we are using loose rules, so 'boo' is around too. >>> browser.open('https://dev.example.org/get_cookie.html') >>> sorted(browser.cookies) ['boo', 'tweedle'] >>> print browser.contents boo: yah tweedle: dee If we set strict_domain_policy to True, then only tweedle is included. >>> browser.cookies.strict_domain_policy = True >>> sorted(browser.cookies) ['tweedle'] >>> browser.open('https://dev.example.org/get_cookie.html') >>> print browser.contents tweedle: dee If we set the "boo" domain to ".example.org" (as it would be set under the more recent Cookie RFC if a server sent the value) then maybe we get the "boo" value again. >>> browser.cookies.forURL('https://example.org').change( ... 'boo', domain=".example.org") Traceback (most recent call last): ... ValueError: policy does not allow this cookie Whoa! Why couldn't we do that? Well, the strict_domain_policy affects what cookies we can set also. With strict rules, ".example.org" can only be set by "*.example.org" domains, *not* example.org itself. OK, we'll create a new cookie then. >>> browser.cookies.forURL('https://snoo.example.org').create( ... 'snoo', 'kums', domain=".example.org") >>> sorted(browser.cookies) ['snoo', 'tweedle'] >>> browser.open('https://dev.example.org/get_cookie.html') >>> print browser.contents snoo: kums tweedle: dee Let's set things back to the way they were. >>> del browser.cookies['snoo'] >>> browser.cookies.strict_domain_policy = False >>> browser.open('https://dev.example.org/get_cookie.html') >>> sorted(browser.cookies) ['boo', 'tweedle'] >>> print browser.contents boo: yah tweedle: dee Now back to the the examples of masking by domain. First, unless you pass an explicit domain, you will be modifying the existing cookie. >>> browser.cookies.getinfo('boo')['domain'] 'example.org' >>> browser.cookies['boo'] = 'hoo' >>> browser.cookies.getinfo('boo')['domain'] 'example.org' >>> browser.cookies.getinfo('boo')['secure'] True Now we mask the cookie, using the domain. >>> browser.cookies.create('boo', 'boo', domain='dev.example.org') >>> browser.cookies['boo'] 'boo' >>> browser.cookies.getinfo('boo')['domain'] 'dev.example.org' >>> browser.cookies.getinfo('boo')['secure'] False >>> browser.cookies['boo'] 'boo' >>> sorted(browser.cookies.keys()) ['boo', 'tweedle'] To identify the additional cookies, you can change the URL... >>> extra_cookies = browser.cookies.forURL( ... 'https://example.org/get_cookie.html') >>> extra_cookies['boo'] 'hoo' >>> extra_cookies.getinfo('boo')['domain'] 'example.org' >>> extra_cookies.getinfo('boo')['secure'] True ...or use ``iterinfo`` and pass in a name. >>> pprint.pprint(list(browser.cookies.iterinfo('boo'))) [{'comment': None, 'commenturl': None, 'domain': 'dev.example.org', 'expires': None, 'name': 'boo', 'path': '/', 'port': None, 'secure': False, 'value': 'boo'}, {'comment': None, 'commenturl': None, 'domain': 'example.org', 'expires': None, 'name': 'boo', 'path': '/', 'port': None, 'secure': True, 'value': 'hoo'}] An odd situation in this case is that deleting a cookie can sometimes reveal another one. >>> browser.open('https://dev.example.org/get_cookie.html') >>> browser.cookies['boo'] 'boo' >>> del browser.cookies['boo'] >>> browser.cookies['boo'] 'hoo' Setting a cookie with a foreign domain from the current URL is not allowed (use forURL to get around this). >>> browser.cookies.create('tweedle', 'dum', domain='locahost.local') Traceback (most recent call last): ... ValueError: current url must match given domain >>> browser.cookies['tweedle'] 'dee' Setting a cookie that will be immediately masked within the current url is also not allowed. >>> browser.cookies.getinfo('tweedle')['domain'] 'dev.example.org' >>> browser.cookies.create('tweedle', 'dum', domain='.example.org') ... # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ValueError: cannot set a cookie that will be hidden by another cookie for this url (https://dev.example.org/get_cookie.html) >>> browser.cookies['tweedle'] 'dee' ``change`` ---------- So far all of our examples in this section have centered on ``create``. ``change`` allows making changes to existing cookies. Changing expiration is a good example. >>> browser.open("http://localhost/@@/testbrowser/cookies.html") >>> browser.cookies['foo'] = 'bar' >>> browser.cookies.change('foo', expires=datetime.datetime(2021, 1, 1)) >>> browser.cookies.getinfo('foo')['expires'] datetime.datetime(2021, 1, 1, 0, 0, tzinfo=) That's the main story. Now here are some edge cases. >>> browser.cookies.change( ... 'foo', ... expires=zope.testbrowser.cookies.expiration_string( ... datetime.datetime(2020, 1, 1))) >>> browser.cookies.getinfo('foo')['expires'] datetime.datetime(2020, 1, 1, 0, 0, tzinfo=) >>> browser.cookies.forURL( ... 'http://localhost/@@/testbrowser/cookies.html').change( ... 'foo', ... expires=zope.testbrowser.cookies.expiration_string( ... datetime.datetime(2019, 1, 1))) >>> browser.cookies.getinfo('foo')['expires'] datetime.datetime(2019, 1, 1, 0, 0, tzinfo=) >>> browser.cookies['foo'] 'bar' >>> browser.cookies.change('foo', expires=datetime.datetime(1999, 1, 1)) >>> len(browser.cookies) 4 While we are at it, it is worth noting that trying to create a cookie that has already expired raises an error. >>> browser.cookies.create('foo', 'bar', ... expires=datetime.datetime(1999, 1, 1)) Traceback (most recent call last): ... AlreadyExpiredError: May not create a cookie that is immediately expired Clearing cookies ---------------- clear, clearAll, clearAllSession allow various clears of the cookies. The ``clear`` method clears all of the cookies for the current page. >>> browser.open('http://localhost/@@/testbrowser/cookies.html') >>> len(browser.cookies) 4 >>> browser.cookies.clear() >>> len(browser.cookies) 0 The ``clearAllSession`` method clears *all* session cookies (for all domains and paths, not just the current URL), as if the browser had been restarted. >>> browser.cookies.clearAllSession() >>> len(browser.cookies) 0 The ``clearAll`` removes all cookies for the browser. >>> browser.cookies.clearAll() >>> len(browser.cookies) 0 Note that explicitly setting a Cookie header is an error if the ``cookies`` mapping has any values; and adding a new cookie to the ``cookies`` mapping is an error if the Cookie header is already set. This is to prevent hard-to- diagnose intermittent errors when one header or the other wins. >>> browser.cookies['boo'] = 'yah' >>> browser.addHeader('Cookie', 'gee=gaw') Traceback (most recent call last): ... ValueError: cookies are already set in `cookies` attribute >>> browser.cookies.clearAll() >>> browser.addHeader('Cookie', 'gee=gaw') >>> browser.cookies['fee'] = 'fi' Traceback (most recent call last): ... ValueError: cookies are already set in `Cookie` header zope.testbrowser-4.0.4/src/zope/testbrowser/__init__.py0000644000000000000000000000125512225722334021451 0ustar 00000000000000############################################################################## # # Copyright (c) 2005 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Browser Simulator for Functional DocTests """ zope.testbrowser-4.0.4/src/zope/testbrowser/README.txt0000644000000000000000000013115012225722334021034 0ustar 00000000000000====================== Detailed Documentation ====================== Different Browsers ------------------ HTTP Browser ~~~~~~~~~~~~ The ``zope.testbrowser.browser`` module exposes a ``Browser`` class that simulates a web browser similar to Mozilla Firefox or IE. >>> from zope.testbrowser.browser import Browser >>> browser = Browser() This version of the browser object can be used to access any web site just as you would do using a normal web browser. WSGI Test Browser ~~~~~~~~~~~~~~~~~ General usage +++++++++++++ There is also a special version of the ``Browser`` class which uses `WebTest`_ and can be used to do functional testing of WSGI applications. It can be imported from ``zope.testbrowser.wsgi``: >>> from zope.testbrowser.wsgi import Browser >>> from zope.testbrowser.tests.test_wsgi import demo_app >>> browser = Browser('http://localhost/', wsgi_app=demo_app) >>> print browser.contents Hello world! ... .. _`WebTest`: http://pypi.python.org/pypi/WebTest To use this browser you have to: * use the `wsgi` extra of the ``zope.testbrowser`` egg, You can also use it with zope layers by: * write a subclass of ``zope.testbrowser.wsgi.Layer`` and override the ``make_wsgi_app`` method, * use an instance of the class as the test layer of your test. Example: >>> import zope.testbrowser.wsgi >>> class SimpleLayer(zope.testbrowser.wsgi.Layer): ... def make_wsgi_app(self): ... return simple_app Where ``simple_app`` is the callable of your WSGI application. Testing a Zope 2/Zope 3/Bluebream WSGI application ++++++++++++++++++++++++++++++++++++++++++++++++++ When testing a Zope 2/Zope 3/Bluebream WSGI application you should wrap your WSGI application under test into ``zope.testbrowser.wsgi.AuthorizationMiddleware`` as all these application servers expect basic authentication headers to be base64 encoded. This middleware handles this for you. Example when using the layer: >>> import zope.testbrowser.wsgi >>> class ZopeSimpleLayer(zope.testbrowser.wsgi.Layer): ... def make_wsgi_app(self): ... return zope.testbrowser.wsgi.AuthorizationMiddleware(simple_app) There is also a BrowserLayer in `zope.app.wsgi.testlayer`_ which does this for you and includes a ``TransactionMiddleware``, too, which could be handy when testing a ZODB based application. .. _`zope.app.wsgi.testlayer` : http://pypi.python.org/pypi/zope.app.wsgi Bowser Usage ------------ We will test this browser against a WSGI test application: >>> from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication >>> wsgi_app = WSGITestApplication() An initial page to load can be passed to the ``Browser`` constructor: >>> browser = Browser('http://localhost/@@/testbrowser/simple.html', wsgi_app=wsgi_app) >>> browser.url 'http://localhost/@@/testbrowser/simple.html' The browser can send arbitrary headers; this is helpful for setting the "Authorization" header or a language value, so that your tests format values the way you expect in your tests, if you rely on zope.i18n locale-based formatting or a similar approach. >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw') >>> browser.addHeader('Accept-Language', 'en-US') An existing browser instance can also `open` web pages: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.url 'http://localhost/@@/testbrowser/simple.html' Once you have opened a web page initially, best practice for writing testbrowser doctests suggests using 'click' to navigate further (as discussed below), except in unusual circumstances. The test browser complies with the IBrowser interface; see ``zope.testbrowser.interfaces`` for full details on the interface. >>> from zope.testbrowser import interfaces >>> from zope.interface.verify import verifyObject >>> verifyObject(interfaces.IBrowser, browser) True Page Contents ------------- The contents of the current page are available: >>> print browser.contents Simple Page

Simple Page

Making assertions about page contents is easy. >>> '

Simple Page

' in browser.contents True Utilizing the doctest facilities, it also possible to do: >>> browser.contents '...

Simple Page

...' Note: Unfortunately, ellipsis (...) cannot be used at the beginning of the output (this is a limitation of doctest). Checking for HTML ----------------- Not all URLs return HTML. Of course our simple page does: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.isHtml True But if we load an image (or other binary file), we do not get HTML: >>> browser.open('http://localhost/@@/testbrowser/zope3logo.gif') >>> browser.isHtml False HTML Page Title ---------------- Another useful helper property is the title: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.title 'Simple Page' If a page does not provide a title, it is simply ``None``: >>> browser.open('http://localhost/@@/testbrowser/notitle.html') >>> browser.title However, if the output is not HTML, then an error will occur trying to access the title: >>> browser.open('http://localhost/@@/testbrowser/zope3logo.gif') >>> browser.title Traceback (most recent call last): ... BrowserStateError: not viewing HTML Headers ------- As you can see, the `contents` of the browser does not return any HTTP headers. The headers are accessible via a separate attribute, which is an ``httplib.HTTPMessage`` instance (httplib is a part of Python's standard library): >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.headers The headers can be accessed as a string: >>> print browser.headers Status: 200 OK Content-Length: 123 Content-Type: text/html;charset=utf-8 Or as a mapping: >>> browser.headers['content-type'] 'text/html;charset=utf-8' Cookies ------- When a Set-Cookie header is available, it can be found in the headers, as seen above. Here, we use a view that will make the server set cookies with the values we provide. >>> browser.open('http://localhost/set_cookie.html?name=foo&value=bar') >>> browser.headers['set-cookie'].replace(';', '') 'foo=bar' It is also available in the browser's ``cookies`` attribute. This is an extended mapping interface that allows getting, setting, and deleting the cookies that the browser is remembering *for the current url*. Here are a few examples. >>> browser.cookies['foo'] 'bar' >>> browser.cookies.keys() ['foo'] >>> browser.cookies.values() ['bar'] >>> browser.cookies.items() [('foo', 'bar')] >>> 'foo' in browser.cookies True >>> 'bar' in browser.cookies False >>> len(browser.cookies) 1 >>> print(dict(browser.cookies)) {'foo': 'bar'} >>> browser.cookies['sha'] = 'zam' >>> len(browser.cookies) 2 >>> sorted(browser.cookies.items()) [('foo', 'bar'), ('sha', 'zam')] >>> browser.open('http://localhost/get_cookie.html') >>> print browser.headers.get('set-cookie') None >>> print browser.contents # server got the cookie change foo: bar sha: zam >>> sorted(browser.cookies.items()) [('foo', 'bar'), ('sha', 'zam')] >>> browser.cookies.clearAll() >>> len(browser.cookies) 0 Many more examples, and a discussion of the additional methods available, can be found in cookies.txt. Navigation and Link Objects --------------------------- If you want to simulate clicking on a link, get the link and `click` on it. In the `navigate.html` file there are several links set up to demonstrate the capabilities of the link objects and their `click` method. The simplest way to get a link is via the anchor text. In other words the text you would see in a browser (text and url searches are substring searches): >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...Link Text...' >>> link = browser.getLink('Link Text') >>> link Link objects comply with the ILink interface. >>> verifyObject(interfaces.ILink, link) True Links expose several attributes for easy access. >>> link.text 'Link Text' >>> link.tag # links can also be image maps. 'a' >>> link.url # it's normalized 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' >>> link.attrs {'href': 'navigate.html?message=By+Link+Text'} Links can be "clicked" and the browser will navigate to the referenced URL. >>> link.click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' >>> browser.contents '...Message: By Link Text...' When finding a link by its text, whitespace is normalized. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...> Link Text \n with Whitespace\tNormalization (and parens) >> link = browser.getLink('Link Text with Whitespace Normalization ' ... '(and parens)') >>> link >>> link.text 'Link Text with Whitespace Normalization (and parens)' >>> link.click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text+with+Normalization' >>> browser.contents '...Message: By Link Text with Normalization...' When a link text matches more than one link, by default the first one is chosen. You can, however, specify the index of the link and thus retrieve a later matching link: >>> browser.getLink('Link Text') >>> browser.getLink('Link Text', index=1) Note that clicking a link object after its browser page has expired will generate an error. >>> link.click() Traceback (most recent call last): ... ExpiredError You can also find the link by its URL, >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...Using the URL...' >>> browser.getLink(url='?message=By+URL').click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+URL' >>> browser.contents '...Message: By URL...' or its id: >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...By Anchor Id...' >>> browser.getLink(id='anchorid').click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Id' >>> browser.contents '...Message: By Id...' You thought we were done here? Not so quickly. The `getLink` method also supports image maps, though not by specifying the coordinates, but using the area's id: >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> link = browser.getLink(id='zope3') >>> link.tag 'area' >>> link.click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=Zope+3+Name' >>> browser.contents '...Message: Zope 3 Name...' Getting a nonexistent link raises an exception. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.getLink('This does not exist') Traceback (most recent call last): ... LinkNotFoundError A convenience method is provided to follow links; this uses the same arguments as `getLink`, but clicks on the link instead of returning the link object. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...Link Text...' >>> browser.follow('Link Text') >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' >>> browser.contents '...Message: By Link Text...' >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.follow(url='?message=By+URL') >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+URL' >>> browser.contents '...Message: By URL...' >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.follow(id='zope3') >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=Zope+3+Name' >>> browser.contents '...Message: Zope 3 Name...' Attempting to follow links that don't exist raises the same exception as asking for the link object: >>> browser.follow('This does not exist') Traceback (most recent call last): ... LinkNotFoundError Other Navigation ---------------- Like in any normal browser, you can reload a page: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.url 'http://localhost/@@/testbrowser/simple.html' >>> browser.reload() >>> browser.url 'http://localhost/@@/testbrowser/simple.html' You can also go back: >>> browser.open('http://localhost/@@/testbrowser/notitle.html') >>> browser.url 'http://localhost/@@/testbrowser/notitle.html' >>> browser.goBack() >>> browser.url 'http://localhost/@@/testbrowser/simple.html' Controls -------- One of the most important features of the browser is the ability to inspect and fill in values for the controls of input forms. To do so, let's first open a page that has a bunch of controls: >>> browser.open('http://localhost/@@/testbrowser/controls.html') Obtaining a Control ~~~~~~~~~~~~~~~~~~~ You look up browser controls with the 'getControl' method. The default first argument is 'label', and looks up the form on the basis of any associated label. >>> control = browser.getControl('Text Control') >>> control >>> browser.getControl(label='Text Control') # equivalent If you request a control that doesn't exist, the code raises a LookupError: >>> browser.getControl('Does Not Exist') Traceback (most recent call last): ... LookupError: label 'Does Not Exist' available items: ... If you request a control with an ambiguous lookup, the code raises an AmbiguityError. >>> browser.getControl('Ambiguous Control') Traceback (most recent call last): ... AmbiguityError: label 'Ambiguous Control' matches: This is also true if an option in a control is ambiguous in relation to the control itself. >>> browser.getControl('Sub-control Ambiguity') Traceback (most recent call last): ... AmbiguityError: label 'Sub-control Ambiguity' matches: Ambiguous controls may be specified using an index value. We use the control's value attribute to show the two controls; this attribute is properly introduced below. >>> browser.getControl('Ambiguous Control', index=0) >>> browser.getControl('Ambiguous Control', index=0).value 'First' >>> browser.getControl('Ambiguous Control', index=1).value 'Second' >>> browser.getControl('Sub-control Ambiguity', index=0) >>> browser.getControl('Sub-control Ambiguity', index=1).optionValue 'ambiguous' >>> browser.getControl('Sub-control Ambiguity', index=2) Traceback (most recent call last): ... LookupError: label 'Sub-control Ambiguity' Index 2 out of range, available choices are 0...1 0: 1: Label searches are against stripped, whitespace-normalized, no-tag versions of the text. Text applied to searches is also stripped and whitespace normalized. The search finds results if the text search finds the whole words of your text in a label. Thus, for instance, a search for 'Add' will match the label 'Add a Client' but not 'Address'. Case is honored. >>> browser.getControl('Label Needs Whitespace Normalization') >>> browser.getControl('label needs whitespace normalization') Traceback (most recent call last): ... LookupError: label 'label needs whitespace normalization' ... >>> browser.getControl(' Label Needs Whitespace ') >>> browser.getControl('Whitespace') >>> browser.getControl('hitespace') Traceback (most recent call last): ... LookupError: label 'hitespace' ... >>> browser.getControl('[non word characters should not confuse]') Multiple labels can refer to the same control (simply because that is possible in the HTML 4.0 spec). >>> browser.getControl('Multiple labels really') >>> browser.getControl('really are possible') >>> browser.getControl('really') # OK: ambiguous labels, but not ambiguous control A label can be connected with a control using the 'for' attribute and also by containing a control. >>> browser.getControl( ... 'Labels can be connected by containing their respective fields') Get also accepts one other search argument, 'name'. Only one of 'label' and 'name' may be used at a time. The 'name' keyword searches form field names. >>> browser.getControl(name='text-value') >>> browser.getControl(name='ambiguous-control-name') Traceback (most recent call last): ... AmbiguityError: name 'ambiguous-control-name' matches: >>> browser.getControl(name='does-not-exist') Traceback (most recent call last): ... LookupError: name 'does-not-exist' available items: ... >>> browser.getControl(name='ambiguous-control-name', index=1).value 'Second' Combining 'label' and 'name' raises a ValueError, as does supplying neither of them. >>> browser.getControl(label='Ambiguous Control', name='ambiguous-control-name') Traceback (most recent call last): ... ValueError: Supply one and only one of "label" and "name" as arguments >>> browser.getControl() Traceback (most recent call last): ... ValueError: Supply one and only one of "label" and "name" as arguments Radio and checkbox fields are unusual in that their labels and names may point to different objects: names point to logical collections of radio buttons or checkboxes, but labels may only be used for individual choices within the logical collection. This means that obtaining a radio button by label gets a different object than obtaining the radio collection by name. Select options may also be searched by label. >>> browser.getControl(name='radio-value') >>> browser.getControl('Zwei') >>> browser.getControl('One') >>> browser.getControl('Tres') Characteristics of controls and subcontrols are discussed below. Control Objects ~~~~~~~~~~~~~~~ Controls provide IControl. >>> ctrl = browser.getControl('Text Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True They have several useful attributes: - the name as which the control is known to the form: >>> ctrl.name 'text-value' - the value of the control, which may also be set: >>> ctrl.value 'Some Text' >>> ctrl.value = 'More Text' >>> ctrl.value 'More Text' - the type of the control: >>> ctrl.type 'text' - a flag describing whether the control is disabled: >>> ctrl.disabled False - and a flag to tell us whether the control can have multiple values: >>> ctrl.multiple False Additionally, controllers for select, radio, and checkbox provide IListControl. These fields have four other attributes and an additional method: >>> ctrl = browser.getControl('Multiple Select Control') >>> ctrl >>> ctrl.disabled False >>> ctrl.multiple True >>> verifyObject(interfaces.IListControl, ctrl) True - 'options' lists all available value options. >>> ctrl.options ['1', '2', '3'] - 'displayOptions' lists all available options by label. The 'label' attribute on an option has precedence over its contents, which is why our last option is 'Third' in the display. >>> ctrl.displayOptions ['Un', 'Deux', 'Third'] - 'displayValue' lets you get and set the displayed values of the control of the select box, rather than the actual values. >>> ctrl.value [] >>> ctrl.displayValue [] >>> ctrl.displayValue = ['Un', 'Deux'] >>> ctrl.displayValue ['Un', 'Deux'] >>> ctrl.value ['1', '2'] - 'controls' gives you a list of the subcontrol objects in the control (subcontrols are discussed below). >>> ctrl.controls [, , ] - The 'getControl' method lets you get subcontrols by their label or their value. >>> ctrl.getControl('Un') >>> ctrl.getControl('Deux') >>> ctrl.getControl('Trois') # label attribute >>> ctrl.getControl('Third') # contents >>> browser.getControl('Third') # ambiguous in the browser, so useful Traceback (most recent call last): ... AmbiguityError: label 'Third' matches: Finally, submit controls provide ISubmitControl, and image controls provide IImageSubmitControl, which extents ISubmitControl. These both simply add a 'click' method. For image submit controls, you may also provide a coordinates argument, which is a tuple of (x, y). These submit the forms, and are demonstrated below as we examine each control individually. ItemControl Objects ~~~~~~~~~~~~~~~~~~~ As introduced briefly above, using labels to obtain elements of a logical radio button or checkbox collection returns item controls, which are parents. Manipulating the value of these controls affects the parent control. >>> browser.getControl(name='radio-value').value ['2'] >>> browser.getControl('Zwei').optionValue # read-only. '2' >>> browser.getControl('Zwei').selected True >>> verifyObject(interfaces.IItemControl, browser.getControl('Zwei')) True >>> browser.getControl('Ein').selected = True >>> browser.getControl('Ein').selected True >>> browser.getControl('Zwei').selected False >>> browser.getControl(name='radio-value').value ['1'] >>> browser.getControl('Ein').selected = False >>> browser.getControl(name='radio-value').value [] >>> browser.getControl('Zwei').selected = True Checkbox collections behave similarly, as shown below. Controls with subcontrols-- Various Controls ~~~~~~~~~~~~~~~~ The various types of controls are demonstrated here. - Text Control The text control we already introduced above. - Password Control >>> ctrl = browser.getControl('Password Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value 'Password' >>> ctrl.value = 'pass now' >>> ctrl.value 'pass now' >>> ctrl.disabled False >>> ctrl.multiple False - Hidden Control >>> ctrl = browser.getControl(name='hidden-value') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value 'Hidden' >>> ctrl.value = 'More Hidden' >>> ctrl.disabled False >>> ctrl.multiple False - Text Area Control >>> ctrl = browser.getControl('Text Area Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value ' Text inside\n area!\n ' >>> ctrl.value = 'A lot of\n text.' >>> ctrl.disabled False >>> ctrl.multiple False - File Control File controls are used when a form has a file-upload field. To specify data, call the add_file method, passing: - A file-like object - a content type, and - a file name >>> ctrl = browser.getControl('File Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value is None True >>> import cStringIO >>> ctrl.add_file(cStringIO.StringIO('File contents'), ... 'text/plain', 'test.txt') The file control (like the other controls) also knows if it is disabled or if it can have multiple values. >>> ctrl.disabled False >>> ctrl.multiple False - Selection Control (Single-Valued) >>> ctrl = browser.getControl('Single Select Control') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value ['1'] >>> ctrl.value = ['2'] >>> ctrl.disabled False >>> ctrl.multiple False >>> ctrl.options ['1', '2', '3'] >>> ctrl.displayOptions ['Uno', 'Dos', 'Third'] >>> ctrl.displayValue ['Dos'] >>> ctrl.displayValue = ['Tres'] >>> ctrl.displayValue ['Third'] >>> ctrl.displayValue = ['Dos'] >>> ctrl.displayValue ['Dos'] >>> ctrl.displayValue = ['Third'] >>> ctrl.displayValue ['Third'] >>> ctrl.value ['3'] - Selection Control (Multi-Valued) This was already demonstrated in the introduction to control objects above. - Checkbox Control (Single-Valued; Unvalued) >>> ctrl = browser.getControl(name='single-unvalued-checkbox-value') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value True >>> ctrl.value = False >>> ctrl.disabled False >>> ctrl.multiple True >>> ctrl.options [True] >>> ctrl.displayOptions ['Single Unvalued Checkbox'] >>> ctrl.displayValue [] >>> verifyObject( ... interfaces.IItemControl, ... browser.getControl('Single Unvalued Checkbox')) True >>> browser.getControl('Single Unvalued Checkbox').optionValue 'on' >>> browser.getControl('Single Unvalued Checkbox').selected False >>> ctrl.displayValue = ['Single Unvalued Checkbox'] >>> ctrl.displayValue ['Single Unvalued Checkbox'] >>> browser.getControl('Single Unvalued Checkbox').selected True >>> browser.getControl('Single Unvalued Checkbox').selected = False >>> browser.getControl('Single Unvalued Checkbox').selected False >>> ctrl.displayValue [] >>> browser.getControl( ... name='single-disabled-unvalued-checkbox-value').disabled True - Checkbox Control (Single-Valued, Valued) >>> ctrl = browser.getControl(name='single-valued-checkbox-value') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value ['1'] >>> ctrl.value = [] >>> ctrl.disabled False >>> ctrl.multiple True >>> ctrl.options ['1'] >>> ctrl.displayOptions ['Single Valued Checkbox'] >>> ctrl.displayValue [] >>> verifyObject( ... interfaces.IItemControl, ... browser.getControl('Single Valued Checkbox')) True >>> browser.getControl('Single Valued Checkbox').selected False >>> browser.getControl('Single Valued Checkbox').optionValue '1' >>> ctrl.displayValue = ['Single Valued Checkbox'] >>> ctrl.displayValue ['Single Valued Checkbox'] >>> browser.getControl('Single Valued Checkbox').selected True >>> browser.getControl('Single Valued Checkbox').selected = False >>> browser.getControl('Single Valued Checkbox').selected False >>> ctrl.displayValue [] - Checkbox Control (Multi-Valued) >>> ctrl = browser.getControl(name='multi-checkbox-value') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value ['1', '3'] >>> ctrl.value = ['1', '2'] >>> ctrl.disabled False >>> ctrl.multiple True >>> ctrl.options ['1', '2', '3'] >>> ctrl.displayOptions ['One', 'Two', 'Three'] >>> ctrl.displayValue ['One', 'Two'] >>> ctrl.displayValue = ['Two'] >>> ctrl.value ['2'] >>> browser.getControl('Two').optionValue '2' >>> browser.getControl('Two').selected True >>> verifyObject(interfaces.IItemControl, browser.getControl('Two')) True >>> browser.getControl('Three').selected = True >>> browser.getControl('Three').selected True >>> browser.getControl('Two').selected True >>> ctrl.value ['2', '3'] >>> browser.getControl('Two').selected = False >>> ctrl.value ['3'] >>> browser.getControl('Three').selected = False >>> ctrl.value [] - Radio Control This is how you get a radio button based control: >>> ctrl = browser.getControl(name='radio-value') This shows the existing value of the control, as it was in the HTML received from the server: >>> ctrl.value ['2'] We can then unselect it: >>> ctrl.value = [] >>> ctrl.value [] We can also reselect it: >>> ctrl.value = ['2'] >>> ctrl.value ['2'] displayValue shows the text the user would see next to the control: >>> ctrl.displayValue ['Zwei'] This is just unit testing: >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.disabled False >>> ctrl.multiple False >>> ctrl.options ['1', '2', '3'] >>> ctrl.displayOptions ['Ein', 'Zwei', 'Drei'] >>> ctrl.displayValue = ['Ein'] >>> ctrl.value ['1'] >>> ctrl.displayValue ['Ein'] The radio control subcontrols were illustrated above. - Image Control >>> ctrl = browser.getControl(name='image-value') >>> ctrl >>> verifyObject(interfaces.IImageSubmitControl, ctrl) True >>> ctrl.value '' >>> ctrl.disabled False >>> ctrl.multiple False - Submit Control >>> ctrl = browser.getControl(name='submit-value') >>> ctrl >>> browser.getControl('Submit This') # value of submit button is a label >>> browser.getControl('Standard Submit Control') # label tag is legal >>> browser.getControl('Submit') # multiple labels, but same control >>> verifyObject(interfaces.ISubmitControl, ctrl) True >>> ctrl.value 'Submit This' >>> ctrl.disabled False >>> ctrl.multiple False Using Submitting Controls ~~~~~~~~~~~~~~~~~~~~~~~~~ Both the submit and image type should be clickable and submit the form: >>> browser.getControl('Text Control').value = 'Other Text' >>> browser.getControl('Submit').click() >>> print browser.contents ... Other Text ... Submit This ... Note that if you click a submit object after the associated page has expired, you will get an error. >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> ctrl = browser.getControl('Submit') >>> ctrl.click() >>> ctrl.click() Traceback (most recent call last): ... ExpiredError All the above also holds true for the image control: >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> browser.getControl('Text Control').value = 'Other Text' >>> browser.getControl(name='image-value').click() >>> print browser.contents ... Other Text ... 1 1 ... >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> ctrl = browser.getControl(name='image-value') >>> ctrl.click() >>> ctrl.click() Traceback (most recent call last): ... ExpiredError But when sending an image, you can also specify the coordinate you clicked: >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> browser.getControl(name='image-value').click((50,25)) >>> print browser.contents ... 50 25 ... Pages Without Controls ~~~~~~~~~~~~~~~~~~~~~~ What would happen if we tried to look up a control on a page that has none? >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.getControl('anything') Traceback (most recent call last): ... LookupError: label 'anything' (there are no form items in the HTML) Forms ----- Because pages can have multiple forms with like-named controls, it is sometimes necessary to access forms by name or id. The browser's `forms` attribute can be used to do so. The key value is the form's name or id. If more than one form has the same name or id, the first one will be returned. >>> browser.open('http://localhost/@@/testbrowser/forms.html') >>> form = browser.getForm(name='one') Form instances conform to the IForm interface. >>> verifyObject(interfaces.IForm, form) True The form exposes several attributes related to forms: - The name of the form: >>> form.name 'one' - The id of the form: >>> form.id '1' - The action (target URL) when the form is submitted: >>> form.action 'http://localhost/@@/testbrowser/forms.html' - The method (HTTP verb) used to transmit the form data: >>> form.method 'GET' Besides those attributes, you have also a couple of methods. Like for the browser, you can get control objects, but limited to the current form... >>> form.getControl(name='text-value') ...and submit the form. >>> form.submit('Submit') >>> print browser.contents ... First Text ... Submitting also works without specifying a control, as shown below, which is it's primary reason for existing in competition with the control submission discussed above. Now let me show you briefly that looking up forms is sometimes important. In the `forms.html` template, we have four forms all having a text control named `text-value`. Now, if I use the browser's `get` method, >>> browser.getControl(name='text-value') Traceback (most recent call last): ... AmbiguityError: name 'text-value' matches: >>> browser.getControl('Text Control') Traceback (most recent call last): ... AmbiguityError: label 'Text Control' matches: I'll always get an ambiguous form field. I can use the index argument, or with the `getForm` method I can disambiguate by searching only within a given form: >>> form = browser.getForm('2') >>> form.getControl(name='text-value').value 'Second Text' >>> form.submit('Submit') >>> browser.contents '...Second Text...' >>> form = browser.getForm('2') >>> form.getControl('Submit').click() >>> browser.contents '...Second Text...' >>> browser.getForm('3').getControl('Text Control').value 'Third Text' The last form on the page does not have a name, an id, or a submit button. Working with it is still easy, thanks to a index attribute that guarantees order. (Forms without submit buttons are sometimes useful for JavaScript.) >>> form = browser.getForm(index=3) >>> form.submit() >>> browser.contents '...Fourth Text...Submitted without the submit button....' If a form is requested that does not exists, an exception will be raised. >>> form = browser.getForm('does-not-exist') Traceback (most recent call last): LookupError If the HTML page contains only one form, no arguments to `getForm` are needed: >>> oneform = Browser(wsgi_app=wsgi_app) >>> oneform.open('http://localhost/@@/testbrowser/oneform.html') >>> form = oneform.getForm() If the HTML page contains more than one form, `index` is needed to disambiguate if no other arguments are provided: >>> browser.getForm() Traceback (most recent call last): ValueError: if no other arguments are given, index is required. Submitting a posts body directly -------------------------------- In addition to the open method, zope.testbrowser.testing.Browser has a ``post`` method that allows a request body to be supplied. This method is particularly helpful when testing Ajax methods. Let's visit a page that echos some interesting values from it's request: >>> browser.open('http://localhost/echo.html') >>> print browser.contents HTTP_ACCEPT_LANGUAGE: en-US HTTP_CONNECTION: close HTTP_HOST: localhost HTTP_USER_AGENT: Python-urllib/2.4 PATH_INFO: /echo.html REQUEST_METHOD: GET Body: '' Now, we'll try a post. The post method takes a URL, a data string, and an optional content type. If we just pass a string, then a URL-encoded query string is assumed: >>> browser.post('http://localhost/echo.html', 'x=1&y=2') >>> print browser.contents CONTENT_LENGTH: 7 CONTENT_TYPE: application/x-www-form-urlencoded HTTP_ACCEPT_LANGUAGE: en-US HTTP_CONNECTION: close HTTP_HOST: localhost HTTP_USER_AGENT: Python-urllib/2.4 PATH_INFO: /echo.html REQUEST_METHOD: POST x: 1 y: 2 Body: '' The body is empty because it is consumed to get form data. We can pass a content-type explicitly: >>> browser.post('http://localhost/echo.html', ... '{"x":1,"y":2}', 'application/x-javascript') >>> print browser.contents CONTENT_LENGTH: 13 CONTENT_TYPE: application/x-javascript HTTP_ACCEPT_LANGUAGE: en-US HTTP_CONNECTION: close HTTP_HOST: localhost HTTP_USER_AGENT: Python-urllib/2.4 PATH_INFO: /echo.html REQUEST_METHOD: POST Body: '{"x":1,"y":2}' Here, the body is left in place because it isn't form data. Performance Testing ------------------- Browser objects keep up with how much time each request takes. This can be used to ensure a particular request's performance is within a tolerable range. Be very careful using raw seconds, cross-machine differences can be huge, pystones is usually a better choice. >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.lastRequestSeconds < 10 # really big number for safety True >>> browser.lastRequestPystones < 10000 # really big number for safety True Handling Errors --------------- Often WSGI middleware or the application itself gracefully handle application errors, such as invalid URLs: >>> browser.open('http://localhost/invalid') Traceback (most recent call last): ... HTTPError: HTTP Error 404: Not Found Note that the above error was thrown by ``mechanize`` and not by the application. For debugging purposes, however, it can be very useful to see the original exception caused by the application. In those cases you can set the ``handleErrors`` property of the browser to ``False``. It is defaulted to ``True``: >>> browser.handleErrors True So when we tell the application not to handle the errors, >>> browser.handleErrors = False we get a different, internal error from the application: >>> browser.open('http://localhost/invalid') Traceback (most recent call last): ... NotFound: /invalid NB: Setting the handleErrors attribute to False will only change anything if the WSGI application obeys the wsgi.handleErrors or paste.throw_errors WSGI environment variables. i.e. it does not catch and handle the original exception when these are set appropriately. When the testbrowser is raising HttpErrors, the errors still hit the test. Sometimes we don't want that to happen, in situations where there are edge cases that will cause the error to be predictably but infrequently raised. Time is a primary cause of this. To get around this, one can set the raiseHttpErrors to False. >>> browser.handleErrors = True >>> browser.raiseHttpErrors = False This will cause HttpErrors not to propagate. >>> browser.open('http://localhost/invalid') The headers are still there, though. >>> '404 Not Found' in str(browser.headers) True If we don't handle the errors, and allow internal ones to propagate, however, this flag doesn't affect things. >>> browser.handleErrors = False >>> browser.open('http://localhost/invalid') Traceback (most recent call last): ... NotFound: /invalid >>> browser.raiseHttpErrors = True Hand-Holding ------------ Instances of the various objects ensure that users don't set incorrect instance attributes accidentally. >>> browser.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Browser' object has no attribute 'nonexistant' >>> form.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Form' object has no attribute 'nonexistant' >>> control.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Control' object has no attribute 'nonexistant' >>> link.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Link' object has no attribute 'nonexistant' HTTPS support ------------- Depending on the scheme of the request the variable wsgi.url_scheme will be set correctly on the request: >>> browser.open('http://localhost/echo_one.html?var=wsgi.url_scheme') >>> print browser.contents 'http' >>> browser.open('https://localhost/echo_one.html?var=wsgi.url_scheme') >>> print browser.contents 'https' see http://www.python.org/dev/peps/pep-3333/ for details. zope.testbrowser-4.0.4/src/zope/testbrowser/over_the_wire.txt0000644000000000000000000000240712225722334022742 0ustar 00000000000000================================= Using testbrowser On the Internet ================================= The ``zope.testbrowser`` module exposes a ``Browser`` class that simulates a web browser similar to Mozilla Firefox or IE. >>> from zope.testbrowser.browser import Browser >>> browser = Browser() It can send arbitrary headers; this is helpful for setting the language value, so that your tests format values the way you expect in your tests, if you rely on zope.i18n locale-based formatting or a similar approach. >>> browser.addHeader('Accept-Language', 'en-US') The browser can `open` web pages: >>> # This is tricky, since in Germany I am forwarded to google.de usually; >>> # The `ncr` forces to really go to google.com. >>> browser.open('http://google.com/ncr') >>> browser.url 'http://www.google.com/' >>> 'html' in browser.contents.lower() True We'll put some text in the query box... >>> browser.getControl(name='q').value = 'zope.testbrowser' ...and then click the search button. >>> browser.getControl('Google Search').click() Traceback (most recent call last): ... RobotExclusionError: HTTP Error 403: request disallowed by robots.txt Oops! Google doesn't let robots use their search engine. Oh well. zope.testbrowser-4.0.4/src/zope/testbrowser/cookies.py0000644000000000000000000003164012225722334021347 0ustar 00000000000000############################################################################## # # Copyright (c) 2008 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## import Cookie import datetime import time import urllib import urlparse import UserDict import mechanize import pytz import zope.interface from zope.testbrowser import interfaces # Cookies class helpers class _StubHTTPMessage(object): def __init__(self, cookies): self._cookies = cookies def getheaders(self, name): if name.lower() != 'set-cookie': return [] else: return self._cookies class _StubResponse(object): def __init__(self, cookies): self.message = _StubHTTPMessage(cookies) def info(self): return self.message def expiration_string(expires): # this is not protected so usable in tests. if isinstance(expires, datetime.datetime): if expires.tzinfo is not None: expires = expires.astimezone(pytz.UTC) expires = expires.strftime('%a, %d %b %Y %H:%M:%S GMT') return expires if getattr(property, 'setter', None) is None: # hack on Python 2.6 spelling of the only part we use here class property(property): __slots__ = () def setter(self, f): return property(self.fget, f, self.fdel, self.__doc__) # end Cookies class helpers class Cookies(object, UserDict.DictMixin): """Cookies for mechanize browser. """ zope.interface.implements(interfaces.ICookies) def __init__(self, mech_browser, url=None): self.mech_browser = mech_browser self._url = url for handler in self.mech_browser.handlers: if getattr(handler, 'cookiejar', None) is not None: self._jar = handler.cookiejar break else: raise RuntimeError('no cookiejar found') @property def strict_domain_policy(self): policy = self._jar.get_policy() flags = (policy.DomainStrictNoDots | policy.DomainRFC2965Match | policy.DomainStrictNonDomain) return policy.strict_ns_domain & flags == flags @strict_domain_policy.setter def strict_domain_policy(self, value): jar = self._jar policy = jar.get_policy() flags = (policy.DomainStrictNoDots | policy.DomainRFC2965Match | policy.DomainStrictNonDomain) policy.strict_ns_domain |= flags if not value: policy.strict_ns_domain ^= flags def forURL(self, url): return self.__class__(self.mech_browser, url) @property def url(self): if self._url is not None: return self._url else: return self.mech_browser.geturl() @property def _request(self): if self._url is not None: return self.mech_browser.request_class(self._url) else: request = self.mech_browser.request if request is None: raise RuntimeError('no request found') return request @property def header(self): request = self.mech_browser.request_class(self.url) self._jar.add_cookie_header(request) return request.get_header('Cookie') def __str__(self): return self.header def __repr__(self): # get the cookies for the current url return '<%s.%s object at %r for %s (%s)>' % ( self.__class__.__module__, self.__class__.__name__, id(self), self.url, self.header) def _raw_cookies(self): return self._jar.cookies_for_request(self._request) def _get_cookies(self, key=None): if key is None: seen = set() for ck in self._raw_cookies(): if ck.name not in seen: yield ck seen.add(ck.name) else: for ck in self._raw_cookies(): if ck.name == key: yield ck _marker = object() def _get(self, key, default=_marker): for ck in self._raw_cookies(): if ck.name == key: return ck if default is self._marker: raise KeyError(key) return default def __getitem__(self, key): return self._get(key).value def getinfo(self, key): return self._getinfo(self._get(key)) def _getinfo(self, ck): res = {'name': ck.name, 'value': ck.value, 'port': ck.port, 'domain': ck.domain, 'path': ck.path, 'secure': ck.secure, 'expires': None, 'comment': ck.comment, 'commenturl': ck.comment_url} if ck.expires is not None: res['expires'] = datetime.datetime.fromtimestamp( ck.expires, pytz.UTC) return res def keys(self): return [ck.name for ck in self._get_cookies()] def __iter__(self): return (ck.name for ck in self._get_cookies()) iterkeys = __iter__ def iterinfo(self, key=None): return (self._getinfo(ck) for ck in self._get_cookies(key)) def iteritems(self): return ((ck.name, ck.value) for ck in self._get_cookies()) def has_key(self, key): return self._get(key, None) is not None __contains__ = has_key def __len__(self): return len(list(self._get_cookies())) def __delitem__(self, key): ck = self._get(key) self._jar.clear(ck.domain, ck.path, ck.name) def create(self, name, value, domain=None, expires=None, path=None, secure=None, comment=None, commenturl=None, port=None): if value is None: raise ValueError('must provide value') ck = self._get(name, None) if (ck is not None and (path is None or ck.path == path) and (domain is None or ck.domain == domain or ck.domain == domain) and (port is None or ck.port == port)): # cookie already exists raise ValueError('cookie already exists') if domain is not None: self._verifyDomain(domain, ck) if path is not None: self._verifyPath(path, ck) now = int(time.time()) if expires is not None and self._is_expired(expires, now): raise zope.testbrowser.interfaces.AlreadyExpiredError( 'May not create a cookie that is immediately expired') self._setCookie(name, value, domain, expires, path, secure, comment, commenturl, port, now=now) def change(self, name, value=None, domain=None, expires=None, path=None, secure=None, comment=None, commenturl=None, port=None): now = int(time.time()) if expires is not None and self._is_expired(expires, now): # shortcut del self[name] else: self._change(self._get(name), value, domain, expires, path, secure, comment, commenturl, port, now) def _change(self, ck, value=None, domain=None, expires=None, path=None, secure=None, comment=None, commenturl=None, port=None, now=None): if value is None: value = ck.value if domain is None: domain = ck.domain else: self._verifyDomain(domain, None) if expires is None: expires = ck.expires if path is None: path = ck.path else: self._verifyPath(domain, None) if secure is None: secure = ck.secure if comment is None: comment = ck.comment if commenturl is None: commenturl = ck.comment_url if port is None: port = ck.port self._setCookie(ck.name, value, domain, expires, path, secure, comment, commenturl, port, ck.version, ck=ck, now=now) def _verifyDomain(self, domain, ck): tmp_domain = domain if domain is not None and domain.startswith('.'): tmp_domain = domain[1:] self_host = mechanize.effective_request_host(self._request) if (self_host != tmp_domain and not self_host.endswith('.' + tmp_domain)): raise ValueError('current url must match given domain') if (ck is not None and ck.domain != tmp_domain and ck.domain.endswith(tmp_domain)): raise ValueError( 'cannot set a cookie that will be hidden by another ' 'cookie for this url (%s)' % (self.url,)) def _verifyPath(self, path, ck): self_path = urlparse.urlparse(self.url)[2] if not self_path.startswith(path): raise ValueError('current url must start with path, if given') if ck is not None and ck.path != path and ck.path.startswith(path): raise ValueError( 'cannot set a cookie that will be hidden by another ' 'cookie for this url (%s)' % (self.url,)) def _setCookie(self, name, value, domain, expires, path, secure, comment, commenturl, port, version=None, ck=None, now=None): for nm, val in self.mech_browser.addheaders: if nm.lower() in ('cookie', 'cookie2'): raise ValueError('cookies are already set in `Cookie` header') if domain and not domain.startswith('.'): # we do a dance here so that we keep names that have been passed # in consistent (i.e., if we get an explicit 'example.com' it stays # 'example.com', rather than converting to '.example.com'). tmp_domain = domain domain = None if secure: protocol = 'https' else: protocol = 'http' url = '%s://%s%s' % (protocol, tmp_domain, path or '/') request = self.mech_browser.request_class(url) else: request = self._request if request is None: raise mechanize.BrowserStateError( 'cannot create cookie without request or domain') c = Cookie.SimpleCookie() name = str(name) c[name] = value.encode('utf8') if secure: c[name]['secure'] = True if domain: c[name]['domain'] = domain if path: c[name]['path'] = path if expires: c[name]['expires'] = expiration_string(expires) if comment: c[name]['comment'] = urllib.quote( comment.encode('utf-8'), safe="/?:@&+") if port: c[name]['port'] = port if commenturl: c[name]['commenturl'] = commenturl if version: c[name]['version'] = version # this use of objects like _StubResponse and _StubHTTPMessage is in # fact supported by the documented client cookie API. cookies = self._jar.make_cookies( _StubResponse([c.output(header='').strip()]), request) assert len(cookies) == 1, ( 'programmer error: %d cookies made' % (len(cookies),)) policy = self._jar._policy if now is None: now = int(time.time()) policy._now = self._jar._now = now # TODO get mechanize to expose this if not policy.set_ok(cookies[0], request): raise ValueError('policy does not allow this cookie') if ck is not None: self._jar.clear(ck.domain, ck.path, ck.name) self._jar.set_cookie(cookies[0]) def __setitem__(self, key, value): ck = self._get(key, None) if ck is None: self.create(key, value) else: self._change(ck, value) def _is_expired(self, value, now): # now = int(time.time()) dnow = datetime.datetime.fromtimestamp(now, pytz.UTC) if isinstance(value, datetime.datetime): if value.tzinfo is None: if value <= dnow.replace(tzinfo=None): return True elif value <= dnow: return True elif isinstance(value, basestring): if datetime.datetime.fromtimestamp( mechanize.str2time(value), pytz.UTC) <= dnow: return True return False def clear(self): # to give expected mapping behavior of resulting in an empty dict, # we use _raw_cookies rather than _get_cookies. for ck in self._raw_cookies(): self._jar.clear(ck.domain, ck.path, ck.name) def clearAllSession(self): self._jar.clear_session_cookies() def clearAll(self): self._jar.clear() zope.testbrowser-4.0.4/src/zope/testbrowser/wsgi.py0000644000000000000000000002110612225722334020660 0ustar 00000000000000############################################################################## # # Copyright (c) 2010-2011 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """WSGI-specific testing code """ import base64 import re import sys from webtest import TestApp import zope.testbrowser.browser import zope.testbrowser.connection class HostNotAllowed(Exception): pass _allowed_2nd_level = set(['example.com', 'example.net', 'example.org']) # RFC 2606 _allowed = set(['localhost', '127.0.0.1']) _allowed.update(_allowed_2nd_level) class WSGIConnection(object): """A ``mechanize`` compatible connection object.""" _allowed = True def __init__(self, test_app, host, timeout=None): self._test_app = TestApp(test_app) self.host = host self.assert_allowed_host() def assert_allowed_host(self): host = self.host if host in _allowed: return for dom in _allowed_2nd_level: if host.endswith('.%s' % dom): return self._allowed = False def set_debuglevel(self, level): pass def request(self, method, url, body=None, headers=None): """Send a request to the publisher. The response will be stored in ``self.response``. """ if body is None: body = '' if url == '': url = '/' # Extract the handle_error option header if sys.version_info >= (2,5): handle_errors_key = 'X-Zope-Handle-Errors' else: handle_errors_key = 'X-zope-handle-errors' handle_errors_header = headers.get(handle_errors_key, True) if handle_errors_key in headers: del headers[handle_errors_key] # Translate string to boolean. handle_errors = {'False': False}.get(handle_errors_header, True) # WebTest always sets 'paste.throw_errors' to True. Setting it to None # here overrides that, but is UGLY. sigh. extra_environ = {'paste.throw_errors': None} if not handle_errors: # There doesn't seem to be a "Right Way" to do this extra_environ['wsgi.handleErrors'] = False # zope.app.wsgi does this extra_environ['paste.throw_errors'] = True # the paste way of doing this extra_environ['x-wsgiorg.throw_errors'] = True # http://wsgi.org/wsgi/Specifications/throw_errors scheme_key = 'X-Zope-Scheme' extra_environ['wsgi.url_scheme'] = headers.get(scheme_key, 'http') if scheme_key in headers: del headers[scheme_key] if not self._allowed: raise HostNotAllowed('%s://%s%s' % (extra_environ['wsgi.url_scheme'], self.host, url)) app = self._test_app # clear our app cookies so that our testbrowser cookie headers don't # get stomped app.cookies.clear() # pass the request to webtest if method == 'GET': assert not body, body response = app.get(url, headers=headers, expect_errors=True, extra_environ=extra_environ) elif method == 'POST': response = app.post(url, body, headers=headers, expect_errors=True, extra_environ=extra_environ) else: raise Exception('Couldnt handle method %s' % method) self.response = response def getresponse(self): """Return a ``mechanize`` compatible response. The goal of ths method is to convert the WebTest's reseponse to a ``mechanize`` compatible response, which is also understood by mechanize. """ response = self.response status = int(response.status[:3]) reason = response.status[4:] headers = response.headers.items() headers.sort() headers.insert(0, ('Status', response.status)) headers = '\r\n'.join('%s: %s' % h for h in headers) # Ugh! WebTest's headers can at times be unicode. That causes weird # problems later when they are shoved into a StringIO. So just cast # to a string for now using ascii. headers = str(headers) content = response.body return zope.testbrowser.connection.Response(content, headers, status, reason) class WSGIHTTPHandler(zope.testbrowser.connection.HTTPHandler): def __init__(self, test_app, *args, **kw): self._test_app = test_app zope.testbrowser.connection.HTTPHandler.__init__(self, *args, **kw) def _connect(self, *args, **kw): return WSGIConnection(self._test_app, *args, **kw) def https_request(self, req): req.add_unredirected_header('X-Zope-Scheme', 'https') return self.http_request(req) class WSGIMechanizeBrowser(zope.testbrowser.connection.MechanizeBrowser): """Special ``mechanize`` browser using the WSGI HTTP handler.""" def __init__(self, test_app, *args, **kw): self._test_app = test_app zope.testbrowser.connection.MechanizeBrowser.__init__(self, *args, **kw) def _http_handler(self, *args, **kw): return WSGIHTTPHandler(self._test_app, *args, **kw) class Browser(zope.testbrowser.browser.Browser): """A WSGI `testbrowser` Browser that uses a WebTest wrapped WSGI app.""" def __init__(self, url=None, wsgi_app=None): if wsgi_app is None: wsgi_app = Layer.get_app() if wsgi_app is None: raise AssertionError("wsgi_app not provided or zope.testbrowser.wsgi.Layer not setup") mech_browser = WSGIMechanizeBrowser(wsgi_app) super(Browser, self).__init__(url=url, mech_browser=mech_browser) # Compatibility helpers to behave like zope.app.testing basicre = re.compile('Basic (.+)?:(.+)?$') def auth_header(header): """This function takes an authorization HTTP header and encode the couple user, password into base 64 like the HTTP protocol wants it. """ match = basicre.match(header) if match: u, p = match.group(1, 2) if u is None: u = '' if p is None: p = '' auth = base64.encodestring('%s:%s' % (u, p)) return 'Basic %s' % auth[:-1] return header def is_wanted_header(header): """Return True if the given HTTP header key is wanted. """ key, value = header return key.lower() not in ('x-content-type-warning', 'x-powered-by') class AuthorizationMiddleware(object): """This middleware makes the WSGI application compatible with the HTTPCaller behavior defined in zope.app.testing.functional: - It modifies the HTTP Authorization header to encode user and password into base64 if it is Basic authentication. """ def __init__(self, wsgi_stack): self.wsgi_stack = wsgi_stack def __call__(self, environ, start_response): # Handle authorization auth_key = 'HTTP_AUTHORIZATION' if auth_key in environ: environ[auth_key] = auth_header(environ[auth_key]) # Remove unwanted headers def application_start_response(status, headers, exc_info=None): headers = filter(is_wanted_header, headers) start_response(status, headers) for entry in self.wsgi_stack(environ, application_start_response): yield entry _APP_UNDER_TEST = None # setup and torn down by the Layer class class Layer(object): """Test layer which sets up WSGI application for use with WebTest/testbrowser. """ __bases__ = () __name__ = 'Layer' @classmethod def get_app(cls): return _APP_UNDER_TEST def make_wsgi_app(self): # Override this method in subclasses of this layer in order to set up # the WSGI application. raise NotImplementedError def cooperative_super(self, method_name): # Calling `super` for multiple inheritance: method = getattr(super(Layer, self), method_name, None) if method is not None: method() def setUp(self): self.cooperative_super('setUp') global _APP_UNDER_TEST if _APP_UNDER_TEST is not None: raise AssertionError("Already Setup") _APP_UNDER_TEST = self.make_wsgi_app() def tearDown(self): global _APP_UNDER_TEST _APP_UNDER_TEST = None self.cooperative_super('tearDown') zope.testbrowser-4.0.4/src/zope/testbrowser/fixed-bugs.txt0000644000000000000000000001401712225722334022136 0ustar 00000000000000========== Fixed Bugs ========== This file includes tests for bugs that were found and then fixed that don't fit into the more documentation-centric sections above. >>> from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication >>> from zope.testbrowser.wsgi import Browser >>> wsgi_app = WSGITestApplication() Unicode URLs ============ Unicode URLs or headers cause the entire constructed request to be unicode, and (as of Python 2.4.4) Cookie.SimpleCookie checks the type of the input against type(""), so it handles the value inappropriately, causing exceptions that ended with:: File "/home/benji/Python-2.4.4/lib/python2.4/Cookie.py", line 623, in load self.update(rawdata) ValueError: dictionary update sequence element #0 has length 1; 2 is required As a work-around, unicode strings passed to Browser.open() are now converted to ASCII before being passed on, as well as the key and value passed to Browser.addHeader(). The tests below failed before the change was put in place. >>> browser = Browser(wsgi_app=wsgi_app) >>> browser.addHeader('Cookie', 'test') >>> browser.open(u'http://localhost/@@/testbrowser/simple.html') >>> browser = Browser(wsgi_app=wsgi_app) >>> browser.addHeader(u'Cookie', 'test') >>> browser.open('http://localhost/@@/testbrowser/simple.html') Spaces in URL ============= When URLs have spaces in them, they're handled correctly (before the bug was fixed, you'd get "ValueError: too many values to unpack"): >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.getLink('Spaces in the URL').click() .goBack() Truncation ==================== The .goBack() method used to truncate the .contents. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> actual_length = len(browser.contents) >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.goBack() >>> len(browser.contents) == actual_length True Labeled Radio Buttons ===================== The .getControl() method was sometimes unable to find radio buttons by label. >>> # import mechanize._form; mechanize._form._show_debug_messages() >>> browser.open('http://localhost/@@/testbrowser/radio.html') >>> browser.getControl('One').optionValue '1' >>> browser.getControl('Two').optionValue '2' >>> browser.getControl('Three').optionValue '3' Fragment URLs ============= Earlier versions of mechanize used to incorrectly follow links containing fragments. We upgraded our dependency to a newer version of mechanize and make sure this regression doesn't come back: >>> browser.open('http://localhost/@@/testbrowser/fragment.html#asdf') >>> browser.url 'http://localhost/@@/testbrowser/fragment.html#asdf' >>> browser.getLink('Follow me') >>> browser.getLink('Follow me').click() Textareas with HTML/XML ======================= >>> browser.open('http://localhost/@@/testbrowser/textarea.html') >>> browser.getControl('Text Area').value '\r\n \r\n &\r\n' .click() with non-200 status ============================ The problem was that with the below controls testbrowser forgot to do after-processing after an exception in mechanize. That means ``_stop_timer()`` and ``_changed()`` were not executed if an exception was raised. Not calling ``_changed()`` resulted in not refreshing ``contents``. The ``contents`` property gets cached on any first access and should be reset on any navigation. The problem is that e.g. a simple 403 status raises an exception. This is how it works with a simple open(): >>> browser.handleErrors=False >>> browser.open('http://localhost/set_status.html') >>> print browser.contents Everything fine >>> browser.open('http://localhost/set_status.html?status=403') Traceback (most recent call last): ... HTTPError: HTTP Error 403: Forbidden >>> print browser.contents Just set a status of 403 These are the various controls: A link: >>> browser.open('http://localhost/@@/testbrowser/status_lead.html') >>> print browser.contents ... >>> browser.getLink('403').click() Traceback (most recent call last): ... HTTPError: HTTP Error 403: Forbidden >>> print browser.contents Just set a status of 403 A submit button: >>> browser.open('http://localhost/@@/testbrowser/status_lead.html') >>> print browser.contents ... >>> browser.getControl(name='status').value = '404' >>> browser.getControl('Submit This').click() Traceback (most recent call last): ... HTTPError: HTTP Error 404: Not Found >>> print browser.contents Just set a status of 404 A submit image control: >>> browser.open('http://localhost/@@/testbrowser/status_lead.html') >>> print browser.contents ... >>> browser.getControl(name='status').value = '403' >>> browser.getControl(name='image-value').click() Traceback (most recent call last): ... HTTPError: HTTP Error 403: Forbidden >>> print browser.contents Just set a status of 403 A javascript-ish form submit: >>> browser.open('http://localhost/@@/testbrowser/status_lead.html') >>> print browser.contents ... >>> browser.getControl(name='status').value = '404' >>> browser.getForm(name='theform').submit() Traceback (most recent call last): ... HTTPError: HTTP Error 404: Not Found >>> print browser.contents Just set a status of 404 A non-javascript-ish form submit: >>> browser.open('http://localhost/@@/testbrowser/status_lead.html') >>> print browser.contents ... >>> browser.getControl(name='status').value = '403' >>> browser.getForm(name='theform').submit(name='submit-value') Traceback (most recent call last): ... HTTPError: HTTP Error 403: Forbidden >>> print browser.contents Just set a status of 403 zope.testbrowser-4.0.4/src/zope/testbrowser/testing.py0000644000000000000000000000224512225722334021367 0ustar 00000000000000############################################################################## # # Copyright (c) 2005 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """BBB for Zope 3-specific testing code """ from zope.testbrowser.connection import Response as PublisherResponse try: import zope.app.testing have_zope_app_testing = True except ImportError: have_zope_app_testing = False if have_zope_app_testing: from zope.app.testing.testbrowser import (PublisherConnection, PublisherHTTPHandler, PublisherMechanizeBrowser, Browser) del have_zope_app_testing zope.testbrowser-4.0.4/src/zope/testbrowser/tests/test_wsgi.py0000644000000000000000000002217112225722334023064 0ustar 00000000000000############################################################################## # # Copyright (c) 2011 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## import unittest from urllib import urlencode import zope.testbrowser.wsgi from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication def demo_app(environ, start_response): # Based on wsgiref.simple_server.demo_app, except it doesn't # emit unicode in the response stream even if WSGI environ contains # unicode keys. Fixes GH#10. from StringIO import StringIO stdout = StringIO() print >> stdout, "Hello world!" print >> stdout h = environ.items() h.sort() for k, v in h: print >> stdout, str(k), '=', repr(v) start_response("200 OK", [('Content-Type', 'text/plain')]) return [stdout.getvalue()] class SimpleLayer(zope.testbrowser.wsgi.Layer): def make_wsgi_app(self): return demo_app SIMPLE_LAYER = SimpleLayer() class TestBrowser(unittest.TestCase): def test_redirect(self): app = WSGITestApplication() browser = zope.testbrowser.wsgi.Browser(wsgi_app=app) # redirecting locally works browser.open('http://localhost/redirect.html?%s' % urlencode(dict(to='/set_status.html'))) self.assertEquals(browser.url, 'http://localhost/set_status.html') browser.open('http://localhost/redirect.html?%s' % urlencode(dict(to='/set_status.html', type='301'))) self.assertEquals(browser.url, 'http://localhost/set_status.html') browser.open('http://localhost/redirect.html?%s' % urlencode(dict(to='http://localhost/set_status.html'))) self.assertEquals(browser.url, 'http://localhost/set_status.html') browser.open('http://localhost/redirect.html?%s' % urlencode(dict(to='http://localhost/set_status.html', type='301'))) self.assertEquals(browser.url, 'http://localhost/set_status.html') # non-local redirects raise HostNotAllowed error self.assertRaises(zope.testbrowser.wsgi.HostNotAllowed, browser.open, 'http://localhost/redirect.html?%s' % urlencode(dict(to='http://www.google.com/'))) self.assertRaises(zope.testbrowser.wsgi.HostNotAllowed, browser.open, 'http://localhost/redirect.html?%s' % urlencode(dict(to='http://www.google.com/', type='301'))) # we're also automatically redirected on submit browser.open('http://localhost/@@/testbrowser/forms.html') self.assertEquals(browser.headers.get('status'), '200 OK') form = browser.getForm(name='redirect') form.submit() self.assertEquals(browser.headers.get('status'), '200 OK') self.assertEquals(browser.url, 'http://localhost/set_status.html') def test_no_redirect(self): app = WSGITestApplication() browser = zope.testbrowser.wsgi.Browser(wsgi_app=app) # tell testbrowser to not handle redirects automatically browser.mech_browser.set_handle_redirect(False) # and tell zope.testbrowser to not raise HTTP errors (everything but # 20x responses is considered an error) browser.raiseHttpErrors = False url = ('http://localhost/redirect.html?%s' % urlencode(dict(to='/set_status.html'))) browser.open(url) # see - we're not redirected self.assertEquals(browser.url, url) self.assertEquals(browser.headers.get('status'), '302 Found') # the same should happen on submit (issue #4) browser.open('http://localhost/@@/testbrowser/forms.html') self.assertEquals(browser.headers.get('status'), '200 OK') form = browser.getForm(name='redirect') form.submit() self.assertEquals(browser.headers.get('status'), '302 Found') self.assertEquals(browser.url, url) def test_allowed_domains(self): browser = zope.testbrowser.wsgi.Browser(wsgi_app=demo_app) # external domains are not allowed self.assertRaises(zope.testbrowser.wsgi.HostNotAllowed, browser.open, 'http://www.google.com') self.assertRaises(zope.testbrowser.wsgi.HostNotAllowed, browser.open, 'https://www.google.com') # internal ones are browser.open('http://localhost') self.assertTrue(browser.contents.startswith('Hello world!\n')) browser.open('http://127.0.0.1') self.assertTrue(browser.contents.startswith('Hello world!\n')) # as are example ones browser.open('http://example.com') self.assertTrue(browser.contents.startswith('Hello world!\n')) browser.open('http://example.net') self.assertTrue(browser.contents.startswith('Hello world!\n')) # and subdomains of example browser.open('http://foo.example.com') self.assertTrue(browser.contents.startswith('Hello world!\n')) browser.open('http://bar.example.net') self.assertTrue(browser.contents.startswith('Hello world!\n')) def test_handle_errors(self): # http://wsgi.org/wsgi/Specifications/throw_errors app = WSGITestApplication() browser = zope.testbrowser.wsgi.Browser(wsgi_app=app) browser.open('http://localhost/echo_one.html?var=x-wsgiorg.throw_errors') self.assertEquals(browser.contents, 'None') browser.open('http://localhost/echo_one.html?var=paste.throw_errors') self.assertEquals(browser.contents, 'None') browser.open('http://localhost/echo_one.html?var=wsgi.handleErrors') self.assertEquals(browser.contents, 'None') browser.handleErrors = False browser.open('http://localhost/echo_one.html?var=x-wsgiorg.throw_errors') self.assertEquals(browser.contents, 'True') browser.open('http://localhost/echo_one.html?var=paste.throw_errors') self.assertEquals(browser.contents, 'True') browser.open('http://localhost/echo_one.html?var=wsgi.handleErrors') self.assertEquals(browser.contents, 'False') class TestWSGILayer(unittest.TestCase): def setUp(self): # test the layer without depending on zope.testrunner SIMPLE_LAYER.setUp() def tearDown(self): SIMPLE_LAYER.tearDown() def test_layer(self): """When the layer is setup, the wsgi_app argument is unnecessary""" browser = zope.testbrowser.wsgi.Browser() browser.open('http://localhost') self.assertTrue(browser.contents.startswith('Hello world!\n')) # XXX test for authorization header munging is missing def test_app_property(self): # The layer has a .app property where the application under test is available self.assertTrue(SIMPLE_LAYER.get_app() is demo_app) def test_there_can_only_be_one(self): another_layer = SimpleLayer() # The layer has a .app property where the application under test is available self.assertRaises(AssertionError, another_layer.setUp) class TestAuthorizationMiddleware(unittest.TestCase): def setUp(self): app = WSGITestApplication() self.unwrapped_browser = zope.testbrowser.wsgi.Browser(wsgi_app=app) app = zope.testbrowser.wsgi.AuthorizationMiddleware(app) self.browser = zope.testbrowser.wsgi.Browser(wsgi_app=app) def test_unwanted_headers(self): #x-powered-by and x-content-type-warning are filtered url = 'http://localhost/set_header.html?x-other=another&x-powered-by=zope&x-content-type-warning=bar' self.browser.open(url) self.assertEquals(self.browser.headers['x-other'], 'another') self.assertTrue('x-other' in self.browser.headers) self.assertFalse('x-powered-by' in self.browser.headers) self.assertFalse('x-content-type-warning' in self.browser.headers) # make sure we are actually testing something self.unwrapped_browser.open(url) self.assertTrue('x-powered-by' in self.unwrapped_browser.headers) self.assertTrue('x-content-type-warning' in self.unwrapped_browser.headers) def test_authorization(self): # Basic authorization headers are encoded in base64 self.browser.addHeader('Authorization', 'Basic mgr:mgrpw') self.browser.open('http://localhost/echo_one.html?var=HTTP_AUTHORIZATION') self.assertEquals(self.browser.contents, repr('Basic bWdyOm1ncnB3')) def test_authorization_other(self): # Non-Basic authorization headers are unmolested self.browser.addHeader('Authorization', 'Digest foobar') self.browser.open('http://localhost/echo_one.html?var=HTTP_AUTHORIZATION') self.assertEquals(self.browser.contents, repr('Digest foobar')) zope.testbrowser-4.0.4/src/zope/testbrowser/tests/__init__.py0000644000000000000000000000000012225722334022576 0ustar 00000000000000zope.testbrowser-4.0.4/src/zope/testbrowser/tests/helper.py0000644000000000000000000000370012225722334022330 0ustar 00000000000000############################################################################## # # Copyright (c) 2004 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## import re import zope.testing.renormalizing class win32CRLFtransformer(object): def sub(self, replacement, text): return text.replace(r'\r', '') checker = zope.testing.renormalizing.RENormalizing([ (re.compile(r'^--\S+\.\S+\.\S+', re.M), '-' * 30), (re.compile(r'boundary=\S+\.\S+\.\S+'), 'boundary=' + '-' * 30), (re.compile(r'^---{10}.*', re.M), '-' * 30), (re.compile(r'boundary=-{10}.*'), 'boundary=' + '-' * 30), (re.compile(r'User-agent:\s+\S+'), 'User-agent: Python-urllib/2.4'), (re.compile(r'HTTP_USER_AGENT:\s+\S+'), 'HTTP_USER_AGENT: Python-urllib/2.4'), (re.compile(r'Content-[Ll]ength:.*'), 'Content-Length: 123'), (re.compile(r'Status: 200.*'), 'Status: 200 OK'), (win32CRLFtransformer(), None), (re.compile(r'User-Agent: Python-urllib/2.[567]'), 'User-agent: Python-urllib/2.4'), (re.compile(r'Host: localhost'), 'Connection: close'), (re.compile(r'Content-Type: '), 'Content-type: '), (re.compile(r'Content-Disposition: '), 'Content-disposition: '), (re.compile(r'; charset=UTF-8'), ';charset=utf-8'), # webtest seems to expire cookies one second before the date set in set_cookie (re.compile(r"'expires': datetime.datetime\(2029, 12, 31, 23, 59, 59, tzinfo=\),"), "'expires': datetime.datetime(2030, 1, 1, 0, 0, tzinfo=),"), ]) zope.testbrowser-4.0.4/src/zope/testbrowser/tests/test_bbb.py0000644000000000000000000000160712225722334022641 0ustar 00000000000000############################################################################## # # Copyright (c) 2011 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## import unittest class TestZopeAppTesting(unittest.TestCase): def test_import(self): try: import zope.app.testing except ImportError: return from zope.testbrowser.testing import Browser browser = Browser() zope.testbrowser-4.0.4/src/zope/testbrowser/tests/test_browser.py0000644000000000000000000003102212225722334023571 0ustar 00000000000000############################################################################## # # Copyright (c) 2004 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Real test for file-upload and beginning of a better internal test framework """ import cStringIO import doctest import httplib import mechanize import socket import sys import zope.testbrowser.browser import zope.testbrowser.tests.helper def set_next_response(body, headers=None, status='200', reason='OK'): global next_response_body global next_response_headers global next_response_status global next_response_reason if headers is None: headers = ( 'Content-Type: text/html\r\n' 'Content-Length: %s\r\n' % len(body)) next_response_body = body next_response_headers = headers next_response_status = status next_response_reason = reason class FauxConnection(object): """A ``mechanize`` compatible connection object.""" def __init__(self, host, timeout=None): pass def set_debuglevel(self, level): pass def _quote(self, url): # the publisher expects to be able to split on whitespace, so we have # to make sure there is none in the URL return url.replace(' ', '%20') def request(self, method, url, body=None, headers=None): if body is None: body = '' if url == '': url = '/' url = self._quote(url) # Construct the headers. header_chunks = [] if headers is not None: for header in headers.items(): header_chunks.append('%s: %s' % header) headers = '\n'.join(header_chunks) + '\n' else: headers = '' # Construct the full HTTP request string, since that is what the # ``HTTPCaller`` wants. request_string = (method + ' ' + url + ' HTTP/1.1\n' + headers + '\n' + body) print request_string.replace('\r', '') def getresponse(self): """Return a ``mechanize`` compatible response. The goal of this method is to convert the Zope Publisher's response to a ``mechanize`` compatible response, which is also understood by mechanize. """ return FauxResponse(next_response_body, next_response_headers, next_response_status, next_response_reason, ) class FauxResponse(object): def __init__(self, content, headers, status, reason): self.content = content self.status = status self.reason = reason self.msg = httplib.HTTPMessage(cStringIO.StringIO(headers), 0) self.content_as_file = cStringIO.StringIO(self.content) def read(self, amt=None): return self.content_as_file.read(amt) def close(self): """To overcome changes in mechanize and socket in python2.5""" pass class FauxHTTPHandler(mechanize.HTTPHandler): http_request = mechanize.HTTPHandler.do_request_ def http_open(self, req): """Open an HTTP connection having a ``mechanize`` request.""" # Here we connect to the publisher. if sys.version_info > (2, 6) and not hasattr(req, 'timeout'): # Workaround mechanize incompatibility with Python # 2.6. See: LP #280334 req.timeout = socket._GLOBAL_DEFAULT_TIMEOUT return self.do_open(FauxConnection, req) class FauxMechanizeBrowser(mechanize.Browser): handler_classes = { # scheme handlers "http": FauxHTTPHandler, "_http_error": mechanize.HTTPErrorProcessor, "_http_default_error": mechanize.HTTPDefaultErrorHandler, # feature handlers "_authen": mechanize.HTTPBasicAuthHandler, "_redirect": mechanize.HTTPRedirectHandler, "_cookies": mechanize.HTTPCookieProcessor, "_refresh": mechanize.HTTPRefreshProcessor, "_referer": mechanize.Browser.handler_classes['_referer'], "_equiv": mechanize.HTTPEquivProcessor, } default_schemes = ["http"] default_others = ["_http_error", "_http_default_error"] default_features = ["_authen", "_redirect", "_cookies"] class Browser(zope.testbrowser.browser.Browser): def __init__(self, url=None): mech_browser = FauxMechanizeBrowser() super(Browser, self).__init__(url=url, mech_browser=mech_browser) def open(self, body, headers=None, status=200, reason='OK', url='http://localhost/'): set_next_response(body, headers, status, reason) zope.testbrowser.browser.Browser.open(self, url) def test_submit_duplicate_name(): """ This test was inspired by bug #723 as testbrowser would pick up the wrong button when having the same name twice in a form. >>> browser = Browser() When given a form with two submit buttons that have the same name: >>> browser.open('''\ ... ...
... ... ...
... ''') # doctest: +ELLIPSIS GET / HTTP/1.1 ... We can specify the second button through it's label/value: >>> browser.getControl('BAD') >>> browser.getControl('BAD').value 'BAD' >>> browser.getControl('BAD').click() # doctest: +REPORT_NDIFF +ELLIPSIS POST / HTTP/1.1 ... Content-type: multipart/form-data; ... Content-disposition: form-data; name="submit_me" BAD ... This also works if the labels have whitespace around them (this tests a regression caused by the original fix for the above): >>> browser.open('''\ ... ...
... ... ...
... ''') # doctest: +ELLIPSIS GET / HTTP/1.1 ... >>> browser.getControl('BAD') >>> browser.getControl('BAD').value ' BAD ' >>> browser.getControl('BAD').click() # doctest: +REPORT_NDIFF +ELLIPSIS POST / HTTP/1.1 ... Content-type: multipart/form-data; ... Content-disposition: form-data; name="submit_me" BAD ... """ def test_file_upload(): """ >>> browser = Browser() When given a form with a file-upload >>> browser.open('''\ ... ...
... ... ...
... ''') # doctest: +ELLIPSIS GET / HTTP/1.1 ... Fill in the form value using add_file: >>> browser.getControl(name='foo').add_file( ... cStringIO.StringIO('sample_data'), 'text/foo', 'x.foo') >>> browser.getControl('OK').click() # doctest: +REPORT_NDIFF +ELLIPSIS POST / HTTP/1.1 ... Content-type: multipart/form-data; ... Content-disposition: form-data; name="foo"; filename="x.foo" Content-type: text/foo sample_data ... You can pass a string to add_file: >>> browser.getControl(name='foo').add_file( ... 'blah blah blah', 'text/blah', 'x.blah') >>> browser.getControl('OK').click() # doctest: +REPORT_NDIFF +ELLIPSIS POST / HTTP/1.1 ... Content-type: multipart/form-data; ... Content-disposition: form-data; name="foo"; filename="x.blah" Content-type: text/blah blah blah blah ... """ def test_submit_gets_referrer(): """ Test for bug #98437: No HTTP_REFERER was sent when submitting a form. >>> browser = Browser() A simple form for testing, like abobe. >>> browser.open('''\ ... ...
... ...
... ''') # doctest: +ELLIPSIS GET / HTTP/1.1 ... Now submit the form, and see that we get an referrer along: >>> form = browser.getForm(id='form') >>> form.submit(name='submit_me') # doctest: +ELLIPSIS POST / HTTP/1.1 ... Referer: http://localhost/ ... """ def test_new_instance_no_contents_should_not_fail(self): """ When first instantiated, the browser has no contents. (Regression test for ) >>> browser = Browser() >>> print browser.contents None """ def test_strip_linebreaks_from_textarea(self): """ >>> browser = Browser() According to http://www.w3.org/TR/html4/appendix/notes.html#h-B.3.1 line break immediately after start tags or immediately before end tags must be ignored, but real browsers only ignore a line break after a start tag. So if we give the following form: >>> browser.open(''' ... ...
... ...
... ''') # doctest: +ELLIPSIS GET / HTTP/1.1 ... The value of the textarea won't contain the first line break: >>> browser.getControl(name='textarea').value 'Foo\\n' Of course, if we add line breaks, so that there are now two line breaks after the start tag, the textarea value will start and end with a line break. >>> browser.open(''' ... ...
... ...
... ''') # doctest: +ELLIPSIS GET / HTTP/1.1 ... >>> browser.getControl(name='textarea').value '\\nFoo\\n' Also, if there is some other whitespace after the start tag, it will be preserved. >>> browser.open(''' ... ...
... ...
... ''') # doctest: +ELLIPSIS GET / HTTP/1.1 ... >>> browser.getControl(name='textarea').value ' Foo ' """ def test_relative_link(): """ RFC 1808 specifies how relative URLs should be resolved, let's see that we conform to it. Let's start with a simple example. >>> browser = Browser() >>> browser.open('''\ ... ... link ... ... ''', url='http://localhost/bar') # doctest: +ELLIPSIS GET /bar HTTP/1.1 ... >>> link = browser.getLink('link') >>> link.url 'http://localhost/foo' It's possible to have a relative URL consisting of only a query part. In that case it should simply be appended to the base URL. >>> browser.open('''\ ... ... link ... ... ''', url='http://localhost/bar') # doctest: +ELLIPSIS GET /bar HTTP/1.1 ... >>> link = browser.getLink('link') >>> link.url 'http://localhost/bar?key=value' In the example above, the base URL was the page URL, but we can also specify a base URL using a tag. >>> browser.open('''\ ... ... link ... ... ''', url='http://localhost/base/bar') # doctest: +ELLIPSIS GET /base/bar HTTP/1.1 ... >>> link = browser.getLink('link') >>> link.url 'http://localhost/base?key=value' """ def test_suite(): return doctest.DocTestSuite( checker=zope.testbrowser.tests.helper.checker, optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS) # additional_tests is for setuptools "setup.py test" support additional_tests = test_suite zope.testbrowser-4.0.4/src/zope/testbrowser/tests/test_doctests.py0000644000000000000000000000256512225722334023750 0ustar 00000000000000############################################################################## # # Copyright (c) 2004-2011 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## import doctest import pkg_resources import unittest import zope.testbrowser.ftests.wsgitestapp import zope.testbrowser.wsgi import zope.testbrowser.tests.helper def test_suite(): flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS suite = doctest.DocFileSuite( 'README.txt', 'cookies.txt', 'fixed-bugs.txt', optionflags=flags, checker=zope.testbrowser.tests.helper.checker, package='zope.testbrowser') wire = doctest.DocFileSuite('over_the_wire.txt', optionflags=flags, package='zope.testbrowser') wire.level = 2 return unittest.TestSuite((suite, wire)) # additional_tests is for setuptools "setup.py test" support additional_tests = test_suite zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/oneform.html0000644000000000000000000000056512225722334023206 0ustar 00000000000000

Single Form Tests

zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/navigate.html0000644000000000000000000000222012225722334023325 0ustar 00000000000000

Navigation Tests

Message: %(message)s

Link Text Link Text with Whitespace Normalization (and parens) Using the URL By Anchor Name By Anchor Id Spaces in the URL
Zope3 zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/fragment.html0000644000000000000000000000016012225722334023333 0ustar 00000000000000 Follow me zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/status_lead.html0000644000000000000000000000103412225722334024041 0ustar 00000000000000

This page leads to status setting

403
zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/__init__.py0000644000000000000000000000117012225722334022755 0ustar 00000000000000############################################################################## # # Copyright (c) Zope Corporation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/controls.html0000644000000000000000000001416512225722334023405 0ustar 00000000000000

Controls Tests

%(text-value)s
%(password-value)s
(label: hee hee) %(hidden-value)s
%(textarea-value)s
%(file-value)s
%(single-select-value)s
%(multi-select-value)s
%(single-unvalued-checkbox-value)s
%(single-disabled-unvalued-checkbox-value)s
%(single-valued-checkbox-value)s
(Multi checkbox: options have the labels) %(multi-checkbox-value)s
(Radio: options have the labels) %(radio-value)s
%(image-value.x)s %(image-value.y)s
%(submit-value)s
If you have a select field with a label that overlaps with one of its options' labels, that is ambiguous.
zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/textarea.html0000644000000000000000000000070512225722334023352 0ustar 00000000000000

Textarea Tests

zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/wsgitestapp.py0000644000000000000000000001240612225722334023574 0ustar 00000000000000############################################################################## # # Copyright (c) 2010 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """A minimal WSGI application used as a test fixture.""" import os import cgi import mimetypes from datetime import datetime from webob import Request, Response class NotFound(Exception): pass _HERE = os.path.dirname(__file__) class WSGITestApplication(object): def __call__(self, environ, start_response): req = Request(environ) handler = {'/set_status.html': set_status, '/echo.html': echo, '/redirect.html': redirect, '/@@/testbrowser/forms.html': forms, '/echo_one.html': echo_one, '/set_header.html': set_header, '/set_cookie.html': set_cookie, '/get_cookie.html': get_cookie, '/inner/set_cookie.html': set_cookie, '/inner/get_cookie.html': get_cookie, '/inner/path/set_cookie.html': set_cookie, '/inner/path/get_cookie.html': get_cookie, }.get(req.path_info) if handler is None and req.path_info.startswith('/@@/testbrowser/'): handler = handle_resource if handler is None: handler = handle_notfound try: resp = handler(req) except Exception, exc: if not environ.get('wsgi.handleErrors', True): raise resp = Response() status = 500 if isinstance(exc, NotFound): status = 404 resp.status = status return resp(environ, start_response) def handle_notfound(req): raise NotFound(req.path_info) class ParamsWrapper(object): def __init__(self, params): self.params = params def __getitem__(self, key): if key in self.params: return cgi.escape(self.params[key]) return '' def handle_resource(req, extra=None): filename = req.path_info.split('/')[-1] type, _ = mimetypes.guess_type(filename) path = os.path.join(_HERE, filename) contents = open(path, 'r').read() if type == 'text/html': params = {} params.update(req.params) if extra is not None: params.update(extra) contents = contents % ParamsWrapper(params) return Response(contents, content_type=type) def forms(req): extra = {} if 'hidden-4' in req.params and 'submit-4' not in req.params: extra['no-submit-button'] = 'Submitted without the submit button.' return handle_resource(req, extra) def get_cookie(req): cookies = ['%s: %s' % i for i in sorted(req.cookies.items())] return Response('\n'.join(cookies)) def set_cookie(req): cookie_parms = {'path': None} cookie_parms.update(dict((str(k), str(v)) for k, v in req.params.items())) name = cookie_parms.pop('name') value = cookie_parms.pop('value') if 'max-age' in cookie_parms: cookie_parms['max_age'] = int(cookie_parms.pop('max-age')) if 'expires' in cookie_parms: cookie_parms['expires'] = datetime.strptime(cookie_parms.pop('expires'), '%a, %d %b %Y %H:%M:%S GMT') resp = Response() resp.set_cookie(name, value, **cookie_parms) return resp def set_header(req): resp = Response() body = [u"Set Headers:"] for k, v in sorted(req.params.items()): body.extend([k, v]) resp.headers.add(k, v) resp.unicode_body = u'\n'.join(body) return resp _interesting_environ = ('CONTENT_LENGTH', 'CONTENT_TYPE', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_CONNECTION', 'HTTP_HOST', 'HTTP_USER_AGENT', 'PATH_INFO', 'REQUEST_METHOD') def echo(req): items = [] for k in _interesting_environ: v = req.environ.get(k, None) if v is None: continue items.append('%s: %s' % (k, v)) items.extend('%s: %s' % x for x in sorted(req.params.items())) if req.method == 'POST' and req.content_type == 'application/x-www-form-urlencoded': body = '' else: body = req.body items.append('Body: %r' % body) return Response('\n'.join(items)) def redirect(req): loc = req.params['to'] resp = Response("You are being redirected to %s" % loc) resp.location = loc resp.status = int(req.params.get('type', 302)) return resp def echo_one(req): resp = repr(req.environ.get(req.params['var'])) return Response(resp) def set_status(req): status = req.params.get('status') if status: resp = Response('Just set a status of %s' % status) resp.status = int(status) return resp return Response('Everything fine') zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/radio.html0000644000000000000000000000065212225722334022634 0ustar 00000000000000

Radio Button Tests



zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/notitle.html0000644000000000000000000000006712225722334023214 0ustar 00000000000000

No Title

zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/cookies.html0000644000000000000000000000006712225722334023172 0ustar 00000000000000

No Title

zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/forms.html0000644000000000000000000000252112225722334022661 0ustar 00000000000000

Forms Tests

%(text-value)s
%(no-submit-button)s
zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/zope3logo.gif0000644000000000000000000000274212225722334023262 0ustar 00000000000000GIF89a‘*ÕÅÕÝ¥½ÉÓßåèïòºÍÖ•²À¨ÀÌùûû÷ùúëð󓱿ðôöÕàæ­¼·ËÔž¸Å±ÇÑÚåé¾ÐØÁÒÚÌÚᠺƵÉÓ˜´Â­ÄÏ¢¼ÈÊØàâêîÝæëš¶Ã»Îרâ诽óöø¬ÃΪÂÍÐÝã›¶ÄÏÜã©ÁÌ???¿¿¿ïïïÏÏÏ///ßß߯¯¯ŸŸŸ___OOOooo¯ÅÐÃÔÜ¡»Ç§¿Ëœ·Ä³ÈÒŒ¬»ÿÿÿ!ù,‘*ÿÀŸpH,ȤrÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°x¸ 92—ô%Ñ$Æðø•¡+¤s‹ÃrÚ•.$r„…I !F†–† 6&{'¢:8;—®a v9 #6»6#:©­G5(ÉÊËÌɯÏB6= 9$ ƒB Û Á; ÅG(>êëìí>3ÐQ/*,Y= 8oK8":2VÔ¨G%B šø@À‚ãÊ5 %]FŽFX¨HA“fŒI²˜·PNÿ‡=9¶`ˆ„E:U¬ÈØP =4 ‡<0xAˆ¨PR¬SqäÅ u+”aôáÆwëV¤€)®E$gWÀüÑBŒ)z°\" px0y‚« = <髮ƑêÚ:˜›?0 Hd°¨6x@¡®‡Á™ «±Îvú™ÿÎ 4¤0ƒl2Ð&D.8 (£qæB´L´jß4×ÊjÝ4±«úÀ/ç:áÒ Ÿf¤L»ü–j…R>q0zøPs¶ÅÁD° ¢ŸIäéð½ "a±Þ‰çÄ=Øð×=ä<‹"ÄœF”Æ„§Ml1vJ¼\ ë,œÄˆtóÔ³\À„ÊáÂLxç«”9×ò¥÷^!Û–KH#ÍÔSgðmm=H„‰(+1ã MpqÄ2çT¶gCÁ1Û90é÷ÑÜuÕ}e}»Ä„ÒT0­ŽÓO^¸PQÝ@š º õÛsrý„l(ÁBE+0+y€'ÁÿÂ\HÄ,E`š÷î»ïa‘0 =õ„PM4 ð¤Qå­ÃR±±V~eˤBëC˜LèÜ*¢ÍDÔSw0úøä‹žœï |k²]ì#ÝFìÖTÃZ3¬Ú°uÙu„2ÄÀÑí*Â^¡>ò6) *  jÂÀ:ÐqB€2à§<ÿ!¡&“{ ç’T­˜kB`rp8' 9Á“ÈZpƒšÜÄq“JNتð†R¸‡¨1¨‡@œ©…„  gø ¢›°€#ÆMD¼Ηƒ(.ñŠE¼ÇÔ :©MÍ>â¿ à|âs€ 8Æ6ºñpŒ£ç‡ ;zope.testbrowser-4.0.4/src/zope/testbrowser/ftests/simple.html0000644000000000000000000000015512225722334023025 0ustar 00000000000000 Simple Page

Simple Page

zope.testbrowser-4.0.4/src/zope.testbrowser.egg-info/dependency_links.txt0000644000000000000000000000000112225722362025075 0ustar 00000000000000 zope.testbrowser-4.0.4/src/zope.testbrowser.egg-info/PKG-INFO0000644000000000000000000020555212225722362022135 0ustar 00000000000000Metadata-Version: 1.1 Name: zope.testbrowser Version: 4.0.4 Summary: Programmable browser for functional black-box tests Home-page: http://pypi.python.org/pypi/zope.testbrowser Author: Zope Corporation and Contributors Author-email: zope-dev@zope.org License: ZPL 2.1 Description: .. contents:: |buildstatus|_ ``zope.testbrowser`` provides an easy-to-use programmable web browser with special focus on testing. It is used in Zope, but it's not Zope specific at all. For instance, it can be used to test or otherwise interact with any web site. .. |buildstatus| image:: https://api.travis-ci.org/zopefoundation/zope.testbrowser.png?branch=master .. _buildstatus: https://travis-ci.org/zopefoundation/zope.testbrowser ====================== Detailed Documentation ====================== Different Browsers ------------------ HTTP Browser ~~~~~~~~~~~~ The ``zope.testbrowser.browser`` module exposes a ``Browser`` class that simulates a web browser similar to Mozilla Firefox or IE. >>> from zope.testbrowser.browser import Browser >>> browser = Browser() This version of the browser object can be used to access any web site just as you would do using a normal web browser. WSGI Test Browser ~~~~~~~~~~~~~~~~~ General usage +++++++++++++ There is also a special version of the ``Browser`` class which uses `WebTest`_ and can be used to do functional testing of WSGI applications. It can be imported from ``zope.testbrowser.wsgi``: >>> from zope.testbrowser.wsgi import Browser >>> from zope.testbrowser.tests.test_wsgi import demo_app >>> browser = Browser('http://localhost/', wsgi_app=demo_app) >>> print browser.contents Hello world! ... .. _`WebTest`: http://pypi.python.org/pypi/WebTest To use this browser you have to: * use the `wsgi` extra of the ``zope.testbrowser`` egg, You can also use it with zope layers by: * write a subclass of ``zope.testbrowser.wsgi.Layer`` and override the ``make_wsgi_app`` method, * use an instance of the class as the test layer of your test. Example: >>> import zope.testbrowser.wsgi >>> class SimpleLayer(zope.testbrowser.wsgi.Layer): ... def make_wsgi_app(self): ... return simple_app Where ``simple_app`` is the callable of your WSGI application. Testing a Zope 2/Zope 3/Bluebream WSGI application ++++++++++++++++++++++++++++++++++++++++++++++++++ When testing a Zope 2/Zope 3/Bluebream WSGI application you should wrap your WSGI application under test into ``zope.testbrowser.wsgi.AuthorizationMiddleware`` as all these application servers expect basic authentication headers to be base64 encoded. This middleware handles this for you. Example when using the layer: >>> import zope.testbrowser.wsgi >>> class ZopeSimpleLayer(zope.testbrowser.wsgi.Layer): ... def make_wsgi_app(self): ... return zope.testbrowser.wsgi.AuthorizationMiddleware(simple_app) There is also a BrowserLayer in `zope.app.wsgi.testlayer`_ which does this for you and includes a ``TransactionMiddleware``, too, which could be handy when testing a ZODB based application. .. _`zope.app.wsgi.testlayer` : http://pypi.python.org/pypi/zope.app.wsgi Bowser Usage ------------ We will test this browser against a WSGI test application: >>> from zope.testbrowser.ftests.wsgitestapp import WSGITestApplication >>> wsgi_app = WSGITestApplication() An initial page to load can be passed to the ``Browser`` constructor: >>> browser = Browser('http://localhost/@@/testbrowser/simple.html', wsgi_app=wsgi_app) >>> browser.url 'http://localhost/@@/testbrowser/simple.html' The browser can send arbitrary headers; this is helpful for setting the "Authorization" header or a language value, so that your tests format values the way you expect in your tests, if you rely on zope.i18n locale-based formatting or a similar approach. >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw') >>> browser.addHeader('Accept-Language', 'en-US') An existing browser instance can also `open` web pages: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.url 'http://localhost/@@/testbrowser/simple.html' Once you have opened a web page initially, best practice for writing testbrowser doctests suggests using 'click' to navigate further (as discussed below), except in unusual circumstances. The test browser complies with the IBrowser interface; see ``zope.testbrowser.interfaces`` for full details on the interface. >>> from zope.testbrowser import interfaces >>> from zope.interface.verify import verifyObject >>> verifyObject(interfaces.IBrowser, browser) True Page Contents ------------- The contents of the current page are available: >>> print browser.contents Simple Page

Simple Page

Making assertions about page contents is easy. >>> '

Simple Page

' in browser.contents True Utilizing the doctest facilities, it also possible to do: >>> browser.contents '...

Simple Page

...' Note: Unfortunately, ellipsis (...) cannot be used at the beginning of the output (this is a limitation of doctest). Checking for HTML ----------------- Not all URLs return HTML. Of course our simple page does: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.isHtml True But if we load an image (or other binary file), we do not get HTML: >>> browser.open('http://localhost/@@/testbrowser/zope3logo.gif') >>> browser.isHtml False HTML Page Title ---------------- Another useful helper property is the title: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.title 'Simple Page' If a page does not provide a title, it is simply ``None``: >>> browser.open('http://localhost/@@/testbrowser/notitle.html') >>> browser.title However, if the output is not HTML, then an error will occur trying to access the title: >>> browser.open('http://localhost/@@/testbrowser/zope3logo.gif') >>> browser.title Traceback (most recent call last): ... BrowserStateError: not viewing HTML Headers ------- As you can see, the `contents` of the browser does not return any HTTP headers. The headers are accessible via a separate attribute, which is an ``httplib.HTTPMessage`` instance (httplib is a part of Python's standard library): >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.headers The headers can be accessed as a string: >>> print browser.headers Status: 200 OK Content-Length: 123 Content-Type: text/html;charset=utf-8 Or as a mapping: >>> browser.headers['content-type'] 'text/html;charset=utf-8' Cookies ------- When a Set-Cookie header is available, it can be found in the headers, as seen above. Here, we use a view that will make the server set cookies with the values we provide. >>> browser.open('http://localhost/set_cookie.html?name=foo&value=bar') >>> browser.headers['set-cookie'].replace(';', '') 'foo=bar' It is also available in the browser's ``cookies`` attribute. This is an extended mapping interface that allows getting, setting, and deleting the cookies that the browser is remembering *for the current url*. Here are a few examples. >>> browser.cookies['foo'] 'bar' >>> browser.cookies.keys() ['foo'] >>> browser.cookies.values() ['bar'] >>> browser.cookies.items() [('foo', 'bar')] >>> 'foo' in browser.cookies True >>> 'bar' in browser.cookies False >>> len(browser.cookies) 1 >>> print(dict(browser.cookies)) {'foo': 'bar'} >>> browser.cookies['sha'] = 'zam' >>> len(browser.cookies) 2 >>> sorted(browser.cookies.items()) [('foo', 'bar'), ('sha', 'zam')] >>> browser.open('http://localhost/get_cookie.html') >>> print browser.headers.get('set-cookie') None >>> print browser.contents # server got the cookie change foo: bar sha: zam >>> sorted(browser.cookies.items()) [('foo', 'bar'), ('sha', 'zam')] >>> browser.cookies.clearAll() >>> len(browser.cookies) 0 Many more examples, and a discussion of the additional methods available, can be found in cookies.txt. Navigation and Link Objects --------------------------- If you want to simulate clicking on a link, get the link and `click` on it. In the `navigate.html` file there are several links set up to demonstrate the capabilities of the link objects and their `click` method. The simplest way to get a link is via the anchor text. In other words the text you would see in a browser (text and url searches are substring searches): >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...Link Text...' >>> link = browser.getLink('Link Text') >>> link Link objects comply with the ILink interface. >>> verifyObject(interfaces.ILink, link) True Links expose several attributes for easy access. >>> link.text 'Link Text' >>> link.tag # links can also be image maps. 'a' >>> link.url # it's normalized 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' >>> link.attrs {'href': 'navigate.html?message=By+Link+Text'} Links can be "clicked" and the browser will navigate to the referenced URL. >>> link.click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' >>> browser.contents '...Message: By Link Text...' When finding a link by its text, whitespace is normalized. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...> Link Text \n with Whitespace\tNormalization (and parens) >> link = browser.getLink('Link Text with Whitespace Normalization ' ... '(and parens)') >>> link >>> link.text 'Link Text with Whitespace Normalization (and parens)' >>> link.click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text+with+Normalization' >>> browser.contents '...Message: By Link Text with Normalization...' When a link text matches more than one link, by default the first one is chosen. You can, however, specify the index of the link and thus retrieve a later matching link: >>> browser.getLink('Link Text') >>> browser.getLink('Link Text', index=1) Note that clicking a link object after its browser page has expired will generate an error. >>> link.click() Traceback (most recent call last): ... ExpiredError You can also find the link by its URL, >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...Using the URL...' >>> browser.getLink(url='?message=By+URL').click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+URL' >>> browser.contents '...Message: By URL...' or its id: >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...By Anchor Id...' >>> browser.getLink(id='anchorid').click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Id' >>> browser.contents '...Message: By Id...' You thought we were done here? Not so quickly. The `getLink` method also supports image maps, though not by specifying the coordinates, but using the area's id: >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> link = browser.getLink(id='zope3') >>> link.tag 'area' >>> link.click() >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=Zope+3+Name' >>> browser.contents '...Message: Zope 3 Name...' Getting a nonexistent link raises an exception. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.getLink('This does not exist') Traceback (most recent call last): ... LinkNotFoundError A convenience method is provided to follow links; this uses the same arguments as `getLink`, but clicks on the link instead of returning the link object. >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.contents '...Link Text...' >>> browser.follow('Link Text') >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text' >>> browser.contents '...Message: By Link Text...' >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.follow(url='?message=By+URL') >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=By+URL' >>> browser.contents '...Message: By URL...' >>> browser.open('http://localhost/@@/testbrowser/navigate.html') >>> browser.follow(id='zope3') >>> browser.url 'http://localhost/@@/testbrowser/navigate.html?message=Zope+3+Name' >>> browser.contents '...Message: Zope 3 Name...' Attempting to follow links that don't exist raises the same exception as asking for the link object: >>> browser.follow('This does not exist') Traceback (most recent call last): ... LinkNotFoundError Other Navigation ---------------- Like in any normal browser, you can reload a page: >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.url 'http://localhost/@@/testbrowser/simple.html' >>> browser.reload() >>> browser.url 'http://localhost/@@/testbrowser/simple.html' You can also go back: >>> browser.open('http://localhost/@@/testbrowser/notitle.html') >>> browser.url 'http://localhost/@@/testbrowser/notitle.html' >>> browser.goBack() >>> browser.url 'http://localhost/@@/testbrowser/simple.html' Controls -------- One of the most important features of the browser is the ability to inspect and fill in values for the controls of input forms. To do so, let's first open a page that has a bunch of controls: >>> browser.open('http://localhost/@@/testbrowser/controls.html') Obtaining a Control ~~~~~~~~~~~~~~~~~~~ You look up browser controls with the 'getControl' method. The default first argument is 'label', and looks up the form on the basis of any associated label. >>> control = browser.getControl('Text Control') >>> control >>> browser.getControl(label='Text Control') # equivalent If you request a control that doesn't exist, the code raises a LookupError: >>> browser.getControl('Does Not Exist') Traceback (most recent call last): ... LookupError: label 'Does Not Exist' available items: ... If you request a control with an ambiguous lookup, the code raises an AmbiguityError. >>> browser.getControl('Ambiguous Control') Traceback (most recent call last): ... AmbiguityError: label 'Ambiguous Control' matches: This is also true if an option in a control is ambiguous in relation to the control itself. >>> browser.getControl('Sub-control Ambiguity') Traceback (most recent call last): ... AmbiguityError: label 'Sub-control Ambiguity' matches: Ambiguous controls may be specified using an index value. We use the control's value attribute to show the two controls; this attribute is properly introduced below. >>> browser.getControl('Ambiguous Control', index=0) >>> browser.getControl('Ambiguous Control', index=0).value 'First' >>> browser.getControl('Ambiguous Control', index=1).value 'Second' >>> browser.getControl('Sub-control Ambiguity', index=0) >>> browser.getControl('Sub-control Ambiguity', index=1).optionValue 'ambiguous' >>> browser.getControl('Sub-control Ambiguity', index=2) Traceback (most recent call last): ... LookupError: label 'Sub-control Ambiguity' Index 2 out of range, available choices are 0...1 0: 1: Label searches are against stripped, whitespace-normalized, no-tag versions of the text. Text applied to searches is also stripped and whitespace normalized. The search finds results if the text search finds the whole words of your text in a label. Thus, for instance, a search for 'Add' will match the label 'Add a Client' but not 'Address'. Case is honored. >>> browser.getControl('Label Needs Whitespace Normalization') >>> browser.getControl('label needs whitespace normalization') Traceback (most recent call last): ... LookupError: label 'label needs whitespace normalization' ... >>> browser.getControl(' Label Needs Whitespace ') >>> browser.getControl('Whitespace') >>> browser.getControl('hitespace') Traceback (most recent call last): ... LookupError: label 'hitespace' ... >>> browser.getControl('[non word characters should not confuse]') Multiple labels can refer to the same control (simply because that is possible in the HTML 4.0 spec). >>> browser.getControl('Multiple labels really') >>> browser.getControl('really are possible') >>> browser.getControl('really') # OK: ambiguous labels, but not ambiguous control A label can be connected with a control using the 'for' attribute and also by containing a control. >>> browser.getControl( ... 'Labels can be connected by containing their respective fields') Get also accepts one other search argument, 'name'. Only one of 'label' and 'name' may be used at a time. The 'name' keyword searches form field names. >>> browser.getControl(name='text-value') >>> browser.getControl(name='ambiguous-control-name') Traceback (most recent call last): ... AmbiguityError: name 'ambiguous-control-name' matches: >>> browser.getControl(name='does-not-exist') Traceback (most recent call last): ... LookupError: name 'does-not-exist' available items: ... >>> browser.getControl(name='ambiguous-control-name', index=1).value 'Second' Combining 'label' and 'name' raises a ValueError, as does supplying neither of them. >>> browser.getControl(label='Ambiguous Control', name='ambiguous-control-name') Traceback (most recent call last): ... ValueError: Supply one and only one of "label" and "name" as arguments >>> browser.getControl() Traceback (most recent call last): ... ValueError: Supply one and only one of "label" and "name" as arguments Radio and checkbox fields are unusual in that their labels and names may point to different objects: names point to logical collections of radio buttons or checkboxes, but labels may only be used for individual choices within the logical collection. This means that obtaining a radio button by label gets a different object than obtaining the radio collection by name. Select options may also be searched by label. >>> browser.getControl(name='radio-value') >>> browser.getControl('Zwei') >>> browser.getControl('One') >>> browser.getControl('Tres') Characteristics of controls and subcontrols are discussed below. Control Objects ~~~~~~~~~~~~~~~ Controls provide IControl. >>> ctrl = browser.getControl('Text Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True They have several useful attributes: - the name as which the control is known to the form: >>> ctrl.name 'text-value' - the value of the control, which may also be set: >>> ctrl.value 'Some Text' >>> ctrl.value = 'More Text' >>> ctrl.value 'More Text' - the type of the control: >>> ctrl.type 'text' - a flag describing whether the control is disabled: >>> ctrl.disabled False - and a flag to tell us whether the control can have multiple values: >>> ctrl.multiple False Additionally, controllers for select, radio, and checkbox provide IListControl. These fields have four other attributes and an additional method: >>> ctrl = browser.getControl('Multiple Select Control') >>> ctrl >>> ctrl.disabled False >>> ctrl.multiple True >>> verifyObject(interfaces.IListControl, ctrl) True - 'options' lists all available value options. >>> ctrl.options ['1', '2', '3'] - 'displayOptions' lists all available options by label. The 'label' attribute on an option has precedence over its contents, which is why our last option is 'Third' in the display. >>> ctrl.displayOptions ['Un', 'Deux', 'Third'] - 'displayValue' lets you get and set the displayed values of the control of the select box, rather than the actual values. >>> ctrl.value [] >>> ctrl.displayValue [] >>> ctrl.displayValue = ['Un', 'Deux'] >>> ctrl.displayValue ['Un', 'Deux'] >>> ctrl.value ['1', '2'] - 'controls' gives you a list of the subcontrol objects in the control (subcontrols are discussed below). >>> ctrl.controls [, , ] - The 'getControl' method lets you get subcontrols by their label or their value. >>> ctrl.getControl('Un') >>> ctrl.getControl('Deux') >>> ctrl.getControl('Trois') # label attribute >>> ctrl.getControl('Third') # contents >>> browser.getControl('Third') # ambiguous in the browser, so useful Traceback (most recent call last): ... AmbiguityError: label 'Third' matches: Finally, submit controls provide ISubmitControl, and image controls provide IImageSubmitControl, which extents ISubmitControl. These both simply add a 'click' method. For image submit controls, you may also provide a coordinates argument, which is a tuple of (x, y). These submit the forms, and are demonstrated below as we examine each control individually. ItemControl Objects ~~~~~~~~~~~~~~~~~~~ As introduced briefly above, using labels to obtain elements of a logical radio button or checkbox collection returns item controls, which are parents. Manipulating the value of these controls affects the parent control. >>> browser.getControl(name='radio-value').value ['2'] >>> browser.getControl('Zwei').optionValue # read-only. '2' >>> browser.getControl('Zwei').selected True >>> verifyObject(interfaces.IItemControl, browser.getControl('Zwei')) True >>> browser.getControl('Ein').selected = True >>> browser.getControl('Ein').selected True >>> browser.getControl('Zwei').selected False >>> browser.getControl(name='radio-value').value ['1'] >>> browser.getControl('Ein').selected = False >>> browser.getControl(name='radio-value').value [] >>> browser.getControl('Zwei').selected = True Checkbox collections behave similarly, as shown below. Controls with subcontrols-- Various Controls ~~~~~~~~~~~~~~~~ The various types of controls are demonstrated here. - Text Control The text control we already introduced above. - Password Control >>> ctrl = browser.getControl('Password Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value 'Password' >>> ctrl.value = 'pass now' >>> ctrl.value 'pass now' >>> ctrl.disabled False >>> ctrl.multiple False - Hidden Control >>> ctrl = browser.getControl(name='hidden-value') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value 'Hidden' >>> ctrl.value = 'More Hidden' >>> ctrl.disabled False >>> ctrl.multiple False - Text Area Control >>> ctrl = browser.getControl('Text Area Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value ' Text inside\n area!\n ' >>> ctrl.value = 'A lot of\n text.' >>> ctrl.disabled False >>> ctrl.multiple False - File Control File controls are used when a form has a file-upload field. To specify data, call the add_file method, passing: - A file-like object - a content type, and - a file name >>> ctrl = browser.getControl('File Control') >>> ctrl >>> verifyObject(interfaces.IControl, ctrl) True >>> ctrl.value is None True >>> import cStringIO >>> ctrl.add_file(cStringIO.StringIO('File contents'), ... 'text/plain', 'test.txt') The file control (like the other controls) also knows if it is disabled or if it can have multiple values. >>> ctrl.disabled False >>> ctrl.multiple False - Selection Control (Single-Valued) >>> ctrl = browser.getControl('Single Select Control') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value ['1'] >>> ctrl.value = ['2'] >>> ctrl.disabled False >>> ctrl.multiple False >>> ctrl.options ['1', '2', '3'] >>> ctrl.displayOptions ['Uno', 'Dos', 'Third'] >>> ctrl.displayValue ['Dos'] >>> ctrl.displayValue = ['Tres'] >>> ctrl.displayValue ['Third'] >>> ctrl.displayValue = ['Dos'] >>> ctrl.displayValue ['Dos'] >>> ctrl.displayValue = ['Third'] >>> ctrl.displayValue ['Third'] >>> ctrl.value ['3'] - Selection Control (Multi-Valued) This was already demonstrated in the introduction to control objects above. - Checkbox Control (Single-Valued; Unvalued) >>> ctrl = browser.getControl(name='single-unvalued-checkbox-value') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value True >>> ctrl.value = False >>> ctrl.disabled False >>> ctrl.multiple True >>> ctrl.options [True] >>> ctrl.displayOptions ['Single Unvalued Checkbox'] >>> ctrl.displayValue [] >>> verifyObject( ... interfaces.IItemControl, ... browser.getControl('Single Unvalued Checkbox')) True >>> browser.getControl('Single Unvalued Checkbox').optionValue 'on' >>> browser.getControl('Single Unvalued Checkbox').selected False >>> ctrl.displayValue = ['Single Unvalued Checkbox'] >>> ctrl.displayValue ['Single Unvalued Checkbox'] >>> browser.getControl('Single Unvalued Checkbox').selected True >>> browser.getControl('Single Unvalued Checkbox').selected = False >>> browser.getControl('Single Unvalued Checkbox').selected False >>> ctrl.displayValue [] >>> browser.getControl( ... name='single-disabled-unvalued-checkbox-value').disabled True - Checkbox Control (Single-Valued, Valued) >>> ctrl = browser.getControl(name='single-valued-checkbox-value') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value ['1'] >>> ctrl.value = [] >>> ctrl.disabled False >>> ctrl.multiple True >>> ctrl.options ['1'] >>> ctrl.displayOptions ['Single Valued Checkbox'] >>> ctrl.displayValue [] >>> verifyObject( ... interfaces.IItemControl, ... browser.getControl('Single Valued Checkbox')) True >>> browser.getControl('Single Valued Checkbox').selected False >>> browser.getControl('Single Valued Checkbox').optionValue '1' >>> ctrl.displayValue = ['Single Valued Checkbox'] >>> ctrl.displayValue ['Single Valued Checkbox'] >>> browser.getControl('Single Valued Checkbox').selected True >>> browser.getControl('Single Valued Checkbox').selected = False >>> browser.getControl('Single Valued Checkbox').selected False >>> ctrl.displayValue [] - Checkbox Control (Multi-Valued) >>> ctrl = browser.getControl(name='multi-checkbox-value') >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.value ['1', '3'] >>> ctrl.value = ['1', '2'] >>> ctrl.disabled False >>> ctrl.multiple True >>> ctrl.options ['1', '2', '3'] >>> ctrl.displayOptions ['One', 'Two', 'Three'] >>> ctrl.displayValue ['One', 'Two'] >>> ctrl.displayValue = ['Two'] >>> ctrl.value ['2'] >>> browser.getControl('Two').optionValue '2' >>> browser.getControl('Two').selected True >>> verifyObject(interfaces.IItemControl, browser.getControl('Two')) True >>> browser.getControl('Three').selected = True >>> browser.getControl('Three').selected True >>> browser.getControl('Two').selected True >>> ctrl.value ['2', '3'] >>> browser.getControl('Two').selected = False >>> ctrl.value ['3'] >>> browser.getControl('Three').selected = False >>> ctrl.value [] - Radio Control This is how you get a radio button based control: >>> ctrl = browser.getControl(name='radio-value') This shows the existing value of the control, as it was in the HTML received from the server: >>> ctrl.value ['2'] We can then unselect it: >>> ctrl.value = [] >>> ctrl.value [] We can also reselect it: >>> ctrl.value = ['2'] >>> ctrl.value ['2'] displayValue shows the text the user would see next to the control: >>> ctrl.displayValue ['Zwei'] This is just unit testing: >>> ctrl >>> verifyObject(interfaces.IListControl, ctrl) True >>> ctrl.disabled False >>> ctrl.multiple False >>> ctrl.options ['1', '2', '3'] >>> ctrl.displayOptions ['Ein', 'Zwei', 'Drei'] >>> ctrl.displayValue = ['Ein'] >>> ctrl.value ['1'] >>> ctrl.displayValue ['Ein'] The radio control subcontrols were illustrated above. - Image Control >>> ctrl = browser.getControl(name='image-value') >>> ctrl >>> verifyObject(interfaces.IImageSubmitControl, ctrl) True >>> ctrl.value '' >>> ctrl.disabled False >>> ctrl.multiple False - Submit Control >>> ctrl = browser.getControl(name='submit-value') >>> ctrl >>> browser.getControl('Submit This') # value of submit button is a label >>> browser.getControl('Standard Submit Control') # label tag is legal >>> browser.getControl('Submit') # multiple labels, but same control >>> verifyObject(interfaces.ISubmitControl, ctrl) True >>> ctrl.value 'Submit This' >>> ctrl.disabled False >>> ctrl.multiple False Using Submitting Controls ~~~~~~~~~~~~~~~~~~~~~~~~~ Both the submit and image type should be clickable and submit the form: >>> browser.getControl('Text Control').value = 'Other Text' >>> browser.getControl('Submit').click() >>> print browser.contents ... Other Text ... Submit This ... Note that if you click a submit object after the associated page has expired, you will get an error. >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> ctrl = browser.getControl('Submit') >>> ctrl.click() >>> ctrl.click() Traceback (most recent call last): ... ExpiredError All the above also holds true for the image control: >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> browser.getControl('Text Control').value = 'Other Text' >>> browser.getControl(name='image-value').click() >>> print browser.contents ... Other Text ... 1 1 ... >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> ctrl = browser.getControl(name='image-value') >>> ctrl.click() >>> ctrl.click() Traceback (most recent call last): ... ExpiredError But when sending an image, you can also specify the coordinate you clicked: >>> browser.open('http://localhost/@@/testbrowser/controls.html') >>> browser.getControl(name='image-value').click((50,25)) >>> print browser.contents ... 50 25 ... Pages Without Controls ~~~~~~~~~~~~~~~~~~~~~~ What would happen if we tried to look up a control on a page that has none? >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.getControl('anything') Traceback (most recent call last): ... LookupError: label 'anything' (there are no form items in the HTML) Forms ----- Because pages can have multiple forms with like-named controls, it is sometimes necessary to access forms by name or id. The browser's `forms` attribute can be used to do so. The key value is the form's name or id. If more than one form has the same name or id, the first one will be returned. >>> browser.open('http://localhost/@@/testbrowser/forms.html') >>> form = browser.getForm(name='one') Form instances conform to the IForm interface. >>> verifyObject(interfaces.IForm, form) True The form exposes several attributes related to forms: - The name of the form: >>> form.name 'one' - The id of the form: >>> form.id '1' - The action (target URL) when the form is submitted: >>> form.action 'http://localhost/@@/testbrowser/forms.html' - The method (HTTP verb) used to transmit the form data: >>> form.method 'GET' Besides those attributes, you have also a couple of methods. Like for the browser, you can get control objects, but limited to the current form... >>> form.getControl(name='text-value') ...and submit the form. >>> form.submit('Submit') >>> print browser.contents ... First Text ... Submitting also works without specifying a control, as shown below, which is it's primary reason for existing in competition with the control submission discussed above. Now let me show you briefly that looking up forms is sometimes important. In the `forms.html` template, we have four forms all having a text control named `text-value`. Now, if I use the browser's `get` method, >>> browser.getControl(name='text-value') Traceback (most recent call last): ... AmbiguityError: name 'text-value' matches: >>> browser.getControl('Text Control') Traceback (most recent call last): ... AmbiguityError: label 'Text Control' matches: I'll always get an ambiguous form field. I can use the index argument, or with the `getForm` method I can disambiguate by searching only within a given form: >>> form = browser.getForm('2') >>> form.getControl(name='text-value').value 'Second Text' >>> form.submit('Submit') >>> browser.contents '...Second Text...' >>> form = browser.getForm('2') >>> form.getControl('Submit').click() >>> browser.contents '...Second Text...' >>> browser.getForm('3').getControl('Text Control').value 'Third Text' The last form on the page does not have a name, an id, or a submit button. Working with it is still easy, thanks to a index attribute that guarantees order. (Forms without submit buttons are sometimes useful for JavaScript.) >>> form = browser.getForm(index=3) >>> form.submit() >>> browser.contents '...Fourth Text...Submitted without the submit button....' If a form is requested that does not exists, an exception will be raised. >>> form = browser.getForm('does-not-exist') Traceback (most recent call last): LookupError If the HTML page contains only one form, no arguments to `getForm` are needed: >>> oneform = Browser(wsgi_app=wsgi_app) >>> oneform.open('http://localhost/@@/testbrowser/oneform.html') >>> form = oneform.getForm() If the HTML page contains more than one form, `index` is needed to disambiguate if no other arguments are provided: >>> browser.getForm() Traceback (most recent call last): ValueError: if no other arguments are given, index is required. Submitting a posts body directly -------------------------------- In addition to the open method, zope.testbrowser.testing.Browser has a ``post`` method that allows a request body to be supplied. This method is particularly helpful when testing Ajax methods. Let's visit a page that echos some interesting values from it's request: >>> browser.open('http://localhost/echo.html') >>> print browser.contents HTTP_ACCEPT_LANGUAGE: en-US HTTP_CONNECTION: close HTTP_HOST: localhost HTTP_USER_AGENT: Python-urllib/2.4 PATH_INFO: /echo.html REQUEST_METHOD: GET Body: '' Now, we'll try a post. The post method takes a URL, a data string, and an optional content type. If we just pass a string, then a URL-encoded query string is assumed: >>> browser.post('http://localhost/echo.html', 'x=1&y=2') >>> print browser.contents CONTENT_LENGTH: 7 CONTENT_TYPE: application/x-www-form-urlencoded HTTP_ACCEPT_LANGUAGE: en-US HTTP_CONNECTION: close HTTP_HOST: localhost HTTP_USER_AGENT: Python-urllib/2.4 PATH_INFO: /echo.html REQUEST_METHOD: POST x: 1 y: 2 Body: '' The body is empty because it is consumed to get form data. We can pass a content-type explicitly: >>> browser.post('http://localhost/echo.html', ... '{"x":1,"y":2}', 'application/x-javascript') >>> print browser.contents CONTENT_LENGTH: 13 CONTENT_TYPE: application/x-javascript HTTP_ACCEPT_LANGUAGE: en-US HTTP_CONNECTION: close HTTP_HOST: localhost HTTP_USER_AGENT: Python-urllib/2.4 PATH_INFO: /echo.html REQUEST_METHOD: POST Body: '{"x":1,"y":2}' Here, the body is left in place because it isn't form data. Performance Testing ------------------- Browser objects keep up with how much time each request takes. This can be used to ensure a particular request's performance is within a tolerable range. Be very careful using raw seconds, cross-machine differences can be huge, pystones is usually a better choice. >>> browser.open('http://localhost/@@/testbrowser/simple.html') >>> browser.lastRequestSeconds < 10 # really big number for safety True >>> browser.lastRequestPystones < 10000 # really big number for safety True Handling Errors --------------- Often WSGI middleware or the application itself gracefully handle application errors, such as invalid URLs: >>> browser.open('http://localhost/invalid') Traceback (most recent call last): ... HTTPError: HTTP Error 404: Not Found Note that the above error was thrown by ``mechanize`` and not by the application. For debugging purposes, however, it can be very useful to see the original exception caused by the application. In those cases you can set the ``handleErrors`` property of the browser to ``False``. It is defaulted to ``True``: >>> browser.handleErrors True So when we tell the application not to handle the errors, >>> browser.handleErrors = False we get a different, internal error from the application: >>> browser.open('http://localhost/invalid') Traceback (most recent call last): ... NotFound: /invalid NB: Setting the handleErrors attribute to False will only change anything if the WSGI application obeys the wsgi.handleErrors or paste.throw_errors WSGI environment variables. i.e. it does not catch and handle the original exception when these are set appropriately. When the testbrowser is raising HttpErrors, the errors still hit the test. Sometimes we don't want that to happen, in situations where there are edge cases that will cause the error to be predictably but infrequently raised. Time is a primary cause of this. To get around this, one can set the raiseHttpErrors to False. >>> browser.handleErrors = True >>> browser.raiseHttpErrors = False This will cause HttpErrors not to propagate. >>> browser.open('http://localhost/invalid') The headers are still there, though. >>> '404 Not Found' in str(browser.headers) True If we don't handle the errors, and allow internal ones to propagate, however, this flag doesn't affect things. >>> browser.handleErrors = False >>> browser.open('http://localhost/invalid') Traceback (most recent call last): ... NotFound: /invalid >>> browser.raiseHttpErrors = True Hand-Holding ------------ Instances of the various objects ensure that users don't set incorrect instance attributes accidentally. >>> browser.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Browser' object has no attribute 'nonexistant' >>> form.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Form' object has no attribute 'nonexistant' >>> control.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Control' object has no attribute 'nonexistant' >>> link.nonexistant = None Traceback (most recent call last): ... AttributeError: 'Link' object has no attribute 'nonexistant' HTTPS support ------------- Depending on the scheme of the request the variable wsgi.url_scheme will be set correctly on the request: >>> browser.open('http://localhost/echo_one.html?var=wsgi.url_scheme') >>> print browser.contents 'http' >>> browser.open('https://localhost/echo_one.html?var=wsgi.url_scheme') >>> print browser.contents 'https' see http://www.python.org/dev/peps/pep-3333/ for details. ======= CHANGES ======= 4.0.4 (2013-10-11) ------------------ - Removed the 'WebTest <= 1.3.4' version pin, fixed tests to work with modern WebTest versions (https://github.com/zopefoundation/zope.testbrowser/issues/10). 4.0.3 (2013-09-04) ------------------ - pinning version 'WebTest <= 1.3.4', because of some incompatibility and test failures - Make zope.testbrowser installable via pip (https://github.com/zopefoundation/zope.testbrowser/issues/6). - When ``Browser.handleErrors`` is False, also add ``x-wsgiorg.throw_errors`` to the environment. http://wsgi.org/wsgi/Specifications/throw_errors - Prevent WebTest from always sending ``paste.throw_errors=True`` in the environment by setting it to ``None`` when ``Browser.handleErrors`` is ``True``. This makes it easier to test error pages. - Made Browser.submit() handle ``raiseHttpErrors`` (https://github.com/zopefoundation/zope.testbrowser/pull/4). - More friendly error messages from getControl() et al: - when you specify an index that is out of bounds, show the available choices - when you fail to find anything, show all the available items 4.0.2 (2011-05-25) ------------------ - Remove test dependency on zope.pagetemplate. 4.0.1 (2011-05-04) ------------------ - Added a hint in documentation how to use ``zope.testbrowser.wsgi.Browser`` to test a Zope 2/Zope 3/Bluebream WSGI application. 4.0.0 (2011-03-14) ------------------ - LP #721252: AmbiguityError now shows all matching controls. - Integrate with WebTest. ``zope.testbrowser.wsgi.Browser`` is a ``Browser`` implementation that uses ``webtest.TestApp`` to drive a WSGI application. This this replaces the wsgi_intercept support added in 3.11. - Re-write the test application as a pure WSGI application using WebOb. Run the existing tests using the WebTest based Browser - Move zope.app.testing based Browser into ``zope.app.testing`` (leaving backwards compatibility imports in-place). Released in ``zope.app.testing`` 3.9.0. 3.11.1 (2011-01-24) ------------------- - Fixing brown bag release 3.11.0. 3.11.0 (2011-01-24) ------------------- - Added `wsgi_intercept` support (came from ``zope.app.wsgi.testlayer``). 3.10.4 (2011-01-14) ------------------- - Move the over-the-wire.txt doctest out of the TestBrowserLayer as it doesn't need or use it. - Fix test compatibility with zope.app.testing 3.8.1. 3.10.3 (2010-10-15) ------------------- - Fixed backwards compatibility with ``zope.app.wsgi.testlayer``. 3.10.2 (2010-10-15) ------------------- - Fixed Python 2.7 compatibility in Browser.handleErrors. 3.10.1 (2010-09-21) ------------------- - Fixed a bug that caused the ``Browser`` to keep it's previous ``contents`` The places are: - Link.click() - SubmitControl.click() - ImageControl.click() - Form.submit() - Also adjusted exception messages at the above places to match pre version 3.4.1 messages. 3.10.0 (2010-09-14) ------------------- - LP #98437: use mechanize's built-in ``submit()`` to submit forms, allowing mechanize to set the "Referer:" (sic) header appropriately. - Fixed tests to run with ``zope.app.testing`` 3.8 and above. 3.9.0 (2010-05-17) ------------------ - LP #568806: Update dependency ``mechanize >= 0.2.0``, which now includes the ``ClientForm`` APIs. Remove use of ``urllib2`` APIs (incompatible with ``mechanize 0.2.0``) in favor of ``mechanize`` equivalents. Thanks to John J. Lee for the patch. - Use stdlib ``doctest`` module, instead of ``zope.testing.doctest``. - **Caution:** This version is no longer fully compatible with Python 2.4: ``handleErrors = False`` no longer works. 3.8.1 (2010-04-19) ------------------ - Pinned dependency on mechanize to prevent use of the upcoming 0.2.0 release before we have time to adjust to its API changes. - LP #98396: testbrowser resolves relative URLs incorrectly. 3.8.0 (2010-03-05) ------------------ - Added ``follow`` convenience method which gets and follows a link. 3.7.0 (2009-12-17) ------------------ - Moved zope.app.testing dependency into the scope of the PublisherConnection class. Zope2 specifies its own PublisherConnection which isn't dependent on zope.app.testing. - Fixed LP #419119: return None when the browser has no contents instead of raising an exception. 3.7.0a1 (2009-08-29) -------------------- - Remove dependency on zope.app.publisher in favor of zope.browserpage, zope.browserresource and zope.ptresource. - Remove dependencies on zope.app.principalannotation and zope.securitypolicy by using the simple PermissiveSecurityPolicy. We aren't testing security in our tests. - Replaced the testing dependency on zope.app.zcmlfiles with explicit dependencies of a minimal set of packages. - Remove unneeded zope.app.authentication from ftesting.zcml. - Test dependency on zope.securitypolicy instead of its app variant. 3.6.0a2 (2009-01-31) -------------------- - Test dependency on zope.site.folder instead of zope.app.folder. - Remove useless test dependency in zope.app.component. 3.6.0a1 (2009-01-08) -------------------- - Author e-mail to zope-dev rather than zope3-dev. - New lines are no longer stripped in XML and HTML code contained in a textarea; fix requires ClientForm >= 0.2.10 (LP #268139). - Added ``cookies`` attribute to browser for easy manipulation of browser cookies. See brief example in main documentation, plus new ``cookies.txt`` documentation. 3.5.1 (2008-10-10) ------------------ - Provide a work around for a mechanize/urllib2 bug on Python 2.6 missing 'timeout' attribute on 'Request' base class. - Provide a work around for a mechanize/urllib2 bug in creating request objects that won't handle fragment URLs correctly. 3.5.0 (2008-03-30) ------------------ - Added a zope.testbrowser.testing.Browser.post method that allows tests to supply a body and a content type. This is handy for testing Ajax requests with non-form input (e.g. JSON). - Remove vendor import of mechanize. - Fix bug that caused HTTP exception tracebacks to differ between version 3.4.0 and 3.4.1. - Workaround for bug in Python Cookie.SimpleCookie when handling unicode strings. - Fix bug introduced in 3.4.1 that created incompatible tracebacks in doctests. This necessitated adding a patched mechanize to the source tree; patches have been sent to the mechanize project. - Fix https://bugs.launchpad.net/bugs/149517 by adding zope.interface and zope.schema as real dependencies - Fix browser.getLink documentation that was not updated since the last API modification. - Move tests for fixed bugs to a separate file. - Removed non-functional and undocumented code intended to help test servers using virtual hosting. 3.4.2 (2007-10-31) ------------------ - Resolve ``ZopeSecurityPolicy`` deprecation warning. 3.4.1 (2007-09-01) ------------------ * Updated to mechanize 0.1.7b and ClientForm 0.2.7. These are now pulled in via egg dependencies. * ``zope.testbrowser`` now works on Python 2.5. 3.4.0 (2007-06-04) ------------------ * Added the ability to suppress raising exceptions on HTTP errors (``raiseHttpErrors`` attribute). * Made the tests more resilient to HTTP header formatting changes with the REnormalizer. 3.4.0a1 (2007-04-22) -------------------- Initial release as a separate project, corresponds to zope.testbrowser from Zope 3.4.0a1 Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Zope Public License Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Internet :: WWW/HTTP zope.testbrowser-4.0.4/src/zope.testbrowser.egg-info/not-zip-safe0000644000000000000000000000000112225722360023253 0ustar 00000000000000 zope.testbrowser-4.0.4/src/zope.testbrowser.egg-info/requires.txt0000644000000000000000000000034212225722362023426 0ustar 00000000000000mechanize>=0.2.0 setuptools zope.interface zope.schema pytz > dev [test] zope.testing WebTest [zope-functional-testing] zope.app.testing >= 3.9.0dev [test_bbb] zope.testbrowser [test,zope-functional-testing] [wsgi] WebTestzope.testbrowser-4.0.4/src/zope.testbrowser.egg-info/SOURCES.txt0000644000000000000000000000315012225722362022712 0ustar 00000000000000.travis.yml CHANGES.rst COPYRIGHT.rst LICENSE.rst MANIFEST.in README.rst bootstrap.py buildout.cfg setup.py tox.ini src/zope/__init__.py src/zope.testbrowser.egg-info/PKG-INFO src/zope.testbrowser.egg-info/SOURCES.txt src/zope.testbrowser.egg-info/dependency_links.txt src/zope.testbrowser.egg-info/namespace_packages.txt src/zope.testbrowser.egg-info/not-zip-safe src/zope.testbrowser.egg-info/requires.txt src/zope.testbrowser.egg-info/top_level.txt src/zope/testbrowser/README.txt src/zope/testbrowser/__init__.py src/zope/testbrowser/browser.py src/zope/testbrowser/connection.py src/zope/testbrowser/cookies.py src/zope/testbrowser/cookies.txt src/zope/testbrowser/fixed-bugs.txt src/zope/testbrowser/interfaces.py src/zope/testbrowser/over_the_wire.txt src/zope/testbrowser/testing.py src/zope/testbrowser/wsgi.py src/zope/testbrowser/ftests/__init__.py src/zope/testbrowser/ftests/controls.html src/zope/testbrowser/ftests/cookies.html src/zope/testbrowser/ftests/forms.html src/zope/testbrowser/ftests/fragment.html src/zope/testbrowser/ftests/navigate.html src/zope/testbrowser/ftests/notitle.html src/zope/testbrowser/ftests/oneform.html src/zope/testbrowser/ftests/radio.html src/zope/testbrowser/ftests/simple.html src/zope/testbrowser/ftests/status_lead.html src/zope/testbrowser/ftests/textarea.html src/zope/testbrowser/ftests/wsgitestapp.py src/zope/testbrowser/ftests/zope3logo.gif src/zope/testbrowser/tests/__init__.py src/zope/testbrowser/tests/helper.py src/zope/testbrowser/tests/test_bbb.py src/zope/testbrowser/tests/test_browser.py src/zope/testbrowser/tests/test_doctests.py src/zope/testbrowser/tests/test_wsgi.pyzope.testbrowser-4.0.4/src/zope.testbrowser.egg-info/namespace_packages.txt0000644000000000000000000000000512225722362025355 0ustar 00000000000000zope zope.testbrowser-4.0.4/src/zope.testbrowser.egg-info/top_level.txt0000644000000000000000000000000512225722362023554 0ustar 00000000000000zope