Module pyseext.tree_helper

Module that contains our TreeHelper class.

Expand source code
"""
Module that contains our TreeHelper class.
"""
import logging
import time
from typing import Union
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement

from pyseext.has_referenced_javascript import HasReferencedJavaScript
from pyseext.core import Core
from pyseext.menu_helper import MenuHelper

class TreeHelper(HasReferencedJavaScript):
    """A class to help with using trees, through Ext's interfaces."""

    # Class variables
    _IS_TREE_LOADING_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.isTreeLoading('{tree_cq}')"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.isTreeLoading
    Requires the inserts: {tree_cq}"""

    _GET_NODE_ELEMENT_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.getNodeElement('{tree_cq}', {node_text_or_data}, '{css_query}')"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.getNodeElement
    Requires the inserts: {tree_cq}, {node_text_or_data}, {css_query}"""

    _GET_NODE_ELEMENT_WITH_ROOT_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.getNodeElement('{tree_cq}', {node_text_or_data}, '{css_query}', {root_node_text_or_data})"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.getNodeElement including a root node to search from
    Requires the inserts: {tree_cq}, {node_text_or_data}, {css_query}, {root_node_text_or_data}"""

    _RELOAD_NODE_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.reloadNode('{tree_cq}', {node_text_or_data})"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.reloadNode
    Requires the inserts: {tree_cq}, {node_text_or_data}"""

    _RELOAD_NODE_WITH_ROOT_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.reloadNode('{tree_cq}', {node_text_or_data}, {root_node_text_or_data})"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.reloadNode including a root node to search from
    Requires the inserts: {tree_cq}, {node_text_or_data}, {root_node_text_or_data}"""

    _ICON_CSS_SELECTOR: str = ".x-tree-icon"
    """The CSS selector to use with get_node_element to find the node icon element.
    """

    _EXPANDER_CSS_SELECTOR: str = ".x-tree-expander"
    """The CSS selector to use with get_node_element to find the node expander element.
    """

    _NODE_TEXT_CSS_SELECTOR: str = ".x-tree-node-text"
    """The CSS selector to use with get_node_element to find the node text element.
    """

    def __init__(self, driver: WebDriver):
        """Initialises an instance of this class

        Args:
            driver (WebDriver): The webdriver to use
        """
        self._logger = logging.getLogger(__name__)
        """The Logger instance for this class instance"""

        self._driver = driver
        """The WebDriver instance for this class instance"""

        self._action_chains = ActionChains(driver)
        """The ActionChains instance for this class instance"""

        self._core = Core(driver)
        """The `Core` instance for this class instance"""

        self._menu_helper = MenuHelper(driver)
        """The `MenuHelper` instance for this class instance"""

        # Initialise our base class
        super().__init__(driver, self._logger)

    def is_tree_loading(self, tree_cq: str):
        """Determine whether the tree (any part of it) is currently loading.

        You should call this before calling any tree interaction methods,
        since we cannot pass things back in callbacks!

        Args:
            tree_cq (str): The component query to use to find the tree.

        Returns:
            bool: True if the tree is loaded, False otherwise.
        """
        script = self._IS_TREE_LOADING_TEMPLATE.format(tree_cq=tree_cq)
        self.ensure_javascript_loaded()
        return self._driver.execute_script(script)

    def wait_until_tree_not_loading(self,
                                    tree_cq: str,
                                    timeout: float = 30,
                                    poll_frequecy: float = 0.2,
                                    recheck_time_if_false: float = 0.2):
        """Waits until the tree identified by the component query is not loading,
        or the timeout is hit

        Args:
            tree_cq (str): The component query for the tree.
            timeout (float, optional): The number of seconds to wait before erroring. Defaults to 30.
            poll_frequency (float, optional): Number of seconds to poll. Defaults to 0.2.
            recheck_time_if_false (float, optional): If we get a result such that no Ajax calls are in progress, this is the amount of time to wait to check again. Defaults to 0.2.
        """
        WebDriverWait(self._driver, timeout, poll_frequency = poll_frequecy).until(TreeHelper.TreeNotLoadingExpectation(tree_cq, recheck_time_if_false))

    def get_node_icon_element(self,
                              tree_cq: str,
                              node_text_or_data: Union[str, dict],
                              root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
        """Finds a node by text or data, then the child HTML element that holds it's icon.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.

        Returns:
            WebElement: The DOM element for the node icon.
        """
        return self.get_node_element(tree_cq, node_text_or_data, self._ICON_CSS_SELECTOR, root_node_text_or_data)

    def get_node_text_element(self,
                              tree_cq: str,
                              node_text_or_data: Union[str, dict],
                              root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
        """Finds a node by text or data, then the child HTML element that holds it's text.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.

        Returns:
            WebElement: The DOM element for the node text.
        """
        return self.get_node_element(tree_cq, node_text_or_data, self._NODE_TEXT_CSS_SELECTOR, root_node_text_or_data)

    def get_node_expander_element(self,
                                  tree_cq: str,
                                  node_text_or_data: Union[str, dict],
                                  root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
        """Finds a node by text or data, then the child HTML element that holds it's expander UI element.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.

        Returns:
            WebElement: The DOM element for the node's expander.
        """
        return self.get_node_element(tree_cq, node_text_or_data, self._EXPANDER_CSS_SELECTOR, root_node_text_or_data)

    def get_node_element(self,
                         tree_cq: str,
                         node_text_or_data: Union[str, dict],
                         css_query: str,
                         root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
        """Finds a node by text or data, then a child element by CSS query.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            css_query (str): The CSS to query for in the found node row element.
                             Some expected ones:
                                Expander UI element = '.x-tree-expander'
                                Node icon = '.x-tree-icon'
                                Node text = '.x-tree-node-text'
                             If need those you'd use one of the other methods though.
                             This is in case need to click on another part of the node's row.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.

        Returns:
            WebElement: The DOM element for the node's expander.
        """
        self.wait_until_tree_not_loading(tree_cq)

        if isinstance(node_text_or_data, str):
            node_text_or_data = f"'{node_text_or_data}'"

        if isinstance(root_node_text_or_data, str):
            root_node_text_or_data = f"'{root_node_text_or_data}'"

        if root_node_text_or_data:
            script = self._GET_NODE_ELEMENT_WITH_ROOT_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data, css_query=css_query, root_node_text_or_data=root_node_text_or_data)
        else:
            script = self._GET_NODE_ELEMENT_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data, css_query=css_query)

        self.ensure_javascript_loaded()
        return self._driver.execute_script(script)

    def open_node_context_menu(self,
                               tree_cq: str,
                               node_text_or_data: Union[str, dict],
                               root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node's icon element by text or data, then right clicks on it.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        self.context_click_node_element(tree_cq, node_text_or_data, self._ICON_CSS_SELECTOR, root_node_text_or_data)

    def check_node_context_menu_contains_items(self,
                                               tree_cq: str,
                                               node_text_or_data: Union[str, dict],
                                               expected_items: list[str],
                                               check_contains_no_additional_items: bool = True,
                                               ignore_spacers: bool = False,
                                               root_node_text_or_data: Union[str, dict, None] = None):
        """Opens the context menu for a node, and checks that it contains the specified items.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            expected_items (list[str]): A list of menu items (text) that we're expecting to find.
                                        Spacers have text containing of a single space (MenuHelper.SPACER_TEXT_CONTEXT), but can be ignored using ignore_spacers if desired.
                                        e.g. ['Add', 'Edit', 'Delete', MenuHelper.SPACER_TEXT_CONTEXT, 'Refresh']
            check_contains_no_additional_items (bool, optional): Indicates whether we should check that there are no additional items in the menu.
                                                                 Defaults to True.
            ignore_spacers (bool, optional): Indicates whether we should include spacers in our checks. Defaults to False.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        self.open_node_context_menu(tree_cq, node_text_or_data, root_node_text_or_data)

        unexpected_items: list[str] = []

        # Get all menu items
        menu_items = self._menu_helper.get_enabled_menu_items()

        # Each element has a text member we can compare.
        for item in menu_items:
            if ignore_spacers is True and item.text == MenuHelper.SPACER_TEXT_CONTEXT:
                continue

            if not item.text in expected_items:
                # Item not expected
                if check_contains_no_additional_items:
                    unexpected_items.append(item.text)
            else:
                # Item expected and found, so remove from expected list.
                expected_items.remove(item.text)

                # If have nothing left to find, and we're not checking for unexpected items, then can break from our loop.
                if not check_contains_no_additional_items and len(expected_items) == 0:
                    break

        # Is there anything left in our expected item list?
        if len(expected_items) > 0:
            raise TreeHelper.ExpectedMenuItemsNotFoundException(tree_cq,
                                                                node_text_or_data,
                                                                expected_items)

        # Was there anything in the menu that was not expected, and we were checking?
        if len(unexpected_items) > 0:
            raise TreeHelper.UnexpectedMenuItemsFoundException(tree_cq,
                                                               node_text_or_data,
                                                               unexpected_items)


    def click_node_expander(self,
                            tree_cq: str,
                            node_text_or_data: Union[str, dict],
                            root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node's expander element by text or data, then clicks on it.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        node = self.get_node_expander_element(tree_cq, node_text_or_data, root_node_text_or_data)

        if node:
            if root_node_text_or_data:
                self._logger.info("Clicking expander on node '%s' (under root '%s') on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, tree_cq)
            else:
                self._logger.info("Clicking expander on node '%s' on tree with CQ '%s'", node_text_or_data, tree_cq)

            self._action_chains.move_to_element(node)
            self._action_chains.click(node)
            self._action_chains.perform()
        else:
            raise TreeHelper.NodeNotFoundException(tree_cq, node_text_or_data, root_node_text_or_data)

    def click_node_element(self,
                           tree_cq: str,
                           node_text_or_data: Union[str, dict],
                           css_query: str,
                           root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node by text or data, then a child element by CSS query, then clicks on it.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            css_query (str): The CSS to query for in the found node row element.
                             Some expected ones:
                                Expander UI element = '.x-tree-expander'
                                Node icon = '.x-tree-icon'
                                Node text = '.x-tree-node-text'
                             If need those you'd use one of the other methods though.
                             This is in case need to click on another part of the node's row.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        node = self.get_node_element(tree_cq, node_text_or_data, css_query, root_node_text_or_data)

        if node:
            if root_node_text_or_data:
                self._logger.info("Clicking on node '%s' (under root '%s'), with CSS query '%s' on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, css_query, tree_cq)
            else:
                self._logger.info("Clicking on node '%s' with CSS query '%s' on tree with CQ '%s'", node_text_or_data, css_query, tree_cq)

            self._action_chains.move_to_element(node)
            self._action_chains.click(node)
            self._action_chains.perform()
        else:
            raise TreeHelper.NodeNotFoundException(tree_cq, node_text_or_data, root_node_text_or_data)

    def context_click_node_element(self,
                                   tree_cq: str,
                                   node_text_or_data: Union[str, dict],
                                   css_query: str,
                                   root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node by text or data, then a child element by CSS query, then right clicks on it.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            css_query (str): The CSS to query for in the found node row element.
                             Some expected ones:
                                Expander UI element = '.x-tree-expander'
                                Node icon = '.x-tree-icon'
                                Node text = '.x-tree-node-text'
                             If need those you'd use one of the other methods though.
                             This is in case need to click on another part of the node's row.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        node = self.get_node_element(tree_cq, node_text_or_data, css_query, root_node_text_or_data)

        if node:
            if root_node_text_or_data:
                self._logger.info("Right clicking on node '%s' (under root '%s'), with CSS query '%s' on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, css_query, tree_cq)
            else:
                self._logger.info("Right clicking on node '%s' with CSS query '%s' on tree with CQ '%s'", node_text_or_data, css_query, tree_cq)

            self._action_chains.move_to_element(node)
            self._action_chains.context_click(node)
            self._action_chains.perform()
        else:
            raise TreeHelper.NodeNotFoundException(tree_cq, node_text_or_data, root_node_text_or_data)

    def wait_for_tree_node(self,
                           tree_cq: str,
                           node_text_or_data: Union[str, dict],
                           parent_node_text_or_data: Union[str, dict],
                           timeout: float = 60) -> WebElement:
        """Method that waits until a tree node is available, refreshing the parent until it's
        found or the timeout is hit.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            parent_node_text_or_data (Union[str, dict]): The node text or data to use to find the nodes parent,
                                                         for refreshing purposes.
            timeout (int, optional): The number of seconds to wait for the row before erroring. Defaults to 60.

        Returns:
            WebElement: The DOM element for the node icon.
        """
        WebDriverWait(self._driver, timeout).until(TreeHelper.NodeFoundExpectation(tree_cq, node_text_or_data, parent_node_text_or_data))
        return self.get_node_icon_element(tree_cq, node_text_or_data)

    def reload_node(self,
                    tree_cq: str,
                    node_text_or_data: Union[str, dict],
                    root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node by text or data, and triggers a reload on it, and its children.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        self.wait_until_tree_not_loading(tree_cq)

        if isinstance(node_text_or_data, str):
            node_text_or_data = f"'{node_text_or_data}'"

        if isinstance(root_node_text_or_data, str):
            root_node_text_or_data = f"'{root_node_text_or_data}'"

        if root_node_text_or_data:
            script = self._RELOAD_NODE_WITH_ROOT_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data, root_node_text_or_data=root_node_text_or_data)
        else:
            script = self._RELOAD_NODE_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data)

        if root_node_text_or_data:
            self._logger.info("Reloading node '%s' (under root '%s') on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, tree_cq)
        else:
            self._logger.info("Reloading node '%s' on tree with CQ '%s'", node_text_or_data, tree_cq)

        self.ensure_javascript_loaded()
        self._driver.execute_script(script)

    class NodeNotFoundException(Exception):
        """Exception class thrown when we failed to find the specified node"""

        def __init__(self,
                     tree_cq: str,
                     node_text_or_data: Union[str, dict],
                     root_node_text_or_data: Union[str, dict, None] = None,
                     css_query: Union[str, None] = None):
            """Initialises an instance of this exception

            Args:
                tree_cq (str): The CQ used to find the tree
                node_text_or_data (Union[str, dict]): The node text or data that we were looking for
                root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                    Can be an immediate parent, or higher up in the tree.
                css_query (str, optional): The CSS that was queryed for in the node that we were looking for.
            """
            if root_node_text_or_data:
                if css_query:
                    self.message = "Failed to find node with data (or text) '{node_text_or_data}' (under root '{root_node_text_or_data}') with CSS query '{css_query}' on tree with CQ '{tree_cq}'."
                else:
                    self.message = "Failed to find node with data (or text) '{node_text_or_data}' (under root '{root_node_text_or_data}') on tree with CQ '{tree_cq}'."
            else:
                if css_query:
                    self.message = "Failed to find node with data (or text) '{node_text_or_data}' with CSS query '{css_query}' on tree with CQ '{tree_cq}'."
                else:
                    self.message = "Failed to find node with data (or text) '{node_text_or_data}' on tree with CQ '{tree_cq}'."

            self._tree_cq = tree_cq
            self._node_text_or_data = node_text_or_data
            self._root_node_text_or_data = root_node_text_or_data
            self._css_query = css_query

            super().__init__(self.message)

        def __str__(self):
            """Returns a string representation of this exception"""

            if self._root_node_text_or_data:
                return self.message.format(node_text_or_data=self._node_text_or_data, root_node_text_or_data=self._root_node_text_or_data, css_query=self._css_query, tree_cq=self._tree_cq)
            else:
                return self.message.format(node_text_or_data=self._node_text_or_data, css_query=self._css_query, tree_cq=self._tree_cq)

    class TreeNotLoadingExpectation:
        """ An expectation for checking that a tree is not loading."""

        def __init__(self, tree_cq: str, recheck_time_if_false: Union[float, None] = None):
            """Initialises an instance of this class.

            Args:
                tree_cq (str): The CQ used to find the tree
                recheck_time_if_false (float, optional): If we get a value of false (so there is not a call in progress),
                                                         this is the amount of time to wait to check again. Defaults to None.
            """
            self._tree_cq = tree_cq
            self._recheck_time_if_false = recheck_time_if_false

        def __call__(self, driver):
            """Method that determines whether the tree is loading."""
            tree_helper = TreeHelper(driver)

            is_tree_loading = tree_helper.is_tree_loading(self._tree_cq)

            if not is_tree_loading and self._recheck_time_if_false:
                time.sleep(self._recheck_time_if_false)
                is_tree_loading = tree_helper.is_tree_loading(self._tree_cq)

            return not is_tree_loading

    class NodeFoundExpectation:
        """ An expectation for checking that a node has been found"""

        def __init__(self,
                     tree_cq: str,
                     node_text_or_data: Union[str, dict],
                     parent_node_text_or_data: Union[str, dict]):
            """Initialises an instance of this class.

            Args:
                tree_cq (str): The component query to use to find the tree.
                node_text_or_data (Union[str, dict]): The node text or data to find.
                parent_node_text_or_data (Union[str, dict]): The node text or data to use to find the nodes parent,
                                                            for refreshing purposes.
            """
            self._tree_cq = tree_cq
            self._node_text_or_data = node_text_or_data
            self._parent_node_text_or_data = parent_node_text_or_data

        def __call__(self, driver):
            """Method that determines whether a node was found.

            If the node is not found the parent tree node is refreshed and the load waited for.
            """
            tree_helper = TreeHelper(driver)

            node = tree_helper.get_node_icon_element(self._tree_cq, self._node_text_or_data, self._parent_node_text_or_data)
            if node:
                return True

            # Trigger a reload of the parent
            tree_helper.reload_node(self._tree_cq, self._parent_node_text_or_data)
            return False

    class UnexpectedMenuItemsFoundException(Exception):
        """Exception class thrown when we have found unexpected menu items in a context menu"""

        def __init__(self,
                     tree_cq: str,
                     node_text_or_data: Union[str, dict],
                     unexpected_menu_items: list[str],
                     message: str = "Unexpected menu items {unexpected_menu_items} found on context menu, on tree with CQ '{tree_cq}' and node with text or data '{node_text_or_data}'."):
            """Initialises an instance of this exception

            Args:
                tree_cq (str): The component query to use to find the tree.
                node_text_or_data (Union[str, dict]): The node text or data to find.
                unexpected_menu_items (list[str]): A list of menu items that were unexpected.
                message (str, optional): The exception message. Defaults to "Unexpected menu items {unexpected_menu_items} found on context menu, on tree with CQ '{tree_cq}' and node with text or data '{node_text_or_data}'.".
            """
            self.message = message
            self._tree_cq = tree_cq
            self._node_text_or_data = node_text_or_data
            self._unexpected_menu_items = unexpected_menu_items

            super().__init__(self.message)

        def __str__(self):
            """Returns a string representation of this exception"""
            return self.message.format(unexpected_menu_items=self._unexpected_menu_items, tree_cq=self._tree_cq, node_text_or_data=self._node_text_or_data)

    class ExpectedMenuItemsNotFoundException(Exception):
        """Exception class thrown when we have failed to find some expected menu items in a context menu"""

        def __init__(self,
                     tree_cq: str,
                     node_text_or_data: Union[str, dict],
                     missing_menu_items: list[str],
                     message: str = "Expected menu items {missing_menu_items} not found on tree with CQ '{tree_cq}' and node with text or data '{node_text_or_data}'."):
            """Initialises an instance of this exception

            Args:
                tree_cq (str): The component query to use to find the tree.
                node_text_or_data (Union[str, dict]): The node text or data to find.
                missing_menu_items (list[str]): A list of menu items that were expected but not found.
                message (str, optional): The exception message. Defaults to "Expected menu items {missing_menu_items} not found on tree with CQ '{tree_cq}' and node with text or data '{node_text_or_data}'.".
            """
            self.message = message
            self._tree_cq = tree_cq
            self._node_text_or_data = node_text_or_data
            self._missing_menu_items = missing_menu_items

            super().__init__(self.message)

        def __str__(self):
            """Returns a string representation of this exception"""
            return self.message.format(missing_menu_items=self._missing_menu_items, tree_cq=self._tree_cq, node_text_or_data=self._node_text_or_data)

Classes

class TreeHelper (driver: selenium.webdriver.remote.webdriver.WebDriver)

A class to help with using trees, through Ext's interfaces.

Initialises an instance of this class

Args

driver : WebDriver
The webdriver to use
Expand source code
class TreeHelper(HasReferencedJavaScript):
    """A class to help with using trees, through Ext's interfaces."""

    # Class variables
    _IS_TREE_LOADING_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.isTreeLoading('{tree_cq}')"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.isTreeLoading
    Requires the inserts: {tree_cq}"""

    _GET_NODE_ELEMENT_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.getNodeElement('{tree_cq}', {node_text_or_data}, '{css_query}')"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.getNodeElement
    Requires the inserts: {tree_cq}, {node_text_or_data}, {css_query}"""

    _GET_NODE_ELEMENT_WITH_ROOT_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.getNodeElement('{tree_cq}', {node_text_or_data}, '{css_query}', {root_node_text_or_data})"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.getNodeElement including a root node to search from
    Requires the inserts: {tree_cq}, {node_text_or_data}, {css_query}, {root_node_text_or_data}"""

    _RELOAD_NODE_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.reloadNode('{tree_cq}', {node_text_or_data})"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.reloadNode
    Requires the inserts: {tree_cq}, {node_text_or_data}"""

    _RELOAD_NODE_WITH_ROOT_TEMPLATE: str = "return globalThis.PySeExt.TreeHelper.reloadNode('{tree_cq}', {node_text_or_data}, {root_node_text_or_data})"
    """The script template to use to call the JavaScript method PySeExt.TreeHelper.reloadNode including a root node to search from
    Requires the inserts: {tree_cq}, {node_text_or_data}, {root_node_text_or_data}"""

    _ICON_CSS_SELECTOR: str = ".x-tree-icon"
    """The CSS selector to use with get_node_element to find the node icon element.
    """

    _EXPANDER_CSS_SELECTOR: str = ".x-tree-expander"
    """The CSS selector to use with get_node_element to find the node expander element.
    """

    _NODE_TEXT_CSS_SELECTOR: str = ".x-tree-node-text"
    """The CSS selector to use with get_node_element to find the node text element.
    """

    def __init__(self, driver: WebDriver):
        """Initialises an instance of this class

        Args:
            driver (WebDriver): The webdriver to use
        """
        self._logger = logging.getLogger(__name__)
        """The Logger instance for this class instance"""

        self._driver = driver
        """The WebDriver instance for this class instance"""

        self._action_chains = ActionChains(driver)
        """The ActionChains instance for this class instance"""

        self._core = Core(driver)
        """The `Core` instance for this class instance"""

        self._menu_helper = MenuHelper(driver)
        """The `MenuHelper` instance for this class instance"""

        # Initialise our base class
        super().__init__(driver, self._logger)

    def is_tree_loading(self, tree_cq: str):
        """Determine whether the tree (any part of it) is currently loading.

        You should call this before calling any tree interaction methods,
        since we cannot pass things back in callbacks!

        Args:
            tree_cq (str): The component query to use to find the tree.

        Returns:
            bool: True if the tree is loaded, False otherwise.
        """
        script = self._IS_TREE_LOADING_TEMPLATE.format(tree_cq=tree_cq)
        self.ensure_javascript_loaded()
        return self._driver.execute_script(script)

    def wait_until_tree_not_loading(self,
                                    tree_cq: str,
                                    timeout: float = 30,
                                    poll_frequecy: float = 0.2,
                                    recheck_time_if_false: float = 0.2):
        """Waits until the tree identified by the component query is not loading,
        or the timeout is hit

        Args:
            tree_cq (str): The component query for the tree.
            timeout (float, optional): The number of seconds to wait before erroring. Defaults to 30.
            poll_frequency (float, optional): Number of seconds to poll. Defaults to 0.2.
            recheck_time_if_false (float, optional): If we get a result such that no Ajax calls are in progress, this is the amount of time to wait to check again. Defaults to 0.2.
        """
        WebDriverWait(self._driver, timeout, poll_frequency = poll_frequecy).until(TreeHelper.TreeNotLoadingExpectation(tree_cq, recheck_time_if_false))

    def get_node_icon_element(self,
                              tree_cq: str,
                              node_text_or_data: Union[str, dict],
                              root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
        """Finds a node by text or data, then the child HTML element that holds it's icon.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.

        Returns:
            WebElement: The DOM element for the node icon.
        """
        return self.get_node_element(tree_cq, node_text_or_data, self._ICON_CSS_SELECTOR, root_node_text_or_data)

    def get_node_text_element(self,
                              tree_cq: str,
                              node_text_or_data: Union[str, dict],
                              root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
        """Finds a node by text or data, then the child HTML element that holds it's text.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.

        Returns:
            WebElement: The DOM element for the node text.
        """
        return self.get_node_element(tree_cq, node_text_or_data, self._NODE_TEXT_CSS_SELECTOR, root_node_text_or_data)

    def get_node_expander_element(self,
                                  tree_cq: str,
                                  node_text_or_data: Union[str, dict],
                                  root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
        """Finds a node by text or data, then the child HTML element that holds it's expander UI element.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.

        Returns:
            WebElement: The DOM element for the node's expander.
        """
        return self.get_node_element(tree_cq, node_text_or_data, self._EXPANDER_CSS_SELECTOR, root_node_text_or_data)

    def get_node_element(self,
                         tree_cq: str,
                         node_text_or_data: Union[str, dict],
                         css_query: str,
                         root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
        """Finds a node by text or data, then a child element by CSS query.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            css_query (str): The CSS to query for in the found node row element.
                             Some expected ones:
                                Expander UI element = '.x-tree-expander'
                                Node icon = '.x-tree-icon'
                                Node text = '.x-tree-node-text'
                             If need those you'd use one of the other methods though.
                             This is in case need to click on another part of the node's row.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.

        Returns:
            WebElement: The DOM element for the node's expander.
        """
        self.wait_until_tree_not_loading(tree_cq)

        if isinstance(node_text_or_data, str):
            node_text_or_data = f"'{node_text_or_data}'"

        if isinstance(root_node_text_or_data, str):
            root_node_text_or_data = f"'{root_node_text_or_data}'"

        if root_node_text_or_data:
            script = self._GET_NODE_ELEMENT_WITH_ROOT_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data, css_query=css_query, root_node_text_or_data=root_node_text_or_data)
        else:
            script = self._GET_NODE_ELEMENT_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data, css_query=css_query)

        self.ensure_javascript_loaded()
        return self._driver.execute_script(script)

    def open_node_context_menu(self,
                               tree_cq: str,
                               node_text_or_data: Union[str, dict],
                               root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node's icon element by text or data, then right clicks on it.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        self.context_click_node_element(tree_cq, node_text_or_data, self._ICON_CSS_SELECTOR, root_node_text_or_data)

    def check_node_context_menu_contains_items(self,
                                               tree_cq: str,
                                               node_text_or_data: Union[str, dict],
                                               expected_items: list[str],
                                               check_contains_no_additional_items: bool = True,
                                               ignore_spacers: bool = False,
                                               root_node_text_or_data: Union[str, dict, None] = None):
        """Opens the context menu for a node, and checks that it contains the specified items.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            expected_items (list[str]): A list of menu items (text) that we're expecting to find.
                                        Spacers have text containing of a single space (MenuHelper.SPACER_TEXT_CONTEXT), but can be ignored using ignore_spacers if desired.
                                        e.g. ['Add', 'Edit', 'Delete', MenuHelper.SPACER_TEXT_CONTEXT, 'Refresh']
            check_contains_no_additional_items (bool, optional): Indicates whether we should check that there are no additional items in the menu.
                                                                 Defaults to True.
            ignore_spacers (bool, optional): Indicates whether we should include spacers in our checks. Defaults to False.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        self.open_node_context_menu(tree_cq, node_text_or_data, root_node_text_or_data)

        unexpected_items: list[str] = []

        # Get all menu items
        menu_items = self._menu_helper.get_enabled_menu_items()

        # Each element has a text member we can compare.
        for item in menu_items:
            if ignore_spacers is True and item.text == MenuHelper.SPACER_TEXT_CONTEXT:
                continue

            if not item.text in expected_items:
                # Item not expected
                if check_contains_no_additional_items:
                    unexpected_items.append(item.text)
            else:
                # Item expected and found, so remove from expected list.
                expected_items.remove(item.text)

                # If have nothing left to find, and we're not checking for unexpected items, then can break from our loop.
                if not check_contains_no_additional_items and len(expected_items) == 0:
                    break

        # Is there anything left in our expected item list?
        if len(expected_items) > 0:
            raise TreeHelper.ExpectedMenuItemsNotFoundException(tree_cq,
                                                                node_text_or_data,
                                                                expected_items)

        # Was there anything in the menu that was not expected, and we were checking?
        if len(unexpected_items) > 0:
            raise TreeHelper.UnexpectedMenuItemsFoundException(tree_cq,
                                                               node_text_or_data,
                                                               unexpected_items)


    def click_node_expander(self,
                            tree_cq: str,
                            node_text_or_data: Union[str, dict],
                            root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node's expander element by text or data, then clicks on it.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        node = self.get_node_expander_element(tree_cq, node_text_or_data, root_node_text_or_data)

        if node:
            if root_node_text_or_data:
                self._logger.info("Clicking expander on node '%s' (under root '%s') on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, tree_cq)
            else:
                self._logger.info("Clicking expander on node '%s' on tree with CQ '%s'", node_text_or_data, tree_cq)

            self._action_chains.move_to_element(node)
            self._action_chains.click(node)
            self._action_chains.perform()
        else:
            raise TreeHelper.NodeNotFoundException(tree_cq, node_text_or_data, root_node_text_or_data)

    def click_node_element(self,
                           tree_cq: str,
                           node_text_or_data: Union[str, dict],
                           css_query: str,
                           root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node by text or data, then a child element by CSS query, then clicks on it.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            css_query (str): The CSS to query for in the found node row element.
                             Some expected ones:
                                Expander UI element = '.x-tree-expander'
                                Node icon = '.x-tree-icon'
                                Node text = '.x-tree-node-text'
                             If need those you'd use one of the other methods though.
                             This is in case need to click on another part of the node's row.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        node = self.get_node_element(tree_cq, node_text_or_data, css_query, root_node_text_or_data)

        if node:
            if root_node_text_or_data:
                self._logger.info("Clicking on node '%s' (under root '%s'), with CSS query '%s' on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, css_query, tree_cq)
            else:
                self._logger.info("Clicking on node '%s' with CSS query '%s' on tree with CQ '%s'", node_text_or_data, css_query, tree_cq)

            self._action_chains.move_to_element(node)
            self._action_chains.click(node)
            self._action_chains.perform()
        else:
            raise TreeHelper.NodeNotFoundException(tree_cq, node_text_or_data, root_node_text_or_data)

    def context_click_node_element(self,
                                   tree_cq: str,
                                   node_text_or_data: Union[str, dict],
                                   css_query: str,
                                   root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node by text or data, then a child element by CSS query, then right clicks on it.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            css_query (str): The CSS to query for in the found node row element.
                             Some expected ones:
                                Expander UI element = '.x-tree-expander'
                                Node icon = '.x-tree-icon'
                                Node text = '.x-tree-node-text'
                             If need those you'd use one of the other methods though.
                             This is in case need to click on another part of the node's row.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        node = self.get_node_element(tree_cq, node_text_or_data, css_query, root_node_text_or_data)

        if node:
            if root_node_text_or_data:
                self._logger.info("Right clicking on node '%s' (under root '%s'), with CSS query '%s' on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, css_query, tree_cq)
            else:
                self._logger.info("Right clicking on node '%s' with CSS query '%s' on tree with CQ '%s'", node_text_or_data, css_query, tree_cq)

            self._action_chains.move_to_element(node)
            self._action_chains.context_click(node)
            self._action_chains.perform()
        else:
            raise TreeHelper.NodeNotFoundException(tree_cq, node_text_or_data, root_node_text_or_data)

    def wait_for_tree_node(self,
                           tree_cq: str,
                           node_text_or_data: Union[str, dict],
                           parent_node_text_or_data: Union[str, dict],
                           timeout: float = 60) -> WebElement:
        """Method that waits until a tree node is available, refreshing the parent until it's
        found or the timeout is hit.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            parent_node_text_or_data (Union[str, dict]): The node text or data to use to find the nodes parent,
                                                         for refreshing purposes.
            timeout (int, optional): The number of seconds to wait for the row before erroring. Defaults to 60.

        Returns:
            WebElement: The DOM element for the node icon.
        """
        WebDriverWait(self._driver, timeout).until(TreeHelper.NodeFoundExpectation(tree_cq, node_text_or_data, parent_node_text_or_data))
        return self.get_node_icon_element(tree_cq, node_text_or_data)

    def reload_node(self,
                    tree_cq: str,
                    node_text_or_data: Union[str, dict],
                    root_node_text_or_data: Union[str, dict, None] = None):
        """Finds a node by text or data, and triggers a reload on it, and its children.

        Args:
            tree_cq (str): The component query to use to find the tree.
            node_text_or_data (Union[str, dict]): The node text or data to find.
            root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                 Can be an immediate parent, or higher up in the tree.
        """
        self.wait_until_tree_not_loading(tree_cq)

        if isinstance(node_text_or_data, str):
            node_text_or_data = f"'{node_text_or_data}'"

        if isinstance(root_node_text_or_data, str):
            root_node_text_or_data = f"'{root_node_text_or_data}'"

        if root_node_text_or_data:
            script = self._RELOAD_NODE_WITH_ROOT_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data, root_node_text_or_data=root_node_text_or_data)
        else:
            script = self._RELOAD_NODE_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data)

        if root_node_text_or_data:
            self._logger.info("Reloading node '%s' (under root '%s') on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, tree_cq)
        else:
            self._logger.info("Reloading node '%s' on tree with CQ '%s'", node_text_or_data, tree_cq)

        self.ensure_javascript_loaded()
        self._driver.execute_script(script)

    class NodeNotFoundException(Exception):
        """Exception class thrown when we failed to find the specified node"""

        def __init__(self,
                     tree_cq: str,
                     node_text_or_data: Union[str, dict],
                     root_node_text_or_data: Union[str, dict, None] = None,
                     css_query: Union[str, None] = None):
            """Initialises an instance of this exception

            Args:
                tree_cq (str): The CQ used to find the tree
                node_text_or_data (Union[str, dict]): The node text or data that we were looking for
                root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                                    Can be an immediate parent, or higher up in the tree.
                css_query (str, optional): The CSS that was queryed for in the node that we were looking for.
            """
            if root_node_text_or_data:
                if css_query:
                    self.message = "Failed to find node with data (or text) '{node_text_or_data}' (under root '{root_node_text_or_data}') with CSS query '{css_query}' on tree with CQ '{tree_cq}'."
                else:
                    self.message = "Failed to find node with data (or text) '{node_text_or_data}' (under root '{root_node_text_or_data}') on tree with CQ '{tree_cq}'."
            else:
                if css_query:
                    self.message = "Failed to find node with data (or text) '{node_text_or_data}' with CSS query '{css_query}' on tree with CQ '{tree_cq}'."
                else:
                    self.message = "Failed to find node with data (or text) '{node_text_or_data}' on tree with CQ '{tree_cq}'."

            self._tree_cq = tree_cq
            self._node_text_or_data = node_text_or_data
            self._root_node_text_or_data = root_node_text_or_data
            self._css_query = css_query

            super().__init__(self.message)

        def __str__(self):
            """Returns a string representation of this exception"""

            if self._root_node_text_or_data:
                return self.message.format(node_text_or_data=self._node_text_or_data, root_node_text_or_data=self._root_node_text_or_data, css_query=self._css_query, tree_cq=self._tree_cq)
            else:
                return self.message.format(node_text_or_data=self._node_text_or_data, css_query=self._css_query, tree_cq=self._tree_cq)

    class TreeNotLoadingExpectation:
        """ An expectation for checking that a tree is not loading."""

        def __init__(self, tree_cq: str, recheck_time_if_false: Union[float, None] = None):
            """Initialises an instance of this class.

            Args:
                tree_cq (str): The CQ used to find the tree
                recheck_time_if_false (float, optional): If we get a value of false (so there is not a call in progress),
                                                         this is the amount of time to wait to check again. Defaults to None.
            """
            self._tree_cq = tree_cq
            self._recheck_time_if_false = recheck_time_if_false

        def __call__(self, driver):
            """Method that determines whether the tree is loading."""
            tree_helper = TreeHelper(driver)

            is_tree_loading = tree_helper.is_tree_loading(self._tree_cq)

            if not is_tree_loading and self._recheck_time_if_false:
                time.sleep(self._recheck_time_if_false)
                is_tree_loading = tree_helper.is_tree_loading(self._tree_cq)

            return not is_tree_loading

    class NodeFoundExpectation:
        """ An expectation for checking that a node has been found"""

        def __init__(self,
                     tree_cq: str,
                     node_text_or_data: Union[str, dict],
                     parent_node_text_or_data: Union[str, dict]):
            """Initialises an instance of this class.

            Args:
                tree_cq (str): The component query to use to find the tree.
                node_text_or_data (Union[str, dict]): The node text or data to find.
                parent_node_text_or_data (Union[str, dict]): The node text or data to use to find the nodes parent,
                                                            for refreshing purposes.
            """
            self._tree_cq = tree_cq
            self._node_text_or_data = node_text_or_data
            self._parent_node_text_or_data = parent_node_text_or_data

        def __call__(self, driver):
            """Method that determines whether a node was found.

            If the node is not found the parent tree node is refreshed and the load waited for.
            """
            tree_helper = TreeHelper(driver)

            node = tree_helper.get_node_icon_element(self._tree_cq, self._node_text_or_data, self._parent_node_text_or_data)
            if node:
                return True

            # Trigger a reload of the parent
            tree_helper.reload_node(self._tree_cq, self._parent_node_text_or_data)
            return False

    class UnexpectedMenuItemsFoundException(Exception):
        """Exception class thrown when we have found unexpected menu items in a context menu"""

        def __init__(self,
                     tree_cq: str,
                     node_text_or_data: Union[str, dict],
                     unexpected_menu_items: list[str],
                     message: str = "Unexpected menu items {unexpected_menu_items} found on context menu, on tree with CQ '{tree_cq}' and node with text or data '{node_text_or_data}'."):
            """Initialises an instance of this exception

            Args:
                tree_cq (str): The component query to use to find the tree.
                node_text_or_data (Union[str, dict]): The node text or data to find.
                unexpected_menu_items (list[str]): A list of menu items that were unexpected.
                message (str, optional): The exception message. Defaults to "Unexpected menu items {unexpected_menu_items} found on context menu, on tree with CQ '{tree_cq}' and node with text or data '{node_text_or_data}'.".
            """
            self.message = message
            self._tree_cq = tree_cq
            self._node_text_or_data = node_text_or_data
            self._unexpected_menu_items = unexpected_menu_items

            super().__init__(self.message)

        def __str__(self):
            """Returns a string representation of this exception"""
            return self.message.format(unexpected_menu_items=self._unexpected_menu_items, tree_cq=self._tree_cq, node_text_or_data=self._node_text_or_data)

    class ExpectedMenuItemsNotFoundException(Exception):
        """Exception class thrown when we have failed to find some expected menu items in a context menu"""

        def __init__(self,
                     tree_cq: str,
                     node_text_or_data: Union[str, dict],
                     missing_menu_items: list[str],
                     message: str = "Expected menu items {missing_menu_items} not found on tree with CQ '{tree_cq}' and node with text or data '{node_text_or_data}'."):
            """Initialises an instance of this exception

            Args:
                tree_cq (str): The component query to use to find the tree.
                node_text_or_data (Union[str, dict]): The node text or data to find.
                missing_menu_items (list[str]): A list of menu items that were expected but not found.
                message (str, optional): The exception message. Defaults to "Expected menu items {missing_menu_items} not found on tree with CQ '{tree_cq}' and node with text or data '{node_text_or_data}'.".
            """
            self.message = message
            self._tree_cq = tree_cq
            self._node_text_or_data = node_text_or_data
            self._missing_menu_items = missing_menu_items

            super().__init__(self.message)

        def __str__(self):
            """Returns a string representation of this exception"""
            return self.message.format(missing_menu_items=self._missing_menu_items, tree_cq=self._tree_cq, node_text_or_data=self._node_text_or_data)

Ancestors

Class variables

var ExpectedMenuItemsNotFoundException

Exception class thrown when we have failed to find some expected menu items in a context menu

var NodeFoundExpectation

An expectation for checking that a node has been found

var NodeNotFoundException

Exception class thrown when we failed to find the specified node

var TreeNotLoadingExpectation

An expectation for checking that a tree is not loading.

var UnexpectedMenuItemsFoundException

Exception class thrown when we have found unexpected menu items in a context menu

Methods

def check_node_context_menu_contains_items(self, tree_cq: str, node_text_or_data: Union[str, dict], expected_items: list[str], check_contains_no_additional_items: bool = True, ignore_spacers: bool = False, root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None)

Opens the context menu for a node, and checks that it contains the specified items.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
expected_items : list[str]
A list of menu items (text) that we're expecting to find. Spacers have text containing of a single space (MenuHelper.SPACER_TEXT_CONTEXT), but can be ignored using ignore_spacers if desired. e.g. ['Add', 'Edit', 'Delete', MenuHelper.SPACER_TEXT_CONTEXT, 'Refresh']
check_contains_no_additional_items : bool, optional
Indicates whether we should check that there are no additional items in the menu. Defaults to True.
ignore_spacers : bool, optional
Indicates whether we should include spacers in our checks. Defaults to False.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.
Expand source code
def check_node_context_menu_contains_items(self,
                                           tree_cq: str,
                                           node_text_or_data: Union[str, dict],
                                           expected_items: list[str],
                                           check_contains_no_additional_items: bool = True,
                                           ignore_spacers: bool = False,
                                           root_node_text_or_data: Union[str, dict, None] = None):
    """Opens the context menu for a node, and checks that it contains the specified items.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        expected_items (list[str]): A list of menu items (text) that we're expecting to find.
                                    Spacers have text containing of a single space (MenuHelper.SPACER_TEXT_CONTEXT), but can be ignored using ignore_spacers if desired.
                                    e.g. ['Add', 'Edit', 'Delete', MenuHelper.SPACER_TEXT_CONTEXT, 'Refresh']
        check_contains_no_additional_items (bool, optional): Indicates whether we should check that there are no additional items in the menu.
                                                             Defaults to True.
        ignore_spacers (bool, optional): Indicates whether we should include spacers in our checks. Defaults to False.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.
    """
    self.open_node_context_menu(tree_cq, node_text_or_data, root_node_text_or_data)

    unexpected_items: list[str] = []

    # Get all menu items
    menu_items = self._menu_helper.get_enabled_menu_items()

    # Each element has a text member we can compare.
    for item in menu_items:
        if ignore_spacers is True and item.text == MenuHelper.SPACER_TEXT_CONTEXT:
            continue

        if not item.text in expected_items:
            # Item not expected
            if check_contains_no_additional_items:
                unexpected_items.append(item.text)
        else:
            # Item expected and found, so remove from expected list.
            expected_items.remove(item.text)

            # If have nothing left to find, and we're not checking for unexpected items, then can break from our loop.
            if not check_contains_no_additional_items and len(expected_items) == 0:
                break

    # Is there anything left in our expected item list?
    if len(expected_items) > 0:
        raise TreeHelper.ExpectedMenuItemsNotFoundException(tree_cq,
                                                            node_text_or_data,
                                                            expected_items)

    # Was there anything in the menu that was not expected, and we were checking?
    if len(unexpected_items) > 0:
        raise TreeHelper.UnexpectedMenuItemsFoundException(tree_cq,
                                                           node_text_or_data,
                                                           unexpected_items)
def click_node_element(self, tree_cq: str, node_text_or_data: Union[str, dict], css_query: str, root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None)

Finds a node by text or data, then a child element by CSS query, then clicks on it.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
css_query : str
The CSS to query for in the found node row element. Some expected ones: Expander UI element = '.x-tree-expander' Node icon = '.x-tree-icon' Node text = '.x-tree-node-text' If need those you'd use one of the other methods though. This is in case need to click on another part of the node's row.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.
Expand source code
def click_node_element(self,
                       tree_cq: str,
                       node_text_or_data: Union[str, dict],
                       css_query: str,
                       root_node_text_or_data: Union[str, dict, None] = None):
    """Finds a node by text or data, then a child element by CSS query, then clicks on it.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        css_query (str): The CSS to query for in the found node row element.
                         Some expected ones:
                            Expander UI element = '.x-tree-expander'
                            Node icon = '.x-tree-icon'
                            Node text = '.x-tree-node-text'
                         If need those you'd use one of the other methods though.
                         This is in case need to click on another part of the node's row.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.
    """
    node = self.get_node_element(tree_cq, node_text_or_data, css_query, root_node_text_or_data)

    if node:
        if root_node_text_or_data:
            self._logger.info("Clicking on node '%s' (under root '%s'), with CSS query '%s' on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, css_query, tree_cq)
        else:
            self._logger.info("Clicking on node '%s' with CSS query '%s' on tree with CQ '%s'", node_text_or_data, css_query, tree_cq)

        self._action_chains.move_to_element(node)
        self._action_chains.click(node)
        self._action_chains.perform()
    else:
        raise TreeHelper.NodeNotFoundException(tree_cq, node_text_or_data, root_node_text_or_data)
def click_node_expander(self, tree_cq: str, node_text_or_data: Union[str, dict], root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None)

Finds a node's expander element by text or data, then clicks on it.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.
Expand source code
def click_node_expander(self,
                        tree_cq: str,
                        node_text_or_data: Union[str, dict],
                        root_node_text_or_data: Union[str, dict, None] = None):
    """Finds a node's expander element by text or data, then clicks on it.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.
    """
    node = self.get_node_expander_element(tree_cq, node_text_or_data, root_node_text_or_data)

    if node:
        if root_node_text_or_data:
            self._logger.info("Clicking expander on node '%s' (under root '%s') on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, tree_cq)
        else:
            self._logger.info("Clicking expander on node '%s' on tree with CQ '%s'", node_text_or_data, tree_cq)

        self._action_chains.move_to_element(node)
        self._action_chains.click(node)
        self._action_chains.perform()
    else:
        raise TreeHelper.NodeNotFoundException(tree_cq, node_text_or_data, root_node_text_or_data)
def context_click_node_element(self, tree_cq: str, node_text_or_data: Union[str, dict], css_query: str, root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None)

Finds a node by text or data, then a child element by CSS query, then right clicks on it.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
css_query : str
The CSS to query for in the found node row element. Some expected ones: Expander UI element = '.x-tree-expander' Node icon = '.x-tree-icon' Node text = '.x-tree-node-text' If need those you'd use one of the other methods though. This is in case need to click on another part of the node's row.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.
Expand source code
def context_click_node_element(self,
                               tree_cq: str,
                               node_text_or_data: Union[str, dict],
                               css_query: str,
                               root_node_text_or_data: Union[str, dict, None] = None):
    """Finds a node by text or data, then a child element by CSS query, then right clicks on it.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        css_query (str): The CSS to query for in the found node row element.
                         Some expected ones:
                            Expander UI element = '.x-tree-expander'
                            Node icon = '.x-tree-icon'
                            Node text = '.x-tree-node-text'
                         If need those you'd use one of the other methods though.
                         This is in case need to click on another part of the node's row.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.
    """
    node = self.get_node_element(tree_cq, node_text_or_data, css_query, root_node_text_or_data)

    if node:
        if root_node_text_or_data:
            self._logger.info("Right clicking on node '%s' (under root '%s'), with CSS query '%s' on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, css_query, tree_cq)
        else:
            self._logger.info("Right clicking on node '%s' with CSS query '%s' on tree with CQ '%s'", node_text_or_data, css_query, tree_cq)

        self._action_chains.move_to_element(node)
        self._action_chains.context_click(node)
        self._action_chains.perform()
    else:
        raise TreeHelper.NodeNotFoundException(tree_cq, node_text_or_data, root_node_text_or_data)
def get_node_element(self, tree_cq: str, node_text_or_data: Union[str, dict], css_query: str, root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None) ‑> selenium.webdriver.remote.webelement.WebElement

Finds a node by text or data, then a child element by CSS query.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
css_query : str
The CSS to query for in the found node row element. Some expected ones: Expander UI element = '.x-tree-expander' Node icon = '.x-tree-icon' Node text = '.x-tree-node-text' If need those you'd use one of the other methods though. This is in case need to click on another part of the node's row.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.

Returns

WebElement
The DOM element for the node's expander.
Expand source code
def get_node_element(self,
                     tree_cq: str,
                     node_text_or_data: Union[str, dict],
                     css_query: str,
                     root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
    """Finds a node by text or data, then a child element by CSS query.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        css_query (str): The CSS to query for in the found node row element.
                         Some expected ones:
                            Expander UI element = '.x-tree-expander'
                            Node icon = '.x-tree-icon'
                            Node text = '.x-tree-node-text'
                         If need those you'd use one of the other methods though.
                         This is in case need to click on another part of the node's row.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.

    Returns:
        WebElement: The DOM element for the node's expander.
    """
    self.wait_until_tree_not_loading(tree_cq)

    if isinstance(node_text_or_data, str):
        node_text_or_data = f"'{node_text_or_data}'"

    if isinstance(root_node_text_or_data, str):
        root_node_text_or_data = f"'{root_node_text_or_data}'"

    if root_node_text_or_data:
        script = self._GET_NODE_ELEMENT_WITH_ROOT_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data, css_query=css_query, root_node_text_or_data=root_node_text_or_data)
    else:
        script = self._GET_NODE_ELEMENT_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data, css_query=css_query)

    self.ensure_javascript_loaded()
    return self._driver.execute_script(script)
def get_node_expander_element(self, tree_cq: str, node_text_or_data: Union[str, dict], root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None) ‑> selenium.webdriver.remote.webelement.WebElement

Finds a node by text or data, then the child HTML element that holds it's expander UI element.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.

Returns

WebElement
The DOM element for the node's expander.
Expand source code
def get_node_expander_element(self,
                              tree_cq: str,
                              node_text_or_data: Union[str, dict],
                              root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
    """Finds a node by text or data, then the child HTML element that holds it's expander UI element.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.

    Returns:
        WebElement: The DOM element for the node's expander.
    """
    return self.get_node_element(tree_cq, node_text_or_data, self._EXPANDER_CSS_SELECTOR, root_node_text_or_data)
def get_node_icon_element(self, tree_cq: str, node_text_or_data: Union[str, dict], root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None) ‑> selenium.webdriver.remote.webelement.WebElement

Finds a node by text or data, then the child HTML element that holds it's icon.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.

Returns

WebElement
The DOM element for the node icon.
Expand source code
def get_node_icon_element(self,
                          tree_cq: str,
                          node_text_or_data: Union[str, dict],
                          root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
    """Finds a node by text or data, then the child HTML element that holds it's icon.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.

    Returns:
        WebElement: The DOM element for the node icon.
    """
    return self.get_node_element(tree_cq, node_text_or_data, self._ICON_CSS_SELECTOR, root_node_text_or_data)
def get_node_text_element(self, tree_cq: str, node_text_or_data: Union[str, dict], root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None) ‑> selenium.webdriver.remote.webelement.WebElement

Finds a node by text or data, then the child HTML element that holds it's text.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.

Returns

WebElement
The DOM element for the node text.
Expand source code
def get_node_text_element(self,
                          tree_cq: str,
                          node_text_or_data: Union[str, dict],
                          root_node_text_or_data: Union[str, dict, None] = None) -> WebElement:
    """Finds a node by text or data, then the child HTML element that holds it's text.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.

    Returns:
        WebElement: The DOM element for the node text.
    """
    return self.get_node_element(tree_cq, node_text_or_data, self._NODE_TEXT_CSS_SELECTOR, root_node_text_or_data)
def is_tree_loading(self, tree_cq: str)

Determine whether the tree (any part of it) is currently loading.

You should call this before calling any tree interaction methods, since we cannot pass things back in callbacks!

Args

tree_cq : str
The component query to use to find the tree.

Returns

bool
True if the tree is loaded, False otherwise.
Expand source code
def is_tree_loading(self, tree_cq: str):
    """Determine whether the tree (any part of it) is currently loading.

    You should call this before calling any tree interaction methods,
    since we cannot pass things back in callbacks!

    Args:
        tree_cq (str): The component query to use to find the tree.

    Returns:
        bool: True if the tree is loaded, False otherwise.
    """
    script = self._IS_TREE_LOADING_TEMPLATE.format(tree_cq=tree_cq)
    self.ensure_javascript_loaded()
    return self._driver.execute_script(script)
def open_node_context_menu(self, tree_cq: str, node_text_or_data: Union[str, dict], root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None)

Finds a node's icon element by text or data, then right clicks on it.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.
Expand source code
def open_node_context_menu(self,
                           tree_cq: str,
                           node_text_or_data: Union[str, dict],
                           root_node_text_or_data: Union[str, dict, None] = None):
    """Finds a node's icon element by text or data, then right clicks on it.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.
    """
    self.context_click_node_element(tree_cq, node_text_or_data, self._ICON_CSS_SELECTOR, root_node_text_or_data)
def reload_node(self, tree_cq: str, node_text_or_data: Union[str, dict], root_node_text_or_data: Union[str, dict, ForwardRef(None)] = None)

Finds a node by text or data, and triggers a reload on it, and its children.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
root_node_text_or_data : Union[str, dict], optional
The text or data indicating the root node under which to search for the node. Can be an immediate parent, or higher up in the tree.
Expand source code
def reload_node(self,
                tree_cq: str,
                node_text_or_data: Union[str, dict],
                root_node_text_or_data: Union[str, dict, None] = None):
    """Finds a node by text or data, and triggers a reload on it, and its children.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        root_node_text_or_data (Union[str, dict], optional): The text or data indicating the root node under which to search for the node.
                                                             Can be an immediate parent, or higher up in the tree.
    """
    self.wait_until_tree_not_loading(tree_cq)

    if isinstance(node_text_or_data, str):
        node_text_or_data = f"'{node_text_or_data}'"

    if isinstance(root_node_text_or_data, str):
        root_node_text_or_data = f"'{root_node_text_or_data}'"

    if root_node_text_or_data:
        script = self._RELOAD_NODE_WITH_ROOT_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data, root_node_text_or_data=root_node_text_or_data)
    else:
        script = self._RELOAD_NODE_TEMPLATE.format(tree_cq=tree_cq, node_text_or_data=node_text_or_data)

    if root_node_text_or_data:
        self._logger.info("Reloading node '%s' (under root '%s') on tree with CQ '%s'", node_text_or_data, root_node_text_or_data, tree_cq)
    else:
        self._logger.info("Reloading node '%s' on tree with CQ '%s'", node_text_or_data, tree_cq)

    self.ensure_javascript_loaded()
    self._driver.execute_script(script)
def wait_for_tree_node(self, tree_cq: str, node_text_or_data: Union[str, dict], parent_node_text_or_data: Union[str, dict], timeout: float = 60) ‑> selenium.webdriver.remote.webelement.WebElement

Method that waits until a tree node is available, refreshing the parent until it's found or the timeout is hit.

Args

tree_cq : str
The component query to use to find the tree.
node_text_or_data : Union[str, dict]
The node text or data to find.
parent_node_text_or_data : Union[str, dict]
The node text or data to use to find the nodes parent, for refreshing purposes.
timeout : int, optional
The number of seconds to wait for the row before erroring. Defaults to 60.

Returns

WebElement
The DOM element for the node icon.
Expand source code
def wait_for_tree_node(self,
                       tree_cq: str,
                       node_text_or_data: Union[str, dict],
                       parent_node_text_or_data: Union[str, dict],
                       timeout: float = 60) -> WebElement:
    """Method that waits until a tree node is available, refreshing the parent until it's
    found or the timeout is hit.

    Args:
        tree_cq (str): The component query to use to find the tree.
        node_text_or_data (Union[str, dict]): The node text or data to find.
        parent_node_text_or_data (Union[str, dict]): The node text or data to use to find the nodes parent,
                                                     for refreshing purposes.
        timeout (int, optional): The number of seconds to wait for the row before erroring. Defaults to 60.

    Returns:
        WebElement: The DOM element for the node icon.
    """
    WebDriverWait(self._driver, timeout).until(TreeHelper.NodeFoundExpectation(tree_cq, node_text_or_data, parent_node_text_or_data))
    return self.get_node_icon_element(tree_cq, node_text_or_data)
def wait_until_tree_not_loading(self, tree_cq: str, timeout: float = 30, poll_frequecy: float = 0.2, recheck_time_if_false: float = 0.2)

Waits until the tree identified by the component query is not loading, or the timeout is hit

Args

tree_cq : str
The component query for the tree.
timeout : float, optional
The number of seconds to wait before erroring. Defaults to 30.
poll_frequency : float, optional
Number of seconds to poll. Defaults to 0.2.
recheck_time_if_false : float, optional
If we get a result such that no Ajax calls are in progress, this is the amount of time to wait to check again. Defaults to 0.2.
Expand source code
def wait_until_tree_not_loading(self,
                                tree_cq: str,
                                timeout: float = 30,
                                poll_frequecy: float = 0.2,
                                recheck_time_if_false: float = 0.2):
    """Waits until the tree identified by the component query is not loading,
    or the timeout is hit

    Args:
        tree_cq (str): The component query for the tree.
        timeout (float, optional): The number of seconds to wait before erroring. Defaults to 30.
        poll_frequency (float, optional): Number of seconds to poll. Defaults to 0.2.
        recheck_time_if_false (float, optional): If we get a result such that no Ajax calls are in progress, this is the amount of time to wait to check again. Defaults to 0.2.
    """
    WebDriverWait(self._driver, timeout, poll_frequency = poll_frequecy).until(TreeHelper.TreeNotLoadingExpectation(tree_cq, recheck_time_if_false))

Inherited members