[Checkins] SVN: Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/ checkpoint

Benji York benji at zope.com
Mon Aug 14 09:14:45 EDT 2006


Log message for revision 69467:
  checkpoint
   - general framework is in place
   - tests run to completion
   - several tests commented out
   - several others raise NotImplementedError (mostly form-related)
  

Changed:
  U   Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/ftests/navigate.html
  U   Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/ftests/simple.html
  U   Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/__resources__/commands.js
  U   Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/__resources__/shim.js
  U   Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/proxy.py
  U   Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/real.py
  U   Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/real.txt

-=-
Modified: Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/ftests/navigate.html
===================================================================
--- Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/ftests/navigate.html	2006-08-14 13:09:26 UTC (rev 69466)
+++ Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/ftests/navigate.html	2006-08-14 13:14:44 UTC (rev 69467)
@@ -9,7 +9,7 @@
 
     <a href="navigate.html?message=By+Link+Text">Link Text</a>
 
-    <a href="navigate.html?message=By+Link+Text+with+Normalization"> Link Text 
+    <a href="navigate.html?message=By+Link+with+Normalization"> Link 
     with     Whitespace	Normalization (and parens) </a>
 
     <a href="navigate.html?message=By+URL">Using the URL</a>
@@ -27,11 +27,11 @@
 
     <img src="./zope3logo.gif" usemap="#zope3logo" />
     <map name="zope3logo">
-      <area shape="rect" alt="Zope3" 
-            href="navigate.html?message=Zope+3+Name" id="zope3" title="Zope 3" 
+      <area shape="rect" alt="Zope3"
+            href="navigate.html?message=Zope+3+Name" id="zope3" title="Zope 3"
             coords="44,7,134,33" />
-      <area shape="circle" alt="Logo" 
-            href="navigate.html?message=Logo" id="logo" title="Logo" 
+      <area shape="circle" alt="Logo"
+            href="navigate.html?message=Logo" id="logo" title="Logo"
             coords="23,21,18" />
     </map>
 

Modified: Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/ftests/simple.html
===================================================================
--- Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/ftests/simple.html	2006-08-14 13:09:26 UTC (rev 69466)
+++ Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/ftests/simple.html	2006-08-14 13:14:44 UTC (rev 69467)
@@ -1,3 +1,5 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"DTD/xhtml1-strict.dtd">
 <html>
   <head>
     <title>Simple Page</title>

Modified: Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/__resources__/commands.js
===================================================================
--- Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/__resources__/commands.js	2006-08-14 13:09:26 UTC (rev 69466)
+++ Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/__resources__/commands.js	2006-08-14 13:14:44 UTC (rev 69467)
@@ -15,7 +15,12 @@
 }
 
 function _tb_getContents(info) {
-    return top.frames[0].document.body.parentNode.innerHTML;
+    temp = document.createElement('div');
+    clone = top.frames[0].document.documentElement.cloneNode(true);
+    temp.appendChild(clone);
+    contents=temp.innerHTML;
+    temp.innerHTML="";
+    return contents;
 }
 
 function _tb_getUrl(info) {
@@ -33,15 +38,67 @@
 }
 
 function _tb_rememberLinkN(info) {
-    links = top.frames[0].document.getElementsByTagName('a');
-    var id = _tb_remembered_links.length
+    function getElementsByTagNames(tagNames,obj) {
+        if (!obj) var obj = document;
+        var resultArray = new Array();
+        for (var i=0;i<tagNames.length;i++) {
+            var tags = obj.getElementsByTagName(tagNames[i]);
+            for (var j=0;j<tags.length;j++) {
+                resultArray.push(tags[j]);
+            }
+        }
+        var testNode = resultArray[0];
+        if (!testNode) return [];
+        if (testNode.sourceIndex) {
+            resultArray.sort(function (a,b) {
+                return a.sourceIndex - b.sourceIndex;
+            });
+        }
+        else if (testNode.compareDocumentPosition) {
+            resultArray.sort(function (a,b) {
+                return 3 - (a.compareDocumentPosition(b) & 6);
+            });
+        }
+        return resultArray;
+}
+//    function getElementsByTagNames(names, node) {
+//        var results = [];
+//        for (var i in names) {
+//            var tags = node.getElementsByTagName(names[i]);
+//            for (var j in tags) {
+//                results.push(tags[j]);
+//            }
+//        }
+//        var example = results[0];
+
+//        if (!example) return [];
+
+//        if (example.sourceIndex) {
+//            results.sort(function (a,b) {return a.sourceIndex - b.sourceIndex;});
+//        }
+//        else if (example.compareDocumentPosition) {
+//            results.sort(function (a,b) {
+//                return 3 - (a.compareDocumentPosition(b) & 6);
+//            });
+//        }
+//        return results;
+//    }
+
+    log(info);
+    var links = getElementsByTagNames(['a', 'area'], top.frames[0].document);
+    var id = _tb_remembered_links.length;
     _tb_remembered_links[id] = links[info[0]];
+    log(links.length);
+    log(id);
+    log(_tb_remembered_links[id]);
     return id;
 }
 
 function _tb_clickRememberedLink(info) {
+    log(info);
     var n = info[0];
     var element = _tb_remembered_links[n];
+    log(element);
     _tb_click(element);
     return '_tb_WAIT_FOR_PAGE_LOAD';
 }

