[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/testing/testbrowser/ add the package formerly known as zc.mechtest

Benji York benji at zope.com
Tue Jul 26 22:58:20 EDT 2005


Log message for revision 37446:
  add the package formerly known as zc.mechtest
  

Changed:
  A   Zope3/trunk/src/zope/app/testing/testbrowser/
  A   Zope3/trunk/src/zope/app/testing/testbrowser/README.txt
  A   Zope3/trunk/src/zope/app/testing/testbrowser/__init__.py
  A   Zope3/trunk/src/zope/app/testing/testbrowser/browser.py
  A   Zope3/trunk/src/zope/app/testing/testbrowser/ftests.py
  A   Zope3/trunk/src/zope/app/testing/testbrowser/testing.py

-=-
Added: Zope3/trunk/src/zope/app/testing/testbrowser/README.txt
===================================================================
--- Zope3/trunk/src/zope/app/testing/testbrowser/README.txt	2005-07-26 22:39:45 UTC (rev 37445)
+++ Zope3/trunk/src/zope/app/testing/testbrowser/README.txt	2005-07-27 02:58:18 UTC (rev 37446)
@@ -0,0 +1,193 @@
+zope.app.testing.testbrowser
+============================
+
+The zope.app.testing.testbrowser module exposes a `Browser` class that
+simulates a web browser similar to Mozilla Firefox or IE.
+
+    >>> from zope.app.testing.testbrowser import Browser
+    >>> browser = Browser()
+    >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+
+The browser can `open` web pages:
+
+    >>> browser.open('http://localhost/++etc++site/default')
+    >>> browser.url
+    'http://localhost/++etc++site/default'
+
+
+Page Contents
+=============
+
+The contents of the current page are available:
+
+    >>> print browser.contents
+    <...
+    <html...>
+    <body...>
+    ...
+
+Making assertions about page contents are easy.
+
+    >>> '<a href="RootErrorReportingUtility">' in browser.contents
+    True
+
+
+Headers
+=======
+
+The page's headers are also available as an httplib.HTTPMessage instance:
+
+    >>> browser.headers
+    <httplib.HTTPMessage instance...>
+
+The headers can be accesed as a string:
+
+    >>> print browser.headers
+    Status: 200 Ok
+    Content-Length: ...
+    Content-Type: text/html;charset=utf-8
+    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
+==========
+
+If you want to simulate clicking on a link, there is a `click` method.
+
+    >>> browser.click('RootErrorReportingUtility')
+    >>> browser.url
+    'http://localhost/++etc++site/default/RootErrorReportingUtility'
+
+We'll navigate to a form and fill in some values and the submit the form.
+
+    >>> browser.click('Configure')
+    >>> browser.url
+    'http://localhost/++etc++site/default/RootErrorReportingUtility/@@configure.html'
+
+
+Forms
+=====
+
+The current page has a form on it, let's look at some of the controls:
+
+    >>> browser.controls['keep_entries']
+    '20'
+    >>> browser.controls['copy_to_zlog']
+    False
+
+If we request a control that doesn't exist, an exception is raised.
+
+    >>> browser.controls['does_not_exist']
+    Traceback (most recent call last):
+    ...
+    KeyError: 'does_not_exist'
+
+We want to change some of the form values and submit.
+
+    >>> browser.controls['keep_entries'] = '40'
+    >>> browser.controls['copy_to_zlog'] = True
+    >>> browser.click('Save Changes')
+
+Are our changes reflected on the resulting page?
+
+    >>> browser.controls['keep_entries']
+    '40'
+    >>> browser.controls['copy_to_zlog']
+    True
+
+The `controls` object also has an `update()` method similar to that of
+a dictionary:
+
+    >>> browser.controls.update(dict(keep_entries='30', copy_to_zlog=False))
+    >>> browser.click('Save Changes')
+    >>> browser.controls['keep_entries']
+    '30'
+    >>> browser.controls['copy_to_zlog']
+    False
+
+Finding Specific Forms
+======================
+
+Because pages can have multiple forms with like-named controls, it is sometimes
+neccesary 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.
+
+XXX these need to be re-targeted to pages registered just for this test
+##    >>> # zope form and use that instead
+##    >>> form = browser.forms['portlet_form']
+
+The form exposes several attributes:
+
+##    >>> form.name
+##    'portlet_form'
+##    >>> form.action
+##    'http://localhost/++etc++site/default/...'
+##    >>> form.method
+##    'POST'
+##    >>> form.id is None
+##    True
+
+The form's controls can also be accessed with the `controls` mapping.
+
+##    >>> form.controls['portlet_action']
+##    '...'
+
+More Forms
+==========
+
+Now, let's navegate to a page with a slightly more complex form.
+
+    >>> browser.click('Registration')
+    >>> browser.click('Advanced Options')
+    >>> browser.click('UtilityRegistration')
+
+Is the expected control on the page?
+
+    >>> 'field.permission' in browser.controls
+    True
+
+Good, let's retrieve it then:
+
+    >>> permission = browser.getControl('field.permission')
+
+What kind of control is it?
+    
+    >>> permission.type
+    'select'
+
+Is it a single- or multi-select?
+
+    >>> permission.multiple
+    False
+
+What options are available for the "field.permission" control?
+
+    >>> permission.options
+    ['', 'zope.Public', ... 'zope.ManageContent', ... 'zope.View', ...]
+
+
+We'll store the current setting so we can set it back later.
+
+    >>> original_permission = permission.value
+
+Let's set one of the options and submit the form.
+
+    >>> permission.value = ['zope.Public']
+    >>> browser.click('Change')
+
+Ok, did our change take effect? (Note that the order may not be preserved for
+multi-selects.)
+
+    >>> browser.controls['field.permission'] == ['zope.Public']
+    True
+
+Let's set it back, so we don't mess anything up.
+
+    >>> permission.value = original_permission
+    >>> browser.click('Change')


Property changes on: Zope3/trunk/src/zope/app/testing/testbrowser/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/app/testing/testbrowser/__init__.py
===================================================================
--- Zope3/trunk/src/zope/app/testing/testbrowser/__init__.py	2005-07-26 22:39:45 UTC (rev 37445)
+++ Zope3/trunk/src/zope/app/testing/testbrowser/__init__.py	2005-07-27 02:58:18 UTC (rev 37446)
@@ -0,0 +1,4 @@
+try:
+    from testing import Browser
+except ImportError:
+    pass


Property changes on: Zope3/trunk/src/zope/app/testing/testbrowser/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/app/testing/testbrowser/browser.py
===================================================================
--- Zope3/trunk/src/zope/app/testing/testbrowser/browser.py	2005-07-26 22:39:45 UTC (rev 37445)
+++ Zope3/trunk/src/zope/app/testing/testbrowser/browser.py	2005-07-27 02:58:18 UTC (rev 37446)
@@ -0,0 +1,248 @@
+import re
+
+class Browser(object):
+    def __init__(self, url=None, mech_browser=None):
+        if mech_browser is None:
+            import mechanize
+            mech_browser = mechanize.Browser()
+
+        self.mech_browser = mech_browser
+        if url is not None:
+            self.open(url)
+
+    def open(self, url, data=None):
+        self.mech_browser.open(url, data)
+
+    def addHeader(self, key, value):
+        self.mech_browser.addheaders.append( (key, value) )
+
+    @property
+    def url(self):
+        return self.mech_browser.geturl()
+
+    def reload(self):
+        self.mech_browser.reload()
+        self._changed()
+
+    def goBack(self, count=1):
+        self.mech_browser.back(self, count)
+        self._changed()
+
+    @property
+    def links(self, *args, **kws):
+        return self.mech_browser.links(*args, **kws)
+
+    @property
+    def isHtml(self):
+        return self.mech_browser.viewing_html()
+
+    @property
+    def title(self):
+        return self.mech_browser.title()
+
+    def click(self, text=None, url=None, id=None, name=None, coord=(1,1)):
+        form, control = self._findControl(text, id, name, type='submit')
+        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:
+            predicate = lambda link: link.attrs.get('id') == id
+            self.mech_browser.follow_link(predicate=predicate)
+        else:
+            if text is not None:
+                text_regex = re.compile(text)
+            else:
+                text_regex = None
+            if url is not None:
+                url_regex = re.compile(url)
+            else:
+                url_regex = None
+
+            self.mech_browser.follow_link(text_regex=text_regex,
+                                          url_regex=url_regex)
+        self._changed()
+
+    @property
+    def _findControl(self):
+        def _findControl(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 re.search(text, str(control.value)))
+                    ) and (type is None or control.type == type)):
+                        self.mech_browser.form = control_form
+                        return control_form, control
+
+            return None, None
+        return _findControl
+        
+    def _findForm(self, id, name, action):
+        for form in self.mech_browser.forms():
+            if ((id is not None and form.attrs.get('id') == id)
+            or (name is not None and form.name == name)
+            or (action is not None and re.search(action, str(form.action)))):
+                self.mech_browser.form = form
+                return form
+
+        return None
+        
+    def _clickSubmit(self, form, control, coord):
+        self.mech_browser.open(form.click(
+                    id=control.id, name=control.name, coord=coord))
+
+    __controls = None
+    @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
+
+    @property
+    def controls(self):
+        return ControlsMapping(self)
+
+    @property
+    def forms(self):
+        return FormsMapping(self)
+
+    def getControl(self, text):
+        form, control = self._findControl(text, text, text)
+        if control is None:
+            raise ValueError('could not locate control: ' + text)
+        return Control(control)
+
+    @property
+    def contents(self):
+        response = self.mech_browser.response()
+        old_location = response.tell()
+        response.seek(0)
+        for line in iter(lambda: response.readline().strip(), ''):
+            pass
+        contents = response.read()
+        response.seek(old_location)
+        return contents
+
+    @property
+    def headers(self):
+        return self.mech_browser.response().info()
+
+    def _changed(self):
+        self.__controls = None
+
+
+class Control(object):
+    def __init__(self, control):
+        self.mech_control = control
+
+    def __getattr__(self, name):
+        names = ['options', 'disabled', 'type', 'name', 'readonly', 'multiple']
+        if name in names:
+            return getattr(self.mech_control, name, None)
+        else:
+            raise AttributeError(name)
+
+    @apply
+    def value():
+        def fget(self):
+            value = self.mech_control.value
+            if self.mech_control.type == 'checkbox':
+                value = bool(value)
+            return value
+        def fset(self, value):
+            if self.mech_control.type == 'checkbox':
+                if value: 
+                    value = ['on']
+                else:
+                    value = []
+            self.mech_control.value = value
+        return property(fget, fset)
+
+    def clear(self):
+        self.mech_control.clear()
+
+    @property
+    def options(self):
+        try:
+            return self.mech_control.possible_items()
+        except:
+            raise AttributeError('options')
+
+
+class FormsMapping(object):
+    def __init__(self, browser):
+        self.browser = browser
+
+    def __getitem__(self, key):
+        try:
+            form = self.browser._findForm(key, key, None)
+        except ValueError:
+            raise KeyError(key)
+        return Form(self.browser, form)
+
+    def __contains__(self, item):
+        return self.browser._findForm(key, key, None) is not None
+
+
+class ControlsMapping(object):
+    def __init__(self, browser, form=None):
+        """Initialize the ControlsMapping
+        
+        browser - a Browser instance
+        form - a ClientForm instance
+        """
+        self.browser = browser
+        self.mech_form = form
+
+    def __getitem__(self, key):
+        form, control = self.browser._findControl(key, key, key)
+        if control is None:
+            raise KeyError(key)
+        if self.mech_form is not None and self.mech_form != form:
+            raise KeyError(key)
+        return Control(control).value
+
+    def __setitem__(self, key, value):
+        form, control = self.browser._findControl(key, key, key)
+        if control is None:
+            raise KeyError(key)
+        Control(control).value = value
+
+    def __contains__(self, item):
+        try:
+            self[item]
+        except KeyError:
+            return False
+        else:
+            return True
+
+    def update(self, mapping):
+        for k, v in mapping.items():
+            self[k] = v
+
+
+class Form(ControlsMapping):
+    
+    def __getattr__(self, name):
+        names = ['action', 'method', 'enctype', 'name']
+        if name in names:
+            return getattr(self.mech_form, name, None)
+        else:
+            raise AttributeError(name)
+
+    @property
+    def id(self):
+        return self.mech_form.attrs.get(id)
+
+    @property
+    def controls(self):
+        return ControlsMapping(browser=self.browser, form=self.mech_form)
+


