[Zope3-checkins] SVN: Zope3/branches/testbrowser-integration/src/zope/testbrowser/ Big refactoring of the testbrowser interface, particularly in regards to

Gary Poster gary at zope.com
Wed Aug 10 16:21:00 EDT 2005


Log message for revision 37848:
  Big refactoring of the testbrowser interface, particularly in regards to
  forms.
  
  Some highlights:
  
  - get controls with labels or names.  getting radio button options or checkbox
    options also works.
  
  - removed mapping interface for accessing control values, and getControl: now
    use 'get' to get the control (or radio/checkbox subcontrol, see below), and
    access control attributes as desired.
  
  - ambiguous searches for controls raises an error.  An index argument is
    available to disambiguate, as well as getting one form on the page and
    calling 'get' for just that form (if that helps the particular case).
  
  - browser.click(...) is for links only.  browser.get(...).click() is now for
    form controls.
  
  - added label support for checkboxes and radio buttons (displayValue,
    displayOptions)
  
  - added support for submitting a form without clicking any control
  
  

Changed:
  U   Zope3/branches/testbrowser-integration/src/zope/testbrowser/README.txt
  U   Zope3/branches/testbrowser-integration/src/zope/testbrowser/browser.py
  U   Zope3/branches/testbrowser-integration/src/zope/testbrowser/ftests/controls.html
  U   Zope3/branches/testbrowser-integration/src/zope/testbrowser/ftests/forms.html
  U   Zope3/branches/testbrowser-integration/src/zope/testbrowser/interfaces.py

-=-
Modified: Zope3/branches/testbrowser-integration/src/zope/testbrowser/README.txt
===================================================================
--- Zope3/branches/testbrowser-integration/src/zope/testbrowser/README.txt	2005-08-10 18:33:49 UTC (rev 37847)
+++ Zope3/branches/testbrowser-integration/src/zope/testbrowser/README.txt	2005-08-10 20:20:59 UTC (rev 37848)
@@ -142,7 +142,7 @@
     >>> browser.contents
     '...Message: <em>By Link Text</em>...'
 
-You can also find the link by (1) its URL,
+You can also find the link by its URL,
 
     >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
     >>> browser.contents
@@ -154,7 +154,7 @@
     >>> browser.contents
     '...Message: <em>By URL</em>...'
 
-or (2) its id:
+or its id:
 
     >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
     >>> browser.contents
@@ -167,32 +167,9 @@
     >>> browser.contents
     '...Message: <em>By Id</em>...'
 
-But there are more interesting cases.  You can also use the `click` method to
-submit forms.  You can either use the submit button's value by simply
-specifying the text:
-
-    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
-    >>> browser.contents
-    '...<input type="submit" name="submit-form" value="Submit" />...'
-
-    >>> browser.click('Submit')
-    >>> browser.url
-    'http://localhost/@@/testbrowser/navigate.html'
-    >>> browser.contents
-    '...Message: <em>By Form Submit</em>...'
-
-Alternatively, you can specify the name of the control:
-
-    >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
-    >>> browser.click(name='submit-form')
-    >>> browser.url
-    'http://localhost/@@/testbrowser/navigate.html'
-    >>> browser.contents
-    '...Message: <em>By Form Submit</em>...'
-
 You thought we were done here? Not so quickly.  The `click` method also
 supports image maps, though not by specifying the coordinates, but using the
-area's title (or other tag attgributes):
+area's id:
 
     >>> browser.open('http://localhost/@@/testbrowser/navigate.html')
     >>> browser.click(id='zope3')
@@ -233,107 +210,114 @@
 
     >>> browser.open('http://localhost/@@/testbrowser/controls.html')
 
+Obtaining a Control
+~~~~~~~~~~~~~~~~~~~
 
-Control Mappings
-~~~~~~~~~~~~~~~~
+You look up browser controls with the 'get' method.  The default first argument
+is 'label', and looks up the form on the basis of any associated label.
 
-You can look up a control's value from a mapping attribute:
+    >>> browser.get('Text Control')
+    <Control name='text-value' type='text'>
+    >>> browser.get(label='Text Control') # equivalent
+    <Control name='text-value' type='text'>
 
-    >>> browser.controls['text-value']
-    'Some Text'
+If you request a control that doesn't exist, the code raises a LookupError:
 
-The key is matched against the value, id and name of the control.  The
-`controls` mapping provides other functions too:
+    >>> browser.get('Does Not Exist')
+    Traceback (most recent call last):
+    ...
+    LookupError: label 'Does Not Exist'
 
-  - Asking for existence:
+If you request a control with an ambiguous lookup, the code raises an 
+AmbiguityError.
 
-      >>> 'text-value' in browser.controls
-      True
-      >>> 'foo-value' in browser.controls
-      False
+    >>> browser.get('Ambiguous Control')
+    Traceback (most recent call last):
+    ...
+    AmbiguityError: label 'Ambiguous Control'
 
-  - Getting the value with a default option:
+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.controls.get('text-value')
-      'Some Text'
-      >>> browser.controls.get('foo-value', 42)
-      42
+    >>> browser.get('Ambiguous Control', index=0)
+    <Control name='ambiguous-control-name' type='text'>
+    >>> browser.get('Ambiguous Control', index=0).value
+    'First'
+    >>> browser.get('Ambiguous Control', index=1).value
+    'Second'
 
-  - Setting an item to a new value:
+The label search uses a whitespace-normalized version of the label, and does
+a substring search, but case is honored.
 
-    >>> browser.controls['text-value'] = 'Some other Text'
-    >>> browser.controls['text-value']
-    'Some other Text'
+    >>> browser.get('Label Needs Whitespace Normalization')
+    <Control name='label-needs-normalization' type='text'>
+    >>> browser.get('label needs whitespace normalization')
+    Traceback (most recent call last):
+    ...
+    LookupError: label 'label needs whitespace normalization'
 
-  - Updating a lot of values at once:
+Multiple labels can refer to the same control (simply because that is possible
+in the HTML 4.0 spec).
 
-    >>> browser.controls['password-value']
-    'Password'
+    >>> browser.get('Multiple labels really')
+    <Control name='two-labels' type='text'>
+    >>> browser.get('really are possible')
+    <Control name='two-labels' type='text'>
+    >>> browser.get('really') # OK: ambiguous labels, but not ambiguous control
+    <Control name='two-labels' type='text'>
 
-    >>> browser.controls.update({'text-value': 'More Text',
-    ...                          'password-value': 'pass now'})
+Get also accepts two other arguments, 'name' and 'value'.  Only one of 'label',
+'name', and 'value' may be used at a time.
 
