tir-framework 2.14.2__tar.gz → 2.14.3__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.3}/PKG-INFO +1 -1
  2. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/base.py +9 -2
  3. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/webapp_internal.py +168 -41
  4. tir_framework-2.14.3/tir/version.py +1 -0
  5. {tir_framework-2.14.2 → tir_framework-2.14.3/tir_framework.egg-info}/PKG-INFO +1 -1
  6. tir_framework-2.14.2/tir/version.py +0 -1
  7. {tir_framework-2.14.2 → tir_framework-2.14.3}/LICENSE +0 -0
  8. {tir_framework-2.14.2 → tir_framework-2.14.3}/MANIFEST.in +0 -0
  9. {tir_framework-2.14.2 → tir_framework-2.14.3}/README.md +0 -0
  10. {tir_framework-2.14.2 → tir_framework-2.14.3}/setup.cfg +0 -0
  11. {tir_framework-2.14.2 → tir_framework-2.14.3}/setup.py +0 -0
  12. {tir_framework-2.14.2 → tir_framework-2.14.3}/tests/test_log.py +0 -0
  13. {tir_framework-2.14.2 → tir_framework-2.14.3}/tests/test_utils.py +0 -0
  14. {tir_framework-2.14.2 → tir_framework-2.14.3}/tests/test_webapp_internal_clicktree.py +0 -0
  15. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/__init__.py +0 -0
  16. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/main.py +0 -0
  17. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/apw_internal.py +0 -0
  18. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/base_database.py +0 -0
  19. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/config.py +0 -0
  20. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/data/sx3.csv +0 -0
  21. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/drivers/linux64/geckodriver +0 -0
  22. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/drivers/windows/chromedriver.exe +0 -0
  23. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/drivers/windows/electron/chromedriver.exe +0 -0
  24. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/drivers/windows/geckodriver.exe +0 -0
  25. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/enumerations.py +0 -0
  26. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/events.py +0 -0
  27. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/language.py +0 -0
  28. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/log.py +0 -0
  29. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/logging_config.py +0 -0
  30. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/numexec.py +0 -0
  31. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/psutil_info.py +0 -0
  32. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/router.py +0 -0
  33. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/third_party/screen_size.py +0 -0
  34. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/third_party/xpath_soup.py +0 -0
  35. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/core/utils.py +0 -0
  36. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir/technologies/poui_internal.py +0 -0
  37. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir_framework.egg-info/SOURCES.txt +0 -0
  38. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir_framework.egg-info/dependency_links.txt +0 -0
  39. {tir_framework-2.14.2 → tir_framework-2.14.3}/tir_framework.egg-info/requires.txt +0 -0
  40. {tir_framework-2.14.2 → tir_framework-2.14.3}/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.3
4
4
  Summary: Test Interface Robot
5
5
  Home-page: https://github.com/totvs/tir
6
6
  Download-URL: https://github.com/totvs/tir
@@ -586,7 +586,14 @@ class Base(unittest.TestCase):
586
586
  tries += 1
587
587
 
588
588
  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)
589
+ if self.webapp_shadowroot():
590
+ shadow_iframe = self.find_shadow_element('iframe', filtered_iframe)
591
+ if shadow_iframe:
592
+ self.driver.switch_to.frame(shadow_iframe[0])
593
+ else:
594
+ self.driver.switch_to.frame(filtered_iframe)
595
+ else:
596
+ self.driver.switch_to.frame(filtered_iframe)
590
597
 
591
598
 
592
599
  def filter_active_iframe(self, iframes):
@@ -1632,7 +1639,7 @@ class Base(unittest.TestCase):
1632
1639
 
1633
1640
  # Filter blocked elements only when WaitProcessing is not in the stack
1634
1641
  # and blocked-container filtering is enabled.
1635
- if not self.search_stack('WaitProcessing') and self.filter_blocked_containers:
1642
+ if not self.search_stack('WaitProcessing') and not self.search_stack('IfExists') and self.filter_blocked_containers:
1636
1643
  non_blocked_elements = list(filter(lambda x: hasattr(x, 'attr') and 'blocked' not in x.attrs, elements))
1637
1644
 
1638
1645
  if isinstance(non_blocked_elements, list):
@@ -1997,6 +1997,7 @@ class WebappInternal(Base):
1997
1997
  logger().debug(f'wa-dialog layers: {container_layers}')
1998
1998
 
1999
1999
  if not success:
2000
+ self.restart_counter += 1
2000
2001
  self.log_error('Home screen not found!')
2001
2002
 
2002
2003
  # wait trasitions between screens to avoid errors in layers number
