def menu_option(menu_label, option_label, browser=default_browser, query_info=None): """Returns the link node (``<a>``) of an option in a menu. :param menu_label: The label of the menu. :type menu_label: string :param menu_label: The label of the option to find in the menu. :type menu_label: string :param browser: The browser instance to operate with. Uses the global singleton default browser by default. :type browser: :py:class:`ftw.testbrowser.core.Browser` :returns: The option link node. :rtype: :py:class:`ftw.testbrowser.nodes.NodeWrapper` :raises: :py:exc:`ftw.testbrowser.exceptions.NoElementFound` """ menu_container = menu(menu_label, browser=browser, query_info=query_info) option_label = normalize_spaces(option_label) for link in menu_container.css( # Plone 4 '.actionMenuContent a, ' # Plone 5 '> ul > li > a'): if normalize_spaces(link.text_content()) == option_label: return link query_info.add_hint('Options in menu {!r}: {!r}'.format( menu_label, menu_options(menu_label, browser=browser))) raise NoElementFound(query_info)
def menu(label, browser=default_browser, query_info=None): """Finds a menu by label and returns its ``<dl class="actionMenu">`` node. :param label: The label of the menu to find. :type label: string :param browser: The browser instance to operate with. Uses the global singleton default browser by default. :type browser: :py:class:`ftw.testbrowser.core.Browser` :returns: The menu container node. :rtype: :py:class:`ftw.testbrowser.nodes.NodeWrapper` :raises: :py:exc:`ftw.testbrowser.exceptions.NoElementFound` """ label = normalize_spaces(label).rstrip(u'\u2026') if IS_PLONE_4: menus_node = container(browser=browser).css('#contentActionMenus').first for span in menus_node.css('.actionMenuHeader > a > span:first-child'): if normalize_spaces(span.text_content()).rstrip(u'\u2026') == label: return span.parent('.actionMenu') else: for menu in container(browser=browser).css('nav li[id^="plone-contentmenu-"]'): for span in menu.css('a > span.plone-toolbar-title'): if normalize_spaces(span.text_content()).rstrip(u'\u2026') == label: return menu query_info.add_hint('Visible menus: {!r}.'.format(menus(browser=browser))) raise NoElementFound(query_info)
def find_field(self, label_or_name): """Finds and returns a field by label or name. :param label_or_name: The label or the name of the field. :type label_or_name: string :returns: The field node :rtype: :py:class:`ftw.testbrowser.nodes.NodeWrapper` """ label = normalize_spaces(label_or_name) for input in self.inputs: if input.name == label_or_name: return input if input.label is None: continue if label in (input.label.text, normalize_spaces(input.label.raw_text)): return input checkbox_labels = (input.label is not None and input.label.css('>span.label')) if checkbox_labels and label == checkbox_labels.first.text: return input return self.find_widget(label_or_name)
def fill(self, values): """Fill the widget inputs with the values passed as arguments. :param values: a list of names and / or labels of the options :type values: list of string """ if not isinstance(values, (list, tuple, set)): values = [values] # deselect existing options for input in self.inputs: input.checked = False # normalize value labels to names reverse_option_map = dict(map(reversed, self.option_map.items())) values = [reverse_option_map.get(value, value) for value in values] # fill new values for input in self.inputs: if 'value' not in input.attrib: continue if input.attrib['value'] in values: input.checked = True values.remove(input.attrib['value']) if values: available_options = self.options raise OptionsNotFound(normalize_spaces(self.label.raw_text), values, available_options)
def find_widget(self, label): """Finds a Plone widget (div.field) in a form. :param label: The label of the widget. :type label: string :returns: Returns the field node or `None`. :rtype: :py:class:`ftw.testbrowser.nodes.NodeWrapper` """ label = normalize_spaces(label) label_node_xpath = '//label[normalize-space(text())="%s"]' % label div_node_xpath = '//div[contains(concat(" ",' + \ 'normalize-space(@class)," ")," label ")]' + \ '[normalize-space(text())="%s"]' % label label_xpath = ' | '.join((label_node_xpath, div_node_xpath)) for label_node in self.xpath(label_xpath): if not label_node.within(self): continue field = label_node.parent(css='div.field') if field: return field return None
def field_labels(self): """A list of label texts and field names of each field in this form. The list contains the whitespace normalized label text of each field. If there is no label or it has an empty text, the fieldname is used instead. :returns: A list of label texts (and field names). :rtype: list of strings """ labels = [] for input in self.inputs: label = (input.label is not None and normalize_spaces(input.label.text)) if label: labels.append(label) elif input.name: labels.append(input.name) checkbox_labels = (input.label is not None and input.label.css('>span.label')) if checkbox_labels: labels.append(checkbox_labels.first.text) return labels
def document_description(browser=default_browser): """Returns the whitespace-normalized document description of the current page or None. """ nodes = browser.css('.documentDescription') if len(nodes) == 0: return None return normalize_spaces(nodes.first.text_content())
def normalized_outerHTML(self): """The whitespace-normalized HTML of the current node and its children. The HTML-Tag of the current node is included. All series of whitespaces (including non-breaking spaces) are replaced with a single space. :returns: HTML :rtype: unicode """ return normalize_spaces(self.outerHTML)
def normalized_innerHTML(self): """The whitespace-normalized HTML content of the current node. The HTML-Tag of the current node is not included. All series of whitespaces (including non-breaking spaces) are replaced with a single space. :returns: HTML :rtype: unicode """ return normalize_spaces(self.innerHTML)
def normalized_text(self, recursive=True): """Returns the whitespace-normalized text of the current node. This includes the text of each node within this node recurively. All whitespaces are reduced to a single space each. .. deprecated:: 1.3.1 Use property :py:func:`ftw.testbrowser.nodes.NodeWrapper.text` instead. :param recursive: Set to ``False`` for not including text of contained tags. :type recursive: Boolean (default: ``True``) :returns: The whitespace normalized text content. :rtype: unicode """ if recursive: return normalize_spaces(self.text_content()) else: return normalize_spaces(self.raw_text or '')
def find_link_by_text(self, text, within=None): """Searches for a link with the passed text. The comparison is done with normalized whitespace and includes the full text within the link, including its subelements' texts. :param text: The text to be looked for. :type text: string :param within: A node object for limiting the scope of the search. :type within: :py:class:`ftw.testbrowser.nodes.NodeWrapper`. :returns: The link object or `None` if nothing matches. :rtype: :py:class:`ftw.testbrowser.nodes.LinkNode` """ text = normalize_spaces(text) if within is None: within = self for link in within.css('a'): if normalize_spaces(link.text_content()) == text: return link return None
def find(self, text): """Find a cell of this table by text. When nothing is found, it falls back to the default ``find`` behavior. .. seealso:: :py:func:`ftw.testbrowser.nodes.NodeWrapper.find` :param text: The text to be looked for. :type text: string :returns: A single node object or `None` if nothing matches. :rtype: :py:class:`ftw.testbrowser.nodes.NodeWrapper` """ text = normalize_spaces(text) for cell in self.cells: if cell.normalized_text() == text: return cell return super(Table, self).find(text)
def messages(browser=default_browser): """Returns a dict with lists of status messages (normalized text) for "info", "warning" and "error". """ messages = {'info': [], 'warning': [], 'error': []} for message in browser.css('.portalMessage'): type_classes = (set(message.classes) & set(messages.keys())) if not type_classes: # unkown message type - skip it continue key = tuple(type_classes)[0] if message.css('dd'): # Plone 4: <dl class="portalMessage info"> # <dt>Info</dt> # <dd>Message</dd> # </dl> text = normalize_spaces(' '.join(message.css('dd').text_content())) elif message.css('strong'): # Plone 5: <div class="portalMessage info"> # <strong>Info</strong> # Message # </div> type_text = message.css('strong').first.text text = re.sub(r'^{} *'.format(re.escape(type_text)), '', message.text) if not text: # message is empty - skip it continue messages[key].append(text) return messages
def erroneous_fields(form): """Returns a mapping of erroneous fields (key is label or name of the field) to a list of error messages for the fields on the form passed as argument. :param form: The form node to check for errors. :type form: :py:class:`ftw.testbrowser.form.Form` :returns: A dict of erroneous fields with error messages. :rtype: dict """ result = {} for input in form.inputs: if not input.parent('.field.error'): continue if input.label is not None: label = input.label.text_content() if not label: label = input.name errors = input.parent('.field').css('.fieldErrorBox').normalized_text() result[normalize_spaces(label)] = errors return result
def messages(browser=default_browser): """Returns a dict with lists of status messages (normalized text) for "info", "warning" and "error". """ messages = {'info': [], 'warning': [], 'error': []} for message in browser.css('.portalMessage'): type_classes = (set(message.classes) & set(messages.keys())) if not type_classes: # unkown message type - skip it continue key = tuple(type_classes)[0] text = normalize_spaces(' '.join(message.css('dd').text_content())) if not text: # message is empty - skip it continue messages[key].append(text) return messages
def first_heading(browser=default_browser): """Returns the whitespace-normalized first heading of the current page. """ first_heading = browser.css('.documentFirstHeading').first return normalize_spaces(first_heading.text_content())