-    >>> browser.controls['text-value']
-    'More Text'
-    >>> browser.controls['password-value']
-    'pass now'
+The 'name' keyword searches form field names.
 
-If we request a control that doesn't exist, an exception is raised.
+    >>> browser.get(name='text-value')
+    <Control name='text-value' type='text'>
+    >>> browser.get(name='ambiguous-control-name')
+    Traceback (most recent call last):
+    ...
+    AmbiguityError: name 'ambiguous-control-name'
+    >>> browser.get(name='does-not-exist')
+    Traceback (most recent call last):
+    ...
+    LookupError: name 'does-not-exist'
+    >>> browser.get(name='ambiguous-control-name', index=1).value
+    'Second'
 
-    >>> browser.controls['does_not_exist']
+Combining any of 'label', and 'name' raises a ValueError, as does
+supplying none of them.
+
+    >>> browser.get(label='Ambiguous Control', name='ambiguous-control-name')
     Traceback (most recent call last):
     ...
-    KeyError: 'does_not_exist'
+    ValueError: Supply one and only one of 'label' and 'name' arguments
+    >>> browser.get()
+    Traceback (most recent call last):
+    ...
+    ValueError: Supply one and only one of 'label' and 'name' 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.
 
+    >>> browser.get(name='radio-value')
+    <ListControl name='radio-value' type='radio'>
+    >>> browser.get('Zwei')
+    <Subcontrol name='radio-value' type='radio' index=1>
+
+Characteristics of controls and subcontrols are discussed below.
+
 Control Objects
 ~~~~~~~~~~~~~~~
 
-But the value of a control is not always everything that there is to know or
-that is interesting.  In those cases, one can access the control object.  The
-string passed into the function will be matched against the value, id and name
-of the control, just as when using the control mapping.
+Controls provide IControl.
 
-    >>> ctrl = browser.getControl('text-value')
+    >>> ctrl = browser.get('Text Control')
     >>> ctrl
     <Control name='text-value' type='text'>
-    >>> ctrl = browser.getControl('text-value-id')
-    >>> ctrl
-    <Control name='text-value' type='text'>
-    >>> ctrl = browser.getControl('More Text')
-    >>> ctrl
-    <Control name='text-value' type='text'>
-
-If you want to search explicitly by name, value, and/or id, you may also use
-keyword arguments 'name', 'text', and 'id'.
-
-    >>> ctrl = browser.getControl(name='text-value')
-    >>> ctrl
-    <Control name='text-value' type='text'>
-    >>> ctrl = browser.getControl(id='text-value-id')
-    >>> ctrl
-    <Control name='text-value' type='text'>
-    >>> ctrl = browser.getControl(text='More Text')
-    >>> ctrl
-    <Control name='text-value' type='text'>
-    >>> ctrl = browser.getControl(
-    ...     id='text-value-id', name='text-value', text='More Text')
-    >>> ctrl
-    <Control name='text-value' type='text'>
-    >>> ctrl = browser.getControl(
-    ...     id='does not exist', name='does not exist', text='More Text')
-    >>> ctrl
-    <Control name='text-value' type='text'>
-
-You may not use both the default argument and any of the other named arguments.
-
-    >>> ctrl = browser.getControl('text-value', name='text-value')
-    Traceback (most recent call last):
-    ...    
-    ValueError: ...
-
-Controls provide IControl.
-
     >>> from zope.interface.verify import verifyObject
     >>> from zope.testbrowser import interfaces
     >>> verifyObject(interfaces.IControl, ctrl)
@@ -346,11 +330,13 @@
     >>> ctrl.name
     'text-value'
 
-  - the value of the control; this attribute can also be set, of course:
+  - the value of the control, which may also be set:
 
     >>> ctrl.value
+    'Some Text'
+    >>> ctrl.value = 'More Text'
+    >>> ctrl.value
     'More Text'
-    >>> ctrl.value = 'Some Text'
 
   - the type of the control:
 
@@ -362,16 +348,15 @@
     >>> ctrl.disabled
     False
 
-  - and there is a flag to tell us whether the control can have multiple
-    values:
+  - 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 three other attributes (at least in theory--see below):
+These fields have three other attributes:
 
-    >>> ctrl = browser.getControl('multi-select-value')
+    >>> ctrl = browser.get('Multiple Select Control')
     >>> ctrl
     <ListControl name='multi-select-value' type='select'>
     >>> ctrl.disabled
@@ -404,14 +389,43 @@
     >>> ctrl.value
     ['1', '2']
 
-Unfortunately, radio fields and checkbox fields do not yet implement
-displayOptions and displayValue, although we hope to support them eventually
-(i.e., basing off of label tags).
+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.
 
+Subcontrol Objects
+~~~~~~~~~~~~~~~~~~
+
+As introduced briefly above, using labels to obtain elements of a logical
+radio button or checkbox collection returns subcontrols, rather than controls.
+Manipulating the value of the subcontrols affects the parent control.
+
+    >>> browser.get(name='radio-value').value
+    ['2']
+    >>> browser.get('Zwei').value
+    True
+    >>> verifyObject(interfaces.ISubcontrol, browser.get('Zwei'))
+    True
+    >>> browser.get('Ein').value = True
+    >>> browser.get('Ein').value
+    True
+    >>> browser.get('Zwei').value
+    False
+    >>> browser.get(name='radio-value').value
+    ['1']
+    >>> browser.get('Ein').value = False
+    >>> browser.get(name='radio-value').value
+    []
+    >>> browser.get('Zwei').value = True
+
+Checkbox collections behave similarly, as shown below.
+
 Various Controls
 ~~~~~~~~~~~~~~~~
 
-There are various types of controls.  They are demonstrated here. 
+The various types of controls are demonstrated here. 
 
   - Text Control
 
@@ -419,14 +433,16 @@
 
   - Password Control
 