@@ -4862,6 +4863,68 @@ class WebappInternal(Base):
4862
4863
  logger().warning(f"Warning SetLateralMenu click last item method exception: {str(e)} ")
4863
4864
 
4864
4865
 
4866
+ def get_shadowroot_button(self, button, term_button, position, check_error=True):
4867
+ """
4868
+ [Internal]
4869
+
4870
+ Captures the button element inside a shadowroot container.
4871
+
4872
+ :param button: Button caption to be located.
4873
+ :type button: str
4874
+ :param term_button: CSS selector used to locate the button candidates.
4875
+ :type term_button: str
4876
+ :param position: Index of the button among the filtered candidates.
4877
+ :type position: int
4878
+ :param check_error: Whether errors should be checked while waiting. - **Default:** True
4879
+ :type check_error: bool
4880
+
4881
+ :return: The located button element (Tag or Selenium element) or None if not found.
4882
+
4883
+ Usage:
4884
+
4885
+ >>> next_button = self.get_shadowroot_button(button, term_button, position, check_error)
4886
+ """
4887
+ regex = r"(<[^>]*>)?"
4888
+ next_button = None
4889
+ filtered_buttons = []
4890
+
4891
+ self.wait_element_timeout(term=button, scrap_type=enum.ScrapType.MIXED, optional_term=term_button, timeout=10, step=0.1, check_error=check_error)
4892
+ self.containers_selectors["GetCurrentContainer"] = "wa-dialog, wa-message-box,.tmodaldialog, body"
4893
+ container = self.get_current_container()
4894
+ button_candidates = container.select(term_button)
4895
+
4896
+ if button_candidates:
4897
+ 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))
4898
+
4899
+ if not filtered_buttons:
4900
+ filtered_buttons = self.return_soup_by_selenium(elements=button_candidates, term=button, selectors='label, span')
4901
+ else:
4902
+ footer = self.execute_js_selector('footer', self.soup_to_selenium(container), get_all=False)
4903
+ if footer:
4904
+ footer_buttons = self.execute_js_selector("wa-button", footer)
4905
+ if not footer_buttons:
4906
+ footer_buttons = footer.find_elements(By.CSS_SELECTOR, "wa-button")
4907
+ if footer_buttons:
4908
+ filtered_buttons = list(filter(lambda x: x.text.strip().replace('\n', '') == button.strip().replace(' \n ', ''), footer_buttons))
4909
+
4910
+ # Fallback: botao em foco entre os candidatos
4911
+ if not (filtered_buttons and len(filtered_buttons) - 1 >= position):
4912
+ 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))
4913
+ if focus_buttons:
4914
+ filtered_buttons = focus_buttons
4915
+
4916
+ # Fallback: busca ampla via web_scrap
4917
+ if not filtered_buttons:
4918
+ filtered_buttons = self.web_scrap(term=button, scrap_type=enum.ScrapType.MIXED, optional_term="wa-button", main_container=self.containers_selectors["SetButton"])
4919
+
4920
+ if filtered_buttons and len(filtered_buttons) - 1 >= position:
4921
+ parents_actives = list(filter(lambda x: self.filter_active_tabs(x), filtered_buttons))
4922
+ if parents_actives:
4923
+ filtered_buttons = parents_actives
4924
+ next_button = filtered_buttons[position]
4925
+
4926
+ return next_button
4927
+
4865
4928
  def SetButton(self, button, sub_item="", position=1, check_error=True):