Property changes on: Zope3/trunk/src/zope/app/testing/testbrowser/browser.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/app/testing/testbrowser/ftests.py
===================================================================
--- Zope3/trunk/src/zope/app/testing/testbrowser/ftests.py	2005-07-26 22:39:45 UTC (rev 37445)
+++ Zope3/trunk/src/zope/app/testing/testbrowser/ftests.py	2005-07-27 02:58:18 UTC (rev 37446)
@@ -0,0 +1,27 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL should accompany this
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+import unittest
+from zope.app.testing.functional import FunctionalDocFileSuite
+
+def test_suite():
+    try:
+        import mechanize
+    except ImportError:
+        return
+    else:
+        return FunctionalDocFileSuite('demo.txt')
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')


Property changes on: Zope3/trunk/src/zope/app/testing/testbrowser/ftests.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/app/testing/testbrowser/testing.py
===================================================================
--- Zope3/trunk/src/zope/app/testing/testbrowser/testing.py	2005-07-26 22:39:45 UTC (rev 37445)
+++ Zope3/trunk/src/zope/app/testing/testbrowser/testing.py	2005-07-27 02:58:18 UTC (rev 37446)
@@ -0,0 +1,113 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL should accompany this
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+import httplib
+import urllib2
+from cStringIO import StringIO
+
+import mechanize
+import ClientCookie
+
+from zope.app.testing.functional import HTTPCaller
+
+
+class PublisherConnection:
+
+    def __init__(self, host):
+        self.host = host
+        self.caller = HTTPCaller()
+
+    def set_debuglevel(self, level):
+        pass
+
+    def request(self, method, url, body=None, headers=None):
+        header_chunks = []
+        if body is None:
+            body = ''
+
+        if headers is not None:
+            for header in headers.items():
+                header_chunks.append('%s: %s' % header)
+            headers = '\n'.join(header_chunks) + '\n'
+        else:
+            headers = ''
+
+        request_string = (method + ' ' + url + ' HTTP/1.1\n'
+                          + headers + '\n' + body)
+
+        self.response = self.caller(request_string)
+
+    def getresponse(self):
+        headers = self.response.header_output.headersl
+        real_response = self.response._response
+        status = real_response.getStatus()
+        reason = real_response._reason # XXX should add a getReason method
+        output = (real_response.getHeaderText(real_response.getHeaders()) +
+                  self.response.getBody())
+        return PublisherResponse(output, status, reason)
+
+
+class PublisherResponse:
+
+    def __init__(self, content, status, reason):
+        self.content = content
+        self.status = status
+        self.reason = reason
+        self.msg = httplib.HTTPMessage(StringIO(content), 0)
+        self.content_as_file = StringIO(content)
+
+    def read(self, amt=None):
+        return self.content_as_file.read(amt)
+
+
+class PublisherHandler(urllib2.HTTPHandler):
+
+    http_request = urllib2.AbstractHTTPHandler.do_request_
+
+    def http_open(self, req):
+        return self.do_open(PublisherConnection, req)
+
+
+import browser
+
+class MyMechBrowser(mechanize.Browser):
+    handler_classes = {
+        # scheme handlers
+        "http": PublisherHandler,
+
+        "_http_error": ClientCookie.HTTPErrorProcessor,
+        "_http_request_upgrade": ClientCookie.HTTPRequestUpgradeProcessor,
+        "_http_default_error": urllib2.HTTPDefaultErrorHandler,
+
+        # feature handlers
+        "_authen": urllib2.HTTPBasicAuthHandler,
+        "_redirect": ClientCookie.HTTPRedirectHandler,
+        "_cookies": ClientCookie.HTTPCookieProcessor,
+        "_refresh": ClientCookie.HTTPRefreshProcessor,
+        "_referer": mechanize.Browser.handler_classes['_referer'],
+        "_equiv": ClientCookie.HTTPEquivProcessor,
+        "_seek": ClientCookie.SeekableProcessor,
+        }
+
+    default_schemes = ["http"]
+    default_others = ["_http_error", "_http_request_upgrade",
+                      "_http_default_error"]
+    default_features = ["_authen", "_redirect", "_cookies", "_seek"]
+
+
+class Browser(browser.Browser):
+    def __init__(self, url=None):
+        mech_browser = MyMechBrowser()
+        mech_browser.add_handler(PublisherHandler())
+        super(Browser, self).__init__(url=url, mech_browser=mech_browser)


Property changes on: Zope3/trunk/src/zope/app/testing/testbrowser/testing.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native



More information about the Zope3-Checkins mailing list