-    >>> ctrl = browser.getControl('password-value')
+    >>> ctrl = browser.get('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.value = 'Password'
     >>> ctrl.disabled
     False
     >>> ctrl.multiple
@@ -434,7 +450,7 @@
 
   - Hidden Control
 
-    >>> ctrl = browser.getControl('hidden-value')
+    >>> ctrl = browser.get(name='hidden-value')
     >>> ctrl
     <Control name='hidden-value' type='hidden'>
     >>> verifyObject(interfaces.IControl, ctrl)
@@ -449,7 +465,7 @@
     
   - Text Area Control
 
-    >>> ctrl = browser.getControl('textarea-value')
+    >>> ctrl = browser.get('Text Area Control')
     >>> ctrl
     <Control name='textarea-value' type='textarea'>
     >>> verifyObject(interfaces.IControl, ctrl)
@@ -464,7 +480,7 @@
 
   - File Control
 
-    >>> ctrl = browser.getControl('file-value')
+    >>> ctrl = browser.get('File Control')
     >>> ctrl
     <Control name='file-value' type='file'>
     >>> verifyObject(interfaces.IControl, ctrl)
@@ -479,7 +495,7 @@
 
   - Selection Control (Single-Valued)
 
-    >>> ctrl = browser.getControl('single-select-value')
+    >>> ctrl = browser.get('Single Select Control')
     >>> ctrl
     <ListControl name='single-select-value' type='select'>
     >>> verifyObject(interfaces.IListControl, ctrl)
@@ -509,13 +525,11 @@
 
   - Checkbox Control (Single-Valued; Unvalued)
 
-    >>> ctrl = browser.getControl('single-unvalued-checkbox-value')
+    >>> ctrl = browser.get(name='single-unvalued-checkbox-value')
     >>> ctrl
     <ListControl name='single-unvalued-checkbox-value' type='checkbox'>
-    >>> interfaces.IListControl.providedBy(ctrl)
+    >>> verifyObject(interfaces.IListControl, ctrl)
     True
-    >>> verifyObject(interfaces.IControl, ctrl) # IListControl when implemented
-    True
     >>> ctrl.value
     True
     >>> ctrl.value = False
@@ -525,28 +539,33 @@
     True
     >>> ctrl.options
     [True]
-    >>> ctrl.displayOptions # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
-    >>> ctrl.displayValue # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
-    >>> ctrl.displayValue = ['One'] # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
+    >>> ctrl.displayOptions
+    ['Single Unvalued Checkbox']
+    >>> ctrl.displayValue
+    []
+    >>> verifyObject(
+    ...     interfaces.ISubcontrol, browser.get('Single Unvalued Checkbox'))
+    True
+    >>> browser.get('Single Unvalued Checkbox').value
+    False
+    >>> ctrl.displayValue = ['Single Unvalued Checkbox']
+    >>> ctrl.displayValue
+    ['Single Unvalued Checkbox']
+    >>> browser.get('Single Unvalued Checkbox').value
+    True
+    >>> browser.get('Single Unvalued Checkbox').value = False
+    >>> browser.get('Single Unvalued Checkbox').value
+    False
+    >>> ctrl.displayValue
+    []
 
   - Checkbox Control (Single-Valued, Valued)
 
-    >>> ctrl = browser.getControl('single-valued-checkbox-value')
+    >>> ctrl = browser.get(name='single-valued-checkbox-value')
     >>> ctrl
     <ListControl name='single-valued-checkbox-value' type='checkbox'>
-    >>> interfaces.IListControl.providedBy(ctrl)
+    >>> verifyObject(interfaces.IListControl, ctrl)
     True
-    >>> verifyObject(interfaces.IControl, ctrl) # IListControl when implemented
-    True
     >>> ctrl.value
     ['1']
     >>> ctrl.value = []
@@ -556,28 +575,33 @@
     True
     >>> ctrl.options
     ['1']
-    >>> ctrl.displayOptions # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
-    >>> ctrl.displayValue # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
-    >>> ctrl.displayValue = ['One'] # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
+    >>> ctrl.displayOptions
+    ['Single Valued Checkbox']
+    >>> ctrl.displayValue
+    []
+    >>> verifyObject(
+    ...     interfaces.ISubcontrol, browser.get('Single Valued Checkbox'))
+    True
+    >>> browser.get('Single Valued Checkbox').value
+    False
+    >>> ctrl.displayValue = ['Single Valued Checkbox']
+    >>> ctrl.displayValue
+    ['Single Valued Checkbox']
+    >>> browser.get('Single Valued Checkbox').value
+    True
+    >>> browser.get('Single Valued Checkbox').value = False
+    >>> browser.get('Single Valued Checkbox').value
+    False
+    >>> ctrl.displayValue
+    []
 
   - Checkbox Control (Multi-Valued)
 
-    >>> ctrl = browser.getControl('multi-checkbox-value')
+    >>> ctrl = browser.get(name='multi-checkbox-value')
     >>> ctrl
     <ListControl name='multi-checkbox-value' type='checkbox'>
-    >>> interfaces.IListControl.providedBy(ctrl)
+    >>> verifyObject(interfaces.IListControl, ctrl)
     True
-    >>> verifyObject(interfaces.IControl, ctrl) # IListControl when implemented
-    True
     >>> ctrl.value
     ['1', '3']
     >>> ctrl.value = ['1', '2']
@@ -587,28 +611,38 @@
     True
     >>> ctrl.options
     ['1', '2', '3']
-    >>> ctrl.displayOptions # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
-    >>> ctrl.displayValue # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
-    >>> ctrl.displayValue = ['One'] # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
+    >>> ctrl.displayOptions
+    ['One', 'Two', 'Three']
+    >>> ctrl.displayValue
+    ['One', 'Two']
+    >>> ctrl.displayValue = ['Two']
+    >>> ctrl.value
+    ['2']
+    >>> browser.get('Two').value
+    True
+    >>> verifyObject(interfaces.ISubcontrol, browser.get('Two'))
+    True
+    >>> browser.get('Three').value = True
+    >>> browser.get('Three').value
+    True
+    >>> browser.get('Two').value
+    True
+    >>> ctrl.value
+    ['2', '3']
+    >>> browser.get('Two').value = False
+    >>> ctrl.value
+    ['3']
+    >>> browser.get('Three').value = False
+    >>> ctrl.value
+    []
 
   - Radio Control
 
-    >>> ctrl = browser.getControl('radio-value')
+    >>> ctrl = browser.get(name='radio-value')
     >>> ctrl
     <ListControl name='radio-value' type='radio'>
-    >>> interfaces.IListControl.providedBy(ctrl)
+    >>> verifyObject(interfaces.IListControl, ctrl)
     True
-    >>> verifyObject(interfaces.IControl, ctrl) # IListControl when implemented
-    True
     >>> ctrl.value
     ['2']
     >>> ctrl.value = []
@@ -620,25 +654,22 @@
     False
     >>> ctrl.options
     ['1', '2', '3']
-    >>> ctrl.displayOptions # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
-    >>> ctrl.displayValue # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
-    >>> ctrl.displayValue = ['One'] # we wish this would work!
-    Traceback (most recent call last):
-    ...    
-    NotImplementedError: ...
+    >>> ctrl.displayOptions
+    ['Ein', 'Zwei', 'Drei']
+    >>> ctrl.displayValue
+    []
+    >>> ctrl.displayValue = ['Ein']
+    >>> ctrl.displayValue
+    ['Ein']
 
+  The radio control subcontrols were illustrated above.
+
   - Image Control
 
-    >>> ctrl = browser.getControl('image-value')
+    >>> ctrl = browser.get(name='image-value')
     >>> ctrl
-    <Control name='image-value' type='image'>
-    >>> verifyObject(interfaces.IControl, ctrl)
+    <ImageControl name='image-value' type='image'>
+    >>> verifyObject(interfaces.IImageSubmitControl, ctrl)
     True
     >>> ctrl.value
     ''
@@ -649,68 +680,74 @@
 
   - Submit Control
 
-    >>> ctrl = browser.getControl('submit-value')
+    >>> ctrl = browser.get(name='submit-value')
     >>> ctrl
-    <Control name='submit-value' type='submit'>
-    >>> verifyObject(interfaces.IControl, ctrl)
+    <SubmitControl name='submit-value' type='submit'>
+    >>> browser.get('Submit This') # value of submit button is a label
+    <SubmitControl name='submit-value' type='submit'>
+    >>> browser.get('Standard Submit Control') # label tag is legal
+    <SubmitControl name='submit-value' type='submit'>
+    >>> browser.get('Submit') # multiple labels, but control is not ambiguous
+    <SubmitControl name='submit-value' type='submit'>
+    >>> verifyObject(interfaces.ISubmitControl, ctrl)
     True
     >>> ctrl.value
-    'Submit'
+    '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.controls['text-value'] = 'Other Text'
-    >>> browser.click('Submit')
+    >>> browser.get('Text Control').value = 'Other Text'
+    >>> browser.get('Submit').click()
     >>> print browser.contents
     <html>
     ...
     <em>Other Text</em>
-    <input type="text" name="text-value" id="text-value-id" value="Some Text" />
+    <input type="text" name="text-value" id="text-value" value="Some Text" />
     ...
-    <em>Submit</em>
-    <input type="submit" name="submit-value" value="Submit" />
+    <em>Submit This</em>
+    <input type="submit" name="submit-value" id="submit-value" value="Submit This" />
     ...
     </html>
 
 And also with the image value:
 
     >>> browser.open('http://localhost/@@/testbrowser/controls.html')
-    >>> browser.controls['text-value'] = 'Other Text'
-    >>> browser.click(name='image-value')
+    >>> browser.get('Text Control').value = 'Other Text'
+    >>> browser.get(name='image-value').click()
     >>> print browser.contents
     <html>
     ...
     <em>Other Text</em>
-    <input type="text" name="text-value" id="text-value-id" value="Some Text" />
+    <input type="text" name="text-value" id="text-value" value="Some Text" />
     ...
     <em>1</em>
     <em>1</em>
-    <input type="image" name="image-value" src="zope3logo.gif" />
+    <input type="image" name="image-value" id="image-value"
+           src="zope3logo.gif" />
     ...
     </html>
 
 But when sending an image, you can also specify the coordinate you clicked:
 
     >>> browser.open('http://localhost/@@/testbrowser/controls.html')
-    >>> browser.click(name='image-value', coord=(50,25))
+    >>> browser.get(name='image-value').click((50,25))
     >>> print browser.contents
     <html>
     ...
     <em>50</em>
     <em>25</em>
-    <input type="image" name="image-value" src="zope3logo.gif" />
+    <input type="image" name="image-value" id="image-value"
+           src="zope3logo.gif" />
     ...
     </html>
 
-
 Forms
 -----
 
@@ -749,20 +786,13 @@
     >>> form.enctype
     'multipart/form-data'
 
-  - The controls for this specific form are also available:
-
-    >>> form.controls
-    <zope.testbrowser.browser.ControlsMapping object at ...>
-    >>> form.controls['text-value']
-    'First Text'
-
 Besides those attributes, you have also a couple of methods.  Like for the
-browser, you can get control objects
+browser, you can get control objects, but limited to the current form...
 
-    >>> form.getControl('text-value')
+    >>> form.get(name='text-value')
     <Control name='text-value' type='text'>
 
-and submit the form:
+...and submit the form.
 
     >>> form.submit('Submit')
     >>> print browser.contents
@@ -772,30 +802,39 @@
     ...
     </html>
 
-Okay, that's it about forms.  Now let me show you briefly that looking up forms
-is sometimes important.  In the `forms.html` template, we have three forms all
-having a text control named `text-value`.  Now, if I use the browser's
-`controls` attribute and `click` method,
+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.
 
-    >>> browser.controls['text-value']
-    'First Text'
-    >>> browser.click('Submit')
-    >>> print browser.contents
-    <html>
+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.get(name='text-value')
+    Traceback (most recent call last):
     ...
-    <em>First Text</em>
+    AmbiguityError: name 'text-value'
+    >>> browser.get('Text Control')
+    Traceback (most recent call last):
     ...
-    </html>
+    AmbiguityError: label 'Text Control'
 
-I can every only get to the first form, making the others unreachable.  But
-with the `forms` mapping I can get to the second and third form as well:
+I'll always get an ambiguous form field.  I can use the index argument, or
+with the `forms` mapping I can disambiguate by searching only within a given
+form:
 
     >>> form = browser.forms['2']
-    >>> form.controls['text-value']
+    >>> form.get(name='text-value').value
     'Second Text'
     >>> form.submit('Submit')
     >>> browser.contents
     '...<em>Second Text</em>...'
+    >>> form = browser.forms['2']
+    >>> form.get('Submit').click()
+    >>> browser.contents
+    '...<em>Second Text</em>...'
+    >>> browser.forms['3'].get('Text Control').value
+    'Third Text'
 
 The `forms` mapping also supports the check for containment
 
@@ -809,7 +848,27 @@
     >>> browser.forms.get('invalid', 42)
     42
 
+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 values attribute that guarantees
+order.  (Forms without submit buttons are sometimes useful for JavaScript.)
 
+    >>> form = browser.forms.values()[3]
+    >>> form.submit()
+    >>> browser.contents
+    '...<em>Fourth Text</em>...'
+
+Other mapping attributes for the forms collection remain unimplemented.
+If useful, contributors implementing these would be welcome.
+
+    >>> browser.forms.items()
+    Traceback (most recent call last):
+    ...
+    AttributeError: 'FormsMapping' object has no attribute 'items'
+    >>> browser.forms.keys()
+    Traceback (most recent call last):
+    ...
+    AttributeError: 'FormsMapping' object has no attribute 'keys'
+
 Handling Errors
 ---------------
 

Modified: Zope3/branches/testbrowser-integration/src/zope/testbrowser/browser.py
===================================================================
--- Zope3/branches/testbrowser-integration/src/zope/testbrowser/browser.py	2005-08-10 18:33:49 UTC (rev 37847)
+++ Zope3/branches/testbrowser-integration/src/zope/testbrowser/browser.py	2005-08-10 20:20:59 UTC (rev 37848)
@@ -17,7 +17,9 @@
 """
 __docformat__ = "reStructuredText"
 import re
+import StringIO
 import mechanize
+import pullparser
 import zope.interface
 
 from zope.testbrowser import interfaces
@@ -52,11 +54,6 @@
         return self.mech_browser.title()
 
     @property
-    def controls(self):
-        """See zope.testbrowser.interfaces.IBrowser"""
-        return ControlsMapping(self)
-
-    @property
     def forms(self):
         """See zope.testbrowser.interfaces.IBrowser"""
         return FormsMapping(self)
@@ -117,21 +114,8 @@
         """See zope.testbrowser.interfaces.IBrowser"""
         self.mech_browser.addheaders.append( (key, value) )
 
-    def click(self, text=None, url=None, id=None, name=None, coord=(1,1)):
+    def click(self, text=None, url=None, id=None):
         """See zope.testbrowser.interfaces.IBrowser"""
-        # Determine whether the click is a form submit and click the submit
-        # button if this is the case.
-        form, control = self._findControl(text, id, name, type='submit')
-        if control is None:
-            form, control = self._findControl(text, id, name, type='image')
-        if control is not None:
-            self._clickSubmit(form, control, coord)
-            self._changed()
-            return
-
-        # If we get here, we didn't find a control to click, so we'll look for
-        # a regular link.
-
         if id is not None:
             def predicate(link):
                 return dict(link.attrs).get('id') == id
@@ -155,32 +139,69 @@
                 text_regex=text_regex, url_regex=url_regex)
         self._changed()
 
-    def getControl(self, search=None, text=None, id=None, name=None):
+    def _findByLabel(self, label, form=None, include_subcontrols=False):
+        # form is None or a mech_form
+        ids = [id for id, l in self._label_tags if label in l]
+        found = []
+        for f in self.mech_browser.forms():
+            if form is None or form == f:
+                for control in f.controls:
+                    if control.type in ('radio', 'checkbox'):
+                        if include_subcontrols:
+                            for ix, attrs in enumerate(control._attrs_list):
+                                sub_id = attrs.get('id')
+                                if sub_id is not None and sub_id in ids:
+                                    found.append(((control, ix), f))
+                    elif (control.id in ids or (
+                        control.type in ('button', 'submit') and
+                        label in str(control.value))):
+                        # the str(control.value) is a hack to get
+                        # string-in-string behavior when the value is a list.
+                        # maybe should be revisited.
+                        found.append((control, f))
+        return found
+
+    def _findByName(self, name, form=None):
+        found = []
+        for f in self.mech_browser.forms():
+            if form is None or form == f:
+                for control in f.controls:
+                    if control.name==name:
+                        found.append((control, f))
+        return found
+
+    def get(self, label=None, name=None, index=None):
         """See zope.testbrowser.interfaces.IBrowser"""
-        if search is not None:
-            if text is not None or id is not None or name is not None:
-                raise ValueError(
-                    'May not pass both search value and any of '
-                    'text, id, or name')
-            text = id = name = search
-        form, control = self._findControl(text, id, name)
-        if control is None:
+        intermediate, msg = self._get_all(
+            label, name, include_subcontrols=True)
+        control, form = self._disambiguate(intermediate, msg, index)
+        return controlFactory(control, form, self)
+
+    def _get_all(self, label, name, form=None, include_subcontrols=False):
+        if not ((label is not None) ^ (name is not None)):
             raise ValueError(
-                'could not locate control: text %r, id %r, name %r' %
-                (text, id, name))
-        return controlFactory(control)
+                "Supply one and only one of 'label' and 'name' arguments")
+        if label is not None:
+            res = self._findByLabel(label, form, include_subcontrols)
+            msg = 'label %r' % label
+        elif name is not None:
+            res = self._findByName(name, form)
+            msg = 'name %r' % name
+        return res, msg
 
-    def _findControl(self, text, id, name, type=None, form=None):
-        for control_form, control in self._controls:
-            if form is None or control_form == form:
-                if (((id is not None and control.id == id)
-                or (name is not None and control.name == name)
-                or (text is not None and text in str(control.value))
-                ) and (type is None or control.type == type)):
-                    self.mech_browser.form = control_form
-                    return control_form, control
-    
-        return None, None
+    def _disambiguate(self, intermediate, msg, index):
+        if intermediate:
+            if index is None:
+                if len(intermediate) > 1:
+                    raise interfaces.AmbiguityError(msg)
+                else:
+                    return intermediate[0]
+            else:
+                try:
+                    return intermediate[index]
+                except KeyError:
+                    msg = '%s index %d' % (msg, index)
+        raise LookupError(msg)
         
     def _findForm(self, id, name, action):
         for form in self.mech_browser.forms():
@@ -196,26 +217,50 @@
         self.mech_browser.open(form.click(
                     id=control.id, name=control.name, coord=coord))
 
-    __controls = None
+    # I'd like a different solution for the caching.  Later.
+
     @property
-    def _controls(self):
-        if self.__controls is None:
-            self.__controls = []
-            for form in self.mech_browser.forms():
-                for control in form.controls:
-                    self.__controls.append( (form, control) )
-        return self.__controls
+    def _label_tags(self): # [(id, label)]
+        cache = []
+        p = pullparser.PullParser(StringIO.StringIO(self.contents))
+        for token in p.tags('label'):
+            if token.type=='starttag':
+                cache.append((dict(token.attrs).get('for'),
+                             p.get_compressed_text(
+                                endat=("endtag", "label"))))
+        self.__dict__['_label_tags'] = cache
+        return cache
 
+    @property
+    def _label_tags_mapping(self):
+        cache = {}
+        for i, l in self._label_tags:
+            found = cache.get(i)
+            if found is None:
+                found = cache[i] = []
+            found.append(l)
+        self.__dict__['_label_tags_mapping'] = cache
+        return cache
+
     def _changed(self):
-        self.__controls = None
+        try:
+            del self.__dict__['_label_tags']
+            del self.__dict__['_label_tags_mapping'] # this depends on
+            # _label_tags, so combining them in the same block should be fine,
+            # as long as _label_tags is first.
+        except KeyError:
+            pass
+        
 
 
 class Control(object):
     """A control of a form."""
     zope.interface.implements(interfaces.IControl)
 
-    def __init__(self, control):
+    def __init__(self, control, form, browser):
         self.mech_control = control
+        self.mech_form = form
+        self.browser = browser
 
         # for some reason ClientForm thinks we shouldn't be able to modify
         # hidden fields, but while testing it is sometimes very important
@@ -265,6 +310,24 @@
         return "<%s name=%r type=%r>" % (
             self.__class__.__name__, self.name, self.type)
 
+def _getLabel(attr, mapping):
+    label = None
+    attr_id = attr.get('id')
+    if attr_id is not None:
+        labels = mapping.get(attr_id, ())
+        for label in labels:
+            if label: # get the first one with text
+                break
+    return label
+
+def _isSelected(mech_control, ix):
+    if mech_control.type == 'radio':
+        # we don't have precise ordering, so we have to guess
+        attr = mech_control._attrs_list[ix]
+        return attr.get('value', 'on') == mech_control._selected
+    else:
+        return mech_control._selected[ix]
+
 class ListControl(Control):
     zope.interface.implements(interfaces.IListControl)
 
@@ -276,34 +339,176 @@
         # attribute error for all others.
 
         def fget(self):
-            return self.mech_control.get_value_by_label()
+            try:
+                return self.mech_control.get_value_by_label()
+            except NotImplementedError:
+                mapping = self.browser._label_tags_mapping
+                res = []
+                for ix in range(len(self.mech_control.possible_items())):
+                    if _isSelected(self.mech_control, ix):
+                        attr = self.mech_control._attrs_list[ix]
+                        res.append(_getLabel(attr, mapping))
+                        if self.mech_control.type == 'radio':
+                            return res
+                            # this is not simply an optimization,
+                            # unfortunately.  We don't have easy access to
+                            # the precise index of the selected radio button,
+                            # but merely the current value.  Therefore, if
+                            # two or more radio buttons of the same name
+                            # have the same value, we can't easily tell which
+                            # is actually checked.  Rather than returning
+                            # all of them, which would arguably be confusing,
+                            # we return the first.
+                return res
+                    
 
         def fset(self, value):
-            self.mech_control.set_value_by_label(value)
+            try:
+                self.mech_control.set_value_by_label(value)
+            except NotImplementedError:
+                mapping = self.browser._label_tags_mapping
+                res = []
+                for v in value:
+                    found = []
+                    for attr in self.mech_control._attrs_list:
+                        attr_value = attr.get('value', 'on')
+                        if attr_value not in found:
+                            attr_id = attr.get('id')
+                            if attr_id is not None:
+                                labels = mapping.get(attr_id, ())
+                                for l in labels:
+                                    if v in l:
+                                        found.append(attr_value)
+                                        break
+                    if not found:
+                        raise LookupError(v)
+                    elif len(found) > 1:
+                        raise interfaces.AmbiguityError(v)
+                    res.extend(found)
+                self.value = res
 
         return property(fget, fset)
 
     @property
     def displayOptions(self):
         """See zope.testbrowser.interfaces.IListControl"""
-        # not implemented for anything other than select;
-        # would be nice if ClientForm implemented for checkbox and radio.
-        # attribute error for all others.
-        return self.mech_control.possible_items(by_label=True)
+        try:
+            return self.mech_control.possible_items(by_label=True)
+        except NotImplementedError:
+            mapping = self.browser._label_tags_mapping
+            res = []
+            for attr in self.mech_control._attrs_list:
+                res.append(_getLabel(attr, mapping))
+            return res
 
     @property
     def options(self):
         """See zope.testbrowser.interfaces.IListControl"""
         if (self.type == 'checkbox'
-        and self.mech_control.possible_items() == ['on']):
+            and self.mech_control.possible_items() == ['on']):
             return [True]
         return self.mech_control.possible_items()
 
-def controlFactory(control):
-    if control.type in ('checkbox', 'select', 'radio'):
-        return ListControl(control)
+    #@property
+    #def subcontrols(self):
+        # XXX
+
+class SubmitControl(Control):
+    zope.interface.implements(interfaces.ISubmitControl)
+
+    def click(self):
+        self.browser._clickSubmit(self.mech_form, self.mech_control, (1,1))
+        self.browser._changed()
+
+class ImageControl(Control):
+    zope.interface.implements(interfaces.IImageSubmitControl)
+
+    def click(self, coord=(1,1)):
+        self.browser._clickSubmit(self.mech_form, self.mech_control, coord)
+        self.browser._changed()
+
+class Subcontrol(object):
+    zope.interface.implements(interfaces.ISubcontrol)
+
+    def __init__(self, control, index, form, browser):
+        self.mech_control = control
+        self.index = index
+        self.mech_form = form
+        self.browser = browser
+
+    @property
+    def control(self):
+        res = controlFactory(self.mech_control, self.mech_form, self.browser)
+        self.__dict__['control'] = res
+        return res
+
+    @property
+    def disabled(self):
+        return bool(self.mech_control._attrs_list[self.index].get('disabled'))
+
+    @apply
+    def value():
+        """See zope.testbrowser.interfaces.IControl"""
+
+        def fget(self):
+            # if a set of radio buttons of the same name have choices
+            # that are the same value, and a radio button is selected for
+            # one of the identical values, radio buttons will always return 
+            # True simply on the basis of whether their value is equal to
+            # the control's current value.  An arguably pathological case.
+            return _isSelected(self.mech_control, self.index)
+
+        def fset(self, value):
+            # if a set of checkboxes of the same name have choices
+            # that are the same value, and a checkbox is selected for
+            # one of the identical values, the first checkbox will be the one
+            # changed in all cases.  An arguably pathological case.
+            if not self.disabled: # TODO is readonly an option?
+                attrs = self.mech_control._attrs_list[self.index]
+                option_value = attrs.get('value', 'on')
+                current = self.mech_control.value
+                if value:
+                    if option_value not in current:
+                        if self.mech_control.multiple:
+                            current.append(option_value)
+                        else:
+                            current = [option_value]
+                        self.mech_control.value = current
+                else:
+                    try:
+                        current.remove(option_value)
+                    except ValueError:
+                        pass
+                    else:
+                        self.mech_control.value = current
+            else:
+                raise AttributeError("control %r, index %d, is disabled" %
+                                     (self.mech_control.name, self.index))
+        return property(fget, fset)
+
+    #def click(self):
+        # XXX
+
+    def __repr__(self):
+        return "<%s name=%r type=%r index=%d>" % (
+            self.__class__.__name__, self.mech_control.name,
+            self.mech_control.type, self.index)
+
+def controlFactory(control, form, browser):
+    if isinstance(control, tuple):
+        # it is a subcontrol
+        control, index = control
+        return Subcontrol(control, index, form, browser)
     else:
-        return Control(control)
+        t = control.type
+        if t in ('checkbox', 'select', 'radio'):
+            return ListControl(control, form, browser)
+        elif t=='submit':
+            return SubmitControl(control, form, browser)
+        elif t=='image':
+            return ImageControl(control, form, browser)
+        else:
+            return Control(control, form, browser)
 
 class FormsMapping(object):
     """All forms on the page of the browser."""
@@ -330,60 +535,23 @@
         """See zope.interface.common.mapping.IReadMapping"""
         return self.browser._findForm(key, key, None) is not None
 
+    def values(self):
+        return [Form(self.browser, form) for form in
+                self.browser.mech_browser.forms()]
 
-class ControlsMapping(object):
-    """A mapping of all controls in a form or a page."""
-    zope.interface.implements(interfaces.IControlsMapping)
 
-    def __init__(self, browser, form=None):
-        """Initialize the ControlsMapping
-        
+class Form(object):
+    """HTML Form"""
+    zope.interface.implements(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
-
-    def __getitem__(self, key):
-        """See zope.testbrowser.interfaces.IControlsMapping"""
-        form, control = self.browser._findControl(key, key, key,
-                                                  form=self.mech_form)
-        if control is None:
-            raise KeyError(key)
-        return controlFactory(control).value
-
-    def get(self, key, default=None):
-        """See zope.interface.common.mapping.IReadMapping"""
-        try:
-            return self[key]
-        except KeyError:
-            return default
-
-    def __contains__(self, item):
-        """See zope.testbrowser.interfaces.IControlsMapping"""
-        try:
-            self[item]
-        except KeyError:
-            return False
-        else:
-            return True
-
-    def __setitem__(self, key, value):
-        """See zope.testbrowser.interfaces.IControlsMapping"""
-        form, control = self.browser._findControl(key, key, key)
-        if control is None:
-            raise KeyError(key)
-        controlFactory(control).value = value
-
-    def update(self, mapping):
-        """See zope.testbrowser.interfaces.IControlsMapping"""
-        for k, v in mapping.items():
-            self[k] = v
-
-
-class Form(ControlsMapping):
-    """HTML Form"""
-    zope.interface.implements(interfaces.IForm)
     
     def __getattr__(self, name):
         # See zope.testbrowser.interfaces.IForm
@@ -398,35 +566,28 @@
         """See zope.testbrowser.interfaces.IForm"""
         return self.mech_form.attrs.get('id')
 
-    @property
-    def controls(self):
+    def submit(self, label=None, name=None, index=None, coord=(1,1)):
         """See zope.testbrowser.interfaces.IForm"""
-        return ControlsMapping(browser=self.browser, form=self.mech_form)
-
-    def submit(self, text=None, id=None, name=None, coord=(1,1)):
-        """See zope.testbrowser.interfaces.IForm"""
-        form, control = self.browser._findControl(
-            text, id, name, type='submit', form=self.mech_form)
-
-        if control is None:
-            form, control = self.browser._findControl(
-                text, id, name, type='image', form=self.mech_form)
-
-        if control is not None:
+        form = self.mech_form
+        if label is not None or name is not None:
+            intermediate, msg = self.browser._get_all(label, name, form)
+            intermediate = [
+                (control, form) for (control, form) in intermediate if
+                control.type in ('submit', 'image')]
+            control, form = self.browser._disambiguate(
+                intermediate, msg, index)
             self.browser._clickSubmit(form, control, coord)
-            self.browser._changed()
+        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.click()
+            self.browser.mech_browser.open(request)
+        self.browser._changed()
 
-
-    def getControl(self, search=None, text=None, id=None, name=None):
-        """See zope.testbrowser.interfaces.IForm"""
-        if search is not None:
-            if text is not None or id is not None or name is not None:
-                raise ValueError(
-                    'May not pass both search value and any of '
-                    'text, id, or name')
-            text = id = name = search
-        form, control = self.browser._findControl(text, id, name,
-                                                  form=self.mech_form)
-        if control is None:
-            raise ValueError('could not locate control: ' + text)
-        return controlFactory(control)
+    def get(self, label=None, name=None, index=None):
+        """See zope.testbrowser.interfaces.IBrowser"""
+        intermediate, msg = self.browser._get_all(
+            label, name, self.mech_form, include_subcontrols=True)
+        control, form = self.browser._disambiguate(intermediate, msg, index)
+        return controlFactory(control, form, self.browser)

Modified: Zope3/branches/testbrowser-integration/src/zope/testbrowser/ftests/controls.html
===================================================================
--- Zope3/branches/testbrowser-integration/src/zope/testbrowser/ftests/controls.html	2005-08-10 18:33:49 UTC (rev 37847)
+++ Zope3/branches/testbrowser-integration/src/zope/testbrowser/ftests/controls.html	2005-08-10 20:20:59 UTC (rev 37848)
@@ -6,44 +6,51 @@
     <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-id'
+        <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" value="Password" />
+        <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" value="Hidden" />
-        (hidden)
+        <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">
+        <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" />
+        <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">
+        <select name="single-select-value" id="single-select-value">
           <option value="1">One</option>
           <option value="2">Two</option>
           <option value="3">Three</option>
@@ -51,9 +58,11 @@
       </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" multiple="multiple">
+        <select name="multi-select-value" id="multi-select-value"
+                multiple="multiple">
           <option value="1">One</option>
           <option value="2">Two</option>
           <option value="3">Three</option>
@@ -65,7 +74,7 @@
             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</label>
+        <label for="single-unvalued-checkbox">Single Unvalued Checkbox</label>
       </div>
 
       <div>
@@ -73,10 +82,11 @@
             tal:content="request/single-valued-checkbox-value"></em>
         <input type="checkbox" name="single-valued-checkbox-value" 
                id="single-valued-checkbox" value="1" checked="checked" />
-        <label for="single-valued-checkbox">Single Valued</label>
+        <label for="single-valued-checkbox">Single Valued Checkbox</label>
       </div>
 
       <div>
+        (Multi checkbox: values are labels)
         <em tal:condition="request/multi-checkbox-value|nothing"
             tal:content="request/multi-checkbox-value"></em>
         <input type="checkbox" name="multi-checkbox-value" value="1" 
@@ -91,30 +101,64 @@
       </div>
 
       <div>
+        (Radio: values are labels)
         <em tal:condition="request/radio-value|nothing"
             tal:content="request/radio-value"></em>
         <input type="radio" name="radio-value" id="radio-value-1" value="1" />
-        <label for="radio-value-1">One</label>
+        <label for="radio-value-1">Ein</label>
         <input type="radio" name="radio-value" id="radio-value-2" value="2"
                checked="checked" />
-        <label for="radio-value-2">Two</label>
+        <label for="radio-value-2">Zwei</label>
         <input type="radio" name="radio-value" id="radio-value-3" value="3" />
-        <label for="radio-value-3">Three</label>
+        <label for="radio-value-3">Drei</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" src="zope3logo.gif" />
+        <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" value="Submit" />
+        <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="two-labels">Multiple labels really</label>
+        <label for="two-labels">really are possible</label>
+        <input type="text" name="two-labels"
+               id="two-labels" />
+      </div>
+
+
     </form>
 
   </body>

Modified: Zope3/branches/testbrowser-integration/src/zope/testbrowser/ftests/forms.html
===================================================================
--- Zope3/branches/testbrowser-integration/src/zope/testbrowser/ftests/forms.html	2005-08-10 18:33:49 UTC (rev 37847)
+++ Zope3/branches/testbrowser-integration/src/zope/testbrowser/ftests/forms.html	2005-08-10 20:20:59 UTC (rev 37848)
@@ -19,9 +19,17 @@
     </form>
 
     <form id="3" name="three" action="forms.html">
-      <input type="text" name="text-value" value="Third Text" />
+      <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">
+      <label for="text-value-4">Text Control</label>
+      <input type="text" name="text-value" id="text-value-4"
+             value="Fourth Text" />
+    </form>
+
   </body>
-</html>
\ No newline at end of file
+</html>

Modified: Zope3/branches/testbrowser-integration/src/zope/testbrowser/interfaces.py
===================================================================
--- Zope3/branches/testbrowser-integration/src/zope/testbrowser/interfaces.py	2005-08-10 18:33:49 UTC (rev 37847)
+++ Zope3/branches/testbrowser-integration/src/zope/testbrowser/interfaces.py	2005-08-10 20:20:59 UTC (rev 37848)
@@ -20,21 +20,9 @@
 import zope.interface
 import zope.schema
 
-class IControlsMapping(zope.interface.common.mapping.IReadMapping):
-    """A mapping of all controls of a page.
+class AmbiguityError(Exception):
+    pass
 
-    If there are multiple controls matching the key, the first one is
-    picked. This can often happen when multiple forms are used on one page. In
-    those cases use the ``IFormMapping`` and ``IForm`` interfaces instead. 
-    """
-    
-    def __setitem__(key, value):
-        """Set the value of a control."""
-
-    def update(mapping):
-        """Update several control values at once."""
-
-
 class IControl(zope.interface.Interface):
     """A control (input field) of a page."""
 
@@ -94,6 +82,37 @@
         default=None,
         required=True)
 
+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 ISubcontrol(zope.interface.Interface):
+    """a radio button or checkbox within a larger multiple-choice control"""
+
+    control = zope.schema.Object(
+        title=u"Control",
+        description=(u"The parent control element."),
+        schema=IControl,
+        required=True)
+        
+    disabled = zope.schema.Bool(
+        title=u"Disabled",
+        description=u"Describes whether a subcontrol is disabled.",
+        default=False,
+        required=False)
+
+    value = zope.schema.Bool(
+        title=u"Value",
+        description=u"Whether the subcontrol is selected",
+        default=None,
+        required=True)
+
 class IFormsMapping(zope.interface.common.mapping.IReadMapping):
     """A mapping of all forms in a page."""
 
@@ -129,22 +148,29 @@
                     u"if specified.",
         required=True)
 
