tir-framework 2.6.0__tar.gz → 2.7.0__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.6.0/tir_framework.egg-info → tir_framework-2.7.0}/PKG-INFO +2 -2
  2. {tir_framework-2.6.0 → tir_framework-2.7.0}/README.md +1 -1
  3. tir_framework-2.7.0/tests/test_utils.py +238 -0
  4. tir_framework-2.7.0/tests/test_webapp_internal_clicktree.py +431 -0
  5. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/main.py +19 -4
  6. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/base.py +32 -1
  7. tir_framework-2.7.0/tir/technologies/core/utils.py +74 -0
  8. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/poui_internal.py +37 -31
  9. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/webapp_internal.py +168 -225
  10. tir_framework-2.7.0/tir/version.py +1 -0
  11. {tir_framework-2.6.0 → tir_framework-2.7.0/tir_framework.egg-info}/PKG-INFO +2 -2
  12. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir_framework.egg-info/SOURCES.txt +3 -0
  13. tir_framework-2.6.0/tir/version.py +0 -1
  14. {tir_framework-2.6.0 → tir_framework-2.7.0}/LICENSE +0 -0
  15. {tir_framework-2.6.0 → tir_framework-2.7.0}/MANIFEST.in +0 -0
  16. {tir_framework-2.6.0 → tir_framework-2.7.0}/setup.cfg +0 -0
  17. {tir_framework-2.6.0 → tir_framework-2.7.0}/setup.py +0 -0
  18. {tir_framework-2.6.0 → tir_framework-2.7.0}/tests/test_log.py +0 -0
  19. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/__init__.py +0 -0
  20. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/apw_internal.py +0 -0
  21. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/base_database.py +0 -0
  22. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/config.py +0 -0
  23. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/data/sx3.csv +0 -0
  24. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/drivers/linux64/geckodriver +0 -0
  25. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/drivers/windows/chromedriver.exe +0 -0
  26. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/drivers/windows/electron/chromedriver.exe +0 -0
  27. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/drivers/windows/geckodriver.exe +0 -0
  28. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/enumerations.py +0 -0
  29. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/events.py +0 -0
  30. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/language.py +0 -0
  31. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/log.py +0 -0
  32. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/logging_config.py +0 -0
  33. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/numexec.py +0 -0
  34. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/psutil_info.py +0 -0
  35. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/router.py +0 -0
  36. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/third_party/screen_size.py +0 -0
  37. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/third_party/xpath_soup.py +0 -0
  38. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir_framework.egg-info/dependency_links.txt +0 -0
  39. {tir_framework-2.6.0 → tir_framework-2.7.0}/tir_framework.egg-info/requires.txt +0 -0
  40. {tir_framework-2.6.0 → tir_framework-2.7.0}/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.6.0
3
+ Version: 2.7.0
4
4
  Summary: Test Interface Robot
5
5
  Home-page: https://github.com/totvs/tir
6
6
  Download-URL: https://github.com/totvs/tir
@@ -192,7 +192,7 @@ Após a configuração do ambiente virtual iremos definir o interpretador do VSc
192
192
 
193
193
  ## Contact
194
194
 