Modified: Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/__resources__/shim.js
===================================================================
--- Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/__resources__/shim.js	2006-08-14 13:09:26 UTC (rev 69466)
+++ Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/__resources__/shim.js	2006-08-14 13:14:44 UTC (rev 69467)
@@ -5,9 +5,10 @@
 function _tb_gotNextCommand(info) {
     log('gotNextCommand');
     var command = info[0];
+    log(command);
     last_result = eval(info[0] + '(' + info[1] + ')');
     if (last_result == '_tb_WAIT_FOR_PAGE_LOAD') {
-        _tb_waitForLoad(eval(_tb_nextCommand));
+        _tb_waitForLoad(_tb_nextCommand);
     } else if (!should_stop) {
         _tb_nextCommand();
     }
@@ -17,8 +18,8 @@
     log('waiting');
     _tb_onloadFunc = function() {
         log('loaded');
-        setTimeout(func, 500);
         _tb_onloadFunc = function() {};
+        func();
     }
 }
 

Modified: Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/proxy.py
===================================================================
--- Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/proxy.py	2006-08-14 13:09:26 UTC (rev 69466)
+++ Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/proxy.py	2006-08-14 13:14:44 UTC (rev 69467)
@@ -14,7 +14,8 @@
 import urlparse
 
 base_dir = os.path.dirname(__file__)
-allowed_resources = ['MochiKit', 'shim.js', 'commands.js', 'start.html']
+allowed_resources = ['MochiKit', 'shim.js', 'commands.js', 'start.html',
+    'workspace.html']
 
 PROXY_PORT = 23123
 PROXY_HOST = '127.0.0.1'

Modified: Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/real.py
===================================================================
--- Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/real.py	2006-08-14 13:09:26 UTC (rev 69466)
+++ Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/real.py	2006-08-14 13:14:44 UTC (rev 69467)
@@ -33,8 +33,8 @@
 def getTagText(soup):
     text = str(soup)
     text = re.sub('<[^>]*>', '', text)
-    text = re.sub(' +', ' ', text)
-    return text
+    text = re.sub('\s+', ' ', text)
+    return text.strip()
 
 
 class Browser(SetattrErrorsMixin):
@@ -59,32 +59,34 @@
     @property
     def url(self):
         """See zope.testbrowser.interfaces.IBrowser"""
-        return self.executeCommand('getUrl')
+        bits = list(urlparse.urlsplit(self.executeCommand('getUrl')))
+        bits[1] = 'localhost'
+        return urlparse.urlunsplit(bits)
 
+
     @property
     def isHtml(self):
         """See zope.testbrowser.interfaces.IBrowser"""
-        raise NotImplemented
+        raise NotImplementedError
 
     @property
     def title(self):
         """See zope.testbrowser.interfaces.IBrowser"""
-        raise NotImplemented
+        raise NotImplementedError
 
     @property
     def contents(self):
         """See zope.testbrowser.interfaces.IBrowser"""
-        # XXX see note in commands.js
         return self.executeCommand('getContents')
 
     @property
     def headers(self):
         """See zope.testbrowser.interfaces.IBrowser"""
-        raise NotImplemented
+        raise NotImplementedError
 
     def handleErrors():
         """See zope.testbrowser.interfaces.IBrowser"""
