[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