[Checkins] SVN: zc.testbrowser/trunk/ initial import of
already-in-progress zc.testbrowser with "real"
Benji York
benji at zope.com
Wed Sep 19 05:34:25 EDT 2007
Log message for revision 79746:
initial import of already-in-progress zc.testbrowser with "real"
Changed:
_U zc.testbrowser/trunk/
A zc.testbrowser/trunk/README.txt
A zc.testbrowser/trunk/buildout.cfg
A zc.testbrowser/trunk/setup.py
A zc.testbrowser/trunk/src/
A zc.testbrowser/trunk/src/zc/
A zc.testbrowser/trunk/src/zc/__init__.py
A zc.testbrowser/trunk/src/zc/testbrowser/
A zc.testbrowser/trunk/src/zc/testbrowser/README.txt
A zc.testbrowser/trunk/src/zc/testbrowser/__init__.py
A zc.testbrowser/trunk/src/zc/testbrowser/browser.py
A zc.testbrowser/trunk/src/zc/testbrowser/dummymodules.py
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/__init__.py
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/controls.html
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/forms.html
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/ftesting.zcml
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/index.html
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/navigate.html
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/notitle.html
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/oneform.html
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/target.html
A zc.testbrowser/trunk/src/zc/testbrowser/ftests/zope3logo.gif
A zc.testbrowser/trunk/src/zc/testbrowser/headers.txt
A zc.testbrowser/trunk/src/zc/testbrowser/interfaces.py
A zc.testbrowser/trunk/src/zc/testbrowser/real.js
A zc.testbrowser/trunk/src/zc/testbrowser/real.py
A zc.testbrowser/trunk/src/zc/testbrowser/real.txt
A zc.testbrowser/trunk/src/zc/testbrowser/screen-shots.txt
A zc.testbrowser/trunk/src/zc/testbrowser/tests.py
-=-
Property changes on: zc.testbrowser/trunk
___________________________________________________________________
Name: svn:ignore
+ develop-eggs
bin
parts
.installed.cfg
Added: zc.testbrowser/trunk/README.txt
===================================================================
--- zc.testbrowser/trunk/README.txt (rev 0)
+++ zc.testbrowser/trunk/README.txt 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,22 @@
+Overview
+========
+
+The zc.testbrowser package provides web user agents (browsers) with
+programmatic interfaces designed to be used for testing web applications,
+especially in conjunction with doctests. This project originates in the Zope 3
+community, but is not Zope-specific.
+
+There are currently three type of testbrowser provided. One for accessing web
+sites via HTTP (zc.testbrowser.browser), one that controls a Firefox web
+browser (zc.testbrowser.real), and one for directly accessing a Zope 3
+application (zope.testbrowser.testing, available seperately).
+
+
+Changes
+=======
+
+1.0 (unreleased)
+----------------
+
+First release under new name (non Zope-specific code extracted from
+zope.testbrowser)
Property changes on: zc.testbrowser/trunk/README.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/buildout.cfg
===================================================================
--- zc.testbrowser/trunk/buildout.cfg (rev 0)
+++ zc.testbrowser/trunk/buildout.cfg 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,24 @@
+[buildout]
+develop = .
+parts = test
+versions = versions
+index = http://download.zope.org/ppix/
+
+[test]
+recipe = zc.recipe.testrunner
+defaults = ['-1', '--auto-color']
+eggs = zc.testbrowser
+
+[versions]
+ClientForm = 0.2.7
+mechanize = 0.1.7b
+setuptools = 0.6c7
+simplejson = 1.7.1
+zc.buildout = 1.0.0b30
+zc.recipe.egg = 1.0.0b6
+zc.recipe.testrunner = 1.0.0b8
+zope.event = 3.4.0
+zope.i18nmessageid = 3.4.0
+zope.interface = 3.4.0
+zope.schema = 3.4.0b1dev-r77624
+zope.testing = 3.5.1
Added: zc.testbrowser/trunk/setup.py
===================================================================
--- zc.testbrowser/trunk/setup.py (rev 0)
+++ zc.testbrowser/trunk/setup.py 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,55 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+import os
+from setuptools import setup, find_packages
+
+long_description = (
+ '.. contents::\n\n'
+ + open('README.txt').read()
+ + '\n\n'
+ + open(os.path.join('src', 'zc', 'testbrowser', 'README.txt')).read()
+ )
+
+setup(
+ name = 'zc.testbrowser',
+ version = '3.4.2dev',
+ url = 'http://pypi.python.org/pypi/zc.testbrowser',
+ license = 'ZPL 2.1',
+ description = 'Programmable browser for functional black-box tests',
+ author = 'Zope Corporation and Contributors',
+ author_email = 'zope3-dev at zope.org',
+ long_description = long_description,
+ classifiers=['Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Zope Public License',
+ 'Programming Language :: Python',
+ 'Topic :: Software Development :: Testing',
+ 'Topic :: Internet :: WWW/HTTP',
+ ],
+
+ packages = find_packages('src'),
+ package_dir = {'': 'src'},
+ namespace_packages = ['zc',],
+ tests_require = ['zope.testing'],
+ install_requires = [
+ 'ClientForm',
+ 'mechanize',
+ 'setuptools',
+ 'simplejson',
+ 'zope.interface',
+ 'zope.schema',
+ ],
+ include_package_data = True,
+ zip_safe = False,
+ )
Property changes on: zc.testbrowser/trunk/setup.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/__init__.py
===================================================================
--- zc.testbrowser/trunk/src/zc/__init__.py (rev 0)
+++ zc.testbrowser/trunk/src/zc/__init__.py 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,7 @@
+# this is a namespace package
+try:
+ import pkg_resources
+ pkg_resources.declare_namespace(__name__)
+except ImportError:
+ import pkgutil
+ __path__ = pkgutil.extend_path(__path__, __name__)
Property changes on: zc.testbrowser/trunk/src/zc/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/README.txt
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/README.txt (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/README.txt 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,1068 @@
+Detailed Documentation
+======================
+
+Before being of much interest, we need to open a web page. ``Browser``
+instances have a ``base`` attribute that sets the URL from which ``open``ed
+URLs are relative. This lets you target tests at servers running in various,
+or even variable locations (like using randomly chosen ports).
+
+ >>> browser = Browser()
+ >>> browser.base = 'http://localhost:%s/' % TEST_PORT
+ >>> browser.open('index.html')
+ >>> browser.url
+ 'http://localhost:.../index.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
+``zc.testbrowser.interfaces`` for full details on the interface.
+
+ >>> import zc.testbrowser.interfaces
+ >>> from zope.interface.verify import verifyObject
+ >>> zc.testbrowser.interfaces.IBrowser.providedBy(browser)
+ True
+
+
+Page Contents
+-------------
+
+The contents of the current page are available:
+
+ >>> browser.contents
+ '...<h1>Simple Page</h1>...'
+
+Note: Unfortunately, ellipsis (...) cannot be used at the beginning of the
+output (this is a limitation of doctest).
+
+Making assertions about page contents is easy.
+
+ >>> '<h1>Simple Page</h1>' in browser.contents
+ True
+
+
+Checking for HTML
+-----------------
+
+Not all URLs return HTML. Of course our simple page does:
+
+ >>> browser.isHtml
+ True
+
+But if we load an image (or other binary file), we do not get HTML:
+
+ >>> browser.open('zope3logo.gif')
+ >>> browser.isHtml
+ False
+
+
+HTML Page Title
+----------------
+
+Another useful helper property is the title:
+
+ >>> browser.open('index.html')
+ >>> browser.title
+ 'Simple Page'
+
+If a page does not provide a title, it is simply ``None``:
+
+ >>> browser.open('notitle.html')
+ >>> browser.title
+
+However, if the output is not HTML, then an error will occur trying to access
+the title:
+
+ >>> browser.open('zope3logo.gif')
+ >>> browser.title
+ Traceback (most recent call last):
+ ...
+ BrowserStateError: not viewing HTML
+
+
+Navigation and Link Objects
+---------------------------
+
+If you want to simulate clicking on a link, get the link and call its `click`
+method. 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:
+
+ >>> browser.open('navigate.html')
+ >>> browser.contents
+ '...<a href="target.html">Link Text</a>...'
+ >>> link = browser.getLink('Link Text')
+ >>> link
+ <Link text='Link Text' url='http://localhost:.../target.html'>
+
+Link objects comply with the ILink interface.
+
+ >>> verifyObject(zc.testbrowser.interfaces.ILink, link)
+ True
+
+Links expose several attributes for easy access.
+
+ >>> link.text
+ 'Link Text'
+
+Links can be "clicked" and the browser will navigate to the referenced URL.
+
+ >>> link.click()
+ >>> browser.url
+ 'http://localhost:.../target.html'
+ >>> browser.contents
+ '...This page is the target of a link...'
+
+When finding a link by its text, whitespace is normalized.
+
+ >>> browser.open('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.text
+ 'Link Text with Whitespace Normalization (and parens)'
+ >>> link.click()
+ >>> browser.url
+ 'http://localhost:.../target.html'
+
+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.open('navigate.html')
+ >>> browser.getLink('Link Text')
+ <Link text='Link Text' ...>
+
+ >>> browser.getLink('Link Text', index=1)
+ <Link text='Link Text with Whitespace Normalization (and parens)' ...>
+
+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 links by URL,
+
+ >>> browser.open('navigate.html')
+ >>> browser.getLink(url='target.html').click()
+ >>> browser.url
+ 'http://localhost:.../target.html'
+
+or its id:
+
+ >>> browser.open('navigate.html')
+ >>> browser.contents
+ '...<a href="target.html" id="anchorid">By Anchor Id</a>...'
+
+ >>> browser.getLink(id='anchorid').click()
+ >>> browser.url
+ 'http://localhost:.../target.html'
+
+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('navigate.html')
+ >>> link = browser.getLink(id='zope3')
+ >>> link.click()
+ >>> browser.url
+ 'http://localhost:.../target.html'
+
+Getting a nonexistent link raises an exception.
+
+ >>> browser.open('navigate.html')
+ >>> browser.getLink('This does not exist')
+ Traceback (most recent call last):
+ ...
+ LinkNotFoundError
+
+
+Other Navigation
+----------------
+
+Like in any normal browser, you can reload a page:
+
+ >>> browser.open('index.html')
+ >>> browser.url
+ 'http://localhost:.../index.html'
+ >>> browser.reload()
+ >>> browser.url
+ 'http://localhost:.../index.html'
+
+You can also go back:
+
+ >>> browser.open('notitle.html')
+ >>> browser.url
+ 'http://localhost:.../notitle.html'
+ >>> browser.goBack()
+ >>> browser.url
+ 'http://localhost:.../index.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('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
+ <Control name='text-value' type='text'>
+ >>> browser.getControl(label='Text Control') # equivalent
+ <Control name='text-value' type='text'>
+
+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'
+
+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'
+
+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'
+
+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)
+ <Control name='ambiguous-control-name' type='text'>
+ >>> browser.getControl('Ambiguous Control', index=0).value
+ 'First'
+ >>> browser.getControl('Ambiguous Control', index=1).value
+ 'Second'
+ >>> browser.getControl('Sub-control Ambiguity', index=0)
+ <ListControl name='ambiguous-subcontrol' type='select'>
+ >>> browser.getControl('Sub-control Ambiguity', index=1).optionValue
+ 'ambiguous'
+
+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')
+ <Control name='label-needs-normalization' type='text'>
+ >>> browser.getControl('label needs whitespace normalization')
+ Traceback (most recent call last):
+ ...
+ LookupError: label 'label needs whitespace normalization'
+ >>> browser.getControl(' Label Needs Whitespace ')
+ <Control name='label-needs-normalization' type='text'>
+ >>> browser.getControl('Whitespace')
+ <Control name='label-needs-normalization' type='text'>
+ >>> browser.getControl('hitespace')
+ Traceback (most recent call last):
+ ...
+ LookupError: label 'hitespace'
+ >>> browser.getControl('[non word characters should not confuse]')
+ <Control name='non-word-characters' type='text'>
+
+Multiple labels can refer to the same control (simply because that is possible
+in the HTML 4.0 spec).
+
+ >>> browser.getControl('Multiple labels really')
+ <Control name='two-labels' type='text'>
+ >>> browser.getControl('really are possible')
+ <Control name='two-labels' type='text'>
+ >>> browser.getControl('really') # OK: ambiguous labels, but not ambiguous control
+ <Control name='two-labels' type='text'>
+
+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')
+ <Control name='contained-in-label' type='text'>
+
+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')
+ <Control name='text-value' type='text'>
+ >>> browser.getControl(name='ambiguous-control-name')
+ Traceback (most recent call last):
+ ...
+ AmbiguityError: name 'ambiguous-control-name'
+ >>> browser.getControl(name='does-not-exist')
+ Traceback (most recent call last):
+ ...
+ LookupError: name 'does-not-exist'
+ >>> 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')
+ <ListControl name='radio-value' type='radio'>
+ >>> browser.getControl('Zwei')
+ <ItemControl name='radio-value' type='radio' optionValue='2' selected=True>
+ >>> browser.getControl('One')
+ <ItemControl name='multi-checkbox-value' type='checkbox' optionValue='1' selected=True>
+ >>> browser.getControl('Tres')
+ <ItemControl name='single-select-value' type='select' optionValue='3' selected=False>
+
+Characteristics of controls and subcontrols are discussed below.
+
+Control Objects
+~~~~~~~~~~~~~~~
+
+Controls provide IControl.
+
+ >>> ctrl = browser.getControl('Text Control')
+ >>> ctrl
+ <Control name='text-value' type='text'>
+ >>> verifyObject(zc.testbrowser.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
+ <ListControl name='multi-select-value' type='select'>
+ >>> ctrl.disabled
+ False
+ >>> ctrl.multiple
+ True
+ >>> verifyObject(zc.testbrowser.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
+ [<ItemControl name='multi-select-value' type='select' optionValue='1' selected=True>,
+ <ItemControl name='multi-select-value' type='select' optionValue='2' selected=True>,
+ <ItemControl name='multi-select-value' type='select' optionValue='3' selected=False>]
+
+ - The 'getControl' method lets you get subcontrols by their label or their value.
+
+ >>> ctrl.getControl('Un')
+ <ItemControl name='multi-select-value' type='select' optionValue='1' selected=True>
+ >>> ctrl.getControl('Deux')
+ <ItemControl name='multi-select-value' type='select' optionValue='2' selected=True>
+ >>> ctrl.getControl('Trois') # label attribute
+ <ItemControl name='multi-select-value' type='select' optionValue='3' selected=False>
+ >>> ctrl.getControl('Third') # contents
+ <ItemControl name='multi-select-value' type='select' optionValue='3' selected=False>
+ >>> browser.getControl('Third') # ambiguous in the browser, so useful
+ Traceback (most recent call last):
+ ...
+ AmbiguityError: label 'Third'
+
+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(zc.testbrowser.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
+ <Control name='password-value' type='password'>
+ >>> verifyObject(zc.testbrowser.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
+ <Control name='hidden-value' type='hidden'>
+ >>> verifyObject(zc.testbrowser.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
+ <Control name='textarea-value' type='textarea'>
+ >>> verifyObject(zc.testbrowser.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
+ <Control name='file-value' type='file'>
+ >>> verifyObject(zc.testbrowser.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
+ <ListControl name='single-select-value' type='select'>
+ >>> verifyObject(zc.testbrowser.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
+ <ListControl name='single-unvalued-checkbox-value' type='checkbox'>
+ >>> verifyObject(zc.testbrowser.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(
+ ... zc.testbrowser.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
+ <ListControl name='single-valued-checkbox-value' type='checkbox'>
+ >>> verifyObject(zc.testbrowser.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(
+ ... zc.testbrowser.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
+ <ListControl name='multi-checkbox-value' type='checkbox'>
+ >>> verifyObject(zc.testbrowser.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(zc.testbrowser.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
+ <ListControl name='radio-value' type='radio'>
+ >>> verifyObject(zc.testbrowser.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
+ <ImageControl name='image-value' type='image'>
+ >>> verifyObject(zc.testbrowser.interfaces.IImageSubmitControl, ctrl)
+ True
+ >>> ctrl.value
+ ''
+ >>> ctrl.disabled
+ False
+ >>> ctrl.multiple
+ False
+
+ - Submit Control
+
+ >>> ctrl = browser.getControl(name='submit-value')
+ >>> ctrl
+ <SubmitControl name='submit-value' type='submit'>
+ >>> browser.getControl('Submit This') # value of submit button is a label
+ <SubmitControl name='submit-value' type='submit'>
+ >>> browser.getControl('Standard Submit Control') # label tag is legal
+ <SubmitControl name='submit-value' type='submit'>
+ >>> browser.getControl('Submit') # multiple labels, but same control
+ <SubmitControl name='submit-value' type='submit'>
+ >>> verifyObject(zc.testbrowser.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()
+ >>> browser.contents
+ "...'text-value': ['Other Text']..."
+
+Note that if you click a submit object after the associated page has expired,
+you will get an error.
+
+ >>> browser.open('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('controls.html')
+ >>> browser.getControl('Text Control').value = 'Other Text'
+ >>> browser.getControl(name='image-value').click()
+ >>> browser.contents
+ "...'text-value': ['Other Text']..."
+
+ >>> browser.open('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('controls.html')
+ >>> browser.getControl(name='image-value').click((50,25))
+ >>> browser.contents
+ "...'image-value.x': ['50']...'image-value.y': ['25']..."
+
+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('forms.html')
+ >>> form = browser.getForm(name='one')
+
+Form instances conform to the IForm interface.
+
+ >>> verifyObject(zc.testbrowser.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:.../forms.html'
+
+ - The method (HTTP verb) used to transmit the form data:
+
+ >>> form.method
+ 'POST'
+
+ - The encoding type of the form data:
+
+ >>> form.enctype
+ 'application/x-www-form-urlencoded'
+
+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')
+ <Control name='text-value' type='text'>
+
+...and submit the form.
+
+ >>> form.submit('Submit')
+ >>> browser.contents
+ "...'text-value': ['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.open('forms.html')
+ >>> browser.getControl(name='text-value')
+ Traceback (most recent call last):
+ ...
+ AmbiguityError: name 'text-value'
+ >>> browser.getControl('Text Control')
+ Traceback (most recent call last):
+ ...
+ AmbiguityError: label 'Text Control'
+
+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
+ "...'text-value': ['Second Text']..."
+ >>> browser.open('forms.html')
+ >>> form = browser.getForm('2')
+ >>> form.getControl('Submit').click()
+ >>> browser.contents
+ "...'text-value': ['Second Text']..."
+ >>> browser.open('forms.html')
+ >>> 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
+ "...'text-value': ['Fourth Text']..."
+
+If a form is requested that does not exists, an exception will be raised.
+
+ >>> browser.open('forms.html')
+ >>> 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:
+
+ >>> browser.open('oneform.html')
+ >>> browser.getForm()
+ <zc.testbrowser...Form object at ...>
+
+If the HTML page contains more than one form, `index` is needed to
+disambiguate if no other arguments are provided:
+
+ >>> browser.open('forms.html')
+ >>> browser.getForm()
+ Traceback (most recent call last):
+ ValueError: if no other arguments are given, index is required.
+
+
+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('index.html')
+ >>> browser.lastRequestSeconds < 10 # really big number for safety
+ True
+ >>> browser.lastRequestPystones < 10000 # really big number for safety
+ 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'
+
+
+Fixed Bugs
+----------
+
+This section includes tests for bugs that were found and then fixed that don't
+fit into the more documentation-centric sections above.
+
+Spaces in URL
+~~~~~~~~~~~~~
+
+When URLs have spaces in them, they're handled correctly (before the bug was
+fixed, you'd get "ValueError: too many values to unpack"):
+
+ >>> browser.open('navigate.html')
+ >>> browser.getLink('Spaces in the URL').click()
+
+.goBack() Truncation
+~~~~~~~~~~~~~~~~~~~~
+
+The .goBack() method used to truncate the .contents.
+
+ >>> browser.open('navigate.html')
+ >>> actual_length = len(browser.contents)
+
+ >>> browser.open('navigate.html')
+ >>> browser.open('index.html')
+ >>> browser.goBack()
+ >>> len(browser.contents) == actual_length
+ True
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/README.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/__init__.py
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/__init__.py (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/__init__.py 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,13 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/browser.py
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/browser.py (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/browser.py 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,698 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+__docformat__ = "reStructuredText"
+
+from cStringIO import StringIO
+import ClientForm
+import mechanize
+import operator
+import re
+import sys
+import time
+import urllib2
+import urlparse
+import zc.testbrowser.interfaces
+import zope.interface
+
+RegexType = type(re.compile(''))
+_compress_re = re.compile(r"\s+")
+compressText = lambda text: _compress_re.sub(' ', text.strip())
+
+def disambiguate(intermediate, msg, index):
+ if intermediate:
+ if index is None:
+ if len(intermediate) > 1:
+ raise ClientForm.AmbiguityError(msg)
+ else:
+ return intermediate[0]
+ else:
+ try:
+ return intermediate[index]
+ except KeyError:
+ msg = '%s index %d' % (msg, index)
+ raise LookupError(msg)
+
+def controlFactory(control, form, browser):
+ if isinstance(control, ClientForm.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)
+
+
+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(zc.testbrowser.interfaces.IBrowser)
+
+ base = None
+ _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._enable_setattr_errors = True
+
+ if url is not None:
+ self.open(url)
+
+ @property
+ def url(self):
+ return self.mech_browser.geturl()
+
+ @property
+ def isHtml(self):
+ return self.mech_browser.viewing_html()
+
+ @property
+ def title(self):
+ return self.mech_browser.title()
+
+ @property
+ def contents(self):
+ if self._contents is not None:
+ return self._contents
+ response = self.mech_browser.response()
+ old_location = response.tell()
+ response.seek(0)
+ self._contents = response.read()
+ response.seek(old_location)
+ return self._contents
+
+ @property
+ def headers(self):
+ return self.mech_browser.response().info()
+
+ @apply
+ def handleErrors():
+ header_key = 'X-zope-handle-errors'
+
+ def get(self):
+ headers = self.mech_browser.addheaders
+ return dict(headers).get(header_key, True)
+
+ def set(self, value):
+ headers = self.mech_browser.addheaders
+ current_value = get(self)
+ if current_value == value:
+ return
+ if header_key in dict(headers):
+ headers.remove((header_key, current_value))
+ headers.append((header_key, value))
+
+ return property(get, set)
+
+ def open(self, url, data=None):
+ if self.base is not None:
+ url = urlparse.urljoin(self.base, url)
+ self._start_timer()
+ try:
+ try:
+ self.mech_browser.open(url, data)
+ except urllib2.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 urllib2.HTTPError(url, code, msg, self.headers, fp=None)
+
+ def _start_timer(self):
+ self.timer.start()
+
+ def _stop_timer(self):
+ self.timer.stop()
+
+ @property
+ def lastRequestPystones(self):
+ return self.timer.elapsedPystones
+
+ @property
+ def lastRequestSeconds(self):
+ return self.timer.elapsedSeconds
+
+ def reload(self):
+ self._start_timer()
+ self.mech_browser.reload()
+ self._stop_timer()
+ self._changed()
+
+ def goBack(self, count=1):
+ self._start_timer()
+ self.mech_browser.back(count)
+ self._stop_timer()
+ self._changed()
+
+ def addHeader(self, key, value):
+ self.mech_browser.addheaders.append( (key, value) )
+
+ def getLink(self, text=None, url=None, id=None, index=0):
+ 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 _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 f in forms:
+ for control in f.controls:
+ phantom = control.type in ('radio', 'checkbox')
+ if not phantom:
+ for l in control.get_labels():
+ if matches(l.text):
+ found.append((control, f))
+ break
+ if include_subcontrols and (
+ phantom or control.type=='select'):
+
+ for i in control.items:
+ for l in i.get_labels():
+ if matches(l.text):
+ found.append((i, f))
+ found_one = True
+ 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):
+ intermediate, msg = self._get_all_controls(
+ label, name, self.mech_browser.forms(), include_subcontrols=True)
+ control, form = disambiguate(intermediate, msg, index)
+ return controlFactory(control, form, self)
+
+ def _get_all_controls(self, label, name, forms, include_subcontrols=False):
+ onlyOne([label, name], '"label" and "name"')
+
+ if label is not None:
+ res = self._findByLabel(label, forms, include_subcontrols)
+ msg = 'label %r' % label
+ elif name is not None:
+ res = self._findByName(name, forms)
+ msg = 'name %r' % name
+ return res, msg
+
+ 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
+ self._start_timer()
+ self.mech_browser.open(form.click(
+ id=control.id, name=control.name, label=label, coord=coord))
+ self._stop_timer()
+
+ def _changed(self):
+ self._counter += 1
+ self._contents = None
+
+
+class Link(SetattrErrorsMixin):
+ zope.interface.implements(zc.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 zc.testbrowser.interfaces.ExpiredError
+ self.browser._start_timer()
+ self.browser.mech_browser.follow_link(self.mech_link)
+ 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(zc.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 ClientForm 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():
+
+ 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 zc.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 = StringIO(file)
+ self.mech_control.add_file(file, content_type, filename)
+
+ def clear(self):
+ if self._browser_counter != self.browser._counter:
+ raise zc.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(zc.testbrowser.interfaces.IListControl)
+
+ @apply
+ def displayValue():
+ # not implemented for anything other than select;
+ # would be nice if ClientForm 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 zc.testbrowser.interfaces.ExpiredError
+ self.mech_control.set_value_by_label(value)
+
+ return property(fget, fset)
+
+ @property
+ def displayOptions(self):
+ 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):
+ 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 zc.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 zc.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), self.mech_form, self.browser)
+ res.__dict__['control'] = self
+ return res
+
+
+class SubmitControl(Control):
+ zope.interface.implements(zc.testbrowser.interfaces.ISubmitControl)
+
+ def click(self):
+ if self._browser_counter != self.browser._counter:
+ raise zc.testbrowser.interfaces.ExpiredError
+ self.browser._clickSubmit(self.mech_form, self.mech_control, (1,1))
+ self.browser._changed()
+
+
+class ImageControl(Control):
+ zope.interface.implements(zc.testbrowser.interfaces.IImageSubmitControl)
+
+ def click(self, coord=(1,1)):
+ if self._browser_counter != self.browser._counter:
+ raise zc.testbrowser.interfaces.ExpiredError
+ self.browser._clickSubmit(self.mech_form, self.mech_control, coord)
+ self.browser._changed()
+
+
+class ItemControl(SetattrErrorsMixin):
+ zope.interface.implements(zc.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 zc.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():
+
+ def fget(self):
+ return self.mech_item.selected
+
+ def fset(self, value):
+ if self._browser_counter != self.browser._counter:
+ raise zc.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 zc.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(zc.testbrowser.interfaces.IForm)
+
+ def __init__(self, browser, form):
+ """Initialize the Form
+
+ browser - a Browser instance
+ form - a ClientForm 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):
+ return self.mech_form.attrs.get('id')
+
+ def submit(self, label=None, name=None, index=None, coord=(1,1)):
+ if self._browser_counter != self.browser._counter:
+ raise zc.testbrowser.interfaces.ExpiredError
+ form = self.mech_form
+ if label is not None or name is not None:
+ intermediate, msg = 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)
+ 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", urllib2.Request)
+ self.browser._start_timer()
+ self.browser.mech_browser.open(request)
+ self.browser._stop_timer()
+ self.browser._changed()
+
+ def getControl(self, label=None, name=None, index=None):
+ if self._browser_counter != self.browser._counter:
+ raise zc.testbrowser.interfaces.ExpiredError
+ intermediate, msg = self.browser._get_all_controls(
+ label, name, (self.mech_form,), include_subcontrols=True)
+ control, form = disambiguate(intermediate, msg, index)
+ return controlFactory(control, form, self.browser)
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/browser.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/dummymodules.py
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/dummymodules.py (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/dummymodules.py 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,15 @@
+class DummyInterfaceModule(object):
+ Interface = object
+
+ def __getattr__(self, name):
+ return lambda *args, **kws: None
+
+interface = DummyInterfaceModule()
+
+class DummySchemaModule(object):
+ def __getattr__(self, name):
+ return lambda *args, **kws: interface.Attribute('')
+
+schema = DummySchemaModule()
+
+
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/dummymodules.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/__init__.py
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/ftests/__init__.py (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/ftests/__init__.py 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1 @@
+# Make a package.
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/ftests/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/controls.html
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/ftests/controls.html (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/ftests/controls.html 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,200 @@
+<html>
+ <body>
+
+ <h1>Controls Tests</h1>
+
+ <form action="controls.html" method="post">
+
+ <div>
+ <label for="text-value">Text Control</label>
+ <em tal:condition="request/text-value|nothing"
+ tal:content="request/text-value"></em>
+ <input type="text" name="text-value" id="text-value"
+ value="Some Text" />
+ </div>
+
+ <div>
+ <label for="password-value">Password Control</label>
+ <em tal:condition="request/password-value|nothing"
+ tal:content="request/password-value"></em>
+ <input type="password" name="password-value" id="password-value"
+ value="Password" />
+ </div>
+
+ <div>
+ <label for="hidden-value">Hidden Control</label> (label: hee hee)
+ <em tal:condition="request/hidden-value|nothing"
+ tal:content="request/hidden-value"></em>
+ <input type="hidden" name="hidden-value" id="hidden-value"
+ value="Hidden" />
+ </div>
+
+ <div>
+ <label for="textarea-value">Text Area Control</label>
+ <em tal:condition="request/textarea-value|nothing"
+ tal:content="request/textarea-value"></em>
+ <textarea name="textarea-value" id="textarea-value">
+ Text inside
+ area!
+ </textarea>
+ </div>
+
+ <div>
+ <label for="file-value">File Control</label>
+ <em tal:condition="request/file-value|nothing"
+ tal:content="request/file-value"></em>
+ <input type="file" name="file-value" id="file-value" />
+ </div>
+
+ <div>
+ <label for="single-select-value">Single Select Control</label>
+ <em tal:condition="request/single-select-value|nothing"
+ tal:content="request/single-select-value"></em>
+ <select name="single-select-value" id="single-select-value">
+ <option value="1">Uno</option>
+ <option value="2">Dos</option>
+ <option value="3" label="Third">Tres</option>
+ </select>
+ </div>
+
+ <div>
+ <label for="multi-select-value">Multiple Select Control</label>
+ <em tal:condition="request/multi-select-value|nothing"
+ tal:content="request/multi-select-value"></em>
+ <select name="multi-select-value" id="multi-select-value"
+ multiple="multiple">
+ <option value="1">Un</option>
+ <option value="2">Deux</option>
+ <option value="3" label="Third">Trois</option>
+ </select>
+ </div>
+
+ <div>
+ <em tal:condition="request/single-unvalued-checkbox-value|nothing"
+ tal:content="request/single-unvalued-checkbox-value"></em>
+ <input type="checkbox" name="single-unvalued-checkbox-value"
+ id="single-unvalued-checkbox" checked="checked" />
+ <label for="single-unvalued-checkbox">Single Unvalued Checkbox</label>
+ </div>
+
+ <div>
+ <em tal:condition="
+ request/single-disabled-unvalued-checkbox-value|nothing"
+ tal:content="request/single-disabled-unvalued-checkbox-value"></em>
+ <input type="checkbox" name="single-disabled-unvalued-checkbox-value"
+ id="single-disabled-unvalued-checkbox" checked="checked"
+ disabled="disabled" />
+ <label for="single-disabled-unvalued-checkbox">
+ Single Disabled Unvalued Checkbox
+ </label>
+ </div>
+
+ <div>
+ <em tal:condition="request/single-valued-checkbox-value|nothing"
+ tal:content="request/single-valued-checkbox-value"></em>
+ <label><input type="checkbox" name="single-valued-checkbox-value"
+ value="1" checked="checked" />Single Valued Checkbox
+ </label>
+ </div>
+
+ <div>
+ (Multi checkbox: options have the labels)
+ <em tal:condition="request/multi-checkbox-value|nothing"
+ tal:content="request/multi-checkbox-value"></em>
+ <label><input type="checkbox" name="multi-checkbox-value" value="1"
+ checked="checked" /> One</label>
+ <input type="checkbox" name="multi-checkbox-value" value="2"
+ id="multi-checkbox-value-2" />
+ <label for="multi-checkbox-value-2">Two</label>
+ <label><input type="checkbox" name="multi-checkbox-value" value="3"
+ id="multi-checkbox-value-3" checked="checked" />Three
+ </label>
+ <label for="multi-checkbox-value-3">Third</label>
+ </div>
+
+ <div>
+ (Radio: options have the labels)
+ <em tal:condition="request/radio-value|nothing"
+ tal:content="request/radio-value"></em>
+ <label><input type="radio" name="radio-value" value="1" />Ein</label>
+ <input type="radio" name="radio-value" id="radio-value-2" value="2"
+ checked="checked" />
+ <label for="radio-value-2">Zwei</label>
+ <label><input type="radio" name="radio-value" id="radio-value-3"
+ value="3" /> Drei</label>
+ <label for="radio-value-3">Third</label>
+ </div>
+
+ <div>
+ <label for="image-value">Image Control</label>
+ <em tal:condition="request/image-value.x|nothing"
+ tal:content="request/image-value.x"></em>
+ <em tal:condition="request/image-value.y|nothing"
+ tal:content="request/image-value.y"></em>
+ <input type="image" name="image-value" id="image-value"
+ src="zope3logo.gif" />
+ </div>
+
+ <div>
+ <label for="submit-value">Standard Submit Control</label>
+ <em tal:condition="request/submit-value|nothing"
+ tal:content="request/submit-value"></em>
+ <input type="submit" name="submit-value" id="submit-value"
+ value="Submit This" />
+ </div>
+
+ <div>
+ <label for="ambiguous-control-name">Ambiguous Control</label>
+ <input type="text" name="ambiguous-control-name"
+ id="ambiguous-control-name" value="First" />
+ </div>
+
+ <div>
+ <label for="ambiguous-control-name">Ambiguous Control</label>
+ <input type="text" name="ambiguous-control-name"
+ id="ambiguous-control-name" value="Second" />
+ </div>
+
+ <div>
+ <label for="label-needs-normalization"> The Label
+ Needs Whitespace Normalization
+ Badly </label>
+ <input type="text" name="label-needs-normalization"
+ id="label-needs-normalization" />
+ </div>
+
+ <div>
+ <label for="non-word-characters">*[non word characters should not
+ confuse]</label>
+ <input type="text" name="non-word-characters"
+ id="non-word-characters" />
+ </div>
+
+ <div>
+ <label for="two-labels">Multiple labels really</label>
+ <label for="two-labels">really are possible</label>
+ <input type="text" name="two-labels"
+ id="two-labels" />
+ </div>
+
+ <div>
+ <label>Labels can be connected by containing their respective fields
+ <input type="text" name="contained-in-label" />
+ </label>
+ </div>
+
+ <div>
+ If you have a select field with a label that overlaps with one of its
+ options' labels, that is ambiguous.
+ <label for="ambiguous-subcontrol">Sub-control Ambiguity</label>
+ <select name="ambiguous-subcontrol" id="ambiguous-subcontrol">
+ <option value="">(none)</option>
+ <option value="ambiguous">Sub-control Ambiguity Exemplified</option>
+ </select>
+ </div>
+
+
+ </form>
+
+ </body>
+</html>
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/forms.html
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/ftests/forms.html (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/ftests/forms.html 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,39 @@
+<html>
+ <body>
+
+ <h1>Forms Tests</h1>
+
+ <em tal:condition="request/text-value|nothing"
+ tal:content="request/text-value" />
+
+ <form id="1" name="one" action="forms.html" method="post">
+ <input type="text" name="text-value" value="First Text" />
+ <input type="image" name="image-1" src="zope3logo.gif" />
+ <input type="submit" name="submit-1" value="Submit" />
+ </form>
+
+ <form id="2" name="two" action="forms.html" method="post">
+ <input type="text" name="text-value" value="Second Text" />
+ <input type="submit" name="submit-2" value="Submit" />
+ </form>
+
+ <form id="3" name="three" action="forms.html" method="post">
+ <label for="text-value-3">Text Control</label>
+ <input type="text" name="text-value" id="text-value-3"
+ value="Third Text" />
+ <input type="submit" name="submit-3" value="Submit" />
+ </form>
+
+ <form action="forms.html" method="post">
+ <label for="text-value-4">Text Control</label>
+ <input type="text" name="text-value" id="text-value-4"
+ value="Fourth Text" />
+ <em tal:condition="python: 'hidden-4' in request.form and
+ 'submit-4' not in request.form"
+ >Submitted without the submit button.</em>
+ <input type="submit" name="submit-4" value="Don't Submit Me" />
+ <input type="hidden" name="hidden-4" value="marker" />
+ </form>
+
+ </body>
+</html>
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/ftesting.zcml
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/ftests/ftesting.zcml (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/ftests/ftesting.zcml 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,38 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ i18n_domain="zope"
+ package="zope.testbrowser"
+ >
+
+ <!-- This file is the equivalent of site.zcml and it is -->
+ <!-- used for functional testing setup -->
+
+ <include package="zope.app.zcmlfiles" />
+ <include package="zope.app.authentication" />
+
+ <!-- Principals -->
+
+ <unauthenticatedPrincipal
+ id="zope.anybody"
+ title="Unauthenticated User" />
+
+
+ <include package="zope.app.securitypolicy" file="meta.zcml"/>
+
+ <securityPolicy
+ component="zope.app.securitypolicy.zopepolicy.ZopeSecurityPolicy" />
+
+ <role id="zope.Anonymous" title="Everybody"
+ description="All users have this role implicitly" />
+
+ <!-- Replace the following directive if you don't want public access -->
+ <grant permission="zope.View"
+ role="zope.Anonymous" />
+
+
+ <browser:resourceDirectory
+ name="testbrowser"
+ directory="ftests" />
+
+</configure>
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/ftests/ftesting.zcml
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/index.html
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/ftests/index.html (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/ftests/index.html 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Simple Page</title>
+ </head>
+ <body>
+ <h1>Simple Page</h1>
+ </body>
+</html>
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/navigate.html
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/ftests/navigate.html (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/ftests/navigate.html 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,35 @@
+<html>
+ <body>
+
+ <h1>Navigation Tests</h1>
+
+ <p tal:condition="request/message|nothing">
+ Message: <em tal:content="request/message">Message</em>
+ </p>
+
+ <a href="target.html">Link Text</a>
+
+ <a href="target.html"> Link Text
+ with Whitespace Normalization (and parens) </a>
+
+ <a href="target.html" id="anchorid">By Anchor Id</a>
+
+ <a href="target.html">Spaces in the URL</a>
+
+ <form action="navigate.html" method="post">
+ <input name="message" value="By Form Submit" />
+ <input type="submit" name="submit-form" value="Submit" />
+ </form>
+
+ <img src="./zope3logo.gif" usemap="#zope3logo" />
+ <map name="zope3logo">
+ <area shape="rect" alt="Zope3"
+ href="target.html" id="zope3" title="Zope 3"
+ coords="44,7,134,33" />
+ <area shape="circle" alt="Logo"
+ href="navigate.html?message=Logo" id="logo" title="Logo"
+ coords="23,21,18" />
+ </map>
+
+ </body>
+</html>
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/notitle.html
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/ftests/notitle.html (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/ftests/notitle.html 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ <h1>No Title</h1>
+ </body>
+</html>
\ No newline at end of file
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/oneform.html
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/ftests/oneform.html (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/ftests/oneform.html 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,14 @@
+<html>
+ <body>
+
+ <h1>Single Form Tests</h1>
+
+ <form id="1" name="one" action="forms.html"
+ enctype="multipart/form-data" method="post">
+ <input type="text" name="text-value" value="First Text" />
+ <input type="image" name="image-1" src="zope3logo.gif" />
+ <input type="submit" name="submit-1" value="Submit" />
+ </form>
+
+ </body>
+</html>
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/target.html
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/ftests/target.html (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/ftests/target.html 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Target Page</title>
+ </head>
+ <body>
+ This page is the target of a link.
+ </body>
+</html>
Added: zc.testbrowser/trunk/src/zc/testbrowser/ftests/zope3logo.gif
===================================================================
(Binary files differ)
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/ftests/zope3logo.gif
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: zc.testbrowser/trunk/src/zc/testbrowser/headers.txt
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/headers.txt (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/headers.txt 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,25 @@
+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.base = 'http://localhost:%s/' % TEST_PORT
+ >>> browser.open('index.html')
+ >>> browser.headers
+ <httplib.HTTPMessage instance...>
+
+The headers can be accessed as a string:
+
+ >>> print browser.headers
+ Server: BaseHTTP
+ Date: Mon, 17 Sep 2007 10:05:42 GMT
+ Connection: close
+ Content-type: text/html
+
+Or as a mapping:
+
+ >>> browser.headers['content-type']
+ 'text/html'
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/headers.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/interfaces.py
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/interfaces.py (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/interfaces.py 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,354 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+__docformat__ = "reStructuredText"
+
+from zope import interface, schema
+
+
+class IBrowser(interface.Interface):
+ """A Programmatic Web Browser."""
+
+ url = schema.URI(
+ title=u"URL",
+ description=u"The URL the browser is currently showing.",
+ required=True)
+
+ headers = schema.Field(
+ title=u"Headers",
+ description=(u"Headers of the HTTP response; a "
+ "``httplib.HTTPMessage``."),
+ required=True)
+
+ contents = schema.Text(
+ title=u"Contents",
+ description=u"The complete response body of the HTTP request.",
+ required=True)
+
+ isHtml = schema.Bool(
+ title=u"Is HTML",
+ description=u"Tells whether the output is HTML or not.",
+ required=True)
+
+ title = schema.TextLine(
+ title=u"Base",
+ description=u"Base URL for opening relative paths",
+ required=False)
+
+ title = schema.TextLine(
+ title=u"Title",
+ description=u"Title of the displayed page",
+ required=False)
+
+ handleErrors = 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 unless a ``base`` has been provided.
+
+ 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):
+ """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 <a> and </a> 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.
+ """
+
+ lastRequestSeconds = 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 = 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(interface.Interface):
+ """A control (input field) of a page."""
+
+ name = schema.TextLine(
+ title=u"Name",
+ description=u"The name of the control.",
+ required=True)
+
+ value = schema.Field(
+ title=u"Value",
+ description=u"The value of the control",
+ default=None,
+ required=True)
+
+ type = 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 = schema.Bool(
+ title=u"Disabled",
+ description=u"Describes whether a control is disabled.",
+ default=False,
+ required=False)
+
+ multiple = 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 = schema.List(
+ title=u"Options",
+ description=u"""\
+ A list of possible values for the control.""",
+ required=True)
+
+ displayOptions = schema.List(
+ # TODO: currently only implemented for select by ClientForm
+ title=u"Options",
+ description=u"""\
+ A list of possible display values for the control.""",
+ required=True)
+
+ displayValue = schema.Field(
+ # TODO: currently only implemented for select by ClientForm
+ 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 = 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(interface.Interface):
+ """a radio button or checkbox within a larger multiple-choice control"""
+
+ control = schema.Object(
+ title=u"Control",
+ description=(u"The parent control element."),
+ schema=IControl,
+ required=True)
+
+ disabled = schema.Bool(
+ title=u"Disabled",
+ description=u"Describes whether a subcontrol is disabled.",
+ default=False,
+ required=False)
+
+ selected = schema.Bool(
+ title=u"Selected",
+ description=u"Whether the subcontrol is selected",
+ default=None,
+ required=True)
+
+ optionValue = schema.TextLine(
+ title=u"Value",
+ description=u"The value of the subcontrol",
+ default=None,
+ required=False)
+
+
+class ILink(interface.Interface):
+
+ def click():
+ """click the link, going to the URL referenced"""
+
+ url = schema.TextLine(
+ title=u"URL",
+ description=u"The normalized URL of the link",
+ required=False)
+
+ text = schema.TextLine(
+ title=u'Text',
+ description=u'The contained text of the link',
+ required=False)
+
+
+class IForm(interface.Interface):
+ """An HTML form of the page."""
+
+ action = schema.TextLine(
+ title=u"Action",
+ description=u"The action (or URI) that is opened upon submittance.",
+ required=True)
+
+ name = schema.TextLine(
+ title=u"Name",
+ description=u"The value of the `name` attribute in the form tag, "
+ u"if specified.",
+ required=True)
+
+ id = 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.
+ """
+
+
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/interfaces.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/real.js
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/real.js (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/real.js 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,110 @@
+var tb_tokens = {};
+var tb_next_token = 0;
+
+tb_page_loaded = false;
+document.getElementById("appcontent"
+ ).addEventListener("load", function() { tb_page_loaded = true; }, true);
+
+function tb_get_link_by_predicate(predicate, index) {
+ var anchors = content.document.getElementsByTagName('a');
+ var i=0;
+ var found = null;
+ if (index == undefined) index = null;
+ for (var x=0; x < anchors.length; x++) {
+ a = anchors[x];
+ if (!predicate(a)) {
+ continue;
+ }
+ // this anchor matches
+
+ // if we weren't given an index, but we found more than
+ // one match, we have an ambiguity
+ if (index == null && i > 0) {
+ return 'ambiguity error';
+ }
+
+ found = x;
+
+ // if we were given an index and we just found it, stop
+ if (index != null && i == index) {
+ break
+ }
+ i++;
+ }
+ if (found != null) {
+ tb_tokens[tb_next_token] = anchors[found];
+ return tb_next_token++;
+ }
+ return false; // link not found
+}
+
+function tb_normalize_whitespace(text) {
+ text = text.replace(/[\n\r]+/g, ' ');
+ text = text.replace(/\s+/g, ' ');
+ text = text.replace(/ +$/g, '');
+ text = text.replace(/^ +/g, '');
+ return text;
+}
+
+function tb_get_link_by_text(text, index) {
+ text = tb_normalize_whitespace(text);
+ return tb_get_link_by_predicate(
+ function (a) {
+ //alert(tb_normalize_whitespace(a.textContent) + '|' + text + '|' + tb_normalize_whitespace(a.textContent).indexOf(text));
+ return tb_normalize_whitespace(a.textContent).indexOf(text) != -1;
+ }, index)
+}
+
+function tb_get_link_by_url(url, index) {
+ return tb_get_link_by_predicate(
+ function (a) {
+ return a.href.indexOf(url) != -1;
+ }, index)
+}
+
+function tb_get_link_by_id(id, index) {
+ return tb_get_link_by_predicate(
+ function (a) {
+ alert(a.id + '|' + id + '|' + (a.id == id));
+ return a.id == id;
+ }, index)
+}
+
+function tb_take_screen_shot(out_path) {
+ // The `subject` is what we want to take a screen shot of.
+ var subject = content.document;
+ var canvas = content.document.createElement('canvas');
+ canvas.width = subject.width;
+ canvas.height = subject.height;
+
+ var ctx = canvas.getContext('2d');
+ ctx.drawWindow(content, 0, 0, subject.width, subject.height, 'rgb(0,0,0)');
+ tb_save_canvas(canvas, out_path);
+}
+
+function tb_save_canvas(canvas, out_path) {
+ var io = Components.classes['@mozilla.org/network/io-service;1'
+ ].getService(Components.interfaces.nsIIOService);
+ var source = io.newURI(canvas.toDataURL('image/png', ''), 'UTF8', null);
+ var persist = Components.classes[
+ '@mozilla.org/embedding/browser/nsWebBrowserPersist;1'
+ ].createInstance(Components.interfaces.nsIWebBrowserPersist);
+ var file = Components.classes['@mozilla.org/file/local;1'
+ ].createInstance(Components.interfaces.nsILocalFile);
+ file.initWithPath(out_path);
+ persist.saveURI(source, null, null, null, null, file);
+}
+
+function tb_follow_link(token) {
+ var a = tb_tokens[token];
+ var evt = a.ownerDocument.createEvent('MouseEvents');
+ evt.initMouseEvent('click', true, true, a.ownerDocument.defaultView,
+ 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+ a.dispatchEvent(evt);
+ // empty the tokens data structure, they're all expired now
+ tb_tokens = {};
+}
+
+function tb_get_link_text(token) {
+ return tb_normalize_whitespace(tb_tokens[token].textContent);
+}
Added: zc.testbrowser/trunk/src/zc/testbrowser/real.py
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/real.py (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/real.py 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,226 @@
+import ClientForm
+import os.path
+import re
+import simplejson
+import socket
+import telnetlib
+import time
+import urlparse
+import zc.testbrowser.browser
+import zc.testbrowser.interfaces
+import zope.interface
+
+PROMPT = re.compile('repl\d?> ')
+
+class BrowserStateError(RuntimeError):
+ pass
+
+class Browser(zc.testbrowser.browser.SetattrErrorsMixin):
+ zope.interface.implements(zc.testbrowser.interfaces.IBrowser)
+
+ base = None
+ raiseHttpErrors = True
+ _counter = 0
+ timeout = 5 # XXX debug only, change back to 60
+
+ def __init__(self, url=None, host='localhost', port=4242):
+ self.timer = zc.testbrowser.browser.PystoneTimer()
+ self.init_repl(host, port)
+ self._enable_setattr_errors = True
+
+ if url is not None:
+ self.open(url)
+
+ def init_repl(self, host, port):
+ dir = os.path.dirname(__file__)
+ js_path = os.path.join(dir, 'real.js')
+ try:
+ self.telnet = telnetlib.Telnet(host, port)
+ except socket.error, e:
+ raise RuntimeError('Error connecting to Firefox at %s:%s.'
+ ' Is MozRepl running?' % (host, port))
+
+ self.telnet.write(open(js_path, 'rt').read())
+ self.expect([PROMPT])
+
+ def execute(self, js):
+ if not js.strip():
+ return
+ self.telnet.write("'MARKER'")
+ self.telnet.read_until('MARKER')
+ self.expect([PROMPT])
+ self.telnet.write(js)
+ i, match, text = self.expect([PROMPT])
+ if '!!!' in text: import pdb;pdb.set_trace() # XXX debug only, remove
+ result = text.rsplit('\n', 1)
+ if len(result) == 1:
+ return None
+ else:
+ return result[0]
+
+ def executeLines(self, js):
+ lines = js.split('\n')
+ for line in lines:
+ self.execute(line)
+
+ def expect(self, res):
+ i, match, text = self.telnet.expect([PROMPT], self.timeout)
+ if match is None:
+ raise RuntimeError('unexpected result from MozRepl')
+ return i, match, text
+
+ def _changed(self):
+ self._counter += 1
+
+ @property
+ def url(self):
+ return self.execute('content.location')
+
+ def waitForPageLoad(self):
+ start = time.time()
+ while self.execute('tb_page_loaded') == 'false':
+ time.sleep(0.001)
+ if time.time() - start > self.timeout:
+ raise RuntimeError('timed out waiting for page load')
+
+ self.execute('tb_page_loaded = false;')
+
+ def open(self, url, data=None):
+ if self.base is not None:
+ url = urlparse.urljoin(self.base, url)
+ assert data is None
+ self.start_timer()
+ try:
+ self.execute('content.location = ' + simplejson.dumps(url))
+ self.waitForPageLoad()
+ finally:
+ self.stop_timer()
+ self._changed()
+
+ # TODO raise non-200 errors
+
+ @property
+ def isHtml(self):
+ return self.execute('content.document.contentType') == 'text/html'
+
+ @property
+ def title(self):
+ if not self.isHtml:
+ raise BrowserStateError('not viewing HTML')
+
+ result = self.execute('content.document.title')
+ if result is '':
+ result = None
+ return result
+
+ @property
+ def contents(self):
+ return self.execute('content.document.documentElement.innerHTML')
+
+ @property
+ def headers(self):
+ raise NotImplementedError
+
+ @apply
+ def handleErrors():
+ def get(self):
+ raise NotImplementedError
+
+ def set(self, value):
+ raise NotImplementedError
+
+ return property(get, set)
+
+ def start_timer(self):
+ self.timer.start()
+
+ def stop_timer(self):
+ self.timer.stop()
+
+ @property
+ def lastRequestPystones(self):
+ return self.timer.elapsedPystones
+
+ @property
+ def lastRequestSeconds(self):
+ return self.timer.elapsedSeconds
+
+ def reload(self):
+ self.start_timer()
+ self.execute('content.document.location = content.document.location')
+ self.waitForPageLoad()
+ self.stop_timer()
+
+ def goBack(self, count=1):
+ self.start_timer()
+ self.execute('content.back()')
+ # Our method of knowing when the page finishes loading doesn't work
+ # for "back", so for now just sleep a little, and hope it is enough.
+ time.sleep(1)
+ self.stop_timer()
+ self._changed()
+
+ def addHeader(self, key, value):
+ raise NotImplementedError
+
+ def getLink(self, text=None, url=None, id=None, index=0):
+ zc.testbrowser.browser.onlyOne((text, url, id), 'text, url, or id')
+ js_index = simplejson.dumps(index)
+ if text is not None:
+ msg = 'text %r' % text
+ token = self.execute('tb_get_link_by_text(%s, %s)'
+ % (simplejson.dumps(text), js_index))
+ elif url is not None:
+ msg = 'url %r' % url
+ token = self.execute('tb_get_link_by_url(%s, %s)'
+ % (simplejson.dumps(url), js_index))
+ elif id is not None:
+ msg = 'id %r' % id
+ token = self.execute('tb_get_link_by_id(%s, %s)'
+ % (simplejson.dumps(id), js_index))
+
+ if token == 'false':
+ raise ValueError('Link not found: ' + msg)
+ if token == 'ambiguity error':
+ raise ClientForm.AmbiguityError(msg)
+
+ return Link(token, self)
+
+ def _follow_link(self, token):
+ self.execute('tb_follow_link(%s)' % token)
+
+ def getControl(self, label=None, name=None, index=None):
+ raise NotImplementedError
+
+ def getForm(self, id=None, name=None, action=None, index=None):
+ raise NotImplementedError
+
+
+class Link(zc.testbrowser.browser.SetattrErrorsMixin):
+ zope.interface.implements(zc.testbrowser.interfaces.ILink)
+
+ def __init__(self, token, browser):
+ self.token = token
+ 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 zc.testbrowser.interfaces.ExpiredError
+ self.browser.start_timer()
+ self.browser._follow_link(self.token)
+ self.browser.stop_timer()
+ self.browser._changed()
+
+ @property
+ def url(self):
+ return self.browser.execute('tb_tokens[%s].href' % self.token)
+
+ @property
+ def text(self):
+ return self.browser.execute('tb_get_link_text(%s)' % self.token)
+
+ def __repr__(self):
+ return "<%s text=%r url=%r>" % (
+ self.__class__.__name__, self.text, self.url)
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/real.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/real.txt
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/real.txt (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/real.txt 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,44 @@
+ >>> from zc.testbrowser.real import Browser
+
+ >>> browser = Browser()
+
+ >>> base_url = 'http://localhost:%s/' % TEST_PORT
+ >>> browser.open(base_url)
+ >>> browser.url == base_url
+ True
+
+ >>> browser.open(base_url + 'index.html')
+
+ >>> browser.isHtml
+ True
+
+ >>> browser.title
+ 'Simple Page'
+
+ >>> browser.contents
+ '<head>...</body>'
+
+ XXX Note that the entire page is not returned; need to find a way to
+ retrieve the entire page.
+
+ >>> url = browser.url
+ >>> browser.reload()
+ >>> browser.url == url
+ True
+
+ >>> browser.goBack()
+ >>> browser.url == base_url
+ True
+
+ >>> browser.open(base_url + 'navigate.html')
+
+ >>> browser.getLink('Link Text')
+ <Link text='Link Text' url='http://localhost:.../target.html'>
+
+ >>> browser.getLink(url='http://')
+ Traceback (most recent call last):
+ ...
+ AmbiguityError: url 'http://'
+
+ >>> browser.getLink(url='http://', index=3)
+ <Link text='Spaces in the URL' url='http://localhost:.../target.html'>
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/real.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/screen-shots.txt
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/screen-shots.txt (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/screen-shots.txt 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,4 @@
+ >>> from zc.testbrowser.real import Browser
+ >>> browser = Browser()
+ >>> browser.open('http://slashdot.org')
+ >>> browser.execute('tb_take_screen_shot("/tmp/1.png")')
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/screen-shots.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.testbrowser/trunk/src/zc/testbrowser/tests.py
===================================================================
--- zc.testbrowser/trunk/src/zc/testbrowser/tests.py (rev 0)
+++ zc.testbrowser/trunk/src/zc/testbrowser/tests.py 2007-09-19 09:34:25 UTC (rev 79746)
@@ -0,0 +1,498 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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.
+#
+##############################################################################
+
+from cStringIO import StringIO
+from zc.testbrowser import browser
+from zope.testing import renormalizing, doctest
+import BaseHTTPServer
+import cgi
+import httplib
+import mechanize
+import os.path
+import pprint
+import random
+import re
+import string
+import threading
+import unittest
+import urllib
+import urllib2
+import zc.testbrowser.browser
+import zc.testbrowser.real
+
+
+web_server_base_path = os.path.join(os.path.split(__file__)[0], 'ftests')
+
+
+class TestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+ def version_string(self):
+ return 'BaseHTTP'
+
+ def date_time_string(self):
+ return 'Mon, 17 Sep 2007 10:05:42 GMT'
+
+ def do_GET(self):
+ if self.path.endswith('robots.txt'):
+ self.send_response(404)
+ self.send_header('Connection', 'close')
+ return
+
+ try:
+ f = open(web_server_base_path + self.path)
+ except IOError:
+ self.send_response(500)
+ self.send_header('Connection', 'close')
+ return
+
+ if self.path.endswith('.gif'):
+ content_type = 'image/gif'
+ elif self.path.endswith('.html'):
+ content_type = 'text/html'
+ else:
+ self.send_response(500, 'unknown file type')
+
+ self.send_response(200)
+ self.send_header('Connection', 'close')
+ self.send_header('Content-type', content_type)
+ self.end_headers()
+ self.wfile.write(f.read())
+ f.close()
+
+ def do_POST(self):
+ body = self.rfile.read(int(self.headers['content-length']))
+ values = cgi.parse_qs(body)
+ self.wfile
+ self.send_response(200)
+ self.send_header('Content-type', 'text/plain')
+ self.end_headers()
+ pprint.pprint(values, self.wfile)
+
+ def log_request(self, *args, **kws):
+ pass
+
+
+def set_next_response(body, headers=None, status='200', reason='OK'):
+ global next_response_body
+ global next_response_headers
+ global next_response_status
+ global next_response_reason
+ if headers is None:
+ headers = (
+ 'Content-Type: text/html\r\n'
+ 'Content-Length: %s\r\n'
+ % len(body)
+ )
+ next_response_body = body
+ next_response_headers = headers
+ next_response_status = status
+ next_response_reason = reason
+
+
+class FauxConnection(object):
+ """A ``urllib2`` compatible connection object."""
+
+ def __init__(self, host):
+ pass
+
+ def set_debuglevel(self, level):
+ pass
+
+ def _quote(self, url):
+ # the publisher expects to be able to split on whitespace, so we have
+ # to make sure there is none in the URL
+ return url.replace(' ', '%20')
+
+
+ def request(self, method, url, body=None, headers=None):
+ if body is None:
+ body = ''
+
+ if url == '':
+ url = '/'
+
+ url = self._quote(url)
+
+ # Construct the headers.
+ header_chunks = []
+ if headers is not None:
+ for header in headers.items():
+ header_chunks.append('%s: %s' % header)
+ headers = '\n'.join(header_chunks) + '\n'
+ else:
+ headers = ''
+
+ # Construct the full HTTP request string, since that is what the
+ # ``HTTPCaller`` wants.
+ request_string = (method + ' ' + url + ' HTTP/1.1\n'
+ + headers + '\n' + body)
+
+ print request_string.replace('\r', '')
+
+ def getresponse(self):
+ return FauxResponse(next_response_body,
+ next_response_headers,
+ next_response_status,
+ next_response_reason,
+ )
+
+
+class FauxResponse(object):
+
+ def __init__(self, content, headers, status, reason):
+ self.content = content
+ self.status = status
+ self.reason = reason
+ self.msg = httplib.HTTPMessage(StringIO(headers), 0)
+ self.content_as_file = StringIO(self.content)
+
+ def read(self, amt=None):
+ return self.content_as_file.read(amt)
+
+ def close(self):
+ """To overcome changes in urllib2 and socket in python2.5"""
+ pass
+
+
+class FauxHTTPHandler(urllib2.HTTPHandler):
+
+ http_request = urllib2.AbstractHTTPHandler.do_request_
+
+ def http_open(self, req):
+ """Open an HTTP connection having a ``urllib2`` request."""
+ # Here we connect to the publisher.
+ return self.do_open(FauxConnection, req)
+
+
+class FauxMechanizeBrowser(mechanize.Browser):
+
+ handler_classes = {
+ # scheme handlers
+ "http": FauxHTTPHandler,
+
+ "_http_error": mechanize.HTTPErrorProcessor,
+ "_http_request_upgrade": mechanize.HTTPRequestUpgradeProcessor,
+ "_http_default_error": urllib2.HTTPDefaultErrorHandler,
+
+ # feature handlers
+ "_authen": urllib2.HTTPBasicAuthHandler,
+ "_redirect": mechanize.HTTPRedirectHandler,
+ "_cookies": mechanize.HTTPCookieProcessor,
+ "_refresh": mechanize.HTTPRefreshProcessor,
+ "_referer": mechanize.Browser.handler_classes['_referer'],
+ "_equiv": mechanize.HTTPEquivProcessor,
+ }
+
+ default_schemes = ["http"]
+ default_others = ["_http_error", "_http_request_upgrade",
+ "_http_default_error"]
+ default_features = ["_authen", "_redirect", "_cookies"]
+
+
+class Browser(browser.Browser):
+
+ def __init__(self, url=None):
+ mech_browser = FauxMechanizeBrowser()
+ super(Browser, self).__init__(url=url, mech_browser=mech_browser)
+
+ def open(self, body, headers=None, status=200, reason='OK'):
+ set_next_response(body, headers, status, reason)
+ browser.Browser.open(self, 'http://localhost/')
+
+def test_submit_duplicate_name():
+ """
+
+This test was inspired by bug #723 as testbrowser would pick up the wrong
+button when having the same name twice in a form.
+
+ >>> browser = Browser()
+
+When given a form with two submit buttons that have the same name:
+
+ >>> browser.open('''\
+ ... <html><body>
+ ... <form action="." method="post" enctype="multipart/form-data">
+ ... <input type="submit" name="submit_me" value="GOOD" />
+ ... <input type="submit" name="submit_me" value="BAD" />
+ ... </form></body></html>
+ ... ''') # doctest: +ELLIPSIS
+ GET / HTTP/1.1
+ ...
+
+We can specify the second button through it's label/value:
+
+ >>> browser.getControl('BAD')
+ <SubmitControl name='submit_me' type='submit'>
+ >>> browser.getControl('BAD').value
+ 'BAD'
+ >>> browser.getControl('BAD').click() # doctest: +REPORT_NDIFF
+ POST / HTTP/1.1
+ Content-length: 176
+ Connection: close
+ Content-type: multipart/form-data; boundary=---------------------------100167997466992641913031254
+ Host: localhost
+ User-agent: Python-urllib/2.4
+ <BLANKLINE>
+ -----------------------------100167997466992641913031254
+ Content-disposition: form-data; name="submit_me"
+ <BLANKLINE>
+ BAD
+ -----------------------------100167997466992641913031254--
+ <BLANKLINE>
+
+This also works if the labels have whitespace around them (this tests a
+regression caused by the original fix for the above):
+
+ >>> browser.open('''\
+ ... <html><body>
+ ... <form action="." method="post" enctype="multipart/form-data">
+ ... <input type="submit" name="submit_me" value=" GOOD " />
+ ... <input type="submit" name="submit_me" value=" BAD " />
+ ... </form></body></html>
+ ... ''') # doctest: +ELLIPSIS
+ GET / HTTP/1.1
+ ...
+ >>> browser.getControl('BAD')
+ <SubmitControl name='submit_me' type='submit'>
+ >>> browser.getControl('BAD').value
+ ' BAD '
+ >>> browser.getControl('BAD').click() # doctest: +REPORT_NDIFF
+ POST / HTTP/1.1
+ Content-length: 176
+ Connection: close
+ Content-type: multipart/form-data; boundary=---------------------------100167997466992641913031254
+ Host: localhost
+ User-agent: Python-urllib/2.4
+ <BLANKLINE>
+ -----------------------------100167997466992641913031254
+ Content-disposition: form-data; name="submit_me"
+ <BLANKLINE>
+ BAD
+ -----------------------------100167997466992641913031254--
+ <BLANKLINE>
+
+"""
+
+def test_file_upload():
+ """
+
+ >>> browser = Browser()
+
+When given a form with a file-upload
+
+ >>> browser.open('''\
+ ... <html><body>
+ ... <form action="." method="post" enctype="multipart/form-data">
+ ... <input name="foo" type="file" />
+ ... <input type="submit" value="OK" />
+ ... </form></body></html>
+ ... ''') # doctest: +ELLIPSIS
+ GET / HTTP/1.1
+ ...
+
+Fill in the form value using add_file:
+
+ >>> browser.getControl(name='foo').add_file(
+ ... StringIO('sample_data'), 'text/foo', 'x.foo')
+ >>> browser.getControl('OK').click()
+ POST / HTTP/1.1
+ Content-length: 173
+ Connection: close
+ Content-type: multipart/form-data; boundary=127.0.0.11000318041146699896411
+ Host: localhost
+ User-agent: Python-urllib/2.99
+ <BLANKLINE>
+ --127.0.0.11000318041146699896411
+ Content-disposition: form-data; name="foo"; filename="x.foo"
+ Content-type: text/foo
+ <BLANKLINE>
+ sample_data
+ --127.0.0.11000318041146699896411--
+ <BLANKLINE>
+
+You can pass a string to add_file:
+
+
+ >>> browser.getControl(name='foo').add_file(
+ ... 'blah blah blah', 'text/blah', 'x.blah')
+ >>> browser.getControl('OK').click()
+ POST / HTTP/1.1
+ Content-length: 178
+ Connection: close
+ Content-type: multipart/form-data; boundary=127.0.0.11000318541146700017052
+ Host: localhost
+ User-agent: Python-urllib/2.98
+ <BLANKLINE>
+ --127.0.0.11000318541146700017052
+ Content-disposition: form-data; name="foo"; filename="x.blah"
+ Content-type: text/blah
+ <BLANKLINE>
+ blah blah blah
+ --127.0.0.11000318541146700017052--
+ <BLANKLINE>
+
+
+ """
+
+
+def test_strip_linebreaks_from_textarea(self):
+ """
+
+ >>> browser = Browser()
+
+According to http://www.w3.org/TR/html4/appendix/notes.html#h-B.3.1 line break
+immediately after start tags or immediately before end tags must be ignored,
+but real browsers only ignore a line break after a start tag. So if we give
+the following form:
+
+ >>> browser.open('''
+ ... <html><body>
+ ... <form action="." method="post" enctype="multipart/form-data">
+ ... <textarea name="textarea">
+ ... Foo
+ ... </textarea>
+ ... </form></body></html>
+ ... ''') # doctest: +ELLIPSIS
+ GET / HTTP/1.1
+ ...
+
+The value of the textarea won't contain the first line break:
+
+ >>> browser.getControl(name='textarea').value
+ 'Foo\\n'
+
+Of course, if we add line breaks, so that there are now two line breaks
+after the start tag, the textarea value will start and end with a line break.
+
+ >>> browser.open('''
+ ... <html><body>
+ ... <form action="." method="post" enctype="multipart/form-data">
+ ... <textarea name="textarea">
+ ...
+ ... Foo
+ ... </textarea>
+ ... </form></body></html>
+ ... ''') # doctest: +ELLIPSIS
+ GET / HTTP/1.1
+ ...
+
+ >>> browser.getControl(name='textarea').value
+ '\\nFoo\\n'
+
+Also, if there is some other whitespace after the start tag, it will be preserved.
+
+ >>> browser.open('''
+ ... <html><body>
+ ... <form action="." method="post" enctype="multipart/form-data">
+ ... <textarea name="textarea"> Foo </textarea>
+ ... </form></body></html>
+ ... ''') # doctest: +ELLIPSIS
+ GET / HTTP/1.1
+ ...
+
+ >>> browser.getControl(name='textarea').value
+ ' Foo '
+ """
+
+class win32CRLFtransformer(object):
+ def sub(self, replacement, text):
+ return text.replace(r'\r','')
+
+checker = renormalizing.RENormalizing([
+ (re.compile(r'^--\S+\.\S+\.\S+', re.M), '-'*30),
+ (re.compile(r'boundary=\S+\.\S+\.\S+'), 'boundary='+'-'*30),
+ (re.compile(r'^---{10}.*', re.M), '-'*30),
+ (re.compile(r'boundary=-{10}.*'), 'boundary='+'-'*30),
+ (re.compile(r'User-agent:\s+\S+'), 'User-agent: Python-urllib/2.4'),
+ (re.compile(r'Content-[Ll]ength:.*'), 'Content-Length: 123'),
+ (re.compile(r'Status: 200.*'), 'Status: 200 OK'),
+ (re.compile(r'httperror_seek_wrapper:', re.M), 'HTTPError:'),
+ (win32CRLFtransformer(), None),
+ (re.compile(r'User-Agent: Python-urllib/2.5'), 'User-agent: Python-urllib/2.4'),
+ (re.compile(r'Host: localhost'), 'Connection: close'),
+ (re.compile(r'Content-Type: '), 'Content-type: '),
+ ])
+
+def serve_requests(server):
+ global server_stopped
+ global server_stop
+ server_stop = False
+ while not server_stop:
+ server.handle_request()
+ server.socket.close()
+
+def setUpServer(test):
+ port = random.randint(20000,30000)
+ test.globs['TEST_PORT'] = port
+ server = BaseHTTPServer.HTTPServer(('localhost', port), TestHandler)
+ thread = threading.Thread(target=serve_requests, args=[server])
+ thread.setDaemon(True)
+ thread.start()
+ test.globs['web_server_thread'] = thread
+
+def tearDownServer(test):
+ global server_stop
+ server_stop = True
+ # make a request, so the last call to `handle_one_request` will return
+ urllib.urlretrieve('http://localhost:%d/' % test.globs['TEST_PORT'])
+ test.globs['web_server_thread'].join()
+
+def setUpReal(test):
+ test.globs['Browser'] = zc.testbrowser.real.Browser
+ setUpServer(test)
+
+def tearDownReal(test):
+ tearDownServer(test)
+
+def setUpReadme(test):
+ test.globs['Browser'] = zc.testbrowser.browser.Browser
+ setUpServer(test)
+
+def tearDownReadme(test):
+ tearDownServer(test)
+
+def setUpHeaders(test):
+ setUpServer(test)
+ test.globs['browser'] = zc.testbrowser.browser.Browser()
+
+def tearDownHeaders(test):
+ tearDownServer(test)
+
+def test_suite():
+ flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
+
+ readme = doctest.DocFileSuite('README.txt', optionflags=flags,
+ checker=checker, setUp=setUpReadme, tearDown=tearDownReadme)
+
+ headers = doctest.DocFileSuite('headers.txt', optionflags=flags,
+ setUp=setUpHeaders, tearDown=tearDownHeaders)
+
+ real = doctest.DocFileSuite('real.txt', optionflags=flags,
+ checker=checker, setUp=setUpReal, tearDown=tearDownReal)
+ real.level = 3
+
+ real_readme = doctest.DocFileSuite('README.txt', optionflags=flags,
+ checker=checker, setUp=setUpReal, tearDown=tearDownReal)
+ real_readme.level = 3
+
+ screen_shots = doctest.DocFileSuite('screen-shots.txt', optionflags=flags)
+ screen_shots.level = 3
+
+ this_file = doctest.DocTestSuite(checker=checker)
+
+ return unittest.TestSuite((this_file, readme, real_readme, real,
+ screen_shots))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
Property changes on: zc.testbrowser/trunk/src/zc/testbrowser/tests.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
More information about the Checkins
mailing list