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.
- {tir_framework-2.6.0/tir_framework.egg-info → tir_framework-2.7.0}/PKG-INFO +2 -2
- {tir_framework-2.6.0 → tir_framework-2.7.0}/README.md +1 -1
- tir_framework-2.7.0/tests/test_utils.py +238 -0
- tir_framework-2.7.0/tests/test_webapp_internal_clicktree.py +431 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/main.py +19 -4
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/base.py +32 -1
- tir_framework-2.7.0/tir/technologies/core/utils.py +74 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/poui_internal.py +37 -31
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/webapp_internal.py +168 -225
- tir_framework-2.7.0/tir/version.py +1 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0/tir_framework.egg-info}/PKG-INFO +2 -2
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir_framework.egg-info/SOURCES.txt +3 -0
- tir_framework-2.6.0/tir/version.py +0 -1
- {tir_framework-2.6.0 → tir_framework-2.7.0}/LICENSE +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/MANIFEST.in +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/setup.cfg +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/setup.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tests/test_log.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/__init__.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/apw_internal.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/base_database.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/config.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/data/sx3.csv +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/drivers/linux64/geckodriver +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/drivers/windows/chromedriver.exe +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/drivers/windows/electron/chromedriver.exe +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/drivers/windows/geckodriver.exe +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/enumerations.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/events.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/language.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/log.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/logging_config.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/numexec.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/psutil_info.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/router.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/third_party/screen_size.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir/technologies/core/third_party/xpath_soup.py +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir_framework.egg-info/dependency_links.txt +0 -0
- {tir_framework-2.6.0 → tir_framework-2.7.0}/tir_framework.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
195
|
+
[Gitter](https://app.element.io/#/room/#totvs-tir_General:gitter.im)
|
|
196
196
|
|
|
197
197
|
## Contributing
|
|
198
198
|
|
|
@@ -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()
|