[Erp5-report] r44707 arnaud.fontaine - in /erp5/trunk/utils/erp5.utils.test_browser: ./ exa...
nobody at svn.erp5.org
nobody at svn.erp5.org
Tue Mar 29 10:24:32 CEST 2011
Author: arnaud.fontaine
Date: Tue Mar 29 10:24:32 2011
New Revision: 44707
URL: http://svn.erp5.org?rev=44707&view=rev
Log:
Add erp5.utils.test_browser which is supposed to deprecate
ERP5Mechanize soonish
Added:
erp5/trunk/utils/erp5.utils.test_browser/
erp5/trunk/utils/erp5.utils.test_browser/CHANGES.txt
erp5/trunk/utils/erp5.utils.test_browser/README.txt
erp5/trunk/utils/erp5.utils.test_browser/examples/
erp5/trunk/utils/erp5.utils.test_browser/examples/testAddPerson.py (with props)
erp5/trunk/utils/erp5.utils.test_browser/setup.py (with props)
erp5/trunk/utils/erp5.utils.test_browser/src/
erp5/trunk/utils/erp5.utils.test_browser/src/erp5/
erp5/trunk/utils/erp5.utils.test_browser/src/erp5/__init__.py
erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/
erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/__init__.py
erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/test_browser/
erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/test_browser/__init__.py
erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/test_browser/browser.py
Added: erp5/trunk/utils/erp5.utils.test_browser/CHANGES.txt
URL: http://svn.erp5.org/erp5/trunk/utils/erp5.utils.test_browser/CHANGES.txt?rev=44707&view=auto
==============================================================================
--- erp5/trunk/utils/erp5.utils.test_browser/CHANGES.txt (added)
+++ erp5/trunk/utils/erp5.utils.test_browser/CHANGES.txt [utf8] Tue Mar 29 10:24:32 2011
@@ -0,0 +1,4 @@
+0.1 (2011-03-25)
+----------------
+
+ - Initial release.
Added: erp5/trunk/utils/erp5.utils.test_browser/README.txt
URL: http://svn.erp5.org/erp5/trunk/utils/erp5.utils.test_browser/README.txt?rev=44707&view=auto
==============================================================================
--- erp5/trunk/utils/erp5.utils.test_browser/README.txt (added)
+++ erp5/trunk/utils/erp5.utils.test_browser/README.txt [utf8] Tue Mar 29 10:24:32 2011
@@ -0,0 +1,6 @@
+API Documentation
+-----------------
+
+You can generate the API documentation using ``epydoc'':
+
+$ epydoc -v src/erp5
Added: erp5/trunk/utils/erp5.utils.test_browser/examples/testAddPerson.py
URL: http://svn.erp5.org/erp5/trunk/utils/erp5.utils.test_browser/examples/testAddPerson.py?rev=44707&view=auto
==============================================================================
--- erp5/trunk/utils/erp5.utils.test_browser/examples/testAddPerson.py (added)
+++ erp5/trunk/utils/erp5.utils.test_browser/examples/testAddPerson.py [utf8] Tue Mar 29 10:24:32 2011
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from erp5.utils.test_browser.browser import Browser
+
+ITERATION = 20
+
+def benchmarkAddPerson(result_dict):
+ """
+ Benchmark adding a person
+ """
+ # Create a browser instance
+ browser = Browser('http://localhost:18080/', 'erp5', username='zope', password='zope')
+
+ # Open ERP5 homepage
+ browser.open()
+
+ # Go to Persons module (person_module)
+ browser.mainForm.submitSelectModule(label='Persons')
+
+ # Create a new person and record the time elapsed in seconds
+ result_dict.setdefault('Create new person', []).append(browser.mainForm.secondSubmitNew())
+
+ # Check whether it has been successfully created
+ assert browser.getTransitionMessage() == 'Object created.'
+
+ # Fill the first and last name of the newly created person
+ browser.mainForm.getControl(name='field_my_first_name').value = 'Foo'
+ browser.mainForm.getControl(name='field_my_last_name').value = 'Bar'
+
+ # Submit the changes, record the time elapsed in seconds
+ result_dict.setdefault('Save', []).append(browser.mainForm.secondSubmitSave())
+
+ # Check whether the changes have been successfully updated
+ assert browser.getTransitionMessage() == 'Data updated.'
+
+ # Validate the person and record confirmation
+ browser.mainForm.submitSelectWorkflow(label='Validate')
+ result_dict.setdefault('Validate', []).append(browser.mainForm.secondSubmitDialogConfirm())
+
+ # Check whether it has been successfully validated
+ assert browser.getTransitionMessage() == 'Status changed.'
+
+if __name__ == '__main__':
+ # Run benchmarkAddPerson ITERATION times and compute the average time it
+ # took for each operation
+ result_dict = {}
+ counter = 0
+ while counter != ITERATION:
+ benchmarkAddPerson(result_dict)
+ counter += 1
+
+ for title, time_list in result_dict.iteritems():
+ print "Average: %s: %.4fs" % (title, float(sum(time_list)) / ITERATION)
Propchange: erp5/trunk/utils/erp5.utils.test_browser/examples/testAddPerson.py
------------------------------------------------------------------------------
svn:executable = *
Added: erp5/trunk/utils/erp5.utils.test_browser/setup.py
URL: http://svn.erp5.org/erp5/trunk/utils/erp5.utils.test_browser/setup.py?rev=44707&view=auto
==============================================================================
--- erp5/trunk/utils/erp5.utils.test_browser/setup.py (added)
+++ erp5/trunk/utils/erp5.utils.test_browser/setup.py [utf8] Tue Mar 29 10:24:32 2011
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from setuptools import setup, find_packages
+
+name = 'erp5.utils.test_browser'
+version = 0.1
+
+setup(
+ name=name,
+ version=version,
+ description="Programmable browser for functional and performance tests for ERP5",
+ author='Arnaud Fontaine',
+ author_email='arnaud.fontaine at nexedi.com',
+ license='ZPL 2.1',
+ install_requires = ['z3c.etestbrowser'],
+ package_dir={'':'src'},
+ packages=find_packages('src'),
+ namespace_packages=['erp5', 'erp5.utils'],
+ include_package_data=True,
+ classifiers=[
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Zope Public License',
+ 'Programming Language :: Python',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Software Development :: Testing',
+ ],
+ url='http://www.erp5.org/',
+)
Propchange: erp5/trunk/utils/erp5.utils.test_browser/setup.py
------------------------------------------------------------------------------
svn:executable = *
Added: erp5/trunk/utils/erp5.utils.test_browser/src/erp5/__init__.py
URL: http://svn.erp5.org/erp5/trunk/utils/erp5.utils.test_browser/src/erp5/__init__.py?rev=44707&view=auto
==============================================================================
--- erp5/trunk/utils/erp5.utils.test_browser/src/erp5/__init__.py (added)
+++ erp5/trunk/utils/erp5.utils.test_browser/src/erp5/__init__.py [utf8] Tue Mar 29 10:24:32 2011
@@ -0,0 +1,7 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
+
Added: erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/__init__.py
URL: http://svn.erp5.org/erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/__init__.py?rev=44707&view=auto
==============================================================================
--- erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/__init__.py (added)
+++ erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/__init__.py [utf8] Tue Mar 29 10:24:32 2011
@@ -0,0 +1,7 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
+
Added: erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/test_browser/__init__.py
URL: http://svn.erp5.org/erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/test_browser/__init__.py?rev=44707&view=auto
==============================================================================
(empty)
Added: erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/test_browser/browser.py
URL: http://svn.erp5.org/erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/test_browser/browser.py?rev=44707&view=auto
==============================================================================
--- erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/test_browser/browser.py (added)
+++ erp5/trunk/utils/erp5.utils.test_browser/src/erp5/utils/test_browser/browser.py [utf8] Tue Mar 29 10:24:32 2011
@@ -0,0 +1,714 @@
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Copyright (c) 2011 Nexedi SARL and Contributors. All Rights Reserved.
+# Arnaud Fontaine <arnaud.fontaine at nexedi.com>
+#
+# First version: ERP5Mechanize from Vincent Pelletier <vincent at nexedi.com>
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+import logging
+
+import sys
+from urlparse import urljoin
+from z3c.etestbrowser.browser import ExtendedTestBrowser
+from zope.testbrowser.browser import onlyOne
+
+def measurementMetaClass(prefix):
+ """
+ Prepare a meta class where the C{prefix} is used to select for which
+ methods measurement methods will be added automatically.
+
+ @param prefix:
+ @type prefix: str
+ @return: The measurement meta class corresponding to the prefix
+ @rtype: type
+ """
+ class MeasurementMetaClass(type):
+ """
+ Meta class to define automatically C{second*} and C{pystone*}
+ methods automatically according to given C{prefix}, and also to
+ define C{lastRequestSeconds} and C{lastRequestPystones} on other
+ classes besides of Browser.
+ """
+ def __new__(metacls, name, bases, dictionary):
+ def applyMeasure(method):
+ """
+ Inner function to add the C{second} and C{pystone} methods to
+ the dictionary of newly created class.
+
+ For example, if the method name is C{submitSave} then
+ C{secondSubmitSave} and C{pystoneSubmitSave} will be added to
+ the newly created class.
+
+ @param method: Instance method to be called
+ @type method: function
+ """
+ # Upper the first character
+ method_name_suffix = method.func_name[0].upper() + method.func_name[1:]
+
+ def innerSecond(self, *args, **kwargs):
+ method(self, *args, **kwargs)
+ return self.lastRequestSeconds
+
+ innerSecond.func_name = 'second' + method_name_suffix
+ dictionary[innerSecond.func_name] = innerSecond
+
+ def innerPystone(self, *args, **kwargs):
+ method(self, *args, **kwargs)
+ return self.lastRequestPystones
+
+ innerPystone.func_name = 'pystone' + method_name_suffix
+ dictionary[innerPystone.func_name] = innerPystone
+
+ # Create second* and pystone* methods only for the methods
+ # prefixed by the given prefix
+ for attribute_name, attribute in dictionary.items():
+ if attribute_name.startswith(prefix) and callable(attribute):
+ applyMeasure(attribute)
+
+ # lastRequestSeconds and lastRequestPystones properties are only
+ # defined on classes inheriting from zope.testbrowser.browser.Browser,
+ # so create these properties for all other classes too
+ if 'Browser' not in bases[0].__name__:
+ dictionary['lastRequestSeconds'] = property(
+ lambda self: self.browser.lastRequestSeconds)
+
+ dictionary['lastRequestPystones'] = property(
+ lambda self: self.browser.lastRequestPystones)
+
+ return super(MeasurementMetaClass,
+ metacls).__new__(metacls, name, bases, dictionary)
+
+ return MeasurementMetaClass
+
+class Browser(ExtendedTestBrowser):
+ """
+ Implements mechanize tests specific to an ERP5 environment through
+ U{ExtendedTestBrowser<http://pypi.python.org/pypi/z3c.etestbrowser>}
+ (providing features to parse XML and access elements using XPATH)
+ using U{zope.testbrowser<http://pypi.python.org/pypi/zope.testbrowser>}
+ (providing benchmark and testing features on top of
+ U{mechanize<http://wwwsearch.sourceforge.net/mechanize/>}).
+
+ @todo:
+ - getFormulatorFieldValue
+ """
+ __metaclass__ = measurementMetaClass(prefix='open')
+
+ def __init__(self,
+ base_url,
+ erp5_site_id,
+ username,
+ password,
+ log_filename=None,
+ is_debug=False):
+ """
+ Create a browser object, allowing to log in right away with the
+ given username and password. The base URL must contain an I{/} at
+ the end.
+
+ @param base_url: Base HTTP URL
+ @type base_url: str
+ @param erp5_site_id: ERP5 site name
+ @type erp5_site_id: str
+ @param username: Username to be used to log into ERP5
+ @type username: str
+ @param password: Password to be used to log into ERP5
+ @param log_filename: Log filename (stdout if none given)
+ @type log_filename: str
+ @param is_debug: Enable or disable debugging (disable by default)
+ @type is_debug: bool
+ """
+ # Meaningful to re-create the MainForm class every time the page
+ # has been changed
+ self._main_form_counter = -1
+ self._main_form = None
+
+ assert base_url[-1] == '/'
+
+ self._base_url = base_url
+ self._erp5_site_id = erp5_site_id
+ self._erp5_base_url = urljoin(self._base_url, self._erp5_site_id) + '/'
+
+ self._username = username
+ self._password = password
+
+ # Only display WARNING message if debugging is not enabled
+ logging_level = level=(is_debug and logging.DEBUG or logging.WARNING)
+ if log_filename:
+ logging.basicConfig(filename=log_filename, level=logging_level)
+ else:
+ logging.basicConfig(stream=sys.stdout, level=logging_level)
+
+ super(Browser, self).__init__()
+
+ # Open login page, then login with the given username and password
+ self.open('login_form')
+ self.mainForm.submitLogin()
+
+ def open(self, url_or_path=None, data=None):
+ """
+ Open a relative (to the ERP5 base URL) or absolute URL. If the
+ given URL is not given, then it will open the home ERP5 page.
+
+ @param url_or_path: Relative or absolute URL
+ @type url_or_path: str
+ """
+ # In case url_or_path is an absolute URL, urljoin() will return
+ # it, otherwise it is a relative path and will be concatenated to
+ # ERP5 base URL
+ absolute_url = urljoin(self._erp5_base_url, url_or_path)
+
+ logging.info("Opening url: " + absolute_url)
+ super(Browser, self).open(absolute_url, data)
+
+ def getCookieValue(self, cookie_name, default=None):
+ """
+ Get the cookie value of the given cookie name.
+
+ @param cookie_name: Name of the cookie
+ @type cookie_name: str
+ @param default: Fallback value if the cookie was not found
+ @type default: str
+ @return: Cookie value
+ @rtype: str
+ """
+ for cookie in self.cookies:
+ if name == cookie.name:
+ return cookie.value
+
+ return default
+
+ @property
+ def mainForm(self):
+ """
+ Get the ERP5 main form of the current page. ERP5 generally use
+ only one form (whose C{id} is C{main_form}) for all the controls
+ within a page. A Form instance is returned including helper
+ methods specific to ERP5.
+
+ @return: The main Form class instance
+ @rtype: Form
+
+ @raise LookupError: The main form could not be found.
+
+ @todo: Perhaps the page could be parsed to generate a class with
+ only appropriate methods, but that would certainly be an
+ huge overhead for little benefit...
+
+ @todo: Patch zope.testbrowser to allow the class to be given
+ rather than duplicating the code
+ """
+ # If the page has not changed, no need to re-create a class, so
+ # just return the main_form instance
+ if self._main_form_counter == self._counter and self._main_form:
+ return self._main_form
+
+ self._main_form_counter = self._counter
+
+ main_form = None
+ for form in self.mech_browser.forms():
+ if form.attrs.get('id') == 'main_form':
+ main_form = form
+
+ if not main_form:
+ raise LookupError("Could not get 'main_form'")
+
+ self.mech_browser.form = form
+ self._main_form = ContextMainForm(self, form)
+ return self._main_form
+
+ def getContextLink(self, text=None, url=None, id=None, index=0):
+ """
+ Get an ERP5 link (see L{zope.testbrowser.interfaces.IBrowser}).
+
+ @todo: Patch zope.testbrowser to allow the class to be given
+ rather than duplicating the code
+ """
+ if id is not None:
+ def predicate(link):
+ return dict(link.attrs).get('id') == id
+ args = {'predicate': predicate}
+ else:
+ if isinstance(text, RegexType):
+ text_regex = text
+ elif text is not None:
+ text_regex = re.compile(re.escape(text), re.DOTALL)
+ else:
+ text_regex = None
+
+ if isinstance(url, RegexType):
+ url_regex = url
+ elif url is not None:
+ url_regex = re.compile(re.escape(url), re.DOTALL)
+ else:
+ url_regex = None
+ args = {'text_regex': text_regex, 'url_regex': url_regex}
+
+ args['nr'] = index
+ return ContextLink(self.mech_browser.find_link(**args), self)
+
+ def getListboxLink(self, line_number, column_number, *args, **kwargs):
+ """
+ Follow the link at the given position.
+
+ @param line_number: Line number of the link
+ @type line_number: int
+ @param column_number: Column number of the link
+ @type column_number: int
+ @param args: positional arguments given to C{getContextLink}
+ @type args: list
+ @param kwargs: keyword arguments given to C{getContextLink}
+ @type kwargs: dict
+ @return: C{Link} at the given line and column number
+ @rtype: L{zope.testbrowser.interfaces.ILink}
+ """
+ xpath_str = '%s//tr[%d]//%s[%d]//a[0]' % (self.browser._listbox_table_xpath_str,
+ line_number,
+ line_number == 1 and 'th' or 'td',
+ column_number)
+
+ return self.getContextLink(url=self.etree.xpath(xpath_str).get('href'),
+ *args, **kwargs)
+
+ def getTransitionMessage(self):
+ """
+ Parses the current page and returns the value of the portal_status
+ message.
+
+ @return: The transition message
+ @rtype: str
+
+ @raise LookupError: Not found
+ """
+ try:
+ return self.etree.xpath('//div[@id="transition_message"]')[0].text
+ except IndexError:
+ raise LookupError("Cannot find div with ID 'transition_message'")
+
+ _listbox_table_xpath_str = '//table[contains(@class, "listbox-table")]'
+
+ def getListboxPosition(self,
+ text,
+ column_number=None,
+ line_number=None,
+ strict=False):
+ """
+ Returns the position number of the first line containing given
+ text in given column or line number (starting from 1).
+
+ @param text: Text to search
+ @type text: str
+ @param column_number: Look into all the cells of this column
+ @type column_number: int
+ @param line_number: Look into all the cells of this line
+ @type line_number: int
+ @param strict: Should given text matches exactly
+ @type strict: bool
+ @return: The cell position
+ @rtype: int
+
+ @raise LookupError: Not found
+ """
+ # Require either column_number or line_number to be given
+ onlyOne([column_number, line_number], '"column_number" and "line_number"')
+
+ cell_type = line_number == 1 and 'th' or 'td'
+
+ if column_number:
+ column_or_line_xpath_str = '//tr//%s[%d]' % (cell_type, column_number)
+ else:
+ column_or_line_xpath_str = '//tr[%d]//%s' % (line_number, cell_type)
+
+ # Get all cells in the column (if column_number is given) or line
+ # (if line_number is given)
+ cell_list = self.etree.xpath(self._listbox_table_xpath_str + \
+ column_or_line_xpath_str)
+
+ # Iterate over the cells list until one the children content
+ # matches the expected text
+ for position, cell in enumerate(cell_list):
+ for child in cell.iterchildren():
+ if not child.text:
+ continue
+
+ if (strict and child.text == text) or \
+ (not strict and text in child.text):
+ return position + 1
+
+ raise LookupError("No matching cell with value '%s'" % text)
+
+ def getRemainingActivityCounter(self):
+ """
+ Return the number of remaining activities
+
+ @return: The number of remaining activities
+ @rtype: int
+ """
+ self.open('portal_activities/countMessage')
+ return self.contents and int(self.contents) or 0
+
+
+from zope.testbrowser.browser import Form, ListControl
+
+class LoginError(Exception):
+ """
+ Exception raised when login fails
+ """
+ pass
+
+class MainForm(Form):
+ """
+ Class defining convenient methods for the main form of ERP5. All the
+ methods specified are those always found in an ERP5 page in contrary
+ to L{ContextMainForm}.
+ """
+ __metaclass__ = measurementMetaClass(prefix='submit')
+
+ def submit(self, label=None, name=None, index=None, *args, **kwargs):
+ """
+ Overriden for logging purpose, and for specifying a default index
+ to 0 if not set, thus avoiding AmbiguityError being raised (in
+ ERP5 there may be several submit fields with the same name)
+ """
+ logging.debug("Submitting (name='%s', label='%s')" % (name, label))
+
+ if label is None and name is None:
+ super(MainForm, self).submit(label=label, name=name, *args, **kwargs)
+ else:
+ if index is None:
+ index = 0
+
+ super(MainForm, self).submit(label=label, name=name, index=index,
+ *args, **kwargs)
+
+ def submitSelect(self, select_name, submit_name, label=None, value=None):
+ """
+ Get the select control whose name attribute is C{select_name},
+ then select the option control specified either by its C{label} or
+ C{value} within that select control, and finally submit it using
+ the submit control whose name attribute is C{submit_name}.
+
+ The C{value} matches an option value if found at the end of the
+ latter (excluding the query string), for example a search for
+ I{/logout} will match I{/erp5/logout} and I{/erp5/logout?foo=bar}
+ (if and only if C{value} contains no query string) but not
+ I{/erp5/logout_bar}.
+
+ Label value is searched as case-sensitive whole words within the
+ labels for each item--that is, a search for I{Add} will match
+ I{Add a contact} but not I{Address}. A word is defined as one or
+ more alphanumeric characters or the underline.
+
+ @param select_name: Select control name
+ @type select_name: str
+ @param submit_name: Submit control name
+ @type submit_name: str
+ @param label: Label of the option control
+ @type label: str
+ @param value: Value of the option control
+ @type value: str
+
+ @raise LookupError: The select, option or submit control could not
+ be found
+ """
+ select_control = self.getControl(name=select_name)
+
+ # zope.testbrowser checks for a whole word but it is also useful
+ # to match the end of the option control value string because in
+ # ERP5, the value could be URL (such as 'http://foo:81/erp5/logout')
+ if value:
+ selected_item = None
+ for item in select_control.options:
+ if '?' not in value:
+ item = item.split('?')[0]
+
+ if item.endswith(value):
+ value = selected_item = item
+
+ logging.debug("select_id='%s', label='%s', value='%s'" % \
+ (select_name, label, value))
+
+ select_control.getControl(label=label, value=value).selected = True
+ self.submit(name=submit_name)
+
+ def submitLogin(self):
+ """
+ Log into ERP5 using the username and password provided in the browser.
+
+ @raise LoginError: Login failed
+
+ @todo: Use information sent back as headers rather than looking
+ into the page content?
+ """
+ logging.debug("Logging in: username='%s', password='%s'" % \
+ (self.browser._username, self.browser._password))
+
+ self.getControl(name='__ac_name').value = self.browser._username
+ self.getControl(name='__ac_password').value = self.browser._password
+ self.submit()
+
+ if 'Logged In as' not in self.browser.contents:
+ raise LoginError
+
+ def submitSelectFavourite(self, label=None, value=None):
+ """
+ Select and submit a favourite, given either by its label (such as
+ I{Log out}) or value (I{/logout}). See L{submitSelect}.
+ """
+ self.submitSelect('select_favorite', 'Base_doFavorite:method', label, value)
+
+ def submitSelectModule(self, label=None, value=None):
+ """
+ Select and submit a module, given either by its label (such as
+ I{Currencies}) or value (such as I{/glossary_module}). See
+ L{submitSelect}.
+ """
+ self.submitSelect('select_module', 'Base_doModule:method', label, value)
+
+ def submitSelectLanguage(self, label=None, value=None):
+ """
+ Select and submit a language, given either by its label (such as
+ I{English}) or value (such as I{en}). See L{submitSelect}.
+ """
+ self.submitSelect('select_language', 'Base_doLanguage:method', label, value)
+
+ def submitSearch(self, search_text):
+ """
+ Fill search field with C{search_text} and submit it.
+
+ @param search_text: Text to search
+ @type search_text: str
+ """
+ self.getControl('field_your_search_text').value = search_text
+ self.submit(name='ERP5Site_viewQuickSearchResultList:method')
+
+ def submitLogout(self):
+ """
+ Perform logout.
+ """
+ self.submitFavourite('select_favorite', 'Base_doFavorite:method',
+ name='logout')
+
+class ContextMainForm(MainForm):
+ """
+ Class defining context-dependent convenient methods for the main
+ form of ERP5.
+
+ @todo:
+ - doListboxAction
+ - doContextListMode
+ - doContextSearch
+ - doContextSort
+ - doContextConfigure
+ - doContextButton
+ - doContextReport
+ - doContextExchange
+ """
+ def submitJump(self, label=None, value=None):
+ """
+ Select and submit a jump, given either by its label (such as
+ I{Queries}) or value (such as
+ I{/person_module/Base_jumpToRelatedObject?portal_type=Foo}). See
+ L{submitSelect}.
+ """
+ self.submitSelect('select_jump', 'Base_doJump:method', label, value)
+
+ def submitAction(self, label=None, value=None):
+ """
+ Select and submit an action, given either by its label (such as
+ I{Add Person}) or value (such as I{add} and I{add Person}). See
+ L{submitSelect}.
+ """
+ self.submitSelect('select_action', 'Base_doAction:method', label, value)
+
+ def submitCut(self):
+ """
+ Cut the previously selected objects.
+ """
+ self.submit(name='Folder_cut:method')
+
+ def submitCopy(self):
+ """
+ Copy the previously selected objects.
+ """
+ self.submit(name='Folder_copy:method')
+
+ def submitPaste(self):
+ """
+ Paste the previously selected objects.
+ """
+ self.submit(name='Folder_paste:method')
+
+ def submitPrint(self):
+ """
+ Print the previously selected objects.
+ """
+ self.submit(name='Folder_print:method')
+
+ def submitNew(self):
+ """
+ Create a new object.
+ """
+ self.submit(name='Folder_create:method')
+
+ def submitDelete(self):
+ """
+ Delete the previously selected objects.
+ """
+ self.submit(name='Folder_deleteObjectList:method')
+
+ def submitSave(self):
+ """
+ Save the previously selected objects.
+ """
+ self.submit(name='Base_edit:method')
+
+ def submitShow(self):
+ """
+ Show the previously selected objects.
+ """
+ self.submit(name='Folder_show:method')
+
+ def submitFilter(self):
+ """
+ Filter the objects.
+ """
+ self.submit(name='Folder_filter:method')
+
+ def submitSelectWorkflow(self, label=None, value=None,
+ script_id='BaseWorkflow_viewWorkflowActionDialog'):
+ """
+ Select and submit a workflow action, given either by its label
+ (such as I{Create User}) or value (such as I{create_user_action}
+ in I{/Person_viewCreateUserActionDialog?workflow_action=create_user_action},
+ with C{script_id=Person_viewCreateUserActionDialog}). See L{submitSelect}.
+
+ When validating an object, L{submitDialogConfirm} allows to
+ perform the validation required on the next page.
+
+ @param script_id: Script identifier
+ @type script_id: str
+ """
+ try:
+ if value:
+ value = '%s?workflow_action=%s' % (script_id, value)
+
+ self.submitSelect('select_action', 'Base_doAction:method', label, value)
+
+ except LookupError:
+ if value:
+ value = '%s?field_my_workflow_action=%s' % (script_id, value)
+
+ self.submitSelect('select_action', 'Base_doAction:method', label, value)
+
+ def submitDialogCancel(self):
+ """
+ Cancel the dialog action. A dialog is showed when validating a
+ workflow or deleting an object for example.
+ """
+ self.submit(name='Base_cancel:method')
+
+ def submitDialogConfirm(self):
+ """
+ Confirm the dialog action. A dialog is showed when validating a
+ workflow or deleting an object for example.
+
+ @todo: Specifying index is kind of ugly (there is C{dummy} field
+ with the same name though)
+ """
+ self.submit(name='Base_callDialogMethod:method')
+
+ def getListboxControl(self, line_number, column_number, *args, **kwargs):
+ """
+ Get the control located at line and column numbers (both starting
+ from 1). The position of a cell from a column or line number can
+ be obtained through calling
+ L{erp5.utils.test_browser.browser.Browser.getListboxPosition}.
+
+ @param line_number: Line number of the field
+ @type line_number: int
+ @param column_number: Column number of the field
+ @type column_number: int
+ @param args: positional arguments given to the parent C{getControl}
+ @type args: list
+ @param kwargs: keyword arguments given to the parent C{getControl}
+ @type kwargs: dict
+ @return: The control found at the given line and column numbers
+ @rtype: L{zope.testbrowser.interfaces.IControl}
+
+ @todo: What if there is more than one field in a cell?
+ """
+ xpath_str = '%s//tr[%d]//%s[%d]/input' % (self.browser._listbox_table_xpath_str,
+ line_number,
+ (line_number == 1 and u'th' or u'td'),
+ column_number)
+
+ input_element = self.browser.etree.xpath(xpath_str)[0]
+
+ control = self.getControl(name=input_element.get('name'), *args, **kwargs)
+
+ # If this is a list control (radio button, checkbox or select
+ # control), then get the item from its value
+ if isinstance(control, ListControl):
+ control = control.getControl(value=input_element.get('value'))
+
+ return control
+
+
+from zope.testbrowser.browser import Link
+
+class ContextLink(Link):
+ """
+ Class defining convenient methods for context-dependent links of
+ ERP5.
+ """
+ __metaclass__ = measurementMetaClass(prefix='submit')
+
+ def clickFirst(self):
+ """
+ Go to the first page.
+ """
+ self.getLink(url='/viewFirst')
+
+ def clickPrevious(self):
+ """
+ Go to the previous page.
+ """
+ self.getLink(url='/viewPrevious').click()
+
+ def clickNext(self):
+ """
+ Go to the next page.
+ """
+ self.getLink(url='/viewNext').click()
+
+ def clickLast(self):
+ """
+ Go to the last page.
+ """
+ self.getLink(url='/viewLast').click()
More information about the Erp5-report
mailing list