-    controls = zope.schema.Object(
-        title=u"Controls",
-        description=(u"A mapping of control elements of the form. The key is "
-                     u"actually quiet flexible and searches the text, id, and "
-                     u"name of the control."),
-        schema=IControlsMapping,
-        required=True)
+    def get(label=None, name=None, index=None):
+        """Get a control in the page.
 
-    def getControl(self, text):
-        """Get a control of the form."""
+        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.
 
-    def submit(text=None, id=None, name=None, coord=(1,1)):
+        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 `text`, `id`, and `name` arguments select the submit button to use
-        to submit the form.
+        to submit the form.  You may use zero or one of them.
+
+        The control code works identically to 'get' except that searches are
+        filtered to find only submit and image controls.
         """
     
 
@@ -176,14 +202,6 @@
         description=u"Title of the displayed page",
         required=False)
 
-    controls = zope.schema.Object(
-        title=u"Controls",
-        description=(u"A mapping of control elements on the page. The key is "
-                     u"actually quiet flexible and searches the text, id, and "
-                     u"name of the control."),
-        schema=IControlsMapping,
-        required=True)
-
     forms = zope.schema.Object(
         title=u"Forms",
         description=(u"A mapping of form elements on the page. The key is "
@@ -220,7 +238,10 @@
         """
 
     def reload():
-        """Reload the current page."""
+        """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.
@@ -229,7 +250,7 @@
         default.
         """
 
-    def click(text=None, url=None, id=None, name=None, coord=(1,1)):
+    def click(text=None, url=None, id=None):
         """Click on a link in the page.
 
         This method opens a new URL that is behind the link. The link itself
@@ -243,23 +264,19 @@
             ``href`` attribute of an anchor tag or the action of a form.
 
           o ``id`` -- The id attribute of the anchor tag submit button.
-
-          o ``name`` -- The name attribute of the anchor tag submit button.
-
-          o ``coord`` -- This is a 2-tuple that describes the coordinates of
-            the mouse cursor on an image map when the mouse button is clicked.
         """
 
-    def getControl(text, type=None, form=None):
+    def get(label=None, name=None, index=None):
         """Get a control in the page.
 
-        This method returns a control object for a particular input field in
-        the page. By default all forms are searched and the first successful
-        hit is returned.
+        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.
 
-        When the ``type`` is specified, only input fields of that type will be
-        searched.
+        If no values are found, the code raises a LookupError.
 
-        The ``form`` specifies a specific form on the page that is searched
-        for a control. The argument must be a form of the ``forms`` mapping.
+        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.
         """



More information about the Zope3-Checkins mailing list