zope.testbrowser-4.0.4/COPYRIGHT.rst 0000644 0000000 0000000 00000000040 12225722334 015302 0 ustar 0000000 0000000 Zope Foundation and Contributors zope.testbrowser-4.0.4/bootstrap.py 0000644 0000000 0000000 00000024435 12225722334 015605 0 ustar 0000000 0000000 ##############################################################################
#
# 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.in 0000644 0000000 0000000 00000000251 12225722334 014742 0 ustar 0000000 0000000 include *.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.rst 0000644 0000000 0000000 00000000651 12225722334 014677 0 ustar 0000000 0000000 |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.rst 0000644 0000000 0000000 00000016363 12225722334 015021 0 ustar 0000000 0000000 =======
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.ini 0000644 0000000 0000000 00000000164 12225722334 014522 0 ustar 0000000 0000000 [tox]
envlist =
py26,py27
[testenv]
deps =
zope.testing
WebTest
commands =
python setup.py test -q
zope.testbrowser-4.0.4/setup.py 0000644 0000000 0000000 00000004703 12225722334 014724 0 ustar 0000000 0000000 ##############################################################################
#
# 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.cfg 0000644 0000000 0000000 00000000073 12225722370 015027 0 ustar 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
zope.testbrowser-4.0.4/PKG-INFO 0000644 0000000 0000000 00000205552 12225722370 014314 0 ustar 0000000 0000000 Metadata-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
...
11
...
>>> 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
...
5025
...
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.cfg 0000644 0000000 0000000 00000001340 12225722334 015514 0 ustar 0000000 0000000 [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.rst 0000644 0000000 0000000 00000004026 12225722334 015024 0 ustar 0000000 0000000 Zope 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.yml 0000644 0000000 0000000 00000000242 12225722334 015315 0 ustar 0000000 0000000 language: 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__.py 0000644 0000000 0000000 00000000070 12225722334 017060 0 ustar 0000000 0000000 __import__('pkg_resources').declare_namespace(__name__)
zope.testbrowser-4.0.4/src/zope/testbrowser/interfaces.py 0000644 0000000 0000000 00000037177 12225722334 022051 0 ustar 0000000 0000000 ##############################################################################
#
# 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.py 0000644 0000000 0000000 00000067251 12225722334 021405 0 ustar 0000000 0000000 ##############################################################################
#
# 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.py 0000644 0000000 0000000 00000006605 12225722334 022055 0 ustar 0000000 0000000 ##############################################################################
#
# 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.txt 0000644 0000000 0000000 00000062335 12225722334 021543 0 ustar 0000000 0000000 =======
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__.py 0000644 0000000 0000000 00000001255 12225722334 021451 0 ustar 0000000 0000000 ##############################################################################
#
# 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.txt 0000644 0000000 0000000 00000131150 12225722334 021034 0 ustar 0000000 0000000 ======================
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