-        raise NotImplemented
+        raise NotImplementedError
 
     def open(self, url, data=None):
         """See zope.testbrowser.interfaces.IBrowser"""
@@ -104,7 +106,7 @@
 
     def executeCommand(self, command, *args):
         """Execute a JavaScript routine on the client (not an official API)"""
-        return self.serverManager.executeCommand(command, *args)
+        return str(self.serverManager.executeCommand(command, *args))
 
     def _start_timer(self):
         self.timer.start()
@@ -138,31 +140,30 @@
 
     def addHeader(self, key, value):
         """See zope.testbrowser.interfaces.IBrowser"""
-        raise NotImplemented
+        raise NotImplementedError
 
     def getLink(self, text=None, url=None, id=None, index=None):
         """See zope.testbrowser.interfaces.IBrowser"""
-        soup = BeautifulSoup(self.contents)('a')
+        soup = BeautifulSoup(self.contents)(re.compile('^(a|area)$'))
         links = []
 
         # "msg" holds the disambiguation message
         # the Link instance below needs to know the index of the a tag (n)
         if text is not None:
             msg = 'text %r' % text
-            links = []
             for n, a in enumerate(soup):
                 # remove all tags from the text in order to search it
                 if text in getTagText(a):
                     links.append((a, n))
         elif url is not None:
-            msg = 'url %r' % text
+            msg = 'url %r' % url
             for n, a in enumerate(soup):
-                if a['href'] == url:
+                if url in a['href']:
                     links.append((a, n))
         elif id is not None:
             msg = 'id %r' % id
             for n, a in enumerate(soup):
-                if a['id'] == id:
+                if a.get('id') == id:
                     links.append((a, n))
 
         link, n = disambiguate(links, msg, index)
@@ -170,11 +171,11 @@
 
     def getControl(self, label=None, name=None, index=None):
         """See zope.testbrowser.interfaces.IBrowser"""
-        raise NotImplemented
+        raise NotImplementedError
 
     def getForm(self, id=None, name=None, action=None, index=None):
         """See zope.testbrowser.interfaces.IBrowser"""
-        raise NotImplemented
+        raise NotImplementedError
 
     def _changed(self):
         self._counter += 1

Modified: Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/real.txt
===================================================================
--- Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/real.txt	2006-08-14 13:09:26 UTC (rev 69466)
+++ Zope3/branches/benji-testbrowser-with-real-browsers-take-2/src/zope/testbrowser/real/real.txt	2006-08-14 13:14:44 UTC (rev 69467)
@@ -6,51 +6,1177 @@
 the testbrowser API.  First, instantiate a Browser object.
 
     >>> from zope.testbrowser.real.real import Browser # XXX real.real should go
-    >>> browser = Browser()
 
-We can open pages and look at their contents.
+An initial page to load can be passed to the ``Browser`` constructor:
 
-    >>> browser.open('http://localhost/')
+    >>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
+    >>> browser.url
+    'http://localhost/@@/testbrowser/simple.html'
+
+<<<snip addHeader calls>>>
+
+An existing browser instance can also `open` web pages:
+
+    >>> browser.open('http://localhost/@@/testbrowser/simple.html')
+    >>> browser.url
+    'http://localhost/@@/testbrowser/simple.html'
+
+Once you have opened a web page initially, best practice for writing
+testbrowser doctests suggests using 'click' to navigate further (as discussed
+below), except in unusual circumstances.
+
+The test browser complies with the IBrowser interface; see
+``zope.testbrowser.interfaces`` for full details on the interface.
+
+XXX uncomment later
+
+    >>> from zope.testbrowser import interfaces
+    >>> from zope.interface.verify import verifyObject
+
+#    >>> verifyObject(interfaces.IBrowser, browser)
+#    True
+
+
+Page Contents
+-------------
+
+The contents of the current page are available:
+
+    >>> print browser.contents # XXX any way to not get screwed up HTML?
+    <html><head><title>Simple Page</title></head>
+    <BLANKLINE>
+      <body>
+        <h1>Simple Page</h1>
+      </body></html>
+
+Making assertions about page contents is easy.
+
+    >>> '<h1>Simple Page</h1>' in browser.contents
+    True
+
+Utilizing the doctest facilities, it also possible to do:
+
     >>> browser.contents
