[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