From 3a90dd401b381821e9c90b5674c9a7cdca998f57 Mon Sep 17 00:00:00 2001 From: Garth Gross Date: Mon, 17 Nov 2025 15:57:06 -0800 Subject: [PATCH] 8.3.2 Initial Commit --- Components/BasicComponent.py | 407 +++++++++++---- Components/Common/Button.py | 22 +- .../Common/Dropdown.py | 28 +- Components/Common/FileUpload.py | 17 +- .../Common/Icon.py | 20 +- Components/Common/TextInput.py | 30 +- .../PerspectiveComponents/Charts/Pie.py | 28 +- .../Charts/PowerChart.py | 270 ++++++---- .../Charts/TimeSeriesChart.py | 24 +- .../PerspectiveComponents/Charts/XYChart.py | 22 +- .../PerspectiveComponents/Common/Checkbox.py | 30 +- .../Common/ComponentModal.py | 4 +- .../Common/DateRangeSelector.py | 20 +- .../Common/DateTimePicker.py | 62 ++- .../PerspectiveComponents/Common/Table.py | 31 +- .../Common/TablePieces/Body.py | 31 +- .../Common/TablePieces/Filter.py | 18 +- .../Common/TablePieces/HeaderAndFooter.py | 25 +- .../Common/TablePieces/Pager.py | 58 ++- .../Common/TagBrowseTree.py | 34 +- .../PerspectiveComponents/Common/Tree.py | 370 +++++++++----- .../Containers/Column.py | 27 +- .../PerspectiveComponents/Containers/Split.py | 20 +- .../PerspectiveComponents/Containers/Tab.py | 20 +- .../Displays/AlarmTable.py | 233 +++++---- .../PerspectiveComponents/Displays/Audio.py | 12 +- .../PerspectiveComponents/Displays/Barcode.py | 20 +- .../Displays/CylindricalTank.py | 22 +- .../Displays/Dashboard.py | 38 +- .../Displays/EquipmentSchedule.py | 42 +- .../Displays/GoogleMap.py | 124 ++--- .../PerspectiveComponents/Displays/Icon.py | 20 +- .../PerspectiveComponents/Displays/Image.py | 14 +- .../Displays/InlineFrame.py | 16 +- .../Displays/LEDDisplay.py | 30 +- .../PerspectiveComponents/Displays/Label.py | 16 +- .../Displays/LinearScale.py | 19 +- .../PerspectiveComponents/Displays/Map.py | 20 +- .../Displays/Markdown.py | 16 +- .../Displays/Progress.py | 16 +- .../PerspectiveComponents/Displays/Table.py | 307 +++++++++--- .../Displays/TagBrowseTree.py | 79 ++- .../Displays/Thermometer.py | 62 +-- .../PerspectiveComponents/Displays/Tree.py | 59 +-- .../Displays/VideoPlayer.py | 24 +- .../Embedding/Accordion.py | 20 +- .../Embedding/Carousel.py | 63 +-- .../Embedding/EmbeddedView.py | 22 +- .../Embedding/FlexRepeater.py | 12 +- .../Embedding/ViewCanvas.py | 18 +- .../Inputs/BarcodeScanner.py | 18 +- .../PerspectiveComponents/Inputs/Button.py | 18 +- .../PerspectiveComponents/Inputs/Checkbox.py | 18 +- .../PerspectiveComponents/Inputs/DateTime.py | 34 +- .../PerspectiveComponents/Inputs/Dropdown.py | 45 +- .../Inputs/FileUpload.py | 71 +-- .../Inputs/FormComponent.py | 471 ++++++++++++++++++ .../Inputs/MultiStateButton.py | 26 +- .../Inputs/NumericEntryField.py | 115 +++-- .../Inputs/OneShotButton.py | 20 +- .../Inputs/PasswordField.py | 26 +- .../Inputs/RadioGroup.py | 30 +- .../Inputs/SignaturePad.py | 16 +- .../PerspectiveComponents/Inputs/Slider.py | 26 +- .../PerspectiveComponents/Inputs/TextArea.py | 18 +- .../PerspectiveComponents/Inputs/TextField.py | 20 +- .../Inputs/ToggleSwitch.py | 32 +- .../Navigation/HorizontalMenu.py | 38 +- .../PerspectiveComponents/Navigation/Link.py | 16 +- .../Navigation/MenuTree.py | 48 +- .../Reporting/ReportViewer.py | 66 +-- .../PerspectiveComponents/Symbols/Symbol.py | 89 +++- Components/PerspectiveComponents/Tooltip.py | 16 +- Helpers/CSSEnumerations.py | 26 + Helpers/Formatting.py | 5 +- Helpers/IAAssert.py | 10 +- .../IAExpectedConditions.py | 22 +- Helpers/IASelenium.py | 32 +- Helpers/Ignition/Alarm.py | 20 +- Helpers/Ignition/Tag.py | 20 +- Helpers/PerspectivePages/DockedViewHelper.py | 4 +- Helpers/PerspectivePages/LoginHelper.py | 22 +- .../PerspectivePages/NotificationHelper.py | 32 +- Helpers/PerspectivePages/PopupHelper.py | 10 +- .../PerspectivePages/QualityOverlayHelper.py | 16 +- Pages/BasicPageObject.py | 6 +- Pages/IgnitionPageObject.py | 10 +- Pages/PagePiece.py | 8 +- Pages/Perspective/AppBar.py | 209 +++++--- Pages/Perspective/DockedView.py | 8 +- Pages/Perspective/Popup.py | 26 +- Pages/Perspective/PrintPreviewPage.py | 16 +- Pages/Perspective/View.py | 9 +- Pages/Perspective/ViewCanvasInstancedView.py | 4 +- Pages/PerspectivePageObject.py | 52 +- 95 files changed, 3124 insertions(+), 1662 deletions(-) rename Components/{PerspectiveComponents => }/Common/Dropdown.py (94%) rename Components/{PerspectiveComponents => }/Common/Icon.py (90%) create mode 100644 Components/PerspectiveComponents/Inputs/FormComponent.py diff --git a/Components/BasicComponent.py b/Components/BasicComponent.py index 9668d15..077172d 100644 --- a/Components/BasicComponent.py +++ b/Components/BasicComponent.py @@ -1,8 +1,12 @@ +import functools +import inspect +import re from enum import Enum from time import sleep -from typing import Optional, Union, List, Tuple, Any +from typing import Optional, Union, List, Tuple, Any, Dict, TypeVar, Type, Self, Callable -from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException, \ +from selenium.common.exceptions import StaleElementReferenceException, ElementClickInterceptedException, \ + TimeoutException, \ ElementNotInteractableException from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By @@ -11,20 +15,61 @@ from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.ui import WebDriverWait +from Helpers import CSSEnumerations from Helpers.CSSEnumerations import CSSPropertyValue from Helpers.IAExpectedConditions import IAExpectedConditions as IAec from Helpers.IASelenium import IASelenium from Helpers.PerspectivePages.Quality import PerspectiveQuality from Helpers.Point import Point +K = List[Union[int, float, str]] +V = TypeVar('V') -class RequestPrintTarget(Enum): - """ - The expected target to be supplied as a parameter to the requestPrint method available to components. - """ - COMPONENT = 'component' - VIEW = 'view' - PAGE = 'page' + +class ComponentCollection(Dict[K, V]): + + def __init__( + self, + by: str, + locator_string_ready_for_formatting: str, + component_class: Type[V], + driver: WebDriver, + parent_locator_list: Optional[list] = None, + timeout: float = 10.0, + description: Optional[str] = None, + poll_freq: float = 0.5): + super().__init__() + self._by = by + self._locator_string_ready_for_formatting = locator_string_ready_for_formatting + self._component_class = component_class + self._driver = driver + self._parent_locator_list = parent_locator_list + self._timeout = timeout + self._description = description + self._poll_freq = poll_freq + + def get(self, list_of_pieces_to_format: K) -> V: + """ + Obtain a component from the running collection. + + :param list_of_pieces_to_format: A list of values to be formatted into the locator string. + :return: A component of the type specified in the constructor. + """ + locator = (self._by, self._locator_string_ready_for_formatting.format(*list_of_pieces_to_format)) + component = super().get(locator) + if not component: + component = self._component_class( + locator=locator, + driver=self._driver, + parent_locator_list=self._parent_locator_list, + timeout=self._timeout, + description=self._description, + poll_freq=self._poll_freq) + # The following line ignores a type difference between the declared class type K and the locator tuple. + # We are storing and retrieving values internally under a different pattern. + # noinspection PyTypeChecker + self[locator] = component + return component class ComponentPiece: @@ -38,10 +83,10 @@ class ComponentPiece: def __init__( self, - locator: Tuple[By, str], + locator: Tuple[str, str], driver: WebDriver, parent_locator_list: Optional[list] = None, - wait_timeout: float = 10.0, + timeout: float = 10.0, description: Optional[str] = None, poll_freq: float = 0.5): """ @@ -52,7 +97,7 @@ def __init__( parent_locator_list: List A list of the locators used to build the parent of this component. A value of None will be interpreted as opting into ignorance of the parentage of the component. - wait_timeout: float + timeout: float The amount of time to poll the DOM before throwing a potential TimeoutException in the event this component could not be located. poll_freq: float @@ -60,32 +105,68 @@ def __init__( """ self._locator = locator self.driver = driver - self.wait_timeout = wait_timeout - self.wait = WebDriverWait(driver=self.driver, timeout=wait_timeout, poll_frequency=poll_freq) + self.timeout = timeout + self.wait = WebDriverWait(driver=self.driver, timeout=timeout, poll_frequency=poll_freq) self.description = description self._parent_locator_list = parent_locator_list.copy() if parent_locator_list else [] self._update_locator_list() self.poll_freq = poll_freq - def click(self, wait_timeout: Optional[Union[int, float]] = None, binding_wait_time: float = 0) -> None: + @staticmethod + def retry_on_stale_element(retry_attempts: int = 3): + def decorator(call: Callable) -> Callable: + @functools.wraps(call) + def wrapper(self, *args, **kwargs): + for attempt in range(retry_attempts): + try: + return call(self, *args, **kwargs) + except StaleElementReferenceException: + pass + # final attempt + return call(self, *args, **kwargs) + # Preserve signature so pytest can see fixture parameters + wrapper.__signature__ = inspect.signature(call) + return wrapper + return decorator + + @retry_on_stale_element() + def boundary_collision(self, other_component: Self) -> bool: + """ + Determine if the boundaries of this component overlap with another component. The compared components + may touch, but not overlap. + + :param other_component: The other component to compare against. + + :returns: True, if the boundaries of this component overlap with another component, False otherwise - even if + the components are touching. + """ + rect_1 = self.find().rect + rect_2 = other_component.find().rect + return (rect_1['x'] < rect_2['x'] + rect_2['width'] + and rect_1['x'] + rect_1['width'] > rect_2['x'] + and rect_1['y'] < rect_2['y'] + rect_2['height'] + and rect_1['y'] + rect_1['height'] > rect_2['y']) + + def click(self, timeout: Optional[Union[int, float]] = None, wait_after_click: float = 0) -> None: """ Click the component. - :param wait_timeout: Poll the DOM up to this amount of time before potentially throwing a TimeoutException. + :param timeout: Poll the DOM up to this amount of time before potentially throwing a TimeoutException. Overrides the default of the component. - :param binding_wait_time: The amount of time to wait after the click event occurs before continuing. + :param wait_after_click: The amount of time to wait after the click event occurs before continuing. :raises TimeoutException: If the component is not found in the DOM. """ - wait = WebDriverWait(driver=self.driver, timeout=wait_timeout) if wait_timeout is not None else self.wait - self.find(wait_timeout=wait_timeout) # do not capture TimeoutException here + wait = WebDriverWait(driver=self.driver, timeout=timeout) if timeout is not None else self.wait + self.find(timeout=timeout) # do not capture TimeoutException here # A TimeoutException here means the element never became clickable. wait.until( IAec.function_returns_true(custom_function=self._click, function_args={}), message="The element never became clickable.") - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=wait_after_click) + @retry_on_stale_element() def click_with_offset(self, x_offset: int, y_offset: int) -> None: """ Click this element, but offset by some pixel count. @@ -106,33 +187,34 @@ def click_with_offset(self, x_offset: int, y_offset: int) -> None: IASelenium(self.driver).take_screenshot_of_element(self.find()) raise ecie - def double_click(self, binding_wait_time: float = 0) -> None: + @retry_on_stale_element() + def double_click(self, wait_after_click: float = 0) -> None: """ Double-click the component. - :param binding_wait_time: The amount of time to wait after the double-click event occurs before continuing. + :param wait_after_click: The amount of time to wait after the double-click event occurs before continuing. :raises TimeoutException: If the component is not found in the DOM. """ ActionChains(self.driver).move_to_element(to_element=self.find()).double_click(on_element=self.find()).perform() - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=wait_after_click) - def find(self, wait_timeout: Optional[Union[int, float]] = None) -> WebElement: + def find(self, timeout: Optional[Union[int, float]] = None) -> WebElement: """ Get a singular Web Element in the DOM which matches the supplied locator for this component. If more than one Web Element would match, return only the first match encountered in the DOM from top to bottom in the document - NOT the viewable area. Distinct from `xfind`, which uses XPATH. - :param wait_timeout: The amount of time (in seconds) to wait to locate the Web Element. + :param timeout: The amount of time (in seconds) to wait to locate the Web Element. :returns: The first Web Element found given the supplied locator for this component. :raises TimeoutException: If no element matches the locator of this component. """ - if wait_timeout is not None: # 0 is acceptable + if timeout is not None: # 0 is acceptable local_wait = WebDriverWait( self.driver, - wait_timeout) + timeout) else: local_wait = self.wait css_locator = self.get_full_css_locator() @@ -145,19 +227,19 @@ def find(self, wait_timeout: Optional[Union[int, float]] = None) -> WebElement: raise TimeoutException( msg=f"Unable to locate element with CSS locator: {css_locator}{description}") from toe - def find_all(self, wait_timeout: Optional[Union[int, float]] = None) -> List[WebElement]: + def find_all(self, timeout: Optional[Union[int, float]] = None) -> List[WebElement]: """ Get a list of all Web Elements in the DOM which match the supplied locator for this component. Distinct from `xfind_all`, which uses XPATH. - :param wait_timeout: The amount of time (in seconds) to wait to locate the Web Element. + :param timeout: The amount of time (in seconds) to wait to locate the Web Element. :returns: A list of all Web Elements in the DOM which match the supplied locator for this component :raises TimeoutException: If no elements match the locator of this component. """ - if wait_timeout is not None: # 0 is acceptable - local_wait = WebDriverWait(self.driver, wait_timeout) + if timeout is not None: # 0 is acceptable + local_wait = WebDriverWait(self.driver, timeout) else: local_wait = self.wait css_locator = self.get_full_css_locator() @@ -168,6 +250,7 @@ def find_all(self, wait_timeout: Optional[Union[int, float]] = None) -> List[Web raise TimeoutException( msg=f"Unable to locate any elements with CSS locator: {css_locator}{description}") from toe + @retry_on_stale_element() def get_computed_height(self, include_units: bool = False) -> str: """ Get the computed height of the component. Must return as a string because of the possibility of included units. @@ -188,6 +271,7 @@ def get_computed_height(self, include_units: bool = False) -> str: height = height.split("px")[0] return height + @retry_on_stale_element() def get_computed_width(self, include_units: bool = False) -> str: """ Get the computed width of the component. Must return as a string because of the possibility of included units. @@ -208,6 +292,7 @@ def get_computed_width(self, include_units: bool = False) -> str: width = width.split("px")[0] return width + @retry_on_stale_element() def get_css_property(self, property_name: Union[CSSPropertyValue, str]) -> str: """ Get a CSS property value of the Web Element defined by this component. @@ -237,6 +322,7 @@ def get_full_xpath_locator(self) -> Tuple[Union[By, str], str]: """ return _LocatorBuilder.get_xpath_locator(locators=self.locator_list) + @retry_on_stale_element() def get_origin(self) -> Point: """ Get the Cartesian Coordinate of the upper-left corner of the component, measured from the @@ -248,6 +334,7 @@ def get_origin(self) -> Point: rect = self.find().rect return Point(x=rect['x'], y=rect['y']) + @retry_on_stale_element() def get_termination(self) -> Point: """ Get the Cartesian Coordinate of the bottom-right corner of the component, measured from the @@ -259,6 +346,7 @@ def get_termination(self) -> Point: rect = self.find().rect return Point(rect['x'] + rect['width'], rect['y'] + rect['height']) + @retry_on_stale_element() def get_text(self) -> str: """ Get the text of this component. @@ -268,6 +356,7 @@ def get_text(self) -> str: """ return self.find().text + @retry_on_stale_element() def hover(self) -> None: """ Hover over this component. @@ -276,36 +365,68 @@ def hover(self) -> None: """ ActionChains(self.driver).move_to_element(self.find()).perform() + @retry_on_stale_element() + def is_displayed(self) -> bool: + """ + Determine if a component is currently displayed. + + :returns: True, if the Component is currently displayed - False otherwise. + """ + try: + return self.find(timeout=0).is_displayed() + except TimeoutException: + return False + + def lose(self, timeout: Optional[float] = None) -> None: + """ + Wait some period of time for the component to not be present in the DOM. + """ + wait = WebDriverWait(self.driver, timeout) if timeout is not None else self.wait + css_locator = self.get_full_css_locator() + try: + wait.until_not(ec.presence_of_element_located(css_locator)) + except TimeoutException as toe: + description = f"\nDescription: {self.description}" if self.description else '' + raise TimeoutException( + msg=f"Element with CSS locator was never removed from the DOM: {css_locator}{description}") from toe + + @retry_on_stale_element() def release_focus(self) -> None: """ Forces a blur() event on the Web Element. """ self.driver.execute_script('arguments[0].blur()', self.find()) - def right_click(self, binding_wait_time: Optional[float] = 0) -> None: + @retry_on_stale_element() + def right_click(self, wait_after_click: Optional[float] = 0) -> None: """ Right-click this component. - :param binding_wait_time: The amount of time to wait after the click event occurs before continuing. + :param wait_after_click: The amount of time to wait after the click event occurs before continuing. :raises TimeoutException: If no element matches the locator of this component. """ IASelenium(driver=self.driver).right_click(web_element=self.find()) - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=wait_after_click) - def scroll_to_element(self, align_to_top: bool = True) -> None: + @retry_on_stale_element() + def scroll_to_element( + self, + behavior: CSSEnumerations.CSS.Behavior = CSSEnumerations.CSS.Behavior.AUTO, + block: CSSEnumerations.CSS.Block = CSSEnumerations.CSS.Block.START, + inline: CSSEnumerations.CSS.Inline = CSSEnumerations.CSS.Inline.NEAREST) -> None: """ - Vertical scroll to this element in the viewport. - - :param align_to_top: Aligns the top of the element with the top of the page (as much as possible) if True, - else aligns the bottom of the element to the bottom of the viewport (as much as possible). - - :raises TimeoutException: If no element matches the locator of this component. + For information about the default values, please visit + https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView """ - self.driver.execute_script('arguments[0].scrollIntoView(' + str(align_to_top).lower() + ');', - self.find()) + self.driver.execute_script( + 'arguments[0].scrollIntoView({behavior: "' + + str(behavior) + '", block: "' + + str(block) + '", inline: "' + + str(inline) + '"});', + self.find()) - def set_locator(self, new_locator: Tuple[By, str]) -> None: + def set_locator(self, new_locator: Tuple[Union[By, str], str]) -> None: """ Force a change in the locator/selector used to locate the component in the DOM. @@ -318,7 +439,7 @@ def wait_on_text_condition( self, text_to_compare: Optional[Any], condition: Union[IAec.TextCondition, IAec.NumericCondition], - wait_timeout: Optional[float] = None) -> str: + timeout: Optional[float] = None) -> str: """ Obtain the text of this Component, after potentially waiting some period of time for the Component to display that text. @@ -326,15 +447,16 @@ def wait_on_text_condition( :param text_to_compare: The text to be used in conjunction with the supplied condition. If the supplied value is None, then no wait will ever occur and the text of the component will be immediately returned. :param condition: The condition to be used to compare the Component text to the provided text. - :param wait_timeout: The amount of time (in seconds) you are willing to wait for the Component to display + :param timeout: The amount of time (in seconds) you are willing to wait for the Component to display the specified text. If not supplied, this will default to the wait timeout supplied in the constructor of the Component. :returns: The text of the Component, after potentially having waited for an expected text match. """ - cond_wait = WebDriverWait(driver=self.driver, timeout=wait_timeout) if wait_timeout is not None else self.wait + cond_wait = WebDriverWait(driver=self.driver, timeout=timeout) if timeout is not None else self.wait text_to_compare = str(text_to_compare) if text_to_compare is not None else text_to_compare text = "" + present_then_removed = False def compare_against_condition(): """ @@ -347,10 +469,15 @@ def compare_against_condition(): """ nonlocal text nonlocal text_to_compare + nonlocal present_then_removed try: text = self.get_text() + present_then_removed = True except TimeoutException as get_text_toe: - get_text_toe.should_raise = True # toe from failing to locate the component/element should be raised + if not present_then_removed: + # toe from failing to locate the component/element at any point in time should be raised. We do + # not want to raise this TOE if the component was present at some point in time. + get_text_toe.should_raise = True raise if text_to_compare is None: # immediately return because the text will NEVER compare correctly against None @@ -378,18 +505,18 @@ def compare_against_condition(): pass # otherwise pass because toe originated from not meeting condition return text - def xfind(self, wait_timeout: Optional[int] = None) -> WebElement: + def xfind(self, timeout: Optional[int] = None) -> WebElement: """ Get a singular Web Element in the DOM which matches the supplied locator for this component. If more than one Web Element would match, return only the first match encountered in the DOM from top to bottom in the document - NOT the viewable area. Distinct from `find`, which uses CSS_SELECTOR. - :param wait_timeout: The amount of time (in seconds) to wait to locate the Web Element. + :param timeout: The amount of time (in seconds) to wait to locate the Web Element. :raises TimeoutException: If no element matches the locator of this component. """ - if wait_timeout: - local_wait = WebDriverWait(self.driver, wait_timeout) + if timeout is not None: + local_wait = WebDriverWait(self.driver, timeout) else: local_wait = self.wait xpath_locator = self.get_full_xpath_locator() @@ -400,17 +527,17 @@ def xfind(self, wait_timeout: Optional[int] = None) -> WebElement: raise TimeoutException( msg=f"Unable to locate any elements with XPath locator: {xpath_locator}{description}") from toe - def xfind_all(self, wait_timeout: Optional[int] = None) -> List[WebElement]: + def xfind_all(self, timeout: Optional[int] = None) -> List[WebElement]: """ Get a list of Web Elements in the DOM which matches the supplied locator for this component. Distinct from `find_all`, which uses CSS_SELECTOR. - :param wait_timeout: The amount of time (in seconds) to wait to locate the Web Element. + :param timeout: The amount of time (in seconds) to wait to locate the Web Element. :raises TimeoutException: If no element matches the locator of this component. """ - if wait_timeout: - local_wait = WebDriverWait(self.driver, wait_timeout) + if timeout is not None: + local_wait = WebDriverWait(self.driver, timeout) else: local_wait = self.wait xpath_locator = self.get_full_xpath_locator() @@ -422,15 +549,16 @@ def xfind_all(self, wait_timeout: Optional[int] = None) -> List[WebElement]: msg=f"Unable to locate any elements with XPath locator: {xpath_locator}{description}") from toe @staticmethod - def wait_on_binding(time_to_wait=0.5) -> None: + def wait_some_time(time_to_wait=0.5) -> None: """ - A glorified hard-coded sleep, used to force the code to wait as bindings evaluate before allowing code to + A glorified hard-coded sleep, used to force the code to wait after some event before allowing code to continue. :param time_to_wait: The amount of time (in seconds) to wait. """ sleep(time_to_wait) + @retry_on_stale_element() def _click(self) -> bool: """ Attempt to click an item, and continue attempting to do so until the item becomes "interactable". @@ -441,7 +569,7 @@ def _click(self) -> bool: :return: True, if the click is successful - False if the element is not ready to be clicked. """ try: - self.find(wait_timeout=0).click() + self.find(timeout=0).click() return True except ElementNotInteractableException: return False @@ -492,76 +620,92 @@ class BasicPerspectiveComponent(BasicComponent): _POPOVER_PROPERTY_INDEX = 1 _POPOVER_DESCRIPTION_INDEX = 2 + class QualityException(Exception): + def __init__(self, full_locator: str, msg: Optional[str] = None): + self.full_locator = full_locator + self.msg = msg if msg else f'The component located with "{self.full_locator}" had a quality ' \ + f'overlay present.' + super().__init__(self.msg) + + class RequestPrintTarget(Enum): + """ + The expected target to be supplied as a parameter to the requestPrint method available to components. + """ + COMPONENT = 'component' + VIEW = 'view' + PAGE = 'page' + def __init__( self, - locator: Tuple[By, str], + locator: Tuple[str, str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): - """ - locator: Tuple - A tuple which defines the CSS selector which would be used to locate the component. - driver: WebDriver - The WebDriver used to drive the session/page. - parent_locator_list: List - A list of the locators used to build the parent of this component. A value of None will be interpreted - as opting into ignorance of the parentage of the component. - wait_timeout: float - The amount of time to poll the DOM before throwing a potential TimeoutException in the event this component - could not be located. - poll_freq: float - A value used to determine how frequently to poll the DOM. Used primarily for performance testing. + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): + """ + :param locator: A tuple which defines the CSS selector which would be used to locate the component. + :param driver: The WebDriver used to drive the session/page. + :param parent_locator_list: A list of the locators used to build the parent of this component. A value of None + will be interpreted as opting into ignorance of the parentage of the component. + :param timeout: The amount of time to poll the DOM before throwing a potential TimeoutException in the event + this component could not be located. + :param poll_freq: A value used to determine how frequently to poll the DOM. Used primarily for performance + testing. + :param raise_exception_for_overlay: If True, attempts to locate this component will raise an exception in + the event the component has any quality overlay present. If False, overlays will be ignored unless + explicitly checked for outside of normal usage. """ super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._quality_overlay_state_div = ComponentPiece( locator=self._QUALITY_OVERLAY_STATE_DIV_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._quality_overlay_footer = ComponentPiece( locator=self._QUALITY_OVERLAY_FOOTER_LOCATOR, driver=driver, parent_locator_list=self._quality_overlay_state_div.locator_list, - wait_timeout=0, + timeout=0, poll_freq=poll_freq) self._quality_overlay_header = ComponentPiece( locator=self._QUALITY_OVERLAY_HEADER_LOCATOR, driver=driver, parent_locator_list=self._quality_overlay_state_div.locator_list, - wait_timeout=0, + timeout=0, poll_freq=poll_freq) self._header_popover_badges = ComponentPiece( locator=self._HEADER_POPOVER_ICON_LOCATOR, driver=driver, parent_locator_list=self._quality_overlay_header.locator_list, - wait_timeout=0, + timeout=0, poll_freq=poll_freq) self._micro_overlay_icons = ComponentPiece( locator=self._MICRO_OVERLAY_ICON_LOCATOR, driver=driver, parent_locator_list=self._quality_overlay_footer.locator_list, - wait_timeout=0, + timeout=0, poll_freq=poll_freq) self._quality_popover = ComponentPiece( locator=self._POPOVER_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._quality_popover_subsections = ComponentPiece( locator=self._POPOVER_SUBSECTION_LOCATOR, driver=driver, parent_locator_list=self._quality_popover.locator_list, poll_freq=poll_freq) + self._raise_exception_for_overlay = raise_exception_for_overlay def click_quality_overlay_popover_icon(self) -> None: """ @@ -571,9 +715,15 @@ def click_quality_overlay_popover_icon(self) -> None: """ if self.quality_overlay_is_in_micro_mode(): # fallback to "first" - self._micro_overlay_icons.click(binding_wait_time=0.1) + self._micro_overlay_icons.click(wait_after_click=0.1) else: - self._header_popover_badges.click(binding_wait_time=0.1) + self._header_popover_badges.click(wait_after_click=0.1) + + def find(self, timeout: Optional[float] = None): + component = super().find(timeout=timeout) # we have to wait for the component before checking for the overlay! + if self._raise_exception_for_overlay and self.quality_overlay_is_displayed(): + raise BasicPerspectiveComponent.QualityException(full_locator=self.get_full_css_locator()[1]) + return component def get_column_span(self) -> int: """ @@ -727,7 +877,7 @@ def quality_popover_is_displayed(self) -> bool: :returns: True, if the quality popover is displayed - False otherwise. """ try: - return self._quality_popover.find() is not None + return self._quality_popover.find(timeout=0) is not None except TimeoutException: return False @@ -739,7 +889,7 @@ def quality_overlay_is_in_micro_mode(self) -> bool: """ try: # fallback to "first" - return self._micro_overlay_icons.find().is_displayed() + return self._micro_overlay_icons.find(timeout=0).is_displayed() except TimeoutException: return False @@ -750,55 +900,92 @@ def quality_overlay_is_displayed(self) -> bool: :returns: True, if the quality overlay is displayed - False otherwise. """ try: - return self._quality_overlay_state_div.find() is not None + return self._quality_overlay_state_div.find(timeout=0) is not None except TimeoutException: return False def wait_for_no_overlay( - self, time_to_wait: int = 5, raise_exception: bool = False, wait_on_binding: float = 0) -> None: + self, timeout: int = 5, raise_exception: bool = False, wait_after_overlay_removed: float = 0) -> bool: """ Wait for any overlay attached to this component to be removed. - :param time_to_wait: The amount of time (in seconds) to wait for the overlay to disappear. + :param timeout: The amount of time (in seconds) to wait for the overlay to disappear. :param raise_exception: If True, will raise a TimeoutException exception when encountered. Otherwise, TimeoutException will be ignored. - :param wait_on_binding: The amount of time to wait after the overlay disperses. This can be useful to ensure - that OPC Tag writes make it to the PLC and that all bindings are returning the current value on the PLC. + :param wait_after_overlay_removed: The amount of time to wait after the overlay disperses. This can be useful + to ensure that OPC Tag writes make it to the PLC and that all bindings are returning the current value + on the PLC. :raises TimeoutException: If the overlay is still present in the DOM after the value supplied to time_to_wait expires. """ try: - WebDriverWait(driver=self.driver, timeout=time_to_wait).until_not( + WebDriverWait(driver=self.driver, timeout=timeout).until_not( IAec.function_returns_true(custom_function=self.quality_overlay_is_displayed, function_args={})) - self.wait_on_binding(time_to_wait=wait_on_binding) + self.wait_some_time(time_to_wait=wait_after_overlay_removed) + return True except TimeoutException as toe: if raise_exception: raise toe + else: + return False class _LocatorBuilder: @staticmethod def get_css_locator(locators: list) -> Tuple[Union[By, str], str]: css_list = [] + complex_css_list = [] + comma_locator_pattern = '(?![^[]*[\'"][^[]*),(?![^[]*[\'"][^[]*])' + + def append_value_for_lists(new_value): + nonlocal css_list + nonlocal complex_css_list + if complex_css_list: + complex_css_list = [f'{_} {new_value}' for _ in complex_css_list] + else: + css_list.append(new_value) + for locator in locators: - by = By.CSS_SELECTOR - value = locator - if type(locator) == tuple: - by = locator[0] - value = locator[1] - if by == By.XPATH: - raise TypeError( - f'Locator: {locator} has a By type of XPATH which is incompatible with the css_selector builder.') - elif by == By.ID: - css_list.append(f'[id="{value}"]') - elif by == By.CLASS_NAME: - css_list.append(f'.{value}') - elif by == By.NAME: - css_list.append(f'[name="{value}"]') + if isinstance(locator, tuple): + by, value = locator else: - css_list.append(value) - return By.CSS_SELECTOR, " ".join(css_list) + by, value = By.CSS_SELECTOR, locator + + match by: + case By.XPATH: + raise TypeError( + f'Locator: {locator} has a By type of XPATH which is incompatible with ' + 'the css_selector builder.') + case By.ID: + append_value_for_lists(f'[id="{value}"]') + case By.CLASS_NAME: + append_value_for_lists(f'.{value}') + case By.NAME: + append_value_for_lists(f'[name="{value}"]') + case _: + split_locator_list = re.split(comma_locator_pattern, value) + if len(split_locator_list) > 1: + complex_list_already_exists = True if complex_css_list else False + locators_to_add = [] + locators_to_remove = set() + for split_locator in split_locator_list: + if split_locator[0] == ' ': + split_locator = split_locator[1:] + if complex_list_already_exists: + for string_locator_list in complex_css_list: + locators_to_remove.add(string_locator_list) + locators_to_add.append(f'{string_locator_list} {split_locator}') + else: + complex_css_list.append( + ' '.join(css_list) + (' ' + split_locator if css_list else split_locator)) + complex_css_list += locators_to_add + for remove_item in locators_to_remove: + complex_css_list.remove(remove_item) + else: + append_value_for_lists(value) + css_string = f'{", " if complex_css_list else " "}'.join(complex_css_list if complex_css_list else css_list) + return By.CSS_SELECTOR, css_string @staticmethod def get_xpath_locator(locators: list) -> Tuple[Union[By, str], str]: diff --git a/Components/Common/Button.py b/Components/Common/Button.py index bfe79e1..06ba3b6 100644 --- a/Components/Common/Button.py +++ b/Components/Common/Button.py @@ -5,7 +5,7 @@ from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon from Helpers.CSSEnumerations import CSSPropertyValue from Helpers.Point import Point @@ -24,17 +24,17 @@ class CommonButton(ComponentPiece): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._internal_button = ComponentPiece( @@ -47,33 +47,33 @@ def __init__( locator=self._INTERNAL_ICON_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, description="The icon in use by the Button.", poll_freq=poll_freq) self._internal_text = ComponentPiece( locator=self._INTERNAL_TEXT_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, description="The container in which the text of the Button resides.", poll_freq=poll_freq) self._internal_image = ComponentPiece( locator=self._INTERNAL_IMAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, description="The image within the Button.", poll_freq=poll_freq) - def click(self, wait_timeout=None, binding_wait_time: float = 0.5) -> None: + def click(self, timeout=None, wait_after_click: float = 0.5) -> None: """ Click the button. """ _button = self._get_html_button() if _button == self: - super().click(wait_timeout=wait_timeout, binding_wait_time=binding_wait_time) + super().click(timeout=timeout, wait_after_click=wait_after_click) else: - _button.click(wait_timeout=wait_timeout, binding_wait_time=binding_wait_time) + _button.click(timeout=timeout, wait_after_click=wait_after_click) def get_css_property_of_icon(self, property_name: Union[CSSPropertyValue, str]) -> str: """ diff --git a/Components/PerspectiveComponents/Common/Dropdown.py b/Components/Common/Dropdown.py similarity index 94% rename from Components/PerspectiveComponents/Common/Dropdown.py rename to Components/Common/Dropdown.py index 71d77a7..40c601e 100644 --- a/Components/PerspectiveComponents/Common/Dropdown.py +++ b/Components/Common/Dropdown.py @@ -1,7 +1,7 @@ import json from enum import Enum from json import JSONDecodeError -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException, ElementNotInteractableException from selenium.webdriver.common.by import By @@ -35,17 +35,17 @@ class CommonDropdown(ComponentPiece): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, parent_locator_list: Optional[List] = None, - wait_timeout: float = 2, + timeout: float = 2, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._expand_icon = ComponentPiece( @@ -73,7 +73,7 @@ def collapse(self) -> None: Collapse the dropdown if it is already expanded. No action is taken if the dropdown is currently collapsed. """ if self.is_expanded(): - self.wait_on_binding(0.5) + self.wait_some_time(0.5) self._click_expansion_icon() IAAssert.is_true( value=self.wait_for_expansion_state(state_to_wait_on=ExpansionState.COLLAPSED), @@ -111,7 +111,7 @@ def get_available_options(self) -> List[str]: needs_to_collapse = not self.is_expanded() self.expand() try: - options = [option.text for option in self._available_options.find_all(wait_timeout=0.5)] + options = [option.text for option in self._available_options.find_all(timeout=0.5)] except TimeoutException: options = [] if needs_to_collapse: @@ -178,7 +178,7 @@ def is_expanded(self) -> bool: :returns: True, if the Dropdown is currently expanded. """ return (self.ACTIVE_CLASS in self.find().get_attribute("class")) \ - and (self._options_container.find(wait_timeout=0) is not None) + and (self._options_container.find(timeout=0) is not None) def option_is_disabled(self, option_text: str) -> bool: """ @@ -224,13 +224,13 @@ def option_is_visible_within_option_modal(self, option_text: str) -> bool: bottom_within_modal = option_termination.Y <= modal_termination.Y return visible and left_within_modal and top_within_modal and right_within_modal and bottom_within_modal - def select_option_by_text_if_not_selected(self, option_text: str, binding_wait_time: float = 0.5) -> None: + def select_option_by_text_if_not_selected(self, option_text: str, wait_after_click: float = 0.5) -> None: """ Select an option from the Dropdown if it is not already selected. No action is taken if the supplied option is already selected. :param option_text: The exact text of the option to select. - :param binding_wait_time: The amount of time (in seconds) to wait after selecting the option before allowing + :param wait_after_click: The amount of time (in seconds) to wait after selecting the option before allowing code to continue. Ignored if the option is already selected. :raises TimeoutException: If the specified option is not present. @@ -240,7 +240,7 @@ def select_option_by_text_if_not_selected(self, option_text: str, binding_wait_t self.scroll_to_element() self.expand() try: - self._get_option(option_text=option_text).click(wait_timeout=1, binding_wait_time=binding_wait_time) + self._get_option(option_text=option_text).click(timeout=1, wait_after_click=wait_after_click) except TimeoutException as toe: raise TimeoutException(msg=f"Failed to locate element with text of \"{option_text}\".") from toe except ElementNotInteractableException as enie: @@ -251,13 +251,13 @@ def select_option_by_text_if_not_selected(self, option_text: str, binding_wait_t f"Container dimensions: {termination.X - origin.X} x {termination.Y - origin.Y}") from enie assert option_text in self.get_selected_options_as_list(), f"Failed to select option: '{option_text}'." - def wait_for_expansion_state(self, state_to_wait_on: ExpansionState, wait_timeout: float = 1) -> bool: + def wait_for_expansion_state(self, state_to_wait_on: ExpansionState, timeout: float = 1) -> bool: """ Wait for the Dropdown to take on an expected state, before eventually returning a value which reflects whether the Dropdown took that state. :param state_to_wait_on: Either expanded or collapsed. - :param wait_timeout: The amount of time (in seconds) to potentially wait for the Dropdown to take on the + :param timeout: The amount of time (in seconds) to potentially wait for the Dropdown to take on the supplied state. :return: True, if the Dropdown eventually took the supplied state - False otherwise. @@ -265,7 +265,7 @@ def wait_for_expansion_state(self, state_to_wait_on: ExpansionState, wait_timeou exp_func = IAec.function_returns_true if state_to_wait_on == ExpansionState.EXPANDED \ else IAec.function_returns_false try: - WebDriverWait(driver=self.driver, timeout=wait_timeout).until( + WebDriverWait(driver=self.driver, timeout=timeout).until( exp_func(custom_function=self.is_expanded, function_args={})) return True except TimeoutException: @@ -280,7 +280,7 @@ def _click_expansion_icon(self) -> None: expected_final_state = ExpansionState.COLLAPSED if self.is_expanded() else ExpansionState.EXPANDED self._expand_icon.click() IAAssert.is_true( - value=self.wait_for_expansion_state(state_to_wait_on=expected_final_state, wait_timeout=1), + value=self.wait_for_expansion_state(state_to_wait_on=expected_final_state, timeout=1), failure_msg="Failed to modify the expansion state of the Dropdown." ) diff --git a/Components/Common/FileUpload.py b/Components/Common/FileUpload.py index 7c583af..c169748 100644 --- a/Components/Common/FileUpload.py +++ b/Components/Common/FileUpload.py @@ -1,9 +1,10 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import BasicComponent, ComponentPiece +from Helpers.Formatting import FilePathFormatting class FileUpload(BasicComponent): @@ -15,17 +16,17 @@ class FileUpload(BasicComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._button = ComponentPiece( @@ -60,10 +61,10 @@ def is_enabled(self) -> bool: """ return "disabled" not in self._button.find().get_attribute("class") - def upload_file_by_path(self, normalized_file_path: str) -> None: + def upload_file_by_path(self, file_path: str) -> None: """ Supply the location of a file to be uploaded. - :param normalized_file_path: The normalized (OS-agnostic) path to the file to be uploaded. + :param file_path: The path to the file to be uploaded. """ - self._input.find().send_keys(normalized_file_path) + self._input.find().send_keys(FilePathFormatting.system_safe_file_path(file_path)) diff --git a/Components/PerspectiveComponents/Common/Icon.py b/Components/Common/Icon.py similarity index 90% rename from Components/PerspectiveComponents/Common/Icon.py rename to Components/Common/Icon.py index 9a8a461..ea92174 100644 --- a/Components/PerspectiveComponents/Common/Icon.py +++ b/Components/Common/Icon.py @@ -1,4 +1,4 @@ -from typing import Tuple, Optional, List +from typing import Tuple, Optional, List, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -9,7 +9,7 @@ class CommonIcon(ComponentPiece): - """An Icon which could be used anywhere within Perspective, including within other components.""" + """An Icon which could be used anywhere - including within other components.""" # This list will grow over time, until Dev takes this on as a unit test for their Icon strip-and-zip _KNOWN_EXTRANEOUS_D_ATTRIBUTE_LIST = [ 'M24 24H0V0h24v24z', @@ -19,17 +19,17 @@ class CommonIcon(ComponentPiece): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._children = ComponentPiece( @@ -60,7 +60,7 @@ def get_fill_color(self) -> str: """ local_fill = self.get_css_property(property_name=CSS.FILL) try: - for child in self._children.find_all(wait_timeout=1): + for child in self._children.find_all(timeout=1): child_fill = child.value_of_css_property("fill") # WebElement (WE), so have to use WE function if child_fill != local_fill and child_fill != 'none': # ignore 'none' because we have 0 control over it return child_fill @@ -96,7 +96,7 @@ def get_stroke_color(self) -> str: """ local_stroke = self.get_css_property(property_name=CSS.STROKE) try: - for child in self._children.find_all(wait_timeout=1): + for child in self._children.find_all(timeout=1): child_stroke = child.value_of_css_property("stroke") # WebElement (WE), so have to use WE function if child_stroke != local_stroke: return child_stroke @@ -128,7 +128,7 @@ def has_extraneous_border(self) -> bool: """ for bad_border in self._bad_borders: try: - return bad_border.find(wait_timeout=0) is not None + return bad_border.find(timeout=0) is not None except TimeoutException: pass return False @@ -140,6 +140,6 @@ def is_rendered(self) -> bool: :returns: True, if rendered - False otherwise. """ try: - return self._internal_g.find(wait_timeout=0.5) is not None + return self._internal_g.find(timeout=0.5) is not None except TimeoutException: return False diff --git a/Components/Common/TextInput.py b/Components/Common/TextInput.py index b160a1b..4f14ae8 100644 --- a/Components/Common/TextInput.py +++ b/Components/Common/TextInput.py @@ -17,17 +17,18 @@ class CommonTextInput(ComponentPiece): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, poll_freq: float = 0.5): - super().__init__( + ComponentPiece.__init__( + self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) # The internal input piece is important for when our component has a quality overlay because any reference @@ -35,7 +36,7 @@ def __init__( self._internal_input = ComponentPiece( locator=self._INTERNAL_INPUT_LOCATOR, driver=self.driver, - wait_timeout=0, + timeout=0, parent_locator_list=self.locator_list, description="The internal element, used primarily for Text Fields, or when the component contains " "a quality overlay.", @@ -70,22 +71,23 @@ def set_text( self, text: Union[float, str], release_focus: bool = True, - binding_wait_time: float = 0) -> None: + wait_after: float = 0) -> None: """ Set the value of this component. :param text: The text to type into the field. :param release_focus: Dictates whether a blur() event is invoked for the component after typing the supplied text. - :param binding_wait_time: The amount of time (in seconds) to wait after typing the supplied text before allowing + :param wait_after: The amount of time (in seconds) to wait after typing the supplied text before allowing code to continue. """ text = str(text) - # strip special characters (ESC, ENTER) while leaving spaces and punctuation - expected_text = text.encode("ascii", "ignore").decode() + # strip special characters (ESC, ENTER) while leaving spaces and punctuation. '\n' and '\t are handled + # as a special cases due to the potential need in markdown components. + expected_text = ''.join(c for c in text if c.isprintable() or c in ['\n', '\t']) if text is None or text == '': text = ' ' + Keys.BACKSPACE - self.wait_on_text_condition(text_to_compare="", condition=TextCondition.DOES_NOT_EQUAL, wait_timeout=0.5) + self.wait_on_text_condition(text_to_compare="", condition=TextCondition.DOES_NOT_EQUAL, timeout=0.5) current_text = self.get_text() # Do NOT wait on matching text to be in place keys_to_send = ''.join([Keys.ARROW_RIGHT for _ in current_text] + [Keys.BACKSPACE for _ in current_text] + @@ -102,15 +104,17 @@ def set_text( .move_to_element_with_offset(to_element=input_object.find(), xoffset=5, yoffset=5) \ .click() \ .perform() + if self.driver.name == 'firefox' and Keys.ESCAPE not in keys_to_send: + input_object.find().clear() input_object.find().send_keys(keys_to_send) if release_focus: self.release_focus() IAAssert.is_equal_to( actual_value=self.wait_on_text_condition( - text_to_compare=expected_text, condition=TextCondition.EQUALS, wait_timeout=binding_wait_time + 0.5), + text_to_compare=expected_text, condition=TextCondition.EQUALS, timeout=wait_after + 0.5), expected_value=expected_text, failure_msg="Failed to set the value of the input.") - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=wait_after) def _needs_to_get_input_element(self) -> bool: """ diff --git a/Components/PerspectiveComponents/Charts/Pie.py b/Components/PerspectiveComponents/Charts/Pie.py index 217c639..2a9a20d 100644 --- a/Components/PerspectiveComponents/Charts/Pie.py +++ b/Components/PerspectiveComponents/Charts/Pie.py @@ -1,4 +1,4 @@ -from typing import Tuple, Optional, List +from typing import Tuple, Optional, List, Union from selenium.common.exceptions import TimeoutException, NoSuchElementException from selenium.webdriver.common.by import By @@ -18,45 +18,47 @@ class Pie(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._sections = ComponentPiece( locator=self._SECTION_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, description="The actual slices of the Pie.", poll_freq=poll_freq) self._title = ComponentPiece( locator=self._TITLE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, description="The title of the Pie Chart.", poll_freq=poll_freq) self._legend_container = ComponentPiece( locator=self._LEGEND_CONTAINER_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, description="The Legend of the Pie Chart.", poll_freq=poll_freq) self._checkboxes = ComponentPiece( locator=self._CHECKBOX_LOCATOR, driver=driver, - parent_locator_list=self._legend_container.locator_list, - wait_timeout=1, + parent_locator_list=self.locator_list, + timeout=1, description="The checkboxes within the legend of the Pie Chart.", poll_freq=poll_freq) @@ -143,7 +145,7 @@ def pie_slice_labels_are_displayed(self) -> bool: locator=(By.CSS_SELECTOR, 'g[aria-label="Series"] > g > g:nth-child(5)'), driver=self.driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=self.poll_freq) try: return len(sections.find_all()[0].find_elements(By.CSS_SELECTOR, 'tspan')) > 0 diff --git a/Components/PerspectiveComponents/Charts/PowerChart.py b/Components/PerspectiveComponents/Charts/PowerChart.py index ad1ca08..aa5e3ec 100644 --- a/Components/PerspectiveComponents/Charts/PowerChart.py +++ b/Components/PerspectiveComponents/Charts/PowerChart.py @@ -1,8 +1,7 @@ -from enum import Enum +from enum import Enum, StrEnum from typing import Union, List, Tuple, Optional -from selenium.common.exceptions import StaleElementReferenceException -from selenium.common.exceptions import TimeoutException +from selenium.common.exceptions import StaleElementReferenceException, TimeoutException from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys @@ -16,12 +15,14 @@ CommonDateRangeSelector, DateRangeSelectorTab, DateRangeSelectorTimeUnit, HistoricalRange, PerspectiveDate from Components.PerspectiveComponents.Common.Table import Table as CommonTable from Components.PerspectiveComponents.Common.TagBrowseTree import CommonTagBrowseTree +from Components.PerspectiveComponents.Common.Tree import Item from Components.PerspectiveComponents.Inputs.TextField import TextField from Helpers.CSSEnumerations import CSS, CSSPropertyValue from Helpers.IAAssert import IAAssert from Helpers.IAExpectedConditions import IAExpectedConditions as IAec from Helpers.IAExpectedConditions.IAExpectedConditions import TextCondition from Helpers.IASelenium import IASelenium +from Helpers.Ignition.Tag import Tag, Folder class OptionBarButton(Enum): @@ -35,7 +36,7 @@ class OptionBarButton(Enum): MORE = "more" -class PenControlColumn(Enum): +class PenControlColumn(StrEnum): """An enumeration of the available Pen Control columns.""" AVERAGE = "average" AXIS = "axis" @@ -46,7 +47,7 @@ class PenControlColumn(Enum): X_TRACE = "xTrace" -class RangeBrushColumn(Enum): +class RangeBrushColumn(StrEnum): """An enumeration of the available Range Brush columns.""" AVERAGE = "average" DELTA = "delta" @@ -61,7 +62,7 @@ class RangeBrushColumn(Enum): UCL = "ucl" -class Tab(Enum): +class Tab(StrEnum): """An enumeration of the available Tabs within the settings panel.""" AXES = "Axes" COLUMNS = "Columns" @@ -69,6 +70,92 @@ class Tab(Enum): PLOTS = "Plots" +class HistoricalTag(Tag): + """ + A HistoricalTag is essentially just a regular tag where we enforce that a historical provider must be supplied. It + is not included within the Tag Helper because only Perspective PowerCharts make use of this sort of Tag. + """ + + def __init__(self, name: str, path: str, historical_provider: str, gateway_name: str, provider: str = "[default]"): + """ + These values are most easily derived while examining an item within the Tag Browse Tree of the PowerChart. + + Assuming a displayed item in the Tag Browse Tree with the following structure... + + MyHistoricalProvider + L_ GatewayName:TagProvider + L_ DirectoryA + L_ DirectoryB + L_ MyTag + + name: "MyTag" + path: "DirectoryA/DirectoryB" + historical_provider: "MyHistoricalProvider" + gateway_name: "GatewayName" + provider: "TagProvider", although "[TagProvider]" may also be supplied and handled. + + :param name: The name of your Tag. + :param path: The slash-delimited path to reach your Tag. + :param historical_provider: The History Provider for your Tag. + :param gateway_name: The Gateway name in use by the Gateway that contains the provider this Tag belongs to. + :param provider: The name of the provider this Tag belongs to. + """ + Tag.__init__(self, name=name, path=path, provider=provider) + self.history_provider = historical_provider + self.gateway_name = gateway_name + # now override/replace the full path computed by Tag.__init__() + self._full_path = f'{self.history_provider}/' \ + f'{self.gateway_name}:{self.get_provider().replace("[", "").replace("]", "")}/' \ + f'{self.get_path()}/' \ + f'{self.get_name()}' + + def __str__(self): + return self._full_path + + +class HistoricalFolder(Folder): + """ + A HistoricalFolder is essentially just a regular Folder where we enforce that a historical provider must be + supplied. It is not included within the Tag Helper because only Perspective PowerCharts make use of this sort + of Folder. + """ + + def __init__(self, name: str, path: str, historical_provider: str, gateway_name: str, provider: str = "[default]"): + """ + These values are most easily derived while examining an item within the Tag Browse Tree of the PowerChart. + + Assuming a displayed item (Folder) in the Tag Browse Tree with the following structure... + + MyHistoricalProvider + L_ GatewayName:TagProvider + L_ DirectoryA + L_ DirectoryB + + name: "DirectoryB" + path: "DirectoryA" + historical_provider: "MyHistoricalProvider" + gateway_name: "GatewayName" + provider: "TagProvider", although "[TagProvider]" may also be supplied and handled. + + :param name: The name of your Folder. + :param path: The slash-delimited path to reach your Folder. + :param historical_provider: The History Provider for your Folder. + :param gateway_name: The Gateway name in use by the Gateway that contains the provider this Folder belongs to. + :param provider: The name of the provider this Folder belongs to. + """ + Folder.__init__(self, name=name, path=path, provider=provider) + self.history_provider = historical_provider + self.gateway_name = gateway_name + # now override/replace the full path computed by Folder.__init__() + self._full_path = f'{self.history_provider}/' \ + f'{self.gateway_name}:{self.get_provider().replace("[", "").replace("]", "")}/' \ + f'{self.get_path()}/' \ + f'{self.get_name()}' + + def __str__(self): + return self._full_path + + class PowerChart(BasicPerspectiveComponent): """A Perspective Power Chart Component.""" @@ -77,7 +164,7 @@ class _PowerChartFooter(ComponentPiece): def __init__( self, - parent_locator_list: List[Tuple[By, str]], + parent_locator_list: List[Tuple[Union[By, str], str]], driver: WebDriver, description: Optional[str] = None, poll_freq: float = 0.5): @@ -113,7 +200,7 @@ class _PowerChartGraph(ComponentPiece): def __init__( self, - parent_locator_list: List[Tuple[By, str]], + parent_locator_list: List[Tuple[Union[By, str], str]], driver: WebDriver, description: Optional[str] = None, poll_freq: float = 0.5): @@ -185,7 +272,7 @@ def get_count_of_graphed_pens(self) -> int: """ try: # each graphed pen has 2 elements - return int(len(self._line_paths.find_all(wait_timeout=1)) / 2) + return int(len(self._line_paths.find_all(timeout=1)) / 2) except TimeoutException: return 0 @@ -251,7 +338,7 @@ def get_x_axis_css_property_value(self, property_name: Union[CSSPropertyValue, s return self._time_axis_labels.get_css_property(property_name=property_name) except TimeoutException as toe: raise TimeoutException(msg="No labels are present for the X (time) axis.") from toe - + def get_y_axis_html_class(self) -> str: """ Obtain the HTML class of the Y axis. @@ -307,7 +394,7 @@ def click_and_drag_chart(self, x_offset: int = 0, y_offset: int = 0) -> None: .move_by_offset(xoffset=x_offset, yoffset=y_offset) \ .release() \ .perform() - self.wait_on_binding(time_to_wait=1) + self.wait_some_time(time_to_wait=1) def get_count_of_x_axis_grid_lines(self, _already_attempted=False) -> int: """ @@ -321,12 +408,6 @@ def get_count_of_x_axis_grid_lines(self, _already_attempted=False) -> int: return len(self._x_axis_grid_line.find_all()) except TimeoutException: return 0 - except StaleElementReferenceException as sere: - if not _already_attempted: - return self.get_count_of_x_axis_grid_lines( - _already_attempted=True) - else: - raise sere def get_count_of_y_axis_grid_lines(self, _already_attempted=False) -> int: """ @@ -340,12 +421,6 @@ def get_count_of_y_axis_grid_lines(self, _already_attempted=False) -> int: return len(self._y_axis_grid_line.find_all()) except TimeoutException: return 0 - except StaleElementReferenceException as sere: - if not _already_attempted: - return self.get_count_of_y_axis_grid_lines( - _already_attempted=True) - else: - raise sere def get_css_property_from_x_axis_grid(self, property_name: Union[CSSPropertyValue, str]) -> str: """ @@ -359,8 +434,6 @@ def get_css_property_from_x_axis_grid(self, property_name: Union[CSSPropertyValu """ try: return self._x_axis_grid_line.get_css_property(property_name=property_name) - except StaleElementReferenceException: - return self.get_css_property_from_x_axis_grid(property_name=property_name) except TimeoutException as toe: raise TimeoutException( msg="No grid line was found for the X axis.") from toe @@ -377,9 +450,6 @@ def get_css_property_from_y_axis_grid(self, property_name: Union[CSSPropertyValu """ try: return self._y_axis_grid_line.get_css_property(property_name=property_name) - except StaleElementReferenceException: - return self.get_css_property_from_y_axis_grid( - property_name=property_name) except TimeoutException as toe: raise TimeoutException( msg="No grid line was found for the Y axis.") from toe @@ -425,7 +495,7 @@ class _PowerChartHeader(ComponentPiece): def __init__( self, - parent_locator_list: List[Tuple[By, str]], + parent_locator_list: List[Tuple[Union[By, str], str]], driver: WebDriver, poll_freq: float = 0.5): super().__init__( @@ -752,7 +822,7 @@ class _PowerChartSettingsPanel(ComponentPiece): def __init__( self, - parent_locator_list: List[Tuple[By, str]], + parent_locator_list: List[Tuple[Union[By, str], str]], driver: WebDriver, poll_freq: float = 0.5): super().__init__( @@ -979,7 +1049,7 @@ def click_normal_state_stroke_color_dropdown(self) -> None: """ self._normal_state_stroke_color_dropdown.click() - def click_tab(self, tab: Tab) -> None: + def click_tab(self, tab: Union[Tab, str]) -> None: """ Click a tab of the Settings panel. Requires that the Settings panel already be open. @@ -987,11 +1057,11 @@ def click_tab(self, tab: Tab) -> None: :raises TimeoutException: If the supplied tab is not present. """ - self._tab.set_locator(new_locator=(By.ID, f"{self._TAB_ID_GENERIC_STRING}{tab.value.lower()}")) + self._tab.set_locator(new_locator=(By.ID, f"{self._TAB_ID_GENERIC_STRING}{tab.lower()}")) self._tab.click() IAAssert.is_true( value=self.tab_is_active(tab=tab), - failure_msg=f"Failed to select the {tab.value} tab.") + failure_msg=f"Failed to select the {tab} tab.") def close_settings_panel(self) -> None: """ @@ -1146,7 +1216,7 @@ def is_displayed(self) -> bool: :returns: True, if the Settings panel is currently open/displayed/expanded - False otherwise. """ try: - return self.find(wait_timeout=1).is_displayed() and "expanded" in self.find().get_attribute("class") + return self.find(timeout=1).is_displayed() and "expanded" in self.find().get_attribute("class") except TimeoutException: return False @@ -1157,13 +1227,13 @@ def pen_settings_are_displayed(self) -> bool: :returns: True, if the inputs used to edit a pen are currently displayed - False otherwise. """ try: - return self._update_pen_name_input_field.find(wait_timeout=1) is not None + return self._update_pen_name_input_field.find(timeout=1) is not None except TimeoutException: return False def set_pen_control_column_display_state_in_settings_panel( self, - pen_control_column: PenControlColumn, + pen_control_column: Union[PenControlColumn, str], should_be_displayed: bool) -> None: """ Set the display state of a column within the pen control table. Requires the Settings panel already be @@ -1178,7 +1248,7 @@ def set_pen_control_column_display_state_in_settings_panel( self.click_tab(tab=Tab.COLUMNS) self._pen_control_column_checkbox.set_locator( new_locator=( - By.ID, f"ia_powerChartComponent__settings__columnsTab__penControl.{pen_control_column.value}")) + By.ID, f"ia_powerChartComponent__settings__columnsTab__penControl.{pen_control_column}")) self._special_checkbox_handling( checkbox_component_piece=self._pen_control_column_checkbox, should_be_selected=should_be_displayed) @@ -1326,7 +1396,7 @@ def set_pen_stroke_color(self, hex_desired_color: str) -> None: expected_value=hex_desired_color, failure_msg="Failed to modify the stroke of the pen while editing in the Settings panel.") - def tab_is_active(self, tab: Tab) -> bool: + def tab_is_active(self, tab: Union[Tab, str]) -> bool: """ Determine if a tab is actively in use. @@ -1337,27 +1407,27 @@ def tab_is_active(self, tab: Tab) -> bool: raises TimeoutException: If the Settings panel is not already open, or if the Settings panel is not currently on the main tabbed layout section. """ - return self._active_tab.find().get_attribute(name="data-category") == tab.value.lower() + return self._active_tab.find().get_attribute(name="data-category") == tab.lower() - def _get_range_brush_checkbox(self, range_brush_column: RangeBrushColumn) -> ComponentPiece: + def _get_range_brush_checkbox(self, range_brush_column: Union[RangeBrushColumn, str]) -> ComponentPiece: """ Obtain a checkbox from the range brush section of the Columns tab. :param range_brush_column: The column for which you would like the checkbox. """ - _checkbox = self._range_brush_column_checkboxes.get(range_brush_column.value) + _checkbox = self._range_brush_column_checkboxes.get(range_brush_column) if not _checkbox: _checkbox = ComponentPiece( locator=( By.ID, - f"ia_powerChartComponent__settings__columnsTab__rangeSelection.{range_brush_column.value}"), + f"ia_powerChartComponent__settings__columnsTab__rangeSelection.{range_brush_column}"), driver=self.driver, parent_locator_list=self.locator_list, - description=f"The {range_brush_column.value} checkbox within the Range Brush section of the " + description=f"The {range_brush_column} checkbox within the Range Brush section of the " f"Columns tab of the Settings Panel.", poll_freq=self.poll_freq ) - self._range_brush_column_checkboxes[range_brush_column.value] = _checkbox + self._range_brush_column_checkboxes[range_brush_column] = _checkbox return _checkbox def _get_pen_visibility_checkbox(self, pen_name: str) -> ComponentPiece: @@ -1510,7 +1580,7 @@ class _PowerChartTable(CommonTable): def __init__( self, - parent_locator_list: List[Tuple[By, str]], + parent_locator_list: List[Tuple[Union[By, str], str]], driver: WebDriver, poll_freq: float = 0.5): super().__init__( @@ -1613,7 +1683,7 @@ def click_pencil_icon_for_pen(self, pen_name: str) -> None: filter( lambda e: e.text == pen_name, self._pen_labels.find_all( - wait_timeout=1)))[0] + timeout=1)))[0] IASelenium( driver=self.driver).hover_over_web_element( web_element=pen_label_web_element) @@ -1749,8 +1819,8 @@ def pen_control_column_is_displayed( return False def _get_pen_control_table_column( - self, pen_control_column: Union[PenControlColumn, RangeBrushColumn]): - pen_control_column = pen_control_column.value + self, pen_control_column: Union[PenControlColumn, RangeBrushColumn, str]): + pen_control_column = pen_control_column _column = self._pen_control_columns.get(pen_control_column) if not _column: _column = ComponentPiece( @@ -1795,7 +1865,7 @@ def remove_pen_by_name(self, pen_name: str) -> None: """ try: pen_label_web_element = list(filter( - lambda e: e.text == pen_name, self._pen_labels.find_all(wait_timeout=1)))[0] + lambda e: e.text == pen_name, self._pen_labels.find_all(timeout=1)))[0] IASelenium(driver=self.driver).hover_over_web_element(web_element=pen_label_web_element) self._delete_pen_icon.set_locator(new_locator=(By.CSS_SELECTOR, "div.tr.hovering svg.delete-pen-icon")) self._delete_pen_icon.click() @@ -1838,11 +1908,11 @@ def toggle_pen_table_display(self) -> None: """ try: self._collapse_icon.click() - self._expand_icon.find(wait_timeout=1) + self._expand_icon.find(timeout=1) except TimeoutException: try: self._expand_icon.click() - self._collapse_icon.find(wait_timeout=1) + self._collapse_icon.find(timeout=1) except TimeoutException as toe: raise TimeoutException( msg=f'Pen table collapse/expand icons could not be located for interaction.') from toe @@ -1859,7 +1929,7 @@ class _PowerChartTagBrowser(CommonTagBrowseTree): def __init__( self, - parent_locator_list: List[Tuple[By, str]], + parent_locator_list: List[Tuple[Union[By, str], str]], driver: WebDriver, poll_freq: float = 0.5): super().__init__( @@ -1889,32 +1959,32 @@ def __init__( "Chart.", poll_freq=poll_freq) - def add_tags_to_chart(self, tag_paths: [str]) -> None: + def add_tags_to_chart(self, tags: List[HistoricalTag]) -> None: """ Add tags to the Power Chart. Requires the Tag Browse Tree already be expanded. If all specified tags are already graphed, no action is taken, and the Tag Browse Tree will remain expanded. - :param tag_paths: A list of tag paths which describe the tags to add to the Power Chart graph as pens. + :param tags: A list of Historical Tags which describe the tags to add to the Power Chart + graph as pens. :raises TimeoutException: If any of the supplied paths does not map to a tag, or if the Tag Browse Tree is not present/expanded. """ - for tag_path in tag_paths: - tag_name = self._split_item_label_path(tag_path)[-1] - self._expand_tag_browser_tree_by_path(path=tag_path) + for tag in tags: + self.expand_to_item(item=self._item_from_tag(tag=tag)) try: WebDriverWait(driver=self.driver, timeout=5).until( IAec.function_returns_true( custom_function=self._select_tag_to_be_added_later, - function_args={"tag_name": tag_name})) + function_args={"tag": tag})) except TimeoutException as toe: - raise TimeoutException(msg=f"No tag found with a name of '{tag_name}'.") from toe + raise TimeoutException(msg=f"No tag found with a path of '{tag}'.") from toe # wait for Apply button to become enabled WebDriverWait(driver=self.driver, timeout=1).until(IAec.function_returns_true( custom_function=self._add_tag_button.is_enabled, function_args={})) if self._add_tag_button.is_enabled(): # tag might already be displayed in the chart, so we might not need to add it - self._add_tag_button.click(binding_wait_time=1) + self._add_tag_button.click(wait_after_click=1) def click_reload_icon(self) -> None: """ @@ -1922,7 +1992,7 @@ def click_reload_icon(self) -> None: :raises TimeoutException: If the Tag Browse Tree is not expanded in the Power Chart. """ - self._reload_icon.click(binding_wait_time=1) + self._reload_icon.click(wait_after_click=1) def collapse_if_expanded(self) -> None: """ @@ -1955,63 +2025,75 @@ def tag_browser_is_expanded(self) -> bool: :returns: True, if the tag Browse Tree is currently expanded - False otherwise. """ try: - return 'expanded' in self.find(wait_timeout=1).get_attribute('class') + return 'expanded' in self.find(timeout=1).get_attribute('class') except TimeoutException: return False - def _expand_tag_browser_tree_by_path(self, path: str) -> None: + def _expand_tag_browser_tree(self, tag: Union[HistoricalTag, HistoricalFolder]) -> None: """ - Expand the Tag Browse Tree structure based on a supplied tag path. + Expand the Tag Browse Tree structure based on a supplied Historical Tag. Does NOT click the tag/folder. - :param path: The path of a Tag as a slash-delimited string. + :param tag: The Historical Tag (or Folder) to expand to. """ - path_nodes = self._split_item_label_path(path) - for i in path_nodes: + path_nodes = tag.get_full_path().split("/") + item = None + for path_piece in path_nodes: try: - if self.item_label_exists_in_tree(item_label=i) and \ - not self.item_is_expanded(item_label=i) \ - and (path_nodes.index(i) != len(path_nodes)-1): - folder_index = self._get_index_of_item_in_tree(item_label=i) - self._folder_icons.find_all()[folder_index].click() - self.wait_on_binding(time_to_wait=1) - WebDriverWait(driver=self.driver, timeout=3, poll_frequency=self.poll_freq).until( - IAec.function_returns_true( - custom_function=self.item_is_expanded, - function_args={'item_label': i})) + item = Item(label_text=path_piece, parent=item) + self.set_expansion_state(item=item, should_be_expanded=True) + WebDriverWait(driver=self.driver, timeout=3, poll_frequency=self.poll_freq).until( + IAec.function_returns_true( + custom_function=self.item_is_expanded, + function_args={'item': item})) except TimeoutException as toe: raise TimeoutException( msg=f"Tag browser tree might not have expanded as expected.\nCheck that the provided " - f"path \'{path}\' is correct.") from toe + f"path \'{item}\' is correct.") from toe + + @staticmethod + def _item_from_tag(tag: Union[Tag, Folder]) -> Item: + """ + We need to build the "linked-list" of Items from the start of the Tag's (or Folder's) full path. + """ + pieces = tag.get_full_path().split("/") # The PowerChart must NOT split on the provider. + item = Item(label_text=pieces.pop(0)) + while len(pieces) > 0: + piece = pieces.pop(0) + if len(piece) > 0: + item = Item(label_text=piece, parent=item) + return item - def _select_tag_to_be_added_later(self, tag_name: str) -> bool: + def _select_tag_to_be_added_later(self, tag: HistoricalTag) -> bool: """ - Select a singular tag for later addition to the Power Chart. + Select a singular Tag for later addition to the Power Chart. - :param tag_name: The name of the tag to select now, for later addition to the Power Chart graph. + :param tag: The Historical Tag to select now, for later addition to the Power Chart graph. :returns: True, if the supplied tag name was selected successfully - False otherwise. """ try: - self.click_item_label(item_label=tag_name) + self.click_item(item=self._item_from_tag(tag=tag)) return True except TimeoutException: return False def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._footer = self._PowerChartFooter( parent_locator_list=self.locator_list, driver=driver, poll_freq=poll_freq) self._graph = self._PowerChartGraph( @@ -2025,21 +2107,21 @@ def __init__( self._tag_browser = self._PowerChartTagBrowser( parent_locator_list=self.locator_list, driver=driver, poll_freq=poll_freq) - def add_tags_to_chart(self, tag_paths: [str]) -> None: + def add_tags_to_chart(self, tags: List[HistoricalTag]) -> None: """ - Add tags to the chart to be graphed as pens. This function will expand the Tag Browser if it is not already + Add Tags to the chart to be graphed as pens. This function will expand the Tag Browser if it is not already expanded, and will leave the Tag Browser expansion state as it was before this function was invoked. - :param tag_paths: A list containing the full tag paths of each tag to add. + :param tags: A list of all the Historical Tags to add to the Power Chart. - :raises TimeoutException: If the Tag Browser button is not present, or if any supplied tag path does not + :raises TimeoutException: If the Tag Browser button is not present, or if any supplied Historical Tag does not identify a Tag to be added. """ _original_state = self._tag_browser.tag_browser_is_expanded() if not _original_state: self._header.click_tag_browser_button() - self._tag_browser.add_tags_to_chart(tag_paths=tag_paths) - self.wait_on_binding(time_to_wait=2) # wait for tags to be graphed + self._tag_browser.add_tags_to_chart(tags=tags) + self.wait_some_time(time_to_wait=2) # wait for tags to be graphed if not _original_state: self._tag_browser.collapse_if_expanded() diff --git a/Components/PerspectiveComponents/Charts/TimeSeriesChart.py b/Components/PerspectiveComponents/Charts/TimeSeriesChart.py index c11505f..f19e2e2 100644 --- a/Components/PerspectiveComponents/Charts/TimeSeriesChart.py +++ b/Components/PerspectiveComponents/Charts/TimeSeriesChart.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -15,45 +15,47 @@ class TimeSeriesChart(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 5, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 5, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._date_time_container = ComponentPiece( locator=self._DATE_TIME_CONTAINER_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, description="The container for the date/time labels.", poll_freq=poll_freq) self._date_container = ComponentPiece( locator=self._DATE_CONTAINER_LOCATOR, driver=driver, parent_locator_list=self._date_time_container.locator_list, - wait_timeout=1, + timeout=1, description="The container which contains only the date information.", poll_freq=poll_freq) self._time_container = ComponentPiece( locator=self._TIME_CONTAINER_LOCATOR, driver=driver, parent_locator_list=self._date_time_container.locator_list, - wait_timeout=1, + timeout=1, description="The container which contains only the time information.", poll_freq=poll_freq) self._time_axis_label = ComponentPiece( locator=self._TIME_AXIS_LABEL_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, description="The label which contains the text of the time axis of the Time Series Chart.", poll_freq=poll_freq) diff --git a/Components/PerspectiveComponents/Charts/XYChart.py b/Components/PerspectiveComponents/Charts/XYChart.py index 0783b00..5d3fee4 100644 --- a/Components/PerspectiveComponents/Charts/XYChart.py +++ b/Components/PerspectiveComponents/Charts/XYChart.py @@ -23,23 +23,25 @@ class XYChart(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 5, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 5, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._bar_coll = {} self._series_coll = {} - def click(self, wait_timeout: Optional[Union[int, float]] = None, binding_wait_time: float = 0) -> None: + def click(self, timeout: Optional[Union[int, float]] = None, wait_after_click: float = 0) -> None: """ Please do not blindly click the XY Chart; no good will come of it. @@ -47,18 +49,18 @@ def click(self, wait_timeout: Optional[Union[int, float]] = None, binding_wait_t """ raise NotImplementedError("Please don't blindly click the XYChart.") - def click_series_label_in_legend_by_text(self, label_text: str, binding_wait_time: float = 0) -> None: + def click_series_label_in_legend_by_text(self, label_text: str, time_to_wait: float = 0) -> None: """ Click the label of a series within the legend of the XY Chart. :param label_text: The text of the label to click. This should be the name of the series. - :param binding_wait_time: How long to wait after the click before allowing code to continue. + :param time_to_wait: How long to wait after the click before allowing code to continue. :raises TimeoutException: If no label with the provided text is present. :raises NoSuchElementException: If unable to locate the click target of the label. """ self._get_series_label_from_legend(label_text=label_text).find_element(*(By.TAG_NAME, "rect")).click() - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=time_to_wait) def get_count_of_bars_in_series(self, series_label: str) -> int: """ diff --git a/Components/PerspectiveComponents/Common/Checkbox.py b/Components/PerspectiveComponents/Common/Checkbox.py index 06e07a5..233092c 100644 --- a/Components/PerspectiveComponents/Common/Checkbox.py +++ b/Components/PerspectiveComponents/Common/Checkbox.py @@ -1,11 +1,11 @@ -from typing import Optional, Tuple, List +from typing import Optional, Tuple, List, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon from Helpers.IAAssert import IAAssert from Helpers.IAExpectedConditions import IAExpectedConditions as IAec @@ -30,17 +30,17 @@ class CommonCheckbox(ComponentPiece): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._click_target = ComponentPiece( @@ -67,17 +67,17 @@ def __init__( parent_locator_list=self.locator_list, poll_freq=poll_freq) - def click(self, wait_timeout: Optional[float] = None, binding_wait_time: float = 0) -> None: + def click(self, timeout: Optional[float] = None, wait_after_click: float = 0) -> None: """ Click the Checkbox. If attempting to set the checkbox to a known state, :func:`set_state` is preferred. - :param wait_timeout: The amount of time you are willing to wait for the checkbox to appear before attempting + :param timeout: The amount of time you are willing to wait for the checkbox to appear before attempting to perform the click. Overrides the component's original wait. - :param binding_wait_time: The amount of time after the click occurs before allowing code to continue. + :param wait_after_click: The amount of time after the click occurs before allowing code to continue. """ - self._click_target.click(wait_timeout=wait_timeout, binding_wait_time=binding_wait_time) + self._click_target.click(timeout=timeout, wait_after_click=wait_after_click) def get_direction_of_label_text(self) -> str: """ @@ -116,14 +116,14 @@ def is_selected(self) -> Optional[bool]: """ return self._STATE_MAP.get(self._icon.find().get_attribute("data-state"), False) - def set_state(self, should_be_selected: Optional[bool], binding_wait_time: float = 0) -> None: + def set_state(self, should_be_selected: Optional[bool], wait_after_click: float = 0) -> None: """ Set the checkbox to a specified state. - :param binding_wait_time: How long to wait before allowing the code to continue. + :param wait_after_click: How long to wait before allowing the code to continue. :param should_be_selected: True if the Checkbox should be selected, False if it should not be selected, or None if the checkbox is a tri-state checkbox and should be 'indeterminate'. """ - self._set_state(should_be_selected=should_be_selected, binding_wait_time=binding_wait_time) + self._set_state(should_be_selected=should_be_selected, wait_after_click=wait_after_click) def _is_enabled(self) -> bool: """Determine if the checkbox is enabled.""" @@ -134,7 +134,7 @@ def _set_state( self, should_be_selected: Optional[bool], already_clicked_once: bool = False, - binding_wait_time: float = 0) -> None: + wait_after_click: float = 0) -> None: """ Set the checkbox to a specified selection state. :param should_be_selected: True if the Checkbox should be selected, False if it should not be selected, or None @@ -143,7 +143,7 @@ def _set_state( should only ever need to call this function twice in a row, in order to swap between the three states. """ if self.is_selected() != should_be_selected: - self.click(binding_wait_time=binding_wait_time) + self.click(wait_after_click=wait_after_click) # could potentially need to click again (checked/unchecked/indeterminate) if not already_clicked_once: # prevent possible recursion issues diff --git a/Components/PerspectiveComponents/Common/ComponentModal.py b/Components/PerspectiveComponents/Common/ComponentModal.py index 5f0ab2d..12df09e 100644 --- a/Components/PerspectiveComponents/Common/ComponentModal.py +++ b/Components/PerspectiveComponents/Common/ComponentModal.py @@ -16,12 +16,12 @@ class ComponentModal(ComponentPiece): def __init__( self, driver: WebDriver, - wait_timeout: Union[float, int] = 2, + timeout: Union[float, int] = 2, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=self._LOCATOR, driver=driver, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) diff --git a/Components/PerspectiveComponents/Common/DateRangeSelector.py b/Components/PerspectiveComponents/Common/DateRangeSelector.py index ef28cea..2260881 100644 --- a/Components/PerspectiveComponents/Common/DateRangeSelector.py +++ b/Components/PerspectiveComponents/Common/DateRangeSelector.py @@ -9,9 +9,9 @@ from Components.BasicComponent import ComponentPiece from Components.Common.Button import CommonButton +from Components.Common.Icon import CommonIcon from Components.PerspectiveComponents.Common.ComponentModal import ComponentModal from Components.PerspectiveComponents.Common.DateTimePicker import CommonDateTimePicker, PerspectiveDate -from Components.PerspectiveComponents.Common.Icon import CommonIcon from Helpers.IAAssert import IAAssert @@ -176,7 +176,7 @@ def __init__( locator=self._AM_PM_SELECTS_LOCATOR, driver=driver, parent_locator_list=self._start_time_input_container.locator_list, - wait_timeout=1) + timeout=1) self._end_time_input_container = ComponentPiece( locator=self._END_TIME_INPUT_CONTAINER_LOCATOR, driver=driver, parent_locator_list=self.locator_list) self._end_time_hour_input = ComponentPiece( @@ -219,7 +219,7 @@ def __init__( locator=self._AM_PM_SELECTS_LOCATOR, driver=driver, parent_locator_list=self._end_time_input_container.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._time_picker_range_info_text = ComponentPiece( locator=self._TIME_PICKER_RANGE_INFO_TEXT_LOCATOR, @@ -239,7 +239,7 @@ def click_clear(self) -> None: :raises TimeoutException: If the clear link is not present. Verify you are using the Historical tab and not the Realtime range tab. """ - self._clear_link.click(wait_timeout=0.5) + self._clear_link.click(timeout=0.5) def click_historical_day(self, numeric_day: Union[int, str]) -> None: """ @@ -408,7 +408,7 @@ def is_displayed(self) -> bool: :returns: True, if the picker is present - False otherwise. """ try: - return self.find(wait_timeout=0.5) is not None + return self.find(timeout=0.5) is not None except TimeoutException: return False @@ -745,7 +745,7 @@ def set_time_value(self, time_value: int) -> None: text_to_set = "".join([Keys.ARROW_RIGHT * len(original), Keys.BACKSPACE * len(original), str(time_value)]) self._input.click() self._input.find().send_keys(text_to_set) - self._message_label.click(binding_wait_time=0.25) # force the loss of focus on the input + self._message_label.click(wait_after_click=0.25) # force the loss of focus on the input IAAssert.is_equal_to( actual_value=self._input.find().get_attribute('value'), expected_value=time_value, @@ -771,7 +771,7 @@ def set_time_value(self, time_value: int) -> None: def __init__( self, driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__(driver=driver, description=description) @@ -900,7 +900,7 @@ def date_range_selector_toggle_icon_is_present(self) -> bool: :returns: True, if the icon is present - False otherwise. """ try: - return self._toggle_icon.find(wait_timeout=0.5) is not None + return self._toggle_icon.find(timeout=0.5) is not None except TimeoutException: return False @@ -1115,7 +1115,7 @@ def modal_or_popover_is_displayed(self) -> bool: :returns: True, if the Date Range Selector is expanded as either a modal or 'popover' - False otherwise. """ try: - return self.find(wait_timeout=0.5) is not None + return self.find(timeout=0.5) is not None except TimeoutException: return False @@ -1186,7 +1186,7 @@ def select_tab(self, tab: DateRangeSelectorTab) -> None: """ _tab_comp = self._historical_tab if tab == DateRangeSelectorTab.HISTORICAL else self._realtime_tab try: - _tab_comp.click(binding_wait_time=1) + _tab_comp.click(wait_after_click=1) except StaleElementReferenceException: self.select_tab(tab=tab) diff --git a/Components/PerspectiveComponents/Common/DateTimePicker.py b/Components/PerspectiveComponents/Common/DateTimePicker.py index 064c590..68f579b 100644 --- a/Components/PerspectiveComponents/Common/DateTimePicker.py +++ b/Components/PerspectiveComponents/Common/DateTimePicker.py @@ -224,17 +224,17 @@ class CommonDateTimePicker(ComponentPiece): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: List[Tuple[By, str]], - wait_timeout: float = 1, + parent_locator_list: List[Tuple[Union[By, str], str]], + timeout: float = 1, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._next_month_chevron = ComponentPiece( @@ -290,19 +290,39 @@ def __init__( driver=driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) + self.previous_second_chevron = ComponentPiece( + locator=self._PREVIOUS_SECOND_SPINNER_LOCATOR, + driver=driver, + parent_locator_list=self.locator_list, + poll_freq=poll_freq) self.previous_minute_chevron = ComponentPiece( locator=self._PREVIOUS_MINUTE_SPINNER_LOCATOR, driver=driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) self._hours_input = CommonTextInput( - locator=self._HOURS_INPUT_LOCATOR, driver=driver, parent_locator_list=parent_locator_list, wait_timeout=1) + locator=self._HOURS_INPUT_LOCATOR, + driver=driver, + parent_locator_list=parent_locator_list, + timeout=1, + description="The hours input field of the DateTimePicker. This piece of the component is only displayed " + "while a format which include hours is in use.") self._minutes_input = CommonTextInput( - locator=self._MINUTES_INPUT_LOCATOR, driver=driver, parent_locator_list=parent_locator_list, wait_timeout=1) + locator=self._MINUTES_INPUT_LOCATOR, + driver=driver, + parent_locator_list=parent_locator_list, + timeout=1, + description="The minutes input field of the DateTimePicker. This piece of the component is only displayed " + "while a format which include minutes is in use.") self._seconds_input = CommonTextInput( - locator=self._SECONDS_INPUT_LOCATOR, driver=driver, parent_locator_list=parent_locator_list, wait_timeout=1) + locator=self._SECONDS_INPUT_LOCATOR, + driver=driver, + parent_locator_list=parent_locator_list, + timeout=1, + description="The seconds input field of the DateTimePicker. This piece of the component is only displayed " + "while a format which include seconds is in use.") self._am_pm_picker = ComponentPiece( - locator=self._AM_PM_PICKER_LOCATOR, driver=driver, parent_locator_list=parent_locator_list, wait_timeout=1) + locator=self._AM_PM_PICKER_LOCATOR, driver=driver, parent_locator_list=parent_locator_list, timeout=1) self._selected_days = ComponentPiece( locator=self._SELECTED_DAYS_SELECTOR, driver=driver, parent_locator_list=self.locator_list) @@ -331,13 +351,19 @@ def get_enabled_days(self) -> List[int]: """ return [int(_.text) for _ in self._enabled_days.find_all()] - def get_hours(self) -> int: + def get_hours(self) -> Optional[int]: """ Obtain the current numeric hour selection. + :returns: The current hours value of the picker, or None if the DateTimePicker is not formatted to + display seconds. + :raises TimeoutException: If the current format is preventing the display of the hours input. """ - return int(self._hours_input.find().get_attribute("value")) + try: + return int(self._hours_input.find().get_attribute("value")) + except TimeoutException: + return None def get_list_of_available_months(self) -> List[str]: """ @@ -359,13 +385,19 @@ def get_list_of_available_years(self) -> List[int]: enabled_options = list(filter(lambda e: e.is_enabled(), all_options)) return [int(option.text) for option in enabled_options] - def get_minutes(self) -> int: + def get_minutes(self) -> Optional[int]: """ Obtain the current minute value in use by the picker. + :returns: The current minutes value of the picker, or None if the DateTimePicker is not formatted to + display seconds. + :raises TimeoutException: If the current format is preventing the display of the minutes input. """ - return int(self._minutes_input.find().get_attribute("value")) + try: + return int(self._minutes_input.find().get_attribute("value")) + except TimeoutException: + return None def get_selected_days(self) -> List[int]: """ @@ -532,10 +564,10 @@ def select_time_only_not_date(self, date: PerspectiveDate) -> None: if seconds is not None: current_seconds = int(self.get_seconds()) if self._seconds_input.find().is_enabled(): - target = self.previous_minute_chevron if int(seconds) < int(current_seconds) else \ + target = self.previous_second_chevron if int(seconds) < int(current_seconds) else \ self.next_second_chevron while current_seconds != int(seconds): - target.click(binding_wait_time=0.5) + target.click(wait_after_click=0.5) new_seconds = int(self.get_seconds()) if new_seconds != current_seconds: current_seconds = new_seconds @@ -554,7 +586,7 @@ def select_time_only_not_date(self, date: PerspectiveDate) -> None: target = self.previous_minute_chevron if int(minutes) < int(current_minutes) else \ self.next_minute_chevron while current_minutes != int(minutes): - target.click(binding_wait_time=0.5) + target.click(wait_after_click=0.5) new_minutes = int(self.get_minutes()) if new_minutes != current_minutes: current_minutes = new_minutes diff --git a/Components/PerspectiveComponents/Common/Table.py b/Components/PerspectiveComponents/Common/Table.py index 7803638..d4f2da1 100644 --- a/Components/PerspectiveComponents/Common/Table.py +++ b/Components/PerspectiveComponents/Common/Table.py @@ -1,5 +1,4 @@ -from typing import List, Tuple -from typing import Optional +from typing import List, Tuple, Optional, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -21,17 +20,17 @@ class Table(ComponentPiece): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, parent_locator_list: Optional[List] = None, - wait_timeout: float = 10, + timeout: float = 10, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._body = Body(driver=driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) @@ -333,3 +332,25 @@ def hover_over_column_header(self, column_index: int) -> None: :raises TimeoutException: If no column exists with the supplied index, or if the Table contains no data. """ self._header.hover_over_column_in_header(column_index=column_index) + + def wait_for_row_count( + self, + include_expanded_subviews_in_count: bool = False, + expected_count: Optional[int] = None, + timeout: Optional[float] = None): + """ + Obtain a count of rows in the Table. + + :param include_expanded_subviews_in_count: If True, expanded Subviews will be included in the count fo rows. + :param expected_count: If supplied, the function will wait some short period of time until this number of rows + appears. + :param timeout: How long to wait up to for the table to have the expected count. A value of None will fall back + to using the wait configured for the component. A value of 0 will immediately return the count of rows + without waiting. + + :returns: A count of rows in the Alarm Table. + """ + return self._body.wait_for_row_count( + include_expanded_subviews_in_count=include_expanded_subviews_in_count, + expected_count=expected_count, + timeout=timeout) diff --git a/Components/PerspectiveComponents/Common/TablePieces/Body.py b/Components/PerspectiveComponents/Common/TablePieces/Body.py index 2631174..ab9c59b 100644 --- a/Components/PerspectiveComponents/Common/TablePieces/Body.py +++ b/Components/PerspectiveComponents/Common/TablePieces/Body.py @@ -3,6 +3,7 @@ from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.wait import WebDriverWait from Components.BasicComponent import ComponentPiece @@ -14,22 +15,23 @@ class Body(ComponentPiece): """The body of a table, which contains information and interactions for rows and cells.""" BODY_CELL_CSS = 'div.tc' - BODY_ROW_CSS = 'div.ia_table__body__row' + BODY_ROWS_AND_SUBVIEW_CSS = 'div.ia_table__body__row' + BODY_ROW_CSS = f'{BODY_ROWS_AND_SUBVIEW_CSS}:not(.subview)' ROW_GROUP_CSS = 'div.tr-group' _EMPTY_MESSAGE_LOCATOR = (By.CSS_SELECTOR, 'div.emptyMessage') def __init__( self, driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=(By.CSS_SELECTOR, "div.tb"), driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._empty_message = ComponentPiece( @@ -47,6 +49,11 @@ def __init__( driver=driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) + self._rows_and_sub_views = ComponentPiece( + locator=(By.CSS_SELECTOR, self.BODY_ROWS_AND_SUBVIEW_CSS), + driver=driver, + parent_locator_list=self.locator_list, + poll_freq=poll_freq) self._cells = ComponentPiece( locator=(By.CSS_SELECTOR, self.BODY_CELL_CSS), driver=driver, @@ -221,7 +228,7 @@ def get_row_index_of_first_row_with_value_in_column( poll_freq=self.poll_freq).find_all() for cell in _cells: if cell.text == str(known_value): - return int(cell.get_attribute("data-row-index")) + return self._get_row_index(web_element=cell) return None def get_row_count( @@ -235,10 +242,8 @@ def get_row_count( :returns: A count of rows in the table, or possibly a count of rows which includes expanded subviews. """ try: - rows = self._rows.find_all(wait_timeout=1) - if not include_expanded_subviews_in_count: - rows = [row for row in rows if "subview" not in row.get_attribute("class")] - return len(rows) + return len(self._rows_and_sub_views.find_all(timeout=0)) if include_expanded_subviews_in_count\ + else len(self._rows.find_all(timeout=0)) except TimeoutException: return 0 @@ -306,7 +311,7 @@ def get_row_width(self, include_units: bool = False) -> str: """ return self._rows.get_computed_width(include_units=include_units) - def wait_for_cell_to_have_text_by_row_index_column_id( + def wait_for_cell_to_have_text( self, zero_based_row_index: Union[int, str], column_id: str, @@ -335,7 +340,7 @@ def wait_for_cell_to_have_text_by_row_index_column_id( driver=self.driver, parent_locator_list=self.locator_list, poll_freq=self.poll_freq).wait_on_text_condition( - text_to_compare=text, condition=TextCondition.EQUALS, wait_timeout=timeout) + text_to_compare=text, condition=TextCondition.EQUALS, timeout=timeout) def wait_for_row_count( self, @@ -375,3 +380,7 @@ def has_expected_row_count(): except TimeoutException: pass return row_count + + @classmethod + def _get_row_index(cls, web_element: WebElement) -> int: + return int(web_element.get_attribute("data-row-index")) diff --git a/Components/PerspectiveComponents/Common/TablePieces/Filter.py b/Components/PerspectiveComponents/Common/TablePieces/Filter.py index 7188672..b706d19 100644 --- a/Components/PerspectiveComponents/Common/TablePieces/Filter.py +++ b/Components/PerspectiveComponents/Common/TablePieces/Filter.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Optional +from typing import List, Tuple, Optional, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -22,28 +22,28 @@ class Filter(ComponentPiece): def __init__( self, driver: WebDriver, - parent_locator_list: List[Tuple[By, str]] = None, - wait_timeout: float = 1, + parent_locator_list: List[Tuple[Union[By, str], str]] = None, + timeout: float = 1, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=(By.CSS_SELECTOR, 'div[class*="Filter"]'), driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._filter_input = CommonTextInput( locator=self._FILTER_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) self._results_label = ComponentPiece( locator=self._RESULTS_LABEL_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) def get_results_text(self) -> str: @@ -77,12 +77,12 @@ def results_text_content_is_displayed(self) -> bool: except TimeoutException: return False - def set_filter_text(self, text: str, binding_wait_time: float = 0) -> None: + def set_filter_text(self, text: str, wait_after: float = 0) -> None: """ Set the text filter to contain some text. :param text: The text you want to supply to the filter of the Table. - :param binding_wait_time: how long (in seconds) after applying the text to the filter you would like to wait + :param wait_after: how long (in seconds) after applying the text to the filter you would like to wait before allowing code to continue. :raises TimeoutException: If the filter input is not present. @@ -93,7 +93,7 @@ def set_filter_text(self, text: str, binding_wait_time: float = 0) -> None: text_accounting_for_empty_str = Keys.SPACE + Keys.BACK_SPACE + Keys.ENTER if text == '' else text self._filter_input.set_text( text=text_accounting_for_empty_str, - binding_wait_time=binding_wait_time) + wait_after=wait_after) IAAssert.is_equal_to( actual_value=self._filter_input.wait_on_text_condition( text_to_compare=text, condition=TextCondition.EQUALS), diff --git a/Components/PerspectiveComponents/Common/TablePieces/HeaderAndFooter.py b/Components/PerspectiveComponents/Common/TablePieces/HeaderAndFooter.py index 6d3f90f..66a0ef3 100644 --- a/Components/PerspectiveComponents/Common/TablePieces/HeaderAndFooter.py +++ b/Components/PerspectiveComponents/Common/TablePieces/HeaderAndFooter.py @@ -1,5 +1,4 @@ -from typing import List, Optional, Tuple -from typing import Union +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException, NoSuchElementException from selenium.webdriver.common.by import By @@ -7,11 +6,11 @@ from Components.BasicComponent import ComponentPiece from Components.Common.Button import CommonButton +from Components.Common.Dropdown import CommonDropdown from Components.Common.TextInput import CommonTextInput from Components.PerspectiveComponents.Common.ComponentModal import ComponentModal from Components.PerspectiveComponents.Common.DateRangeSelector \ import CommonDateRangeSelector, HistoricalRange, PerspectiveDate -from Components.PerspectiveComponents.Common.Dropdown import CommonDropdown from Helpers.CSSEnumerations import CSS from Helpers.IAAssert import IAAssert from Helpers.IAExpectedConditions.IAExpectedConditions import TextCondition @@ -38,12 +37,12 @@ class FilterModal(ComponentModal): _DATE_RANGE_PICKER_LOCATOR = (By.CSS_SELECTOR, "div.iaDateRangeTimePicker") def __init__(self, driver: WebDriver, description: Optional[str] = None): - super().__init__(driver=driver, wait_timeout=1, description=description) + super().__init__(driver=driver, timeout=1, description=description) self._condition_dd = CommonDropdown( locator=self._CONDITION_DROPDOWN_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1) + timeout=1) self._basic_value_input = CommonTextInput( locator=self._BASIC_VALUE_INPUT_LOCATOR, driver=driver, parent_locator_list=self.locator_list) self._remove_filter_link = ComponentPiece( @@ -80,7 +79,7 @@ def click_remove_filter(self) -> None: :raises TimeoutException: If the 'Remove Filter' link is not present. """ - self._remove_filter_link.scroll_to_element(align_to_top=True) + self._remove_filter_link.scroll_to_element() self._remove_filter_link.click() def end_time_hours_input_is_enabled(self) -> bool: @@ -327,7 +326,7 @@ def set_value_for_condition(self, text: Union[int, str]) -> None: :raises TimeoutException: if the value input field is not present. :raises AssertionError: If we fail to set the input to the supplied value. """ - self._basic_value_input.set_text(text=text, binding_wait_time=1) + self._basic_value_input.set_text(text=text, wait_after=1) IAAssert.is_equal_to( actual_value=self._basic_value_input.wait_on_text_condition( text_to_compare=text, condition=TextCondition.EQUALS), @@ -387,15 +386,15 @@ class Footer(ComponentPiece): def __init__( self, driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 1, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 1, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=self._FOOTER_CONTAINER_LOCATOR, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._footer_rows = ComponentPiece( @@ -435,15 +434,15 @@ class Header(ComponentPiece): def __init__( self, driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 1, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 1, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=self._HEADER_CONTAINER_LOCATOR, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._header_rows = ComponentPiece( diff --git a/Components/PerspectiveComponents/Common/TablePieces/Pager.py b/Components/PerspectiveComponents/Common/TablePieces/Pager.py index 7e4048b..0f152e1 100644 --- a/Components/PerspectiveComponents/Common/TablePieces/Pager.py +++ b/Components/PerspectiveComponents/Common/TablePieces/Pager.py @@ -1,14 +1,14 @@ from enum import Enum from typing import List, Optional, Tuple, Union -from selenium.common.exceptions import TimeoutException, StaleElementReferenceException +from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.support.select import Select from selenium.webdriver.support.wait import WebDriverWait -from Components.BasicComponent import ComponentPiece +from Components.BasicComponent import BasicComponent, ComponentPiece from Helpers.IAAssert import IAAssert from Helpers.IAExpectedConditions import IAExpectedConditions as IAec @@ -35,7 +35,7 @@ class Location(Enum): def __init__( self, driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( @@ -97,7 +97,7 @@ def click_first_page_button(self) -> None: :raises TimeoutException: If the 'First' page option is not present in the Pager. :raises AssertionError: If clicking the 'First' page button does result in the Table displaying the first page. """ - self._first_page_button.click(binding_wait_time=0.5) + self._first_page_button.click(wait_after_click=0.5) if len(self.get_all_listed_pages()) > 0: IAAssert.is_equal_to( actual_value=self.get_active_page(), @@ -111,26 +111,27 @@ def click_last_page_button(self) -> None: :raises TimeoutException: If the 'Last' page option is not present in the Pager. """ - self._last_page_button.click(binding_wait_time=0.5) + self._last_page_button.click(wait_after_click=0.5) # unable to assert destination page number because of the dynamic length of data. - def click_next_page_chevron(self, binding_wait_time: float = 0.5) -> None: + def click_next_page_chevron(self, wait_after_click: float = 0.5) -> None: """ Click the chevron which would take the user to the next page. - :param binding_wait_time: The amount of time (in seconds) after clicking the next page chevron before allowing + :param wait_after_click: The amount of time (in seconds) after clicking the next page chevron before allowing code to continue. :raises TimeoutException: If the 'next' chevron does not exist, or if no page numbers exist in the Pager. """ original_page_number = self.get_active_page() - self._next_page_chevron.click(binding_wait_time=binding_wait_time) + self._next_page_chevron.click(wait_after_click=wait_after_click) IAAssert.is_equal_to( actual_value=self.get_active_page(), expected_value=original_page_number + 1, as_type=int, failure_msg="We failed to get to the next page after clicking the next page chevron.") + @BasicComponent.retry_on_stale_element() def click_page_number(self, desired_page: Union[int, str]) -> None: """ Click a page number within the Pager. @@ -148,17 +149,17 @@ def click_page_number(self, desired_page: Union[int, str]) -> None: as_type=int, failure_msg=f"After clicking the option for page {desired_page}, we failed to display that page.") - def click_previous_page_chevron(self, binding_wait_time: float = 0.5) -> None: + def click_previous_page_chevron(self, wait_after_click: float = 0.5) -> None: """ Click the chevron which would take the user to the previous page. - :param binding_wait_time: The amount of time (in seconds) after clicking the previous page chevron before + :param wait_after_click: The amount of time (in seconds) after clicking the previous page chevron before allowing code to continue. :raises TimeoutException: If the 'previous' chevron does not exist. """ original_page_number = self.get_active_page() - self._previous_page_chevron.click(binding_wait_time=binding_wait_time) + self._previous_page_chevron.click(wait_after_click=wait_after_click) IAAssert.is_equal_to( actual_value=self.get_active_page(), expected_value=original_page_number - 1, @@ -172,10 +173,11 @@ def first_page_button_is_displayed(self) -> bool: :returns: True, if a user can see the 'First' page option in the Pager. """ try: - return self._first_page_button.find().is_displayed() + return self._first_page_button.is_displayed() except TimeoutException: return False + @BasicComponent.retry_on_stale_element() def first_page_button_is_enabled(self) -> bool: """ Determine if the 'First' page option is enabled in the Pager. @@ -207,6 +209,7 @@ def get_all_listed_pages(self) -> List[int]: except TimeoutException: return [] + @BasicComponent.retry_on_stale_element() def get_all_row_display_options(self) -> List[str]: """ Obtain all selection options form the dropdown which controls how many rows may be displayed in the Table. @@ -218,6 +221,7 @@ def get_all_row_display_options(self) -> List[str]: """ return [_.text for _ in Select(webelement=self._row_count_select.find()).options] + @BasicComponent.retry_on_stale_element() def get_last_displayed_page_number(self) -> int: """ Obtain the number of the last page available in the Pager. This value may not be the count of Pages of data, @@ -232,6 +236,7 @@ def get_last_displayed_page_number(self) -> int: """ return int(self._page_numbers.find_all()[-1].text) + @BasicComponent.retry_on_stale_element() def get_selected_row_count_option_from_dropdown(self) -> int: """ Obtain the currently selected VALUE from the dropdown which dictates how many rows are displayed. @@ -251,16 +256,17 @@ def jump_input_is_displayed(self) -> bool: :returns: True, if the user is able to type a page number into the Pager - False otherwise. """ try: - return self._page_jump_input.find().is_displayed() + return self._page_jump_input.is_displayed() except TimeoutException: return False - def jump_to_page(self, page_to_jump_to: Union[int, str], binding_wait_time: float = 1) -> None: + @BasicComponent.retry_on_stale_element() + def jump_to_page(self, page_to_jump_to: Union[int, str], wait_after: float = 1) -> None: """ Use the page jump input field to go to a specific page in the Table. :param page_to_jump_to: The page of the Table to go to. - :param binding_wait_time: The amount of time (in seconds) to wait after supplying the page number before + :param wait_after: The amount of time (in seconds) to wait after supplying the page number before allowing code to continue. :raises TimeoutException: If the page jump input is not present. @@ -269,7 +275,7 @@ def jump_to_page(self, page_to_jump_to: Union[int, str], binding_wait_time: floa self._page_jump_input.find().click() self.driver.execute_script('arguments[0].value = ""', self._page_jump_input.find()) self._page_jump_input.find().send_keys(str(page_to_jump_to) + Keys.ENTER) - self._page_jump_input.wait_on_binding(binding_wait_time) + self._page_jump_input.wait_some_time(wait_after) # we can only manage the following assertion if page numbers are listed if len(self.get_all_listed_pages()) > 0: IAAssert.is_equal_to( @@ -285,7 +291,7 @@ def last_page_button_is_displayed(self) -> bool: :returns: True, if a user can see the 'Last' page option in the Pager. """ try: - return self._last_page_button.find().is_displayed() + return self._last_page_button.is_displayed() except TimeoutException: return False @@ -304,6 +310,7 @@ def last_page_button_is_enabled(self) -> bool: except TimeoutException: return False + @BasicComponent.retry_on_stale_element() def next_page_chevron_is_enabled(self) -> bool: """ Determine if the next page chevron is enabled. @@ -326,11 +333,12 @@ def is_displayed(self, location: Location = Location.BOTTOM) -> bool: locator=(By.CSS_SELECTOR, f'{self._PAGER_LOCATOR[1]}.{location.value}'), driver=self.driver, parent_locator_list=self._parent_locator_list, - wait_timeout=0, - poll_freq=self.poll_freq).find().is_displayed() + timeout=0, + poll_freq=self.poll_freq).is_displayed() except TimeoutException: return False + @BasicComponent.retry_on_stale_element() def is_hidden(self) -> bool: """ Determine if the Pager is currently hidden. @@ -349,10 +357,11 @@ def is_present(self) -> bool: :returns: True, if the Pager exists - False otherwise. """ try: - return self.find(wait_timeout=0.5) is not None + return self.find(timeout=0.5) is not None except TimeoutException: return False + @BasicComponent.retry_on_stale_element() def page_number_is_displayed(self, desired_page: Union[int, str]) -> bool: """ Determine if a specific page number is currently displayed. @@ -365,6 +374,7 @@ def page_number_is_displayed(self, desired_page: Union[int, str]) -> bool: """ return str(desired_page) in [_.text for _ in self._page_numbers.find_all()] + @BasicComponent.retry_on_stale_element() def previous_page_chevron_is_enabled(self) -> bool: """ Determine if the previous page chevron is enabled. @@ -382,10 +392,11 @@ def row_select_dropdown_is_displayed(self) -> bool: :returns: True, if the row count dropdown is displayed - False otherwise. """ try: - return self._row_count_select.find().is_displayed() - except (TimeoutException, StaleElementReferenceException): + return self._row_count_select.is_displayed() + except TimeoutException: return False + @BasicComponent.retry_on_stale_element() def set_displayed_row_count(self, count_of_rows: Union[int, str], attempted=False) -> None: """ Set the number of displayed rows in the Table. The count of rows must be a valid value from the @@ -399,7 +410,7 @@ def set_displayed_row_count(self, count_of_rows: Union[int, str], attempted=Fals :raises AssertionError: If unable to set the row count dropdown to the supplied row count. """ Select(webelement=self._row_count_select.find()).select_by_value(str(count_of_rows)) - self.wait_on_binding(time_to_wait=1) + self.wait_some_time(time_to_wait=1) if str(count_of_rows) != str(self.get_selected_row_count_option_from_dropdown()): if not attempted: self.set_displayed_row_count(count_of_rows, attempted=True) @@ -412,6 +423,7 @@ def set_displayed_row_count(self, count_of_rows: Union[int, str], attempted=Fals as_type=int, failure_msg="We failed to specify a count of rows to select.") + @BasicComponent.retry_on_stale_element() def _last_page_button_is_enabled(self) -> bool: """Determine if the 'Last' page button is enabled.""" return 'disabled' not in self._last_page_button.find().get_attribute('class') diff --git a/Components/PerspectiveComponents/Common/TagBrowseTree.py b/Components/PerspectiveComponents/Common/TagBrowseTree.py index b425596..85b14d6 100644 --- a/Components/PerspectiveComponents/Common/TagBrowseTree.py +++ b/Components/PerspectiveComponents/Common/TagBrowseTree.py @@ -1,13 +1,13 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import BasicPerspectiveComponent, ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon -from Components.PerspectiveComponents.Common.Tree import CommonTree +from Components.Common.Icon import CommonIcon from Components.Common.TextInput import CommonTextInput +from Components.PerspectiveComponents.Common.Tree import CommonTree class CommonTagBrowseTree(CommonTree, BasicPerspectiveComponent): @@ -17,10 +17,10 @@ class CommonTagBrowseTree(CommonTree, BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, description: Optional[str] = None, poll_freq: float = 0.5): CommonTree.__init__( @@ -30,7 +30,7 @@ def __init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._no_results = ComponentPiece( @@ -50,13 +50,13 @@ def __init__( poll_freq=poll_freq ) - def click_refresh_icon(self, binding_wait_time=5) -> None: + def click_refresh_icon(self, wait_after_click=5) -> None: """ Click the refresh icon of the Tag Browse Tree. :raises TimeoutException: If the refresh icon is not present. """ - self._reload_icon.click(binding_wait_time=binding_wait_time) + self._reload_icon.click(wait_after_click=wait_after_click) def get_refresh_icon_path(self) -> str: """ @@ -66,30 +66,30 @@ def get_refresh_icon_path(self) -> str: """ return self._reload_icon.get_icon_name() - def no_results_message_is_displayed(self, wait_timeout=5) -> bool: + def no_results_message_is_displayed(self, timeout=5) -> bool: """ Determine if the Tag Browse Tree is currently displaying a message conveying that no results are found for the configured provider. - :param wait_timeout: The amount of time (in seconds) to wait for the message to appear. + :param timeout: The amount of time (in seconds) to wait for the message to appear. :returns: True, if the message is displayed - False otherwise. """ try: - return self._no_results.find(wait_timeout=wait_timeout).is_displayed() + return self._no_results.find(timeout=timeout).is_displayed() except TimeoutException: return False - def refresh_icon_is_displayed(self, wait_timeout=5) -> bool: + def refresh_icon_is_displayed(self, timeout=5) -> bool: """ Determine if the refresh icon is currently displayed. - :param wait_timeout: The amount of time (in seconds) to wait for the icon to appear. + :param timeout: The amount of time (in seconds) to wait for the icon to appear. :returns: True, if the icon is displayed - False otherwise. """ try: - return self._reload_icon.find(wait_timeout=wait_timeout).is_displayed() + return self._reload_icon.find(timeout=timeout).is_displayed() except TimeoutException: return False @@ -99,7 +99,7 @@ def set_filter_text(self, filter_text: str) -> None: :param filter_text: The text we input to filter the tags. """ - self._filter_tf.set_text(text=filter_text, binding_wait_time=0.5) + self._filter_tf.set_text(text=filter_text, wait_after=0.5) def filter_text_field_is_displayed(self) -> bool: """ @@ -108,6 +108,6 @@ def filter_text_field_is_displayed(self) -> bool: :returns: True, if the Filter text field is displayed - False otherwise. """ try: - return self._filter_tf.find(wait_timeout=0).is_displayed() + return self._filter_tf.find(timeout=0).is_displayed() except TimeoutException: return False diff --git a/Components/PerspectiveComponents/Common/Tree.py b/Components/PerspectiveComponents/Common/Tree.py index b9513c7..84d2f26 100644 --- a/Components/PerspectiveComponents/Common/Tree.py +++ b/Components/PerspectiveComponents/Common/Tree.py @@ -1,25 +1,107 @@ import sys -from typing import List, Optional, Tuple +import time +from typing import List, Optional, Tuple, Self, Union -from selenium.common.exceptions import TimeoutException +from selenium.common.exceptions import TimeoutException, StaleElementReferenceException from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon + +_ITEM_CLASS = "tree-item" +_TREE_ITEM_LOCATOR = (By.CSS_SELECTOR, f".{_ITEM_CLASS}") + + +class Item: + _locator = None + _ancestry = None + _path = None + """ + Items are understood to be the individual line items within a Perspective Tree component. They are most helpful + when their parent (and the parent of their parent, and so on) is also supplied. On the back-end, we build a + locator for the Item based off of its derived ancestry. + If failing to use or locate an Item, make sure you are supplying the EXACT ancestry, and that each ancestor is + expanded. + """ + + def __init__(self, label_text: str, parent: Optional[Self] = None, zero_indexed_path: Optional[str] = None): + self.label = label_text + self.parent = parent + self.index_path = zero_indexed_path + self._build_ancestry() # ancestry must be derived first because both the locator and path are dependent + self._build_locator() + self._build_path() + + def get_ancestry(self) -> List[Self]: + """ + Obtain a list of Items, where each Item in the list describes the ancestry chain of the Item. + + Example: Consider an Item with label text of "Child", and a parent with label text of "Parent". The "Parent" + item also has a parent with label text of "Grandparent". The returned text of the Items in the returned + list would be ["Grandparent", "Parent", "Child"]. + + :return: A list of Items, with the last item in the list being the direct parent of this Item + """ + return self._ancestry + + def get_locator(self) -> Tuple[Union[By, str], str]: + """ + Obtain the locator which describes this exact Item in the Tree based on its derived ancestry. + + :return: A tuple where the 0th item is a By locator type and the 1th item is a CSS string. + """ + return self._locator + + def get_path(self) -> str: + """ + Obtain the label text path which describes the route used to locate this Item. Not to be confused with + index_path, which is derived within Perspective and which represents the location of Items based on a numeric + index. Primarily used for troubleshooting. + + :return: A slash-delimited string where each slash separates the label text of an item and its parent. + """ + return self._path + + def _build_ancestry(self) -> None: + ancestry = [] + _parent = self.parent + while _parent is not None and len(_parent.label) > 0: + ancestry.insert(0, _parent) + _parent = _parent.parent + self._ancestry = ancestry + + def _build_locator(self) -> None: + path_pieces = [item.label for item in self.get_ancestry()] + path_pieces.append(self.label) + # all pieces of path get a parent-node + ancestry_css_pieces = [ + f'[class*="-node"][data-label="{item_label}"]' for item_label in path_pieces] + # and now the last entry should be the tree item + terminal_css_piece = f'{_TREE_ITEM_LOCATOR[1]}[data-label="{path_pieces[-1]}"]' + self._locator = By.CSS_SELECTOR, " ".join(ancestry_css_pieces + [terminal_css_piece]) + + def _build_path(self) -> None: + self._path = "/".join([item.label for item in self.get_ancestry()] + [self.label]) + + def __str__(self): + return self._path class CommonTree(ComponentPiece): """ A Tree of items, as is found as part of the Tree Component, the Tag Browse Tree, and the Power Chart. - Selenium is limited in how it locates items for interaction, and so if two visible nodes share the same label text - the first (top-most) item is the one which will be interacted with. - - https://youtrack.ia.local/issue/IGN-7575 exists to move the data-label attribute up higher, which would allow - for us to target specific items which have duplicated text by item path instead of zero-index. + Prior to 8.1.39, Selenium was unable to discern Tree items which shared names. We allowed for interacting with + items by their label name, but if two displayed items in the tree had a label of "X", Selenium would always + interact with the highest item. + + In 8.1.39, a new locator was added which allowed for a much better location strategy within the component. + Functions which previously accepted labels for interaction now require an Item object. Items behave as something + approaching a linked list, where an Item knows about its parent, and that parent knows about its own parent. This + allows for us to build a highly specific locator at runtime. """ _EXPANDED_ICON_CLASS = "ia_treeComponent__expandIcon--expanded" _FOLDER_EXPAND_ICON_LOCATOR = ( @@ -27,22 +109,20 @@ class CommonTree(ComponentPiece): _LABEL_LOCATOR = ( By.CSS_SELECTOR, 'div.label-wrapper-text') _TOP_LEVEL_ITEMS_LOCATOR = (By.CSS_SELECTOR, 'div.tree>div>div>div.tree-row') - _ITEM_CLASS = "tree-item" - _TREE_ITEM_LOCATOR = (By.CSS_SELECTOR, f".{_ITEM_CLASS}") def __init__( self, driver: WebDriver, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], parent_locator_list: Optional[List] = None, - wait_timeout: float = 3, + timeout: float = 3, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._folder_icons = ComponentPiece( @@ -61,7 +141,7 @@ def __init__( parent_locator_list=self.locator_list, poll_freq=poll_freq) self._tree_items = ComponentPiece( - locator=self._TREE_ITEM_LOCATOR, + locator=_TREE_ITEM_LOCATOR, driver=self.driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) @@ -70,130 +150,166 @@ def __init__( self._expand_collapse_icons = {} self._labels = {} - def click_expansion_icon_for_item(self, item_label: str, binding_wait_time: int = 0) -> None: + def click_expansion_icon(self, item: Item, wait_after_click: int = 0) -> None: """ - Click the expand icon for an item regardless of expansion state. If you are attempting to set the expansion + Click the expand icon for an Item regardless of expansion state. If you are attempting to set the expansion state, you should use :func:`set_expansion_state_for_item` - :param item_label: The text of the item for which we will click the expansion icon. - :param binding_wait_time: The amount of time (in seconds) to wait after the click event before allowing code to + :param item: The Item for which we will click the expansion icon. + :param wait_after_click: The amount of time (in seconds) to wait after the click event before allowing code to continue. - :raises TimeoutException: If no icon exists or if no item with the supplied label exists. + :raises TimeoutException: If no icon exists or if no Item with the supplied label exists. """ - self._get_expansion_icon(item_label=item_label).click(binding_wait_time=binding_wait_time) + self._get_expansion_icon(item=item).click(wait_after_click=wait_after_click) - def click_item_label(self, item_label: str, binding_wait_time: float = 0) -> None: + def click_item(self, item: Item, wait_after_click: float = 0) -> None: """ - Click the label of an item. + Click the label of an Item. - :param item_label: The text of the item you would like to click. - :param binding_wait_time: The amount of time (in seconds) to wait after the click event before allowing code to + :param item: The Item you would like to click. + :param wait_after_click: The amount of time (in seconds) to wait after the click event before allowing code to continue. - :raises TimeoutException: If no item with the supplied text is available to be clicked. This could either mean - no item with that text exists, or the target item could be inside an item which has not yet been expanded. + :raises TimeoutException: If no Item with the same text is available to be clicked. This could either mean + no Item with that text exists, or the target Item could be inside an Item which has not yet been expanded. """ - label = self._labels.get(item_label) + path = str(item) + label = self._labels.get(path) if not label: label = ComponentPiece( locator=self._LABEL_LOCATOR, driver=self.driver, - parent_locator_list=self._get_item_by_label(item_label=item_label).locator_list, + parent_locator_list=self._get_item(item=item).locator_list, poll_freq=self.poll_freq) - self._labels[item_label] = label - label.click(binding_wait_time=binding_wait_time) + self._labels[path] = label + label.click(wait_after_click=wait_after_click) + + def expand_to_item(self, item: Item, + max_attempts: int = 3, + delay_between_attempts: float = 1.0) -> None: + """ + Expand the Tree along the path of the Item until the Item is displayed. If the target item is not displayed + after the configured number of attempts, raise a TimeoutException. + This function does not actually click the supplied Item. + + :param item: The Item to which the Tree should be expanded. + :param max_attempts: The maximum number of full attempts. + :param delay_between_attempts: The delay in seconds between retry attempts. - def expansion_icon_is_present_for_item(self, item_label: str) -> bool: + :raises TimeoutException: If any part of the ancestry of the Item was not found, or if the Item itself was + not displayed after expanding the ancestry of the Item, or if expansion fails after max_attempts. + """ + for attempt in range(1, max_attempts + 1): + try: + for path_item in item.get_ancestry(): + self.set_expansion_state(item=path_item, should_be_expanded=True) + # If we reach here without exception, check if item is displayed + if self.item_is_displayed(item=item): + break + except (TimeoutException, StaleElementReferenceException) as e: + if attempt >= max_attempts: + raise TimeoutException( + f"Could not expand ancestry to display item {item} after {max_attempts} attempts" + ) from e + else: + time.sleep(delay_between_attempts) + + def expansion_icon_is_displayed(self, item: Item) -> bool: """ - Determine if an item is currently displaying an expansion icon. + Determine if an Item is currently displaying an expansion icon. - :returns: True, if an expansion icon is displayed for the first item with the supplied text. False, if an - expansion icon is not displayed, or if no item with the supplied text exists. + :returns: True, if an expansion icon is displayed for the first Item with the supplied text. False, if an + expansion icon is not displayed, or if no Item with the supplied text exists. """ try: return ComponentPiece( locator=(By.CSS_SELECTOR, "g"), driver=self.driver, - parent_locator_list=self._get_expansion_icon(item_label=item_label).locator_list, - wait_timeout=1, + parent_locator_list=self._get_expansion_icon(item=item).locator_list, + timeout=1, poll_freq=self.poll_freq).find().is_displayed() except TimeoutException: return False - def get_fill_color_of_expansion_icon_for_item(self, item_label: str) -> str: + def get_fill_color_of_expansion_icon(self, item: Item) -> str: """ - Obtain the fill color of the expansion icon for an item. + Obtain the fill color of the expansion icon for an Item. - :returns: The fill color of the expansion icon for the first item with the supplied text as a string. Note that + :returns: The fill color of the expansion icon for the first Item with the supplied text as a string. Note that different browsers may return this string in different formats (RGB vs hex). - :raises TimeoutException: If no expansion icon exists for the item, or if no item with the supplied text exists. + :raises TimeoutException: If no expansion icon exists for the Item, or if no Item with matching text exists. """ - return self._get_expansion_icon(item_label=item_label).get_fill_color() + return self._get_expansion_icon(item=item).get_fill_color() - def get_fill_color_of_icon_for_item(self, item_label: str) -> str: + def get_fill_color_of_icon(self, item: Item) -> str: """ Obtain the fill color of the icon for an item. - :returns: The fill color of the icon for the first item with the supplied text as a string. Note that - different browsers may return this string in different formats (RGB vs hex). + :returns: The fill color of the icon for the Item. Note that different browsers may return this string in + different formats (RGB vs hex). - :raises TimeoutException: If no icon exists for the item, or if no item with the supplied text exists. + :raises TimeoutException: If no icon exists for the item, or if no Item with the supplied text exists. """ - return self._get_item_icon(item_label=item_label).get_fill_color() + return self._get_icon(item=item).get_fill_color() - def get_path_of_expansion_icon_for_item(self, item_label: str) -> str: + def get_path_of_expansion_icon(self, item: Item) -> str: """ - Obtain the path of the expansion icon in use by an item. + Obtain the path of the expansion icon in use by an Item. :returns: A slash-delimited string representing the path of the svg in use. - :raises TimeoutException: If no expansion icon exists for the item, or if no item with the supplied text exists. + :raises TimeoutException: If no expansion icon exists for the Item, or if no Item with matching text exists. """ - return self._get_expansion_icon(item_label=item_label).get_icon_name() + return self._get_expansion_icon(item=item).get_icon_name() - def get_path_of_icon_for_item(self, item_label: str) -> str: + def get_path_of_node_icon(self, item: Item) -> str: """ - Obtain the path of the icon in use by an item. + Obtain the path of the icon in use by an Item. :returns: A slash-delimited string representing the path of the svg in use. - :raises TimeoutException: If no icon exists for the item, or if no item with the supplied text exists. + :raises TimeoutException: If no icon exists for the Item, or if no Item with matching text exists. """ - return self._get_item_icon(item_label=item_label).get_icon_name() + return self._get_icon(item=item).get_icon_name() def get_text_of_top_level_items(self) -> List[str]: """ Obtain the text of all items located at the top-most level. + + :returns: A list of strings, where each entry in the list is the displayed text of an Item at the top-most + level of the Tree. """ try: return [_.text for _ in self._top_level_items.find_all()] except TimeoutException: return [] - def get_text_of_all_items_in_tree(self, wait_timeout: float = 5) -> List[str]: + def get_text_of_all_items_in_tree(self, timeout: float = 5) -> List[str]: """ Obtain a list which contains the text of all displayed items in the Tree. - :param wait_timeout: How long to wait (in seconds) for any tree items to appear. + :returns: A list of strings, where each entry in the list is the displayed text of an Item in the Tree. + + :param timeout: How long to potentially wait (in seconds) for any Tree Items to appear. """ try: - return [item.text for item in self._tree_items.find_all(wait_timeout=wait_timeout)] + return [item.text for item in self._tree_items.find_all(timeout=timeout)] except TimeoutException: return [] - def multi_select_items( - self, item_labels: List[str], inclusive_multi_selection: bool = True, wait_timeout: float = 5) -> None: + def multi_select( + self, items: List[Item], inclusive_multi_selection: bool = True, timeout: float = 5) -> None: """ - Select multiple items within the Tree. Note that any selections previous to this function could be lost. + Select multiple Items within the Tree. Note that any selections previous to this function could be lost. - :param item_labels: A list of item labels to select. + :param items: A list of Items to select. :param inclusive_multi_selection: Dictates whether we are holding Shift (True) or CMD/Ctrl (False) while making the selections. - :param wait_timeout: The amount of time to wait for each item to become available before attempting selection. + :param timeout: The amount of time to wait for each Item to become available before attempting selection. - :raises TimeoutException: If no item exists with text matching any individual entry from the items_list. + :raises TimeoutException: If no Item exists with text matching any individual entry from the supplied list. """ actions = ActionChains(self.driver) if inclusive_multi_selection: @@ -202,134 +318,138 @@ def multi_select_items( mod_key = Keys.COMMAND if sys.platform.startswith('darwin') else Keys.CONTROL actions.key_down(mod_key) - piece_list = [self._get_item_by_label(item_label=_, wait_timeout=wait_timeout) for _ in item_labels] + piece_list = [self._get_item(item=item, timeout=timeout) for item in items] for piece in piece_list: actions.click(on_element=piece.find()).pause(1) actions.key_up(mod_key) actions.perform() - self.wait_on_binding(1) + self.wait_some_time(1) - def item_label_exists_in_tree(self, item_label: str, wait_timeout: int = 5) -> bool: + def item_is_displayed(self, item: Item) -> bool: """ - Determine if an item exists based on its label text. + Determine if an Item is displayed within the Tree. - :param item_label: The label text of the item whose existence you wish to verify. - :param wait_timeout: The amount of time (in seconds) you are willing to wait for the item to appear. + :param item: The Item whose existence you wish to verify. - :returns: True, if any item matches the supplied text - False otherwise. + :returns: True, if the Item is displayed within the Tree - False otherwise. """ try: - return self._get_item_by_label(item_label=item_label, wait_timeout=wait_timeout).find() is not None + return self._get_item(item=item).find(timeout=0).is_displayed() except TimeoutException: return False - def item_is_expanded(self, item_label: str) -> bool: + def item_is_expanded(self, item: Item) -> bool: """ - Determine if an item is expanded. + Determine if an Item is expanded. - :param item_label: The label text of the item we will check the expanded status of. + :param item: The Item we will check the expanded status of. - :raises TimeoutException: If the item we check does not have an expansion icon. This likely means that the - specified item is a terminal node and may not be expanded. + :raises TimeoutException: If the Item we check does not have an expansion icon. This likely means that the + specified Item is a terminal node and may not be expanded. """ try: return self._EXPANDED_ICON_CLASS in self._get_expansion_icon( - item_label=item_label).find().get_attribute(name="class") + item=item).find(timeout=0).get_attribute(name="class") except TimeoutException: return False - def node_icon_is_present_for_item(self, item_label: str) -> bool: + def node_icon_is_displayed(self, item: Item) -> bool: """ - Determine if an item is currently displaying an icon which would convey whether the item is a + Determine if an Item is currently displaying an icon which would convey whether the Item is a directory/folder or a terminal node. - :param item_label: The text of the item we will check for a node icon. + :param item: The Item we will check for a node icon. - :returns: True, if an icon which would convey the type of the item is displayed for the first item with the - supplied text. False, if an icon is not displayed, or if no item with the supplied text exists. + :returns: True, if an icon which would convey the type of the Item is displayed for the Item. False, if an + icon is not displayed, or if no Item with matching text exists. """ try: return ComponentPiece( locator=(By.CSS_SELECTOR, "g"), driver=self.driver, - parent_locator_list=self._get_item_icon(item_label=item_label).locator_list, - wait_timeout=1, + parent_locator_list=self._get_icon(item=item).locator_list, + timeout=0, poll_freq=self.poll_freq).find().is_displayed() except TimeoutException: return False - def select_item_in_tree(self, item_label: str, wait_timeout: int = 5, binding_wait_time: int = 1) -> None: + def select_item(self, item: Item, timeout: int = 5, wait_after_click: int = 1) -> None: """ - Select a single item in the Tree. + Select a single Item in the Tree. - :param item_label: The label text of the item to click. - :param wait_timeout: The amount of time (in seconds) to wait for the item to appear. - :param binding_wait_time: The amount of time (in seconds) after clicking the item to wait before allowing code + :param item: The Item to click. + :param timeout: The amount of time (in seconds) to potentially wait for the Item to appear. + :param wait_after_click: The amount of time (in seconds) to wait after clicking the Item before allowing code to continue. - :raises TimeoutException: If no item with the supplied text exists. + :raises TimeoutException: If no Item with matching text exists. """ - self._get_item_by_label( - item_label=item_label, wait_timeout=wait_timeout).click(binding_wait_time=binding_wait_time) + self.expand_to_item(item=item) + self._get_item(item=item, timeout=timeout).click(wait_after_click=wait_after_click) - def set_expansion_state_for_item(self, item_label: str, should_be_expanded: bool) -> None: + def set_expansion_state(self, item: Item, should_be_expanded: bool) -> None: """ - Set an item to be expanded or collapsed. + Set an Item to be expanded or collapsed. - :param item_label: The label text of the item to expand or collapse. - :param should_be_expanded: A True value specifies the item should be expanded, while a False value specifies the - item should be collapsed. + :param item: The Item to expand or collapse. + :param should_be_expanded: A True value specifies the Item should be expanded, while a False value specifies the + Item should be collapsed. - :raises TimeoutException: If no item with the supplied text exists, or if the specified item has no expansion + :raises TimeoutException: If no Item with matching text exists, or if the specified Item has no expansion icon. """ - if self.item_is_expanded(item_label=item_label) != should_be_expanded: + self.expand_to_item(item=item) + if self.item_is_expanded(item=item) != should_be_expanded: try: - self._get_expansion_icon(item_label=item_label).click() + self._get_expansion_icon(item=item).click(wait_after_click=0.5) # allow UI to catch up after except TimeoutException as toe: - toe.msg = f"Node with label of '{item_label}' does not contain an expand/collapse icon." - raise toe - - def _get_expansion_icon(self, item_label: str) -> CommonIcon: - """Obtain the icon which conveys the expansion status of the item.""" - icon = self._expand_collapse_icons.get(item_label) + raise TimeoutException( + msg=f"Node with path of '{item}' does not contain an expand/collapse icon, so we may not " + f"expand it.") from toe + + def _get_expansion_icon(self, item: Item) -> CommonIcon: + """Obtain the icon which conveys the expansion status of the Item.""" + path = str(item) + icon = self._expand_collapse_icons.get(path) if not icon: icon = CommonIcon( locator=(By.CSS_SELECTOR, "svg.expand-icon"), driver=self.driver, - parent_locator_list=self._get_item_by_label(item_label=item_label).locator_list, + parent_locator_list=self._get_item(item=item).locator_list, poll_freq=self.poll_freq) - self._expand_collapse_icons[item_label] = icon + self._expand_collapse_icons[path] = icon return icon - def _get_item_icon(self, item_label: str) -> CommonIcon: - """Obtain the icon which conveys whether the item is a directory/folder or a terminal node.""" - icon = self._item_icons.get(item_label) + def _get_icon(self, item: Item) -> CommonIcon: + """Obtain the icon which conveys whether the Item is a directory/folder or a terminal node.""" + path = str(item) + icon = self._item_icons.get(path) if not icon: icon = CommonIcon( locator=(By.CSS_SELECTOR, "svg.node-icon"), driver=self.driver, - parent_locator_list=self._get_item_by_label(item_label=item_label).locator_list, + parent_locator_list=self._get_item(item=item).locator_list, poll_freq=self.poll_freq) - self._item_icons[item_label] = icon + self._item_icons[path] = icon return icon - def _get_index_of_item_in_tree(self, item_label: str) -> int: + def _get_index_of_item(self, item: Item) -> int: """Get the zero-indexed position among all items for the label with the supplied text.""" - return [_.text for _ in self._tree_items.find_all()].index(item_label) - - def _get_item_by_label(self, item_label: str, wait_timeout: float = 5) -> ComponentPiece: - """Obtain the first item with the supplied text.""" - item = self._items.get(item_label) - if not item: - item = ComponentPiece( - locator=(By.CSS_SELECTOR, f'{self._TREE_ITEM_LOCATOR[1]}[data-label="{item_label}"]'), + return [_.text for _ in self._tree_items.find_all()].index(item.label) + + def _get_item(self, item: Item, timeout: float = 5) -> ComponentPiece: + """Obtain the ComponentPiece which is described by the provided Item.""" + path = str(item) + item_cp = self._items.get(path) + if not item_cp: + item_cp = ComponentPiece( + locator=item.get_locator(), driver=self.driver, parent_locator_list=self.locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=self.poll_freq) - self._items[item_label] = item - return item + self._items[path] = item_cp + return item_cp @classmethod def _split_item_label_path(cls, slash_delimited_label_path: str) -> List[str]: diff --git a/Components/PerspectiveComponents/Containers/Column.py b/Components/PerspectiveComponents/Containers/Column.py index 48a2c4c..900df9f 100644 --- a/Components/PerspectiveComponents/Containers/Column.py +++ b/Components/PerspectiveComponents/Containers/Column.py @@ -1,4 +1,4 @@ -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Union from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.webdriver.common.by import By @@ -14,29 +14,31 @@ class Column(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 5, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 5, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._rows = ComponentPiece( locator=self._ROW_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=2, + timeout=2, poll_freq=poll_freq) self._indexed_rows_dict = {} def element_with_locator_is_in_row_by_index( - self, element_locator: Tuple[By, str], zero_based_row_index: int) -> bool: + self, element_locator: Tuple[Union[By, str], str], zero_based_row_index: int) -> bool: """ Determine if the supplied locator exists within a specified row. @@ -52,7 +54,8 @@ def element_with_locator_is_in_row_by_index( except TimeoutException: return False - def get_count_of_web_elements_in_row_by_locator(self, zero_based_row_index: int, locator: Tuple[By, str]) -> int: + def get_count_of_web_elements_in_row_by_locator( + self, zero_based_row_index: int, locator: Tuple[Union[By, str], str]) -> int: """ Obtain a count of the number of instances which match the supplied locator within a specified row. @@ -71,7 +74,7 @@ def get_count_of_web_elements_in_row_by_locator(self, zero_based_row_index: int, # return 0 if no element with locator is found. return 0 - def get_row_of_component_by_locator(self, element_locator: Tuple[By, str]) -> Optional[int]: + def get_row_of_component_by_locator(self, element_locator: Tuple[Union[By, str], str]) -> Optional[int]: """ Obtain the row index of the row which contains the supplied locator. @@ -100,7 +103,7 @@ def _get_row_by_index(self, index: int) -> ComponentPiece: locator=(By.CSS_SELECTOR, f'{self._ROW_CONTAINER_LOCATOR[1]} div[data-row-index="{index}"]'), driver=self.driver, parent_locator_list=self.locator_list, - wait_timeout=2, + timeout=2, poll_freq=self.poll_freq) self._indexed_rows_dict[index] = row return row diff --git a/Components/PerspectiveComponents/Containers/Split.py b/Components/PerspectiveComponents/Containers/Split.py index 8613ea9..48b5644 100644 --- a/Components/PerspectiveComponents/Containers/Split.py +++ b/Components/PerspectiveComponents/Containers/Split.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.action_chains import ActionChains @@ -7,7 +7,7 @@ from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import ComponentPiece, BasicPerspectiveComponent -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon from Helpers.CSSEnumerations import CSS @@ -27,19 +27,21 @@ class Split(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._content_wrapper = ComponentPiece( locator=self._CONTENT_WRAPPER_LOCATOR, driver=driver, @@ -97,7 +99,7 @@ def drag_split_handle_to_pixel_value(self, desired_position_in_pixels: float) -> first_pane_width = float(self._first_pane.get_computed_width()) handle_width = float(self._split_handle.get_computed_width()) current_pixel_position = first_pane_width + (handle_width/2) - pixel_count = (float(desired_position_in_pixels) - current_pixel_position) + pixel_count = int(float(desired_position_in_pixels) - current_pixel_position) ActionChains(self.driver)\ .click_and_hold(self._split_handle.find())\ .pause(seconds=5)\ diff --git a/Components/PerspectiveComponents/Containers/Tab.py b/Components/PerspectiveComponents/Containers/Tab.py index aef31cf..98f060f 100644 --- a/Components/PerspectiveComponents/Containers/Tab.py +++ b/Components/PerspectiveComponents/Containers/Tab.py @@ -21,24 +21,26 @@ class Tab(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._tabs = ComponentPiece( locator=self._TAB_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) self._tabs_by_index = {} self._active_tab = ComponentPiece( @@ -55,7 +57,7 @@ def click_tab_by_index(self, index: Union[int, str]) -> None: :raises TimeoutException: If the supplied index is not present. """ - self._get_tab_by_index(index=index).click(binding_wait_time=0.5) + self._get_tab_by_index(index=index).click(wait_after_click=0.5) def _get_tab_by_index(self, index: int) -> ComponentPiece: """ @@ -89,7 +91,7 @@ def get_count_of_tabs(self) -> int: :returns: The count of tabs which belong to this container. """ try: - return len(self._tabs.find_all(wait_timeout=1)) + return len(self._tabs.find_all(timeout=1)) except TimeoutException: return 0 diff --git a/Components/PerspectiveComponents/Displays/AlarmTable.py b/Components/PerspectiveComponents/Displays/AlarmTable.py index 5578549..b34a874 100644 --- a/Components/PerspectiveComponents/Displays/AlarmTable.py +++ b/Components/PerspectiveComponents/Displays/AlarmTable.py @@ -18,6 +18,7 @@ from Components.PerspectiveComponents.Common.TablePieces.Filter import Filter from Components.PerspectiveComponents.Common.TablePieces.HeaderAndFooter import Header from Components.PerspectiveComponents.Common.TablePieces.Pager import Pager +from Helpers import CSSEnumerations from Helpers.IAAssert import IAAssert from Helpers.IAExpectedConditions import IAExpectedConditions as IAec from Helpers.IASelenium import IASelenium @@ -125,18 +126,19 @@ class _AlarmTable(Table, BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: List[Tuple[By, str]], - wait_timeout: float = 10, + parent_locator_list: List[Tuple[Union[By, str], str]], + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): # use in last __init__ Table.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) BasicPerspectiveComponent.__init__( @@ -144,9 +146,9 @@ def __init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, - poll_freq=poll_freq - ) + timeout=timeout, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) # apply here, otherwise False is used) self._alarm_table_toolbar = _AlarmTableToolbar( driver=self.driver, parent_locator_list=self.locator_list, @@ -178,11 +180,11 @@ def apply_filter_string(self, filter_string: str, wait_for_filtered_results: flo :raises TimeoutException: If the text filter is not present. """ - self._alarm_table_filter.set_filter_text(text=filter_string, binding_wait_time=wait_for_filtered_results) + self._alarm_table_filter.set_filter_text(text=filter_string, wait_after=wait_for_filtered_results) def apply_prefilters( self, - prefilter_list: List[Union[AlarmState, AlarmPriority]], + prefilter_list: List[Union[AlarmState, AlarmPriority, str]], close_modal_after: bool = True) -> None: """ Expand the popover/modal which contains the Alarm States and Priorities, and then apply the supplied @@ -201,7 +203,7 @@ def apply_prefilters( prefilter_list=prefilter_list) if close_modal_after: self.close_any_open_popover_or_modal() - self._alarm_table_toolbar.wait_on_binding(time_to_wait=1) + self._alarm_table_toolbar.wait_some_time(time_to_wait=1) def apply_show_all_events_prefilter(self) -> None: """ @@ -295,11 +297,11 @@ def click_gear_icon(self) -> None: """ self.click_column_configuration_icon() - def click_next_page_chevron(self, binding_wait_time: float = 0) -> None: + def click_next_page_chevron(self, wait_after_click: float = 0) -> None: """ Within the Pager, click the '>' option in order to go to the next page. - :param binding_wait_time: How long to wait (in seconds) after clicking the '>' option before allowing code to + :param wait_after_click: How long to wait (in seconds) after clicking the '>' option before allowing code to continue. :raises TimeoutException: If the '>' option is not present. @@ -307,7 +309,7 @@ def click_next_page_chevron(self, binding_wait_time: float = 0) -> None: """ original_page_number = self.get_active_page() self._pager.find().click() - self._pager.click_next_page_chevron(binding_wait_time=binding_wait_time) + self._pager.click_next_page_chevron(wait_after_click=wait_after_click) IAAssert.is_equal_to( actual_value=self.get_active_page(), expected_value=original_page_number + 1, @@ -329,18 +331,18 @@ def click_page_number(self, page_number: int) -> None: failure_msg=f"Failed to set the Alarm Table to display page {page_number} by clicking that page number " f"within the Pager.") - def click_previous_page_chevron(self, binding_wait_time: float = 0) -> None: + def click_previous_page_chevron(self, wait_after_click: float = 0) -> None: """ Within the Pager, click the '<' option in order to go to the previous page. - :param binding_wait_time: How long to wait (in seconds) after clicking the '<' option before allowing code to + :param wait_after_click: How long to wait (in seconds) after clicking the '<' option before allowing code to continue. :raises TimeoutException: If the '<' option is not present. :raises AssertionError: If unsuccessful in advancing the Alarm Table to the previous page. """ original_page_number = self.get_active_page() - self._pager.click_previous_page_chevron(binding_wait_time=binding_wait_time) + self._pager.click_previous_page_chevron(wait_after_click=wait_after_click) IAAssert.is_equal_to( actual_value=self.get_active_page(), expected_value=original_page_number - 1, @@ -454,7 +456,7 @@ def details_modal_is_open(self) -> bool: """ return self._alarm_table_body.details_modal_is_open() - def dismiss_prefilter_pill(self, prefilter: Union[AlarmState, AlarmPriority]) -> None: + def dismiss_prefilter_pill(self, prefilter: Union[AlarmState, AlarmPriority, str]) -> None: """ Remove an applied Prefilter from being used as a condition for an Alarm to be displayed. @@ -756,18 +758,18 @@ def get_text_of_filter_results(self) -> str: """ return self._alarm_table_toolbar.get_text_of_filter_results() - def jump_to_page(self, page_number: int, binding_wait_time: float = 1) -> None: + def jump_to_page(self, page_number: int, wait_after_click: float = 1) -> None: """ Use the page jump input field to go to a specific page in the Table. :param page_number: The page of the Table to go to. - :param binding_wait_time: The amount of time (in seconds) to wait after supplying the page number before + :param wait_after_click: The amount of time (in seconds) to wait after supplying the page number before allowing code to continue. :raises TimeoutException: If the page jump input is not present. :raises AssertionError: If the Table does not end up on the supplied page. """ - self._pager.jump_to_page(page_to_jump_to=page_number, binding_wait_time=binding_wait_time) + self._pager.jump_to_page(page_to_jump_to=page_number, wait_after=wait_after_click) def results_label_is_displayed(self) -> bool: """ @@ -844,7 +846,7 @@ def pager_is_present(self) -> bool: """ return self._pager.is_present() - def prefilter_is_applied(self, prefilter: Union[AlarmState, AlarmPriority]) -> bool: + def prefilter_is_applied(self, prefilter: Union[AlarmState, AlarmPriority, str]) -> bool: """ Determine if a Prefilter is currently applied as a conditional prefilter. @@ -927,17 +929,22 @@ def toolbar_is_displayed(self) -> bool: return self._alarm_table_toolbar.is_displayed() def wait_for_row_count( - self, expected_count: Optional[int] = None): + self, + expected_count: Optional[int] = None, + timeout: float = 0, + **kwargs): """ Obtain a count of rows in the Alarm Table. :param expected_count: If supplied, the function will wait some short period of time until this number of rows appears. + :param timeout: The amount of time you are willing to wait for the Table to have the expected count of rows. A + value of 0 will result in an immediate return. :returns: A count of rows in the Alarm Table. """ return self._alarm_table_body.wait_for_row_count( - include_expanded_subviews_in_count=False, expected_count=expected_count) + include_expanded_subviews_in_count=False, expected_count=expected_count, timeout=timeout) class _AlarmTableBody(Body): @@ -960,14 +967,14 @@ class _AlarmTableBody(Body): def __init__( self, driver: WebDriver, - parent_locator_list: List[Tuple[By, str]], - wait_timeout: float = 10, + parent_locator_list: List[Tuple[Union[By, str], str]], + timeout: float = 10, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) # OVERWRITE inherited piece @@ -1077,7 +1084,7 @@ def details_modal_is_open(self) -> bool: :returns: True, if the Details popover/modal is currently open - False otherwise. """ try: - return self._details_modal.find(wait_timeout=0.5) is not None + return self._details_modal.find(timeout=0.5) is not None except TimeoutException: return False @@ -1187,8 +1194,8 @@ def get_notes_from_details_modal(self) -> str: :raises TimeoutException: If the Details popover/modal is not already open. """ - self._notes_tab_in_details_modal.click(wait_timeout=1) - return self._notes_field.find(wait_timeout=1).text + self._notes_tab_in_details_modal.click(timeout=1) + return self._notes_field.find(timeout=1).text def get_selection_state_for_rows(self, known_value: str, column_for_value: AlarmTableColumn) -> List[bool]: """ @@ -1253,7 +1260,7 @@ def _click_details_for_row_with_text_in_column(self, text_in_column: str, column """ try: self._get_complex_cell(cell_text=text_in_column, column=column).hover() - self._details_hover_piece.click(wait_timeout=5, binding_wait_time=1) + self._details_hover_piece.click(timeout=5, wait_after_click=1) self.wait.until(IAec.function_returns_true( custom_function=self.details_modal_is_open, function_args={})) @@ -1395,7 +1402,7 @@ def _get_row_data_as_dict(self, alarm_event_id: str) -> dict: locator=(By.CSS_SELECTOR, f'>div'), driver=self.driver, parent_locator_list=_row.locator_list, - wait_timeout=0, + timeout=0, poll_freq=self.poll_freq).find_all() for cell in cells: row_data[cell.get_attribute("data-column-id")] = cell.text @@ -1452,14 +1459,14 @@ class _AlarmTableFilter(Filter): def __init__( self, driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 1, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 1, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._filter_icon = ComponentPiece( @@ -1476,7 +1483,7 @@ def __init__( locator=self._COLLAPSE_ICON_LOCATOR, driver=driver, parent_locator_list=self._container.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def collapse_text_filter(self) -> None: @@ -1487,7 +1494,7 @@ def collapse_text_filter(self) -> None: """ if self.filter_is_expanded(): try: - self._collapse_icon.click(binding_wait_time=1) + self._collapse_icon.click(wait_after_click=1) except ElementNotInteractableException: pass except TimeoutException: @@ -1505,7 +1512,7 @@ def expand_filter(self) -> None: """ if not self.filter_is_expanded(): try: - self._filter_icon.click(wait_timeout=1) + self._filter_icon.click(timeout=1) self.filter_is_expanded() # glorified wait for the animation of the filter expansion except ElementNotInteractableException as enie: # This error is sometimes thrown even though the click registers. @@ -1536,7 +1543,7 @@ def filter_is_expanded(self) -> bool: IAec.function_returns_true(custom_function=self._filter_is_expanded, function_args={})) if expanded: # wait for any animation to complete - self.wait_on_binding(time_to_wait=self.wait_timeout) + self.wait_some_time(time_to_wait=self.timeout) return expanded except TimeoutException: return False @@ -1547,21 +1554,21 @@ def _filter_is_expanded(self) -> bool: :returns: True, if the text filter is expanded - False otherwise. """ - return "isExpanded" in self._container.find(wait_timeout=0.5).get_attribute("class") + return "isExpanded" in self._container.find(timeout=0.5).get_attribute("class") - def set_filter_text(self, text: str, binding_wait_time: float = 2) -> None: + def set_filter_text(self, text: str, wait_after: float = 2) -> None: """ Expand the text filter before typing the supplied text into the filter input. :param text: The text to type into the filter. - :param binding_wait_time: How long (in seconds) to wait after typing the supplied text before allowing code to + :param wait_after: How long (in seconds) to wait after typing the supplied text before allowing code to continue. :raises AssertionError: If unsuccessful in applying the supplied text to the filter. :raises TimeoutException: If the icon to expand the filter is not present. """ self.expand_filter() - super().set_filter_text(text=text, binding_wait_time=binding_wait_time) + super().set_filter_text(text=text, wait_after=wait_after) def text_filtering_is_enabled(self) -> bool: """ @@ -1571,7 +1578,7 @@ def text_filtering_is_enabled(self) -> bool: otherwise. """ try: - return self._filter_icon.find(wait_timeout=1) is not None + return self._filter_icon.find(timeout=1) is not None except TimeoutException: return False @@ -1596,7 +1603,7 @@ def __init__( locator=(By.CSS_SELECTOR, f'{self._MENU_ITEM_LOCATOR[1]}[data-label="{text}"]'), driver=driver, parent_locator_list=ComponentModal(driver=driver).locator_list, - wait_timeout=1, + timeout=1, description=description) @@ -1625,15 +1632,15 @@ class _AlarmTableToolbar(ComponentPiece): def __init__( self, driver: WebDriver, - parent_locator_list: List[Tuple[By, str]], - wait_timeout: float = 5, + parent_locator_list: List[Tuple[Union[By, str], str]], + timeout: float = 5, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=(By.CSS_SELECTOR, "div.alarmTableToolbar"), driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) # Non-component locator lists @@ -1676,7 +1683,7 @@ def __init__( locator=self._GEAR_LOCATOR, driver=self.driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._prefilter_pills = ComponentPiece( locator=self._PREFILTER_PILLS_LOCATOR, @@ -1717,7 +1724,7 @@ def __init__( self._already_attempted_to_click_icon = False def apply_prefilters( - self, prefilter_list: List[Union[AlarmState, AlarmPriority, AlarmStatusTableAlarmState]]) -> None: + self, prefilter_list: List[Union[AlarmState, AlarmPriority, AlarmStatusTableAlarmState, str]]) -> None: """ Set the conditional display of an alarm to require it have at least one of the supplied States or Priorities. @@ -1728,7 +1735,7 @@ def apply_prefilters( application of all prefilters in list results in all states or priorities being applied, as the requested state or priority will not be present as a prefilter. """ - prefilter_list_as_strings = [_.value for _ in prefilter_list] + prefilter_list_as_strings = [str(_) for _ in prefilter_list] try: self.wait.until(IAec.function_returns_true( custom_function=self._apply_prefilters, @@ -1762,7 +1769,7 @@ def apply_show_all_events_prefilter(self) -> None: 'prefilter_list_as_strings': ["Show All"] })) all_applied_states_and_priorities = self.get_text_of_applied_prefilters() - for state in [_.value for _ in AlarmState]: + for state in [str(_) for _ in AlarmState]: IAAssert.does_not_contain( iterable=all_applied_states_and_priorities, expected_value=state, @@ -1783,7 +1790,7 @@ def click_remove_all_prefilters_button(self) -> None: :raises TimeoutException: If the 'Remove All' button is not present. """ - self._remove_all_button.click(wait_timeout=2) + self._remove_all_button.click(timeout=2) def column_configuration_icon_is_present(self) -> bool: """ @@ -1804,7 +1811,7 @@ def column_configuration_popover_or_modal_is_open(self) -> bool: :returns: True, if the popover/modal is currently displayed - False otherwise. """ try: - return self._config_popover_or_modal_header.find(wait_timeout=0.5) is not None + return self._config_popover_or_modal_header.find(timeout=0.5) is not None except TimeoutException: return False @@ -1853,7 +1860,7 @@ def click_popover_or_modal_close_icon(self) -> None: popover/modal. """ try: - self._popover_or_modal_close_icon.click(wait_timeout=2) + self._popover_or_modal_close_icon.click(timeout=2) self._already_attempted_to_click_icon = False # on success, make sure we reset our check except TimeoutException: pass # nothing to close @@ -1891,7 +1898,7 @@ def collapse_text_filter_if_expanded(self) -> None: if self.text_filter_is_expanded(): self.wait.until(ec.element_to_be_clickable( self._text_filter_close_icon.get_full_css_locator())).click() - self.wait_on_binding(time_to_wait=1) + self.wait_some_time(time_to_wait=1) def dismiss_prefilter_pill( self, prefilter: Union[AlarmState, AlarmPriority, AlarmStatusTableAlarmState]) -> None: @@ -1905,13 +1912,13 @@ def dismiss_prefilter_pill( """ prefilters = self._prefilter_pills.find_all() for i in range(len(prefilters)): - if prefilter.value.lower() in prefilters[i].text.lower(): + if str(prefilter).lower() in prefilters[i].text.lower(): prefilters[i].find_element(*self._PRE_FILTER_REMOVE_ICON_LOCATOR).click() break IAAssert.does_not_contain( iterable=[prefilter.text.lower() for prefilter in self._prefilter_pills.find_all()], - expected_value=prefilter.value.lower(), - failure_msg=f"Failed to remove the {prefilter.value} State as a conditional filter by clicking the " + expected_value=str(prefilter).lower(), + failure_msg=f"Failed to remove the {prefilter} State as a conditional filter by clicking the " f"associated pill in the Alarm Table.") def expand_text_filter_if_collapsed(self) -> None: @@ -1934,7 +1941,7 @@ def expand_text_filter_if_collapsed(self) -> None: except TimeoutException: # ignore TimeoutException and raise AssertionError instead. pass - self.wait_on_binding(time_to_wait=1) + self.wait_some_time(time_to_wait=1) IAAssert.is_true( value=self.text_filter_is_expanded(), failure_msg="Failed to expand the text filter of the Alarm Table.") @@ -1946,7 +1953,7 @@ def filtered_results_are_displayed(self) -> bool: :returns: True, if any conditional or text filtering is currently applied to the Alarm Table. """ try: - return self._filtered_results_label.find(wait_timeout=1) is not None + return self._filtered_results_label.find(timeout=1) is not None except TimeoutException: return False @@ -2042,7 +2049,7 @@ def is_displayed(self) -> bool: :returns: True, if the Alarm Table is currently displayed - False otherwise. """ try: - return self.find(wait_timeout=0.5).is_displayed() + return self.find(timeout=0.5).is_displayed() except TimeoutException: return False @@ -2053,11 +2060,11 @@ def prefilter_popover_or_modal_is_displayed(self) -> bool: :returns: True, if the State/Priority popover/modal is currently displayed - False otherwise. """ try: - return self._prefilter_popover_or_modal.find(wait_timeout=0.5).is_displayed() + return self._prefilter_popover_or_modal.find(timeout=0.5).is_displayed() except TimeoutException: return False - def prefilter_is_applied(self, prefilter: Union[AlarmState, AlarmPriority]) -> bool: + def prefilter_is_applied(self, prefilter: Union[AlarmState, AlarmPriority, str]) -> bool: """ Determine if a Prefilter is currently applied as a conditional prefilter. @@ -2066,7 +2073,7 @@ def prefilter_is_applied(self, prefilter: Union[AlarmState, AlarmPriority]) -> b :returns: True, if the supplied Prefilter is currently applied as a conditional filter - False otherwise. """ try: - return prefilter.value in [_.text for _ in self._prefilter_pills.find_all()] + return str(prefilter) in [_.text for _ in self._prefilter_pills.find_all()] except StaleElementReferenceException: return self.prefilter_is_applied( prefilter=prefilter) @@ -2170,7 +2177,7 @@ def _set_selection_state_of_columns( "class")) == should_be_displayed: self._get_menu_item( menu_item_label=column_name).click( - binding_wait_time=0.5) + wait_after_click=0.5) did_click = True except TimeoutException: pass # no checkbox with given name @@ -2190,7 +2197,7 @@ def _set_selection_state_of_menu_item(self, menu_item_label: str, should_be_disp menu_item = self._get_menu_item(menu_item_label=menu_item_label) if ("isActive" in menu_item.find().find_element( By.CSS_SELECTOR, "div").get_attribute("class")) != should_be_displayed: - menu_item.click(binding_wait_time=0.5) + menu_item.click(wait_after_click=0.5) def _get_menu_item(self, menu_item_label: str) -> _AlarmTableMenuItem: """ @@ -2219,7 +2226,7 @@ def _get_column_menu_item_checkbox(self, menu_item_label: str) -> ComponentPiece locator=(By.TAG_NAME, 'svg'), driver=self.driver, parent_locator_list=self._get_menu_item(menu_item_label=menu_item_label).locator_list, - wait_timeout=1) + timeout=1) self._menu_item_checkboxes[menu_item_label] = checkbox return checkbox @@ -2260,14 +2267,14 @@ class _AlarmJournalTableFooter(ComponentPiece): def __init__( self, driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, poll_freq: float = 0.5): super().__init__( locator=self._FOOTER_CONTAINER_LOCATOR, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) self._for_alarm_button = ComponentPiece( locator=self._VIEW_INSTANCES_FOR_ALARM_BUTTON_LOCATOR, @@ -2311,15 +2318,17 @@ def __init__( self, locator, driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: int = 5, - poll_freq: float = 0.5): + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: int = 5, + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, - poll_freq=poll_freq) + timeout=timeout, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._date_range_selector = self._AlarmJournalTableDateRangeSelector( driver=driver, parent_locator_list=self.locator_list, @@ -2610,19 +2619,21 @@ class AlarmStatusTable(_AlarmTable): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 5, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 5, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator, driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._alarm_table_toolbar = self._AlarmStatusTableToolbar( driver=self.driver, parent_locator_list=self.locator_list, @@ -2669,7 +2680,7 @@ def __init__( # The Shelve time selector is its own modal too self._shelve_times = ComponentPiece(locator=self._SHELVE_TIMES_LOCATOR, driver=self.driver, poll_freq=poll_freq) - def click(self, wait_timeout=None, binding_wait_time: float = 0) -> None: + def click(self, timeout=None, wait_after_click: float = 0) -> None: """:raises NotImplementedError: Because the Alarm Status Table should not be blindly clicked.""" raise NotImplementedError("Please do not blindly click the Alarm Status Table.") @@ -2714,7 +2725,7 @@ def apply_prefilters( prefilter_list=prefilter_list) if close_modal_after: self.close_any_open_popover_or_modal() - self._alarm_table_toolbar.wait_on_binding(time_to_wait=1) + self._alarm_table_toolbar.wait_some_time(time_to_wait=1) def click_shelve_button(self) -> None: """ @@ -3051,7 +3062,7 @@ def notes_modal_is_displayed(self) -> bool: acknowledgement is displayed -False otherwise. """ try: - return self._notes_modal.find(wait_timeout=3).is_displayed() + return self._notes_modal.find(timeout=3).is_displayed() except TimeoutException: return False @@ -3116,13 +3127,13 @@ class _AlarmStatusTableToolbar(_AlarmTableToolbar): def __init__( self, driver: WebDriver, - parent_locator_list: List[Tuple[By, str]], - wait_timeout: float = 5, + parent_locator_list: List[Tuple[Union[By, str], str]], + timeout: float = 5, poll_freq: float = 0.5): super().__init__( driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) # Non-component locator lists self.active_tab = ComponentPiece( @@ -3151,7 +3162,7 @@ def click_active_alarms_tab(self) -> None: :raises AssertionError: If unsuccessful in getting to the ACTIVE tab of the Alarm Status Table. """ self.tabs.find_all()[0].click() - self.tabs.wait_on_binding(time_to_wait=0.5) + self.tabs.wait_some_time(time_to_wait=0.5) try: self.wait.until(IAec.function_returns_true( custom_function=self._tab_is_active, @@ -3173,7 +3184,7 @@ def click_shelved_alarms_tab(self, transition_wait_time: float = 0.5) -> None: :raises AssertionError: If unsuccessful in getting to the ACTIVE tab of the Alarm Status Table. """ self.tabs.find_all()[1].click() - self.tabs.wait_on_binding(time_to_wait=transition_wait_time) + self.tabs.wait_some_time(time_to_wait=transition_wait_time) try: self.wait.until(IAec.function_returns_true( custom_function=self._tab_is_active, @@ -3270,14 +3281,14 @@ class _AlarmStatusTableFooter(ComponentPiece): def __init__( self, driver: WebDriver, - parent_locator_list: List[Tuple[By, str]], - wait_timeout: float = 5, + parent_locator_list: List[Tuple[Union[By, str], str]], + timeout: float = 5, poll_freq: float = 0.5): super().__init__( locator=(By.CSS_SELECTOR, "div.alarmTableFoot"), driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) # Non-component locator lists self.acknowledge_button = ComponentPiece( @@ -3406,7 +3417,7 @@ def click_unshelve_button(self) -> None: :raises TimeoutException: If the 'Unshelve' button is not present. """ - self.wait_on_binding(time_to_wait=1) # wait on the animation of the button + self.wait_some_time(time_to_wait=1) # wait on the animation of the button self.wait.until(IAec.function_returns_true( custom_function=self._click_unshelve_button_and_report_success_of_click, function_args={} @@ -3448,7 +3459,7 @@ def _alarm_confirmation_message_has_expected_text(self, expected_shelve_message: discrepancy, or if the confirmation message has not yet appeared. """ try: - return self.shelve_confirmation_message.find(wait_timeout=0).text == str(expected_shelve_message) + return self.shelve_confirmation_message.find(timeout=0).text == str(expected_shelve_message) except TimeoutException: return False @@ -3472,7 +3483,7 @@ def _click_shelve_button_and_report_success_of_click(self) -> bool: :returns: True, if the click was successful - False otherwise. """ try: - self.shelve_button.find(wait_timeout=0.5).click() + self.shelve_button.find(timeout=0.5).click() return True except ElementNotInteractableException: return False @@ -3484,7 +3495,7 @@ def _click_unshelve_button_and_report_success_of_click(self) -> bool: :returns: True, if the click was successful - False otherwise. """ try: - self.unshelve_button.find(wait_timeout=0.5).click() + self.unshelve_button.find(timeout=0.5).click() return True except ElementNotInteractableException: return False @@ -3515,13 +3526,13 @@ class _AlarmStatusTableBody(_AlarmTableBody): def __init__( self, driver: WebDriver, - parent_locator_list: List[Tuple[By, str]], - wait_timeout: int = 5, + parent_locator_list: List[Tuple[Union[By, str], str]], + timeout: int = 5, poll_freq: float = 0.5): super().__init__( driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) self._table_cells = ComponentPiece( locator=(By.CSS_SELECTOR, 'div.tc.ia_table__cell'), @@ -3598,7 +3609,7 @@ def _set_selection_state_for_rows(self, indices: List[int], should_be_selected: if (self._CHECKBOX_CHECKED in checkbox_svg_element.get_attribute('class')) \ != should_be_selected: # Click the row instead of the checkbox because of the dock handler - IASelenium(self.driver).scroll_to_element(row, align_to_top=False).click() + IASelenium(self.driver).scroll_to_element(row, block=CSSEnumerations.CSS.Block.END).click() did_click = True return not did_click except StaleElementReferenceException: @@ -3631,7 +3642,7 @@ def get_count_of_selected_rows(self) -> int: :returns: A count which represents the number of currently selected rows in the Alarm Status Table. """ try: - return len(self._selected_rows.find_all(wait_timeout=0.5)) + return len(self._selected_rows.find_all(timeout=0.5)) except TimeoutException: return 0 @@ -3656,11 +3667,11 @@ def get_alarm_column_data_with_known_value( local_desired_cells_object = self._get_cell_object_by_column_object(desired_column) values = [] try: - known_cell_elements = local_known_cells_object.find_all(wait_timeout=3) + known_cell_elements = local_known_cells_object.find_all(timeout=3) except TimeoutException: known_cell_elements = [] try: - desired_cell_elements = local_desired_cells_object.find_all(wait_timeout=3) + desired_cell_elements = local_desired_cells_object.find_all(timeout=3) except TimeoutException: desired_cell_elements = [] for index in range(len(known_cell_elements)): @@ -3683,17 +3694,21 @@ class _AlarmStatusTableHeader(_AlarmTableHeader): """ _SELECT_ALL_CHECKBOX_LOCATOR = (By.CSS_SELECTOR, 'div.thc[data-column-id="select"]') - def __init__(self, driver: WebDriver, parent_locator_list: List[Tuple[By, str]], poll_freq: float = 0.5): + def __init__( + self, + driver: WebDriver, + parent_locator_list: List[Tuple[Union[By, str], str]], + poll_freq: float = 0.5): super().__init__( driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._select_all_checkbox = CommonCheckbox( locator=self._SELECT_ALL_CHECKBOX_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def get_select_all_checkbox_state(self) -> Optional[bool]: @@ -3729,8 +3744,8 @@ def set_select_all_checkbox_state(self, should_be_selected: bool) -> None: :raises TimeoutException: If the checkbox which drives the selection of all rows is not present. :raises AssertionError: If unsuccessful in setting the state of the "select-all" checkbox. """ - self._select_all_checkbox.scroll_to_element(align_to_top=True) - self._select_all_checkbox.set_state(should_be_selected=should_be_selected, binding_wait_time=0.5) + self._select_all_checkbox.scroll_to_element() + self._select_all_checkbox.set_state(should_be_selected=should_be_selected, wait_after_click=0.5) IAAssert.is_equal_to( actual_value=self.get_select_all_checkbox_state(), expected_value=should_be_selected, diff --git a/Components/PerspectiveComponents/Displays/Audio.py b/Components/PerspectiveComponents/Displays/Audio.py index 95f0d8f..91e3688 100644 --- a/Components/PerspectiveComponents/Displays/Audio.py +++ b/Components/PerspectiveComponents/Displays/Audio.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -13,17 +13,19 @@ class Audio(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._audio = ComponentPiece( locator=self._AUDIO_LOCATOR, driver=driver, diff --git a/Components/PerspectiveComponents/Displays/Barcode.py b/Components/PerspectiveComponents/Displays/Barcode.py index a5f1e1e..badc3d9 100644 --- a/Components/PerspectiveComponents/Displays/Barcode.py +++ b/Components/PerspectiveComponents/Displays/Barcode.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -16,19 +16,21 @@ class Barcode(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._barcode_value = ComponentPiece( locator=self._VALUE_LOCATOR, driver=driver, @@ -57,7 +59,7 @@ def barcode_is_displaying_value(self) -> bool: :returns: True, if the Barcode Component is currently displaying any value - False otherwise. """ try: - return self._barcode_value.find(wait_timeout=0) is not None + return self._barcode_value.find(timeout=0) is not None except TimeoutException: return False @@ -81,7 +83,7 @@ def is_in_error_state(self) -> bool: :returns: True, if the Barcode Component is currently in an error state - False otherwise. """ try: - return self._barcode_error_state.find(wait_timeout=0) is not None + return self._barcode_error_state.find(timeout=0) is not None except TimeoutException: return False diff --git a/Components/PerspectiveComponents/Displays/CylindricalTank.py b/Components/PerspectiveComponents/Displays/CylindricalTank.py index 7e05704..d193b83 100644 --- a/Components/PerspectiveComponents/Displays/CylindricalTank.py +++ b/Components/PerspectiveComponents/Displays/CylindricalTank.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -17,29 +17,31 @@ class CylindricalTank(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) - self._tank = ComponentPiece(locator=self._TANK_LOCATOR, driver=driver, wait_timeout=0, poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) + self._tank = ComponentPiece(locator=self._TANK_LOCATOR, driver=driver, timeout=0, poll_freq=poll_freq) self._tank_liquid = ComponentPiece( locator=self._LIQUID_LOCATOR, driver=driver, - wait_timeout=0, + timeout=0, poll_freq=poll_freq) self._tank_value = ComponentPiece( locator=self._VALUE_DISPLAY_LOCATOR, driver=driver, - wait_timeout=0, + timeout=0, poll_freq=poll_freq) def get_liquid_property(self, property_name: CSSPropertyValue) -> str: diff --git a/Components/PerspectiveComponents/Displays/Dashboard.py b/Components/PerspectiveComponents/Displays/Dashboard.py index ea52692..63094aa 100644 --- a/Components/PerspectiveComponents/Displays/Dashboard.py +++ b/Components/PerspectiveComponents/Displays/Dashboard.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException, NoSuchElementException from selenium.webdriver import ActionChains @@ -39,19 +39,21 @@ class Dashboard(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._grid = ComponentPiece( locator=self._GRID_LOCATOR, driver=driver, @@ -67,7 +69,7 @@ def __init__( driver=driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) - self._shared_modal = ComponentModal(driver=driver, wait_timeout=2, poll_freq=poll_freq) + self._shared_modal = ComponentModal(driver=driver, timeout=2, poll_freq=poll_freq) self._add_remove_cancel_button = CommonButton( locator=self._ADD_OR_REMOVE_CANCEL_LOCATOR, driver=driver, @@ -82,19 +84,19 @@ def __init__( locator=self._REMOVE_WIDGET_MODAL_LOCATOR, driver=driver, parent_locator_list=self._shared_modal.locator_list, - wait_timeout=2, + timeout=2, poll_freq=poll_freq) self._add_widget_modal = ComponentPiece( locator=self._ADD_WIDGET_MODAL_LOCATOR, driver=driver, parent_locator_list=self._shared_modal.locator_list, - wait_timeout=2, + timeout=2, poll_freq=poll_freq) self._add_widget_categories = ComponentPiece( locator=self._ADD_WIDGET_MODAL_CATEGORY_LOCATOR, driver=driver, parent_locator_list=self._add_widget_modal.locator_list, - wait_timeout=2, + timeout=2, poll_freq=poll_freq) self._add_widget_modal_title = ComponentPiece( locator=self._ADD_WIDGET_MODAL_TITLE_LOCATOR, @@ -105,7 +107,7 @@ def __init__( locator=self._WIDGET_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=2, + timeout=2, poll_freq=poll_freq) self._delete_widget = ComponentPiece( locator=self._DELETE_LOCATOR, @@ -217,8 +219,8 @@ def click_edit_toggle(self) -> None: raise TimeoutException(msg="Unable to locate a Dashboard with the defined locator.") from toe try: self._edit_toggle.hover() - self._edit_toggle.wait_on_binding(time_to_wait=1) - self._edit_toggle.click(binding_wait_time=1) + self._edit_toggle.wait_some_time(time_to_wait=1) + self._edit_toggle.click(wait_after_click=1) except TimeoutException as toe: raise TimeoutException( msg="The edit toggle of the Dashboard was not present. Make sure the toggle is turned on " @@ -273,16 +275,16 @@ def collapse_category(self, category: str) -> None: value=self.category_is_expanded(category=category), failure_msg=f"Failed to collapse the '{category}' category.") - def confirm_remove_widget(self, binding_wait_time: int = 1) -> None: + def confirm_remove_widget(self, wait_after_click: int = 1) -> None: """ Click the "Confirm" button visible while attempting to remove a widget from the Dashboard. - :param binding_wait_time: How long to wait (in seconds) after clicking before allowing code to continue. + :param wait_after_click: How long to wait (in seconds) after clicking before allowing code to continue. :raises TimeoutException: If the "Confirm" button is not present. """ try: - self._add_remove_confirm_button.click(binding_wait_time=binding_wait_time) + self._add_remove_confirm_button.click(wait_after_click=wait_after_click) except TimeoutException as toe: raise TimeoutException( msg="Unable to locate the 'Remove' button in the confirmation modal while deleting a widget.") from toe @@ -656,7 +658,7 @@ def _delete_active_widget(self) -> None: except TimeoutException as toe: raise TimeoutException( msg="There was no X available for the active widget, or no widget was selected/active.") from toe - self.confirm_remove_widget(binding_wait_time=1) + self.confirm_remove_widget(wait_after_click=1) def _get_category(self, category: str) -> WebElement: """ diff --git a/Components/PerspectiveComponents/Displays/EquipmentSchedule.py b/Components/PerspectiveComponents/Displays/EquipmentSchedule.py index 2557a35..f584b44 100644 --- a/Components/PerspectiveComponents/Displays/EquipmentSchedule.py +++ b/Components/PerspectiveComponents/Displays/EquipmentSchedule.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.action_chains import ActionChains @@ -7,7 +7,7 @@ from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import ComponentPiece, BasicPerspectiveComponent -from Components.PerspectiveComponents.Common.Dropdown import CommonDropdown +from Components.Common.Dropdown import CommonDropdown from Components.PerspectiveComponents.Inputs.Button import Button @@ -33,19 +33,21 @@ class EquipmentSchedule(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._primary_header = ComponentPiece(locator=self._PRIMARY_HEADER_LOCATOR, driver=driver, poll_freq=poll_freq) self._secondary_header = ComponentPiece( locator=self._SECONDARY_HEADER_LOCATOR, @@ -82,7 +84,7 @@ def click_increment_zoom_level(self) -> None: Method scrolls to the zoom level element because increment button is out of frame with FireFox automation. """ self._zoom_level.scroll_to_element() - self._increment_button.click(binding_wait_time=1) + self._increment_button.click(wait_after_click=1) def click_decrement_zoom_level(self) -> None: """ @@ -90,7 +92,7 @@ def click_decrement_zoom_level(self) -> None: Method scrolls to the zoom level element because decrement button is out of frame with FireFox automation. """ self._zoom_level.scroll_to_element() - self._decrement_button.click(binding_wait_time=1) + self._decrement_button.click(wait_after_click=1) def click_delete_event_button(self) -> None: """ @@ -101,7 +103,7 @@ def click_delete_event_button(self) -> None: Method scrolls to zoom level element because delete button is out of frame with Firefox automation. """ self._zoom_level.scroll_to_element() - self._delete_button.click(binding_wait_time=1) + self._delete_button.click(wait_after_click=1) def delete_button_is_displayed(self) -> bool: """ @@ -232,7 +234,7 @@ def add_event(self, row_index: int, column_index: int, count_of_columns_to_span: """ cell_element = self._get_internal_piece( locator=(By.CSS_SELECTOR, f'[data-column="{column_index}"][data-row="{row_index}"]')) - cell_element.scroll_to_element(align_to_top=True) + cell_element.scroll_to_element() offset = int(cell_element.get_computed_width(include_units=False)) ActionChains(driver=self.driver) \ .click_and_hold(on_element=cell_element.find()) \ @@ -240,7 +242,7 @@ def add_event(self, row_index: int, column_index: int, count_of_columns_to_span: .move_by_offset(xoffset=offset * count_of_columns_to_span, yoffset=0) \ .release() \ .perform() - cell_element.wait_on_binding(1) + cell_element.wait_some_time(1) def move_event(self, item_id: str, event_id: str, count_of_columns_to_drag: int) -> None: """ @@ -255,14 +257,14 @@ def move_event(self, item_id: str, event_id: str, count_of_columns_to_drag: int) """ event_element = self._get_internal_piece( locator=(By.CSS_SELECTOR, f'[data-itemid="{item_id}"][data-eventid="{event_id}"]')) - event_element.scroll_to_element(align_to_top=True) + event_element.scroll_to_element() offset = int(float(event_element.get_computed_width(include_units=False))) ActionChains(driver=self.driver) \ .click_and_hold(on_element=event_element.find()) \ .move_by_offset(xoffset=offset * count_of_columns_to_drag, yoffset=0) \ .release() \ .perform() - event_element.wait_on_binding(1) + event_element.wait_some_time(1) def resize_event(self, item_id: str, event_id: str, count_of_columns_to_drag: int, direction: Direction) -> None: """ @@ -284,14 +286,14 @@ def resize_event(self, item_id: str, event_id: str, count_of_columns_to_drag: in locator=( By.CSS_SELECTOR, f'[data-itemid-eventid="{item_id}_{event_id}"][data-direction="{direction.value}"]')) - event_resize_element.scroll_to_element(align_to_top=True) + event_resize_element.scroll_to_element() ActionChains(driver=self.driver) \ .click_and_hold(on_element=event_resize_element.find()) \ .pause(2) \ .move_by_offset(xoffset=offset * count_of_columns_to_drag, yoffset=0) \ .release() \ .perform() - event_resize_element.wait_on_binding(1) + event_resize_element.wait_some_time(1) def event_is_resizable(self, item_id: str, event_id: str, direction: Direction) -> bool: """ @@ -326,12 +328,12 @@ def select_event(self, item_id: str, event_id: str) -> None: self._get_internal_piece( locator=( By.CSS_SELECTOR, - f'[data-itemid="{item_id}"][data-eventid="{event_id}"]')).click(binding_wait_time=1) + f'[data-itemid="{item_id}"][data-eventid="{event_id}"]')).click(wait_after_click=1) def set_zoom_level(self, text: str): - self._zoom_level.select_option_by_text_if_not_selected(option_text=text, binding_wait_time=1) + self._zoom_level.select_option_by_text_if_not_selected(option_text=text, wait_after_click=1) - def _get_internal_piece(self, locator: Tuple[By, str]) -> ComponentPiece: + def _get_internal_piece(self, locator: Tuple[Union[By, str], str]) -> ComponentPiece: """ Obtain some internal piece of the Equipment Schedule (almost always an event) via the locator which uniquely identifies the internal piece. diff --git a/Components/PerspectiveComponents/Displays/GoogleMap.py b/Components/PerspectiveComponents/Displays/GoogleMap.py index 91279fd..8d1fd57 100644 --- a/Components/PerspectiveComponents/Displays/GoogleMap.py +++ b/Components/PerspectiveComponents/Displays/GoogleMap.py @@ -67,48 +67,48 @@ class _MapTypeControls(ComponentPiece): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, parent_locator_list: Optional[list] = None, - wait_timeout: float = 2, + timeout: float = 2, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._dropdown_map_type_controls = ComponentPiece( locator=self._DROPDOWN_MAP_TYPE_CONTROLS_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._map_control = ComponentPiece( locator=self._MAP_CONTROL_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._satellite_control = ComponentPiece( locator=self._SATELLITE_CONTROL_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._labels_list_item = ComponentPiece( locator=self._LABELS_LIST_ITEM_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._terrain_list_item = ComponentPiece( locator=self._TERRAIN_LIST_ITEM_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def set_map_type(self, map_type: Union[MapType, MapSubtype], should_be_selected: bool) -> None: @@ -153,7 +153,7 @@ def labels_checkbox_is_selected(self) -> bool: :raises TimeoutException: If the MapType UI is not found. """ - return self._labels_list_item.find(wait_timeout=0).get_attribute('aria-checked') == 'true' + return self._labels_list_item.find(timeout=0).get_attribute('aria-checked') == 'true' def terrain_checkbox_is_selected(self) -> bool: """ @@ -163,7 +163,7 @@ def terrain_checkbox_is_selected(self) -> bool: :raises TimeoutException: If the MapType UI is not found. """ - return self._terrain_list_item.find(wait_timeout=0).get_attribute('aria-checked') == 'true' + return self._terrain_list_item.find(timeout=0).get_attribute('aria-checked') == 'true' def map_type_dropdown_is_displayed(self): """ @@ -172,7 +172,7 @@ def map_type_dropdown_is_displayed(self): :returns: True if the dropdown menu is displayed, False otherwise. """ try: - return self._dropdown_map_type_controls.find(wait_timeout=0) is not None + return self._dropdown_map_type_controls.find(timeout=0) is not None except TimeoutException: return False @@ -184,7 +184,7 @@ def map_type_dropdown_is_expanded(self) -> bool: :raises TimeoutException: If the MapType UI is not found or dropdown is not displayed. """ - return self._dropdown_map_type_controls.find(wait_timeout=0).get_attribute(name='aria-expanded') == 'true' + return self._dropdown_map_type_controls.find(timeout=0).get_attribute(name='aria-expanded') == 'true' def expand_map_type_dropdown(self): """ @@ -202,7 +202,7 @@ def _expand_map_type_dropdown(self): """ try: if not self.map_type_dropdown_is_expanded(): - self._dropdown_map_type_controls.click(wait_timeout=0, binding_wait_time=0.5) + self._dropdown_map_type_controls.click(timeout=0, wait_after_click=0.5) return self.map_type_dropdown_is_expanded() except TimeoutException: return False @@ -223,7 +223,7 @@ def _collapse_map_type_dropdown(self): """ try: if self.map_type_dropdown_is_expanded(): - self._dropdown_map_type_controls.click(wait_timeout=0, binding_wait_time=0.5) + self._dropdown_map_type_controls.click(timeout=0, wait_after_click=0.5) return not self.map_type_dropdown_is_expanded() except TimeoutException: return False @@ -240,11 +240,11 @@ def _set_map_base_type(self, map_type: MapType) -> None: if map_type.value == MapType.MAP.value: self._map_control.click() if self.terrain_checkbox_is_selected(): - self._terrain_list_item.click(wait_timeout=0) + self._terrain_list_item.click(timeout=0) else: self._satellite_control.click() if self.labels_checkbox_is_selected(): - self._labels_list_item.click(wait_timeout=0) + self._labels_list_item.click(timeout=0) def _set_map_subtype(self, map_subtype: MapSubtype, should_be_selected: bool) -> None: """ @@ -260,12 +260,12 @@ def _set_map_subtype(self, map_subtype: MapSubtype, should_be_selected: bool) -> self._map_control.click() if self.terrain_checkbox_is_selected() != should_be_selected: self.wait.until(ec.presence_of_element_located(self._TERRAIN_LIST_ITEM_LOCATOR)).click() - self.wait_on_binding() + self.wait_some_time() else: self._satellite_control.click() if self.labels_checkbox_is_selected() != should_be_selected: self.wait.until(ec.presence_of_element_located(self._LABELS_LIST_ITEM_LOCATOR)).click() - self.wait_on_binding() + self.wait_some_time() def _check_selected_main_map_type(self) -> MapType: """ @@ -277,85 +277,87 @@ def _check_selected_main_map_type(self) -> MapType: return MapType.MAP if MapType.MAP.value == self._dropdown_map_type_controls.get_text() \ else MapType.SATELLITE else: - if self._map_control.find(wait_timeout=0).get_attribute(name='aria-checked') == 'true': + if self._map_control.find(timeout=0).get_attribute(name='aria-checked') == 'true': return MapType.MAP else: return MapType.SATELLITE def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._dismiss_overlay_button = ComponentPiece( locator=self._DISMISS_OVERLAY_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._zoom_out_button = ComponentPiece( locator=self._ZOOM_OUT_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._zoom_in_button = ComponentPiece( locator=self._ZOOM_IN_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._full_screen_button = ComponentPiece( locator=self._FULL_SCREEN_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._tilt_map_button = ComponentPiece( locator=self._TILT_MAP_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._rotate_tilt_map_clockwise_button = ComponentPiece( locator=self._ROTATE_TILT_MAP_CLOCKWISE_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._rotate_tilt_map_counterclockwise_button = ComponentPiece( locator=self._ROTATE_TILT_MAP_COUNTERCLOCKWISE_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._external_link = ComponentPiece( locator=self._EXTERNAL_LINK_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._map_type_controls = self._MapTypeControls( locator=self._MAP_TYPE_CONTROLS_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=5, + timeout=1, poll_freq=poll_freq) self._general_close_popup_button = ComponentPiece( locator=self._GENERAL_CLOSE_POPUP_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def click_dismiss_overlay_button(self) -> None: @@ -364,14 +366,14 @@ def click_dismiss_overlay_button(self) -> None: """ self._dismiss_overlay_button.click() - def click_full_screen_button(self, binding_wait_time: float = 0.5) -> None: + def click_full_screen_button(self, wait_after_click: float = 0.5) -> None: """ Clicks full screen button. - :param binding_wait_time: The amount of time (in seconds) to wait after the click before allowing code + :param wait_after_click: The amount of time (in seconds) to wait after the click before allowing code to continue. """ - self._full_screen_button.click(binding_wait_time=binding_wait_time) + self._full_screen_button.click(wait_after_click=wait_after_click) def click(self, x_offset: int = 0, y_offset: int = 0) -> None: """ @@ -385,60 +387,60 @@ def click(self, x_offset: int = 0, y_offset: int = 0) -> None: .click()\ .perform() - def click_rotate_map_clockwise_button(self, binding_wait_time: float = 0.5) -> None: + def click_rotate_map_clockwise_button(self, wait_after_click: float = 0.5) -> None: """ Clicks the rotate clockwise button. - :param binding_wait_time: The amount of time (in seconds) to wait after the click before allowing code + :param wait_after_click: The amount of time (in seconds) to wait after the click before allowing code to continue. :raises TimeoutException: If the rotate clockwise button is not found. """ - self._rotate_tilt_map_clockwise_button.click(binding_wait_time=binding_wait_time) + self._rotate_tilt_map_clockwise_button.click(wait_after_click=wait_after_click) - def click_rotate_map_counterclockwise_button(self, binding_wait_time: float = 0.5) -> None: + def click_rotate_map_counterclockwise_button(self, wait_after_click: float = 0.5) -> None: """ Clicks the rotate counterclockwise button. - :param binding_wait_time: The amount of time (in seconds) to wait after the click before allowing code + :param wait_after_click: The amount of time (in seconds) to wait after the click before allowing code to continue. :raises TimeoutException: If the rotate counterclockwise button is not found. """ - self._rotate_tilt_map_counterclockwise_button.click(binding_wait_time=binding_wait_time) + self._rotate_tilt_map_counterclockwise_button.click(wait_after_click=wait_after_click) - def click_tilt_map_button(self, binding_wait_time: float = 0.5) -> None: + def click_tilt_map_button(self, wait_after_click: float = 0.5) -> None: """ Clicks the tilt control button. - :param binding_wait_time: The amount of time (in seconds) to wait after the click before allowing code + :param wait_after_click: The amount of time (in seconds) to wait after the click before allowing code to continue. """ - self._tilt_map_button.click(binding_wait_time=binding_wait_time) + self._tilt_map_button.click(wait_after_click=wait_after_click) - def click_zoom_in_button(self, binding_wait_time: float = 0.5) -> None: + def click_zoom_in_button(self, wait_after_click: float = 0.5) -> None: """ Clicks the zoom in button. - :param binding_wait_time: The amount of time (in seconds) to wait before allowing code to continue. + :param wait_after_click: The amount of time (in seconds) to wait before allowing code to continue. """ - self._zoom_in_button.click(binding_wait_time=binding_wait_time) + self._zoom_in_button.click(wait_after_click=wait_after_click) - def click_zoom_out_button(self, binding_wait_time: float = 0.5) -> None: + def click_zoom_out_button(self, wait_after_click: float = 0.5) -> None: """ Clicks the zoom out button. - :param binding_wait_time: The amount of time (in seconds) to wait after the click before allowing code + :param wait_after_click: The amount of time (in seconds) to wait after the click before allowing code to continue. """ - self._zoom_out_button.click(binding_wait_time=binding_wait_time) + self._zoom_out_button.click(wait_after_click=wait_after_click) def close_all_expanded_popups(self) -> None: """ Closes all general popups such as popups from markers, KML, and clickable icons. The list is reversed before iterating to close popups in the highest z-order first. """ - all_general_popups = self._general_close_popup_button.find_all(wait_timeout=0) + all_general_popups = self._general_close_popup_button.find_all(timeout=0) all_general_popups.reverse() for close_popup_button in all_general_popups: close_popup_button.click() @@ -450,7 +452,7 @@ def dismiss_overlay_button_is_displayed(self) -> bool: :returns: True if the button is found, False otherwise. """ try: - return self._dismiss_overlay_button.find(wait_timeout=0).is_displayed() + return self._dismiss_overlay_button.find(timeout=0).is_displayed() except TimeoutException: return False @@ -462,7 +464,7 @@ def double_click(self, x_offset: int = 0, y_offset: int = 0) -> None: :param y_offset: The y offset in pixels to move the cursor to after locating the component. """ ActionChains(driver=self.driver)\ - .move_to_element_with_offset(to_element=self.find(wait_timeout=0), xoffset=x_offset, yoffset=y_offset)\ + .move_to_element_with_offset(to_element=self.find(timeout=0), xoffset=x_offset, yoffset=y_offset)\ .double_click()\ .perform() @@ -474,7 +476,7 @@ def double_right_click_map_with_offset(self, x_offset: int = 0, y_offset: int = :param y_offset: The y offset in pixels to move the cursor to after locating the component. """ ActionChains(driver=self.driver)\ - .move_to_element_with_offset(to_element=self.find(wait_timeout=0), xoffset=x_offset, yoffset=y_offset)\ + .move_to_element_with_offset(to_element=self.find(timeout=0), xoffset=x_offset, yoffset=y_offset)\ .context_click()\ .context_click()\ .perform() @@ -509,7 +511,7 @@ def get_map_center(self) -> GeographicPoint: from toe center = re.search( '(?<=ll=)(.*)(?=&z)', - self._external_link.find(wait_timeout=0).get_attribute('href')).group().split(',') + self._external_link.find(timeout=0).get_attribute('href')).group().split(',') return GeographicPoint(latitude=float(center[0]), longitude=float(center[1])) def get_map_type(self) -> Union[MapType, MapSubtype]: @@ -551,7 +553,7 @@ def get_popup_count(self) -> int: :returns: The number of all open general popups. """ try: - return len(self._general_close_popup_button.find_all(wait_timeout=0)) + return len(self._general_close_popup_button.find_all(timeout=0)) except TimeoutException: return 0 @@ -647,7 +649,7 @@ def zoom_in_button_is_enabled(self) -> bool: :returns: True if the zoom in button is enabled, False otherwise. """ - return self._zoom_in_button.find(wait_timeout=0).is_enabled() + return self._zoom_in_button.find(timeout=0).is_enabled() def zoom_out_button_is_enabled(self) -> bool: """ @@ -655,4 +657,4 @@ def zoom_out_button_is_enabled(self) -> bool: :returns: True if the zoom out button is enabled, False otherwise. """ - return self._zoom_out_button.find(wait_timeout=0).is_enabled() + return self._zoom_out_button.find(timeout=0).is_enabled() diff --git a/Components/PerspectiveComponents/Displays/Icon.py b/Components/PerspectiveComponents/Displays/Icon.py index 761a38f..d5b6745 100644 --- a/Components/PerspectiveComponents/Displays/Icon.py +++ b/Components/PerspectiveComponents/Displays/Icon.py @@ -1,10 +1,10 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import BasicPerspectiveComponent -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon class Icon(BasicPerspectiveComponent, CommonIcon): @@ -12,18 +12,19 @@ class Icon(BasicPerspectiveComponent, CommonIcon): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): CommonIcon.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) BasicPerspectiveComponent.__init__( @@ -31,6 +32,7 @@ def __init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) diff --git a/Components/PerspectiveComponents/Displays/Image.py b/Components/PerspectiveComponents/Displays/Image.py index b63191d..9dea071 100644 --- a/Components/PerspectiveComponents/Displays/Image.py +++ b/Components/PerspectiveComponents/Displays/Image.py @@ -13,19 +13,21 @@ class Image(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 5, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 5, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._internal_img = ComponentPiece( locator=self._INTERNAL_IMAGE_LOCATOR, parent_locator_list=self.locator_list, diff --git a/Components/PerspectiveComponents/Displays/InlineFrame.py b/Components/PerspectiveComponents/Displays/InlineFrame.py index f67dcf1..2572015 100644 --- a/Components/PerspectiveComponents/Displays/InlineFrame.py +++ b/Components/PerspectiveComponents/Displays/InlineFrame.py @@ -1,4 +1,4 @@ -from typing import Tuple, Optional, List +from typing import Tuple, Optional, List, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -12,19 +12,21 @@ class InlineFrame(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._iframe = ComponentPiece(locator=self._IFRAME_LOCATOR, driver=driver, poll_freq=poll_freq) def get_source(self) -> str: diff --git a/Components/PerspectiveComponents/Displays/LEDDisplay.py b/Components/PerspectiveComponents/Displays/LEDDisplay.py index e6e8e06..4d91b76 100644 --- a/Components/PerspectiveComponents/Displays/LEDDisplay.py +++ b/Components/PerspectiveComponents/Displays/LEDDisplay.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -21,30 +21,32 @@ class Segments(Enum): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._digit = ComponentPiece( locator=self._DIGIT_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._background_rect = ComponentPiece( locator=self._BACKGROUND_RECT_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._digit_coll = {} @@ -62,7 +64,7 @@ def get_active_diode_color(self) -> str: locator=(By.TAG_NAME, "use.ia_ledComponent__diode--on"), driver=self.driver, parent_locator_list=self._get_digit_by_index(zero_based_index_from_right=0).locator_list, - wait_timeout=1, + timeout=1, poll_freq=self.poll_freq).get_css_property(property_name=CSS.FILL) def get_background_color(self) -> str: @@ -102,7 +104,7 @@ def get_inactive_diode_color(self) -> str: locator=(By.TAG_NAME, "use.ia_ledComponent__diode--off"), driver=self.driver, parent_locator_list=self._get_digit_by_index(zero_based_index_from_right=0).locator_list, - wait_timeout=1, + timeout=1, poll_freq=self.poll_freq).get_css_property(property_name=CSS.FILL) def get_text(self) -> str: @@ -124,7 +126,7 @@ def is_fourteen_segment_display(self) -> bool: locator=(By.TAG_NAME, "use"), driver=self.driver, parent_locator_list=self._get_digit_by_index(zero_based_index_from_right=0).locator_list, - wait_timeout=1, + timeout=1, poll_freq=self.poll_freq).find_all()) > 7 except TimeoutException: # If we fail to find the expected fourteen-segment svgs, we should at least verify the LED exists. @@ -160,7 +162,7 @@ def _get_display_value_as_text(self, trim: bool = False) -> str: displayed_text.insert( 0, self._get_digit_by_index(zero_based_index_from_right=i) - .find(wait_timeout=0) + .find(timeout=0) .get_attribute(name="data-char")) except TimeoutException: pass @@ -181,7 +183,7 @@ def _get_digit_by_index(self, zero_based_index_from_right: int) -> ComponentPiec locator=(By.CSS_SELECTOR, f'svg[data-index="{zero_based_index_from_right}"]'), driver=self.driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=self.poll_freq) self._digit_coll[zero_based_index_from_right] = digit return digit diff --git a/Components/PerspectiveComponents/Displays/Label.py b/Components/PerspectiveComponents/Displays/Label.py index dcdc8b1..faf529b 100644 --- a/Components/PerspectiveComponents/Displays/Label.py +++ b/Components/PerspectiveComponents/Displays/Label.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -11,16 +11,18 @@ class Label(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) diff --git a/Components/PerspectiveComponents/Displays/LinearScale.py b/Components/PerspectiveComponents/Displays/LinearScale.py index bd5c352..21a566f 100644 --- a/Components/PerspectiveComponents/Displays/LinearScale.py +++ b/Components/PerspectiveComponents/Displays/LinearScale.py @@ -1,4 +1,4 @@ -from typing import Tuple, Optional, List +from typing import Tuple, Optional, List, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -12,25 +12,26 @@ class LinearScale(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 5, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 5, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq - ) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._transform_element = ComponentPiece( locator=self._TRANSFORM_ELEMENT_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def is_horizontal(self) -> bool: diff --git a/Components/PerspectiveComponents/Displays/Map.py b/Components/PerspectiveComponents/Displays/Map.py index fb717e9..97192bc 100644 --- a/Components/PerspectiveComponents/Displays/Map.py +++ b/Components/PerspectiveComponents/Displays/Map.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By @@ -20,19 +20,21 @@ class Map(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._map_marker = ComponentPiece( locator=(By.CSS_SELECTOR, self._MARKER_CSS), driver=driver, @@ -103,7 +105,7 @@ def click_zoom_in_button(self) -> None: :raises TimeoutException: If the built-in zoom-in button is not present. """ - self._map_zoom_in.click(binding_wait_time=1) + self._map_zoom_in.click(wait_after_click=1) def click_zoom_out_button(self) -> None: """ @@ -111,7 +113,7 @@ def click_zoom_out_button(self) -> None: :raises TimeoutException: If the built-in zoom-out button is not present. """ - self._map_zoom_out.click(binding_wait_time=1) + self._map_zoom_out.click(wait_after_click=1) def get_height_of_popup_wrapper(self, include_units: bool = False) -> str: """ diff --git a/Components/PerspectiveComponents/Displays/Markdown.py b/Components/PerspectiveComponents/Displays/Markdown.py index 9816224..ef29960 100644 --- a/Components/PerspectiveComponents/Displays/Markdown.py +++ b/Components/PerspectiveComponents/Displays/Markdown.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -11,19 +11,21 @@ class Markdown(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._text_container = ComponentPiece( locator=self._TEXT_CONTAINER_LOCATOR, driver=driver, diff --git a/Components/PerspectiveComponents/Displays/Progress.py b/Components/PerspectiveComponents/Displays/Progress.py index dab0f92..9ebae1f 100644 --- a/Components/PerspectiveComponents/Displays/Progress.py +++ b/Components/PerspectiveComponents/Displays/Progress.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -25,19 +25,21 @@ class Progress(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._track = ComponentPiece( locator=(By.CSS_SELECTOR, "div"), driver=driver, parent_locator_list=self.locator_list) self._bar = ComponentPiece( diff --git a/Components/PerspectiveComponents/Displays/Table.py b/Components/PerspectiveComponents/Displays/Table.py index 7cd72a3..5d5ff28 100644 --- a/Components/PerspectiveComponents/Displays/Table.py +++ b/Components/PerspectiveComponents/Displays/Table.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import StrEnum from typing import List, Optional, Tuple, Union from selenium.common.exceptions import StaleElementReferenceException, NoSuchElementException, TimeoutException, \ @@ -6,17 +6,19 @@ from selenium.webdriver import Keys from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement from Components.BasicComponent import ComponentPiece, BasicPerspectiveComponent +from Components.Common.Icon import CommonIcon from Components.Common.TextInput import CommonTextInput from Components.PerspectiveComponents.Common.DateRangeSelector import HistoricalRange from Components.PerspectiveComponents.Common.DateTimePicker import PerspectiveDate -from Components.PerspectiveComponents.Common.Icon import CommonIcon from Components.PerspectiveComponents.Common.Table import Table as CommonTable from Components.PerspectiveComponents.Common.TablePieces.Body import Body from Components.PerspectiveComponents.Common.TablePieces.Filter import Filter from Components.PerspectiveComponents.Common.TablePieces.HeaderAndFooter import Header, FilterModal, Footer from Components.PerspectiveComponents.Common.TablePieces.Pager import Pager +from Helpers import CSSEnumerations from Helpers.CSSEnumerations import CSS from Helpers.IAAssert import IAAssert from Helpers.IASelenium import IASelenium @@ -25,7 +27,7 @@ class ColumnConfigurations: """Enums relating to ways a column can be configured/rendered, and also runtime settings like column filtering.""" - class Render(Enum): + class Render(StrEnum): """Available settings for how a column may be rendered.""" AUTO = "auto" NUMBER = "number" @@ -36,7 +38,7 @@ class Render(Enum): class Filter: """Column filtering settings, including conditions and filter icon visibility""" - class Condition(Enum): + class Condition(StrEnum): """ This is a complete collection of conditions, but not all conditions are applicable based on the column type. """ @@ -60,7 +62,7 @@ class Condition(Enum): STARTS_WITH = "starts with" # string TRUE = "true" - class Visible(Enum): + class Visible(StrEnum): """Settings for specifying the appearance of the column filter icon in the header.""" ALWAYS = "always" ON_HOVER = "on-hover" @@ -84,20 +86,45 @@ def __init__(self, driver: WebDriver, parent_locator_list: Optional[List] = None locator=self._COPY_OPTION_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._empty_message = ComponentPiece( locator=self._EMPTY_MESSAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=0.5, + timeout=0.5, poll_freq=poll_freq) self._empty_icon = CommonIcon( locator=self._EMPTY_MESSAGE_ICON_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=0.5, + timeout=0.5, poll_freq=poll_freq) + self._active_editing_cell = ComponentPiece( + driver=driver, + locator=(By.CSS_SELECTOR, "div>textarea"), + parent_locator_list=self.locator_list + ) + + def cell_is_open_for_editing(self, zero_based_row_index: int, column_id: str) -> bool: + """ + Determine if the cell of a Table is open for editing. + + :param zero_based_row_index: The zero-based index of the row to check. + :param column_id: The field in use by the column to be checked. + + :returns: True, if the cell is open for editing; False otherwise. + """ + _locator = (By.CSS_SELECTOR, f'{self.__get_row_css_by_row_index(row_index=zero_based_row_index)} ' + f'{self.__get_column_css_by_column_id(column_id=column_id)} textarea.ia_textArea') + try: + return ComponentPiece( + locator=_locator, + driver=self.driver, + parent_locator_list=self.locator_list, + poll_freq=self.poll_freq).find(timeout=0) is not None + except TimeoutException: + return False def cell_is_root_selection(self, row_index: int, column_index: int) -> bool: """ @@ -123,6 +150,26 @@ def cell_is_root_selection(self, row_index: int, column_index: int) -> bool: except NoSuchElementException: return False + def cell_is_selected_by_row_id_column_id(self, row_id: int, column_id: str) -> bool: + """ + Determine if the cell of a Table is selected based on row id and column id. + + :param row_id: The unique identifier of the row to check. + :param column_id: The field in use by the column to be checked. + + :returns: True, if the cell is selected; False otherwise. + """ + _locator = (By.CSS_SELECTOR, f'{self.__get_row_css_by_row_id(row_id=row_id)} ' + f'{self.__get_column_css_by_column_id(column_id=column_id)} div.t-selected') + try: + return ComponentPiece( + locator=_locator, + driver=self.driver, + parent_locator_list=self.locator_list, + poll_freq=self.poll_freq).find(timeout=0) is not None + except TimeoutException: + return False + def cell_is_selected(self, row_index: int, column_index: int) -> bool: """ Determine if a specified cell is currently part of the active selection of the Table. @@ -154,7 +201,7 @@ def click_copy_option(self) -> None: :raises TimeoutException: If the context menu which contains the copy option is not present. """ - self._copy_option.click(wait_timeout=1) + self._copy_option.click(timeout=1) def collapse_subview_by_row_index(self, row_index: int) -> None: """ @@ -167,7 +214,7 @@ def collapse_subview_by_row_index(self, row_index: int) -> None: if self.subview_is_expanded(row_index=row_index): locator = (By.CSS_SELECTOR, self.__get_subview_css_by_row_index(row_index=row_index)) subview_arrow = ComponentPiece(locator=locator, driver=self.driver, parent_locator_list=self.locator_list) - subview_arrow.click(binding_wait_time=0.5) + subview_arrow.click(wait_after_click=0.5) def contains_subviews(self) -> bool: """ @@ -207,23 +254,24 @@ def copy_option_is_enabled(self) -> bool: """ return 'disabled' not in self._copy_option.find().get_attribute("class") - def double_click_cell(self, row_index: int, column_index: int, binding_wait_time: float = 0) -> None: + def double_click_cell(self, row_index: int, column_index: int, wait_after_click: float = 0) -> None: """ Double-click a cell of the Table. :param row_index: The zero-based index of the row containing the target cell. :param column_index: The zero-based index of the column containing the target cell. - :param binding_wait_time: The amount of time (in seconds) to wait before allowing code to continue. + :param wait_after_click: The amount of time (in seconds) to wait before allowing code to continue. :raises TimeoutException: If the supplied indices do not define a cell of the Table. """ - _locator = (By.CSS_SELECTOR, f'{self.__get_row_css_by_row_index(row_index=row_index)} ' + # We will obtain the row group by row index, and the column will always be in the 0th row within that group. + _locator = (By.CSS_SELECTOR, f'{self.__get_row_group_css_by_row_index(row_index=row_index)} ' f'{self.__get_column_css_by_index(column_index=column_index)}') ComponentPiece( locator=_locator, driver=self.driver, parent_locator_list=self.locator_list, - poll_freq=self.poll_freq).double_click(binding_wait_time=binding_wait_time) + poll_freq=self.poll_freq).double_click(wait_after_click=wait_after_click) def expand_subview_by_row_index(self, row_index: int) -> None: """ @@ -240,11 +288,47 @@ def expand_subview_by_row_index(self, row_index: int) -> None: driver=self.driver, parent_locator_list=self.locator_list, poll_freq=self.poll_freq)\ - .click(binding_wait_time=0.5) + .click(wait_after_click=0.5) IAAssert.is_true( value=self.subview_is_expanded(row_index=row_index), failure_msg=f"Failed to expand a subview for row {row_index}.") + def get_row_index_of_active_editing_cell(self) -> int | None: + """ + Retrieve the row index of the active editing cell. + + :returns: The row index of the active editing cell. + If no active editing cell is found, returns None. + """ + try: + return self._get_row_index(web_element=self._active_editing_cell.find(timeout=0)) + except TimeoutException: + return None + + def get_column_id_of_active_editing_cell(self) -> str | None: + """ + Retrieve the column id of the active editing cell. + + :returns: The column id of the active editing cell. + If no active editing cell is found, returns None. + """ + try: + cell = self._active_editing_cell.find(timeout=0) + return cell.get_attribute(name="data-column-id") + except TimeoutException: + return None + + def get_text_of_active_editing_cell(self) -> str | None: + """ + Get the text content of the active editing cell. + + :returns: The text content of the active editing cell if it exists, otherwise None. + """ + try: + return self._active_editing_cell.find(timeout=0).text + except TimeoutException: + return None + def get_count_of_selected_rows(self) -> int: """ Obtain a count of the rows which are at least partially selected. @@ -379,7 +463,7 @@ def rows_are_completely_selected(self, list_of_row_indices: List[Union[int, str] return False return True - def set_cell_data_by_row_index_column_id( + def set_cell_content( self, zero_based_row_index: int, column_id: str, @@ -395,12 +479,9 @@ def set_cell_data_by_row_index_column_id( :param commit_value: A boolean flag indicating whether to commit changes immediately or not. If True, changes will be committed by appending Keys.ENTER to it after setting the cell data. If False, changes will not be committed, allowing for further modifications before committing. - - :raises: TimeoutException: If the cell is not already in an editable state; this function does not prepare the - cell for editing. """ - _locator = (By.CSS_SELECTOR, f'{self.__get_row_css_by_row_index(row_index=zero_based_row_index)} ' + _locator = (By.CSS_SELECTOR, f'{self.__get_row_group_css_by_row_index(row_index=zero_based_row_index)} ' f'{self.__get_column_css_by_column_id(column_id=column_id)} textarea.ia_textArea') if commit_value: text += Keys.ENTER @@ -423,13 +504,19 @@ def set_cell_data_by_row_index_column_id( except TimeoutException: pass - def scroll_to_row(self, row_index: int, align_to_top: bool = False) -> None: + def scroll_to_row( + self, + row_index: int, + behavior: CSSEnumerations.CSS.Behavior = CSSEnumerations.CSS.Behavior.AUTO, + block: CSSEnumerations.CSS.Block = CSSEnumerations.CSS.Block.START, + inline: CSSEnumerations.CSS.Inline = CSSEnumerations.CSS.Inline.NEAREST) -> None: """ Scroll the Table so that the specified row is visible. + :param inline: The CSS Inline value to use when scrolling the row into view. + :param block: The CSS Block value to use when scrolling the row into view. + :param behavior: The CSS Behavior value to use when scrolling the row into view. :param row_index: The zero-based index of the row we will scroll to. - :param align_to_top: If True, we will try to align the row to the Top of the viewport. If False, we will attempt - to align the row to the bottom of the viewport. :raises TimeoutException: If no row exists with the supplied index. """ @@ -439,16 +526,17 @@ def scroll_to_row(self, row_index: int, align_to_top: bool = False) -> None: driver=self.driver, parent_locator_list=self.locator_list, poll_freq=self.poll_freq) - min_index = min([int(row.get_attribute('data-row-index')) for row in rows.find_all()]) - max_index = max([int(row.get_attribute('data-row-index')) for row in rows.find_all()]) + row_indices = [self._get_row_index(web_element=row) for row in rows.find_all()] + min_index = min(row_indices) + max_index = max(row_indices) if min_index > row_index: - self._scroll_to_row(row_index=min_index, align_to_top=False) - self.scroll_to_row(row_index=row_index, align_to_top=align_to_top) + self._scroll_to_row(row_index=min_index, block=CSSEnumerations.CSS.Block.END) + self.scroll_to_row(row_index=row_index, behavior=behavior, block=block, inline=inline) elif max_index < row_index: - self._scroll_to_row(row_index=max_index, align_to_top=True) - self.scroll_to_row(row_index=row_index, align_to_top=align_to_top) + self._scroll_to_row(row_index=max_index, ) + self.scroll_to_row(row_index=row_index, behavior=behavior, block=block, inline=inline) else: - self._scroll_to_row(row_index=row_index, align_to_top=align_to_top) + self._scroll_to_row(row_index=row_index, behavior=behavior, block=block, inline=inline) def subview_is_expanded(self, row_index: int) -> bool: """ @@ -462,7 +550,7 @@ def subview_is_expanded(self, row_index: int) -> bool: locator=(By.CSS_SELECTOR, self.__get_subview_css_by_row_index(row_index=row_index)), driver=self.driver, parent_locator_list=self.locator_list, - poll_freq=self.poll_freq).find(wait_timeout=1).get_attribute("class") + poll_freq=self.poll_freq).find(timeout=1).get_attribute("class") except TimeoutException: return False @@ -482,13 +570,16 @@ def _get_count_of_selected_rows(self) -> int: except StaleElementReferenceException: return self.get_count_of_selected_rows() - def _scroll_to_row(self, row_index: int, align_to_top: bool = False) -> None: + def _scroll_to_row( + self, + row_index: int, + behavior: CSSEnumerations.CSS.Behavior = CSSEnumerations.CSS.Behavior.AUTO, + block: CSSEnumerations.CSS.Block = CSSEnumerations.CSS.Block.START, + inline: CSSEnumerations.CSS.Inline = CSSEnumerations.CSS.Inline.NEAREST) -> None: """ Scroll the Table so that the specified row is visible. :param row_index: The zero-based index of the row we will scroll to. - :param align_to_top: If True, we will try to align the row to the Top of the viewport. If False, we will attempt - to align the row to the bottom of the viewport. :raises TimeoutException: If no row exists with the supplied index. """ @@ -497,20 +588,31 @@ def _scroll_to_row(self, row_index: int, align_to_top: bool = False) -> None: driver=self.driver, parent_locator_list=self.locator_list, poll_freq=self.poll_freq) - row.scroll_to_element(align_to_top=align_to_top) - row.wait_on_binding() + row.scroll_to_element(behavior=behavior, block=block, inline=inline) + row.wait_some_time() - def __get_column_css_by_index(self, column_index: Union[int, str]) -> str: - """Obtain the CSS which defines a column based on its index.""" - return f'{self._CELL_LOCATOR[1]}[data-column-index="{column_index}"]' + @classmethod + def _get_row_index(cls, web_element: WebElement) -> int: + return int(web_element.get_attribute("data-row-id")) def __get_column_css_by_column_id(self, column_id: Union[int, str]) -> str: """Obtain the CSS which defines a column based on its id.""" return f'{self._CELL_LOCATOR[1]}[data-column-id="{column_id}"]' + def __get_column_css_by_index(self, column_index: Union[int, str]) -> str: + """Obtain the CSS which defines a column based on its index.""" + return f'{self._CELL_LOCATOR[1]}[data-column-index="{column_index}"]' + + def __get_row_css_by_row_id(self, row_id: Union[int, str]) -> str: + """Obtain the CSS which defines a row based on its unique id.""" + return f'{self._ROW_GROUP_LOCATOR[1]}[data-row-id="{row_id}"] {self._ROW_LOCATOR[1]}' + def __get_row_css_by_row_index(self, row_index: Union[int, str]) -> str: """Obtain the CSS which defines a row based on its index.""" - return f'{self._ROW_GROUP_LOCATOR[1]}[data-row-index="{row_index}"] {self._ROW_LOCATOR[1]}' + return f'{self.__get_row_group_css_by_row_index(row_index=row_index)} {self._ROW_LOCATOR[1]}' + + def __get_row_group_css_by_row_index(self, row_index: Union[int, str]) -> str: + return f'{self._ROW_GROUP_LOCATOR[1]}[data-row-index="{row_index}"]' def __get_subview_css_by_row_index(self, row_index: Union[int, str]) -> str: """Obtain the CSS which defines a subview based on the index of the row which contains it.""" @@ -524,13 +626,13 @@ class _TableHeader(Header): def __init__( self, driver: WebDriver, - parent_locator_list: List[Tuple[By, str]], - wait_timeout: float = 1, + parent_locator_list: List[Tuple[Union[By, str], str]], + timeout: float = 1, poll_freq: float = 0.5): super().__init__( driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) self._filter_modal = FilterModal(driver=driver) self._header_row_groups = {} @@ -632,7 +734,7 @@ def get_termination_of_header_row_group_cell( return self._get_cell_from_header_row_group( zero_based_header_row_group_index=zero_based_header_row_group_index, label=label_text).get_termination() - def set_column_filter_condition(self, condition: ColumnConfigurations.Filter.Condition) -> None: + def set_column_filter_condition(self, condition: Union[ColumnConfigurations.Filter.Condition, str]) -> None: """ Set the current filtering condition of a column within the open filtering modal. @@ -641,7 +743,7 @@ def set_column_filter_condition(self, condition: ColumnConfigurations.Filter.Con :raises TimeoutException: If the column filtering modal is not present. :raises AssertionError: If the interaction does not result in the condition being selected. """ - self._filter_modal.set_condition(condition=condition.value) + self._filter_modal.set_condition(condition=condition) def _get_filter_by_column_index(self, column_index: int) -> ComponentPiece: """ @@ -653,7 +755,7 @@ def _get_filter_by_column_index(self, column_index: int) -> ComponentPiece: locator=(By.CSS_SELECTOR, "div.filter-button svg"), driver=self.driver, parent_locator_list=self._get_header_cell_by_index(column_index=column_index).locator_list, - wait_timeout=1) + timeout=1) self._filter_cells[column_index] = filter_comp return filter_comp @@ -718,18 +820,19 @@ class Table(CommonTable, BasicPerspectiveComponent): """ def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): CommonTable.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) BasicPerspectiveComponent.__init__( @@ -737,8 +840,9 @@ def __init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, - poll_freq=poll_freq) + timeout=timeout, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._body = _TableBody(driver=driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) self._filter = Filter(driver=driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) self._filter_modal = FilterModal(driver=driver) @@ -767,6 +871,33 @@ def cell_is_root_selection(self, row_index: int, column_index: int) -> bool: """ return self._body.cell_is_root_selection(row_index=row_index, column_index=column_index) + def cell_is_open_for_editing(self, zero_based_row_index: int, column_id: str, ) -> bool: + """ + Determine if the cell of a Table is open for editing. + + :param zero_based_row_index: The zero-based index of the row to check. + :param column_id: The field in use by the column to be checked. + + :returns: True, if the cell is open for editing; False otherwise. + + :raises TimeoutException: If the supplied row index or column ID is invalid. + """ + return self._body.cell_is_open_for_editing(zero_based_row_index=zero_based_row_index, column_id=column_id) + + def cell_is_selected_by_row_id_column_id(self, row_id: int, column_id: str, ) -> bool: + """ + Determine if the cell of a Table is selected based on the provided row ID and column ID. + + :param row_id: The unique identifier of the row to check. + :param column_id: The field in use by the column to be checked. + + :returns: True, if the cell is selected; False otherwise. + + :raises TimeoutException: If the supplied row ID or column ID is invalid. + """ + return self._body.cell_is_selected_by_row_id_column_id( + row_id=row_id, column_id=column_id) + def cell_is_selected(self, row_index: int, column_index: int) -> bool: """ Determine if a specified cell is currently part of the active selection of the Table. @@ -780,7 +911,7 @@ def cell_is_selected(self, row_index: int, column_index: int) -> bool: """ return self._body.cell_is_selected(row_index=row_index, column_index=column_index) - def click(self, wait_timeout=None, binding_wait_time: float = 0) -> None: + def click(self, timeout=None, wait_after_click: float = 0) -> None: """ This method is only here to override the inherited method and prevent any generic clicking of the Table. @@ -918,16 +1049,17 @@ def copy_option_is_enabled(self) -> bool: """ return self._body.copy_option_is_enabled() - def double_click_cell_in_body(self, row_index: int, column_index: int) -> None: + def double_click_cell_in_body(self, row_index: int, column_index: int, wait_after_click: float = 0) -> None: """ Double-click a cell in the body of the Table. :param row_index: The zero-based index of the row containing the target cell. :param column_index: The zero-based index of the column containing the target cell. + :param wait_after_click: The amount of time to wait after the double-click before allowing code to continue. :raises TimeoutException: If the supplied indices do not define a cell of the Table. """ - self._body.double_click_cell(row_index=row_index, column_index=column_index) + self._body.double_click_cell(row_index=row_index, column_index=column_index, wait_after_click=wait_after_click) def end_time_hours_input_is_enabled_in_filtering_modal(self) -> bool: """ @@ -1025,6 +1157,38 @@ def footer_is_present(self) -> bool: """ return self._footer.footer_is_present() + def get_row_index_of_active_editing_cell(self) -> int: + """ + Retrieve the row index of the active editing cell. + + :returns: The row index of the active editing cell. + If no active editing cell is found, returns None. + + :raises TimeoutException: If the active editing cell is not found within the specified timeout. + """ + return self._body.get_row_index_of_active_editing_cell() + + def get_column_id_of_active_editing_cell(self) -> str: + """ + Retrieve the column id of the active editing cell. + + :returns: The column id of the active editing cell. + If no active editing cell is found, returns None. + + :raises TimeoutException: If the active editing cell is not found within the specified timeout. + """ + return self._body.get_column_id_of_active_editing_cell() + + def get_text_of_active_editing_cell(self) -> str: + """ + Get the text content of the active editing cell. + + :returns: The text content of the active editing cell if it exists, otherwise None. + + :raises TimeoutException: If the active editing cell is not found within the specified timeout. + """ + return self._body.get_text_of_active_editing_cell() + def get_active_page(self) -> int: """ Obtain the number of the currently displayed page of the Table. @@ -1531,17 +1695,23 @@ def row_select_dropdown_is_displayed(self) -> bool: """ return self._pager.row_select_dropdown_is_displayed() - def scroll_to_row(self, row_index: int, align_to_top: bool = False) -> None: + def scroll_to_row( + self, + row_index: int, + behavior: CSSEnumerations.CSS.Behavior = CSSEnumerations.CSS.Behavior.AUTO, + block: CSSEnumerations.CSS.Block = CSSEnumerations.CSS.Block.START, + inline: CSSEnumerations.CSS.Inline = CSSEnumerations.CSS.Inline.NEAREST) -> None: """ Scroll the Table so that the specified row is visible. + :param inline: The inline alignment of the row in the viewport. Default is NEAREST. + :param block: The block alignment of the row in the viewport. Default is START. + :param behavior: The scrolling behavior to use. Default is AUTO. :param row_index: The zero-based index of the row we will scroll to. - :param align_to_top: If True, we will try to align the row to the Top of the viewport. If False, we will attempt - to align the row to the bottom of the viewport. :raises TimeoutException: If no row exists with the supplied index. """ - return self._body.scroll_to_row(row_index=row_index, align_to_top=align_to_top) + return self._body.scroll_to_row(row_index=row_index, behavior=behavior, block=block, inline=inline) def seconds_input_is_enabled_in_modal(self) -> bool: """ @@ -1577,7 +1747,7 @@ def select_cells_by_indices(self, list_of_row_column_index_tuples: List[Tuple[in :raises TimeoutException: If any of the tuples fail to define a cell of the Table. """ self._body.inclusive_select_cells_by_indices(list_of_row_column_index_tuples=list_of_row_column_index_tuples) - self.wait_on_binding(time_to_wait=0.5) + self.wait_some_time(time_to_wait=0.5) def select_rows_to_display(self, count_of_rows: int) -> None: """ @@ -1588,7 +1758,7 @@ def select_rows_to_display(self, count_of_rows: int) -> None: """ self._pager.set_displayed_row_count(count_of_rows=count_of_rows) - def set_column_filter_condition(self, condition: ColumnConfigurations.Filter.Condition) -> None: + def set_column_filter_condition(self, condition: Union[ColumnConfigurations.Filter.Condition, str]) -> None: """ Select a condition from the dropdown. @@ -1597,7 +1767,7 @@ def set_column_filter_condition(self, condition: ColumnConfigurations.Filter.Con :raises TimeoutException: If the condition dropdown is not present. :raises AssertionError: If we fail to select the supplied condition. """ - self._filter_modal.set_condition(condition=condition.value) + self._filter_modal.set_condition(condition=condition) def set_column_filter_date(self, date: PerspectiveDate, apply: bool = True) -> None: """ @@ -1677,7 +1847,7 @@ def set_filter_text(self, text: str) -> None: """ self._filter.set_filter_text(text=text) - def set_cell_data_by_row_index_column_id( + def set_cell_content( self, row_index: int, column_id: str, text: str, commit_value: bool) -> None: """ Set the data of a cell in the table specified by the given parameters. @@ -1690,8 +1860,11 @@ def set_cell_data_by_row_index_column_id( :param commit_value: A boolean flag indicating whether to commit changes immediately or not. If True, changes will be committed by pressing Enter after setting the cell data. If False, changes will not be committed, allowing for further modifications before committing. + + :raises: TimeoutException: If the cell is not already in an editable state; this function does not prepare the + cell for editing. """ - self._body.set_cell_data_by_row_index_column_id( + self._body.set_cell_content( zero_based_row_index=row_index, column_id=column_id, text=text, commit_value=commit_value) def start_time_hours_input_is_enabled(self) -> bool: @@ -1735,9 +1908,9 @@ def subview_is_expanded(self, row_index: int) -> bool: """ return self._body.subview_is_expanded(row_index=row_index) - def wait_for_cell_to_have_text_by_row_index_column_id( + def wait_for_cell_to_have_text( self, zero_based_row_index: Union[int, str], column_id: str, text: str, timeout: float = 0): - return self._body.wait_for_cell_to_have_text_by_row_index_column_id( + return self._body.wait_for_cell_to_have_text( zero_based_row_index=zero_based_row_index, column_id=column_id, text=text, diff --git a/Components/PerspectiveComponents/Displays/TagBrowseTree.py b/Components/PerspectiveComponents/Displays/TagBrowseTree.py index fc59ca4..96c6c98 100644 --- a/Components/PerspectiveComponents/Displays/TagBrowseTree.py +++ b/Components/PerspectiveComponents/Displays/TagBrowseTree.py @@ -1,10 +1,12 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import BasicPerspectiveComponent from Components.PerspectiveComponents.Common.TagBrowseTree import CommonTagBrowseTree +from Components.PerspectiveComponents.Common.Tree import Item +from Helpers.Ignition.Tag import Tag, Folder class TagBrowseTree(CommonTagBrowseTree, BasicPerspectiveComponent): @@ -12,12 +14,13 @@ class TagBrowseTree(CommonTagBrowseTree, BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): CommonTagBrowseTree.__init__( self, locator=locator, @@ -30,33 +33,61 @@ def __init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, - poll_freq=poll_freq) + timeout=timeout, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) + + def multi_select( + self, items: List[Item], inclusive_multi_selection: bool = True, timeout: float = 5) -> None: + """ + Unused. Tag Browse Trees should enforce that Tags are being selected. This is especially important due to the + special handling within their extended use in Power Charts. + """ + raise NotImplementedError(f"Use {self.multi_select_tags.__name__} instead.") - def multi_select_tags(self, tag_name_list: [str], inclusive_multi_selection: bool, wait_timeout: int = 5) -> None: + def multi_select_tags( + self, tags: List[Union[Tag, Folder]], inclusive_multi_selection: bool, timeout: int = 5) -> None: """ - Select multiple tags within the Tag Browse Tree. + Select multiple Tags (or Folders) within the Tag Browse Tree. - :param tag_name_list: A list of tag names to be selected. If multiple tags with the same name are visible, only - the first of each such tag will be selected. - :param inclusive_multi_selection: A True value specifies that all tags between the supplied tags should also be - selected, and so SHIFT will be held. A False value specifies that ONLY the supplied tags should be selected, + :param tags: A list of Tags or Folders to be selected. + :param inclusive_multi_selection: A True value specifies that all Tags between the supplied Tags should also be + selected, and so SHIFT will be held. A False value specifies that ONLY the supplied Tags should be selected, and so CONTROL will be held. - :param wait_timeout: The amount of time to wait (in seconds) for each tag to become visible. + :param timeout: The amount of time to potentially wait (in seconds) for each Tag to become visible. - :raises TimeoutException: If any of the supplied tags did not exist. + :raises TimeoutException: If any of the supplied Tags did not exist. """ - self.multi_select_items( - item_labels=tag_name_list, inclusive_multi_selection=inclusive_multi_selection, wait_timeout=wait_timeout) + super().multi_select( + items=[self._item_from_tag(tag=tag) for tag in tags], + inclusive_multi_selection=inclusive_multi_selection, + timeout=timeout) - def select_tag_from_tree(self, tag_name: str, wait_timeout: int = 5) -> None: + def select_item(self, item: Item, timeout: int = 5, wait_after_click: int = 1) -> None: """ - Select a tag from the Tag Browse Tree. + Unused. Tag Browse Trees should enforce that Tags are being selected. This is especially important due to the + special handling within their extended use in Power Charts. + """ + raise NotImplementedError(f"Use {self.select_tag.__name__} instead") + + def select_tag(self, tag: Union[Tag, Folder], timeout: int = 5) -> None: + """ + Select a Tag or Folder from the Tag Browse Tree. - :param tag_name: The name of the tag to select. If multiple tags with the same name are displayed, the first - tag with this name will be selected. - :param wait_timeout: The amount of time (in seconds) to wait for the supplied tag to appear. + :param tag: The Tag or Folder to select. + :param timeout: The amount of time (in seconds) to potentially wait for the supplied Tag to appear. - :raises TimeoutException: If no such tag exists. + :raises TimeoutException: If no such Tag exists. """ - self.select_item_in_tree(item_label=tag_name, wait_timeout=wait_timeout) + super().select_item(item=self._item_from_tag(tag=tag), timeout=timeout) + + @staticmethod + def _item_from_tag(tag: Union[Tag, Folder]) -> Item: + # we don't want the provider. + pieces = tag.get_full_path().split(tag.get_provider())[1].split("/") + item = Item(label_text=pieces.pop(0)) + while len(pieces) > 0: + piece = pieces.pop() + if len(piece) > 0: + item = Item(label_text=piece, parent=item) + return item diff --git a/Components/PerspectiveComponents/Displays/Thermometer.py b/Components/PerspectiveComponents/Displays/Thermometer.py index d39f4a6..206813c 100644 --- a/Components/PerspectiveComponents/Displays/Thermometer.py +++ b/Components/PerspectiveComponents/Displays/Thermometer.py @@ -24,19 +24,21 @@ class Thermometer(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._glass = ComponentPiece( locator=self._GLASS_LOCATOR, driver=driver, @@ -83,11 +85,11 @@ def __init__( parent_locator_list=self.locator_list, poll_freq=poll_freq) - def get_all_interval_colors(self, wait_timeout: float = 0) -> List[str]: + def get_all_interval_colors(self, timeout: float = 0) -> List[str]: """ Obtain a list of all interval colors as strings. - :param wait_timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. + :param timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. :returns: A list containing the color of all intervals of the thermometer as strings. Note that different browsers may return these values in different formats (RGB vs hex). @@ -96,14 +98,14 @@ def get_all_interval_colors(self, wait_timeout: float = 0) -> List[str]: """ return [ Color.from_string(interval.value_of_css_property(CSS.STROKE.value)).hex - for interval in self._interval.find_all(wait_timeout=wait_timeout) + for interval in self._interval.find_all(timeout=timeout) ] - def get_all_tick_colors(self, wait_timeout: float = 0) -> List[str]: + def get_all_tick_colors(self, timeout: float = 0) -> List[str]: """ Obtain a list of all tick colors as strings. - :param wait_timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. + :param timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. :returns: A list containing the color of all ticks of the thermometer as strings. Note that different browsers may return these values in different formats (RGB vs hex). @@ -112,14 +114,14 @@ def get_all_tick_colors(self, wait_timeout: float = 0) -> List[str]: """ return [ Color.from_string(tick.value_of_css_property(CSS.STROKE.value)).hex - for tick in self._tick.find_all(wait_timeout=wait_timeout) + for tick in self._tick.find_all(timeout=timeout) ] - def get_all_tick_label_colors(self, wait_timeout: float = 0) -> List[str]: + def get_all_tick_label_colors(self, timeout: float = 0) -> List[str]: """ Obtain a list of all tick label colors as strings. - :param wait_timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. + :param timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. :returns: A list containing the color of all tick labels of the thermometer as strings. Note that different browsers may return these values in different formats (RGB vs hex). @@ -128,7 +130,7 @@ def get_all_tick_label_colors(self, wait_timeout: float = 0) -> List[str]: """ return [ Color.from_string(tick_label.value_of_css_property(CSS.FILL.value)).hex - for tick_label in self._tick_label.find_all(wait_timeout=wait_timeout) + for tick_label in self._tick_label.find_all(timeout=timeout) ] def get_color_of_labels(self, label_index=0) -> str: @@ -162,11 +164,11 @@ def get_displayed_value_font_size(self) -> str: """ return self._value_label.find().value_of_css_property(CSS.FONT_SIZE.value) - def get_glass_color(self, wait_timeout: float = 0) -> str: + def get_glass_color(self, timeout: float = 0) -> str: """ Obtain the color of the glass of the Thermometer as a string. - :param wait_timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. + :param timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. :returns: The color of the Thermometer's glass as a string. Note that different browsers may return these values in different formats (RGB vs hex). @@ -174,7 +176,7 @@ def get_glass_color(self, wait_timeout: float = 0) -> str: :raises TimeoutException: In the event the glass of the Thermometer could not be located. """ return Color.from_string( - self._glass.find(wait_timeout=wait_timeout).value_of_css_property(CSS.STROKE.value) + self._glass.find(timeout=timeout).value_of_css_property(CSS.STROKE.value) ).hex def get_list_of_levels(self) -> List[str]: @@ -190,11 +192,11 @@ def get_list_of_levels(self) -> List[str]: custom_function=self._get_list_of_levels, function_args={})) - def get_mercury_color(self, wait_timeout: float = 0) -> str: + def get_mercury_color(self, timeout: float = 0) -> str: """ Obtain the color (fill) of the mercury within the Thermometer. - :param wait_timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. + :param timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. :returns: The mercury color (fill) for the Thermometer as a string. Note that different browsers may return these values in different formats (RGB vs hex). @@ -202,19 +204,19 @@ def get_mercury_color(self, wait_timeout: float = 0) -> str: :raises TimeoutException: In the event no mercury could be located. """ return Color.from_string( - self._liquid.find(wait_timeout=wait_timeout).value_of_css_property(CSS.FILL.value) + self._liquid.find(timeout=timeout).value_of_css_property(CSS.FILL.value) ).hex - def get_count_of_intervals(self, wait_timeout: float = 0) -> int: + def get_count_of_intervals(self, timeout: float = 0) -> int: """ Obtain a count of currently displayed tick labels. - :param wait_timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. + :param timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. :returns: A count of interval displayed for the Thermometer. """ try: - return len(self._interval.find_all(wait_timeout=wait_timeout)) + return len(self._interval.find_all(timeout=timeout)) except TimeoutException: return 0 @@ -238,23 +240,23 @@ def get_temperature_font_color(self) -> str: """ return Color.from_string(self._value_label.find().value_of_css_property(CSS.COLOR.value)).hex - def get_unit_type(self, wait_timeout: float = 0) -> str: + def get_unit_type(self, timeout: float = 0) -> str: """ Obtain the units in use for the Thermometer. - :param wait_timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. + :param timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. :returns: The units displayed in the unit label. :raises TimeoutException: If the units label is not currently displayed. """ - return self._unit_label.find(wait_timeout=wait_timeout).text[1:] + return self._unit_label.find(timeout=timeout).text[1:] - def get_unit_color(self, wait_timeout: float = 0) -> str: + def get_unit_color(self, timeout: float = 0) -> str: """ Obtain the font color in use for the units label. - :param wait_timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. + :param timeout: The amount of time (in seconds) to wait for the Thermometer to appear before returning. :returns: The color in use for the units label as a string. Note that different browsers may return these values in different formats (RGB vs hex). @@ -262,7 +264,7 @@ def get_unit_color(self, wait_timeout: float = 0) -> str: :raises TimeoutException: If the units label is not currently displayed. """ return Color.from_string( - self._unit_label.find(wait_timeout=wait_timeout).value_of_css_property(CSS.FILL.value) + self._unit_label.find(timeout=timeout).value_of_css_property(CSS.FILL.value) ).hex def _get_list_of_levels(self) -> Union[List[str], bool]: diff --git a/Components/PerspectiveComponents/Displays/Tree.py b/Components/PerspectiveComponents/Displays/Tree.py index d031a85..139f90b 100644 --- a/Components/PerspectiveComponents/Displays/Tree.py +++ b/Components/PerspectiveComponents/Displays/Tree.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -6,7 +6,6 @@ from Components.BasicComponent import BasicPerspectiveComponent, ComponentPiece from Components.PerspectiveComponents.Common.Tree import CommonTree -from Helpers.IASelenium import IASelenium class Tree(CommonTree, BasicPerspectiveComponent): @@ -14,18 +13,19 @@ class Tree(CommonTree, BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): CommonTree.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) BasicPerspectiveComponent.__init__( @@ -33,48 +33,23 @@ def __init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._items_by_path = {} - def item_exists_in_tree(self, item_path: str, item_label: str, wait_timeout: int = 2) -> bool: - """ - Determine if an item exists in the Tree. - - :param item_path: The zero-indexed path of the item. A value of 0/0/1 translates to 'The 1th grandchild of the - 0th child of the 0th top-level node.' - :param item_label: The label text of the item to verify the presence of. - :param wait_timeout: The amount of time (in seconds) to wait for the item with the supplied path to appear. - - :returns: True, if an item with the supplied text exists at the specified location - False otherwise. - """ - try: - return self._get_item_by_path( - path=item_path, wait_timeout=wait_timeout).get_text() == item_label - except TimeoutException: - return False - - def right_click(self, **kwargs) -> None: + def right_click(self, wait_after_click: float = 0) -> None: """ Right-click the Tree. As individual items do not allow for their own events, this click will always target the 0th top-level item (if one exists). """ try: - IASelenium(driver=self.driver).right_click( - web_element=self._get_item_by_path(path="0", wait_timeout=1).find()) - except TimeoutException: - super().right_click() - - def _get_item_by_path(self, path: str, wait_timeout: int = 2) -> ComponentPiece: - """Obtain a specific item based on its zero-indexed path.""" - item_component = self._items_by_path.get(path) - if not item_component: - item_component = ComponentPiece( - locator=(By.CSS_SELECTOR, f'div[data-item-path="{path}"]'), + ComponentPiece( + locator=(By.CSS_SELECTOR, f'div[data-item-path="0"]'), driver=self.driver, parent_locator_list=self.locator_list, - wait_timeout=wait_timeout, - poll_freq=self.poll_freq) - self._items_by_path[path] = item_component - return item_component + timeout=1, + poll_freq=self.poll_freq).right_click(wait_after_click=wait_after_click) + except TimeoutException: + super().right_click() diff --git a/Components/PerspectiveComponents/Displays/VideoPlayer.py b/Components/PerspectiveComponents/Displays/VideoPlayer.py index 4bd45ca..9f3099b 100644 --- a/Components/PerspectiveComponents/Displays/VideoPlayer.py +++ b/Components/PerspectiveComponents/Displays/VideoPlayer.py @@ -57,19 +57,21 @@ class PlayRate(Enum): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._error_message = ComponentPiece( locator=self._ERROR_MESSAGE_LOCATOR, driver=driver, @@ -271,7 +273,7 @@ def click_volume_icon(self) -> None: :raises ElementClickInterceptedException: If the base controls are configured to hide during times of inactivity. """ - self._volume_icon.click(binding_wait_time=0.25) + self._volume_icon.click(wait_after_click=0.25) def controls_are_displayed(self) -> bool: """ @@ -341,7 +343,7 @@ def get_current_volume(self) -> int: if not modal_displayed_originally: self._volume_icon.click() try: - return int(self._volume_percent.find(wait_timeout=1).get_attribute("style").split(' ')[-1].split('%')[0]) + return int(self._volume_percent.find(timeout=1).get_attribute("style").split(' ')[-1].split('%')[0]) finally: if not modal_displayed_originally: self.click_volume_icon() @@ -439,7 +441,7 @@ def set_volume(self, desired_volume: Union[int, str]) -> None: def _play_rate_options_displayed(self) -> bool: """Determine if the play-rate options are displayed.""" - return len(self._play_rate_options.find_all(wait_timeout=0.5)) > 0 + return len(self._play_rate_options.find_all(timeout=0.5)) > 0 def _select_play_rate(self, desired_rate: PlayRate) -> None: """Set the play rate of the Video Player.""" @@ -461,7 +463,7 @@ def _set_volume(self, desired_volume: int) -> None: while current_volume != desired_volume: ActionChains(driver=self.driver).move_by_offset(xoffset=0, yoffset=directional_modifier).perform() current_volume = int(self._volume_percent.find( - wait_timeout=1).get_attribute("style").split(' ')[-1].split('%')[0]) + timeout=1).get_attribute("style").split(' ')[-1].split('%')[0]) if value_needs_adjustment: ActionChains(driver=self.driver).release().perform() if self._volume_modal_is_displayed(): @@ -470,7 +472,7 @@ def _set_volume(self, desired_volume: int) -> None: def _volume_modal_is_displayed(self) -> bool: """Determine if the volume modal is displayed.""" try: - return self._volume_slider_handle.find(wait_timeout=0.5) is not None + return self._volume_slider_handle.find(timeout=0.5) is not None except TimeoutException: return False diff --git a/Components/PerspectiveComponents/Embedding/Accordion.py b/Components/PerspectiveComponents/Embedding/Accordion.py index 44b1ee2..814bf44 100644 --- a/Components/PerspectiveComponents/Embedding/Accordion.py +++ b/Components/PerspectiveComponents/Embedding/Accordion.py @@ -1,11 +1,11 @@ -from typing import Tuple, Optional, List +from typing import Tuple, Optional, List, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import BasicPerspectiveComponent, ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon class Accordion(BasicPerspectiveComponent): @@ -19,19 +19,21 @@ class Accordion(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._headers = ComponentPiece( locator=self._GENERIC_ITEM_HEADER_LOCATOR, driver=self.driver, @@ -333,6 +335,6 @@ def _get_header_text_component_by_index(self, index: int) -> ComponentPiece: locator=self._ITEM_HEADER_TEXT_LOCATOR, driver=self.driver, parent_locator_list=self._get_header_by_index(index=index).locator_list, - wait_timeout=2) + timeout=2) self._header_text_collection[index] = header_text return header_text diff --git a/Components/PerspectiveComponents/Embedding/Carousel.py b/Components/PerspectiveComponents/Embedding/Carousel.py index e42c0c0..4fbe94d 100644 --- a/Components/PerspectiveComponents/Embedding/Carousel.py +++ b/Components/PerspectiveComponents/Embedding/Carousel.py @@ -1,13 +1,14 @@ from typing import Optional, List, Tuple, Union -from Helpers.IAExpectedConditions import IAExpectedConditions as IAec + from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.support import expected_conditions as ec from Components.BasicComponent import BasicPerspectiveComponent, ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon from Helpers.CSSEnumerations import CSS, CSSPropertyValue +from Helpers.IAExpectedConditions import IAExpectedConditions as IAec class Carousel(BasicPerspectiveComponent): @@ -24,24 +25,26 @@ class Carousel(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._arrows = ComponentPiece( locator=self._ARROW_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._dot_coll = {} self._dot_icon_coll = {} @@ -49,19 +52,19 @@ def __init__( locator=self._DOT_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._active_dot = ComponentPiece( locator=self._ACTIVE_DOT_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._view_containers = ComponentPiece( locator=self._VIEW_CONTAINER_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def arrows_are_displayed(self) -> bool: @@ -104,8 +107,7 @@ def click_next_arrow(self, transition_speed: int = 1) -> None: """ Click the 'next' arrow in order to transition to the next slide. - :param transition_speed: The amount of time to wait while the Carousel goes through its transition. Synonymous - with binding_wait_time. + :param transition_speed: The amount of time to wait while the Carousel goes through its transition. :raises TimeoutException: If no arrows are found. :raises IndexError: If only 1 arrow is found? Should never happen unless a user hides the next arrow through @@ -118,16 +120,15 @@ def click_next_arrow(self, transition_speed: int = 1) -> None: except TimeoutException: pass # We have to perform a wait due to a FireFox issue not clicking the next arrow button. - self.wait_on_binding(time_to_wait=0.25) + self.wait_some_time(time_to_wait=0.25) self._arrows.find_all()[1].click() - self._arrows.wait_on_binding(time_to_wait=transition_speed) + self._arrows.wait_some_time(time_to_wait=transition_speed) def click_previous_arrow(self, transition_speed: int = 1) -> None: """ Click the 'previous' arrow in order to transition to the previous slide. - :param transition_speed: The amount of time to wait while the Carousel goes through its transition. Synonymous - with binding_wait_time. + :param transition_speed: The amount of time to wait while the Carousel goes through its transition. :raises TimeoutException: If no arrows are found. :raises IndexError: If only 1 arrow is found? Should never happen unless a user hides the previous arrow through @@ -140,9 +141,9 @@ def click_previous_arrow(self, transition_speed: int = 1) -> None: except TimeoutException: pass # We have to perform a wait due to a FireFox issue not clicking the previous arrow button. - self.wait_on_binding(time_to_wait=0.25) + self.wait_some_time(time_to_wait=0.25) self._arrows.find_all()[0].click() - self._arrows.wait_on_binding(time_to_wait=transition_speed) + self._arrows.wait_some_time(time_to_wait=transition_speed) def get_index_of_next_slide(self, current_index: Optional[int] = None) -> int: """ @@ -233,7 +234,7 @@ def get_count_of_dots(self) -> int: Obtain the count of rendered dots of the Carousel. """ try: - return len(self._dots.find_all(wait_timeout=0)) + return len(self._dots.find_all(timeout=0)) except TimeoutException: return 0 @@ -283,27 +284,27 @@ def next_arrow_is_enabled(self) -> bool: """ return "disabled" not in self._arrows.find_all()[1].get_attribute("class") - def pause_by_hovering(self, binding_wait_time: float = 1) -> None: + def pause_by_hovering(self, wait_after_hover: float = 1) -> None: """ Attempt to pause the Carousel by hovering the mouse over the Component. Note that this function only has any effect if the Carousel is configured to pause while a user hovers over it. - :param binding_wait_time: How long (in seconds) to wait after hovering over the component before allowing code + :param wait_after_hover: How long (in seconds) to wait after hovering over the component before allowing code to continue. """ self.hover() - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=wait_after_hover) - def pause_by_hovering_on_dots(self, binding_wait_time: float = 1) -> None: + def pause_by_hovering_on_dots(self, wait_after_hover: float = 1) -> None: """ Attempt to pause the Carousel by hovering the mouse over the dots of the Carousel. Note that this function only has any effect if the Carousel is configured to pause while a user hovers over the dots of the Carousel. - :param binding_wait_time: How long (in seconds) to wait after hovering over the component before allowing code + :param wait_after_hover: How long (in seconds) to wait after hovering over the component before allowing code to continue. """ self._dots.hover() - self._dots.wait_on_binding(time_to_wait=binding_wait_time) + self._dots.wait_some_time(time_to_wait=wait_after_hover) def previous_arrow_is_enabled(self) -> bool: """ @@ -333,7 +334,7 @@ def wait_for_next_slide(self, original_index: int, time_to_wait: float = 10) -> try: self._get_dot( zero_based_index=self.get_index_of_next_slide( - current_index=original_index), must_be_active=True).find(wait_timeout=time_to_wait) + current_index=original_index), must_be_active=True).find(timeout=time_to_wait) except TimeoutException: pass return self.get_current_index_from_dots() @@ -354,7 +355,7 @@ def wait_for_previous_slide(self, original_index: int, time_to_wait: float = 10) try: self._get_dot( zero_based_index=self.determine_index_of_previous_slide( - current_index=original_index), must_be_active=True).find(wait_timeout=time_to_wait) + current_index=original_index), must_be_active=True).find(timeout=time_to_wait) except TimeoutException: pass return self.get_current_index_from_dots() @@ -373,7 +374,7 @@ def _get_dot(self, zero_based_index: int, must_be_active: bool = False) -> Compo locator=(By.CSS_SELECTOR, f'{locator}[data-index="{zero_based_index}"]'), driver=self.driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=self.poll_freq) self._dot_coll[(zero_based_index, must_be_active)] = dot return dot @@ -391,7 +392,7 @@ def _get_dot_icon(self, zero_based_index: int) -> CommonIcon: driver=self.driver, parent_locator_list=self._get_dot( zero_based_index=zero_based_index).locator_list, - wait_timeout=1, + timeout=1, poll_freq=self.poll_freq) self._dot_icon_coll[zero_based_index] = icon return icon diff --git a/Components/PerspectiveComponents/Embedding/EmbeddedView.py b/Components/PerspectiveComponents/Embedding/EmbeddedView.py index 030af03..ae78124 100644 --- a/Components/PerspectiveComponents/Embedding/EmbeddedView.py +++ b/Components/PerspectiveComponents/Embedding/EmbeddedView.py @@ -1,4 +1,4 @@ -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -16,36 +16,38 @@ class EmbeddedView(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._primary_message = ComponentPiece( locator=self._PRIMARY_MESSAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._secondary_message = ComponentPiece( locator=self._SECONDARY_MESSAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._state_display = ComponentPiece( locator=self._STATE_DISPLAY_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def get_primary_message(self) -> str: diff --git a/Components/PerspectiveComponents/Embedding/FlexRepeater.py b/Components/PerspectiveComponents/Embedding/FlexRepeater.py index 66aec1c..a7a9185 100644 --- a/Components/PerspectiveComponents/Embedding/FlexRepeater.py +++ b/Components/PerspectiveComponents/Embedding/FlexRepeater.py @@ -1,4 +1,4 @@ -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -15,17 +15,19 @@ class FlexRepeater(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._children = ComponentPiece( locator=self._DIRECT_CHILD_LOCATOR, driver=driver, parent_locator_list=self.locator_list) diff --git a/Components/PerspectiveComponents/Embedding/ViewCanvas.py b/Components/PerspectiveComponents/Embedding/ViewCanvas.py index a386aa6..19e4606 100644 --- a/Components/PerspectiveComponents/Embedding/ViewCanvas.py +++ b/Components/PerspectiveComponents/Embedding/ViewCanvas.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -18,24 +18,26 @@ class ViewCanvas(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 5, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 5, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._children = ComponentPiece( locator=self._CHILD_VIEW_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def get_count_of_instanced_views(self) -> int: diff --git a/Components/PerspectiveComponents/Inputs/BarcodeScanner.py b/Components/PerspectiveComponents/Inputs/BarcodeScanner.py index 9371c3f..5daa464 100644 --- a/Components/PerspectiveComponents/Inputs/BarcodeScanner.py +++ b/Components/PerspectiveComponents/Inputs/BarcodeScanner.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -13,20 +13,22 @@ class BarcodeScanner(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) - self._label = ComponentPiece(locator=self._LABEL_LOCATOR, driver=driver, wait_timeout=1, poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) + self._label = ComponentPiece(locator=self._LABEL_LOCATOR, driver=driver, timeout=1, poll_freq=poll_freq) def is_displaying_value(self) -> bool: """ diff --git a/Components/PerspectiveComponents/Inputs/Button.py b/Components/PerspectiveComponents/Inputs/Button.py index aacf6fe..d356a4d 100644 --- a/Components/PerspectiveComponents/Inputs/Button.py +++ b/Components/PerspectiveComponents/Inputs/Button.py @@ -1,4 +1,4 @@ -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -12,18 +12,19 @@ class Button(CommonButton, BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): CommonButton.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) BasicPerspectiveComponent.__init__( @@ -31,6 +32,7 @@ def __init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) diff --git a/Components/PerspectiveComponents/Inputs/Checkbox.py b/Components/PerspectiveComponents/Inputs/Checkbox.py index c9dff9d..8c2b2a7 100644 --- a/Components/PerspectiveComponents/Inputs/Checkbox.py +++ b/Components/PerspectiveComponents/Inputs/Checkbox.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -12,23 +12,25 @@ class Checkbox(CommonCheckbox, BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): CommonCheckbox.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) BasicPerspectiveComponent.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) diff --git a/Components/PerspectiveComponents/Inputs/DateTime.py b/Components/PerspectiveComponents/Inputs/DateTime.py index 5ac6f04..8071a84 100644 --- a/Components/PerspectiveComponents/Inputs/DateTime.py +++ b/Components/PerspectiveComponents/Inputs/DateTime.py @@ -15,18 +15,19 @@ class DateTimePicker(CommonDateTimePicker, BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): CommonDateTimePicker.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) BasicPerspectiveComponent.__init__( @@ -34,9 +35,10 @@ def __init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) def get_selected_day(self) -> Optional[int]: """ @@ -58,8 +60,8 @@ def is_in_date_time_mode(self) -> bool: otherwise. """ try: - return self._hours_input.find(wait_timeout=0) is not None and \ - self._minutes_input.find(wait_timeout=0) is not None + return self._hours_input.find(timeout=0) is not None and \ + self._minutes_input.find(timeout=0) is not None except TimeoutException: return False @@ -86,16 +88,16 @@ class DateTimeInput(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, poll_freq: float = 0.5): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) # picker is in a modal self._picker = DateTimePicker( @@ -103,7 +105,7 @@ def __init__( driver=driver, parent_locator_list=ComponentModal( driver=driver).locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) self._input = ComponentPiece( locator=self._INPUT_LOCATOR, @@ -145,7 +147,7 @@ def expand(self) -> None: Expands the DateTime Input so that the DateTime Picker is displayed. """ if not self.is_expanded() and not self.is_in_time_mode(): - self._input.click(binding_wait_time=0.5) + self._input.click(wait_after_click=0.5) def get_available_months_from_dropdown(self) -> List[str]: """ @@ -274,7 +276,7 @@ def is_expanded(self) -> bool: :returns: True, if the picker is displayed """ try: - return self._picker.find(wait_timeout=0.5) is not None + return self._picker.find(timeout=0.5) is not None except TimeoutException: return False diff --git a/Components/PerspectiveComponents/Inputs/Dropdown.py b/Components/PerspectiveComponents/Inputs/Dropdown.py index 3f84977..bda3302 100644 --- a/Components/PerspectiveComponents/Inputs/Dropdown.py +++ b/Components/PerspectiveComponents/Inputs/Dropdown.py @@ -8,7 +8,7 @@ from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import ComponentPiece, BasicPerspectiveComponent -from Components.PerspectiveComponents.Common.Dropdown import CommonDropdown +from Components.Common.Dropdown import CommonDropdown from Helpers.CSSEnumerations import CSSPropertyValue from Helpers.IAAssert import IAAssert from Helpers.IAExpectedConditions import IAExpectedConditions as IAec @@ -27,31 +27,33 @@ class Dropdown(CommonDropdown, BasicPerspectiveComponent): _SELECTED_OPTION_LOCATOR = (By.CSS_SELECTOR, '> div') _COMMON_CONTAINER_LOCATOR = (By.CSS_SELECTOR, 'div.iaDropdownCommon_container') _NO_RESULTS_MODAL_LOCATOR = (By.CSS_SELECTOR, 'a.iaDropdownCommon_option.ia_dropdown__option__noResults') + _SELECTED_OPTION_CLASS = 'ia_dropdown__option--selected' def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 4, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 4, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): CommonDropdown.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) BasicPerspectiveComponent.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, # only needs to be done once - poll_freq=poll_freq - ) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._placeholder = ComponentPiece( locator=self._PLACEHOLDER_LOCATOR, driver=self.driver, @@ -106,7 +108,7 @@ def clear_all_selections(self) -> None: :raises TimeoutException: If the clear all selected options 'X' is not present. :raises AssertionError: If unsuccessful in removing all selected options. """ - self._clear_all_options.click(wait_timeout=1) + self._clear_all_options.click(timeout=1) IAAssert.is_equal_to( actual_value=len(self.get_selected_options_as_list()), expected_value=0, @@ -161,7 +163,7 @@ def get_common_container_css_property(self, property_name: Union[CSSPropertyValu def get_count_of_active_dropdowns(self) -> int: """Obtain a count of how many Dropdowns are currently displaying options for selection.""" try: - return len(self._all_active_dropdowns.find_all(wait_timeout=1)) + return len(self._all_active_dropdowns.find_all(timeout=1)) except TimeoutException: return 0 @@ -323,14 +325,25 @@ def option_has_focus(self, option_text: str) -> bool: except TimeoutException: return False + def option_is_selected_in_expanded_options(self, option_text: str) -> bool: + """ + Determine if an option is selected. + + :param option_text: The text of the option to check. + + :raises TimeoutException: If no options are currently displayed, or no option with the supplied text exists. + """ + return self._SELECTED_OPTION_CLASS in self._get_option( + option_text=option_text).find().get_attribute("class") + def placeholder_text_is_displayed(self) -> bool: """Determine if placeholder text is currently displayed.""" try: - return len(self._placeholder.find(wait_timeout=1).text) > 0 + return len(self._placeholder.find(timeout=1).text) > 0 except TimeoutException: return False - def select_option_by_text_if_not_selected(self, option_text: str, binding_wait_time: float = 0.5) -> None: + def select_option_by_text_if_not_selected(self, option_text: str, wait_after_click: float = 0.5) -> None: """ Select an option. If the option is already selected, no action is taken. @@ -338,7 +351,7 @@ def select_option_by_text_if_not_selected(self, option_text: str, binding_wait_t so this function will attempt to filter the displayed options if the desired option is not readily seen. :param option_text: The text of the option you would like to select. - :param binding_wait_time: The amount of time to wait after selecting an option before allowing the code to + :param wait_after_click: The amount of time to wait after selecting an option before allowing the code to continue. :raises TimeoutException: If no option with the supplied text exists as an option. @@ -356,7 +369,7 @@ def select_option_by_text_if_not_selected(self, option_text: str, binding_wait_t # filtering the displayed options didn't work, so our only option is to scroll and hope it works self._get_option(option_text=option_text).scroll_to_element() try: - self._get_option(option_text=option_text).click(wait_timeout=1, binding_wait_time=binding_wait_time) + self._get_option(option_text=option_text).click(timeout=1, wait_after_click=wait_after_click) except TimeoutException as toe: toe.msg = f"Failed to locate element with text of \"{option_text}\"." raise toe @@ -407,4 +420,4 @@ def _option_has_focus(self, option_text: str) -> bool: :raises TimeoutException: If no options are currently displayed, or no option with the supplied text exists. """ return self._FOCUSED_OPTION_CLASS in self._get_option( - option_text=option_text).find(wait_timeout=0).get_attribute("class") + option_text=option_text).find(timeout=0).get_attribute("class") diff --git a/Components/PerspectiveComponents/Inputs/FileUpload.py b/Components/PerspectiveComponents/Inputs/FileUpload.py index 16f02c1..5359b70 100644 --- a/Components/PerspectiveComponents/Inputs/FileUpload.py +++ b/Components/PerspectiveComponents/Inputs/FileUpload.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -7,8 +7,9 @@ from Components.BasicComponent import BasicPerspectiveComponent, ComponentPiece from Components.Common.FileUpload import FileUpload as CommonFileUpload +from Components.Common.Icon import CommonIcon from Components.PerspectiveComponents.Common.ComponentModal import ComponentModal -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Helpers.Formatting import FilePathFormatting class FileUpload(BasicPerspectiveComponent, CommonFileUpload): @@ -44,19 +45,30 @@ class FileUpload(BasicPerspectiveComponent, CommonFileUpload): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): - super().__init__( + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): + CommonFileUpload.__init__( + self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) + BasicPerspectiveComponent.__init__( + self, + locator=locator, + driver=driver, + parent_locator_list=parent_locator_list, + timeout=timeout, + description=description, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._display_message = ComponentPiece( locator=self._SPAN_LOCATOR, driver=driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) self._primary_icon = CommonIcon( @@ -161,7 +173,7 @@ def clear_uploads(self, max_number_of_attempts: int = 10) -> None: while not self.summary_panel_is_visible() and attempts < max_number_of_attempts: self.click_next_page() attempts += 1 - self._get_clear_uploads_link().click(wait_timeout=1) + self._get_clear_uploads_link().click(timeout=1) def click_next_page(self) -> None: """ @@ -169,7 +181,7 @@ def click_next_page(self) -> None: :raises TimeoutException: If already on the last page, or if the summary is not present. """ - self._next_page_link.click(binding_wait_time=1) + self._next_page_link.click(wait_after_click=1) def click_previous_page(self) -> None: """ @@ -177,7 +189,7 @@ def click_previous_page(self) -> None: :raises TimeoutException: If already on the first page, or if the summary is not present. """ - self._previous_page_link.click(binding_wait_time=1) + self._previous_page_link.click(wait_after_click=1) def _expose_file_input_element(self) -> None: """ @@ -274,7 +286,7 @@ def get_total_count_of_results(self) -> int: on_summary_page = self.get_current_page_index() == -1 if on_summary_page: self.click_previous_page() - self.wait_on_binding(time_to_wait=0.5) + self.wait_some_time(time_to_wait=0.5) count_of_results = int(self._file_message.get_text().split(" ")[-1]) if on_summary_page: self.click_next_page() @@ -287,7 +299,7 @@ def get_warning_text(self) -> str: :raises TimeoutException: If no warning is currently displayed. """ try: - return self._get_file_upload_error().find(wait_timeout=0.5).text + return self._get_file_upload_error().find(timeout=0.5).text except TimeoutException: return "" @@ -304,11 +316,11 @@ def hover_over_info_icon(self) -> None: def is_using_small_layout(self) -> bool: """Determine if the component is currently rendering in the 'small' layout.""" try: - return self._primary_icon.find(wait_timeout=0) is not None + return self._primary_icon.find(timeout=0) is not None except TimeoutException: return False - def is_displaying_hover_info_icon(self, wait_timeout: int = 1) -> bool: + def is_displaying_hover_info_icon(self, timeout: int = 1) -> bool: """ Determine if the component is currently displaying the information icon. @@ -316,7 +328,7 @@ def is_displaying_hover_info_icon(self, wait_timeout: int = 1) -> bool: if rendering in the 'small' or 'large' layout OR no file extension restrictions are in place. """ try: - return self._supported_file_types_icon.find(wait_timeout=wait_timeout) is not None + return self._supported_file_types_icon.find(timeout=timeout) is not None except TimeoutException: return False @@ -343,8 +355,8 @@ def navigate_to_file_summary_by_index(self, desired_index: int) -> None: current_index = self.get_current_page_index() # stop if we reach the desired page or the summary page while current_index != desired_index and current_index != -1: - self._previous_page_link.click(binding_wait_time=0.5) if current_index > desired_index \ - else self._next_page_link.click(binding_wait_time=0.5) + self._previous_page_link.click(wait_after_click=0.5) if current_index > desired_index \ + else self._next_page_link.click(wait_after_click=0.5) current_index = self.get_current_page_index() def next_page_link_is_enabled(self) -> bool: @@ -369,7 +381,7 @@ def small_layout_modal_is_displayed(self) -> bool: return ComponentPiece( locator=self._SPAN_LOCATOR, driver=self.driver, - wait_timeout=1, + timeout=1, parent_locator_list=self._modal.locator_list).find() is not None except TimeoutException: return False @@ -377,24 +389,25 @@ def small_layout_modal_is_displayed(self) -> bool: def summary_panel_is_visible(self) -> bool: """Determine if the Summary panel is visible in the upload summary pagination panel.""" try: - return "Summary" in self._file_message.find(wait_timeout=0.5).text + return "Summary" in self._file_message.find(timeout=0.5).text except TimeoutException: return False - def upload_file_by_path(self, normalized_file_path: str) -> None: + def upload_file_by_path(self, file_path: str) -> None: """ Upload an individual file by supplying a normalized file path. - :param normalized_file_path: An OS-agnostic string file path. + :param file_path: A string file path. """ - self.upload_files(normalized_file_path_list=[normalized_file_path]) + self.upload_files(path_list=[file_path]) - def upload_files(self, normalized_file_path_list: List[str]) -> None: + def upload_files(self, path_list: List[str]) -> None: """ - Upload a list of files by supplying normalized file paths. + Upload a list of files by supplying a list of file paths. - :param normalized_file_path_list: A list of OS-agnostic string file paths. + :param path_list: A list of string file paths. """ + normalized_file_path_list = [FilePathFormatting.system_safe_file_path(file_path) for file_path in path_list] self.driver.execute_script('arguments[0].style.display="block";', self._find_file_input()) self._find_file_input().send_keys('\n'.join(normalized_file_path_list)) self.driver.execute_script('arguments[0].style.display="none";', self._find_file_input()) @@ -413,7 +426,7 @@ def upload_was_successful(self) -> bool: locator=self._SUCCESS_MESSAGE_LOCATOR, driver=self.driver, parent_locator_list=parent_locator_list, - poll_freq=self.poll_freq).find(wait_timeout=5).text == 'Upload Successful!' + poll_freq=self.poll_freq).find(timeout=5).text == 'Upload Successful!' except TimeoutException: return False @@ -424,7 +437,7 @@ def warning_is_displayed(self) -> bool: :returns: True, if a warning is currently displayed - False otherwise. """ try: - return self._get_file_upload_error().find(wait_timeout=0.5) is not None + return self._get_file_upload_error().find(timeout=0.5) is not None except TimeoutException: return False @@ -440,7 +453,7 @@ def _find_file_input(self) -> WebElement: locator=self._INPUT_LOCATOR, driver=self.driver, parent_locator_list=self._modal.locator_list if self.is_using_small_layout() else self.locator_list, - poll_freq=self.poll_freq).find(wait_timeout=2) + poll_freq=self.poll_freq).find(timeout=2) def _get_file_upload_error(self) -> ComponentPiece: """ diff --git a/Components/PerspectiveComponents/Inputs/FormComponent.py b/Components/PerspectiveComponents/Inputs/FormComponent.py new file mode 100644 index 0000000..21cf64f --- /dev/null +++ b/Components/PerspectiveComponents/Inputs/FormComponent.py @@ -0,0 +1,471 @@ +from typing import Optional, List, Tuple, Union + +from selenium.common import ElementNotInteractableException, NoSuchElementException, \ + TimeoutException, ElementClickInterceptedException, JavascriptException +from selenium.webdriver import ActionChains, Keys +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver + +from Components.BasicComponent import BasicPerspectiveComponent, ComponentPiece +from Components.Common.Button import CommonButton +from Components.Common.Dropdown import CommonDropdown +from Components.Common.TextInput import CommonTextInput +from Helpers.IAAssert import IAAssert +from Helpers.Point import Point + + +class FormComponent(BasicPerspectiveComponent): + """ + A Perspective Form Component. + """ + _FORM_CANCEL_ACTION_BUTTON_LOCATOR = (By.CSS_SELECTOR, ".ia_form__button--cancel") + _FORM_SUBMIT_ACTION_BUTTON_LOCATOR = (By.CSS_SELECTOR, ".ia_form__button--submit") + _FORM_NOTIFICATION_LOCATOR = (By.CSS_SELECTOR, ".ia_form__notifications") + + def __init__( + self, + driver: WebDriver, + locator: Tuple[Union[By, str], str] = None, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None): + BasicPerspectiveComponent.__init__( + self, + driver=driver, + locator=locator, + parent_locator_list=parent_locator_list) + self._cancel_action_button = CommonButton( + locator=self._FORM_CANCEL_ACTION_BUTTON_LOCATOR, + driver=self.driver, + parent_locator_list=self.locator_list) + self._submit_action_button = CommonButton( + locator=self._FORM_SUBMIT_ACTION_BUTTON_LOCATOR, + driver=self.driver, + parent_locator_list=self.locator_list) + self._form_notification = ComponentPiece( + locator=self._FORM_NOTIFICATION_LOCATOR, + driver=driver, + description="The form inline notification that displays at the bottom of the form when awaitResponse is " + "set to true.", + timeout=0) + + def click_cancel_action_button( + self, timeout: Optional[Union[int, float]] = None, wait_after_click: float = 0) -> None: + """ + Click the cancel action button. + + :param timeout: Poll the DOM up to this amount of time before potentially throwing a TimeoutException. + Overrides the default of the component. + :param wait_after_click: The amount of time to wait after the click event occurs before continuing. + + :raises TimeoutException: If the component is not found in the DOM. + """ + self._cancel_action_button.click(timeout=timeout, wait_after_click=wait_after_click) + + def click_submit_action_button( + self, timeout: Optional[Union[int, float]] = None, wait_after_click: float = 0) -> None: + """ + Click the submit action button. + + :param timeout: Poll the DOM up to this amount of time before potentially throwing a TimeoutException. + Overrides the default of the component. + :param wait_after_click: The amount of time to wait after the click event occurs before continuing. + + :raises TimeoutException: If the component is not found in the DOM. + """ + self._submit_action_button.click(timeout=timeout, wait_after_click=wait_after_click) + + def cancel_action_button_is_displayed(self) -> bool: + """ + Checks if the cancel action button is displayed. + This function returns a boolean indicating whether the cancel button is visible to the user on the form. + :return: True if the cancel action button is displayed, False otherwise. + """ + return self._cancel_action_button.is_displayed() + + def cancel_action_button_is_enabled(self) -> bool: + """ + Checks if the form cancel action button is enabled. + This function returns a boolean indicating whether the cancel button is enabled and clickable by the user. + return: True if the cancel action button is enabled, False otherwise. + """ + return self._cancel_action_button.is_enabled() + + def get_cancel_action_button_origin(self) -> Point: + """ + Get the Cartesian Coordinate of the upper-left corner of the cancel button, measured from the top-left of the + viewport. + + :returns: The Cartesian Coordinate of the upper-left corner of the cancel button, measured from the top-left + of the viewport. + """ + return self._cancel_action_button.get_origin() + + def get_submit_action_button_origin(self) -> Point: + """ + Get the Cartesian Coordinate of the upper-left corner of the submit button, measured from the top-left of the + viewport. + + :returns: The Cartesian Coordinate of the upper-left corner of the submit button, measured from the top-left + of the viewport. + """ + return self._submit_action_button.get_origin() + + def get_cancel_action_button_text(self) -> str: + """ + Retrieves the text from the cancel action button. + This method locates the cancel action button on the interface and returns the text displayed on it. Raises an + exception if the element is not found. + + Returns: + str: The text context of the cancel action button. + + Raises: + NoSuchElementException: If the cancel action button is not found. + """ + button_text = self._cancel_action_button.find().text + if not button_text: # If text is empty or none + raise NoSuchElementException("Cancel action button not found on the page.") + return button_text + + def get_origin_of_cancel_action_button(self) -> Point: + """ + Get the Cartesian Coordinate of the upper-left corner of the cancel action button, measured from the + top-left of the viewport. + + :returns: The Cartesian Coordinate of the upper-left corner of the cancel action button, measured from the + top-left of the viewport. + """ + return self._cancel_action_button.get_origin() + + def get_origin_of_submit_action_button(self) -> Point: + """ + Get the Cartesian Coordinate of the upper-left corner of the submit action button, measured from the + top-left of the viewport. + + :returns: The Cartesian Coordinate of the upper-left corner of the submit action button, measured from the + top-left of the viewport. + """ + return self._submit_action_button.get_origin() + + def get_submit_action_button_text(self) -> str: + """ + Retrieves the text from the submit action button. + This method locates the submit action button on the interface and returns the text displayed on it. Raises an + exception if the element is not found. + + Returns: + str: The text context of the submit action button. + + Raises: + NoSuchElementException: If the submit action button is not found. + """ + button_text = self._submit_action_button.find().text + if not button_text: # If text is empty or none + raise NoSuchElementException("Submit action button not found on the page.") + return button_text + + def get_termination_of_cancel_action_button(self) -> Point: + """ + Get the Cartesian Coordinate of the bottom-right corner of the cancel action button, measured from the + top-left of the viewport. + + :returns: The Cartesian Coordinate of the bottom-right corner of the cancel action button, measured from the + top-left of the viewport. + """ + return self._cancel_action_button.get_termination() + + def get_termination_of_submit_action_button(self) -> Point: + """ + Get the Cartesian Coordinate of the bottom-right corner of the submit action button, measured from the + top-left of the viewport. + + :returns: The Cartesian Coordinate of the bottom-right corner of the submit action button, measured from the + top-left of the viewport. + """ + return self._submit_action_button.get_termination() + + def form_notification_is_displayed(self) -> bool: + """ + Determine whether the form inline notification is currently visible. + + :returns: True if the notification is visible, False otherwise. + """ + try: + return self._form_notification.is_displayed() + except TimeoutException: + return False + + def submit_action_button_is_displayed(self) -> bool: + """ + Checks if the form submit action button is displayed. + This function returns a boolean indicating whether the submit button is visible to the user on the form. + :return: True if the submit action button is displayed, False otherwise. + """ + return self._submit_action_button.is_displayed() + + def submit_action_button_is_enabled(self) -> bool: + """ + Checks if the form submit action button is enabled. + This function returns a boolean indicating whether the submit button is enabled and clickable by the user. + return: True if the submit action button is enabled, False otherwise. + """ + return self._submit_action_button.is_enabled() + + +class _FormWidget(ComponentPiece): + """ + A generic class intended to be inherited from by any sort of Perspective Form widget. When using widgets, + it is highly recommended to add a `domId` property to the `widget` object. + """ + _FORM_VALIDATION_ERROR_LOCATOR = (By.CSS_SELECTOR, ".ia_form__error") + + def __init__( + self, + driver: WebDriver, + locator: Tuple[Union[By, str], str], + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, + description: Optional[str] = None): + ComponentPiece.__init__( + self, + driver=driver, + locator=locator, + parent_locator_list=parent_locator_list, + timeout=timeout, + description=description) + self._validation_message = ComponentPiece( + driver=driver, + locator=self._FORM_VALIDATION_ERROR_LOCATOR, + parent_locator_list=self.locator_list, + description="The validation message for a widget.") + + def get_widget_validation_message(self) -> str: + """ + Retrieves the widget validation message displayed on the form. + + :raises TimeoutException: If the validation message is not displayed. + + :return: The text content of the validation message element. + """ + return self._validation_message.get_text() + + def validation_warning_is_displayed(self) -> bool: + """ + Determine whether the widget validation warning is currently visible. + + :returns: True if the validation warning is visible, False otherwise. + """ + return self._validation_message.is_displayed() + + +class _FormInputWidget(_FormWidget): + """ + A generic class intended to be inherited from by any sort of Perspective Form input widget. When using widgets, + it is highly recommended to add a `domId` property to the `widget` object. + """ + _FORM_INPUT_FIELD_LOCATOR = (By.CSS_SELECTOR, ".ia_form__controlBody>:first-child") + + def __init__( + self, + driver: WebDriver, + locator: Tuple[Union[By, str], str], + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, + description: Optional[str] = None): + _FormWidget.__init__( + self, + driver=driver, + locator=locator, + parent_locator_list=parent_locator_list, + timeout=timeout, + description=description) + self._input_field = CommonTextInput( + driver=driver, + locator=self._FORM_INPUT_FIELD_LOCATOR, + parent_locator_list=self.locator_list, + timeout=timeout) + + def can_receive_focus(self) -> bool: + """ + Determine if the input field can receive focus. + + :returns: True if the input field can receive focus, False otherwise. + """ + try: + element = self._input_field.find() + self.driver.execute_script("arguments[0].focus();", element) + return self.driver.execute_script( + "return document.activeElement === arguments[0];", element) + except JavascriptException: + return False + + def is_enabled(self) -> bool: + """ + Determine if the input field is enabled. + + :returns: True if the input field is enabled, False otherwise. + """ + return self._input_field.find().get_attribute(name="disabled") is None + + def is_read_only(self) -> bool: + """ + Determine if the input field is read-only. + + :returns: True if the input field is read-only, False otherwise. + """ + return self._input_field.find().get_attribute(name="readonly") is not None + + def _set_text( + self, + text: str, + release_focus: bool = True, + wait_after: float = 0) -> None: + """ + Set the text of the input field for the widget. + + :param text: The text you would like to apply. + :param release_focus: dictates whether a `blur()` event is invoked for the component after typing the supplied + text. Default is True. + :param wait_after: How long to wait after supplying the provided text and applying the potential `blur()` event. + + :raises AssertionError: If the text was not set to the supplied value. + :raises ElementNotInteractableException: If the input field is read-only. + """ + if self._input_field.find().get_attribute(name="readonly"): + raise ElementNotInteractableException(f"The input field is read-only.") + + text = str(text) + # strip special characters (ESC, ENTER) while leaving spaces and punctuation + expected_text = text.encode("ascii", "ignore").decode() + if text is None or text == '': + text = ' ' + Keys.BACKSPACE + + try: + self._input_field.click() + except ElementClickInterceptedException: + # Handle overlay interference + ActionChains(driver=self.driver) \ + .move_to_element_with_offset(to_element=self._input_field.find(), xoffset=5, yoffset=5) \ + .click() \ + .perform() + + current_text = self._input_field.find().get_attribute('value') # Get current text without waiting + keys_to_send = ''.join([Keys.ARROW_RIGHT for _ in current_text] + + [Keys.BACKSPACE for _ in current_text] + + [text]) + + self._input_field.find().send_keys(keys_to_send) + if release_focus: + self._input_field.release_focus() + # Verify the text is set correctly + IAAssert.is_equal_to( + actual_value=self._input_field.find().get_attribute('value'), + expected_value=expected_text, + failure_msg=f"Failed to set the text for this widget.") + self.wait_some_time(time_to_wait=wait_after) + + +class NumberWidget(_FormInputWidget): + """ + A Perspective Form `number` widget. When using widgets, it is highly recommended to add a `domId` property to + the `widget` object. + """ + def __init__(self, + driver: WebDriver, + locator: Tuple[Union[By, str], str], + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10): + _FormInputWidget.__init__( + self, + driver=driver, + locator=locator, + parent_locator_list=parent_locator_list, + timeout=timeout) + + def get_text(self) -> str: + return self._input_field.get_text() + + def set_text(self, text: Union[float, str], release_focus: bool = True, wait_after: float = 0) -> None: + _FormInputWidget._set_text(self, text=str(text), release_focus=release_focus, wait_after=wait_after) + + +class TextWidget(_FormInputWidget): + """ + A Perspective Form `text` widget. When using widgets, it is highly recommended to add a `domId` property to + the `widget` object. + """ + def __init__(self, + driver: WebDriver, + locator: Tuple[Union[By, str], str], + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10): + _FormInputWidget.__init__( + self, + driver=driver, + locator=locator, + parent_locator_list=parent_locator_list, + timeout=timeout) + + def get_text(self) -> str: + return self._input_field.get_text() + + def get_placeholder_text(self) -> str: + return self._input_field.get_placeholder_text() + + def set_text(self, text: str, release_focus: bool = True, wait_after: float = 0) -> None: + _FormInputWidget._set_text(self, text=text, release_focus=release_focus, wait_after=wait_after) + + +class TextAreaWidget(_FormInputWidget): + """ + A Perspective Form `text-area` widget. When using widgets, it is highly recommended to add a `domId` property to + the `widget` object. + """ + def __init__(self, + driver: WebDriver, + locator: Tuple[Union[By, str], str], + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10): + _FormInputWidget.__init__( + self, + driver=driver, + locator=locator, + parent_locator_list=parent_locator_list, + timeout=timeout) + + def get_text(self) -> str: + return self._input_field.get_text() + + def set_text(self, text: str, release_focus: bool = True, wait_after: float = 0) -> None: + _FormInputWidget._set_text(self, text=text, release_focus=release_focus, wait_after=wait_after) + + +class DropdownWidget(_FormWidget): + """ + A Perspective Form `dropdown` widget. When using widgets, it is highly recommended to add a `domId` property to + the `widget` object. + """ + # does not use _FormInputWidget because the "input" field for Dropdowns is not an element. + def __init__(self, + driver: WebDriver, + locator: Tuple[Union[By, str], str], + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10): + _FormWidget.__init__( + self, + driver=driver, + locator=locator, + parent_locator_list=parent_locator_list) + self._dropdown = CommonDropdown( + driver=driver, + locator=(By.CSS_SELECTOR, ".ia_form__controlBody>:first-child"), # we still want the first child + parent_locator_list=self.locator_list, + timeout=timeout) + + def get_selected_options(self) -> list[str]: + return self._dropdown.get_selected_options_as_list() + + def get_text(self) -> str: + return self._dropdown.get_text() + + def select_option_by_text_if_not_selected(self, option_text: str) -> None: + self._dropdown.select_option_by_text_if_not_selected(option_text=option_text) diff --git a/Components/PerspectiveComponents/Inputs/MultiStateButton.py b/Components/PerspectiveComponents/Inputs/MultiStateButton.py index 16b2628..912f574 100644 --- a/Components/PerspectiveComponents/Inputs/MultiStateButton.py +++ b/Components/PerspectiveComponents/Inputs/MultiStateButton.py @@ -17,19 +17,21 @@ class MultiStateButton(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._generic_button = ComponentPiece( locator=self._GENERAL_BUTTON_LOCATOR, driver=driver, @@ -45,19 +47,19 @@ def click(self, **kwargs) -> None: """ raise NotImplementedError - def click_button_by_text(self, button_text: str, wait_timeout: float = 0.5, binding_wait_time: float = 0.5) -> None: + def click_button_by_text(self, button_text: str, timeout: float = 0.5, wait_after_click: float = 0.5) -> None: """ Click one of the Multi-State Buttons by text. If multiple buttons share the same text only the first match will be clicked. :param button_text: The text of the button you would like to click. - :param wait_timeout: The amount of time to wait until a button with the specified text becomes visible. - :param binding_wait_time: The amount of time after the click event to wait before allowing code to continue. + :param timeout: The amount of time to wait until a button with the specified text becomes visible. + :param wait_after_click: The amount of time after the click event to wait before allowing code to continue. :raises TimeoutException: If no button with the specified text is found. """ self._get_button_by_text(button_text=button_text).click( - wait_timeout=wait_timeout, binding_wait_time=binding_wait_time) + timeout=timeout, wait_after_click=wait_after_click) def get_css_property_value_of_button_by_text( self, @@ -73,9 +75,9 @@ def get_css_property_value_of_button_by_text( """ return self._get_button_by_text(button_text=button_text).get_css_property(property_name=property_name) - def get_button_count(self, wait_timeout: float = 0.5) -> int: + def get_button_count(self, timeout: float = 0.5) -> int: """Obtain a count of all states contained within this Multi-State Button.""" - return len(self._generic_button.find_all(wait_timeout=wait_timeout)) + return len(self._generic_button.find_all(timeout=timeout)) def get_button_gap(self) -> float: """Obtain the current gap between each button.""" diff --git a/Components/PerspectiveComponents/Inputs/NumericEntryField.py b/Components/PerspectiveComponents/Inputs/NumericEntryField.py index 9dd683e..a6d2a2d 100644 --- a/Components/PerspectiveComponents/Inputs/NumericEntryField.py +++ b/Components/PerspectiveComponents/Inputs/NumericEntryField.py @@ -43,26 +43,28 @@ class _NEF(BasicPerspectiveComponent, CommonTextInput): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): BasicPerspectiveComponent.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) CommonTextInput.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) @@ -70,7 +72,7 @@ def wait_on_text_condition( self, text_to_compare: Optional[Any], condition: NumericCondition, - wait_timeout: Optional[float] = None) -> str: + timeout: Optional[float] = None) -> str: """ Obtain the text of this Numeric Entry Field, after potentially waiting some period of time for the Component to display that text. @@ -78,14 +80,14 @@ def wait_on_text_condition( :param text_to_compare: The text to be used in conjunction with the supplied condition. If the supplied value is None, then no wait will ever occur and the text of the component will be immediately returned. :param condition: The condition to be used to compare the Component text to the provided text. - :param wait_timeout: The amount of time (in seconds) you are willing to wait for the Component to display + :param timeout: The amount of time (in seconds) you are willing to wait for the Component to display the specified text. If not supplied, this will default to the wait timeout supplied in the constructor of the Component. :returns: The text of the Component as a string, after potentially having waited for an expected text match. """ return CommonTextInput.wait_on_text_condition( - self, text_to_compare=text_to_compare, condition=condition, wait_timeout=wait_timeout) + self, text_to_compare=text_to_compare, condition=condition, timeout=timeout) class ButtonNEF(_NEF): @@ -96,20 +98,22 @@ class ButtonNEF(_NEF): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): _NEF.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._edit_icon = ComponentPiece( locator=(By.TAG_NAME, "a"), driver=driver, @@ -117,7 +121,7 @@ def __init__( poll_freq=poll_freq) self._modal = ComponentModal( driver=driver, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) # no parent self._apply_button = CommonButton( locator=self._APPLY_BUTTON_LOCATOR, @@ -197,7 +201,7 @@ def get_value_attribute_from_modal(self) -> Optional[str]: """ if not self.modal_is_displayed(): self.click_edit_icon() - return self._modal_input.find(wait_timeout=0).get_attribute("value") + return self._modal_input.find(timeout=0).get_attribute("value") def modal_is_displayed(self) -> bool: """ @@ -223,7 +227,7 @@ def placeholder_is_displayed(self) -> bool: attribute - False otherwise. """ placeholder_attr_value = self.get_placeholder_text() - value_attr_value = self._internal_input.find(wait_timeout=0).get_attribute("value") + value_attr_value = self._internal_input.find(timeout=0).get_attribute("value") return (placeholder_attr_value is not None) and (len(placeholder_attr_value) > 0) and (not value_attr_value) def placeholder_is_displayed_in_modal(self) -> bool: @@ -245,7 +249,7 @@ def set_text( self, text: Union[float, str], apply_cancel_no_action: Optional[bool] = True, - binding_wait_time: float = 0.5) -> None: + wait_after: float = 0.5) -> None: """ Convenience function to set the text of the Numeric Entry Field. Handles the opening of the entry modal and also applies, cancels, or takes no action as directed via arguments. @@ -253,7 +257,7 @@ def set_text( :param text: The value which you would like to apply to the Numeric Entry Field. :param apply_cancel_no_action: If True, apply changes. If False, cancel changes after typing supplied text. If None, take no action - leaving the entry modal displayed without any changes committed. - :param binding_wait_time: The amount of time to wait after applying or cancelling changes. Ignored if no action + :param wait_after: The amount of time to wait after applying or cancelling changes. Ignored if no action is to be taken after sending text. :raises AssertionError: If application or cancellation is specified, and the final displayed (and formatted) @@ -270,11 +274,10 @@ def set_text( value=self.wait_on_text_condition( text_to_compare=text, condition=NumericCondition.EQUALS, - wait_timeout=binding_wait_time + 0.5)), # hard-coded extra "soft" wait time due to tags + timeout=wait_after + 0.5)), # hard-coded extra "soft" wait time due to tags expected_value=_remove_commas(value=_zero_out(expected_value=text)), failure_msg="Failed to set the text of the Button-mode Numeric Entry Field.", as_type=str) - self.wait_on_binding(time_to_wait=binding_wait_time) def _set_text(self, text: Union[float, str]) -> None: """ @@ -295,16 +298,25 @@ def __init__( locator, driver: WebDriver, parent_locator_list: Optional[List] = None, - wait_timeout: float = 5, + timeout: float = 5, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): _NEF.__init__( self, - locator=locator, - driver=driver, - parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + locator=locator, + driver=driver, + parent_locator_list=parent_locator_list, + timeout=timeout, description=description, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) + CommonTextInput.__init__( + self, + locator=locator, + driver=driver, + parent_locator_list=parent_locator_list, + timeout=timeout, poll_freq=poll_freq) def get_manual_entry_text(self) -> str: @@ -355,13 +367,13 @@ def set_text( self, text: Union[float, int, str], release_focus: bool = True, - binding_wait_time: float = 1) -> None: + wait_after: float = 1) -> None: """ Set the text of the Direct Numeric Entry Field. :param text: The value which you would like to apply to the Numeric Entry Field. :param release_focus: Specifies whether to commit the text after typing. - :param binding_wait_time: The amount of time to wait after applying or cancelling changes. Ignored if no action + :param wait_after: The amount of time to wait after applying or cancelling changes. Ignored if no action is to be taken after sending text. :raises AssertionError: If the final displayed (and formatted) value of the Numeric Entry Field does not match @@ -379,11 +391,11 @@ def set_text( value=self.wait_on_text_condition( text_to_compare=text, condition=NumericCondition.EQUALS, - wait_timeout=binding_wait_time + 0.5)), # hard-coded extra "soft" wait time due to tags + timeout=wait_after + 0.5)), # hard-coded extra "soft" wait time due to tags expected_value=_remove_commas(value=_zero_out(expected_value=text)), failure_msg="Failed to apply the supplied text to the Direct-variant Numeric Entry Field.", as_type=str) - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=wait_after) class ProtectedNEF(DirectNEF): @@ -396,32 +408,34 @@ class ProtectedNEF(DirectNEF): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): DirectNEF.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) def set_text( self, text: Union[float, int, str], release_focus: bool = True, - binding_wait_time: float = 1) -> None: + wait_after: float = 1) -> None: """ Pass-through function to set the text of the Protected Numeric Entry Field via double-click. :param text: The value which you would like to apply to the Numeric Entry Field. :param release_focus: Specifies whether to commit the text after typing. - :param binding_wait_time: The amount of time to wait after applying or cancelling changes. Ignored if no action + :param wait_after: The amount of time to wait after applying or cancelling changes. Ignored if no action is to be taken after sending text. :raises AssertionError: If the final displayed (and formatted) value of the Numeric Entry Field does not match @@ -430,19 +444,19 @@ def set_text( self.set_text_via_double_click( text=text, release_focus=release_focus, - binding_wait_time=binding_wait_time) + wait_after=wait_after) def set_text_via_double_click( self, text: Union[float, int, str], release_focus: bool = True, - binding_wait_time: float = 1) -> None: + wait_after: float = 1) -> None: """ Set the text of the Protected Numeric Entry Field via double-click. :param text: The value which you would like to apply to the Numeric Entry Field. :param release_focus: Specifies whether to commit the text after typing. - :param binding_wait_time: The amount of time to wait after applying or cancelling changes. Ignored if no action + :param wait_after: The amount of time to wait after applying or cancelling changes. Ignored if no action is to be taken after sending text. :raises AssertionError: If the final displayed (and formatted) value of the Numeric Entry Field does not match @@ -450,29 +464,30 @@ def set_text_via_double_click( """ IASelenium(driver=self.driver).double_click(web_element=self.find()) self._set_text(text=str(text), release_focus=release_focus) + self.wait_some_time(time_to_wait=wait_after) if release_focus: IAAssert.is_equal_to( actual_value=_remove_commas( value=self.wait_on_text_condition( text_to_compare=text, condition=NumericCondition.EQUALS, - wait_timeout=binding_wait_time + 0.5)), # hard-coded extra "soft" wait time due to tags + timeout=wait_after + 0.5)), # hard-coded extra "soft" wait time due to tags expected_value=_remove_commas(value=_zero_out(expected_value=text)), failure_msg="Failed to apply the supplied text to the Protected-variant Numeric Entry Field.", as_type=str) - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=wait_after) def set_text_via_long_press( self, text: str, release_focus: bool = True, - binding_wait_time: float = 1) -> None: + wait_after: float = 1) -> None: """ Set the text of the Protected Numeric Entry Field via a long-press. :param text: The value which you would like to apply to the Numeric Entry Field. :param release_focus: Specifies whether to commit the text after typing. - :param binding_wait_time: The amount of time to wait after applying or cancelling changes. Ignored if no action + :param wait_after: The amount of time to wait after applying or cancelling changes. Ignored if no action is to be taken after sending text. :raises AssertionError: If the final displayed (and formatted) value of the Numeric Entry Field does not match @@ -486,12 +501,12 @@ def set_text_via_long_press( value=self.wait_on_text_condition( text_to_compare=text, condition=NumericCondition.EQUALS, - wait_timeout=binding_wait_time + 0.5)), # hard-coded extra "soft" wait time due to tags + timeout=wait_after + 0.5)), # hard-coded extra "soft" wait time due to tags expected_value=_remove_commas(value=_zero_out(expected_value=text)), failure_msg="Failed to apply the supplied text to the Protected-variant Numeric Entry Field while " "activating the input via a long-press interaction.", as_type=str) - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=wait_after) def _set_text(self, text: str, release_focus: bool = False) -> None: """ diff --git a/Components/PerspectiveComponents/Inputs/OneShotButton.py b/Components/PerspectiveComponents/Inputs/OneShotButton.py index 0a9a25d..6931461 100644 --- a/Components/PerspectiveComponents/Inputs/OneShotButton.py +++ b/Components/PerspectiveComponents/Inputs/OneShotButton.py @@ -1,12 +1,12 @@ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import ComponentPiece +from Components.Common.Icon import CommonIcon from Components.PerspectiveComponents.Common.ComponentModal import ComponentModal -from Components.PerspectiveComponents.Common.Icon import CommonIcon from Components.PerspectiveComponents.Inputs.Button import Button @@ -19,19 +19,21 @@ class OneShotButton(Button): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._modal = ComponentModal(driver=driver, poll_freq=poll_freq) self._cancel_button = Button( locator=self._CANCEL_BUTTON_LOCATOR, @@ -77,6 +79,6 @@ def confirmation_modal_is_displayed(self) -> bool: :returns: True, if the confirmation modal is present - False otherwise. """ try: - return self._modal.find(wait_timeout=0.5) is not None and self._confirm_button.find(0.5) is not None + return self._modal.find(timeout=0.5) is not None and self._confirm_button.find(0.5) is not None except TimeoutException: return False diff --git a/Components/PerspectiveComponents/Inputs/PasswordField.py b/Components/PerspectiveComponents/Inputs/PasswordField.py index f5290cc..61456b6 100644 --- a/Components/PerspectiveComponents/Inputs/PasswordField.py +++ b/Components/PerspectiveComponents/Inputs/PasswordField.py @@ -5,8 +5,8 @@ from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import BasicPerspectiveComponent +from Components.Common.Icon import CommonIcon from Components.Common.TextInput import CommonTextInput -from Components.PerspectiveComponents.Common.Icon import CommonIcon class PasswordField(BasicPerspectiveComponent): @@ -15,30 +15,32 @@ class PasswordField(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._internal_input = CommonTextInput( locator=(By.TAG_NAME, "input"), driver=driver, parent_locator_list=self.locator_list, - wait_timeout=2, + timeout=2, poll_freq=poll_freq) self._icon = CommonIcon( locator=self._SHOW_PASSWORD_ICON_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def display_password(self, should_be_displayed: bool) -> None: @@ -95,19 +97,19 @@ def set_text( self, text: Union[int, float, str], release_focus: bool = True, - binding_wait_time: float = 0) -> None: + wait_after: float = 0) -> None: """ Set the text content of the Password Field. :param text: The text content you would like to set the Password Field to contain. :param release_focus: Dictates whether the ENTER Key is supplied as a means to commit the value. - :param binding_wait_time: The amount of time to wait after applying the supplied text before we allow code to + :param wait_after: The amount of time to wait after applying the supplied text before we allow code to continue. """ self._internal_input.find().clear() text = text + Keys.ENTER if release_focus else text self._internal_input.find().send_keys(text) - self.wait_on_binding(time_to_wait=binding_wait_time) + self.wait_some_time(time_to_wait=wait_after) def show_password_icon_is_visible(self) -> bool: """ diff --git a/Components/PerspectiveComponents/Inputs/RadioGroup.py b/Components/PerspectiveComponents/Inputs/RadioGroup.py index c748a71..f114b00 100644 --- a/Components/PerspectiveComponents/Inputs/RadioGroup.py +++ b/Components/PerspectiveComponents/Inputs/RadioGroup.py @@ -1,12 +1,12 @@ from enum import Enum -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import BasicPerspectiveComponent, ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon class RadioGroup(BasicPerspectiveComponent): @@ -31,41 +31,43 @@ class TextPosition(Enum): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._radio_group_form = ComponentPiece( locator=self._RADIO_GROUP_FORM_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1) + timeout=1) self._internal_radio_group_label_text = ComponentPiece( locator=self._INTERNAL_RADIO_GROUP_LABEL_TEXT_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1) + timeout=1) self._radio_group_button_value = ComponentPiece( locator=self._RADIO_GROUP_BUTTON_VALUE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1) + timeout=1) self._radio_wrapper_coll = {} self._input_coll = {} self._icon_coll = {} self._label_coll = {} def click_radio_button( - self, button_text: str = None, button_value: str = None, binding_wait_time: float = 2) -> None: + self, button_text: str = None, button_value: str = None, wait_after_click: float = 2) -> None: """ Click a Radio Button by supplying either the text of the Radio button or the value of the Radio button. The actual click will occur on the label for the Radio button. @@ -73,11 +75,11 @@ def click_radio_button( :param button_text: The text of the Radio button you would like to click. If multiple radio buttons have the same exact text the first match will be clicked. Takes precedence over button_value. :param button_value: The value of the Radio button you would like to click. - :param binding_wait_time: The amount of time after any click event before we allow code to continue. + :param wait_after_click: The amount of time after any click event before we allow code to continue. :raises TimeoutException: If no Radio button with the provided text or value is present. """ - self._get_label(button_text=button_text, button_value=button_value).click(binding_wait_time=binding_wait_time) + self._get_label(button_text=button_text, button_value=button_value).click(wait_after_click=wait_after_click) def get_count_of_radio_buttons_in_group(self) -> int: """Get a count of buttons within the Radio Group.""" diff --git a/Components/PerspectiveComponents/Inputs/SignaturePad.py b/Components/PerspectiveComponents/Inputs/SignaturePad.py index b55b306..f993ebd 100644 --- a/Components/PerspectiveComponents/Inputs/SignaturePad.py +++ b/Components/PerspectiveComponents/Inputs/SignaturePad.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Union from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By @@ -29,19 +29,21 @@ class SignaturePad(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._action_bar = ComponentPiece( locator=self._SIG_PAD_BAR_LOCATOR, driver=driver, diff --git a/Components/PerspectiveComponents/Inputs/Slider.py b/Components/PerspectiveComponents/Inputs/Slider.py index 078dba9..9cb7cdd 100644 --- a/Components/PerspectiveComponents/Inputs/Slider.py +++ b/Components/PerspectiveComponents/Inputs/Slider.py @@ -1,4 +1,4 @@ -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver import ActionChains @@ -18,19 +18,21 @@ class Slider(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 3, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 3, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._wrapper = ComponentPiece( locator=self._WRAPPER_LOCATOR, driver=driver, @@ -57,19 +59,19 @@ def __init__( parent_locator_list=self._wrapper.locator_list, poll_freq=poll_freq) - def click(self, wait_timeout: int = 1, binding_wait_time: float = 0.5) -> None: + def click(self, timeout: int = 1, wait_after_click: float = 0.5) -> None: """ :raises NotImplementedError: Clicking a Slider serves no purpose and risks setting the Slider to an unknown value. Please use click_handle instead. """ raise NotImplementedError("Please do not blindly click the Slider.") - def click_handle(self, binding_wait_time: float = 0.5) -> None: + def click_handle(self, wait_after_click: float = 0.5) -> None: """ Click the handle of the Slider. Note that this has the potential to change the value of the slider because different browser drivers click in different locations within the handle. """ - self._handle.click(binding_wait_time=binding_wait_time) + self._handle.click(wait_after_click=wait_after_click) def drag_to_percent_of_axis(self, percent: float) -> None: """ @@ -114,7 +116,7 @@ def drag_to_value(self, value: int, max_attempts: int = 150) -> None: and (int(self.get_max()) > int(self.get_current_value())) \ and (attempts < max_attempts): attempts += 1 - ActionChains(driver=self.driver).move_by_offset(x_step, (y_step * -1.0)).perform() + ActionChains(driver=self.driver).move_by_offset(x_step, (y_step * -1)).perform() ActionChains(driver=self.driver).release().perform() elif int(self.get_current_value()) > value: ActionChains(driver=self.driver).click_and_hold(on_element=self._handle.find()).perform() @@ -122,7 +124,7 @@ def drag_to_value(self, value: int, max_attempts: int = 150) -> None: and (int(self.get_min()) < int(self.get_current_value())) \ and (attempts < max_attempts): attempts += 1 - ActionChains(driver=self.driver).move_by_offset((x_step * -1.0), y_step).perform() + ActionChains(driver=self.driver).move_by_offset((x_step * -1), y_step).perform() ActionChains(driver=self.driver).release().perform() if self.get_current_value() != value: raise ValueError( diff --git a/Components/PerspectiveComponents/Inputs/TextArea.py b/Components/PerspectiveComponents/Inputs/TextArea.py index e47d26f..6496479 100644 --- a/Components/PerspectiveComponents/Inputs/TextArea.py +++ b/Components/PerspectiveComponents/Inputs/TextArea.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -12,26 +12,28 @@ class TextArea(BasicPerspectiveComponent, CommonTextInput): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): BasicPerspectiveComponent.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) CommonTextInput.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) diff --git a/Components/PerspectiveComponents/Inputs/TextField.py b/Components/PerspectiveComponents/Inputs/TextField.py index be1822c..c052e73 100644 --- a/Components/PerspectiveComponents/Inputs/TextField.py +++ b/Components/PerspectiveComponents/Inputs/TextField.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.common import TimeoutException from selenium.webdriver.common.by import By @@ -14,33 +14,35 @@ class TextField(BasicPerspectiveComponent, CommonTextInput): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): BasicPerspectiveComponent.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) CommonTextInput.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self.no_wait_text_input = CommonTextInput( locator=self._locator, driver=self.driver, parent_locator_list=self._parent_locator_list, - wait_timeout=0) + timeout=0) def get_text(self) -> str: self.find() diff --git a/Components/PerspectiveComponents/Inputs/ToggleSwitch.py b/Components/PerspectiveComponents/Inputs/ToggleSwitch.py index 3b268c1..b88ba9d 100644 --- a/Components/PerspectiveComponents/Inputs/ToggleSwitch.py +++ b/Components/PerspectiveComponents/Inputs/ToggleSwitch.py @@ -1,4 +1,4 @@ -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -25,19 +25,21 @@ class ToggleSwitch(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 10, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 10, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self.ts_thumb = ComponentPiece( locator=self._TS_THUMB_LOCATOR, driver=self.driver, @@ -70,31 +72,31 @@ def is_selected(self) -> bool: :return: True, if the Toggle Switch is currently selected (on/True/active) - False otherwise. """ - return self._TS_SELECTED_CLASS in self.ts_thumb.find(wait_timeout=0).get_attribute('class') + return self._TS_SELECTED_CLASS in self.ts_thumb.find(timeout=0).get_attribute('class') - def set_switch(self, should_be_selected: bool = True, binding_wait_time: float = 0.5) -> None: + def set_switch(self, should_be_selected: bool = True, wait_after_click: float = 0.5) -> None: """ Set the state of the Toggle Switch. Takes no action if the Toggle Switch already has the specified state. :param should_be_selected: The desired state of the Toggle Switch. - :param binding_wait_time: The amount of time to wait after any click action is taken before continuing. + :param wait_after_click: The amount of time to wait after any click action is taken before continuing. :raises AssertionError: If unsuccessful in applying the desired state to the Toggle Switch. """ if not self.wait_on_selection_state(expected_selection_state=should_be_selected): - self.click(binding_wait_time=binding_wait_time) + self.click(wait_after_click=wait_after_click) IAAssert.is_true( value=self.wait_on_selection_state(expected_selection_state=should_be_selected), failure_msg=f"Failed to set the state of a Toggle Switch to {should_be_selected}.") - def wait_on_selection_state(self, expected_selection_state: bool, wait_timeout: float = 1) -> bool: + def wait_on_selection_state(self, expected_selection_state: bool, timeout: float = 1) -> bool: """ Determine whether the Toggle Switch has the supplied state. Preferable to is_selected because has_state works for both active/inactive states. :param expected_selection_state: The state you want to wait for the Toggle Switch to take, where True is selected, and False is unselected. - :param wait_timeout: The amount of time (in seconds) you are willing to wait for the Toggle Switch to take the + :param timeout: The amount of time (in seconds) you are willing to wait for the Toggle Switch to take the specified state. :returns: True, if the Toggle Switch has the specified selection state - False otherwise. @@ -105,8 +107,8 @@ def wait_on_selection_state(self, expected_selection_state: bool, wait_timeout: # verify if they are False too early will almost always return True because the front-end has not caught # up to the backend. This is most prevalent when dealing with a Toggle Switch immediately after it has # entered the DOM, like when Popups are opened. - self.wait_on_binding(0.25) - return WebDriverWait(driver=self.driver, timeout=wait_timeout).until( + self.wait_some_time(0.25) + return WebDriverWait(driver=self.driver, timeout=timeout).until( IAec.function_returns_true( custom_function=self._has_selection_state, function_args={'expected_selection_state': expected_selection_state})) diff --git a/Components/PerspectiveComponents/Navigation/HorizontalMenu.py b/Components/PerspectiveComponents/Navigation/HorizontalMenu.py index 68b15c7..f98e141 100644 --- a/Components/PerspectiveComponents/Navigation/HorizontalMenu.py +++ b/Components/PerspectiveComponents/Navigation/HorizontalMenu.py @@ -1,12 +1,12 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import BasicPerspectiveComponent, ComponentPiece +from Components.Common.Icon import CommonIcon from Components.PerspectiveComponents.Common.ComponentModal import ComponentModal -from Components.PerspectiveComponents.Common.Icon import CommonIcon from Helpers.IASelenium import IASelenium from Helpers.Point import Point @@ -20,43 +20,45 @@ class HorizontalMenu(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 5, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 5, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._menu_items = ComponentPiece( locator=self._MENU_ITEM_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._child_item_modal = ComponentModal(driver=driver) self._child_menu_items = ComponentPiece( locator=self._CHILD_MENU_ITEM_LOCATOR, driver=driver, parent_locator_list=self._child_item_modal.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._side_scroll_areas = ComponentPiece( locator=self._SIDE_SCROLL_AREA_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._side_scroll_clickable_area = ComponentPiece( locator=self._SIDE_SCROLL_AREA_LOCATOR, driver=driver, parent_locator_list=self._side_scroll_areas.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._menu_item_coll = {} self._child_item_coll = {} @@ -141,7 +143,7 @@ def child_items_are_visible(self) -> bool: except TimeoutException: return False - def click_visible_base_item(self, text_of_base_item: str, binding_wait_time: float = 0) -> None: + def click_visible_base_item(self, text_of_base_item: str, wait_after_click: float = 0) -> None: """ Click a base-level item by text. @@ -161,7 +163,7 @@ def click_visible_base_item(self, text_of_base_item: str, binding_wait_time: flo text_of_base_item=text_of_base_item): raise NoSuchElementException(f'No item with text of \'{text_of_base_item}\' could be found') self._get_top_level_menu_item( - text_of_base_item=text_of_base_item).click(binding_wait_time=binding_wait_time) + text_of_base_item=text_of_base_item).click(wait_after_click=wait_after_click) def click_visible_child_item(self, text_of_child_item: str) -> None: """ @@ -260,7 +262,7 @@ def _get_child_menu_item(self, text_of_child_item: str) -> ComponentPiece: locator=(By.CSS_SELECTOR, f'.horizontal-menu-menu-item[data-label="{text_of_child_item}"]'), driver=self.driver, parent_locator_list=self._child_item_modal.locator_list, - wait_timeout=2, + timeout=2, poll_freq=self.poll_freq) self._child_item_coll[text_of_child_item] = item return item @@ -275,7 +277,7 @@ def _get_top_level_menu_item(self, text_of_base_item: str) -> ComponentPiece: locator=(By.CSS_SELECTOR, f'{self._MENU_ITEM_LOCATOR[1]}[data-label="{text_of_base_item}"]'), driver=self.driver, parent_locator_list=self.locator_list, - wait_timeout=2, + timeout=2, poll_freq=self.poll_freq) self._menu_item_coll[text_of_base_item] = item return item @@ -291,7 +293,7 @@ def _get_child_item_icon(self, text_of_child_item: str) -> CommonIcon: driver=self.driver, parent_locator_list=self._get_child_menu_item( text_of_child_item=text_of_child_item).locator_list, - wait_timeout=1, + timeout=1, poll_freq=self.poll_freq) self._top_level_menu_item_icon_coll[text_of_child_item] = icon return icon @@ -307,7 +309,7 @@ def _get_top_level_item_icon(self, text_of_base_item: str) -> CommonIcon: driver=self.driver, parent_locator_list=self._get_top_level_menu_item( text_of_base_item=text_of_base_item).locator_list, - wait_timeout=1, + timeout=1, poll_freq=self.poll_freq) self._top_level_menu_item_icon_coll[text_of_base_item] = icon return icon diff --git a/Components/PerspectiveComponents/Navigation/Link.py b/Components/PerspectiveComponents/Navigation/Link.py index 62bd493..8eb30bb 100644 --- a/Components/PerspectiveComponents/Navigation/Link.py +++ b/Components/PerspectiveComponents/Navigation/Link.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple, List +from typing import Optional, Tuple, List, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -11,19 +11,21 @@ class Link(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: int = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: int = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._anchor = ComponentPiece( locator=(By.TAG_NAME, "a"), driver=driver, diff --git a/Components/PerspectiveComponents/Navigation/MenuTree.py b/Components/PerspectiveComponents/Navigation/MenuTree.py index 6f4928c..13816f6 100644 --- a/Components/PerspectiveComponents/Navigation/MenuTree.py +++ b/Components/PerspectiveComponents/Navigation/MenuTree.py @@ -6,7 +6,7 @@ from selenium.webdriver.support.wait import WebDriverWait from Components.BasicComponent import BasicPerspectiveComponent, ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon from Helpers.CSSEnumerations import CSSPropertyValue from Helpers.IAAssert import IAAssert from Helpers.IAExpectedConditions import IAExpectedConditions as IAec @@ -20,13 +20,15 @@ def __init__( driver: WebDriver, locator: Tuple[Union[By, str], str], parent_locator_list: Optional[Tuple[Union[By, str], str]] = None, - wait_timeout: float = 1): + timeout: float = 1, + raise_exception_for_overlay: bool = False): BasicPerspectiveComponent.__init__( self, locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout) + timeout=timeout, + raise_exception_for_overlay=raise_exception_for_overlay) self._item_icons = {} self._item_nav_icons = {} self._items = {} @@ -50,7 +52,7 @@ def back_action_is_present_in_submenu(self) -> bool: :return: True, if a 'Back' action is available to the user - False otherwise. """ try: - return self._get_submenu_back_action().find(wait_timeout=0) is not None + return self._get_submenu_back_action().find(timeout=0) is not None except TimeoutException: return False @@ -62,10 +64,10 @@ def click_back_action_within_submenu(self, until_at_top_level: bool = False) -> :raises TimeoutException: If no 'Back' action is currently available, or if no submenu is displayed. """ if self.submenu_is_expanded(): - self._get_submenu_back_action().find_all(wait_timeout=0)[-1].click() - self._get_submenu_back_action().wait_on_binding(0.5) # allow submenu time to be removed from DOM + self._get_submenu_back_action().find_all(timeout=0)[-1].click() + self._get_submenu_back_action().wait_some_time(0.5) # allow submenu time to be removed from DOM try: - if until_at_top_level and self._get_submenu_back_action().find(wait_timeout=0) is not None: + if until_at_top_level and self._get_submenu_back_action().find(timeout=0) is not None: self.click_back_action_within_submenu(until_at_top_level=until_at_top_level) except TimeoutException: pass @@ -85,7 +87,7 @@ def click_menu_item(self, item_text: str, is_in_submenu: bool = False) -> None: :raises TimeoutException: If no item (or submenu item) exists with the specified text. """ target = self._get_submenu_item(item_text=item_text) if is_in_submenu else self._get_item(item_text=item_text) - target.click(binding_wait_time=0.5) # wait for potential submenu animation + target.click(wait_after_click=0.5) # wait for potential submenu animation def get_css_value_of_icon_in_use_by_menu_item( self, @@ -237,7 +239,7 @@ def get_text_of_current_back_action(self) -> str: :raises TimeoutException: If no Back action is displayed, or if no submenu is displayed. """ - return self._get_submenu_back_action().find_all(wait_timeout=0)[-1].text + return self._get_submenu_back_action().find_all(timeout=0)[-1].text def header_text_is_displayed(self) -> bool: """ @@ -246,7 +248,7 @@ def header_text_is_displayed(self) -> bool: :return: True, if a header is currently displayed within a submenu - False otherwise. """ try: - return self._header.find(wait_timeout=0) is not None + return self._header.find(timeout=0) is not None except TimeoutException: return False @@ -261,7 +263,7 @@ def icon_is_displayed_for_item(self, item_text: str, is_in_submenu: bool) -> boo """ try: return self._get_item_icon( - item_text=item_text, is_in_submenu=is_in_submenu).find(wait_timeout=0).is_displayed() + item_text=item_text, is_in_submenu=is_in_submenu).find(timeout=0).is_displayed() except TimeoutException: return False @@ -277,7 +279,7 @@ def item_is_displayed(self, item_text: str, is_in_submenu: bool = False) -> bool """ try: item = self._get_submenu_item(item_text=item_text) if is_in_submenu else self._get_item(item_text=item_text) - return item.find(wait_timeout=0).is_displayed() + return item.find(timeout=0).is_displayed() except TimeoutException: return False @@ -293,7 +295,7 @@ def item_is_present(self, item_text: str, is_in_submenu: bool = False) -> bool: """ try: return self._get_item_or_submenu_item( - item_text=item_text, is_in_submenu=is_in_submenu).find(wait_timeout=0) is not None + item_text=item_text, is_in_submenu=is_in_submenu).find(timeout=0) is not None except TimeoutException: return False @@ -308,13 +310,13 @@ def nav_icon_is_displayed_for_item(self, item_text: str, is_in_submenu: bool) -> """ try: return self._get_item_nav_icon( - item_text=item_text, is_in_submenu=is_in_submenu).find(wait_timeout=0).is_displayed() + item_text=item_text, is_in_submenu=is_in_submenu).find(timeout=0).is_displayed() except TimeoutException: return False - def scroll_to_item(self, item_text: str, align_to_top: bool, is_in_submenu: bool = False) -> None: + def scroll_to_item(self, item_text: str, is_in_submenu: bool = False) -> None: self._get_item_or_submenu_item( - item_text=item_text, is_in_submenu=is_in_submenu).scroll_to_element(align_to_top=align_to_top) + item_text=item_text, is_in_submenu=is_in_submenu).scroll_to_element() def submenu_is_expanded(self) -> bool: """ @@ -323,7 +325,7 @@ def submenu_is_expanded(self) -> bool: :return: True, if a submenu is currently expanded - False otherwise. """ try: - return self._get_submenu_back_action().find(wait_timeout=0) is not None + return self._get_submenu_back_action().find(timeout=0) is not None except TimeoutException: return False @@ -332,25 +334,25 @@ def wait_on_item_presence( item_text: str, should_be_present: bool, is_in_submenu: bool = False, - wait_timeout: Optional[float] = None): + timeout: Optional[float] = None): """ Wait some period of time until an item either becomes present in the Menu Tree or is removed from the Menu Tree. :param item_text: The text of the item to query for until it has the expected presence. :param should_be_present: Specifies whether you are waiting for the item to be present or to not be present. :param is_in_submenu: Clarifies whether the item should be expected as a top-level item or within a submenu. - :param wait_timeout: How long (in seconds) to remain polling for the item. + :param timeout: How long (in seconds) to remain polling for the item. :return: True, if the item presence state matches the state specified in `should_be_present` - False otherwise. """ item = self._get_item_or_submenu_item(item_text=item_text, is_in_submenu=is_in_submenu) - wait = self.wait if wait_timeout is None else WebDriverWait(driver=self.driver, timeout=wait_timeout) + wait = self.wait if timeout is None else WebDriverWait(driver=self.driver, timeout=timeout) def check_for_presence(): nonlocal item nonlocal should_be_present try: - return (item.find(wait_timeout=0) is not None) == should_be_present + return (item.find(timeout=0) is not None) == should_be_present except TimeoutException: return should_be_present is False # item wasn't found, which might be expected @@ -405,7 +407,7 @@ def _get_submenu_back_action(self, text: Optional[str] = None) -> ComponentPiece locator=(By.CSS_SELECTOR, locator_string), driver=self.driver, parent_locator_list=self._submenu_group.locator_list, - wait_timeout=1) + timeout=1) def _get_submenu_item(self, item_text: Optional[str] = None) -> ComponentPiece: item = self._items.get(item_text) @@ -423,6 +425,6 @@ def _get_submenu_item(self, item_text: Optional[str] = None) -> ComponentPiece: @staticmethod def _item_is_present(item: ComponentPiece) -> bool: try: - return item.find(wait_timeout=0) is not None + return item.find(timeout=0) is not None except TimeoutException: return False diff --git a/Components/PerspectiveComponents/Reporting/ReportViewer.py b/Components/PerspectiveComponents/Reporting/ReportViewer.py index af769a6..a39276e 100644 --- a/Components/PerspectiveComponents/Reporting/ReportViewer.py +++ b/Components/PerspectiveComponents/Reporting/ReportViewer.py @@ -27,90 +27,92 @@ class ReportViewer(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]] = None, - wait_timeout: float = 2, + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]] = None, + timeout: float = 2, description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, - wait_timeout=wait_timeout, + timeout=timeout, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._contents = ComponentPiece( locator=self._REPORT_CONTENTS_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._zoom_level_select = ComponentPiece( locator=self._ZOOM_LEVEL_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._first_page_action = ComponentPiece( locator=self._FIRST_PAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._previous_page_action = ComponentPiece( locator=self._PREVIOUS_PAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._next_page_action = ComponentPiece( locator=self._NEXT_PAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._last_page_action = ComponentPiece( locator=self._LAST_PAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._current_page_action = ComponentPiece( locator=self._CURRENT_PAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._total_pages_label = ComponentPiece( locator=self._TOTAL_PAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._download_link = ComponentPiece( locator=self._DOWNLOAD_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._open_in_new_tab_link = ComponentPiece( locator=self._TAB_OPEN_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._message = ComponentPiece( locator=self._COMPONENT_MESSAGE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) self._loading_spinner = ComponentPiece( locator=self._SPINNER_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1, + timeout=1, poll_freq=poll_freq) def click_first_page_button(self) -> None: @@ -171,7 +173,7 @@ def input_page_number(self, page_number: Union[int, str]) -> None: :param page_number: The page you would like to type into the Report Viewer in order to view the supplied page. """ - self._current_page_action.click(binding_wait_time=0.5) + self._current_page_action.click(wait_after_click=0.5) self._current_page_action.find().clear() self._current_page_action.find().send_keys(str(page_number)) @@ -224,35 +226,35 @@ def get_component_message(self) -> str: :raises TimeoutException: If no Component Error message is found. """ self.spinner_appeared() - self.spinner_was_removed(wait_timeout=5) - return self._message.find(wait_timeout=3).text + self.spinner_was_removed(timeout=5) + return self._message.find(timeout=3).text - def spinner_appeared(self, wait_timeout: float = 2) -> bool: + def spinner_appeared(self, timeout: float = 2) -> bool: """ Wait until the loading spinner has appeared. - :param wait_timeout: How long to wait for the spinner to appear. + :param timeout: How long to wait for the spinner to appear. :returns: True, once the spinner has appeared - False, if the spinner never appeared. """ try: - return WebDriverWait(driver=self.driver, timeout=wait_timeout).until( + return WebDriverWait(driver=self.driver, timeout=timeout).until( IAec.function_returns_true( custom_function=self._spinner_appeared, function_args={})) except TimeoutException: return False - def spinner_was_removed(self, wait_timeout: float = 2) -> bool: + def spinner_was_removed(self, timeout: float = 2) -> bool: """ Wait until the loading spinner has been removed. - :param wait_timeout: How long to wait for the spinner to be removed. + :param timeout: How long to wait for the spinner to be removed. :returns: True, once the spinner has been removed - False, if the spinner remains in place. """ try: - return WebDriverWait(driver=self.driver, timeout=wait_timeout).until( + return WebDriverWait(driver=self.driver, timeout=timeout).until( IAec.function_returns_true( custom_function=self._spinner_disappeared, function_args={})) @@ -277,7 +279,7 @@ def _spinner_disappeared(self) -> bool: :returns: True, if the spinner is no longer present - False if the spinner remains in place. """ try: - return self._loading_spinner.find(wait_timeout=0) is None + return self._loading_spinner.find(timeout=0) is None except TimeoutException: return True @@ -288,21 +290,21 @@ def _spinner_appeared(self) -> bool: :returns: True, if the spinner is displayed - False otherwise. """ try: - return self._loading_spinner.find(wait_timeout=0).is_displayed() + return self._loading_spinner.find(timeout=0).is_displayed() except TimeoutException: return False - def _report_content_is_not_empty(self, wait_timeout: float = 3) -> bool: + def _report_content_is_not_empty(self, timeout: float = 3) -> bool: """ Waits until the Report Viewer is displaying any content other than the spinner. - :param wait_timeout: The amount of time to wait for the Report Viewer to display any report content. + :param timeout: The amount of time to wait for the Report Viewer to display any report content. :returns: True if the Report Viewer is displaying any content before the specified wait period has lapsed - False otherwise. """ try: - report_contents = WebDriverWait(driver=self.driver, timeout=wait_timeout).until( + report_contents = WebDriverWait(driver=self.driver, timeout=timeout).until( ec.presence_of_element_located( self._REPORT_CONTENTS_LOCATOR)).get_attribute('innerHTML') return report_contents is not None and len(report_contents) > 0 diff --git a/Components/PerspectiveComponents/Symbols/Symbol.py b/Components/PerspectiveComponents/Symbols/Symbol.py index c4ded58..11e1c27 100644 --- a/Components/PerspectiveComponents/Symbols/Symbol.py +++ b/Components/PerspectiveComponents/Symbols/Symbol.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -86,17 +86,19 @@ class CommonSymbol(BasicPerspectiveComponent): def __init__( self, - locator: Tuple[By, str], + locator: Tuple[Union[By, str], str], driver: WebDriver, - parent_locator_list: Optional[List[Tuple[By, str]]], + parent_locator_list: Optional[List[Tuple[Union[By, str], str]]], description: Optional[str] = None, - poll_freq: float = 0.5): + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): super().__init__( locator=locator, driver=driver, parent_locator_list=parent_locator_list, description=description, - poll_freq=poll_freq) + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._main_svg = ComponentPiece( locator=self.MAIN_SVG_LOCATOR, driver=driver, parent_locator_list=self.locator_list, poll_freq=poll_freq) self._label_text = ComponentPiece( @@ -247,8 +249,19 @@ def feet_have_display_state(self, feet_display_state: Feet) -> bool: class Motor(_SymbolWithFeet): """A Perspective Motor Symbol.""" - def __init__(self, locator, driver: WebDriver, parent_locator_list: Optional[List] = None, poll_freq: float = 0.5): - super().__init__(locator=locator, driver=driver, parent_locator_list=parent_locator_list, poll_freq=poll_freq) + def __init__( + self, + locator, + driver: WebDriver, + parent_locator_list: Optional[List] = None, + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): + super().__init__( + locator=locator, + driver=driver, + parent_locator_list=parent_locator_list, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) class Pump(_SymbolWithFeet): @@ -261,8 +274,19 @@ class Variant(Enum): CENTRIFUGAL = "centrifugal" VACUUM = "vacuum" - def __init__(self, locator, driver: WebDriver, parent_locator_list: Optional[List] = None, poll_freq: float = 0.5): - super().__init__(locator=locator, driver=driver, parent_locator_list=parent_locator_list, poll_freq=poll_freq) + def __init__( + self, + locator, + driver: WebDriver, + parent_locator_list: Optional[List] = None, + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): + super().__init__( + locator=locator, + driver=driver, + parent_locator_list=parent_locator_list, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) def pump_is_variant(self, variant: Variant) -> bool: """ @@ -277,8 +301,19 @@ class Sensor(CommonSymbol): """A Perspective Sensor Symbol.""" _SENSOR_BODY_LOCATOR = (By.CSS_SELECTOR, "g.sensor_sensor_body") - def __init__(self, locator, driver: WebDriver, parent_locator_list: Optional[List] = None, poll_freq: float = 0.5): - super().__init__(locator=locator, driver=driver, parent_locator_list=parent_locator_list, poll_freq=poll_freq) + def __init__( + self, + locator, + driver: WebDriver, + parent_locator_list: Optional[List] = None, + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): + super().__init__( + locator=locator, + driver=driver, + parent_locator_list=parent_locator_list, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._body = ComponentPiece( locator=self._SENSOR_BODY_LOCATOR, driver=driver, @@ -377,8 +412,19 @@ class State(Enum): FAILED_TO_OPEN = "failedToOpen" PARTIALLY_CLOSED = "partiallyClosed" - def __init__(self, locator, driver: WebDriver, parent_locator_list: Optional[List] = None, poll_freq: float = 0.5): - super().__init__(locator=locator, driver=driver, parent_locator_list=parent_locator_list, poll_freq=poll_freq) + def __init__( + self, + locator, + driver: WebDriver, + parent_locator_list: Optional[List] = None, + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): + super().__init__( + locator=locator, + driver=driver, + parent_locator_list=parent_locator_list, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._main_valve = ComponentPiece( locator=self._MAIN_VALVE_LOCATOR, driver=driver, @@ -438,8 +484,19 @@ class Orientation(Enum): VERTICAL = "vertical" HORIZONTAL = "horizontal" - def __init__(self, locator, driver: WebDriver, parent_locator_list: Optional[List] = None, poll_freq: float = 0.5): - super().__init__(locator=locator, driver=driver, parent_locator_list=parent_locator_list, poll_freq=poll_freq) + def __init__( + self, + locator, + driver: WebDriver, + parent_locator_list: Optional[List] = None, + poll_freq: float = 0.5, + raise_exception_for_overlay: bool = False): + super().__init__( + locator=locator, + driver=driver, + parent_locator_list=parent_locator_list, + poll_freq=poll_freq, + raise_exception_for_overlay=raise_exception_for_overlay) self._agitator_and_rotor = ComponentPiece( locator=self._AGITATOR_AND_ROTOR_LOCATOR, driver=driver, @@ -453,6 +510,6 @@ def agitator_and_rotor_are_displayed(self) -> bool: returns: True, if both are displayed - False otherwise. """ try: - return len(self._agitator_and_rotor.find_all(wait_timeout=1)) == 2 + return len(self._agitator_and_rotor.find_all(timeout=1)) == 2 except TimeoutException: return False diff --git a/Components/PerspectiveComponents/Tooltip.py b/Components/PerspectiveComponents/Tooltip.py index b201e94..7457887 100644 --- a/Components/PerspectiveComponents/Tooltip.py +++ b/Components/PerspectiveComponents/Tooltip.py @@ -25,21 +25,21 @@ class Tooltip(ComponentPiece): def __init__( self, driver: WebDriver, - wait_timeout: Union[int, float] = 2, + timeout: Union[int, float] = 2, description: Optional[str] = None, poll_freq: float = 0.5): super().__init__( locator=self._COMPONENT_TOOLTIP_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=wait_timeout, + timeout=timeout, description=description, poll_freq=poll_freq) self._tail = ComponentPiece( locator=self._TAIL_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=wait_timeout, + timeout=timeout, poll_freq=poll_freq) def any_component_tooltip_is_displayed(self) -> bool: @@ -65,20 +65,20 @@ def component_tooltip_has_tail(self) -> bool: return False def get_count_of_tooltips( - self, expected_count: int = 1, wait_timeout: Union[int, float] = 5, poll_frequency: float = 0.5) -> int: + self, expected_count: int = 1, timeout: Union[int, float] = 5, poll_frequency: float = 0.5) -> int: """ Obtain a count of all displayed tooltips. :param expected_count: If supplied, we will attempt to wait until this many tooltips are displayed before returning a value. - :param wait_timeout: The amount of time (in seconds) to wait for any tooltips to appear. Use this when tooltips + :param timeout: The amount of time (in seconds) to wait for any tooltips to appear. Use this when tooltips contain any configured delay. :param poll_frequency: How often to poll the DOM as we wait for an expected count of tooltips. :returns: A count of displayed tooltips. """ try: - WebDriverWait(driver=self.driver, timeout=wait_timeout, poll_frequency=poll_frequency).until( + WebDriverWait(driver=self.driver, timeout=timeout, poll_frequency=poll_frequency).until( IAec.function_returns_true( custom_function=self._expected_number_of_tooltips_displayed, function_args={'expected_count': expected_count})) @@ -86,7 +86,7 @@ def get_count_of_tooltips( except TimeoutException: pass try: - return len(self.find_all(wait_timeout=1)) + return len(self.find_all(timeout=1)) except TimeoutException: return 0 @@ -159,6 +159,6 @@ def _expected_number_of_tooltips_displayed(self, expected_count: int) -> bool: otherwise. """ try: - return len(self.find_all(wait_timeout=0)) == expected_count + return len(self.find_all(timeout=0)) == expected_count except TimeoutException: return False diff --git a/Helpers/CSSEnumerations.py b/Helpers/CSSEnumerations.py index b934cb5..87ccd5e 100644 --- a/Helpers/CSSEnumerations.py +++ b/Helpers/CSSEnumerations.py @@ -35,6 +35,18 @@ class AlignItems: STRETCH = CSSPropertyValue("stretch") BACKGROUND_COLOR = CSSPropertyValue("background-color") + + class Behavior: + AUTO = CSSPropertyValue("auto") + INSTANT = CSSPropertyValue("instant") + SMOOTH = CSSPropertyValue("smooth") + + class Block: + CENTER = CSSPropertyValue("center") + END = CSSPropertyValue("end") + NEAREST = CSSPropertyValue("nearest") + START = CSSPropertyValue("start") + BORDER_TOP_COLOR = CSSPropertyValue("border-top-color") BORDER_TOP_LEFT_RADIUS = CSSPropertyValue("border-top-left-radius") BORDER_TOP_RIGHT_RADIUS = CSSPropertyValue("border-top-right-radius") @@ -69,7 +81,21 @@ class FlexFlow: ROW_REVERSE = CSSPropertyValue("row-reverse") FONT_SIZE = CSSPropertyValue("font-size") + + class GridArea: + GRID_ROW_END = CSSPropertyValue("grid-row-end") + GRID_ROW_START = CSSPropertyValue("grid-row-start") + GRID_COLUMN_END = CSSPropertyValue("grid-column-end") + GRID_COLUMN_START = CSSPropertyValue("grid-column-start") + HEIGHT = CSSPropertyValue("height") + + class Inline: + CENTER = CSSPropertyValue("center") + END = CSSPropertyValue("end") + NEAREST = CSSPropertyValue("nearest") + START = CSSPropertyValue("start") + JUSTIFY_CONTENT = CSSPropertyValue('justify-content') MARGIN_BOTTOM = CSSPropertyValue("margin-bottom") MARGIN_LEFT = CSSPropertyValue("margin-left") diff --git a/Helpers/Formatting.py b/Helpers/Formatting.py index d98a7ec..97b5af6 100644 --- a/Helpers/Formatting.py +++ b/Helpers/Formatting.py @@ -9,16 +9,15 @@ class FilePathFormatting: """ @staticmethod - def system_safe_file_path(exec_dir: str, file_path: str) -> str: + def system_safe_file_path(file_path: str) -> str: """ Attempts to take a standard string file path and make it work across any operating system. - :param exec_dir: The execution directory to be used in constructing the file path. :param file_path: The path of the file. :returns: A platform-agnostic path to a file. """ - return str(Path(os.path.join(exec_dir, file_path)).resolve()) + return str(Path(file_path).resolve()) @staticmethod def create_incremented_file_path(file_path: str) -> str: diff --git a/Helpers/IAAssert.py b/Helpers/IAAssert.py index 7cad1c0..2b2657f 100644 --- a/Helpers/IAAssert.py +++ b/Helpers/IAAssert.py @@ -67,7 +67,7 @@ def is_equal_to( if as_type == Color: # Color only accepts a tuple constructor, which throws a wrench into our previous casting. assert IAAssert._color_convert(color_as_string=actual_value) ==\ - IAAssert._color_convert(color_as_string=expected_value),\ + IAAssert._color_convert(color_as_string=expected_value), \ IAAssert._generate_failure_msg( msg=f'Assert {IAAssert._color_convert(color_as_string=actual_value)} == ' f'{IAAssert._color_convert(color_as_string=expected_value)} -> ' @@ -252,18 +252,18 @@ def is_not_equal_to( if as_type == Color: # special handling for color comparisons assert IAAssert._color_convert(color_as_string=actual_value) \ - != IAAssert._color_convert(color_as_string=expected_value),\ + != IAAssert._color_convert(color_as_string=expected_value), \ IAAssert._generate_failure_msg( msg=f'Assert {IAAssert._color_convert(color_as_string=actual_value)} != ' f'{IAAssert._color_convert(color_as_string=expected_value)}.', failure_msg=failure_msg) else: - assert as_type(actual_value) != as_type(expected_value),\ + assert as_type(actual_value) != as_type(expected_value), \ IAAssert._generate_failure_msg(msg=f'Assert {as_type(actual_value)} != ' f'{as_type(expected_value)}', failure_msg=failure_msg) else: - assert actual_value != expected_value,\ + assert actual_value != expected_value, \ IAAssert._generate_failure_msg(msg=f'Assert {actual_value} != {expected_value}', failure_msg=failure_msg) @@ -349,7 +349,7 @@ def _color_convert(cls, color_as_string: str) -> str: return Color.from_string(color_as_string).hex @classmethod - def _generate_failure_msg(cls, msg: str, failure_msg: str) -> str: + def _generate_failure_msg(cls, msg: str, failure_msg: Optional[str] = None) -> str: """Convenience function to concatenate and pretty-print failure messages to a defined assertion message.""" return msg if not failure_msg else msg + '\nMessage: ' + failure_msg diff --git a/Helpers/IAExpectedConditions/IAExpectedConditions.py b/Helpers/IAExpectedConditions/IAExpectedConditions.py index 641aedd..0a85f47 100644 --- a/Helpers/IAExpectedConditions/IAExpectedConditions.py +++ b/Helpers/IAExpectedConditions/IAExpectedConditions.py @@ -4,7 +4,7 @@ Leaving class names in lowercase to conform with existing expected_conditions classes. """ from enum import Enum -from typing import Tuple, Callable +from typing import Tuple, Callable, Dict, Union from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, WebDriverException from selenium.webdriver.common.by import By @@ -36,7 +36,7 @@ class child_element_has_partial_css_class(object): An expectation that within a WebElement there is a specific child WebElement which has a provided string as part of its CSS class. """ - def __init__(self, parent_web_element: WebElement, locator: Tuple[By, str], text: str): + def __init__(self, parent_web_element: WebElement, locator: Tuple[Union[By, str], str], text: str): """ :param parent_web_element: The WebElement you expect to contain a child WebElement which contains your supplied text as part of its HTML `class` attribute. @@ -78,7 +78,7 @@ class element_in_list_contains_text(object): An expectation that within a collection of elements, one of the elements contains the expected text. Waits if the list of elements is empty, or if none of the elements currently contain the text. """ - def __init__(self, locator: Tuple[By, str], text: str): + def __init__(self, locator: Tuple[Union[By, str], str], text: str): """ :param locator: The tuple used to describe the collection of elements :param text: The expected text content of any one of the WebElements described by the supplied locator. @@ -105,7 +105,7 @@ class element_identified_by_locator_equals_text(object): An expectation for checking if the given text is an exact match to the text of the element identified by the supplied locator. """ - def __init__(self, locator: Tuple[By, str], text: str): + def __init__(self, locator: Tuple[Union[By, str], str], text: str): """ :param locator: The tuple used to describe the singular WebElement we expect to have the supplied text. If more than one WebElement would match the supplied locator, only the first will be checked. @@ -130,7 +130,7 @@ class element_identified_by_locator_contains_text(object): An expectation for checking that the given text is contained in the text of the element identified by the supplied locator. """ - def __init__(self, locator: Tuple[By, str], text: str): + def __init__(self, locator: Tuple[Union[By, str], str], text: str): """ :param locator: The tuple used to describe the singular WebElement we expect to contain the supplied text. If more than one WebElement would match the supplied locator, only the first will be checked. @@ -157,7 +157,7 @@ class element_identified_by_locator_does_not_equal_text(object): Sometimes we want to wait for text to change away from a value and evaluate instead of evaluating while waiting for some expected value """ - def __init__(self, locator: Tuple[By, str], text: str): + def __init__(self, locator: Tuple[Union[By, str], str], text: str): """ :param locator: The tuple which describes the singular WebElement you expect to not have the supplied text. :param text: The text you expect to not be present in the WebElement described by the supplied locator. @@ -180,7 +180,7 @@ class element_identified_by_locator_has_value(object): """ Checks to see if the WebElement described by the supplied locator has the supplied expected value. """ - def __init__(self, locator: Tuple[By, str], expected_value: str): + def __init__(self, locator: Tuple[Union[By, str], str], expected_value: str): """ :param locator: The tuple which describes the singular WebElement from which we will check the HTMl `value` attribute for the supplied expected value. @@ -228,7 +228,7 @@ class child_element_exists(object): An expectation for checking that a child component exists and returns the child element. This allows us to wait for an element to exist within another element """ - def __init__(self, parent_web_element: WebElement, child_locator: Tuple[By, str]): + def __init__(self, parent_web_element: WebElement, child_locator: Tuple[Union[By, str], str]): """ :param parent_web_element: The WebElement you expect to contain a child described by the supplied child locator. :param child_locator: The locator which describes a WebElement you expect to eventually be a child of the @@ -274,7 +274,7 @@ def myFunc(arg1, arg2): "arg1": someValue: "arg2": someOtherValue}) """ - def __init__(self, custom_function: Callable, function_args: dict): + def __init__(self, custom_function: Callable, function_args: Dict = {}): """ :param custom_function: An actual function (non-invoked) to execute with some supplied arguments. :param function_args: A dictionary containing the arguments of the supplied function. @@ -305,7 +305,7 @@ def myFunc(arg1, arg2): "arg1": someValue: "arg2": someOtherValue}) """ - def __init__(self, custom_function: Callable, function_args: dict): + def __init__(self, custom_function: Callable, function_args: Dict = {}): """ :param custom_function: An actual function (non-invoked) to execute with some supplied arguments. :param function_args: A dictionary containing the arguments of the supplied function. @@ -321,7 +321,7 @@ def __call__(self, driver): class element_is_fully_in_viewport(object): - def __init__(self, driver: WebDriver, locator: Tuple[By, str]): + def __init__(self, driver: WebDriver, locator: Tuple[Union[By, str], str]): """ :param driver: The WebDriver of the Selenium session. :param locator: The locator which describes the singular element to verify is within the boundaries of the diff --git a/Helpers/IASelenium.py b/Helpers/IASelenium.py index de7be17..d71d808 100644 --- a/Helpers/IASelenium.py +++ b/Helpers/IASelenium.py @@ -10,6 +10,7 @@ from selenium.webdriver.remote.webdriver import WebDriver, WebElement from selenium.webdriver.support.wait import WebDriverWait +from Helpers import CSSEnumerations from Helpers.Formatting import FilePathFormatting from Helpers.IAExpectedConditions import IAExpectedConditions as IAec @@ -73,7 +74,8 @@ def close_extra_tabs(self) -> None: Close any browser tabs which are not the 0th tab, and then switch to the 0th tab. """ while self.get_tab_count() > 1: - self.switch_to_tab_by_index(zero_based_index=1) + max_tab = self.get_tab_count() - 1 + self.switch_to_tab_by_index(zero_based_index=max_tab) self.close_current_tab() self.switch_to_tab_by_index(zero_based_index=0) @@ -220,19 +222,31 @@ def right_click(self, web_element: WebElement) -> None: finally: self.enable_context_menu() - def scroll_to_element(self, web_element: WebElement, align_to_top: bool = True) -> WebElement: + def scroll_to_element( + self, + web_element: WebElement, + behavior: CSSEnumerations.CSS.Behavior = CSSEnumerations.CSS.Behavior.AUTO, + block: CSSEnumerations.CSS.Block = CSSEnumerations.CSS.Block.START, + inline: CSSEnumerations.CSS.Inline = CSSEnumerations.CSS.Inline.NEAREST) -> WebElement: """ Scroll to a WebElement. + :param inline: The CSS 'inline' setting to apply to the WebElement. + :param block: The CSS 'block' setting to apply to the WebElement. + :param behavior: The CSS 'behavior' setting to apply to the WebElement. :param web_element: The WebElement to scroll to. - :param align_to_top: If True, page will attempt to scroll such that the WebElement will have its top edge - touching the top of the viewport. If False, the page will attempt to scroll such that the WebElement will - have its bottom edge touching the bottom of the viewport. + + For more information about the available settings, see: + https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView :returns: The supplied WebElement, to allow for chaining. """ - align_string = str(align_to_top).lower() - self.driver.execute_script('arguments[0].scrollIntoView(' + align_string + ');', web_element) + self.driver.execute_script( + 'arguments[0].scrollIntoView({behavior: "' + + str(behavior) + '", block: "' + + str(block) + '", inline: "' + + str(inline) + '"});', + web_element) return web_element def scroll_to_element_with_settings( @@ -342,7 +356,7 @@ def take_screenshot(self, directory: str = 'Screenshots', screenshot_name: str = screenshot_folder = f'{os.getcwd()}/{directory}' if not os.path.exists(screenshot_folder): os.mkdir(screenshot_folder) - safe_path = FilePathFormatting.system_safe_file_path(screenshot_folder, screenshot_title) + safe_path = FilePathFormatting.system_safe_file_path(f'{screenshot_folder}/{screenshot_title}') self.driver.save_screenshot(safe_path) self.logger.info(msg=f'Screenshot taken: {safe_path}') return safe_path @@ -365,7 +379,7 @@ def take_screenshot_of_element(self, web_element: WebElement, directory: str = ' screenshot_folder = f'{os.getcwd()}/{directory}' if not os.path.exists(screenshot_folder): os.mkdir(screenshot_folder) - safe_path = FilePathFormatting.system_safe_file_path(screenshot_folder, screenshot_title) + safe_path = FilePathFormatting.system_safe_file_path(f'{screenshot_folder}/{screenshot_title}') print(f'Screenshot path: \'{safe_path}\'') self.driver.save_screenshot(safe_path) return safe_path diff --git a/Helpers/Ignition/Alarm.py b/Helpers/Ignition/Alarm.py index 6893541..ca8104c 100644 --- a/Helpers/Ignition/Alarm.py +++ b/Helpers/Ignition/Alarm.py @@ -1,11 +1,11 @@ import copy import json import re -from enum import Enum +from enum import Enum, StrEnum from typing import Dict, List, Optional -class AlarmState(Enum): +class AlarmState(StrEnum): """ An enumeration of the possible states an alarm could have. Each state here should have a matching event type in Ignition's Alarm Journal. @@ -19,17 +19,17 @@ class AlarmState(Enum): DISABLED = 'Disabled' -class AlarmStatusTableAlarmState(Enum): +class AlarmStatusTableAlarmState(StrEnum): """ The Alarm Status Table has its own unique set of states, built out of intersections of basic Alarm States. """ - ACTIVE_ACKNOWLEDGED = ", ".join([AlarmState.ACTIVE.value, AlarmState.ACKNOWLEDGED.value]) - ACTIVE_UNACKNOWLEDGED = ", ".join([AlarmState.ACTIVE.value, AlarmState.UNACKNOWLEDGED.value]) - CLEARED_ACKNOWLEDGED = ", ".join([AlarmState.CLEARED.value, AlarmState.ACKNOWLEDGED.value]) - CLEARED_UNACKNOWLEDGED = ", ".join([AlarmState.CLEARED.value, AlarmState.UNACKNOWLEDGED.value]) + ACTIVE_ACKNOWLEDGED = ", ".join([AlarmState.ACTIVE, AlarmState.ACKNOWLEDGED]) + ACTIVE_UNACKNOWLEDGED = ", ".join([AlarmState.ACTIVE, AlarmState.UNACKNOWLEDGED]) + CLEARED_ACKNOWLEDGED = ", ".join([AlarmState.CLEARED, AlarmState.ACKNOWLEDGED]) + CLEARED_UNACKNOWLEDGED = ", ".join([AlarmState.CLEARED, AlarmState.UNACKNOWLEDGED]) -class AlarmPriority(Enum): +class AlarmPriority(StrEnum): """ An enumeration of the possible priorities an alarm could have. """ @@ -119,8 +119,8 @@ def duplicate(self): @staticmethod def _to_camel_case(name) -> str: - init, *temp = name.split('_') - return ''.join([init.lower(), *map(str.title, temp)]) + pieces = name.split('_') + return pieces[0].lower() + ''.join([piece.title() for piece in pieces[1:]]) class AlarmHelper: diff --git a/Helpers/Ignition/Tag.py b/Helpers/Ignition/Tag.py index d24cf89..3a9234f 100644 --- a/Helpers/Ignition/Tag.py +++ b/Helpers/Ignition/Tag.py @@ -2,7 +2,7 @@ import json import re from enum import Enum -from typing import List, Dict, Optional, Union +from typing import List, Dict, Optional from Helpers.Ignition.Alarm import AlarmHelper, AlarmDefinition @@ -53,7 +53,7 @@ def __init__(self, name: str, path: str, provider: str = '[default]'): self._path = path self._provider = f'{provider}' self._base_path = f'{self._provider}{path}' - self._full_path = f'{self._base_path}{name}' if self._path == '' else f'{self._base_path}/{name}' + self._full_path = f'{self._base_path}{"/" if self._base_path != self._provider else ""}{name}' self.tag_type = None self.access_rights = None self.alarm_eval_enabled = None @@ -215,15 +215,21 @@ def __str__(self): class Tag(_BaseTag): """ - The Tag class allows for easy storage and reference of Tag attributes. This class does not perform as a Tag, only - storing information for later reference. + The Tag class allows for easy storage and reference of Tag attributes. This class does not perform as a Tag - it + only stores information for later reference. """ def __init__(self, name: str, path: str, provider: str = '[default]'): + """ + :param name: The name of the Tag. + :param path: The slash-delimited folder structure between the provider and the Tag. + :param provider: The name of the provider of this Tag + """ super().__init__( name=name, path=path, provider=provider) self.tag_type = 'AtomicTag' + self.default_value = None self.value_source = ValueSource.MEMORY def get_alarm(self, alarm_name: str) -> AlarmDefinition: @@ -390,6 +396,12 @@ def get_type_id_path(self): class Folder(_ComplexTag): def __init__(self, name: str, path: str, provider: str = '[default]'): + """ + :param name: The name of the Folder. + :param path: The slash-delimited path between the provider and the Folder. Supply an empty string if the Folder + is a top-level folder. + :param provider: The provider the Folder exists in. + """ super().__init__( name=name, path=path, diff --git a/Helpers/PerspectivePages/DockedViewHelper.py b/Helpers/PerspectivePages/DockedViewHelper.py index cb84159..ebed98c 100644 --- a/Helpers/PerspectivePages/DockedViewHelper.py +++ b/Helpers/PerspectivePages/DockedViewHelper.py @@ -56,13 +56,13 @@ class DockedViewHelper(PagePiece): def __init__(self, driver: WebDriver): super().__init__(driver=driver) self._modal = ComponentPiece( - locator=self._DOCK_MODAL_LOCATOR, driver=driver, wait_timeout=1) + locator=self._DOCK_MODAL_LOCATOR, driver=driver, timeout=1) self._docked_views = {} def click_modal_overlay(self) -> None: """Click a Docked View modal overlay, if present. If no overlay is present, no action is taken.""" if self.modal_overlay_is_present(): - self._modal.click(wait_timeout=1, binding_wait_time=1) + self._modal.click(timeout=1, wait_after_click=1) def docked_view_with_root_id_is_expanded( self, docked_view_root_container_id: str, side: Optional[Side] = None) -> bool: diff --git a/Helpers/PerspectivePages/LoginHelper.py b/Helpers/PerspectivePages/LoginHelper.py index e405939..d2b0276 100644 --- a/Helpers/PerspectivePages/LoginHelper.py +++ b/Helpers/PerspectivePages/LoginHelper.py @@ -29,33 +29,33 @@ def __init__(self, driver: WebDriver): locator=self._USERNAME_FIELD_CSS_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=0, + timeout=0, description="The username input of the Perspective Login page.") self._exit_login_button = BasicComponent( locator=self._EXIT_LOGIN_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=0, + timeout=0, description="The button which allows a user to exit login. Available only within projects which do not " "require authentication.") self._submit_button = BasicComponent( locator=self._SUBMIT_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=0, + timeout=0, description="A generic 'submit' button, available while inputting both the username and the password on " "the Perspective Login page.") self._password_input = BasicComponent( locator=self._PASSWORD_FIELD_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=0, + timeout=0, description="The password input of the Perspective Login page.") self._continue_descriptor = BasicComponent( locator=self._CONTINUE_DESCRIPTOR_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=0, + timeout=0, description="The label on the interstitial 'continue' page which informs a user they must continue to the " "Login page.") @@ -71,7 +71,7 @@ def continue_to_log_in(self) -> None: page. """ try: - self._submit_button.click(wait_timeout=3) + self._submit_button.click(timeout=3) except TimeoutException: pass self.wait_for_login_page() @@ -89,8 +89,8 @@ def log_in_through_login_page(self, username: str, password: str = "password") - self.continue_to_log_in() try: self._username_input.find().send_keys(username) - self._submit_button.click(binding_wait_time=1) - self._password_input.find(wait_timeout=1).send_keys(password) + self._submit_button.click(wait_after_click=1) + self._password_input.find(timeout=1).send_keys(password) self._submit_button.click() except TimeoutException: pass # user credentials may have been cached. @@ -135,7 +135,7 @@ def log_in_through_active_session(self, username: str, password: str = "password "the App Bar.") if self._app_bar.sign_out_button_is_displayed(): # potential for project to not require auth self._app_bar.click_sign_out_button() - self._app_bar.wait_for_revealer_or_app_bar_to_be_displayed(wait_timeout=3) + self._app_bar.wait_for_revealer_or_app_bar_to_be_displayed(timeout=3) if not (self.on_interstitial_continue_to_login_page() or self.on_login_page()): # Still in project, because auth not required self._app_bar.click_sign_in_button() @@ -149,7 +149,7 @@ def on_interstitial_continue_to_login_page(self) -> bool: :returns: True, if the user can see 'Continue to Login' - False otherwise. """ try: - self._submit_button.find(wait_timeout=0) # to trigger the TOE if we are not on this page. + self._submit_button.find(timeout=0) # to trigger the TOE if we are not on this page. return self._continue_descriptor.get_text() == 'You must log in to continue.' except TimeoutException: return False @@ -174,6 +174,6 @@ def wait_for_login_page(self, time_to_wait: int = 10) -> None: displayed before allowing code to continue. """ try: - self._username_input.find(wait_timeout=time_to_wait) + self._username_input.find(timeout=time_to_wait) except TimeoutException: pass diff --git a/Helpers/PerspectivePages/NotificationHelper.py b/Helpers/PerspectivePages/NotificationHelper.py index 73f3b74..d37822c 100644 --- a/Helpers/PerspectivePages/NotificationHelper.py +++ b/Helpers/PerspectivePages/NotificationHelper.py @@ -26,43 +26,43 @@ def __init__(self, driver: WebDriver): locator=self._NOTIFICATION_LOCATOR, driver=driver, description="A generic notification, used primarily for determining presence.", - wait_timeout=0) + timeout=0) self._close_icon = ComponentPiece( locator=self._CLOSE_ICON_LOCATOR, driver=driver, parent_locator_list=self._notifications.locator_list, description="The 'X' icon in the upper-right of notifications which allow for dismissal.", - wait_timeout=0) + timeout=0) self._body = ComponentPiece( locator=self._BODY_LOCATOR, driver=driver, parent_locator_list=self._notifications.locator_list, description="The body of the Notification, where the details and summary reside.", - wait_timeout=0) + timeout=0) self._icon = ComponentPiece( locator=self._ICON_LOCATOR, driver=driver, parent_locator_list=self._body.locator_list, description="The icon on the left-hand side of the Notification.", - wait_timeout=0) + timeout=0) self._summary = ComponentPiece( locator=self._SUMMARY_LOCATOR, driver=driver, parent_locator_list=self._body.locator_list, description="The summary (short description) of what the Notification is in regard to.", - wait_timeout=0) + timeout=0) self._details = ComponentPiece( locator=self._DETAILS_LOCATOR, driver=driver, parent_locator_list=self._body.locator_list, description="The in-depth details of the Notification.", - wait_timeout=0) + timeout=0) self._action_item = ComponentPiece( locator=self._ACTION_ITEM_LOCATOR, driver=driver, parent_locator_list=self._notifications.locator_list, description="The interactive piece of the Notification, used to reset or ignore the Notification.", - wait_timeout=0 + timeout=0 ) def any_notification_is_visible(self) -> bool: @@ -83,7 +83,7 @@ def close_any_notifications(self) -> None: :raises TimeoutException: If any of the Notifications does not allow for dismissal. """ while self.any_notification_is_visible(): - self._close_icon.click(binding_wait_time=1) # glorified sleep + self._close_icon.click(wait_after_click=1) # glorified sleep def get_count_of_current_notifications(self) -> int: """ @@ -92,7 +92,7 @@ def get_count_of_current_notifications(self) -> int: :returns: A count of the number of displayed Notifications. """ try: - return len(self._notifications.find_all(wait_timeout=0)) + return len(self._notifications.find_all(timeout=0)) except TimeoutException: return 0 @@ -106,7 +106,7 @@ def get_details(self, notification_index: int = 0) -> str: :raises IndexError: If the supplied index is invalid based on the number of displayed notifications. """ - return self._details.find_all(wait_timeout=0)[notification_index].text + return self._details.find_all(timeout=0)[notification_index].text def get_details_color(self, notification_index: int = 0) -> str: """ @@ -119,7 +119,7 @@ def get_details_color(self, notification_index: int = 0) -> str: :raises IndexError: If the supplied index is invalid based on the number of displayed notifications. """ - return self._details.find_all(wait_timeout=0)[notification_index].value_of_css_property(CSS.COLOR.value) + return self._details.find_all(timeout=0)[notification_index].value_of_css_property(CSS.COLOR.value) def get_icon_color(self, notification_index: int = 0) -> str: """ @@ -132,7 +132,7 @@ def get_icon_color(self, notification_index: int = 0) -> str: :raises IndexError: If the supplied index is invalid based on the number of displayed notifications. """ - return self._icon.find_all(wait_timeout=0)[notification_index].value_of_css_property(CSS.FILL.value) + return self._icon.find_all(timeout=0)[notification_index].value_of_css_property(CSS.FILL.value) def get_icon_id(self, notification_index: int = 0) -> str: """ @@ -145,7 +145,7 @@ def get_icon_id(self, notification_index: int = 0) -> str: :raises IndexError: If the supplied index is invalid based on the number of displayed notifications. """ return self._icon.find_all( - wait_timeout=0)[notification_index].find_element(By.TAG_NAME, "symbol").get_attribute("id") + timeout=0)[notification_index].find_element(By.TAG_NAME, "symbol").get_attribute("id") def get_summary(self, notification_index: int = 0) -> str: """ @@ -157,7 +157,7 @@ def get_summary(self, notification_index: int = 0) -> str: :raises IndexError: If the supplied index is invalid based on the number of displayed notifications. """ - return self._summary.find_all(wait_timeout=0)[notification_index].text + return self._summary.find_all(timeout=0)[notification_index].text def get_summary_color(self, notification_index: int = 0) -> str: """ @@ -170,7 +170,7 @@ def get_summary_color(self, notification_index: int = 0) -> str: :raises IndexError: If the supplied index is invalid based on the number of displayed notifications. """ - return self._summary.find_all(wait_timeout=0)[notification_index].value_of_css_property(CSS.COLOR.value) + return self._summary.find_all(timeout=0)[notification_index].value_of_css_property(CSS.COLOR.value) def click_notification_action(self, notification_index: int = 0) -> None: """ @@ -181,4 +181,4 @@ def click_notification_action(self, notification_index: int = 0) -> None: :raises IndexError: If the supplied index is invalid based on the number of displayed notifications. """ self._notifications.find_all( - wait_timeout=0)[notification_index].find_element(*self._ACTION_ITEM_LOCATOR).click() + timeout=0)[notification_index].find_element(*self._ACTION_ITEM_LOCATOR).click() diff --git a/Helpers/PerspectivePages/PopupHelper.py b/Helpers/PerspectivePages/PopupHelper.py index 591f8a9..80b464f 100644 --- a/Helpers/PerspectivePages/PopupHelper.py +++ b/Helpers/PerspectivePages/PopupHelper.py @@ -22,9 +22,9 @@ def __init__(self, driver: WebDriver): locator=self._GENERIC_POPUP_LOCATOR, driver=driver, description="A generic definition of a Popup.", - wait_timeout=0) # no parent + timeout=0) # no parent self._modal = ComponentPiece( - locator=self._MODAL_LOCATOR, driver=driver, parent_locator_list=None, wait_timeout=0) + locator=self._MODAL_LOCATOR, driver=driver, parent_locator_list=None, timeout=0) def close_all_popups(self) -> None: """ @@ -35,7 +35,7 @@ def close_all_popups(self) -> None: """ # find all popups try: - list_of_popups = self._generic_popup.find_all(wait_timeout=1) + list_of_popups = self._generic_popup.find_all(timeout=1) # now sort them so the "highest" is first. popups_sorted_by_z_index = sorted( list_of_popups, key=lambda e: e.value_of_css_property(CSS.Z_INDEX.value), reverse=True) @@ -57,7 +57,7 @@ def get_count_of_all_open_popups(self) -> int: :returns: A count of how many Popups are currently displayed. """ try: - return len(self._generic_popup.find_all(wait_timeout=0)) + return len(self._generic_popup.find_all(timeout=0)) except TimeoutException: return 0 @@ -68,6 +68,6 @@ def overlay_is_present(self) -> bool: :returns: True, if any Popup is displaying as a modal - False otherwise. """ try: - return self._modal.find(wait_timeout=0) is not None + return self._modal.find(timeout=0) is not None except TimeoutException: return False diff --git a/Helpers/PerspectivePages/QualityOverlayHelper.py b/Helpers/PerspectivePages/QualityOverlayHelper.py index a4b4a71..9719c94 100644 --- a/Helpers/PerspectivePages/QualityOverlayHelper.py +++ b/Helpers/PerspectivePages/QualityOverlayHelper.py @@ -35,50 +35,50 @@ class QualityOverlayHelper: def __init__(self, driver: WebDriver): self.driver = driver self._popover = ComponentPiece( - locator=self._POPOVER_LOCATOR, driver=driver, parent_locator_list=None, wait_timeout=0) + locator=self._POPOVER_LOCATOR, driver=driver, parent_locator_list=None, timeout=0) self._popover_icon = ComponentPiece( locator=self._POPOVER_ICON_LOCATOR, driver=driver, parent_locator_list=self._popover.locator_list, - wait_timeout=0, + timeout=0, description="The icon in the upper-right of the popover which allows for direct dismissal.") self._popover_back_button = ComponentPiece( locator=self._BACK_BUTTON_LOCATOR, driver=driver, parent_locator_list=self._popover.locator_list, - wait_timeout=0, + timeout=0, description="The '<' button/chevron in the bottom of the popover, available only when a component has " "multiple faulted bindings.") self._popover_next_button = ComponentPiece( locator=self._NEXT_BUTTON_LOCATOR, driver=driver, parent_locator_list=self._popover.locator_list, - wait_timeout=0, + timeout=0, description="The '>' button/chevron in the bottom of the popover, available only when a component has " "multiple faulted bindings.") self._card_count = ComponentPiece( locator=self._CARD_COUNT_LOCATOR, driver=driver, parent_locator_list=self._popover.locator_list, - wait_timeout=0, + timeout=0, description="The piece of popover UI which describes how many faulted bindings a component has.") self._body_section = ComponentPiece( locator=self._BODY_SECTION_LOCATOR, driver=driver, parent_locator_list=self._popover.locator_list, - wait_timeout=0, + timeout=0, description="The area between the header and footer of the popover.") self._body_descriptor = ComponentPiece( locator=self._BODY_DESCRIPTOR_LOCATOR, driver=driver, parent_locator_list=self._body_section.locator_list, - wait_timeout=0, + timeout=0, description="The label which SHOULD read as 'Subcode'.") self._body_content = ComponentPiece( locator=self._BODY_CONTENT_LOCATOR, driver=driver, parent_locator_list=self._body_section.locator_list, - wait_timeout=0, + timeout=0, description="The string designation of the subcode conveyed by a popover.") def any_popover_is_displayed(self) -> bool: diff --git a/Pages/BasicPageObject.py b/Pages/BasicPageObject.py index c608f24..a5c65a0 100644 --- a/Pages/BasicPageObject.py +++ b/Pages/BasicPageObject.py @@ -21,18 +21,18 @@ def __init__( self, driver: WebDriver, url: str, - wait_timeout: float = 10, + timeout: float = 10, primary_locator: Tuple[Union[By, str], str] = None): """ :param driver: The WebDriver in use for the browser window. :param url: The URL of this page. - :param wait_timeout: The amount of time (in seconds) to implicitly wait. + :param timeout: The amount of time (in seconds) to implicitly wait. :param primary_locator: A Tuple which describes how to locate a unique element on the page, if there is one. """ PagePiece.__init__( self, driver=driver, - wait_timeout=wait_timeout, + timeout=timeout, primary_locator=primary_locator) self.url = url self._navigation_string = f'Navigated to {self.url}' diff --git a/Pages/IgnitionPageObject.py b/Pages/IgnitionPageObject.py index ea636e0..cbc7bee 100644 --- a/Pages/IgnitionPageObject.py +++ b/Pages/IgnitionPageObject.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -17,17 +17,17 @@ def __init__( driver: WebDriver, gateway_address: str, destination_path: str, - wait_timeout: float = 10, - primary_locator: Tuple[By, str] = None): + timeout: float = 10, + primary_locator: Tuple[Union[By, str], str] = None): """ :param driver: The WebDriver being used to control the browser. :param gateway_address: The address (including port) of your Gateway. Expects scheme, all domains, and port. :param destination_path: The "subdirectory" and "path" pieces of the URL of this page resource, including a leading slash - eg: "/path/to/my/page" - :param wait_timeout: The amount of time (in seconds) this page resource should implicitly wait when needed. + :param timeout: The amount of time (in seconds) this page resource should implicitly wait when needed. :param primary_locator: A locator which describes an HTML element unique to this page. """ self._PATH = destination_path self.url = gateway_address + destination_path BasicPageObject.__init__( - self, driver=driver, url=self.url, wait_timeout=wait_timeout, primary_locator=primary_locator) + self, driver=driver, url=self.url, timeout=timeout, primary_locator=primary_locator) diff --git a/Pages/PagePiece.py b/Pages/PagePiece.py index 8ecc7e3..c8a906c 100644 --- a/Pages/PagePiece.py +++ b/Pages/PagePiece.py @@ -20,16 +20,16 @@ class PagePiece(object): def __init__( self, driver: WebDriver, - wait_timeout: float = 10, + timeout: float = 10, primary_locator: Tuple[Union[By, str], str] = None): """ :param driver: The WebDriver instance which is controlling the browser. - :param wait_timeout: The amount of time this page should implicitly wait when using functions which would wait. + :param timeout: The amount of time this page should implicitly wait when using functions which would wait. :param primary_locator: The locator which defines a known unique element within this content. """ self.driver = driver - self._wait_timeout = wait_timeout - self.wait = WebDriverWait(self.driver, self._wait_timeout) + self._timeout = timeout + self.wait = WebDriverWait(self.driver, self._timeout) self.primary_locator = primary_locator self.selenium = IASelenium(driver=self.driver) self.py_logger = logging.getLogger(self.__class__.__name__) diff --git a/Pages/Perspective/AppBar.py b/Pages/Perspective/AppBar.py index 3c9446d..aa99b87 100644 --- a/Pages/Perspective/AppBar.py +++ b/Pages/Perspective/AppBar.py @@ -1,3 +1,5 @@ +import math +import re from enum import Enum from time import sleep from typing import Optional @@ -9,11 +11,12 @@ from selenium.webdriver.support.wait import WebDriverWait from Components.BasicComponent import ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon from Helpers.CSSEnumerations import CSSPropertyValue from Helpers.Filtering import Items from Helpers.IAAssert import IAAssert from Helpers.IAExpectedConditions import IAExpectedConditions as IAec +from Helpers.IASelenium import IASelenium from Pages.PagePiece import PagePiece @@ -66,6 +69,8 @@ class TogglePosition(Enum): _GATEWAY_URL_LABEL_LOCATOR = (By.CSS_SELECTOR, f"div.gateway-url .{_DETAIL_DATA_CLASS}") _SESSION_ID_LABEL_LOCATOR = (By.CSS_SELECTOR, f"div.session-id .{_DETAIL_DATA_CLASS}") _PAGE_ID_LABEL_LOCATOR = (By.CSS_SELECTOR, f"div.page-id .{_DETAIL_DATA_CLASS}") + _PAGE_STATS_LOCATOR = (By.CSS_SELECTOR, "div.page-stats div.left") + _VALUE_LABELS_LOCATOR = (By.CSS_SELECTOR, "div.page-stats div.value-label") _PROJECT_NAME_LABEL_LOCATOR = (By.CSS_SELECTOR, "div.project-page h4") _UPDATE_STATUS_LOCATOR = (By.CSS_SELECTOR, 'div.update-status') _LEARN_MORE_BUTTON_LOCATOR = (By.CSS_SELECTOR, "a.visit-site-link") @@ -75,211 +80,225 @@ class TogglePosition(Enum): def __init__(self, driver: WebDriver): super().__init__(driver=driver, primary_locator=self._APP_BAR_WRAPPER_LOCATOR) + self.selenium = IASelenium(driver=self.driver) self._animation_wait = WebDriverWait(driver=driver, timeout=self._ANIMATION_DELAY_IN_SECONDS) self._about_button = CommonIcon( locator=self._ABOUT_BUTTON_LOCATOR, driver=driver, description="The button at the extreme left of the expanded App Bar, which opens an info modal.", - wait_timeout=0) + timeout=0) self._about_button_local_symbol = ComponentPiece( locator=self._ABOUT_BUTTON_LOCAL_SYMBOL_LOCATOR, driver=driver, parent_locator_list=self._about_button.locator_list, description="The icon used within the 'About' button within the expanded App Bar.", - wait_timeout=0) + timeout=0) self._about_panel = ComponentPiece( locator=self._ABOUT_MODAL_AND_SESSION_STATUS_MODAL_LOCATOR, driver=driver, parent_locator_list=None, description="The modal displayed which either displays info regarding Ignition, or a configured custom " "View.", - wait_timeout=0) + timeout=0) self._about_modal_and_session_status_modal_title = ComponentPiece( locator=self._ABOUT_MODAL_TITLE_LOCATOR, driver=driver, parent_locator_list=self._about_panel.locator_list, description="The title of the 'About' modal.", - wait_timeout=0) + timeout=0) self._app_bar_wrapper = ComponentPiece( locator=self._APP_BAR_WRAPPER_LOCATOR, driver=driver, parent_locator_list=None, description="The highest-level locator available to the App Bar.", - wait_timeout=0) + timeout=0) self._action_panel_toggle_button = ComponentPiece( locator=self._ACTION_PANEL_TOGGLE_BUTTON_LOCATOR, driver=driver, parent_locator_list=self._app_bar_wrapper.locator_list, description="The button in the center of the App Bar which expands and collapses the Action panel.", - wait_timeout=0) + timeout=0) self._generic_app_bar_notification_icon = CommonIcon( locator=self._GENERIC_NOTIFICATION_ICON_LOCATOR, driver=driver, parent_locator_list=self._action_panel_toggle_button.locator_list, description="A notification icon within the Action panel button (NOT the Revealer).", - wait_timeout=0) + timeout=0) self._action_panel = ComponentPiece( locator=self._ACTION_PANEL_LOCATOR, driver=driver, parent_locator_list=None, description="The panel which appears above the App Bar when the central button is clicked.", - wait_timeout=0) + timeout=0) self._connection_security_label = ComponentPiece( locator=self._CONNECTION_SECURITY_LABEL_LOCATOR, driver=driver, parent_locator_list=self._action_panel.locator_list, description="The label which displays the connection security of the Session.", - wait_timeout=0) + timeout=0) self._status_button = ComponentPiece( locator=self._STATUS_BUTTON_LOCATOR, driver=driver, parent_locator_list=self._action_panel.locator_list, description="The STATUS button within the Action panel.", - wait_timeout=0) + timeout=0) self._username_label = ComponentPiece( locator=self._USERNAME_LABEL_LOCATOR, driver=driver, parent_locator_list=self._action_panel.locator_list, description="The label in the Action panel which conveys which user is logged in (if any).", - wait_timeout=0) + timeout=0) self._revealer = ComponentPiece( locator=self._REVEALER_LOCATOR, driver=driver, parent_locator_list=self._app_bar_wrapper.locator_list, description="The small piece of UI in the session (usually bottom-right) which allows for expanding the " "App Bar.", - wait_timeout=0) + timeout=0) self._expand_icon = CommonIcon( locator=self._SHOW_ICON_LOCATOR, driver=driver, parent_locator_list=self._revealer.locator_list, description="The expand icon, which is only present while the App Bar is collapsed.", - wait_timeout=0) + timeout=0) self._collapse_icon = CommonIcon( locator=self._HIDE_ICON_LOCATOR, driver=driver, parent_locator_list=self._app_bar_wrapper.locator_list, # Belongs to the App Bar, NOT the Revealer! description="The collapse icon, which is only present while the App Bar is expanded.", - wait_timeout=0) + timeout=0) self._timer_icon = CommonIcon( locator=self._TIMER_ICON_LOCATOR, driver=driver, parent_locator_list=self._revealer.locator_list, description="The timer icon, which is only present while using Perspective in Trial mode.", - wait_timeout=0) + timeout=0) self._exit_project_icon = CommonIcon( locator=self._EXIT_PROJECT_LOCATOR, driver=driver, parent_locator_list=self._app_bar_wrapper.locator_list, description="The Exit Project icon, found only in workstation.", - wait_timeout=0) + timeout=0) self._generic_revealer_notification_icon = CommonIcon( locator=self._GENERIC_NOTIFICATION_ICON_LOCATOR, driver=driver, parent_locator_list=self._revealer.locator_list, description="A notification icon within the Revealer (NOT the Action panel button).", - wait_timeout=0) + timeout=0) self._session_status_modal = ComponentPiece( locator=self._ABOUT_MODAL_AND_SESSION_STATUS_MODAL_LOCATOR, driver=driver, parent_locator_list=None, description="The Session Status modal, which houses information about the Session and Gateway.", - wait_timeout=0) + timeout=0) self._gateway_tab = ComponentPiece( locator=self._GATEWAY_TAB_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The GATEWAY tab in the Session Status modal.", - wait_timeout=0) + timeout=0) self._project_tab = ComponentPiece( locator=self._PROJECT_TAB_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The PROJECT tab in the Session Status modal.", - wait_timeout=0) + timeout=0) self._gateway_name = ComponentPiece( locator=self._SESSION_STATUS_MODAL_GATEWAY_NAME_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The label which displays the name of the Gateway.", - wait_timeout=0) + timeout=0) self._connection_status_label = ComponentPiece( locator=self._CONNECTION_STATUS_LABEL_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The label which houses the connection status in the Session Status modal.", - wait_timeout=0) + timeout=0) self._session_status_panel_close_icon = CommonIcon( locator=self._CLOSE_ICON_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The 'X' in the upper-right of the Session Status modal.", - wait_timeout=0) + timeout=0) self._gateway_url_label = ComponentPiece( locator=self._GATEWAY_URL_LABEL_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The label which houses the Gateway URL information in the Session Status modal.", - wait_timeout=0) + timeout=0) self._session_id_label = ComponentPiece( locator=self._SESSION_ID_LABEL_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The label which houses the Session ID information in the Session Status modal.", - wait_timeout=0) + timeout=0) self._page_id_label = ComponentPiece( locator=self._PAGE_ID_LABEL_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The label which houses the Page ID information in the Session Status modal.", - wait_timeout=0) + timeout=0) + self._value_labels = ComponentPiece( + locator=self._VALUE_LABELS_LOCATOR, + driver=driver, + parent_locator_list=self._session_status_modal.locator_list, + description="", + timeout=0) + self._page_stats = ComponentPiece( + locator=self._PAGE_STATS_LOCATOR, + driver=driver, + parent_locator_list=self._session_status_modal.locator_list, + description="The Page stats for the Page shown that houses view resources, number of components, number of " + "property changes, latency, and uptime.", + timeout=0) self._project_name_label = ComponentPiece( locator=self._PROJECT_NAME_LABEL_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The label which contains the project name at the top of the Project tab.", - wait_timeout=0) + timeout=0) self._up_to_date_status_label = ComponentPiece( locator=self._UPDATE_STATUS_LOCATOR, driver=driver, parent_locator_list=self._session_status_modal.locator_list, description="The label which describes whether a project is up-to-date with the Gateway.", - wait_timeout=0) + timeout=0) self._tooltip = ComponentPiece( locator=self._TOOLTIP_LOCATOR, driver=driver, parent_locator_list=None, description="the tooltip which is only visible when hovering over the expand/collapse icon of the " "Revealer.", - wait_timeout=1) # this piece does need a wait period as it takes a moment to appear after the hover event. + timeout=1) # this piece does need a wait period as it takes a moment to appear after the hover event. self._generic_text_button = ComponentPiece( locator=self._GENERIC_TEXT_BUTTON_LOCATOR, driver=driver, parent_locator_list=None, description="A 'button' (
) within the greater Action panel.", - wait_timeout=0) + timeout=0) self._about_panel_close_icon = CommonIcon( locator=self._CLOSE_ICON_LOCATOR, driver=driver, parent_locator_list=None, description="The 'X' in the upper-right of the 'About' modal.", - wait_timeout=0) + timeout=0) self._learn_more_button = ComponentPiece( locator=self._LEARN_MORE_BUTTON_LOCATOR, driver=driver, parent_locator_list=self._about_panel.locator_list, description="The button within the 'About' modal which has text of 'LEARN MORE ABOUT IGNITION'.", - wait_timeout=1) + timeout=1) self._view_wrapper = ComponentPiece( locator=self._CUSTOM_VIEW_WRAPPER_LOCATOR, driver=driver, parent_locator_list=self._about_panel.locator_list, - wait_timeout=1) + timeout=1) self._generic_custom_view_top_level = ComponentPiece( locator=self._CUSTOM_VIEW_LOCATOR, driver=driver, parent_locator_list=self._about_panel.locator_list, - wait_timeout=1) + timeout=1) def about_modal_is_displayed(self) -> bool: """ @@ -288,7 +307,7 @@ def about_modal_is_displayed(self) -> bool: :returns: True, if the 'About Ignition' panel is displayed - False otherwise. """ try: - return self._about_panel.find(wait_timeout=0).is_displayed() \ + return self._about_panel.find(timeout=0).is_displayed() \ and "open" in self._about_panel.find().get_attribute("class") \ and self._about_modal_and_session_status_modal_title.get_text() != self._SESSION_STATUS_MODAL_TITLE_TEXT except TimeoutException: @@ -301,7 +320,7 @@ def action_panel_is_expanded(self) -> bool: :returns: True, if the Action Panel is currently displayed - False otherwise. """ try: - return self._action_panel.find(wait_timeout=0).is_displayed() and \ + return self._action_panel.find(timeout=0).is_displayed() and \ self._action_panel.get_termination().Y <= self._app_bar_wrapper.get_origin().Y except TimeoutException: return False @@ -312,7 +331,7 @@ def click_about_button(self) -> None: """ self._expand_app_bar_if_not_expanded() # some tests check button appearance after click, so let the button have time to change color - self._about_button.click(binding_wait_time=1) + self._about_button.click(wait_after_click=1) def click_learn_more_about_ignition_button(self) -> None: """ @@ -332,7 +351,7 @@ def click_sign_in_button(self) -> None: self._expand_action_panel_if_not_expanded() Items( items=self._generic_text_button.find_all( - wait_timeout=1)).filter( + timeout=1)).filter( lambda e: e.text == 'SIGN IN')[0].click() def click_sign_out_button(self) -> None: @@ -344,7 +363,7 @@ def click_sign_out_button(self) -> None: self._expand_action_panel_if_not_expanded() Items( items=self._generic_text_button.find_all( - wait_timeout=1)).filter( + timeout=1)).filter( lambda e: e.text == 'SIGN OUT')[0].click() def click_status_button(self) -> None: @@ -617,7 +636,7 @@ def get_icon_path_of_about_button(self) -> str: self._expand_app_bar_if_not_expanded() try: # Not using the standard icon structure because it isn't technically a Perspective component - return self._about_button_local_symbol.find(wait_timeout=0).get_attribute("id") + return self._about_button_local_symbol.find(timeout=0).get_attribute("id") except TimeoutException: return self._about_button.get_icon_name() @@ -636,6 +655,79 @@ def get_logged_in_user(self) -> Optional[str]: except TimeoutException: return None + def get_number_of_view_resources(self) -> int: + """ + Obtain the number of view resources on the Perspective page from the Page Stats modal. + + :returns: The number of view resources of the Perspective page. + + :raises AssertionError: If unsuccessful in opening the Session Status modal. + """ + + self._open_session_status_modal_if_not_open() + number_of_view_resources = self._value_labels.find_all()[0] + self.selenium.scroll_to_element(web_element=number_of_view_resources) + return int(number_of_view_resources.text) + + def get_number_of_components(self) -> int: + """ + Obtain the number of components on the Perspective page from the Page Stats modal. + + :returns: The number of components of the Perspective page. + + :raises AssertionError: If unsuccessful in opening the Session Status modal. + """ + self._open_session_status_modal_if_not_open() + number_of_components = self._value_labels.find_all()[1] + self.selenium.scroll_to_element(web_element=number_of_components) + return int(number_of_components.text) + + def get_number_of_property_changes(self) -> int: + """ + Obtain the number of property changes on the Perspective page from the Page Stats modal. + + :returns: The number of property changes of the Perspective page. + + :raises AssertionError: If unsuccessful in opening the Session Status modal. + """ + self._open_session_status_modal_if_not_open() + number_of_property_changes = self._value_labels.find_all()[2] + self.selenium.scroll_to_element(web_element=number_of_property_changes) + return int(number_of_property_changes.text) + + def get_latency(self) -> int: + """ + Obtain the latency of the Perspective page from the Page Stats modal. + + :returns: The latency of the Perspective page. + + :raises AssertionError: If unsuccessful in opening the Session Status modal. + """ + self._open_session_status_modal_if_not_open() + latency = self._value_labels.find_all()[3] + self.selenium.scroll_to_element(web_element=latency) + return int(latency.text) + + def get_page_uptime_in_seconds(self) -> int: + """ + Obtain the uptime of the Perspective page from the Page Stats modal. + + :returns: The total uptime of the Perspective page in seconds. + + :raises AssertionError: If unsuccessful in opening the Session Status modal. + """ + self._open_session_status_modal_if_not_open() + uptime = self._page_stats.find_all()[4] + self.selenium.scroll_to_element(web_element=uptime) + regex_pattern = r"[\d]+" + total_time_seconds = 0 + units = re.findall(regex_pattern, uptime.text) + units.reverse() + + for index in range(len(units)): + total_time_seconds += (int(units[index]) * math.pow(60, index)) + return total_time_seconds + def get_page_id(self) -> str: """ Obtain the Page ID in use for this Page of the Session. @@ -645,7 +737,7 @@ def get_page_id(self) -> str: :raises AssertionError: If unsuccessful in opening the Session Status modal. """ self._open_session_status_modal_if_not_open() - return self._page_id_label.get_text() + return self._page_id_label.get_text().removeprefix("Page ID: ") def get_project_status(self) -> str: """ @@ -705,7 +797,7 @@ def hover_over_revealer(self) -> None: revealer_icon = \ self._collapse_icon if self.is_expanded() else self._expand_icon revealer_icon.hover() - revealer_icon.wait_on_binding(time_to_wait=0.5) + revealer_icon.wait_some_time(time_to_wait=0.5) def is_expanded(self) -> bool: """ @@ -772,7 +864,7 @@ def revealer_displayed_in_position(self, toggle_position: TogglePosition) -> boo :returns: True, if the Revealer is displayed in the supplied position - False otherwise. """ - return (f'bar-reveal-{toggle_position.value}' in self._revealer.find(wait_timeout=0).get_attribute('class'))\ + return (f'bar-reveal-{toggle_position.value}' in self._revealer.find(timeout=0).get_attribute('class'))\ and self._revealer.find().is_displayed() def revealer_tooltip_is_displayed(self) -> bool: @@ -783,7 +875,7 @@ def revealer_tooltip_is_displayed(self) -> bool: :returns: True, if the tooltip of the Revealer is currently displayed - False otherwise. """ try: - return self._tooltip.find(wait_timeout=0).is_displayed() + return self._tooltip.find(timeout=0).is_displayed() except TimeoutException: return False @@ -810,7 +902,7 @@ def revealer_is_displayed(self) -> bool: :returns: True, if the Revealer is currently displayed - False otherwise. """ try: - return self._revealer.find(wait_timeout=0).is_displayed() + return self._revealer.find(timeout=0).is_displayed() except TimeoutException: return False @@ -821,7 +913,7 @@ def session_status_modal_is_open(self) -> bool: :returns: True, if the Session Status modal is currently open - False otherwise. """ try: - return self._session_status_modal.find(wait_timeout=0).is_displayed() \ + return self._session_status_modal.find(timeout=0).is_displayed() \ and "open" in self._session_status_modal.find().get_attribute("class") \ and self._about_modal_and_session_status_modal_title.get_text() == self._SESSION_STATUS_MODAL_TITLE_TEXT except TimeoutException: @@ -865,25 +957,26 @@ def status_button_is_displayed(self) -> bool: """ self._expand_action_panel_if_not_expanded() try: - return self._status_button.find(wait_timeout=0).is_displayed() + return self._status_button.find(timeout=0).is_displayed() except TimeoutException: return False - def wait_for_revealer_or_app_bar_to_be_displayed(self, wait_timeout: int = 10) -> None: + def wait_for_revealer_or_app_bar_to_be_displayed(self, timeout: int = 10) -> bool: """ Wait for the Session to display either the Revealer or the expanded App Bar. Useful for waiting for Perspective Pages. - :param wait_timeout: The amount of time (in seconds) you ar wiling to wait for the Revealer to be present + :param timeout: The amount of time (in seconds) you ar wiling to wait for the Revealer to be present before allowing code to continue. - - :raises TimeoutException: If neither the Revealer nor the App Bar become visible in the allotted time. + :returns: True, if either the Revealer or the App Bar is displayed before the timeout period has elapsed + - False otherwise. """ try: - WebDriverWait(driver=self.driver, timeout=wait_timeout).until( + WebDriverWait(driver=self.driver, timeout=timeout).until( IAec.function_returns_true(custom_function=self._app_bar_or_revealer_is_displayed, function_args={})) + return True except TimeoutException as toe: - raise TimeoutException(msg="Neither the App Bar nor the Revealer ever became visible.") from toe + return False def _app_bar_is_fully_collapsed(self) -> bool: """ @@ -914,7 +1007,7 @@ def _app_bar_or_revealer_is_displayed(self) -> bool: except TimeoutException: pass try: - app_bar_is_displayed = self._app_bar_wrapper.find(wait_timeout=0).is_displayed() + app_bar_is_displayed = self._app_bar_wrapper.find(timeout=0).is_displayed() except TimeoutException: pass return revealer_is_displayed or app_bar_is_displayed @@ -971,7 +1064,7 @@ def _expand_app_bar_if_not_expanded(self) -> None: :raises AssertionError: If unsuccessful in expanding the App Bar. """ if not self.is_expanded(): - self._expand_icon.click(wait_timeout=0.5) + self._expand_icon.click(timeout=0.5) self._wait_for_app_bar_to_be_fully_expanded() IAAssert.is_true( value=self.is_expanded(), @@ -985,7 +1078,7 @@ def _get_generic_text_button(self, button_text: str) -> WebElement: """ return Items( items=self._generic_text_button.find_all( - wait_timeout=1)).filter( + timeout=1)).filter( lambda e: e.text == button_text)[0] def _open_about_modal_if_not_open(self) -> None: @@ -1045,7 +1138,7 @@ def _session_status_modal_is_open(self) -> bool: :returns: True, if the Session Status modal is currently open and visible to the user - False otherwise. """ try: - return self._session_status_modal.find(wait_timeout=0).is_displayed() \ + return self._session_status_modal.find(timeout=0).is_displayed() \ and "open" in self._session_status_modal.find().get_attribute("class") except TimeoutException: return False diff --git a/Pages/Perspective/DockedView.py b/Pages/Perspective/DockedView.py index 65e2bc0..fb8dc8f 100644 --- a/Pages/Perspective/DockedView.py +++ b/Pages/Perspective/DockedView.py @@ -1,6 +1,6 @@ from enum import Enum from time import sleep -from typing import Tuple, Optional +from typing import Tuple, Optional, Union from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By @@ -33,7 +33,7 @@ def __init__( driver: WebDriver, side: Side, root_id: str, - primary_locator: Tuple[By, str] = None, + primary_locator: Tuple[Union[By, str], str] = None, view_resource_path: Optional[str] = None, config_id: Optional[str] = None): """ @@ -60,7 +60,7 @@ def __init__( handle_locator = ( By.CSS_SELECTOR, f"div.docked-view-{self._side.value}{dock_id_piece} div.view-toggle") - self._dock_toggle_handle = ComponentPiece(driver=driver, locator=handle_locator, wait_timeout=1) + self._dock_toggle_handle = ComponentPiece(driver=driver, locator=handle_locator, timeout=1) def click_handle(self) -> None: """ @@ -196,7 +196,7 @@ def is_expanded(self) -> bool: View does not have an HTML `id` attribute applied to the root node. """ try: - return self._root.find(wait_timeout=0).is_displayed() + return self._root.find(timeout=0).is_displayed() except TimeoutException: return False diff --git a/Pages/Perspective/Popup.py b/Pages/Perspective/Popup.py index f23480a..022cb56 100644 --- a/Pages/Perspective/Popup.py +++ b/Pages/Perspective/Popup.py @@ -7,7 +7,7 @@ from selenium.webdriver.remote.webdriver import WebDriver from Components.BasicComponent import ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon from Helpers.IAAssert import IAAssert from Helpers.IASelenium import IASelenium from Pages.Perspective.View import View @@ -64,31 +64,31 @@ def __init__( driver=driver, parent_locator_list=self.locator_list, description="The title bar of the Popup, where the title text and close icon are displayed.", - wait_timeout=0) + timeout=0) self._drag_handle = ComponentPiece( locator=self._DRAG_HANDLE_LOCATOR, driver=driver, parent_locator_list=self.locator_list, description="The drag handle located within the title bar.", - wait_timeout=0) + timeout=0) self._popup_body = ComponentPiece( locator=self._BODY_LOCATOR, driver=driver, parent_locator_list=self.locator_list, description="The internal contents of the Popup, where the View is visible.", - wait_timeout=0) + timeout=0) self._close_icon = CommonIcon( locator=self._CLOSE_ICON_LOCATOR, driver=driver, parent_locator_list=self.locator_list, description="The 'X' used to close the Popup.", - wait_timeout=0) + timeout=0) self._modal = ComponentPiece( locator=self._MODAL_LOCATOR, driver=driver, description="The overlay displayed when a Popup is opened as a modal.", parent_locator_list=None, - wait_timeout=0) + timeout=0) self._resize_zones = {} self._make_resize_zones() @@ -172,7 +172,7 @@ def has_close_icon(self) -> bool: :returns: True, if this Popup contains a close icon - False otherwise. """ try: - return self._close_icon.find(wait_timeout=0) is not None + return self._close_icon.find(timeout=0) is not None except TimeoutException: return False @@ -184,7 +184,7 @@ def has_title_bar(self) -> bool: :returns: True, if the Popup is currently displaying a title bar at the top of the Popup. """ try: - return self.title_bar.find(wait_timeout=0) is not None + return self.title_bar.find(timeout=0) is not None except TimeoutException: return False @@ -196,7 +196,7 @@ def is_modal(self) -> bool: :returns: True, if the Popup is declaring itself to be a modal AND an overlay exists - False otherwise. """ - return 'popup-modal' in self.find(wait_timeout=0).get_attribute('class') and self.overlay_is_present() + return 'popup-modal' in self.find(timeout=0).get_attribute('class') and self.overlay_is_present() def is_draggable(self) -> bool: """ @@ -205,7 +205,7 @@ def is_draggable(self) -> bool: :returns: True, if the Popup has a draggable handle within the title bar - False otherwise. """ try: - return self._drag_handle.find(wait_timeout=0) is not None + return self._drag_handle.find(timeout=0) is not None except TimeoutException: return False @@ -228,7 +228,7 @@ def overlay_is_present(self) -> bool: :returns: True if ANY overlay is present, even if for another Popup. False if no overlays are displayed. """ try: - return self._modal.find(wait_timeout=0) is not None + return self._modal.find(timeout=0) is not None except TimeoutException: return False @@ -239,7 +239,7 @@ def is_displayed(self) -> bool: :returns: True, if this Popup is currently displayed - False otherwise. """ try: - return self.find(wait_timeout=0).is_displayed() + return self.find(timeout=0).is_displayed() except TimeoutException: return False @@ -272,4 +272,4 @@ def _make_resize_zones(self) -> None: parent_locator_list=self.locator_list, description=f"The resize zone of the {self.formatted_id} Popup in the {resize_zone.value} region " f"({resize_zone}).", - wait_timeout=0) + timeout=0) diff --git a/Pages/Perspective/PrintPreviewPage.py b/Pages/Perspective/PrintPreviewPage.py index 645b24c..42783ea 100644 --- a/Pages/Perspective/PrintPreviewPage.py +++ b/Pages/Perspective/PrintPreviewPage.py @@ -7,7 +7,7 @@ class ChromePrintPreviewPage: """Interface for the print preview page implemented in Chrome.""" - CANCEL_BUTTON_LOCATOR = (By.CSS_SELECTOR, 'div.controls > cr-button.cancel-button') + CANCEL_BUTTON_LOCATOR = (By.CSS_SELECTOR, 'cr-button.cancel-button') PRINT_PREVIEW_APP_LOCATOR = (By.CSS_SELECTOR, 'print-preview-app') PRINT_PREVIEW_BUTTON_STRIP_LOCATOR = (By.CSS_SELECTOR, 'print-preview-button-strip') SIDE_BAR_LOCATOR = (By.ID, 'sidebar') @@ -18,33 +18,33 @@ def __init__(self, driver: WebDriver): locator=self.PRINT_PREVIEW_APP_LOCATOR, driver=driver, parent_locator_list=None, - wait_timeout=1, + timeout=1, poll_freq=0) - def click_cancel_button(self, wait_timeout: float = 1) -> None: + def click_cancel_button(self, timeout: float = 1) -> None: """ Closes the print preview page. When the page is closed, the print preview's window handle will be invalid. This resource does not attempt to manage driver windows, so they will need to be handled elsewhere. - :param wait_timeout: The amount of time (in seconds) to wait for the print preview tag to appear before + :param timeout: The amount of time (in seconds) to wait for the print preview tag to appear before returning. """ - self._print_preview_app.find(wait_timeout=wait_timeout).shadow_root.find_element(*self.SIDE_BAR_LOCATOR) \ + self._print_preview_app.find(timeout=timeout).shadow_root.find_element(*self.SIDE_BAR_LOCATOR) \ .shadow_root.find_element(*self.PRINT_PREVIEW_BUTTON_STRIP_LOCATOR).shadow_root \ .find_element(*self.CANCEL_BUTTON_LOCATOR).click() - def print_preview_page_is_open(self, wait_timeout: float = 1) -> bool: + def print_preview_page_is_open(self, timeout: float = 1) -> bool: """ Checks if the print preview page is open by attempting to find the print-preview-app tag in the DOM. - :param wait_timeout: The amount of time (in seconds) to wait for the print preview tag to appear + :param timeout: The amount of time (in seconds) to wait for the print preview tag to appear before returning. :returns: True, if the print-preview-app tag can be located within the timeout window - False otherwise. """ try: - return self._print_preview_app.find(wait_timeout=wait_timeout) is not None + return self._print_preview_app.find(timeout=timeout) is not None except TimeoutException: return False diff --git a/Pages/Perspective/View.py b/Pages/Perspective/View.py index 5ee722b..725272b 100644 --- a/Pages/Perspective/View.py +++ b/Pages/Perspective/View.py @@ -1,6 +1,5 @@ from typing import Tuple, Optional -from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from Pages.PagePiece import PagePiece @@ -18,21 +17,21 @@ class View(PagePiece): def __init__( self, driver: WebDriver, - primary_locator: Tuple[By, str] = None, + primary_locator: Tuple[str, str] = None, view_resource_path: Optional[str] = None, - wait_timeout: float = 10): + timeout: float = 10): """ :param driver: The WebDriver in use for the browser window. :param primary_locator: The primary locator used to identify a component of the View. This will be the same as the `root_id` in most cases. :param view_resource_path: The path to the View within the Designer, after `Perspective/Views`. - :param wait_timeout: The amount of time this resource should implicitly wait when querying. + :param timeout: The amount of time this resource should implicitly wait when querying. """ PagePiece.__init__( self, driver=driver, primary_locator=primary_locator, - wait_timeout=wait_timeout) + timeout=timeout) self._view_resource_path = view_resource_path def get_view_resource_path(self) -> Optional[str]: diff --git a/Pages/Perspective/ViewCanvasInstancedView.py b/Pages/Perspective/ViewCanvasInstancedView.py index 85cf5eb..5006f67 100644 --- a/Pages/Perspective/ViewCanvasInstancedView.py +++ b/Pages/Perspective/ViewCanvasInstancedView.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Tuple, Union from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver @@ -20,7 +20,7 @@ class ViewCanvasInstancedView(View): def __init__( self, driver: WebDriver, - root_locator: Tuple, + root_locator: Tuple[Union[By, str], str], view_resource_path: str = None): """ :param driver: The WebDriver in use for the browser window. diff --git a/Pages/PerspectivePageObject.py b/Pages/PerspectivePageObject.py index beea99d..ef7c573 100644 --- a/Pages/PerspectivePageObject.py +++ b/Pages/PerspectivePageObject.py @@ -7,7 +7,7 @@ from selenium.webdriver.support import expected_conditions as ec from Components.BasicComponent import BasicComponent, ComponentPiece -from Components.PerspectiveComponents.Common.Icon import CommonIcon +from Components.Common.Icon import CommonIcon from Helpers.CSSEnumerations import CSSPropertyValue from Helpers.Point import Point from Pages.IgnitionPageObject import IgnitionPageObject @@ -46,26 +46,26 @@ def __init__( locator=self._SEPARATOR_LOCATOR, driver=driver, parent_locator_list=self.locator_list, - wait_timeout=1) + timeout=1) self._item_coll = {} self._icon_coll = {} def click_item( - self, item_text: str, submenu_depth: Optional[int] = None, binding_wait_time: float = 0.5) -> None: + self, item_text: str, submenu_depth: Optional[int] = None, wait_after_click: float = 0.5) -> None: """ Click an item within the context menu. For items within submenus, you must first hover over whichever other items would expand the submenus. :param item_text: The text of the item to click. :param submenu_depth: The depth of the submenu to which the item belongs. - :param binding_wait_time: The amount of time to wait after clicking the item before allowing code to + :param wait_after_click: The amount of time to wait after clicking the item before allowing code to continue. """ if submenu_depth is not None: # For sub-menu items, we must first hover over the 0th element in that sub-menu, otherwise # the sub-menu collapses as we leave our current item self._get_item(submenu_depth=submenu_depth).hover() - self._get_item(item_text=item_text, submenu_depth=submenu_depth).click(binding_wait_time=binding_wait_time) + self._get_item(item_text=item_text, submenu_depth=submenu_depth).click(wait_after_click=wait_after_click) def get_path_of_expansion_icon(self, item_text: str, submenu_depth: Optional[int] = None) -> str: """ @@ -116,16 +116,16 @@ def get_property_of_separator(self, index: int, property_name: CSSPropertyValue) :raises IndexError: If separators are found, but fewer than the specified index. """ # allow IndexError to bubble up if encountered - return self._separators.find_all(wait_timeout=0)[index].get_attribute(property_name.value) + return self._separators.find_all(timeout=0)[index].get_attribute(property_name.value) def hover_over_item( - self, item_text: str, submenu_depth: Optional[int] = None, binding_wait_time: float = 0) -> None: + self, item_text: str, submenu_depth: Optional[int] = None, wait_after_hover: float = 0) -> None: """ Hover over an item of the context menu. This is extremely important while dealing with submenus. :param item_text: The text of the item to hover over. :param submenu_depth: The depth of the item which will be hovered over. - :param binding_wait_time: The amount of time after hovering over the item to wait before allowing code to + :param wait_after_hover: The amount of time after hovering over the item to wait before allowing code to continue. """ if submenu_depth is not None: @@ -135,7 +135,7 @@ def hover_over_item( self._get_item(item_text=item_text, submenu_depth=submenu_depth).hover() self._get_item( item_text=item_text, - submenu_depth=submenu_depth).wait_on_binding(time_to_wait=binding_wait_time) + submenu_depth=submenu_depth).wait_some_time(time_to_wait=wait_after_hover) def is_displayed(self) -> bool: """ @@ -144,7 +144,7 @@ def is_displayed(self) -> bool: :return: True, if a context menu is displayed on the current Page - False otherwise. """ try: - return self.find(wait_timeout=0) is not None + return self.find(timeout=0) is not None except TimeoutException: return False @@ -159,7 +159,7 @@ def item_contains_configured_icon(self, item_text: str, submenu_depth: Optional[ """ try: return self._get_icon( - item_text=item_text, submenu_depth=submenu_depth).find(wait_timeout=0).is_displayed() + item_text=item_text, submenu_depth=submenu_depth).find(timeout=0).is_displayed() except TimeoutException: return False @@ -174,7 +174,7 @@ def item_contains_submenu_expansion_icon(self, item_text: str, submenu_depth: Op """ try: return self._get_expansion_icon( - item_text=item_text, submenu_depth=submenu_depth).find(wait_timeout=0).is_displayed() + item_text=item_text, submenu_depth=submenu_depth).find(timeout=0).is_displayed() except TimeoutException: return False @@ -191,7 +191,7 @@ def item_is_displayed(self, item_text: str, submenu_depth: Optional[int] = None) """ try: item = self._get_item(item_text=item_text, submenu_depth=submenu_depth) - return item.find(wait_timeout=0).is_displayed() and item.get_text() == item_text + return item.find(timeout=0).is_displayed() and item.get_text() == item_text except TimeoutException: return False @@ -209,7 +209,7 @@ def submenu_is_displayed(self, submenu_depth: int = 0) -> bool: submenu_depth=submenu_depth), driver=self.driver, parent_locator_list=None, - wait_timeout=1).find(wait_timeout=0).is_displayed() + timeout=1).find(timeout=0).is_displayed() except TimeoutException: return False @@ -233,7 +233,7 @@ def _get_expansion_icon(self, item_text: str, submenu_depth: Optional[int] = Non locator=self._SUBMENU_EXPANSION_ICON_LOCATOR, driver=self.driver, parent_locator_list=self._get_item(item_text=item_text, submenu_depth=submenu_depth).locator_list, - wait_timeout=1) + timeout=1) self._icon_coll[identifier] = icon return icon @@ -257,7 +257,7 @@ def _get_icon(self, item_text: str, submenu_depth: Optional[int] = None) -> Comm locator=(By.CSS_SELECTOR, 'svg'), driver=self.driver, parent_locator_list=parent_locator_list, - wait_timeout=1) + timeout=1) self._icon_coll[identifier] = icon return icon @@ -283,7 +283,7 @@ def _get_item(self, item_text: Optional[str] = None, submenu_depth: Optional[int locator=(By.CSS_SELECTOR, f'{self._ITEM_LOCATOR[1]}{label_piece}'), driver=self.driver, parent_locator_list=parent_locator_list, - wait_timeout=1) + timeout=1) self._item_coll[identifier] = item return item @@ -299,7 +299,7 @@ def __init__( primary_view_resource_path: str = None, primary_locator: Tuple = None, configured_tab_title: str = None, - wait_timeout: int = 10): + timeout: int = 10): """ :param driver: The WebDriver in use for the browser window. :param gateway_address: The address of the Gateway this page belongs to. @@ -307,7 +307,7 @@ def __init__( :param primary_view_resource_path: The path of the primary View in use for this Page. :param primary_locator: A tuple which describes an element unique to this Page. :param configured_tab_title: The title configured for this tab. - :param wait_timeout: The amount of time (in seconds) to implicitly wait. + :param timeout: The amount of time (in seconds) to implicitly wait. """ self._internal_page_url_path = page_config_path self._full_perspective_path = self._PERSPECTIVE_PATH_PREFIX + self._internal_page_url_path @@ -316,7 +316,7 @@ def __init__( gateway_address=gateway_address, destination_path=self._full_perspective_path, primary_locator=primary_locator, - wait_timeout=wait_timeout) + timeout=timeout) self.app_bar = AppBar(driver=driver) self._primary_view_resource_path = primary_view_resource_path self.configured_page_title = configured_tab_title @@ -324,16 +324,16 @@ def __init__( driver=driver) def click_item_of_component_context_menu( - self, item_text: str, submenu_depth: Optional[int] = None, binding_wait_time: float = 0.5) -> None: + self, item_text: str, submenu_depth: Optional[int] = None, wait_after_click: float = 0.5) -> None: """ Click an item within the context menu displayed on the Page. :param item_text: The text of the item you wish to click. :param submenu_depth: The depth of the submenu to query for. - :param binding_wait_time: The amount of time to wait after clicking the specified item before allowing code to + :param wait_after_click: The amount of time to wait after clicking the specified item before allowing code to continue. """ self._component_context_menu.click_item( - item_text=item_text, submenu_depth=submenu_depth, binding_wait_time=binding_wait_time) + item_text=item_text, submenu_depth=submenu_depth, wait_after_click=wait_after_click) def component_context_menu_is_displayed(self) -> bool: """ @@ -483,17 +483,17 @@ def get_width_of_component_context_menu(self, include_units: bool = False) -> st return self._component_context_menu.get_computed_width(include_units=include_units) def hover_over_item_of_component_context_menu( - self, item_text: str, submenu_depth: Optional[int] = None, binding_wait_time: float = 0) -> None: + self, item_text: str, submenu_depth: Optional[int] = None, wait_after_hover: float = 0) -> None: """ Hover over an item of the context menu. This is extremely important while dealing with submenus. :param item_text: The text of the item to hover over. :param submenu_depth: The depth of the item which will be hovered over. - :param binding_wait_time: The amount of time after hovering over the item to wait before allowing code to + :param wait_after_hover: The amount of time after hovering over the item to wait before allowing code to continue. """ self._component_context_menu.hover_over_item( - item_text=item_text, submenu_depth=submenu_depth, binding_wait_time=binding_wait_time) + item_text=item_text, submenu_depth=submenu_depth, wait_after_hover=wait_after_hover) def item_in_component_context_menu_contains_icon(self, item_text: str, submenu_depth: Optional[int] = None) -> bool: """