-    u'...</head>...'
+    '...<h1>Simple Page</h1>...'
 
+Note: Unfortunately, ellipsis (...) cannot be used at the beginning of the
+output (this is a limitation of doctest).
+
+
+
+Checking for HTML
+-----------------
+
+#Not all URLs return HTML.  Of course our simple page does:
+#
+#    >>> browser.open('http://localhost/@@/testbrowser/simple.html')
+#
+#    >>> browser.isHtml
+#    True
+
+#But if we load an image (or other binary file), we do not get HTML:
+#
+#    >>> browser.open('http://localhost/@@/testbrowser/zope3logo.gif')
+#    >>> browser.isHtml
+#    False
+
+
+
+HTML Page Title
+----------------
+
+Another useful helper property is the title:
+
+    >>> browser.open('http://localhost/@@/testbrowser/simple.html')
+    >>> browser.title
+    'Simple Page'
+
+If a page does not provide a title, it is simply ``None``:
+
+    >>> browser.open('http://localhost/@@/testbrowser/notitle.html')
+    >>> browser.title
+
+However, if the output is not HTML, then an error will occur trying to access
+the title:
+
+#    >>> browser.open('http://localhost/@@/testbrowser/zope3logo.gif')
+#    >>> browser.title
+#    Traceback (most recent call last):
+#    ...
+#    BrowserStateError: not viewing HTML
+
+
+Headers
+-------
+
+As you can see, the `contents` of the browser does not return any HTTP
+headers.  The headers are accessible via a separate attribute, which is an
+``httplib.HTTPMessage`` instance (httplib is a part of Python's standard
+library):
+
+    >>> browser.open('http://localhost/@@/testbrowser/simple.html')
+    >>> browser.headers
+    <httplib.HTTPMessage instance...>
+
+The headers can be accessed as a string:
+
+    >>> print browser.headers
+    Status: 200 Ok
+    Content-Length: ...
+    Content-Type: text/html;charset=utf-8
+    X-Content-Type-Warning: guessed from content
+    X-Powered-By: Zope (www.zope.org), Python (www.python.org)
+
+Or as a mapping:
+
+    >>> browser.headers['content-type']
+    'text/html;charset=utf-8'
+
+
+Navigation and Link Objects
+---------------------------
+
+If you want to simulate clicking on a link, get the link and `click` on it.
+In the `navigate.html` file there are several links set up to demonstrate the
+capabilities of the link objects and their `click` method.
+
+The simplest way to get a link is via the anchor text.  In other words
+the text you would see in a browser (text and url searches are substring
+searches):
+
+    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
     >>> browser.contents
-    u'...[top]...'
+    '...<a href="navigate.html?message=By+Link+Text">Link Text</a>...'
+    >>> link = browser.getLink('Link Text')
+    >>> link
+    <Link text='Link Text'
+      url='http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text'>
 
-    >>> browser.getLink('[top]', index=0)
-    <Link text='[top]' url=u'http://localhost:23123/@@SelectedManagementView.html'>
+Link objects comply with the ILink interface.
 
-If a link doesn't exist, we get an exception.
+    >>> verifyObject(interfaces.ILink, link)
+    True
 
-    >>> browser.getLink('does not exist')
+Links expose several attributes for easy access.
+
+    >>> link.text
+    'Link Text'
+    >>> link.tag # links can also be image maps.
+    'a'
+    >>> link.url # it's normalized
+    'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text'
+    >>> link.attrs
+    {'href': 'navigate.html?message=By+Link+Text'}
+
+Links can be "clicked" and the browser will navigate to the referenced URL.
+
+    >>> link.click()
+    >>> browser.url
+    'http://localhost/@@/testbrowser/navigate.html?message=By+Link+Text'
+    >>> browser.contents
+    '...Message: <em>By Link Text</em>...'
+
+When finding a link by its text, whitespace is normalized.
+
+    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
+    >>> browser.contents
+    '...> Link \n    with     Whitespace\tNormalization (and parens) </...'
+    >>> link = browser.getLink('Link with Whitespace Normalization '
+    ...                        '(and parens)')
+    >>> link
+    <Link text='Link with Whitespace Normalization (and parens)'...>
+    >>> link.text
+    'Link with Whitespace Normalization (and parens)'
+    >>> link.click()
+    >>> browser.url
+    'http://localhost/@@/testbrowser/navigate.html?message=By+Link+with+Normalization'
+    >>> browser.contents
+    '...Message: <em>By Link with Normalization</em>...'
+
+Note that clicking a link object after its browser page has expired will
+generate an error.
+
+    >>> link.click()
     Traceback (most recent call last):
     ...
