tir-framework 2.14.2__tar.gz → 2.14.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tir_framework-2.14.2/tir_framework.egg-info → tir_framework-2.14.4}/PKG-INFO +1 -1
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/base.py +11 -2
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/poui_internal.py +86 -2
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/webapp_internal.py +264 -97
- tir_framework-2.14.4/tir/version.py +1 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4/tir_framework.egg-info}/PKG-INFO +1 -1
- tir_framework-2.14.2/tir/version.py +0 -1
- {tir_framework-2.14.2 → tir_framework-2.14.4}/LICENSE +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/MANIFEST.in +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/README.md +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/setup.cfg +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/setup.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tests/test_log.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tests/test_utils.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tests/test_webapp_internal_clicktree.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/__init__.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/main.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/apw_internal.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/base_database.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/config.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/data/sx3.csv +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/linux64/geckodriver +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/windows/chromedriver.exe +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/windows/electron/chromedriver.exe +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/windows/geckodriver.exe +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/enumerations.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/events.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/language.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/log.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/logging_config.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/numexec.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/psutil_info.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/router.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/third_party/screen_size.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/third_party/xpath_soup.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/utils.py +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir_framework.egg-info/SOURCES.txt +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir_framework.egg-info/dependency_links.txt +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir_framework.egg-info/requires.txt +0 -0
- {tir_framework-2.14.2 → tir_framework-2.14.4}/tir_framework.egg-info/top_level.txt +0 -0
|
@@ -232,6 +232,8 @@ class Base(unittest.TestCase):
|
|
|
232
232
|
except StaleElementReferenceException:
|
|
233
233
|
logger().debug("********Element Stale click*********")
|
|
234
234
|
return False
|
|
235
|
+
except ElementClickInterceptedException as e:
|
|
236
|
+
logger().debug(e.msg)
|
|
235
237
|
except Exception as e:
|
|
236
238
|
logger().debug(f"Warning click method Exception: {str(e)}")
|
|
237
239
|
return False
|
|
@@ -586,7 +588,14 @@ class Base(unittest.TestCase):
|
|
|
586
588
|
tries += 1
|
|
587
589
|
|
|
588
590
|
if filtered_iframe:
|
|
589
|
-
|
|
591
|
+
if self.webapp_shadowroot():
|
|
592
|
+
shadow_iframe = self.find_shadow_element('iframe', filtered_iframe)
|
|
593
|
+
if shadow_iframe:
|
|
594
|
+
self.driver.switch_to.frame(shadow_iframe[0])
|
|
595
|
+
else:
|
|
596
|
+
self.driver.switch_to.frame(filtered_iframe)
|
|
597
|
+
else:
|
|
598
|
+
self.driver.switch_to.frame(filtered_iframe)
|
|
590
599
|
|
|
591
600
|
|
|
592
601
|
def filter_active_iframe(self, iframes):
|
|
@@ -1632,7 +1641,7 @@ class Base(unittest.TestCase):
|
|
|
1632
1641
|
|
|
1633
1642
|
# Filter blocked elements only when WaitProcessing is not in the stack
|
|
1634
1643
|
# and blocked-container filtering is enabled.
|
|
1635
|
-
if not self.search_stack('WaitProcessing') and self.filter_blocked_containers:
|
|
1644
|
+
if not self.search_stack('WaitProcessing') and not self.search_stack('IfExists') and self.filter_blocked_containers:
|
|
1636
1645
|
non_blocked_elements = list(filter(lambda x: hasattr(x, 'attr') and 'blocked' not in x.attrs, elements))
|
|
1637
1646
|
|
|
1638
1647
|
if isinstance(non_blocked_elements, list):
|
|
@@ -5768,6 +5768,90 @@ class PouiInternal(Base):
|
|
|
5768
5768
|
|
|
5769
5769
|
self.set_program(program_name, program_desc, module)
|
|
5770
5770
|
|
|
5771
|
+
def ensure_load_after_program(self, timeout: int = None) -> None:
|
|
5772
|
+
"""
|
|
5773
|
+
[Internal]
|
|
5774
|
+
|
|
5775
|
+
Ensures the routine screen finished loading after ``set_program``.
|
|
5776
|
+
|
|
5777
|
+
After confirming the program there is a slow transition while the
|
|
5778
|
+
elements load. On some routines the po-loading component takes longer
|
|
5779
|
+
than the 15s handled by :func:`_po_loading` to even appear, so relying
|
|
5780
|
+
only on it is not reliable.
|
|
5781
|
+
|
|
5782
|
+
This method loops until one of the *expected* landing screens is rendered
|
|
5783
|
+
(the **browse** grid, a **wizard** dialog or the **parameters** dialog)
|
|
5784
|
+
and no po-loading is active on top of it. While po-loading is present
|
|
5785
|
+
(even if it shows up late) the screen is not considered ready and the
|
|
5786
|
+
method keeps waiting for the transition to finish.
|
|
5787
|
+
|
|
5788
|
+
:param timeout: Maximum time, in seconds, to wait for an expected screen.
|
|
5789
|
+
If ``None``, uses ``config.time_out``. - **Default:** None
|
|
5790
|
+
:type timeout: int
|
|
5791
|
+
:return: None
|
|
5792
|
+
:rtype: None
|
|
5793
|
+
|
|
5794
|
+
Usage:
|
|
5795
|
+
|
|
5796
|
+
>>> self.ensure_load_after_program()
|
|
5797
|
+
>>> self.ensure_load_after_program(timeout=90)
|
|
5798
|
+
"""
|
|
5799
|
+
|
|
5800
|
+
if timeout is None:
|
|
5801
|
+
timeout = self.config.time_out
|
|
5802
|
+
|
|
5803
|
+
# CSS selectors that identify each expected landing screen after opening
|
|
5804
|
+
# a routine. Adjust here if a new landing screen needs to be supported.
|
|
5805
|
+
expected_screens = {
|
|
5806
|
+
"browse": ("wa-tgrid, .dict-tgetdados, .dict-tcbrowse, .dict-tgrid, "
|
|
5807
|
+
".dict-twbrowse, .dict-tsbrowse, .dict-msbrgetdbase, "
|
|
5808
|
+
".dict-brgetddb, .dict-msselbr, thf-grid"),
|
|
5809
|
+
"wizard": "wa-dialog wa-stepper, wa-dialog [class*='wizard'], .twizard",
|
|
5810
|
+
"parameters": ("wa-dialog wa-text-input, wa-dialog .dict-tget, "
|
|
5811
|
+
".tmodaldialog .tget"),
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5814
|
+
logger().info("Ensuring the routine screen finished loading...")
|
|
5815
|
+
|
|
5816
|
+
endtime = time.time() + timeout
|
|
5817
|
+
loaded_screen = None
|
|
5818
|
+
|
|
5819
|
+
while time.time() < endtime:
|
|
5820
|
+
# Find which expected screen is currently rendered (if any).
|
|
5821
|
+
loaded_screen = None
|
|
5822
|
+
for screen_name, selector in expected_screens.items():
|
|
5823
|
+
if self.element_exists(term=selector,
|
|
5824
|
+
scrap_type=enum.ScrapType.CSS_SELECTOR,
|
|
5825
|
+
main_container='body',
|
|
5826
|
+
check_error=False):
|
|
5827
|
+
loaded_screen = screen_name
|
|
5828
|
+
break
|
|
5829
|
+
|
|
5830
|
+
# Small delay so a late po-loading has time to appear on top of the
|
|
5831
|
+
# screen before we check for it, avoiding a false "ready" result.
|
|
5832
|
+
time.sleep(0.5)
|
|
5833
|
+
|
|
5834
|
+
loading = self.element_exists(term='po-loading',
|
|
5835
|
+
scrap_type=enum.ScrapType.CSS_SELECTOR,
|
|
5836
|
+
main_container='body',
|
|
5837
|
+
check_error=False)
|
|
5838
|
+
|
|
5839
|
+
# Ready only when an expected screen is rendered and nothing is
|
|
5840
|
+
# loading on top of it. While po-loading is present (even if it shows
|
|
5841
|
+
# up late) we keep waiting for the transition to finish.
|
|
5842
|
+
if loaded_screen and not loading:
|
|
5843
|
+
break
|
|
5844
|
+
|
|
5845
|
+
loaded_screen = None
|
|
5846
|
+
time.sleep(1)
|
|
5847
|
+
|
|
5848
|
+
if loaded_screen:
|
|
5849
|
+
logger().info(f"Expected screen loaded: '{loaded_screen}'.")
|
|
5850
|
+
else:
|
|
5851
|
+
logger().warning("None of the expected screens (browse, wizard, "
|
|
5852
|
+
"parameters) finished loading within the timeout of "
|
|
5853
|
+
f"{timeout}s.")
|
|
5854
|
+
|
|
5771
5855
|
def set_program(self, program_name: str = "", program_desc: str = "", module: str = ""):
|
|
5772
5856
|
|
|
5773
5857
|
self.escape_to_main_menu()
|
|
@@ -5844,8 +5928,8 @@ class PouiInternal(Base):
|
|
|
5844
5928
|
self.log_error(message)
|
|
5845
5929
|
message = 'setUpClass - ' + message if self.log.get_testcase_stack() == 'setUpClass' else message
|
|
5846
5930
|
self.assertTrue(False, message)
|
|
5847
|
-
|
|
5848
|
-
self.
|
|
5931
|
+
|
|
5932
|
+
self.ensure_load_after_program()
|
|
5849
5933
|
|
|
5850
5934
|
if not program_name in self.closed_user_guide_routines:
|
|
5851
5935
|
closed_user_guide = self._close_user_guide()
|
|
@@ -1968,6 +1968,7 @@ class WebappInternal(Base):
|
|
|
1968
1968
|
except Exception as e:
|
|
1969
1969
|
logger().exception(str(e))
|
|
1970
1970
|
|
|
1971
|
+
|
|
1971
1972
|
def escape_to_main_menu(self):
|
|
1972
1973
|
"""[Internal]
|
|
1973
1974
|
|
|
@@ -1997,6 +1998,7 @@ class WebappInternal(Base):
|
|
|
1997
1998
|
logger().debug(f'wa-dialog layers: {container_layers}')
|
|
1998
1999
|
|
|
1999
2000
|
if not success:
|
|
2001
|
+
self.restart_counter += 1
|
|
2000
2002
|
self.log_error('Home screen not found!')
|
|
2001
2003
|
|
|
2002
2004
|
# wait trasitions between screens to avoid errors in layers number
|
|
@@ -4862,6 +4864,68 @@ class WebappInternal(Base):
|
|
|
4862
4864
|
logger().warning(f"Warning SetLateralMenu click last item method exception: {str(e)} ")
|
|
4863
4865
|
|
|
4864
4866
|
|
|
4867
|
+
def get_shadowroot_button(self, button, term_button, position, check_error=True):
|
|
4868
|
+
"""
|
|
4869
|
+
[Internal]
|
|
4870
|
+
|
|
4871
|
+
Captures the button element inside a shadowroot container.
|
|
4872
|
+
|
|
4873
|
+
:param button: Button caption to be located.
|
|
4874
|
+
:type button: str
|
|
4875
|
+
:param term_button: CSS selector used to locate the button candidates.
|
|
4876
|
+
:type term_button: str
|
|
4877
|
+
:param position: Index of the button among the filtered candidates.
|
|
4878
|
+
:type position: int
|
|
4879
|
+
:param check_error: Whether errors should be checked while waiting. - **Default:** True
|
|
4880
|
+
:type check_error: bool
|
|
4881
|
+
|
|
4882
|
+
:return: The located button element (Tag or Selenium element) or None if not found.
|
|
4883
|
+
|
|
4884
|
+
Usage:
|
|
4885
|
+
|
|
4886
|
+
>>> next_button = self.get_shadowroot_button(button, term_button, position, check_error)
|
|
4887
|
+
"""
|
|
4888
|
+
regex = r"(<[^>]*>)?"
|
|
4889
|
+
next_button = None
|
|
4890
|
+
filtered_buttons = []
|
|
4891
|
+
|
|
4892
|
+
self.wait_element_timeout(term=button, scrap_type=enum.ScrapType.MIXED, optional_term=term_button, timeout=10, step=0.1, check_error=check_error)
|
|
4893
|
+
self.containers_selectors["GetCurrentContainer"] = "wa-dialog, wa-message-box,.tmodaldialog, body"
|
|
4894
|
+
container = self.get_current_container()
|
|
4895
|
+
button_candidates = container.select(term_button)
|
|
4896
|
+
|
|
4897
|
+
if button_candidates:
|
|
4898
|
+
filtered_buttons = list(filter(lambda x: hasattr(x, 'caption') and button.lower() in re.sub(regex, '', x['caption'].lower()) and self.element_is_displayed(x), button_candidates))
|
|
4899
|
+
|
|
4900
|
+
if not filtered_buttons:
|
|
4901
|
+
filtered_buttons = self.return_soup_by_selenium(elements=button_candidates, term=button, selectors='label, span')
|
|
4902
|
+
else:
|
|
4903
|
+
footer = self.execute_js_selector('footer', self.soup_to_selenium(container), get_all=False)
|
|
4904
|
+
if footer:
|
|
4905
|
+
footer_buttons = self.execute_js_selector("wa-button", footer)
|
|
4906
|
+
if not footer_buttons:
|
|
4907
|
+
footer_buttons = footer.find_elements(By.CSS_SELECTOR, "wa-button")
|
|
4908
|
+
if footer_buttons:
|
|
4909
|
+
filtered_buttons = list(filter(lambda x: x.text.strip().replace('\n', '') == button.strip().replace(' \n ', ''), footer_buttons))
|
|
4910
|
+
|
|
4911
|
+
# Fallback: botao em foco entre os candidatos
|
|
4912
|
+
if not (filtered_buttons and len(filtered_buttons) - 1 >= position):
|
|
4913
|
+
focus_buttons = list(filter(lambda x: (hasattr(x, 'caption') and button.lower() in re.sub(regex, '', x['caption'].lower())) and 'focus' in x.get('class'), button_candidates))
|
|
4914
|
+
if focus_buttons:
|
|
4915
|
+
filtered_buttons = focus_buttons
|
|
4916
|
+
|
|
4917
|
+
# Fallback: busca ampla via web_scrap
|
|
4918
|
+
if not filtered_buttons:
|
|
4919
|
+
filtered_buttons = self.web_scrap(term=button, scrap_type=enum.ScrapType.MIXED, optional_term="wa-button", main_container=self.containers_selectors["SetButton"])
|
|
4920
|
+
|
|
4921
|
+
if filtered_buttons and len(filtered_buttons) - 1 >= position:
|
|
4922
|
+
parents_actives = list(filter(lambda x: self.filter_active_tabs(x), filtered_buttons))
|
|
4923
|
+
if parents_actives:
|
|
4924
|
+
filtered_buttons = parents_actives
|
|
4925
|
+
next_button = filtered_buttons[position]
|
|
4926
|
+
|
|
4927
|
+
return next_button
|
|
4928
|
+
|
|
4865
4929
|
def SetButton(self, button, sub_item="", position=1, check_error=True):
|
|
4866
4930
|
"""
|
|
4867
4931
|
Method that clicks on a button on the screen.
|
|
@@ -4897,7 +4961,10 @@ class WebappInternal(Base):
|
|
|
4897
4961
|
"""
|
|
4898
4962
|
logger().info(f"Clicking on {button}")
|
|
4899
4963
|
|
|
4900
|
-
|
|
4964
|
+
# Only the first SetButton usage in the session ignores the
|
|
4965
|
+
# element_is_on_top filter. Subsequent calls keep the normal behavior.
|
|
4966
|
+
first_setbutton_use = not getattr(self, "_setbutton_used", False)
|
|
4967
|
+
self._setbutton_used = True
|
|
4901
4968
|
|
|
4902
4969
|
self.wait_blocker()
|
|
4903
4970
|
|
|
@@ -4906,13 +4973,16 @@ class WebappInternal(Base):
|
|
|
4906
4973
|
else:
|
|
4907
4974
|
term_button="button, .thbutton"
|
|
4908
4975
|
|
|
4976
|
+
id_container = None
|
|
4909
4977
|
container = self.get_current_container()
|
|
4910
4978
|
if container and 'id' in container.attrs:
|
|
4911
4979
|
id_container = container.attrs['id']
|
|
4912
4980
|
|
|
4913
4981
|
try:
|
|
4914
4982
|
restore_zoom = False
|
|
4983
|
+
restore_zoom_recaptured = False
|
|
4915
4984
|
soup_element = ""
|
|
4985
|
+
id_parent_element = None
|
|
4916
4986
|
if (button.lower().strip() == "x"):
|
|
4917
4987
|
self.set_button_x(position, check_error)
|
|
4918
4988
|
return
|
|
@@ -4930,57 +5000,31 @@ class WebappInternal(Base):
|
|
|
4930
5000
|
success = False
|
|
4931
5001
|
endtime = time.time() + self.config.time_out
|
|
4932
5002
|
starttime = time.time()
|
|
5003
|
+
halftime = time.time() + (self.config.time_out / 2)
|
|
4933
5004
|
|
|
4934
5005
|
if self.config.smart_test or self.config.debug_log:
|
|
4935
5006
|
logger().debug(f"***System Info*** Before Clicking on button:{button}")
|
|
4936
5007
|
system_info()
|
|
4937
5008
|
|
|
4938
|
-
regex = r"(<[^>]*>)?"
|
|
4939
|
-
filtered_button = []
|
|
4940
5009
|
next_button = None
|
|
4941
5010
|
while(time.time() < endtime and not soup_element):
|
|
5011
|
+
# During the first half of the timeout, also require the element to be the
|
|
5012
|
+
# topmost one at its center point (not intercepted by an overlay). After that,
|
|
5013
|
+
# fall back to the legacy behavior (element_is_displayed only).
|
|
5014
|
+
# The very first SetButton usage always skips this on_top check.
|
|
5015
|
+
enforce_on_top = (not first_setbutton_use) and (time.time() < halftime)
|
|
4942
5016
|
if self.webapp_shadowroot():
|
|
4943
|
-
self.
|
|
4944
|
-
self.containers_selectors["GetCurrentContainer"] = "wa-dialog, wa-message-box,.tmodaldialog, body"
|
|
4945
|
-
soup = self.get_current_container()
|
|
4946
|
-
soup_objects = soup.select(term_button)
|
|
4947
|
-
|
|
4948
|
-
if soup_objects and not filtered_button:
|
|
4949
|
-
filtered_button = list(filter(lambda x: hasattr(x,'caption') and button.lower() in re.sub(regex,'',x['caption'].lower()) and self.element_is_displayed(x), soup_objects ))
|
|
4950
|
-
|
|
4951
|
-
if not filtered_button:
|
|
4952
|
-
filtered_button = self.return_soup_by_selenium(elements=soup_objects, term=button, selectors='label, span')
|
|
4953
|
-
|
|
4954
|
-
if self.webapp_shadowroot():
|
|
4955
|
-
if not soup_objects:
|
|
4956
|
-
footer = self.execute_js_selector('footer', self.soup_to_selenium(soup), get_all=False)
|
|
4957
|
-
if footer:
|
|
4958
|
-
buttons = self.execute_js_selector("wa-button", footer)
|
|
4959
|
-
if not buttons:
|
|
4960
|
-
buttons = footer.find_elements(By.CSS_SELECTOR, "wa-button")
|
|
4961
|
-
if buttons:
|
|
4962
|
-
filtered_button = list(filter(lambda x: x.text.strip().replace('\n', '') == button.strip().replace(' \n ', ''), buttons))
|
|
4963
|
-
|
|
4964
|
-
if filtered_button and len(filtered_button) - 1 >= position:
|
|
4965
|
-
parents_actives = list(filter(lambda x: self.filter_active_tabs(x), filtered_button ))
|
|
4966
|
-
if parents_actives:
|
|
4967
|
-
filtered_button = parents_actives
|
|
4968
|
-
next_button = filtered_button[position]
|
|
4969
|
-
else:
|
|
4970
|
-
filtered_button = list(filter(lambda x: (hasattr(x,'caption') and button.lower() in re.sub(regex,'',x['caption'].lower())) and 'focus' in x.get('class'), soup_objects ))
|
|
4971
|
-
|
|
4972
|
-
if not filtered_button:
|
|
4973
|
-
filtered_button = self.web_scrap(term=button, scrap_type=enum.ScrapType.MIXED, optional_term="wa-button", main_container = self.containers_selectors["SetButton"])
|
|
5017
|
+
next_button = self.get_shadowroot_button(button, term_button, position, check_error)
|
|
4974
5018
|
|
|
4975
5019
|
if next_button:
|
|
4976
5020
|
id_parent_element = next_button['id'] if hasattr(next_button, 'id') and type(next_button) == Tag else None
|
|
4977
5021
|
soup_element = self.soup_to_selenium(next_button) if type(next_button) == Tag else next_button
|
|
4978
5022
|
self.scroll_to_element(soup_element)
|
|
4979
|
-
soup_element = soup_element if self.element_is_displayed(soup_element) else None
|
|
5023
|
+
soup_element = soup_element if self.element_is_displayed(soup_element) and (not enforce_on_top or self.element_is_on_top(soup_element)) else None
|
|
4980
5024
|
if soup_element == None:
|
|
4981
5025
|
bodySoup = self.get_current_DOM().select('body')
|
|
4982
5026
|
self.driver.execute_script("arguments[0].style.cssText+='transform: scale(0.8)';", self.soup_to_selenium(bodySoup[0]))
|
|
4983
|
-
soup_element = soup_element if self.element_is_displayed(soup_element) else None
|
|
5027
|
+
soup_element = soup_element if self.element_is_displayed(soup_element) and (not enforce_on_top or self.element_is_on_top(soup_element)) else None
|
|
4984
5028
|
restore_zoom = True
|
|
4985
5029
|
|
|
4986
5030
|
else:
|
|
@@ -5038,6 +5082,7 @@ class WebappInternal(Base):
|
|
|
5038
5082
|
|
|
5039
5083
|
button_element_id = None
|
|
5040
5084
|
button_element_id = soup_element.get_attribute('id') or 'unknow'
|
|
5085
|
+
current_clicked_element = soup_element() if callable(soup_element) else soup_element
|
|
5041
5086
|
|
|
5042
5087
|
rows_box_state_before = []
|
|
5043
5088
|
rows = []
|
|
@@ -5056,46 +5101,90 @@ class WebappInternal(Base):
|
|
|
5056
5101
|
initial_layers = self.check_layers(".tmodaldialog, wa-dialog, wa-message-box, .ui-dialog")
|
|
5057
5102
|
popup_before = self.check_layers(".tmenupopupitem, wa-menu-popup")
|
|
5058
5103
|
|
|
5059
|
-
# Configurações de retry (fixas, sem novos parâmetros)
|
|
5060
|
-
max_click_attempts = 3
|
|
5061
5104
|
click_attempt = 0
|
|
5062
5105
|
click_verified = False
|
|
5106
|
+
recaptured = False
|
|
5107
|
+
# Primeira metade do time_out para a captura inicial; se o clique não for
|
|
5108
|
+
# verificado até aqui, recaptura o botão (container já atualizado após a
|
|
5109
|
+
# transição) e usa a metade restante.
|
|
5110
|
+
half_time = starttime + (self.config.time_out / 2)
|
|
5111
|
+
while not click_verified and ( time.time() < endtime):
|
|
5112
|
+
# Recaptura única, no meio do time_out, quando o clique ainda não foi
|
|
5113
|
+
# verificado — evita reutilizar um soup_element obsoleto capturado da
|
|
5114
|
+
# tela anterior durante uma transição.
|
|
5115
|
+
if not recaptured and time.time() >= half_time:
|
|
5116
|
+
logger().debug(
|
|
5117
|
+
f" [RECAPTURE] Click on '{button}' not verified within first half of time_out; "
|
|
5118
|
+
f"recapturing element (container may have changed after transition)"
|
|
5119
|
+
)
|
|
5120
|
+
new_element, new_restore_zoom = self.recapture_setbutton_element(button, term_button, position, check_error)
|
|
5121
|
+
if new_element is not None:
|
|
5122
|
+
soup_element = new_element
|
|
5123
|
+
restore_zoom_recaptured = new_restore_zoom
|
|
5124
|
+
try:
|
|
5125
|
+
button_element_id = soup_element.get_attribute('id') or 'unknow'
|
|
5126
|
+
except Exception:
|
|
5127
|
+
button_element_id = 'unknow'
|
|
5128
|
+
current_clicked_element = soup_element() if callable(soup_element) else soup_element
|
|
5129
|
+
logger().debug(f" [RECAPTURE] New element captured (id: {button_element_id})")
|
|
5130
|
+
else:
|
|
5131
|
+
logger().warning(f" [RECAPTURE] Could not recapture '{button}'; keeping previously captured element")
|
|
5132
|
+
recaptured = True
|
|
5133
|
+
|
|
5134
|
+
# Rebaseline do estado pré-clique após a transição, para que os
|
|
5135
|
+
# checks de verificação existentes comparem contra o estado correto.
|
|
5136
|
+
container_before_click = self.get_current_container()
|
|
5137
|
+
if container_before_click and 'id' in container_before_click.attrs:
|
|
5138
|
+
initial_container_id = container_before_click.attrs['id']
|
|
5139
|
+
initial_dom_hash = hash(str(self.get_current_DOM()))
|
|
5140
|
+
skip_focus_retry = True
|
|
5141
|
+
container_texts_before = self.get_current_container_texts()
|
|
5142
|
+
df_before, grids_on_screen_before = self.grid_dataframe(grid_number=0, wait=False, check_error=False, current_container=True, throw_error=False)
|
|
5143
|
+
rows = []
|
|
5144
|
+
rows_box_state_before = []
|
|
5145
|
+
if grids_on_screen_before:
|
|
5146
|
+
rows = self.execute_js_selector('tr', self.soup_to_selenium(grids_on_screen_before))
|
|
5147
|
+
if rows:
|
|
5148
|
+
rows_box_state_before = list(map(lambda x: self.get_row_divs_style(x), rows))
|
|
5149
|
+
initial_layers = self.check_layers(".tmodaldialog, wa-dialog, wa-message-box, .ui-dialog")
|
|
5150
|
+
popup_before = self.check_layers(".tmenupopupitem, wa-menu-popup")
|
|
5063
5151
|
|
|
5064
|
-
while click_attempt < max_click_attempts and not click_verified:
|
|
5065
5152
|
click_attempt += 1
|
|
5066
|
-
current_clicked_element = None
|
|
5067
|
-
|
|
5068
|
-
logger().debug(f"Click attempt {click_attempt}/{max_click_attempts} on '{button}' (id: {button_element_id} in container: {initial_container_id})")
|
|
5069
5153
|
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
# Small delay to ensure stability (avoids clicking during transitions)
|
|
5076
|
-
time.sleep(0.5)
|
|
5077
|
-
|
|
5078
|
-
if click_attempt == 1:
|
|
5079
|
-
self.send_action(action=self.click, element=lambda: soup_element)
|
|
5080
|
-
elif click_attempt == 2:
|
|
5081
|
-
logger().debug(" |-- Using ActionChains for greater robustness")
|
|
5082
|
-
self.click(soup_element, click_type=enum.ClickType.ACTIONCHAINS)
|
|
5083
|
-
else:
|
|
5084
|
-
logger().debug(" |-- Using JavaScript click as last resort")
|
|
5085
|
-
self.click(soup_element, click_type=enum.ClickType.JS)
|
|
5154
|
+
logger().debug(
|
|
5155
|
+
f"Click attempt {click_attempt} (recaptured={recaptured}) "
|
|
5156
|
+
f"on '{button}' (id: {button_element_id} in container: {initial_container_id})"
|
|
5157
|
+
)
|
|
5086
5158
|
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
while time.time() < endtime and not popup_item():
|
|
5090
|
-
self.click(soup_element)
|
|
5159
|
+
self.scroll_to_element(soup_element)
|
|
5160
|
+
self.set_element_focus(soup_element)
|
|
5091
5161
|
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
self.driver.execute_script("arguments[0].style.cssText+='transform: scale(1)';", self.soup_to_selenium(bodySoup[0]))
|
|
5095
|
-
soup_element = soup_element if self.element_is_displayed(soup_element) else None
|
|
5162
|
+
# Small delay to ensure stability (avoids clicking during transitions)
|
|
5163
|
+
time.sleep(0.5)
|
|
5096
5164
|
|
|
5097
|
-
|
|
5098
|
-
|
|
5165
|
+
# O send_action já cicla internamente entre JS, ActionChains e
|
|
5166
|
+
# Selenium, então uma única chamada cobre as técnicas de clique.
|
|
5167
|
+
# Limitamos o tempo para que o controle retorne a este laço e a
|
|
5168
|
+
# verificação/recaptura possam atuar: antes da recaptura, até o
|
|
5169
|
+
# half_time; depois, até o endtime restante (piso mínimo para
|
|
5170
|
+
# permitir ao menos um ciclo completo de tipos de clique).
|
|
5171
|
+
if not recaptured:
|
|
5172
|
+
send_action_timeout = half_time - time.time()
|
|
5173
|
+
else:
|
|
5174
|
+
send_action_timeout = endtime - time.time()
|
|
5175
|
+
if send_action_timeout < 10:
|
|
5176
|
+
send_action_timeout = 10
|
|
5177
|
+
self.send_action(action=self.click, element=lambda: soup_element, timeout=send_action_timeout)
|
|
5178
|
+
|
|
5179
|
+
if button.lower() == self.language.other_actions.lower():
|
|
5180
|
+
popup_item = lambda: self.wait_element_timeout(term=".tmenupopupitem, wa-menu-popup", scrap_type=enum.ScrapType.CSS_SELECTOR, main_container="body", check_error=False)
|
|
5181
|
+
while time.time() < endtime and not popup_item():
|
|
5182
|
+
self.click(soup_element)
|
|
5183
|
+
|
|
5184
|
+
if restore_zoom or restore_zoom_recaptured:
|
|
5185
|
+
bodySoup = self.get_current_DOM().select('body')
|
|
5186
|
+
self.driver.execute_script("arguments[0].style.cssText+='transform: scale(1)';", self.soup_to_selenium(bodySoup[0]))
|
|
5187
|
+
soup_element = soup_element if self.element_is_displayed(soup_element) else None
|
|
5099
5188
|
|
|
5100
5189
|
# Verification: waits up to 3s to detect click effect
|
|
5101
5190
|
verification_timeout = time.time() + 3
|
|
@@ -5204,7 +5293,7 @@ class WebappInternal(Base):
|
|
|
5204
5293
|
except Exception:
|
|
5205
5294
|
pass
|
|
5206
5295
|
|
|
5207
|
-
if not click_verified
|
|
5296
|
+
if not click_verified:
|
|
5208
5297
|
logger().warning(f" [WARN] Click on '{button}' had no detectable effect on attempt {click_attempt}")
|
|
5209
5298
|
time.sleep(0.5)
|
|
5210
5299
|
|
|
@@ -5213,7 +5302,7 @@ class WebappInternal(Base):
|
|
|
5213
5302
|
logger().debug(f"Container after click '{button}': {current_container_id}")
|
|
5214
5303
|
|
|
5215
5304
|
if not click_verified:
|
|
5216
|
-
logger().warning(f" [WARN] Click on '{button}' may not have been effective after {
|
|
5305
|
+
logger().warning(f" [WARN] Click on '{button}' may not have been effective after {click_attempt} attempts (recaptured={recaptured}). Continuing execution...")
|
|
5217
5306
|
|
|
5218
5307
|
if sub_item and ',' not in sub_item:
|
|
5219
5308
|
logger().info(f"Clicking on {sub_item}")
|
|
@@ -5317,6 +5406,54 @@ class WebappInternal(Base):
|
|
|
5317
5406
|
self.reset_container_position()
|
|
5318
5407
|
|
|
5319
5408
|
|
|
5409
|
+
def recapture_setbutton_element(self, button, term_button, position, check_error=True):
|
|
5410
|
+
"""
|
|
5411
|
+
[Internal]
|
|
5412
|
+
|
|
5413
|
+
Re-captures the SetButton target element using the same capture logic as the
|
|
5414
|
+
initial capture in :meth:`SetButton` (respecting ``webapp_shadowroot()``,
|
|
5415
|
+
``restore_zoom``, ``position`` and ``element_is_displayed`` filtering, with the
|
|
5416
|
+
fallback without ``filter_blocked_containers``).
|
|
5417
|
+
|
|
5418
|
+
This is used by ``SetButton`` when the initial single capture may have grabbed a
|
|
5419
|
+
stale element from the previous container during a screen transition. After the
|
|
5420
|
+
transition completes the container is already updated, so recapturing returns the
|
|
5421
|
+
correct element.
|
|
5422
|
+
|
|
5423
|
+
:return: Tuple ``(soup_element, restore_zoom)``. ``soup_element`` is a Selenium
|
|
5424
|
+
element (or ``None`` when not found) and ``restore_zoom`` indicates that a
|
|
5425
|
+
zoom-out was applied to make the element visible.
|
|
5426
|
+
:rtype: tuple
|
|
5427
|
+
"""
|
|
5428
|
+
soup_element = None
|
|
5429
|
+
restore_zoom = False
|
|
5430
|
+
try:
|
|
5431
|
+
if self.webapp_shadowroot():
|
|
5432
|
+
next_button = self.get_shadowroot_button(button, term_button, position, check_error)
|
|
5433
|
+
if next_button:
|
|
5434
|
+
soup_element = self.soup_to_selenium(next_button) if type(next_button) == Tag else next_button
|
|
5435
|
+
if soup_element is not None:
|
|
5436
|
+
self.scroll_to_element(soup_element)
|
|
5437
|
+
soup_element = soup_element if self.element_is_displayed(soup_element) else None
|
|
5438
|
+
if soup_element is None and next_button is not None:
|
|
5439
|
+
bodySoup = self.get_current_DOM().select('body')
|
|
5440
|
+
self.driver.execute_script("arguments[0].style.cssText+='transform: scale(0.8)';", self.soup_to_selenium(bodySoup[0]))
|
|
5441
|
+
soup_element = self.soup_to_selenium(next_button) if type(next_button) == Tag else next_button
|
|
5442
|
+
soup_element = soup_element if self.element_is_displayed(soup_element) else None
|
|
5443
|
+
restore_zoom = True
|
|
5444
|
+
else:
|
|
5445
|
+
soup_objects = self.web_scrap(term=button, scrap_type=enum.ScrapType.MIXED, optional_term="button, .thbutton", main_container=self.containers_selectors["SetButton"], check_error=check_error)
|
|
5446
|
+
if isinstance(soup_objects, list):
|
|
5447
|
+
soup_objects = list(filter(lambda x: self.element_is_displayed(x), soup_objects))
|
|
5448
|
+
if soup_objects and len(soup_objects) - 1 >= position:
|
|
5449
|
+
self.wait_until_to(expected_condition="element_to_be_clickable", element=soup_objects[position], locator=By.XPATH, timeout=True)
|
|
5450
|
+
soup_element = self.soup_to_selenium(soup_objects[position])
|
|
5451
|
+
except Exception as e:
|
|
5452
|
+
logger().debug(f"Recapture of button '{button}' failed: {e}")
|
|
5453
|
+
soup_element = None
|
|
5454
|
+
return soup_element, restore_zoom
|
|
5455
|
+
|
|
5456
|
+
|
|
5320
5457
|
def get_current_container_texts(self):
|
|
5321
5458
|
"""This method returns a list of all texts from current container descendents
|
|
5322
5459
|
"""
|
|
@@ -7409,7 +7546,7 @@ class WebappInternal(Base):
|
|
|
7409
7546
|
cell_filled = self.select_combo(selenium_input(), user_value)
|
|
7410
7547
|
time.sleep(1)
|
|
7411
7548
|
# if modal opened close it
|
|
7412
|
-
if current_layers()
|
|
7549
|
+
if current_layers() == initial_layers + 1:
|
|
7413
7550
|
self.send_keys(selenium_column, Keys.TAB)
|
|
7414
7551
|
else:
|
|
7415
7552
|
if cell_opened:
|
|
@@ -8706,22 +8843,6 @@ class WebappInternal(Base):
|
|
|
8706
8843
|
while( element_selenium and time.time() < endtime and self.switch_to_active_element() != element_selenium() ):
|
|
8707
8844
|
time.sleep(step)
|
|
8708
8845
|
|
|
8709
|
-
def wait_element_is_not_focused(self, element_selenium = None, time_out = 5, step = 0.1):
|
|
8710
|
-
"""
|
|
8711
|
-
[Internal]
|
|
8712
|
-
|
|
8713
|
-
Waits until the element loses focus.
|
|
8714
|
-
|
|
8715
|
-
:param element_selenium: Lambda or callable returning the Selenium element to check.
|
|
8716
|
-
:param time_out: Maximum time in seconds to wait. - **Default:** 5
|
|
8717
|
-
:type time_out: float
|
|
8718
|
-
:param step: Time in seconds between each check. - **Default:** 0.1
|
|
8719
|
-
:type step: float
|
|
8720
|
-
"""
|
|
8721
|
-
endtime = time.time() + time_out
|
|
8722
|
-
while( element_selenium and time.time() < endtime and self.switch_to_active_element() == element_selenium() ):
|
|
8723
|
-
time.sleep(step)
|
|
8724
|
-
|
|
8725
8846
|
def switch_to_active_element(self):
|
|
8726
8847
|
"""
|
|
8727
8848
|
[Internal]
|
|
@@ -10845,20 +10966,59 @@ class WebappInternal(Base):
|
|
|
10845
10966
|
"""
|
|
10846
10967
|
[Internal]
|
|
10847
10968
|
|
|
10969
|
+
"""
|
|
10970
|
+
try:
|
|
10971
|
+
if type(element) == Tag:
|
|
10972
|
+
element_selenium = self.soup_to_selenium(element, twebview)
|
|
10973
|
+
else:
|
|
10974
|
+
element_selenium = element
|
|
10975
|
+
|
|
10976
|
+
if isinstance(element, list):
|
|
10977
|
+
call_stack = list(filter(lambda x: 'webapp_internal.py' == x.filename.split(self.replace_slash('\\'))[-1], inspect.stack()))
|
|
10978
|
+
for n in call_stack: logger().debug(f'element_is_displayed Error: {str(n.function)}')
|
|
10979
|
+
element_selenium = next(iter(element),None)
|
|
10980
|
+
|
|
10981
|
+
if element_selenium:
|
|
10982
|
+
return element_selenium.is_displayed()
|
|
10983
|
+
else:
|
|
10984
|
+
return False
|
|
10985
|
+
except Exception as e:
|
|
10986
|
+
logger().debug(f'element_is_displayed exception: {str(e)}')
|
|
10987
|
+
return False
|
|
10988
|
+
|
|
10989
|
+
|
|
10990
|
+
def element_is_on_top(self, element=None, twebview=False):
|
|
10991
|
+
"""
|
|
10992
|
+
[Internal]
|
|
10993
|
+
|
|
10994
|
+
Checks whether the element is actually the topmost element at its center
|
|
10995
|
+
point, i.e. it is not covered/intercepted by another element (which would
|
|
10996
|
+
cause an ElementClickInterceptedException). Uses document.elementFromPoint
|
|
10997
|
+
via JavaScript, since is_displayed() may return false positives.
|
|
10848
10998
|
"""
|
|
10849
10999
|
if type(element) == Tag:
|
|
10850
11000
|
element_selenium = self.soup_to_selenium(element, twebview)
|
|
11001
|
+
elif isinstance(element, list):
|
|
11002
|
+
element_selenium = next(iter(element), None)
|
|
10851
11003
|
else:
|
|
10852
11004
|
element_selenium = element
|
|
10853
11005
|
|
|
10854
|
-
if
|
|
10855
|
-
|
|
10856
|
-
for n in call_stack: logger().debug(f'element_is_displayed Error: {str(n.function)}')
|
|
10857
|
-
element_selenium = next(iter(element),None)
|
|
11006
|
+
if not element_selenium:
|
|
11007
|
+
return False
|
|
10858
11008
|
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
11009
|
+
try:
|
|
11010
|
+
is_on_top = bool(self.driver.execute_script("""
|
|
11011
|
+
const el = arguments[0];
|
|
11012
|
+
if (!el) return false;
|
|
11013
|
+
const r = el.getBoundingClientRect();
|
|
11014
|
+
if (r.width === 0 || r.height === 0) return false;
|
|
11015
|
+
const topo = document.elementFromPoint(r.left + r.width/2, r.top + r.height/2);
|
|
11016
|
+
return topo === el || el.contains(topo);
|
|
11017
|
+
""", element_selenium))
|
|
11018
|
+
logger().debug(f"element_is_on_top return: {is_on_top}")
|
|
11019
|
+
return is_on_top
|
|
11020
|
+
except Exception as e:
|
|
11021
|
+
logger().debug(f"element_is_on_top Error: {str(e)}")
|
|
10862
11022
|
return False
|
|
10863
11023
|
|
|
10864
11024
|
|
|
@@ -11650,7 +11810,7 @@ class WebappInternal(Base):
|
|
|
11650
11810
|
else:
|
|
11651
11811
|
self.log_error("Doesn't contain that key in json object")
|
|
11652
11812
|
|
|
11653
|
-
def send_action(self, action = None, element = None, value = None, right_click=False, click_type=None, wait_change=True):
|
|
11813
|
+
def send_action(self, action = None, element = None, value = None, right_click=False, click_type=None, wait_change=True, timeout=None):
|
|
11654
11814
|
"""
|
|
11655
11815
|
|
|
11656
11816
|
Sends an action to element and compare it object state change.
|
|
@@ -11661,6 +11821,12 @@ class WebappInternal(Base):
|
|
|
11661
11821
|
:param right_click: True if you want a right click
|
|
11662
11822
|
:param click_type: ClickType enum. 1-3 types- **Default:** None
|
|
11663
11823
|
:type click_type: int
|
|
11824
|
+
:param timeout: Optional upper bound (in seconds) for the internal retry loop.
|
|
11825
|
+
When ``None`` (default) the full ``self.config.time_out`` is used,
|
|
11826
|
+
preserving the original behavior. Callers such as ``SetButton`` pass
|
|
11827
|
+
a bounded value so control returns to their own retry/recapture loop
|
|
11828
|
+
instead of blocking here for the entire ``time_out``.
|
|
11829
|
+
:type timeout: float
|
|
11664
11830
|
:return: True if there was a change in the object
|
|
11665
11831
|
"""
|
|
11666
11832
|
|
|
@@ -11699,7 +11865,8 @@ class WebappInternal(Base):
|
|
|
11699
11865
|
|
|
11700
11866
|
click_type = 1 if not main_click_type else click_type
|
|
11701
11867
|
|
|
11702
|
-
|
|
11868
|
+
action_timeout = timeout if timeout is not None else self.config.time_out
|
|
11869
|
+
endtime = time.time() + action_timeout
|
|
11703
11870
|
try:
|
|
11704
11871
|
while ((time.time() < endtime) and not check_changed()):
|
|
11705
11872
|
logger().debug(f"Trying to send action")
|
|
@@ -11716,7 +11883,7 @@ class WebappInternal(Base):
|
|
|
11716
11883
|
action(click_type=enum.ClickType(click_type), element=element())
|
|
11717
11884
|
elif action:
|
|
11718
11885
|
action()
|
|
11719
|
-
|
|
11886
|
+
|
|
11720
11887
|
time.sleep(1)
|
|
11721
11888
|
|
|
11722
11889
|
if soup_select:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.14.4'
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '2.14.2'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/linux64/geckodriver
RENAMED
|
File without changes
|
{tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/windows/chromedriver.exe
RENAMED
|
File without changes
|
|
File without changes
|
{tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/windows/geckodriver.exe
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/third_party/screen_size.py
RENAMED
|
File without changes
|
{tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/third_party/xpath_soup.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|