testmuai-selenium-bindings 0.1.0b1__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.
- testmuai_selenium_bindings-0.1.0b1/PKG-INFO +59 -0
- testmuai_selenium_bindings-0.1.0b1/README.md +28 -0
- testmuai_selenium_bindings-0.1.0b1/pyproject.toml +48 -0
- testmuai_selenium_bindings-0.1.0b1/setup.cfg +4 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/__init__.py +132 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_clear.py +77 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_click.py +54 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_drag_drop.py +32 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_element_drag.py +59 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_engine.py +171 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_hover.py +38 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_scroll_into_view.py +65 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_scroll_until_element.py +173 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_search.py +72 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_set_input_files.py +215 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_textual_query.py +138 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_type.py +71 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_capability.py +190 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_config.py +115 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_configure.py +40 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_decorator.py +33 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_errors.py +66 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_heal.py +509 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_heal_cascade.py +176 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/__init__.py +0 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/_frame.py +165 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/_http.py +106 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/_screenshot.py +251 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/_tagify.py +27 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/clear_at_coordinate.py +23 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/clear_element.py +73 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/click.py +137 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/dialog.py +41 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/dom_wait.py +22 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/drag_at_coordinate.py +57 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/driver.py +31 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/execute_api.py +406 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/execute_db.py +126 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/execute_js.py +88 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/find_element.py +73 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/input_value.py +328 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/js_templates.py +122 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/math.py +152 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/navigate.py +113 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/navigation.py +29 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/network.py +197 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/scroll.py +186 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/smart_wait.py +256 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/smartui.py +7 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/snapshot.py +134 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/tabs.py +120 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/textual_query.py +225 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/url.py +15 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/vision_query.py +93 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/wait.py +117 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_route_failure.py +71 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_session.py +206 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_step.py +66 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_test_config.py +83 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_vars.py +496 -0
- testmuai_selenium_bindings-0.1.0b1/testmu_selenium/condition.py +888 -0
- testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/PKG-INFO +59 -0
- testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/SOURCES.txt +126 -0
- testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/dependency_links.txt +1 -0
- testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/requires.txt +15 -0
- testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/top_level.txt +1 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_clear.py +228 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_click.py +162 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_drag_drop.py +83 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_element_drag.py +144 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_engine.py +552 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_hover.py +134 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_scroll_into_view.py +207 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_scroll_until_element.py +185 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_search.py +221 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_set_input_files.py +337 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_textual_query.py +471 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_action_type.py +198 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_assertion_parity.py +99 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_capability.py +316 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_check_until_condition.py +96 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_clear_at_coordinate.py +29 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_click.py +118 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_condition.py +234 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_config_run_target.py +58 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_config_url_resolution.py +153 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_config_was_set.py +50 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_configure.py +90 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_decorator.py +55 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_drag_at_coordinate.py +146 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_execute_api.py +100 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_execute_db.py +83 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_find_element.py +150 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_handle_alert.py +68 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_heal_browser_coordinate.py +228 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_heal_cascade.py +521 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_heal_helpers.py +133 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_heal_init.py +227 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_heal_list_xpaths.py +258 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_heal_textual_query.py +291 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_heal_vision_query.py +127 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_helpers.py +333 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_http.py +124 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_input_value.py +296 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_math.py +50 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_navigate.py +202 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_navigation.py +27 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_network_assertion.py +53 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_network_query.py +251 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_public_api.py +71 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_resolve_variable.py +121 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_route_failure.py +197 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_scroll.py +235 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_search_root.py +83 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_session.py +241 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_session_local.py +133 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_session_options.py +55 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_session_pending_failures.py +127 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_smart_wait.py +233 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_smartui.py +61 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_step.py +70 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_tabs.py +169 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_test_config.py +59 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_textual_query.py +208 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_url.py +16 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_vars.py +265 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_vision_query_param_resolution.py +32 -0
- testmuai_selenium_bindings-0.1.0b1/tests/test_wait.py +94 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: testmuai-selenium-bindings
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Testmu binding for Selenium Python — thin test runtime for LambdaTest exports
|
|
5
|
+
Author-email: LambdaTest <engineering@lambdatest.com>
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Framework :: Pytest
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: selenium>=4.17.0
|
|
18
|
+
Requires-Dist: httpx>=0.27.0
|
|
19
|
+
Requires-Dist: tenacity>=8.5.0
|
|
20
|
+
Requires-Dist: pyotp>=2.9.0
|
|
21
|
+
Requires-Dist: requests>=2.28.0
|
|
22
|
+
Requires-Dist: Pillow>=11.0.0
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
24
|
+
Requires-Dist: lambdatest-selenium-driver>=1.0.7
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
28
|
+
Requires-Dist: respx>=0.20; extra == "dev"
|
|
29
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
30
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
31
|
+
|
|
32
|
+
# testmuai-selenium-bindings
|
|
33
|
+
|
|
34
|
+
Python runtime bindings for Selenium-based generated tests.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
pip install testmuai-selenium-bindings
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import testmu_selenium
|
|
46
|
+
|
|
47
|
+
testmu_selenium.configure(build="my-build", name="my-test")
|
|
48
|
+
|
|
49
|
+
@testmu_selenium.test
|
|
50
|
+
def my_test(driver):
|
|
51
|
+
driver.get("https://example.com")
|
|
52
|
+
el = testmu_selenium.findElement(driver, [{"selector": "h1", "isXPath": False}])
|
|
53
|
+
assert el is not None
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
testmu_selenium.run(my_test)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The PyPI distribution name is `testmuai-selenium-bindings`; the import name is `testmu_selenium`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# testmuai-selenium-bindings
|
|
2
|
+
|
|
3
|
+
Python runtime bindings for Selenium-based generated tests.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install testmuai-selenium-bindings
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import testmu_selenium
|
|
15
|
+
|
|
16
|
+
testmu_selenium.configure(build="my-build", name="my-test")
|
|
17
|
+
|
|
18
|
+
@testmu_selenium.test
|
|
19
|
+
def my_test(driver):
|
|
20
|
+
driver.get("https://example.com")
|
|
21
|
+
el = testmu_selenium.findElement(driver, [{"selector": "h1", "isXPath": False}])
|
|
22
|
+
assert el is not None
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
testmu_selenium.run(my_test)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The PyPI distribution name is `testmuai-selenium-bindings`; the import name is `testmu_selenium`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "testmuai-selenium-bindings"
|
|
7
|
+
version = "0.1.0b1"
|
|
8
|
+
description = "Testmu binding for Selenium Python — thin test runtime for LambdaTest exports"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "LicenseRef-Proprietary"
|
|
11
|
+
authors = [{name = "LambdaTest", email = "engineering@lambdatest.com"}]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Framework :: Pytest",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"selenium>=4.17.0",
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"tenacity>=8.5.0",
|
|
27
|
+
"pyotp>=2.9.0",
|
|
28
|
+
"requests>=2.28.0",
|
|
29
|
+
"Pillow>=11.0.0",
|
|
30
|
+
"python-dotenv>=1.0.0",
|
|
31
|
+
"lambdatest-selenium-driver>=1.0.7",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"pytest-mock>=3.12",
|
|
38
|
+
"respx>=0.20",
|
|
39
|
+
"build>=1.0",
|
|
40
|
+
"twine>=5.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
include = ["testmu_selenium*"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""testmu_selenium — Selenium runtime bindings for generated tests.
|
|
2
|
+
|
|
3
|
+
Public API (consumed by codegen-emitted test.py):
|
|
4
|
+
configure(**kwargs) — set test config at module top
|
|
5
|
+
@testmu_selenium.test — decorator for test function
|
|
6
|
+
testmu_selenium.run(fn) — single-driver session lifecycle
|
|
7
|
+
testmu_selenium.step(...) — step context manager
|
|
8
|
+
var(template), set_var(name, value) — variable store + template substitution
|
|
9
|
+
findElement, clickElement, input_value, executeJS, visionQuery — runtime helpers
|
|
10
|
+
_heal_cascade(...) → HealResult — autoheal cascade dispatcher
|
|
11
|
+
Public exception classes: TestmuConfigError, AutohealExhausted, ClickAllMethodsFailed
|
|
12
|
+
|
|
13
|
+
Sync API. Single driver per test. Selenium-only.
|
|
14
|
+
"""
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
__version__ = _pkg_version("testmuai-selenium-bindings")
|
|
19
|
+
except PackageNotFoundError:
|
|
20
|
+
__version__ = "0.0.0+unknown"
|
|
21
|
+
|
|
22
|
+
# Load .env BEFORE submodule imports so module-level os.getenv() in _config.py reads
|
|
23
|
+
# values from a developer's local .env. On HyperExecute, env vars are platform-injected
|
|
24
|
+
# and python-dotenv is absent — silent no-op.
|
|
25
|
+
try:
|
|
26
|
+
from dotenv import load_dotenv as _load_dotenv
|
|
27
|
+
_load_dotenv()
|
|
28
|
+
except ImportError:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
from testmu_selenium._configure import configure
|
|
32
|
+
from testmu_selenium._decorator import test
|
|
33
|
+
from testmu_selenium._session import run
|
|
34
|
+
from testmu_selenium._step import step
|
|
35
|
+
from testmu_selenium._vars import var, set_var, get_variable_value, resolve_variable
|
|
36
|
+
from testmu_selenium._heal_cascade import _heal_cascade, HealResult
|
|
37
|
+
from testmu_selenium._route_failure import route_failure
|
|
38
|
+
from testmu_selenium._test_config import load_test_config
|
|
39
|
+
from testmu_selenium._errors import (
|
|
40
|
+
TestmuConfigError, AutohealExhausted, HealTierMiss, HealAuthError, ClickAllMethodsFailed,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Helpers re-exported at top level (codegen emits these as bare names)
|
|
44
|
+
from testmu_selenium._helpers.click import clickElement, add_clickElement_to_webelement
|
|
45
|
+
from testmu_selenium._helpers.find_element import findElement
|
|
46
|
+
from testmu_selenium._helpers.driver import get_driver
|
|
47
|
+
from testmu_selenium._helpers.input_value import input_value
|
|
48
|
+
from testmu_selenium._helpers.execute_js import executeJS
|
|
49
|
+
from testmu_selenium._helpers.vision_query import visionQuery
|
|
50
|
+
from testmu_selenium._helpers.textual_query import textualQuery
|
|
51
|
+
from testmu_selenium._helpers.smartui import smartui_snapshot
|
|
52
|
+
|
|
53
|
+
# Auto-monkey-patch WebElement on package import — generated test.py expects el.clickElement(...)
|
|
54
|
+
add_clickElement_to_webelement()
|
|
55
|
+
|
|
56
|
+
# Additional monkey-patches if available
|
|
57
|
+
try:
|
|
58
|
+
from testmu_selenium._helpers.input_value import add_input_value_to_webelement
|
|
59
|
+
add_input_value_to_webelement()
|
|
60
|
+
except ImportError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
from testmu_selenium._helpers.clear_element import add_clear_element_to_webelement
|
|
65
|
+
add_clear_element_to_webelement()
|
|
66
|
+
except ImportError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
# High-level action wrappers — single-call replacements for the codegen-emitted
|
|
70
|
+
# retry+heal loops. Imported AFTER the WebElement monkey-patches so the runners
|
|
71
|
+
# can rely on el.clickElement / el.input_value being present.
|
|
72
|
+
from testmu_selenium._action_click import click
|
|
73
|
+
from testmu_selenium._action_type import type as _type
|
|
74
|
+
from testmu_selenium._action_textual_query import textual_query
|
|
75
|
+
from testmu_selenium._action_search import search
|
|
76
|
+
from testmu_selenium._action_hover import hover
|
|
77
|
+
from testmu_selenium._action_clear import clear
|
|
78
|
+
from testmu_selenium._action_set_input_files import set_input_files
|
|
79
|
+
from testmu_selenium._action_scroll_into_view import scroll_into_view
|
|
80
|
+
from testmu_selenium._action_scroll_until_element import scroll_until_element
|
|
81
|
+
from testmu_selenium._action_drag_drop import drag_drop
|
|
82
|
+
from testmu_selenium._action_element_drag import element_drag
|
|
83
|
+
from testmu_selenium._helpers.drag_at_coordinate import drag_at_coordinate
|
|
84
|
+
from testmu_selenium._helpers.clear_at_coordinate import clear_at_coordinate
|
|
85
|
+
from testmu_selenium._helpers.navigate import navigate
|
|
86
|
+
from testmu_selenium._helpers.wait import wait, wait_until, wait_for_load, check_until_condition
|
|
87
|
+
|
|
88
|
+
# Codegen emits the bare camelCase name in test.py — alias matches the generated code convention.
|
|
89
|
+
checkUntilCondition = check_until_condition
|
|
90
|
+
from testmu_selenium._helpers.tabs import new_tab, switch_tab, close_tab
|
|
91
|
+
from testmu_selenium._helpers.scroll import scroll
|
|
92
|
+
from testmu_selenium._helpers.navigation import refresh, go_back, go_forward
|
|
93
|
+
from testmu_selenium._helpers.dialog import handle_alert
|
|
94
|
+
from testmu_selenium._helpers.url import get_url, get_title
|
|
95
|
+
from testmu_selenium._helpers.math import evaluate_math
|
|
96
|
+
from testmu_selenium._helpers.network import evaluate_network_assertion, network_query
|
|
97
|
+
from testmu_selenium._helpers.execute_api import execute_api
|
|
98
|
+
from testmu_selenium._helpers.execute_db import execute_db
|
|
99
|
+
# Bind to the public name `type` at module level — note that this shadows the
|
|
100
|
+
# builtin only when accessed via `from testmu_selenium import type`. Codegen
|
|
101
|
+
# uses `testmu_selenium.type(...)` so no shadowing concern there.
|
|
102
|
+
type = _type # noqa: A001
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
# Lifecycle
|
|
107
|
+
"configure", "test", "run", "step",
|
|
108
|
+
# Failure routing
|
|
109
|
+
"route_failure",
|
|
110
|
+
# HyperExecute per-run test config (data-driven test_params)
|
|
111
|
+
"load_test_config",
|
|
112
|
+
# Variable store
|
|
113
|
+
"var", "set_var", "get_variable_value", "resolve_variable",
|
|
114
|
+
# Runtime helpers
|
|
115
|
+
"findElement", "get_driver", "clickElement", "input_value", "executeJS", "visionQuery", "textualQuery", "smartui_snapshot",
|
|
116
|
+
# High-level action wrappers — element-level (find + heal + act)
|
|
117
|
+
"click", "type", "search", "hover", "clear", "set_input_files", "scroll_into_view",
|
|
118
|
+
"drag_drop", "element_drag", "drag_at_coordinate", "clear_at_coordinate", "textual_query",
|
|
119
|
+
# High-level action wrappers — driver-level (no find, no heal)
|
|
120
|
+
"navigate", "wait", "wait_until", "wait_for_load", "check_until_condition", "checkUntilCondition",
|
|
121
|
+
"new_tab", "switch_tab", "close_tab",
|
|
122
|
+
"scroll", "scroll_until_element", "refresh", "go_back", "go_forward",
|
|
123
|
+
"handle_alert",
|
|
124
|
+
"get_url", "get_title",
|
|
125
|
+
# Driver-agnostic helpers
|
|
126
|
+
"evaluate_math", "evaluate_network_assertion", "network_query",
|
|
127
|
+
"execute_api", "execute_db",
|
|
128
|
+
# Heal
|
|
129
|
+
"_heal_cascade", "HealResult",
|
|
130
|
+
# Exceptions
|
|
131
|
+
"TestmuConfigError", "AutohealExhausted", "ClickAllMethodsFailed",
|
|
132
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""High-level clear action — find element, clear field, heal on failure.
|
|
2
|
+
|
|
3
|
+
Canvas-coordinate path.
|
|
4
|
+
The selector-tier path uses element.clear() (Selenium native);
|
|
5
|
+
the COORDINATE-tier fallback uses 3 sequential ActionBuilder instances:
|
|
6
|
+
1. move_to_location(x, y) + click + perform
|
|
7
|
+
2. key_down(CTRL) + send_keys('a') + key_up(CTRL) + perform
|
|
8
|
+
3. send_keys(Keys.DELETE) + perform
|
|
9
|
+
"""
|
|
10
|
+
from selenium.common.exceptions import InvalidElementStateException
|
|
11
|
+
from selenium.webdriver.common.actions.action_builder import ActionBuilder
|
|
12
|
+
from selenium.webdriver.common.keys import Keys
|
|
13
|
+
|
|
14
|
+
from testmu_selenium._action_engine import _ActionSpec, _run_action
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _clear_runner(element, ctx):
|
|
18
|
+
try:
|
|
19
|
+
element.clear()
|
|
20
|
+
except InvalidElementStateException:
|
|
21
|
+
# Native clear() enforces the W3C is-editable precondition and rejects
|
|
22
|
+
# masked/readonly inputs. Fall back to keystroke clearing (Ctrl+A +
|
|
23
|
+
# Delete) — the same select-all/delete sequence the COORDINATE tier
|
|
24
|
+
# uses, which the browser routes to the focused element without the
|
|
25
|
+
# editability gate.
|
|
26
|
+
_keystroke_clear(ctx['driver'], element)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _keystroke_clear(driver, element):
|
|
31
|
+
try:
|
|
32
|
+
element.click()
|
|
33
|
+
except Exception:
|
|
34
|
+
driver.execute_script("arguments[0].focus();", element)
|
|
35
|
+
|
|
36
|
+
ab1 = ActionBuilder(driver)
|
|
37
|
+
ab1.key_action.key_down(Keys.CONTROL)
|
|
38
|
+
ab1.key_action.send_keys('a')
|
|
39
|
+
ab1.key_action.key_up(Keys.CONTROL)
|
|
40
|
+
ab1.perform()
|
|
41
|
+
|
|
42
|
+
ab2 = ActionBuilder(driver)
|
|
43
|
+
ab2.key_action.send_keys(Keys.DELETE)
|
|
44
|
+
ab2.perform()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _clear_coord_runner(driver, x, y, ctx):
|
|
48
|
+
ab1 = ActionBuilder(driver)
|
|
49
|
+
ab1.pointer_action.move_to_location(x, y)
|
|
50
|
+
ab1.pointer_action.click()
|
|
51
|
+
ab1.perform()
|
|
52
|
+
|
|
53
|
+
ab2 = ActionBuilder(driver)
|
|
54
|
+
ab2.key_action.key_down(Keys.CONTROL)
|
|
55
|
+
ab2.key_action.send_keys('a')
|
|
56
|
+
ab2.key_action.key_up(Keys.CONTROL)
|
|
57
|
+
ab2.perform()
|
|
58
|
+
|
|
59
|
+
ab3 = ActionBuilder(driver)
|
|
60
|
+
ab3.key_action.send_keys(Keys.DELETE)
|
|
61
|
+
ab3.perform()
|
|
62
|
+
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_CLEAR_SPEC = _ActionSpec(runner=_clear_runner, coord_runner=_clear_coord_runner)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def clear(driver, selector, *, description='', tiers=None, autoheal=True,
|
|
70
|
+
max_attempts=4, retry_delay=0.5, search_root=None):
|
|
71
|
+
"""Find element and clear it, retrying with heal cascade on recoverable errors."""
|
|
72
|
+
return _run_action(
|
|
73
|
+
driver, _CLEAR_SPEC, selector,
|
|
74
|
+
description=description, tiers=tiers, autoheal=autoheal,
|
|
75
|
+
max_attempts=max_attempts, retry_delay=retry_delay,
|
|
76
|
+
search_root=search_root,
|
|
77
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""High-level click action — find element, click with strategy, heal on failure."""
|
|
2
|
+
from selenium.webdriver.common.actions.action_builder import ActionBuilder
|
|
3
|
+
|
|
4
|
+
from testmu_selenium._action_engine import _ActionSpec, _run_action
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _click_runner(element, ctx):
|
|
8
|
+
return element.clickElement(
|
|
9
|
+
ctx['driver'],
|
|
10
|
+
ctx.get('strategy', 'se_js_ac'),
|
|
11
|
+
ctx.get('modifiers'),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Coordinate-tier fallback. Heal cascade exhausted all selector-based tiers and
|
|
16
|
+
# returned viewport pixel coords — i.e. a canvas/vision target with no DOM node
|
|
17
|
+
# (a DOM target heals to a selector and never reaches here). REAL-click at those
|
|
18
|
+
# coords via ActionBuilder pointer actions, NOT elementFromPoint(x,y).click():
|
|
19
|
+
# a JS .click() synthesizes a MouseEvent with clientX/clientY = 0, so a canvas
|
|
20
|
+
# onclick that reads e.clientX grounds to the wrong location. A real pointer
|
|
21
|
+
# click carries the true coords. strategy/modifiers are not honoured on the
|
|
22
|
+
# coord fallback — visual-location click only.
|
|
23
|
+
def _click_coord_runner(driver, x, y, ctx):
|
|
24
|
+
_ = ctx # strategy/modifiers ignored on coord-fallback — visual-location click only
|
|
25
|
+
ab = ActionBuilder(driver)
|
|
26
|
+
ab.pointer_action.move_to_location(x, y)
|
|
27
|
+
ab.pointer_action.click()
|
|
28
|
+
ab.perform()
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Recoverable set inherits the engine default — see _DEFAULT_RECOVERABLE
|
|
33
|
+
# in _action_engine. Keeps click in lockstep with future additions/removals.
|
|
34
|
+
_CLICK_SPEC = _ActionSpec(runner=_click_runner, coord_runner=_click_coord_runner)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def click(driver, selector, *, strategy='se_js_ac', modifiers=None,
|
|
38
|
+
description='', tiers=None, autoheal=True,
|
|
39
|
+
max_attempts=4, retry_delay=0.5, search_root=None):
|
|
40
|
+
"""Find element and click it, retrying with heal cascade on recoverable errors.
|
|
41
|
+
|
|
42
|
+
Equivalent to the codegen-emitted retry-loop pattern but encapsulated.
|
|
43
|
+
|
|
44
|
+
search_root: optional WebElement (e.g. a shadow-root child) to resolve the
|
|
45
|
+
selector against for shadow-DOM piercing. Only the element lookup uses it;
|
|
46
|
+
the click is always dispatched via driver. None = top-level document lookup.
|
|
47
|
+
"""
|
|
48
|
+
return _run_action(
|
|
49
|
+
driver, _CLICK_SPEC, selector,
|
|
50
|
+
description=description, tiers=tiers, autoheal=autoheal,
|
|
51
|
+
max_attempts=max_attempts, retry_delay=retry_delay,
|
|
52
|
+
search_root=search_root,
|
|
53
|
+
strategy=strategy, modifiers=modifiers,
|
|
54
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""drag_drop wrapper — element-pair drag.
|
|
2
|
+
|
|
3
|
+
Wrapper takes already-resolved WebElements. Heal cascade lives upstream in
|
|
4
|
+
FindElementNode's emit (findElement runtime helper). No _run_action wrap.
|
|
5
|
+
|
|
6
|
+
Ported from the existing Selenium drag implementation.
|
|
7
|
+
The (0.1, 0.1) move_by_offset before release is deliberate — some frameworks
|
|
8
|
+
need to register an event that a draggable is hovering over the drop zone to
|
|
9
|
+
activate it for the drop.
|
|
10
|
+
"""
|
|
11
|
+
from selenium.webdriver.common.action_chains import ActionChains
|
|
12
|
+
|
|
13
|
+
# Drop-zone activation jiggle. Hardcoded.
|
|
14
|
+
_DROP_ZONE_NUDGE_X = 0.1
|
|
15
|
+
_DROP_ZONE_NUDGE_Y = 0.1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def drag_drop(driver, source_element, target_element):
|
|
19
|
+
"""Element-pair drag with drop-zone activation jiggle.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
driver: Selenium WebDriver.
|
|
23
|
+
source_element: Already-resolved WebElement to drag from.
|
|
24
|
+
target_element: Already-resolved WebElement to drop onto.
|
|
25
|
+
"""
|
|
26
|
+
ActionChains(driver) \
|
|
27
|
+
.move_to_element(source_element) \
|
|
28
|
+
.click_and_hold(source_element) \
|
|
29
|
+
.move_to_element(target_element) \
|
|
30
|
+
.move_by_offset(_DROP_ZONE_NUDGE_X, _DROP_ZONE_NUDGE_Y) \
|
|
31
|
+
.release() \
|
|
32
|
+
.perform()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""element_drag wrapper — element-relative drag with integer step loop.
|
|
2
|
+
|
|
3
|
+
Wrapper takes an already-resolved WebElement. Heal cascade lives upstream in
|
|
4
|
+
FindElementNode's emit. No _run_action wrap.
|
|
5
|
+
|
|
6
|
+
Ported from the existing Selenium drag implementation (non-canvas branch).
|
|
7
|
+
The 5px step + 0.01s pause is required for sliders,
|
|
8
|
+
scrubbers, and signature-pad-like UIs that fire oninput on motion samples.
|
|
9
|
+
|
|
10
|
+
Selenium 4.x's PointerActions.move_by truncates both axes via int(x), int(y).
|
|
11
|
+
A naive float step (dx/steps) therefore drops the fractional part on every
|
|
12
|
+
iteration and the cumulative drag undershoots the target — e.g. (-352, -239)
|
|
13
|
+
becomes 71 × (-4, -3) = (-284, -213), 60-70px short of the drop zone. Use
|
|
14
|
+
integer ±5 step + remainder per the existing runtime.
|
|
15
|
+
"""
|
|
16
|
+
from selenium.webdriver.common.action_chains import ActionChains
|
|
17
|
+
|
|
18
|
+
# Step granularity and inter-step pause. Hardcoded.
|
|
19
|
+
_DRAG_STEP_PX = 5
|
|
20
|
+
_DRAG_STEP_PAUSE = 0.01
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def element_drag(driver, element, dx, dy):
|
|
24
|
+
"""Element-relative drag with integer 5px step + remainder loop.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
driver: Selenium WebDriver.
|
|
28
|
+
element: Already-resolved source WebElement.
|
|
29
|
+
dx: X-axis delta in pixels.
|
|
30
|
+
dy: Y-axis delta in pixels.
|
|
31
|
+
"""
|
|
32
|
+
dx = int(dx)
|
|
33
|
+
dy = int(dy)
|
|
34
|
+
|
|
35
|
+
actions = ActionChains(driver)
|
|
36
|
+
actions.click_and_hold(element)
|
|
37
|
+
|
|
38
|
+
remaining_x = dx
|
|
39
|
+
remaining_y = dy
|
|
40
|
+
|
|
41
|
+
while remaining_x != 0 or remaining_y != 0:
|
|
42
|
+
if abs(remaining_x) >= _DRAG_STEP_PX:
|
|
43
|
+
step_x = _DRAG_STEP_PX if remaining_x > 0 else -_DRAG_STEP_PX
|
|
44
|
+
else:
|
|
45
|
+
step_x = remaining_x
|
|
46
|
+
|
|
47
|
+
if abs(remaining_y) >= _DRAG_STEP_PX:
|
|
48
|
+
step_y = _DRAG_STEP_PX if remaining_y > 0 else -_DRAG_STEP_PX
|
|
49
|
+
else:
|
|
50
|
+
step_y = remaining_y
|
|
51
|
+
|
|
52
|
+
actions.move_by_offset(step_x, step_y)
|
|
53
|
+
actions.pause(_DRAG_STEP_PAUSE)
|
|
54
|
+
|
|
55
|
+
remaining_x -= step_x
|
|
56
|
+
remaining_y -= step_y
|
|
57
|
+
|
|
58
|
+
actions.release()
|
|
59
|
+
actions.perform()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Internal find+heal+act engine used by high-level action wrappers.
|
|
2
|
+
|
|
3
|
+
Public API to the rest of the package: _ActionSpec dataclass + _run_action.
|
|
4
|
+
Public API of the package: the per-verb wrappers in _action_click.py and
|
|
5
|
+
_action_type.py — they're the only thing codegen and human test authors see.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
_log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
from selenium.common.exceptions import (
|
|
15
|
+
NoSuchElementException, StaleElementReferenceException,
|
|
16
|
+
ElementClickInterceptedException, ElementNotInteractableException, TimeoutException,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from testmu_selenium._helpers.find_element import findElement
|
|
20
|
+
from testmu_selenium._heal_cascade import _heal_cascade
|
|
21
|
+
from testmu_selenium._helpers.smart_wait import SmartWait
|
|
22
|
+
from testmu_selenium._step import _current_step
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Relocate cascade for element actions (click/type/search/hover/...). TEXTUAL_QUERY
|
|
26
|
+
# is intentionally absent: textual-query autoheal belongs only to the textual_query
|
|
27
|
+
# action's _direct_textual_read (the textual_query endpoint read), not to the
|
|
28
|
+
# relocate cascade — the endpoint returns a read value, not a locator.
|
|
29
|
+
#
|
|
30
|
+
# Tier order is COORDINATE → VISION_QUERY. Both are vision-grounded: they look at a
|
|
31
|
+
# screenshot for the intent and fail honestly when the element is genuinely absent.
|
|
32
|
+
# LIST_XPATHS was removed from the default cascade: as the only non-vision tier it
|
|
33
|
+
# re-ranks xpaths from the DOM and returns *a* plausible match even when the real
|
|
34
|
+
# target is gone, turning a real failure into a false PASS (COORDINATE + VISION_QUERY
|
|
35
|
+
# both correctly missed the target element, then LIST_XPATHS relocated to an unrelated
|
|
36
|
+
# banner and the step passed green). LIST_XPATHS only ever ran after both vision tiers
|
|
37
|
+
# reported the element not visible — exactly when a structural xpath match is least
|
|
38
|
+
# trustworthy. Dropping it matches the existing runtime's desktop-resolver policy, which fails honestly
|
|
39
|
+
# with no legacy-xpath fallback. The LIST_XPATHS tier implementation still exists and
|
|
40
|
+
# can be opted into via an explicit tiers= argument.
|
|
41
|
+
_DEFAULT_HEAL_TIERS = ('COORDINATE', 'VISION_QUERY')
|
|
42
|
+
|
|
43
|
+
# Default recoverable set — covers the common stale/intercepted/timeout cases
|
|
44
|
+
# wrappers can extend or shrink this via their own _ActionSpec.
|
|
45
|
+
_DEFAULT_RECOVERABLE = (
|
|
46
|
+
NoSuchElementException, StaleElementReferenceException,
|
|
47
|
+
ElementClickInterceptedException, ElementNotInteractableException,
|
|
48
|
+
TimeoutException,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class _ActionSpec:
|
|
54
|
+
"""Describes how to run a single high-level action verb.
|
|
55
|
+
|
|
56
|
+
runner: takes (element, ctx_dict) and performs the verb. ctx_dict
|
|
57
|
+
includes 'driver' plus any wrapper-specific kwargs.
|
|
58
|
+
recoverable_exceptions: tuple of exception types that trigger a heal
|
|
59
|
+
attempt. Other exceptions propagate immediately.
|
|
60
|
+
coord_runner: optional fallback dispatched when the heal cascade
|
|
61
|
+
resolves to viewport pixel coordinates (HealResult.coordinates,
|
|
62
|
+
COORDINATE tier) rather than a real CSS/XPath. Signature:
|
|
63
|
+
coord_runner(driver, x, y, ctx) -> Any. Verbs that omit it
|
|
64
|
+
cause _run_action to raise NotImplementedError when COORDINATE
|
|
65
|
+
heal fires — preferred over silently propagating a placeholder
|
|
66
|
+
back into findElement (pre-2026-05-01 behaviour).
|
|
67
|
+
"""
|
|
68
|
+
runner: Callable[[Any, dict], Any]
|
|
69
|
+
recoverable_exceptions: tuple = _DEFAULT_RECOVERABLE
|
|
70
|
+
coord_runner: Callable[[Any, int, int, dict], Any] | None = None
|
|
71
|
+
op_type: str = "click"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _run_action(
|
|
75
|
+
driver,
|
|
76
|
+
spec: _ActionSpec,
|
|
77
|
+
primary_selector,
|
|
78
|
+
*,
|
|
79
|
+
description: str = '',
|
|
80
|
+
tiers=None,
|
|
81
|
+
autoheal: bool = True,
|
|
82
|
+
max_attempts: int = 4,
|
|
83
|
+
retry_delay: float = 0.5,
|
|
84
|
+
search_root=None,
|
|
85
|
+
**runner_kwargs,
|
|
86
|
+
):
|
|
87
|
+
"""Find element → invoke spec.runner. On recoverable exception with
|
|
88
|
+
autoheal=True, run heal cascade and retry up to max_attempts.
|
|
89
|
+
|
|
90
|
+
Returns spec.runner's return value on success.
|
|
91
|
+
Raises spec's recoverable exception on final attempt failure.
|
|
92
|
+
Raises AutohealExhausted from heal_cascade if all tiers miss.
|
|
93
|
+
Non-recoverable exceptions propagate immediately.
|
|
94
|
+
"""
|
|
95
|
+
if tiers is None:
|
|
96
|
+
tiers = list(_DEFAULT_HEAL_TIERS)
|
|
97
|
+
_log.info(" [AutoHeal] autoheal=%s", autoheal)
|
|
98
|
+
selector = primary_selector
|
|
99
|
+
frame_info = None
|
|
100
|
+
first_exc = None
|
|
101
|
+
SmartWait(driver).smart_wait(is_vision=False)
|
|
102
|
+
for attempt in range(max_attempts):
|
|
103
|
+
# Logged before findElement's own "Finding element..." trace.
|
|
104
|
+
_log.info(" is_retry: %s", attempt > 0)
|
|
105
|
+
try:
|
|
106
|
+
el = findElement(driver, selector, description=description, allow_autoheal=False,
|
|
107
|
+
search_root=search_root)
|
|
108
|
+
ctx = {'driver': driver, 'frame_info': frame_info, **runner_kwargs}
|
|
109
|
+
return spec.runner(el, ctx)
|
|
110
|
+
except spec.recoverable_exceptions as exc:
|
|
111
|
+
if first_exc is None:
|
|
112
|
+
first_exc = exc
|
|
113
|
+
if not autoheal:
|
|
114
|
+
raise
|
|
115
|
+
if attempt == max_attempts - 1:
|
|
116
|
+
raise exc from first_exc
|
|
117
|
+
_log.info(
|
|
118
|
+
" [retry] attempt %d/%d after %s: %s",
|
|
119
|
+
attempt + 1, max_attempts, type(exc).__name__, exc,
|
|
120
|
+
)
|
|
121
|
+
_active_step = _current_step.get(None)
|
|
122
|
+
if _active_step is not None:
|
|
123
|
+
_active_step.auto_heal = True
|
|
124
|
+
heal_result = _heal_cascade(
|
|
125
|
+
description=description,
|
|
126
|
+
current_selector=selector,
|
|
127
|
+
current_frame_info=frame_info,
|
|
128
|
+
tiers=tiers,
|
|
129
|
+
exception=exc,
|
|
130
|
+
driver=driver,
|
|
131
|
+
op_type=spec.op_type,
|
|
132
|
+
)
|
|
133
|
+
# COORDINATE-tier short-circuit — the cascade has no actionable
|
|
134
|
+
# selector but the vision/locate service returned viewport pixel coords. Dispatch
|
|
135
|
+
# via spec.coord_runner instead of re-entering findElement; doing
|
|
136
|
+
# otherwise crashes Chrome with InvalidSelectorException on a
|
|
137
|
+
# synthetic 'coord:x,y' placeholder.
|
|
138
|
+
if heal_result.coordinates is not None:
|
|
139
|
+
_log.info(
|
|
140
|
+
" [AutoHeal] relocated via %s -> coordinates %s",
|
|
141
|
+
heal_result.tier_used, heal_result.coordinates,
|
|
142
|
+
)
|
|
143
|
+
if spec.coord_runner is None:
|
|
144
|
+
raise NotImplementedError(
|
|
145
|
+
f"heal cascade resolved to coordinates {heal_result.coordinates} "
|
|
146
|
+
f"via tier {heal_result.tier_used!r}, but spec has no coord_runner. "
|
|
147
|
+
f"Either provide one on _ActionSpec or drop COORDINATE from tiers."
|
|
148
|
+
)
|
|
149
|
+
ctx = {'driver': driver, 'frame_info': heal_result.frame_info, **runner_kwargs}
|
|
150
|
+
x, y = heal_result.coordinates
|
|
151
|
+
return spec.coord_runner(driver, x, y, ctx)
|
|
152
|
+
# Belt+suspenders: synthetic 'coord:x,y' placeholders must never
|
|
153
|
+
# appear in HealResult.selectors. Pre-fix the COORDINATE tier
|
|
154
|
+
# produced them and Chrome rejected the next findElement with
|
|
155
|
+
# InvalidSelectorException. Surface future regressions here
|
|
156
|
+
# rather than in a cluster log.
|
|
157
|
+
for s in heal_result.selectors or []:
|
|
158
|
+
sel = s.get("selector") if isinstance(s, dict) else None
|
|
159
|
+
if isinstance(sel, str) and sel.startswith("coord:"):
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"HealResult.selectors contains synthetic 'coord:' placeholder: {s}. "
|
|
162
|
+
f"COORDINATE tier must use HealResult.coordinates instead."
|
|
163
|
+
)
|
|
164
|
+
selector = heal_result.selectors
|
|
165
|
+
frame_info = heal_result.frame_info
|
|
166
|
+
_log.info(
|
|
167
|
+
" [AutoHeal] relocated via %s -> %s",
|
|
168
|
+
heal_result.tier_used,
|
|
169
|
+
[s.get("selector") for s in heal_result.selectors],
|
|
170
|
+
)
|
|
171
|
+
time.sleep(retry_delay)
|