Skip to content

Commit

Permalink
Updates and Fixes for 8.1.37 (#5)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
garth-gross authored Feb 1, 2024
1 parent 0ae376e commit df04462
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Components/Common/TextInput.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
77 changes: 75 additions & 2 deletions Components/PerspectiveComponents/Displays/GoogleMap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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(',')
Expand All @@ -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.
Expand All @@ -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.
Expand Down
94 changes: 93 additions & 1 deletion Components/PerspectiveComponents/Inputs/NumericEntryField.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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],
Expand All @@ -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()
Expand Down Expand Up @@ -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],
Expand Down
4 changes: 3 additions & 1 deletion Helpers/Ignition/Alarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {}
Expand All @@ -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:
Expand Down
23 changes: 22 additions & 1 deletion Helpers/Ignition/Tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"""
Expand Down
14 changes: 14 additions & 0 deletions Helpers/PerspectiveAlarm.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
9 changes: 3 additions & 6 deletions Pages/Perspective/TerminalStates/TerminalStatePageObject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
28 changes: 28 additions & 0 deletions Pages/PerspectivePageObject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down

0 comments on commit df04462

Please sign in to comment.