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.
Files changed (40) hide show
  1. {tir_framework-2.14.2/tir_framework.egg-info → tir_framework-2.14.4}/PKG-INFO +1 -1
  2. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/base.py +11 -2
  3. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/poui_internal.py +86 -2
  4. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/webapp_internal.py +264 -97
  5. tir_framework-2.14.4/tir/version.py +1 -0
  6. {tir_framework-2.14.2 → tir_framework-2.14.4/tir_framework.egg-info}/PKG-INFO +1 -1
  7. tir_framework-2.14.2/tir/version.py +0 -1
  8. {tir_framework-2.14.2 → tir_framework-2.14.4}/LICENSE +0 -0
  9. {tir_framework-2.14.2 → tir_framework-2.14.4}/MANIFEST.in +0 -0
  10. {tir_framework-2.14.2 → tir_framework-2.14.4}/README.md +0 -0
  11. {tir_framework-2.14.2 → tir_framework-2.14.4}/setup.cfg +0 -0
  12. {tir_framework-2.14.2 → tir_framework-2.14.4}/setup.py +0 -0
  13. {tir_framework-2.14.2 → tir_framework-2.14.4}/tests/test_log.py +0 -0
  14. {tir_framework-2.14.2 → tir_framework-2.14.4}/tests/test_utils.py +0 -0
  15. {tir_framework-2.14.2 → tir_framework-2.14.4}/tests/test_webapp_internal_clicktree.py +0 -0
  16. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/__init__.py +0 -0
  17. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/main.py +0 -0
  18. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/apw_internal.py +0 -0
  19. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/base_database.py +0 -0
  20. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/config.py +0 -0
  21. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/data/sx3.csv +0 -0
  22. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/linux64/geckodriver +0 -0
  23. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/windows/chromedriver.exe +0 -0
  24. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/windows/electron/chromedriver.exe +0 -0
  25. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/drivers/windows/geckodriver.exe +0 -0
  26. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/enumerations.py +0 -0
  27. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/events.py +0 -0
  28. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/language.py +0 -0
  29. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/log.py +0 -0
  30. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/logging_config.py +0 -0
  31. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/numexec.py +0 -0
  32. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/psutil_info.py +0 -0
  33. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/router.py +0 -0
  34. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/third_party/screen_size.py +0 -0
  35. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/third_party/xpath_soup.py +0 -0
  36. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir/technologies/core/utils.py +0 -0
  37. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir_framework.egg-info/SOURCES.txt +0 -0
  38. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir_framework.egg-info/dependency_links.txt +0 -0
  39. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir_framework.egg-info/requires.txt +0 -0
  40. {tir_framework-2.14.2 → tir_framework-2.14.4}/tir_framework.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tir_framework
3
- Version: 2.14.2
3
+ Version: 2.14.4
4
4
  Summary: Test Interface Robot
5
5
  Home-page: https://github.com/totvs/tir
6
6
  Download-URL: https://github.com/totvs/tir
@@ -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
- self.driver.switch_to.frame(self.find_shadow_element('iframe', filtered_iframe)[0]) if self.webapp_shadowroot() else self.driver.switch_to.frame(filtered_iframe)
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._po_loading()
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
- initial_program = ['sigaadv', 'sigamdi']
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.wait_element_timeout(term=button, scrap_type=enum.ScrapType.MIXED, optional_term=term_button, timeout=10, step=0.1, check_error=check_error)
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
- if self.webapp_shadowroot():
5071
- self.scroll_to_element(soup_element)
5072
- self.set_element_focus(soup_element)
5073
- current_clicked_element = soup_element
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
- if button.lower() == self.language.other_actions.lower():
5088
- popup_item = lambda: self.wait_element_timeout(term=".tmenupopupitem, wa-menu-popup", scrap_type=enum.ScrapType.CSS_SELECTOR, main_container="body", check_error=False)
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
- if restore_zoom:
5093
- bodySoup = self.get_current_DOM().select('body')
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
- if current_clicked_element is not None:
5098
- self.wait_element_is_not_focused(lambda: current_clicked_element)
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 and click_attempt < max_click_attempts:
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 {max_click_attempts} attempts. Continuing execution...")
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() > initial_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 isinstance(element, list):
10855
- call_stack = list(filter(lambda x: 'webapp_internal.py' == x.filename.split(self.replace_slash('\\'))[-1], inspect.stack()))
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
- if element_selenium:
10860
- return element_selenium.is_displayed()
10861
- else:
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
- endtime = time.time() + self.config.time_out
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tir_framework
3
- Version: 2.14.2
3
+ Version: 2.14.4
4
4
  Summary: Test Interface Robot
5
5
  Home-page: https://github.com/totvs/tir
6
6
  Download-URL: https://github.com/totvs/tir
@@ -1 +0,0 @@
1
- __version__ = '2.14.2'
File without changes
File without changes
File without changes
File without changes