195
- [Gitter](https://gitter.im/totvs-tir/General)
195
+ [Gitter](https://app.element.io/#/room/#totvs-tir_General:gitter.im)
196
196
 
197
197
  ## Contributing
198
198
 
@@ -140,7 +140,7 @@ Após a configuração do ambiente virtual iremos definir o interpretador do VSc
140
140
 
141
141
  ## Contact
142
142
 
143
- [Gitter](https://gitter.im/totvs-tir/General)
143
+ [Gitter](https://app.element.io/#/room/#totvs-tir_General:gitter.im)
144
144
 
145
145
  ## Contributing
146
146
 
@@ -0,0 +1,238 @@
1
+ """Unit tests for Utils class and its methods."""
2
+
3
+ import inspect
4
+ import os
5
+ import sys
6
+ import unittest
7
+ from unittest.mock import MagicMock, patch
8
+ from pathlib import Path
9
+
10
+ # Add the parent directory to the path so we can import tir module
11
+ repo_root = Path(__file__).resolve().parents[1]
12
+ sys.path.insert(0, str(repo_root))
13
+
14
+ from tir.technologies.core.utils import Utils
15
+
16
+
17
+ class TestUtilsGetMainEntrypointFromStack(unittest.TestCase):
18
+ """Test cases for Utils.get_main_entrypoint_from_stack method."""
19
+
20
+ def setUp(self):
21
+ """Initialize test fixtures."""
22
+ self.utils = Utils()
23
+
24
+ def test_get_main_entrypoint_with_default_parameters(self):
25
+ """Test method returns a string value with default parameters."""
26
+ result = self.utils.get_main_entrypoint_from_stack()
27
+ self.assertIsInstance(result, str)
28
+
29
+ def test_get_main_entrypoint_returns_fallback_when_no_match(self):
30
+ """Test method returns fallback value when no matching frame is found."""
31
+ result = self.utils.get_main_entrypoint_from_stack(
32
+ target_modules=["nonexistent/module.py"],
33
+ fallback="custom_fallback"
34
+ )
35
+ self.assertEqual(result, "custom_fallback")
36
+
37
+ def test_get_main_entrypoint_returns_default_fallback(self):
38
+ """Test method returns default fallback when no matching frame is found."""
39
+ result = self.utils.get_main_entrypoint_from_stack(
40
+ target_modules=["nonexistent/module.py"]
41
+ )
42
+ self.assertEqual(result, "function_name")
43
+
44
+ def test_get_main_entrypoint_ignores_specified_functions(self):
45
+ """Test that specified functions in ignored_functions are skipped."""
46
+ # Create a set of functions to ignore
47
+ ignored_set = {"__getattribute__", "_subscribe_routes", "__init__", "<lambda>"}
48
+
49
+ result = self.utils.get_main_entrypoint_from_stack(
50
+ target_modules=["test_utils.py"],
51
+ ignored_functions=ignored_set,
52
+ fallback="test_fallback"
53
+ )
54
+
55
+ # The result should not be in the ignored set or should be fallback
56
+ if result != "test_fallback":
57
+ self.assertNotIn(result, ignored_set)
58
+
59
+ def test_get_main_entrypoint_with_custom_ignored_functions(self):
60
+ """Test method with custom ignored functions set."""
61
+ custom_ignored = {"test_method", "setup"}
62
+ result = self.utils.get_main_entrypoint_from_stack(
63
+ target_modules=["test_utils.py"],
64
+ ignored_functions=custom_ignored,
65
+ fallback="not_found"
66
+ )
67
+
68
+ self.assertIsInstance(result, str)
69
+
70
+ def test_get_main_entrypoint_case_insensitive_matching(self):
71
+ """Test that module matching is case-insensitive."""
72
+ # Test with uppercase module name
73
+ result1 = self.utils.get_main_entrypoint_from_stack(
74
+ target_modules=["TEST_UTILS.PY"],
75
+ fallback="not_found"
76
+ )
77
+
78
+ # Test with lowercase module name
79
+ result2 = self.utils.get_main_entrypoint_from_stack(
80
+ target_modules=["test_utils.py"],
81
+ fallback="not_found"
82
+ )
83
+
84
+ # Both should return the same result
85
+ self.assertEqual(result1, result2)
86
+
87
+ def test_get_main_entrypoint_with_path_separators(self):
88
+ """Test that method handles both forward and backward slashes."""
89
+ # Test with forward slashes
90
+ result1 = self.utils.get_main_entrypoint_from_stack(
91
+ target_modules=["tests/test_utils.py"],
92
+ fallback="not_found"
93
+ )
94
+
95
+ # Test with backward slashes (Windows style)
96
+ result2 = self.utils.get_main_entrypoint_from_stack(
97
+ target_modules=["tests\\test_utils.py"],
98
+ fallback="not_found"
99
+ )
100
+
101
+ # Both should return the same result
102
+ self.assertEqual(result1, result2)
103
+
104
+ def test_get_main_entrypoint_multiple_target_modules(self):
105
+ """Test method with multiple target modules."""
106
+ result = self.utils.get_main_entrypoint_from_stack(
107
+ target_modules=["nonexistent.py", "test_utils.py", "another.py"],
108
+ fallback="not_found"
109
+ )
110
+
111
+ self.assertIsInstance(result, str)
112
+
113
+ @patch('inspect.stack')
114
+ def test_get_main_entrypoint_with_mocked_stack(self, mock_stack):
115
+ """Test method behavior with mocked call stack."""
116
+ # Create mock stack frames
117
+ mock_frame1 = MagicMock()
118
+ mock_frame1.filename = "c:\\TOTVS\\repos\\tir\\tir\\main.py"
119
+ mock_frame1.function = "test_function"
120
+
121
+ mock_frame2 = MagicMock()
122
+ mock_frame2.filename = "c:\\other\\path\\module.py"
123
+ mock_frame2.function = "other_function"
124
+
125
+ mock_stack.return_value = [mock_frame1, mock_frame2]
126
+
127
+ result = self.utils.get_main_entrypoint_from_stack(
128
+ target_modules=["tir/main.py"]
129
+ )
130
+
131
+ self.assertEqual(result, "test_function")
132
+
133
+ @patch('inspect.stack')
134
+ def test_get_main_entrypoint_skips_ignored_functions(self, mock_stack):
135
+ """Test that method skips frames with ignored functions."""
136
+ # Create mock stack frames
137
+ mock_frame1 = MagicMock()
138
+ mock_frame1.filename = "c:\\TOTVS\\repos\\tir\\tir\\main.py"
139
+ mock_frame1.function = "__init__" # This should be ignored
140
+
141
+ mock_frame2 = MagicMock()
142
+ mock_frame2.filename = "c:\\TOTVS\\repos\\tir\\tir\\main.py"
143
+ mock_frame2.function = "user_function" # This should be returned
144
+
145
+ mock_stack.return_value = [mock_frame1, mock_frame2]
146
+
147
+ result = self.utils.get_main_entrypoint_from_stack(
148
+ target_modules=["tir/main.py"]
149
+ )
150
+
151
+ self.assertEqual(result, "user_function")
152
+
153
+ @patch('inspect.stack')
154
+ def test_get_main_entrypoint_returns_first_match(self, mock_stack):
155
+ """Test that method returns the first matching function."""
156
+ # Create mock stack frames
157
+ mock_frame1 = MagicMock()
158
+ mock_frame1.filename = "c:\\TOTVS\\repos\\tir\\tir\\main.py"
159
+ mock_frame1.function = "first_function"
160
+
161
+ mock_frame2 = MagicMock()
162
+ mock_frame2.filename = "c:\\TOTVS\\repos\\tir\\tir\\main.py"
163
+ mock_frame2.function = "second_function"
164
+
165
+ mock_stack.return_value = [mock_frame1, mock_frame2]
166
+
167
+ result = self.utils.get_main_entrypoint_from_stack(
168
+ target_modules=["tir/main.py"]
169
+ )
170
+
171
+ # Should return the first matching function
172
+ self.assertEqual(result, "first_function")
173
+
174
+ def test_get_main_entrypoint_empty_ignored_functions(self):
175
+ """Test method with empty ignored functions set."""
176
+ result = self.utils.get_main_entrypoint_from_stack(
177
+ target_modules=["test_utils.py"],
178
+ ignored_functions=set(),
179
+ fallback="fallback"
180
+ )
181
+
182
+ self.assertIsInstance(result, str)
183
+
184
+ def test_get_main_entrypoint_returns_string_type(self):
185
+ """Test that method always returns a string."""
186
+ test_cases = [
187
+ {"target_modules": None},
188
+ {"target_modules": ["nonexistent.py"]},
189
+ {"fallback": "custom"},
190
+ {"ignored_functions": set()},
191
+ ]
192
+
193
+ for kwargs in test_cases:
194
+ result = self.utils.get_main_entrypoint_from_stack(**kwargs)
195
+ self.assertIsInstance(result, str,
196
+ f"Expected str, got {type(result)} for kwargs: {kwargs}")
197
+
198
+ def test_utils_instantiation(self):
199
+ """Test that Utils class can be instantiated."""
200
+ utils_instance = Utils()
201
+ self.assertIsNotNone(utils_instance)
202
+ self.assertTrue(hasattr(utils_instance, 'get_main_entrypoint_from_stack'))
203
+
204
+ def test_get_main_entrypoint_with_absolute_path_module(self):
205
+ """Test method with absolute path in target_modules."""
206
+ abs_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_utils.py"))
207
+
208
+ result = self.utils.get_main_entrypoint_from_stack(
209
+ target_modules=[abs_path],
210
+ fallback="not_found"
211
+ )
212
+
213
+ self.assertIsInstance(result, str)
214
+
215
+
216
+ class TestUtilsClass(unittest.TestCase):
217
+ """Test cases for Utils class itself."""
218
+
219
+ def test_utils_has_get_main_entrypoint_method(self):
220
+ """Test that Utils class has the get_main_entrypoint_from_stack method."""
221
+ utils = Utils()
222
+ self.assertTrue(hasattr(utils, 'get_main_entrypoint_from_stack'))
223
+ self.assertTrue(callable(getattr(utils, 'get_main_entrypoint_from_stack')))
224
+
225
+ def test_utils_docstring_exists(self):
226
+ """Test that Utils class has proper documentation."""
227
+ self.assertIsNotNone(Utils.__doc__)
228
+ self.assertTrue(len(Utils.__doc__) > 0)
229
+
230
+ def test_get_main_entrypoint_docstring_exists(self):
231
+ """Test that method has proper documentation."""
232
+ method = Utils.get_main_entrypoint_from_stack
233
+ self.assertIsNotNone(method.__doc__)
234
+ self.assertTrue(len(method.__doc__) > 0)
235
+
236
+
237
+ if __name__ == '__main__':
238
+ unittest.main()
@@ -0,0 +1,431 @@
1
+ import importlib.util
2
+ import logging
3
+ import sys
4
+ import types
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+ from unittest.mock import Mock
8
+
9
+
10
+ repo_root = Path(__file__).resolve().parents[1]
11
+ module_path = repo_root / "tir" / "technologies" / "webapp_internal.py"
12
+
13
+
14
+ class FakeElement:
15
+ def __init__(self, text="", attrs=None, parent=None):
16
+ self.text = text
17
+ self.attrs = attrs or {}
18
+ self.parent = parent
19
+ self.click_count = 0
20
+
21
+ def get_attribute(self, name):
22
+ return self.attrs.get(name)
23
+
24
+ def click(self):
25
+ self.click_count += 1
26
+
27
+
28
+ def _register_module(name, module=None):
29
+ module = module or types.ModuleType(name)
30
+ sys.modules[name] = module
31
+
32
+ if "." in name:
33
+ parent_name, child_name = name.rsplit(".", 1)
34
+ parent = sys.modules.get(parent_name)
35
+ if parent is None:
36
+ parent = _register_module(parent_name)
37
+ setattr(parent, child_name, module)
38
+
39
+ return module
40
+
41
+
42
+ def load_webapp_internal_module():
43
+ cached = sys.modules.get("tir_webapp_internal_test")
44
+ if cached is not None:
45
+ return cached
46
+
47
+ # External dependency stubs used only to import the module for isolated unit tests.
48
+ _register_module("pandas")
49
+ _register_module("cv2")
50
+
51
+ bs4_mod = _register_module("bs4")
52
+ bs4_mod.BeautifulSoup = type("BeautifulSoup", (), {})
53
+ bs4_mod.Tag = type("Tag", (), {})
54
+
55
+ _register_module("selenium")
56
+ _register_module("selenium.webdriver")
57
+ _register_module("selenium.webdriver.common")
58
+ _register_module("selenium.webdriver.support")
59
+
60
+ keys_mod = _register_module("selenium.webdriver.common.keys")
61
+ keys_mod.Keys = type("Keys", (), {"CONTROL": "CONTROL", "F5": "F5"})
62
+
63
+ by_mod = _register_module("selenium.webdriver.common.by")
64
+ by_mod.By = type("By", (), {"CSS_SELECTOR": "css", "XPATH": "xpath"})
65
+
66
+ action_mod = _register_module("selenium.webdriver.common.action_chains")
67
+ action_mod.ActionChains = type("ActionChains", (), {})
68
+
69
+ support_ui_mod = _register_module("selenium.webdriver.support.ui")
70
+ support_ui_mod.Select = type("Select", (), {})
71
+
72
+ expected_conditions_mod = _register_module("selenium.webdriver.support.expected_conditions")
73
+ expected_conditions_mod.EC = type("EC", (), {})
74
+
75
+ exceptions_mod = _register_module("selenium.common.exceptions")
76
+ exceptions_mod.WebDriverException = type("WebDriverException", (Exception,), {})
77
+ exceptions_mod.NoSuchElementException = type("NoSuchElementException", (Exception,), {})
78
+
79
+ tir_pkg = _register_module("tir")
80
+ tir_pkg.__path__ = []
81
+ technologies_pkg = _register_module("tir.technologies")
82
+ technologies_pkg.__path__ = []
83
+ core_pkg = _register_module("tir.technologies.core")
84
+ core_pkg.__path__ = []
85
+ third_party_pkg = _register_module("tir.technologies.core.third_party")
86
+ third_party_pkg.__path__ = []
87
+
88
+ enum_mod = _register_module("tir.technologies.core.enumerations")
89
+ enum_mod.ClickType = type("ClickType", (), {"SELENIUM": "selenium", "JS": "js"})
90
+ enum_mod.ScrapType = type(
91
+ "ScrapType",
92
+ (),
93
+ {
94
+ "TEXT": "text",
95
+ "MIXED": "mixed",
96
+ "CSS_SELECTOR": "css",
97
+ "SCRIPT": "script",
98
+ },
99
+ )
100
+ enum_mod.MessageType = type(
101
+ "MessageType",
102
+ (),
103
+ {
104
+ "CORRECT": "correct",
105
+ "INCORRECT": "incorrect",
106
+ "DISABLED": "disabled",
107
+ "ASSERTERROR": "asserterror",
108
+ },
109
+ )
110
+
111
+ base_module = _register_module("tir.technologies.core.base")
112
+ base_module.Base = type("Base", (), {"errors": []})
113
+
114
+ log_module = _register_module("tir.technologies.core.log")
115
+ log_module.Log = type("Log", (), {})
116
+ log_module.nump = None
117
+
118
+ config_module = _register_module("tir.technologies.core.config")
119
+ config_module.ConfigLoader = lambda *args, **kwargs: None
120
+
121
+ language_module = _register_module("tir.technologies.core.language")
122
+ language_module.LanguagePack = type("LanguagePack", (), {})
123
+
124
+ xpath_module = _register_module("tir.technologies.core.third_party.xpath_soup")
125
+ xpath_module.xpath_soup = lambda *args, **kwargs: "//stub"
126
+
127
+ psutil_module = _register_module("tir.technologies.core.psutil_info")
128
+ psutil_module.system_info = lambda: {}
129
+
130
+ numexec_module = _register_module("tir.technologies.core.numexec")
131
+ numexec_module.NumExec = type("NumExec", (), {})
132
+
133
+ logging_config_module = _register_module("tir.technologies.core.logging_config")
134
+ logging_config_module.logger = lambda: logging.getLogger("test_webapp_internal_clicktree")
135
+
136
+ base_database_module = _register_module("tir.technologies.core.base_database")
137
+ base_database_module.BaseDatabase = type("BaseDatabase", (), {})
138
+
139
+ spec = importlib.util.spec_from_file_location("tir_webapp_internal_test", str(module_path))
140
+ module = importlib.util.module_from_spec(spec)
141
+ spec.loader.exec_module(module)
142
+ sys.modules["tir_webapp_internal_test"] = module
143
+ return module
144
+
145
+
146
+ def build_webapp():
147
+ module = load_webapp_internal_module()
148
+ webapp = module.WebappInternal.__new__(module.WebappInternal)
149
+ webapp.config = SimpleNamespace(time_out=0.01)
150
+ webapp.tree_base_element = ()
151
+ webapp.wait_blocker = Mock()
152
+ webapp.check_layers = Mock(return_value=0)
153
+ webapp.scroll_to_element = Mock()
154
+ webapp.log_error = Mock()
155
+ webapp.click = Mock()
156
+ webapp.return_last_zindex = Mock(return_value=0)
157
+ webapp.element_exists = Mock(return_value=False)
158
+ webapp.soup_to_selenium = Mock(side_effect=lambda element: element)
159
+ return webapp, module
160
+
161
+
162
+ def test_clicktree_delegates_to_internal_method():
163
+ webapp, _ = build_webapp()
164
+ webapp.click_tree = Mock()
165
+
166
+ webapp.ClickTree("nivel 1 > nivel 2", right_click=True, position=2, tree_number=3)
167
+
168
+ webapp.click_tree.assert_called_once_with("nivel 1 > nivel 2", True, 2, 3)
169
+
170
+
171
+ def test_click_tree_clicks_the_last_matching_node_without_browser_dependencies():
172
+ webapp, _ = build_webapp()
173
+
174
+ node = FakeElement(
175
+ text=" Financeiro ",
176
+ attrs={"hidden": None, "closed": "", "hierarchy": "01", "icon": None},
177
+ )
178
+ clickable_part = FakeElement(text="Financeiro", parent=node)
179
+
180
+ webapp.find_tree_bs = Mock(return_value=[node])
181
+ webapp.element_is_displayed = Mock(return_value=True)
182
+ webapp.check_toggler = Mock(return_value=False)
183
+ webapp.check_hierarchy = Mock(return_value=False)
184
+
185
+ selected_nodes = [None, node, node]
186
+
187
+ def treenode_selected(_label, _tree_number=0):
188
+ return selected_nodes.pop(0) if selected_nodes else node
189
+
190
+ def execute_js_selector(selector, element, get_all=True):
191
+ if selector == ".toggler, .lastchild, .data, label":
192
+ return [clickable_part]
193
+ return None
194
+
195
+ webapp.treenode_selected = Mock(side_effect=treenode_selected)
196
+ webapp.execute_js_selector = Mock(side_effect=execute_js_selector)
197
+
198
+ webapp.click_tree("Financeiro", right_click=False, position=1, tree_number=0)
199
+
200
+ assert clickable_part.click_count == 1
201
+ webapp.log_error.assert_not_called()
202
+
203
+
204
+ def test_click_tree_logs_error_when_no_tree_node_is_found():
205
+ webapp, _ = build_webapp()
206
+ webapp.config.time_out = 0
207
+ webapp.find_tree_bs = Mock(return_value=[])
208
+ webapp.element_is_displayed = Mock(return_value=True)
209
+ webapp.treenode_selected = Mock(return_value=None)
210
+ webapp.execute_js_selector = Mock(return_value=None)
211
+ webapp.check_toggler = Mock(return_value=False)
212
+ webapp.check_hierarchy = Mock(return_value=False)
213
+
214
+ webapp.click_tree("Item inexistente", right_click=False, position=1, tree_number=0)
215
+
216
+ webapp.log_error.assert_called_once()
217
+ assert "Couldn't click on tree element" in webapp.log_error.call_args.args[0]
218
+
219
+
220
+ def test_click_tree_with_right_click_on_last_node():
221
+ """Test that right_click=True is passed to click method when last item is clicked."""
222
+ webapp, _ = build_webapp()
223
+
224
+ node = FakeElement(
225
+ text="Menu Item",
226
+ attrs={"hidden": None, "closed": "", "hierarchy": "01", "icon": None},
227
+ )
228
+ clickable_part = FakeElement(text="Menu Item", parent=node)
229
+
230
+ webapp.find_tree_bs = Mock(return_value=[node])
231
+ webapp.element_is_displayed = Mock(return_value=True)
232
+ webapp.check_toggler = Mock(return_value=False)
233
+ webapp.check_hierarchy = Mock(return_value=False)
234
+ webapp.treenode_selected = Mock(return_value=node)
235
+ webapp.return_last_zindex = Mock(return_value=100)
236
+
237
+ def execute_js_selector(selector, element, get_all=True):
238
+ if selector == ".toggler, .lastchild, .data, label":
239
+ return [clickable_part]
240
+ return None
241
+
242
+ webapp.execute_js_selector = Mock(side_effect=execute_js_selector)
243
+
244
+ webapp.click_tree("Menu Item", right_click=True, position=1, tree_number=0)
245
+
246
+ # Verify that the main element was clicked (right_click=True path)
247
+ # The actual right_click happens in the loop after regular click
248
+ webapp.log_error.assert_not_called()
249
+
250
+
251
+ def test_click_tree_with_multiple_hierarchy_levels():
252
+ """Test navigating through single level tree (simpler version of multi-level)."""
253
+ webapp, _ = build_webapp()
254
+
255
+ pai = FakeElement(text="Pai", attrs={"hierarchy": "01", "closed": "", "icon": None})
256
+ pai_clickable = FakeElement(text="Pai", parent=pai)
257
+
258
+ webapp.find_tree_bs = Mock(return_value=[pai])
259
+ webapp.element_is_displayed = Mock(return_value=True)
260
+ webapp.check_toggler = Mock(return_value=False)
261
+ webapp.check_hierarchy = Mock(return_value=False)
262
+ webapp.treenode_selected = Mock(return_value=pai)
263
+
264
+ def execute_js_selector(selector, element, get_all=True):
265
+ if selector == ".toggler, .lastchild, .data, label":
266
+ return [pai_clickable]
267
+ return None
268
+
269
+ webapp.execute_js_selector = Mock(side_effect=execute_js_selector)
270
+
271
+ # Click the single level
272
+ webapp.click_tree("Pai", right_click=False, position=1, tree_number=0)
273
+
274
+ # Verify successful click
275
+ webapp.log_error.assert_not_called()
276
+
277
+
278
+ def test_click_tree_respects_position_parameter():
279
+ """Test that position parameter is considered when multiple elements exist."""
280
+ webapp, _ = build_webapp()
281
+
282
+ # Create multiple nodes with same text
283
+ node1 = FakeElement(text="Duplicado", attrs={"hidden": None, "closed": "", "hierarchy": "01"})
284
+ node2 = FakeElement(text="Duplicado", attrs={"hidden": None, "closed": "", "hierarchy": "02"})
285
+
286
+ webapp.find_tree_bs = Mock(return_value=[node1, node2])
287
+ webapp.element_is_displayed = Mock(return_value=True)
288
+ webapp.check_toggler = Mock(return_value=False)
289
+ webapp.check_hierarchy = Mock(return_value=False)
290
+
291
+ # Return node2 when treenode_selected is called
292
+ webapp.treenode_selected = Mock(return_value=node2)
293
+
294
+ def execute_js_selector(selector, element, get_all=True):
295
+ if selector == ".toggler, .lastchild, .data, label":
296
+ if element == node1:
297
+ return [FakeElement(text="Duplicado")]
298
+ elif element == node2:
299
+ return [FakeElement(text="Duplicado")]
300
+ return None
301
+
302
+ webapp.execute_js_selector = Mock(side_effect=execute_js_selector)
303
+
304
+ # Position 2 should try to select the second matching node
305
+ webapp.click_tree("Duplicado", right_click=False, position=2, tree_number=0)
306
+
307
+ # Verify successful click
308
+ webapp.log_error.assert_not_called()
309
+
310
+
311
+ def test_click_tree_updates_tree_base_element_for_non_last_items():
312
+ """Test that tree_base_element is updated when navigating intermediate nodes."""
313
+ webapp, _ = build_webapp()
314
+ webapp.tree_base_element = ()
315
+
316
+ pai = FakeElement(text="Pai", attrs={"hierarchy": "01", "closed": "true", "icon": None})
317
+ filho = FakeElement(text="Filho", attrs={"hierarchy": "01-01", "closed": "", "icon": None})
318
+ pai_clickable = FakeElement(text="Pai", parent=pai)
319
+
320
+ webapp.find_tree_bs = Mock(return_value=[pai, filho])
321
+ webapp.element_is_displayed = Mock(return_value=True)
322
+ webapp.check_toggler = Mock(return_value=True)
323
+ webapp.check_hierarchy = Mock(side_effect=[True, False])
324
+ webapp.treenode_selected = Mock(side_effect=[None, pai, filho])
325
+
326
+ def execute_js_selector(selector, element, get_all=True):
327
+ if selector == ".toggler, .lastchild, .data, label":
328
+ return [pai_clickable]
329
+ return None
330
+
331
+ webapp.execute_js_selector = Mock(side_effect=execute_js_selector)
332
+
333
+ webapp.click_tree("Pai > Filho", right_click=False, position=1, tree_number=0)
334
+
335
+ # Verify tree_base_element was set to (label_filtered, element_class_item)
336
+ assert webapp.tree_base_element != ()
337
+ assert webapp.tree_base_element[0] == "pai" # label_filtered is lowercased
338
+
339
+
340
+ def test_click_tree_with_hidden_element():
341
+ """Test that hidden elements are filtered out during element search."""
342
+ webapp, _ = build_webapp()
343
+
344
+ hidden_node = FakeElement(text="Hidden", attrs={"hidden": "true", "closed": "", "hierarchy": "01"})
345
+ visible_node = FakeElement(text="Visible", attrs={"hidden": None, "closed": "", "hierarchy": "02"})
346
+ clickable = FakeElement(text="Visible", parent=visible_node)
347
+
348
+ webapp.find_tree_bs = Mock(return_value=[hidden_node, visible_node])
349
+
350
+ def element_is_displayed_side_effect(element):
351
+ # hidden_node has hidden attribute, so it's not displayed
352
+ if element == hidden_node:
353
+ return False
354
+ return True
355
+
356
+ webapp.element_is_displayed = Mock(side_effect=element_is_displayed_side_effect)
357
+ webapp.check_toggler = Mock(return_value=False)
358
+ webapp.check_hierarchy = Mock(return_value=False)
359
+ webapp.treenode_selected = Mock(return_value=visible_node)
360
+
361
+ def execute_js_selector(selector, element, get_all=True):
362
+ if selector == ".toggler, .lastchild, .data, label":
363
+ return [clickable]
364
+ return None
365
+
366
+ webapp.execute_js_selector = Mock(side_effect=execute_js_selector)
367
+
368
+ webapp.click_tree("Visible", right_click=False, position=1, tree_number=0)
369
+
370
+ # Verify hidden element was skipped (log_error not called)
371
+ webapp.log_error.assert_not_called()
372
+
373
+
374
+ def test_click_tree_handles_text_normalization():
375
+ """Test that extra spaces in labels are normalized during matching."""
376
+ webapp, _ = build_webapp()
377
+
378
+ node = FakeElement(
379
+ text=" Item com espaços ",
380
+ attrs={"hidden": None, "closed": "", "hierarchy": "01", "icon": None},
381
+ )
382
+ clickable_part = FakeElement(text="Item com espaços", parent=node)
383
+
384
+ webapp.find_tree_bs = Mock(return_value=[node])
385
+ webapp.element_is_displayed = Mock(return_value=True)
386
+ webapp.check_toggler = Mock(return_value=False)
387
+ webapp.check_hierarchy = Mock(return_value=False)
388
+ webapp.treenode_selected = Mock(return_value=node)
389
+
390
+ def execute_js_selector(selector, element, get_all=True):
391
+ if selector == ".toggler, .lastchild, .data, label":
392
+ return [clickable_part]
393
+ return None
394
+
395
+ webapp.execute_js_selector = Mock(side_effect=execute_js_selector)
396
+
397
+ # Search with single spaces should match label with multiple spaces
398
+ webapp.click_tree("Item com espaços", right_click=False, position=1, tree_number=0)
399
+
400
+ # Verify normalization worked (no error)
401
+ webapp.log_error.assert_not_called()
402
+
403
+
404
+ def test_click_tree_tree_number_parameter():
405
+ """Test that tree_number parameter is used to locate the correct tree."""
406
+ webapp, _ = build_webapp()
407
+
408
+ tree_node = FakeElement(text="Tree2Item", attrs={"hidden": None, "closed": "", "hierarchy": "02"})
409
+ clickable = FakeElement(text="Tree2Item", parent=tree_node)
410
+
411
+ # find_tree_bs is called with tree_number to select the correct tree
412
+ webapp.find_tree_bs = Mock(return_value=[tree_node])
413
+ webapp.element_is_displayed = Mock(return_value=True)
414
+ webapp.check_toggler = Mock(return_value=False)
415
+ webapp.check_hierarchy = Mock(return_value=False)
416
+ webapp.treenode_selected = Mock(return_value=tree_node)
417
+
418
+ def execute_js_selector(selector, element, get_all=True):
419
+ if selector == ".toggler, .lastchild, .data, label":
420
+ return [clickable]
421
+ return None
422
+
423
+ webapp.execute_js_selector = Mock(side_effect=execute_js_selector)
424
+
425
+ # Use tree_number=2 (should be converted to index 1)
426
+ webapp.click_tree("Tree2Item", right_click=False, position=1, tree_number=2)
427
+
428
+ # Verify find_tree_bs was called with the correct tree_number
429
+ assert webapp.find_tree_bs.called
430
+ # Verify successful click (no error logged)
431
+ webapp.log_error.assert_not_called()