-    LookupError: text 'does not exist'
+    ExpiredError
 
-Links can be clicked.
+You can also find the link by its URL,
 
-    >>> browser.getLink('[top]', index=0).click()
+    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
+    >>> browser.contents
+    '...<a href="navigate.html?message=By+URL">Using the URL</a>...'
 
-We can retrieve the current address.
+    >>> browser.getLink(url='?message=By+URL').click()
+    >>> browser.url
+    'http://localhost/@@/testbrowser/navigate.html?message=By+URL'
+    >>> browser.contents
+    '...Message: <em>By URL</em>...'
 
+or its id:
+
+    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
+    >>> browser.contents
+    '...<a href="navigate.html?message=By+Id"
+    id="anchorid">By Anchor Id</a>...'
+
+    >>> browser.getLink(id='anchorid').click()
     >>> browser.url
-    u'http://localhost:23123/'
+    'http://localhost/@@/testbrowser/navigate.html?message=By+Id'
+    >>> browser.contents
+    '...Message: <em>By Id</em>...'
 
-Reload works.
+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:
 
-    >>> original_contents = browser.contents
+    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
+    >>> link = browser.getLink(id='zope3')
+    >>> link.tag
+    'area'
+    >>> link.click()
+    >>> browser.url
+    'http://localhost/@@/testbrowser/navigate.html?message=Zope+3+Name'
+    >>> browser.contents
+    '...Message: <em>Zope 3 Name</em>...'
+
+Getting a nonexistent link raises an exception.
+
+    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
+    >>> browser.getLink('This does not exist')
+    Traceback (most recent call last):
+    ...
+    LookupError: text 'This does not exist'
+
+
+Other Navigation
+----------------
+
+Like in any normal browser, you can reload a page:
+
+    >>> browser.open('http://localhost/@@/testbrowser/simple.html')
+    >>> browser.url
+    'http://localhost/@@/testbrowser/simple.html'
     >>> browser.reload()
-    >>> original_contents == browser.contents
+    >>> browser.url
+    'http://localhost/@@/testbrowser/simple.html'
+
+You can also go back:
+
+    >>> browser.open('http://localhost/@@/testbrowser/notitle.html')
+    >>> browser.url
+    'http://localhost/@@/testbrowser/notitle.html'
+    >>> browser.goBack()
+    >>> browser.url
+    'http://localhost/@@/testbrowser/simple.html'
+
+
+Controls
+--------
+
+One of the most important features of the browser is the ability to inspect
+and fill in values for the controls of input forms.  To do so, let's first open
+a page that has a bunch of controls:
+
+    >>> browser.open('http://localhost/@@/testbrowser/controls.html')
+
+Obtaining a Control
+~~~~~~~~~~~~~~~~~~~
+
+You look up browser controls with the 'getControl' method.  The default first
+argument is 'label', and looks up the form on the basis of any associated
+label.
+
+    >>> control = browser.getControl('Text Control')
+    >>> control
+    <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'>
+    >>> browser.getControl('One')
+    <ItemControl name='multi-checkbox-value' type='checkbox' optionValue='1'>
+    >>> browser.getControl('Tres')
+    <ItemControl name='single-select-value' type='select' optionValue='3'>
+
+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(interfaces.IControl, ctrl)
     True
 
+They have several useful attributes:
 
-goBack doesn't work. XXX
+  - the name as which the control is known to the form:
 
