From df04462d1666b263e2ae68864d6a347b9b37da57 Mon Sep 17 00:00:00 2001 From: Garth Gross <145372855+garth-gross@users.noreply.github.com> Date: Thu, 1 Feb 2024 13:48:03 -0800 Subject: [PATCH] Updates and Fixes for 8.1.37 (#5) Modified: - Components/Common/TextInput.py - Components/PerspectiveComponents/Displays/GoogleMap.py - Components/PerspectiveComponents/Inputs/NumericEntryField.py - Helpers/Ignition/Alarm.py - Helpers/Ignition/Tag.py - Helpers/PerspectiveAlarm.py - Pages/Perspective/TerminalStates/TerminalStatePageObject.py - Pages/PerspectivePageObject.py --- Components/Common/TextInput.py | 2 +- .../Displays/GoogleMap.py | 77 ++++++++++++++- .../Inputs/NumericEntryField.py | 94 ++++++++++++++++++- Helpers/Ignition/Alarm.py | 4 +- Helpers/Ignition/Tag.py | 23 ++++- Helpers/PerspectiveAlarm.py | 14 +++ .../TerminalStates/TerminalStatePageObject.py | 9 +- Pages/PerspectivePageObject.py | 28 ++++++ 8 files changed, 239 insertions(+), 12 deletions(-) diff --git a/Components/Common/TextInput.py b/Components/Common/TextInput.py index 90efb69..bd10d82 100644 --- a/Components/Common/TextInput.py +++ b/Components/Common/TextInput.py @@ -63,7 +63,7 @@ def placeholder_text_exists(self) -> bool: :returns: True, if a component is currently displaying placeholder text - False otherwise. """ - return self.find().get_attribute("placeholder") is not None + return (self.find().get_attribute("placeholder") is not None) and (self.find().get_attribute("value") is None) def set_text( self, diff --git a/Components/PerspectiveComponents/Displays/GoogleMap.py b/Components/PerspectiveComponents/Displays/GoogleMap.py index c9955bb..d4d799f 100644 --- a/Components/PerspectiveComponents/Displays/GoogleMap.py +++ b/Components/PerspectiveComponents/Displays/GoogleMap.py @@ -47,8 +47,11 @@ class GoogleMap(BasicPerspectiveComponent): _ROTATE_TILT_MAP_COUNTERCLOCKWISE_BUTTON_LOCATOR = (By.CSS_SELECTOR, 'button[title="Rotate map counterclockwise"]') _EXTERNAL_LINK_LOCATOR = (By.CSS_SELECTOR, 'a[title="Open this area in Google Maps (opens a new window)"]') _GENERAL_CLOSE_POPUP_BUTTON_LOCATOR = (By.CSS_SELECTOR, 'button[title="Close"]') - _MAP_TYPE_CONTROLS_LOCATOR = (By.CSS_SELECTOR, 'div[role="menuitemradio" aria-checked="true"] | ' - 'button[title="Change map style"].span') + # Map type controls div classes will be one of the following, depending on its layout (configured by + # mapType.controlStyle). The wildcard allows us to locate it in either layout. + # In dropdown mode: gmnoprint gm-style-mtc + # In horizontal_bar mode: gmnoprint gm-style-mtc-bbw + _MAP_TYPE_CONTROLS_LOCATOR = (By.CSS_SELECTOR, 'div.gmnoprint[class*=" gm-style-mtc"]') class _MapTypeControls(ComponentPiece): """ @@ -456,12 +459,34 @@ def double_right_click_map_with_offset(self, x_offset: int = 0, y_offset: int = .context_click()\ .perform() + def get_full_screen_button_height(self, include_units: bool = False) -> str: + """ + Obtains the computed height of the full screen button. + + :returns: The computed height of the full screen button. + """ + return self._full_screen_button.get_computed_height(include_units=include_units) + + def get_full_screen_button_width(self, include_units: bool = False) -> str: + """ + Obtains the computed width of the full screen button. + + :returns: The computed width of the full screen button. + """ + return self._full_screen_button.get_computed_width(include_units=include_units) + def get_map_center(self) -> GeographicPoint: """ Obtains the central latitude and longitude point of the Google Map's current position. :returns: A GeographicPoint representing the latitude and longitude of the center of the map. """ + try: + self.wait.until(ec.presence_of_element_located(self._EXTERNAL_LINK_LOCATOR)) + except TimeoutException as toe: + raise TimeoutException( + f"Failed to get the map center position because the link that we parse it from was not present.") \ + from toe center = re.search( '(?<=ll=)(.*)(?=&z)', self._external_link.find(wait_timeout=0).get_attribute('href')).group().split(',') @@ -475,6 +500,22 @@ def get_map_type(self) -> Union[MapType, MapSubtype]: """ return self._map_type_controls.get_selected_map_type() + def get_map_type_controls_height(self, include_units: bool = False) -> str: + """ + Obtains the computed width of the map type controls, in either layout setting (dropdown or horizontal bar.) + + :returns: The computed height of the map type controls. + """ + return self._map_type_controls.get_computed_height(include_units=include_units) + + def get_map_type_controls_width(self, include_units: bool = False) -> str: + """ + Obtains the computed width of the map type controls, in either layout setting (dropdown or horizontal bar.) + + :returns: The computed width of the map type controls. + """ + return self._map_type_controls.get_computed_width(include_units=include_units) + def get_map_zoom(self) -> float: """ Obtains the current zoom of the Google Map. @@ -494,6 +535,38 @@ def get_popup_count(self) -> int: except TimeoutException: return 0 + def get_zoom_in_button_height(self, include_units: bool = False) -> str: + """ + Obtains the computed height of the zoom in button. + + :returns: The computed height of the zoom in button. + """ + return self._zoom_in_button.get_computed_height(include_units=include_units) + + def get_zoom_in_button_width(self, include_units: bool = False) -> str: + """ + Obtains the computed width of the zoom in button. + + :returns: The computed width of the zoom in button. + """ + return self._zoom_in_button.get_computed_width(include_units=include_units) + + def get_zoom_out_button_height(self, include_units: bool = False) -> str: + """ + Obtains the computed height of the zoom out button. + + :returns: The computed height of the zoom out button. + """ + return self._zoom_out_button.get_computed_height(include_units=include_units) + + def get_zoom_out_button_width(self, include_units: bool = False) -> str: + """ + Obtains the computed width of the zoom out button. + + :returns: The computed width of the zoom out button. + """ + return self._zoom_out_button.get_computed_width(include_units=include_units) + def right_click(self, x_offset: int = 0, y_offset: int = 0) -> None: """ Attempts to find the Google Map component and then right click at the supplied pixel offset. diff --git a/Components/PerspectiveComponents/Inputs/NumericEntryField.py b/Components/PerspectiveComponents/Inputs/NumericEntryField.py index 7811625..16797cb 100644 --- a/Components/PerspectiveComponents/Inputs/NumericEntryField.py +++ b/Components/PerspectiveComponents/Inputs/NumericEntryField.py @@ -124,6 +124,40 @@ def get_modal_origin(self) -> Point: """ return self._modal.get_origin() + def get_placeholder_text(self) -> str: + """ + Obtain the text which would be displayed as a placeholder. This function makes no claims about the visibility + of this text. + + :return: The text which would be displayed if the component would display a placeholder. + """ + if self._needs_to_get_input_element(): + input_elem = self.find().find_element(By.TAG_NAME, "input") + else: + input_elem = self.find() + return input_elem.get_attribute("placeholder") + + def get_placeholder_text_from_modal(self) -> str: + """ + Obtain the text which would be displayed as a placeholder in the input modal. This function makes no claims + about the visibility of this text. + + :return: The text which would be displayed if the component would display a placeholder. + """ + if not self.modal_is_displayed(): + self.click_edit_icon() + return self._modal_input.find().get_attribute("placeholder") + + def get_value_attribute_from_modal(self) -> Optional[str]: + """ + Obtain the value of the `value` attribute in the input of the modal. + + :return: The value of the `value` attribute of the input within the modal. + """ + if not self.modal_is_displayed(): + self.click_edit_icon() + return self._modal_input.find(wait_timeout=0).get_attribute("value") + def modal_is_displayed(self) -> bool: """ Determine if the entry modal is currently displayed. @@ -135,6 +169,37 @@ def modal_is_displayed(self) -> bool: except TimeoutException: return False + def placeholder_is_displayed(self) -> bool: + """ + Determine if the main component is displaying placeholder text. + + This is entirely dependent on HTML rules, which dictate a placeholder will be displayed if + 1. The `placeholder` attribute has a valid string value with length greater than 0 + 2. The `value` attribute is has no value (an empty string attribute value counts as a value, but the Session + back-end might be an empty string while the HTML attribute is None). + + :return: True, if the component has a valid `placeholder` attribute value and no value for the `value` + attribute - False otherwise. + """ + placeholder_attr_value = self.get_placeholder_text() + value_attr_value = self._internal_input.find(wait_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: + """ + Determine if the input modal is displaying placeholder text. + + This is entirely dependent on HTML rules, which dictate a placeholder will be displayed if + 1. The `placeholder` attribute has a valid string value with length greater than 0 + 2. The `value` attribute is has no value (an empty string attribute value counts as a value, but the Session + back-end might be an empty string while the HTML attribute is None). + + :return: True, if the input modal has a valid `placeholder` attribute value and no value for the `value` + attribute - False otherwise. + """ + attr_value = self.get_placeholder_text_from_modal() + return (attr_value is not None) and (len(attr_value) > 0) and (not self.get_value_attribute_from_modal()) + def set_text( self, text: Union[float, str], @@ -153,7 +218,8 @@ def set_text( :raises AssertionError: If application or cancellation is specified, and the final displayed (and formatted) value of the Numeric Entry Field does not match the supplied text. """ - self.click_edit_icon() + if not self.modal_is_displayed(): + self.click_edit_icon() self._set_text(text=text) if apply_cancel_no_action is not None: self.click_apply() if apply_cancel_no_action else self.click_cancel() @@ -223,6 +289,32 @@ def get_manual_entry_text(self) -> str: self._internal_input.find().send_keys(Keys.ENTER) # ENTER to avoid closing any popup return text + def get_placeholder_text(self) -> str: + """ + Obtain the text which would be displayed as a placeholder. This function makes no claims about the visibility + of this text. + + :return: The text which would be displayed if the component would display a placeholder. + """ + return self._internal_input.find().get_attribute("placeholder") + + def placeholder_is_displayed(self) -> bool: + """ + Determine if the Numeric Entry Field is displaying placeholder text. + + This is entirely dependent on HTML rules, which dictate a placeholder will be displayed if + 1. The `placeholder` attribute has a valid string value with length greater than 0 + 2. The `value` attribute is has no value (an empty string attribute value counts as a value, but the Session + back-end might be an empty string while the HTML attribute is None). + + :return: True, if the component has a valid `placeholder` attribute value and no value for the `value` + attribute - False otherwise. + """ + attr_value = self.get_placeholder_text() + return (attr_value is not None) \ + and (len(attr_value) > 0) \ + and (not self._internal_input.find().get_attribute("value")) + def set_text( self, text: Union[float, int, str], diff --git a/Helpers/Ignition/Alarm.py b/Helpers/Ignition/Alarm.py index 9c4bc32..6893541 100644 --- a/Helpers/Ignition/Alarm.py +++ b/Helpers/Ignition/Alarm.py @@ -76,6 +76,7 @@ def __init__(self, name: str): self.time_off_delay_seconds = None self.time_on_delay_seconds = None self.timestamp_source = None + self.source = None # Alarm Mode Settings self.active_condition = None @@ -96,6 +97,7 @@ def get_name(self) -> str: def to_dict(self) -> Dict: local_alarm = self.duplicate() special_case = ['_name'] + ignore_list = ['source'] # Get set attributes of the Tag Object attrs = vars(local_alarm) alarm_dict = {} @@ -105,7 +107,7 @@ def to_dict(self) -> Dict: del attrs[item] for attr in attrs: ignition_key = self._to_camel_case(attr) - if getattr(local_alarm, attr) is not None: + if getattr(local_alarm, attr) is not None and attr not in ignore_list: if isinstance(getattr(local_alarm, attr), Enum): alarm_dict[ignition_key] = getattr(local_alarm, attr).value else: diff --git a/Helpers/Ignition/Tag.py b/Helpers/Ignition/Tag.py index 822d77f..d24cf89 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 +from typing import List, Dict, Optional, Union from Helpers.Ignition.Alarm import AlarmHelper, AlarmDefinition @@ -399,6 +399,27 @@ def __init__(self, name: str, path: str, provider: str = '[default]'): class TagHelper: + @staticmethod + def build_tag_from_dictionary(tag_configs: Dict): + provider = '' + path = '' + name = tag_configs.get('name') + if 'path' in tag_configs: + provider = f'[{tag_configs.get("path").get("source")}]' + path = TagHelper._process_path(tag_configs.get('path', '').get('pathParts', '')) + match tag_configs.get('tagType'): + case 'UdtInstance': + tag = UdtInstance(name=name, provider=provider, path=path, type_id='') + case 'UdtType': + tag = UdtDef(name=name, provider=provider, path=path) + case 'Folder': + tag = Folder(name=name, provider=provider, path=path) + case _: + tag = Tag(name=name, provider=provider, path=path) + for key in tag_configs.keys(): + tag = TagHelper._process_key(tag, key, tag_configs[key]) + return tag + @staticmethod def build_tag_from_string(dict_as_string: str): """Build a Tag, UdtInstance, UdtDef or Folder object from a string representation of a dict""" diff --git a/Helpers/PerspectiveAlarm.py b/Helpers/PerspectiveAlarm.py index b39426e..ac6ef26 100644 --- a/Helpers/PerspectiveAlarm.py +++ b/Helpers/PerspectiveAlarm.py @@ -1,3 +1,6 @@ +from Helpers.Ignition.Alarm import AlarmDefinition + + class PerspectiveAlarm(object): """ Defines the configuration of an Ignition alarm, and is not intended to be used for storing information @@ -16,3 +19,14 @@ def __init__(self, name: str, display_path: str, priority: str, label: str, self.source_path = source_path self.tag_path = tag_path self.notes = notes + + def get_alarm_definition(self) -> AlarmDefinition: + definition = AlarmDefinition(name=self.name) + definition.display_path = self.display_path + definition.priority = self.priority + definition.label = self.label + definition.notes = self.notes + definition.source = self.source_path + definition.tag_path = self.tag_path + definition.notes = self.notes + return definition diff --git a/Pages/Perspective/TerminalStates/TerminalStatePageObject.py b/Pages/Perspective/TerminalStates/TerminalStatePageObject.py index 3c82f4b..ee4d506 100644 --- a/Pages/Perspective/TerminalStates/TerminalStatePageObject.py +++ b/Pages/Perspective/TerminalStates/TerminalStatePageObject.py @@ -106,17 +106,14 @@ def launch_gateway_button_is_present(self) -> bool: except NoSuchElementException: return False - def wait_for_terminal_state_page(self) -> None: + def wait_for_terminal_state_page(self) -> bool: """ Wait for the Terminal State Page to load, but allow code to continue in the event the page does not load. """ try: - self.wait.until( - method=ec.presence_of_element_located( - self._HEADER_LOCATOR), - message="The header was not found on the Terminal State page - the page may not have loaded") + return self.wait.until(method=ec.presence_of_element_located(self._HEADER_LOCATOR)) is not None except TimeoutException: - pass + return False def is_current_page(self) -> bool: """ diff --git a/Pages/PerspectivePageObject.py b/Pages/PerspectivePageObject.py index 3524c97..beea99d 100644 --- a/Pages/PerspectivePageObject.py +++ b/Pages/PerspectivePageObject.py @@ -368,6 +368,20 @@ def get_count_of_components_with_errors(self) -> int: errors_caught_in_boundary_list += self.driver.find_elements(By.CSS_SELECTOR, f'div.{error_class}') return len(error_over_lay_list + errors_caught_in_boundary_list) + def get_height_of_component_context_menu(self, include_units: bool = False) -> str: + """ + Get the computed height of the component context menu. Must return as a string because of the possibility of + included units. + + :param include_units: Include the units of height (typically "px") if True, otherwise return only the numeric + value (as a str). + + :returns: The computed height of the component context menu as a string. + + :raises TimeoutException: If the component is not found in the DOM. + """ + return self._component_context_menu.get_computed_height(include_units=include_units) + def get_origin_of_component_context_menu(self) -> Point: """ Get the Cartesian Coordinate of the upper-left corner of the Context Menu, measured from the @@ -454,6 +468,20 @@ def get_termination_of_component_context_menu(self) -> Point: """ return self._component_context_menu.get_termination() + def get_width_of_component_context_menu(self, include_units: bool = False) -> str: + """ + Get the computed width of the component context menu. Must return as a string because of the possibility of + included units. + + :param include_units: Include the units of width (typically "px") if True, otherwise return only the numeric + value (as a str). + + :returns: The computed width of the component context menu as a string. + + :raises TimeoutException: If the component is not found in the DOM. + """ + 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: """