4866
4929
  """
4867
4930
  Method that clicks on a button on the screen.
@@ -4935,42 +4998,10 @@ class WebappInternal(Base):
4935
4998
  logger().debug(f"***System Info*** Before Clicking on button:{button}")
4936
4999
  system_info()
4937
5000
 
4938
- regex = r"(<[^>]*>)?"
4939
- filtered_button = []
4940
5001
  next_button = None
4941
5002
  while(time.time() < endtime and not soup_element):
4942
5003
  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"])
5004
+ next_button = self.get_shadowroot_button(button, term_button, position, check_error)
4974
5005
 
4975
5006
  if next_button:
4976
5007
  id_parent_element = next_button['id'] if hasattr(next_button, 'id') and type(next_button) == Tag else None
@@ -5056,16 +5087,64 @@ class WebappInternal(Base):
5056
5087
  initial_layers = self.check_layers(".tmodaldialog, wa-dialog, wa-message-box, .ui-dialog")
5057
5088
  popup_before = self.check_layers(".tmenupopupitem, wa-menu-popup")
5058
5089
 
5059
- # Configurações de retry (fixas, sem novos parâmetros)
5060
5090
  max_click_attempts = 3
5061
5091
  click_attempt = 0
5062
5092
  click_verified = False
5093
+ recaptured = False
5094
+ # Primeira metade do time_out para a captura inicial; se o clique não for
5095
+ # verificado até aqui, recaptura o botão (container já atualizado após a
5096
+ # transição) e usa a metade restante.
5097
+ half_time = starttime + (self.config.time_out / 2)
5098
+ while not click_verified and (click_attempt < max_click_attempts or time.time() < endtime):
5099
+ # Recaptura única, no meio do time_out, quando o clique ainda não foi
5100
+ # verificado — evita reutilizar um soup_element obsoleto capturado da
5101
+ # tela anterior durante uma transição.
5102
+ if not recaptured and time.time() >= half_time:
5103
+ logger().debug(
5104
+ f" [RECAPTURE] Click on '{button}' not verified within first half of time_out; "
5105
+ f"recapturing element (container may have changed after transition)"
5106
+ )
5107
+ new_element, new_restore_zoom = self.recapture_setbutton_element(button, term_button, position, check_error)
5108
+ if new_element is not None:
5109
+ soup_element = new_element
5110
+ restore_zoom = new_restore_zoom
5111
+ try:
5112
+ button_element_id = soup_element.get_attribute('id') or 'unknow'
5113
+ except Exception:
5114
+ button_element_id = 'unknow'
5115
+ logger().debug(f" [RECAPTURE] New element captured (id: {button_element_id})")
5116
+ else:
5117
+ logger().warning(f" [RECAPTURE] Could not recapture '{button}'; keeping previously captured element")
5118
+ recaptured = True
5119
+
5120
+ # Rebaseline do estado pré-clique após a transição, para que os
5121
+ # checks de verificação existentes comparem contra o estado correto.
5122
+ container_before_click = self.get_current_container()
5123
+ if container_before_click and 'id' in container_before_click.attrs:
5124
+ initial_container_id = container_before_click.attrs['id']
5125
+ initial_dom_hash = hash(str(self.get_current_DOM()))
5126
+ skip_focus_retry = True
5127
+ container_texts_before = self.get_current_container_texts()
5128
+ df_before, grids_on_screen_before = self.grid_dataframe(grid_number=0, wait=False, check_error=False, current_container=True, throw_error=False)
5129
+ rows = []
5130
+ rows_box_state_before = []
5131
+ if grids_on_screen_before:
5132
+ rows = self.execute_js_selector('tr', self.soup_to_selenium(grids_on_screen_before))
5133
+ if rows:
5134
+ rows_box_state_before = list(map(lambda x: self.get_row_divs_style(x), rows))
5135
+ initial_layers = self.check_layers(".tmodaldialog, wa-dialog, wa-message-box, .ui-dialog")
5136
+ popup_before = self.check_layers(".tmenupopupitem, wa-menu-popup")
5063
5137
 
5064
- while click_attempt < max_click_attempts and not click_verified:
5065
5138
  click_attempt += 1
5139
+ # Estratégia progressiva: avança ao longo das iterações do laço e NÃO
5140
+ # reinicia na recaptura; após a última, mantém a mais robusta (JS).
5141
+ strategy_index = min(click_attempt - 1, max_click_attempts - 1)
5066
5142
  current_clicked_element = None
5067
5143
 
5068
- logger().debug(f"Click attempt {click_attempt}/{max_click_attempts} on '{button}' (id: {button_element_id} in container: {initial_container_id})")
5144
+ logger().debug(
5145
+ f"Click attempt {click_attempt} (strategy {strategy_index}, recaptured={recaptured}) "
5146
+ f"on '{button}' (id: {button_element_id} in container: {initial_container_id})"
5147
+ )
5069
5148
 
5070
5149
  if self.webapp_shadowroot():
5071
5150
  self.scroll_to_element(soup_element)
@@ -5075,9 +5154,9 @@ class WebappInternal(Base):
5075
5154
  # Small delay to ensure stability (avoids clicking during transitions)
5076
5155
  time.sleep(0.5)
5077
5156
 
5078
- if click_attempt == 1:
5157
+ if strategy_index == 0:
5079
5158
  self.send_action(action=self.click, element=lambda: soup_element)
5080
- elif click_attempt == 2:
5159
+ elif strategy_index == 1:
5081
5160
  logger().debug(" |-- Using ActionChains for greater robustness")
5082
5161
  self.click(soup_element, click_type=enum.ClickType.ACTIONCHAINS)
5083
5162
  else:
@@ -5204,7 +5283,7 @@ class WebappInternal(Base):
5204
5283
  except Exception:
5205
5284
  pass
5206
5285
 
5207
- if not click_verified and click_attempt < max_click_attempts:
5286
+ if not click_verified:
5208
5287
  logger().warning(f" [WARN] Click on '{button}' had no detectable effect on attempt {click_attempt}")
5209
5288
  time.sleep(0.5)
5210
5289
 
@@ -5213,7 +5292,7 @@ class WebappInternal(Base):
5213
5292
  logger().debug(f"Container after click '{button}': {current_container_id}")
5214
5293
 
5215
5294
  if not click_verified:
5216
- logger().warning(f" [WARN] Click on '{button}' may not have been effective after {max_click_attempts} attempts. Continuing execution...")
5295
+ logger().warning(f" [WARN] Click on '{button}' may not have been effective after {click_attempt} attempts (recaptured={recaptured}). Continuing execution...")
5217
5296
 
5218
5297
  if sub_item and ',' not in sub_item:
5219
5298
  logger().info(f"Clicking on {sub_item}")
@@ -5317,6 +5396,54 @@ class WebappInternal(Base):
5317
5396
  self.reset_container_position()
5318
5397
 
5319
5398
 
5399
+ def recapture_setbutton_element(self, button, term_button, position, check_error=True):
5400
+ """
5401
+ [Internal]
5402
+
5403
+ Re-captures the SetButton target element using the same capture logic as the
5404
+ initial capture in :meth:`SetButton` (respecting ``webapp_shadowroot()``,
5405
+ ``restore_zoom``, ``position`` and ``element_is_displayed`` filtering, with the
5406
+ fallback without ``filter_blocked_containers``).
5407
+
5408
+ This is used by ``SetButton`` when the initial single capture may have grabbed a
5409
+ stale element from the previous container during a screen transition. After the
5410
+ transition completes the container is already updated, so recapturing returns the
5411
+ correct element.
5412
+
5413
+ :return: Tuple ``(soup_element, restore_zoom)``. ``soup_element`` is a Selenium
5414
+ element (or ``None`` when not found) and ``restore_zoom`` indicates that a
5415
+ zoom-out was applied to make the element visible.
5416
+ :rtype: tuple
5417
+ """
5418
+ soup_element = None
5419
+ restore_zoom = False
5420
+ try:
5421
+ if self.webapp_shadowroot():
5422
+ next_button = self.get_shadowroot_button(button, term_button, position, check_error)
5423
+ if next_button:
5424
+ soup_element = self.soup_to_selenium(next_button) if type(next_button) == Tag else next_button
5425
+ if soup_element is not None:
5426
+ self.scroll_to_element(soup_element)
5427
+ soup_element = soup_element if self.element_is_displayed(soup_element) else None
5428
+ if soup_element is None and next_button is not None:
5429
+ bodySoup = self.get_current_DOM().select('body')
5430
+ self.driver.execute_script("arguments[0].style.cssText+='transform: scale(0.8)';", self.soup_to_selenium(bodySoup[0]))
5431
+ soup_element = self.soup_to_selenium(next_button) if type(next_button) == Tag else next_button
5432
+ soup_element = soup_element if self.element_is_displayed(soup_element) else None
5433
+ restore_zoom = True
5434
+ else:
5435
+ 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)
5436
+ if isinstance(soup_objects, list):
5437
+ soup_objects = list(filter(lambda x: self.element_is_displayed(x), soup_objects))
5438
+ if soup_objects and len(soup_objects) - 1 >= position:
5439
+ self.wait_until_to(expected_condition="element_to_be_clickable", element=soup_objects[position], locator=By.XPATH, timeout=True)
5440
+ soup_element = self.soup_to_selenium(soup_objects[position])
5441
+ except Exception as e:
5442
+ logger().debug(f"Recapture of button '{button}' failed: {e}")
5443
+ soup_element = None
5444
+ return soup_element, restore_zoom
5445
+
5446
+
5320
5447
  def get_current_container_texts(self):
5321
5448
  """This method returns a list of all texts from current container descendents
5322
5449
  """
@@ -7409,7 +7536,7 @@ class WebappInternal(Base):
7409
7536
  cell_filled = self.select_combo(selenium_input(), user_value)
7410
7537
  time.sleep(1)
7411
7538
  # if modal opened close it
7412
- if current_layers() > initial_layers:
7539
+ if current_layers() == initial_layers + 1:
7413
7540
  self.send_keys(selenium_column, Keys.TAB)
7414
7541
  else:
7415
7542
  if cell_opened:
@@ -0,0 +1 @@
1
+ __version__ = '2.14.3'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tir_framework
3
- Version: 2.14.2
3
+ Version: 2.14.3
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