-    >>> browser.getLink('[top]', index=0).click()
-    >>> browser.goBack()
-    >>> original_contents == browser.contents
+    >>> 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(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'>,
+     <ItemControl name='multi-select-value' type='select' optionValue='2'>,
+     <ItemControl name='multi-select-value' type='select' optionValue='3'>]
+
+  - 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'>
+    >>> ctrl.getControl('Deux')
+    <ItemControl name='multi-select-value' type='select' optionValue='2'>
+    >>> ctrl.getControl('Trois') # label attribute
+    <ItemControl name='multi-select-value' type='select' optionValue='3'>
+    >>> ctrl.getControl('Third') # contents
+    <ItemControl name='multi-select-value' type='select' optionValue='3'>
+    >>> 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(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(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(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(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(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(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(interfaces.IListControl, ctrl)
+    True
+    >>> ctrl.value
+    True
+    >>> ctrl.value = False
+    >>> ctrl.disabled
+    False
+    >>> ctrl.multiple
+    True
+    >>> ctrl.options
+    [True]
+    >>> ctrl.displayOptions
+    ['Single Unvalued Checkbox']
+    >>> ctrl.displayValue
+    []
+    >>> verifyObject(
+    ...     interfaces.IItemControl,
+    ...     browser.getControl('Single Unvalued Checkbox'))
+    True
+    >>> browser.getControl('Single Unvalued Checkbox').optionValue
+    'on'
+    >>> browser.getControl('Single Unvalued Checkbox').selected
+    False
+    >>> ctrl.displayValue = ['Single Unvalued Checkbox']
+    >>> ctrl.displayValue
+    ['Single Unvalued Checkbox']
+    >>> browser.getControl('Single Unvalued Checkbox').selected
+    True
+    >>> browser.getControl('Single Unvalued Checkbox').selected = False
+    >>> browser.getControl('Single Unvalued Checkbox').selected
+    False
+    >>> ctrl.displayValue
+    []
+    >>> browser.getControl(
+    ...     name='single-disabled-unvalued-checkbox-value').disabled
+    True
+
+  - Checkbox Control (Single-Valued, Valued)
+
+    >>> ctrl = browser.getControl(name='single-valued-checkbox-value')
+    >>> ctrl
+    <ListControl name='single-valued-checkbox-value' type='checkbox'>
+    >>> verifyObject(interfaces.IListControl, ctrl)
+    True
+    >>> ctrl.value
+    ['1']
+    >>> ctrl.value = []
+    >>> ctrl.disabled
+    False
+    >>> ctrl.multiple
+    True
+    >>> ctrl.options
+    ['1']
+    >>> ctrl.displayOptions
+    ['Single Valued Checkbox']
+    >>> ctrl.displayValue
+    []
+    >>> verifyObject(
+    ...     interfaces.IItemControl,
+    ...     browser.getControl('Single Valued Checkbox'))
+    True
+    >>> browser.getControl('Single Valued Checkbox').selected
+    False
+    >>> browser.getControl('Single Valued Checkbox').optionValue
+    '1'
+    >>> ctrl.displayValue = ['Single Valued Checkbox']
+    >>> ctrl.displayValue
+    ['Single Valued Checkbox']
+    >>> browser.getControl('Single Valued Checkbox').selected
+    True
+    >>> browser.getControl('Single Valued Checkbox').selected = False
+    >>> browser.getControl('Single Valued Checkbox').selected
+    False
+    >>> ctrl.displayValue
+    []
+
+  - Checkbox Control (Multi-Valued)
+
+    >>> ctrl = browser.getControl(name='multi-checkbox-value')
+    >>> ctrl
+    <ListControl name='multi-checkbox-value' type='checkbox'>
+    >>> verifyObject(interfaces.IListControl, ctrl)
+    True
+    >>> ctrl.value
+    ['1', '3']
+    >>> ctrl.value = ['1', '2']
+    >>> ctrl.disabled
+    False
+    >>> ctrl.multiple
+    True
+    >>> ctrl.options
+    ['1', '2', '3']
+    >>> ctrl.displayOptions
+    ['One', 'Two', 'Three']
+    >>> ctrl.displayValue
+    ['One', 'Two']
+    >>> ctrl.displayValue = ['Two']
+    >>> ctrl.value
+    ['2']
+    >>> browser.getControl('Two').optionValue
+    '2'
+    >>> browser.getControl('Two').selected
+    True
+    >>> verifyObject(interfaces.IItemControl, browser.getControl('Two'))
+    True
+    >>> browser.getControl('Three').selected = True
+    >>> browser.getControl('Three').selected
+    True
+    >>> browser.getControl('Two').selected
+    True
+    >>> ctrl.value
+    ['2', '3']
+    >>> browser.getControl('Two').selected = False
+    >>> ctrl.value
+    ['3']
+    >>> browser.getControl('Three').selected = False
+    >>> ctrl.value
+    []
+
+  - Radio Control
+
+    This is how you get a radio button based control:
+
+    >>> ctrl = browser.getControl(name='radio-value')
+
+    This shows the existing value of the control, as it was in the
+    HTML received from the server:
+
+    >>> ctrl.value
+    ['2']
+
+    We can then unselect it:
+
+    >>> ctrl.value = []
+    >>> ctrl.value
+    []
+
+    We can also reselect it:
+
+    >>> ctrl.value = ['2']
+    >>> ctrl.value
+    ['2']
+
+    displayValue shows the text the user would see next to the
+    control:
+
+    >>> ctrl.displayValue
+    ['Zwei']
+
+    This is just unit testing:
+
+    >>> ctrl
+    <ListControl name='radio-value' type='radio'>
+    >>> verifyObject(interfaces.IListControl, ctrl)
+    True
+    >>> ctrl.disabled
+    False
+    >>> ctrl.multiple
+    False
+    >>> ctrl.options
+    ['1', '2', '3']
+    >>> ctrl.displayOptions
+    ['Ein', 'Zwei', 'Drei']
+    >>> ctrl.displayValue = ['Ein']
+    >>> ctrl.value
+    ['1']
+    >>> ctrl.displayValue
+    ['Ein']
+
+    The radio control subcontrols were illustrated above.
+
+  - Image Control
+
+    >>> ctrl = browser.getControl(name='image-value')
+    >>> ctrl
+    <ImageControl name='image-value' type='image'>
+    >>> verifyObject(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(interfaces.ISubmitControl, ctrl)
+    True
+    >>> ctrl.value
+    'Submit This'
+    >>> ctrl.disabled
+    False
+    >>> ctrl.multiple
+    False
+
+Using Submitting Controls
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Both the submit and image type should be clickable and submit the form:
+
+    >>> browser.getControl('Text Control').value = 'Other Text'
+    >>> browser.getControl('Submit').click()
+    >>> print browser.contents
+    <html>
+    ...
+    <em>Other Text</em>
+    <input type="text" name="text-value" id="text-value" value="Some Text" />
+    ...
+    <em>Submit This</em>
+    <input type="submit" name="submit-value" id="submit-value" value="Submit This" />
+    ...
+    </html>
+
+Note that if you click a submit object after the associated page has expired,
+you will get an error.
+
+    >>> browser.open('http://localhost/@@/testbrowser/controls.html')
+    >>> ctrl = browser.getControl('Submit')
+    >>> ctrl.click()
+    >>> ctrl.click()
+    Traceback (most recent call last):
+    ...
+    ExpiredError
+
+All the above also holds true for the image control:
+
+    >>> browser.open('http://localhost/@@/testbrowser/controls.html')
+    >>> browser.getControl('Text Control').value = 'Other Text'
+    >>> browser.getControl(name='image-value').click()
+    >>> print browser.contents
+    <html>
+    ...
+    <em>Other Text</em>
+    <input type="text" name="text-value" id="text-value" value="Some Text" />
+    ...
+    <em>1</em>
+    <em>1</em>
+    <input type="image" name="image-value" id="image-value"
+           src="zope3logo.gif" />
+    ...
+    </html>
+
+    >>> browser.open('http://localhost/@@/testbrowser/controls.html')
+    >>> ctrl = browser.getControl(name='image-value')
+    >>> ctrl.click()
+    >>> ctrl.click()
+    Traceback (most recent call last):
+    ...
+    ExpiredError
+
+But when sending an image, you can also specify the coordinate you clicked:
+
+    >>> browser.open('http://localhost/@@/testbrowser/controls.html')
+    >>> browser.getControl(name='image-value').click((50,25))
+    >>> print browser.contents
+    <html>
+    ...
+    <em>50</em>
+    <em>25</em>
+    <input type="image" name="image-value" id="image-value"
+           src="zope3logo.gif" />
+    ...
+    </html>
+
+
+Forms
+-----
+
+Because pages can have multiple forms with like-named controls, it is sometimes
+necessary to access forms by name or id.  The browser's `forms` attribute can
+be used to do so.  The key value is the form's name or id.  If more than one
+form has the same name or id, the first one will be returned.
+
+    >>> browser.open('http://localhost/@@/testbrowser/forms.html')
+    >>> form = browser.getForm(name='one')
+
+Form instances conform to the IForm interface.
+
+    >>> verifyObject(interfaces.IForm, form)
+    True
+
+The form exposes several attributes related to forms:
+
+  - The name of the form:
+
+    >>> form.name
+    'one'
+
+  - The id of the form:
+
+    >>> form.id
+    '1'
+
+  - The action (target URL) when the form is submitted:
+
+    >>> form.action
+    'http://localhost/@@/testbrowser/forms.html'
+
+  - The method (HTTP verb) used to transmit the form data:
+
+    >>> form.method
+    'POST'
+
+  - The encoding type of the form data:
+
+    >>> form.enctype
+    'multipart/form-data'
+
+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')
+    >>> print browser.contents
+    <html>
+    ...
+    <em>First Text</em>
+    ...
+    </html>
+
+Submitting also works without specifying a control, as shown below, which is
+it's primary reason for existing in competition with the control submission
+discussed above.
+
+Now let me show you briefly that looking up forms is sometimes important.  In
+the `forms.html` template, we have four forms all having a text control named
+`text-value`.  Now, if I use the browser's `get` method,
+
+    >>> browser.getControl(name='text-value')
+    Traceback (most recent call last):
+    ...
+    AmbiguityError: name 'text-value'
+    >>> 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
+    '...<em>Second Text</em>...'
+    >>> form = browser.getForm('2')
+    >>> form.getControl('Submit').click()
+    >>> browser.contents
+    '...<em>Second Text</em>...'
+    >>> 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
+    '...<em>Fourth Text</em>...<em>Submitted without the submit button.</em>...'
+
+If a form is requested that does not exists, an exception will be raised.
+
+    >>> form = browser.getForm('does-not-exist')
+    Traceback (most recent call last):
+    LookupError
+
+
+Performance Testing
+-------------------
+
+Browser objects keep up with how much time each request takes.  This can be
+used to ensure a particular request's performance is within a tolerable range.
+Be very careful using raw seconds, cross-machine differences can be huge,
+pystones is usually a better choice.
+
+    >>> browser.open('http://localhost/@@/testbrowser/simple.html')
+    >>> browser.lastRequestSeconds < 10 # really big number for safety
+    True
+    >>> browser.lastRequestPystones < 10000 # really big number for safety
+    True
+
+
+Handling Errors when using Zope 3's Publisher
+---------------------------------------------
+
+A very useful feature of the publisher is the automatic graceful handling of
+application errors, such as invalid URLs:
+
+    >>> browser.open('http://localhost/invalid')
+    Traceback (most recent call last):
+    ...
+    HTTPError: HTTP Error 404: Not Found
+
+Note that the above error was thrown by ``urllib2`` and not by the
+publisher.  For debugging purposes, however, it can be very useful to see the
+original exception caused by the application.  In those cases you can set the
+``handleErrors`` property of the browser to ``False``.  It is defaulted to
+``True``:
+
+    >>> browser.handleErrors
+    True
+
+So when we tell the publisher not to handle the errors,
+
+    >>> browser.handleErrors = False
+
+we get a different, Zope internal error:
+
+    >>> browser.open('http://localhost/invalid')
+    Traceback (most recent call last):
+    ...
+    NotFound: Object: <zope.app.folder.folder.Folder object at ...>,
+              name: u'invalid'
+
+NB: Setting the handleErrors attribute to False will only change
+    anything if the http server you're testing is using Zope 3's
+    publisher or can otherwise respond appropriately to an
+    'X-zope-handle-errors' header in requests.
+
+
+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('http://localhost/@@/testbrowser/navigate.html')
+    >>> browser.getLink('Spaces in the URL').click()
+
+.goBack() Truncation
+~~~~~~~~~~~~~~~~~~~~
+
+The .goBack() method used to truncate the .contents.
+
+    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
+    >>> actual_length = len(browser.contents)
+
+    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
+    >>> browser.open('http://localhost/@@/testbrowser/simple.html')
+    >>> browser.goBack()
+    >>> len(browser.contents) == actual_length
+    True
+
 When we're done with the browser we have to close it.
 
     >>> browser.close()



More information about the Checkins mailing list