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.
Files changed (128) hide show
  1. testmuai_selenium_bindings-0.1.0b1/PKG-INFO +59 -0
  2. testmuai_selenium_bindings-0.1.0b1/README.md +28 -0
  3. testmuai_selenium_bindings-0.1.0b1/pyproject.toml +48 -0
  4. testmuai_selenium_bindings-0.1.0b1/setup.cfg +4 -0
  5. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/__init__.py +132 -0
  6. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_clear.py +77 -0
  7. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_click.py +54 -0
  8. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_drag_drop.py +32 -0
  9. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_element_drag.py +59 -0
  10. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_engine.py +171 -0
  11. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_hover.py +38 -0
  12. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_scroll_into_view.py +65 -0
  13. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_scroll_until_element.py +173 -0
  14. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_search.py +72 -0
  15. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_set_input_files.py +215 -0
  16. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_textual_query.py +138 -0
  17. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_action_type.py +71 -0
  18. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_capability.py +190 -0
  19. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_config.py +115 -0
  20. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_configure.py +40 -0
  21. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_decorator.py +33 -0
  22. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_errors.py +66 -0
  23. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_heal.py +509 -0
  24. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_heal_cascade.py +176 -0
  25. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/__init__.py +0 -0
  26. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/_frame.py +165 -0
  27. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/_http.py +106 -0
  28. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/_screenshot.py +251 -0
  29. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/_tagify.py +27 -0
  30. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/clear_at_coordinate.py +23 -0
  31. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/clear_element.py +73 -0
  32. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/click.py +137 -0
  33. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/dialog.py +41 -0
  34. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/dom_wait.py +22 -0
  35. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/drag_at_coordinate.py +57 -0
  36. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/driver.py +31 -0
  37. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/execute_api.py +406 -0
  38. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/execute_db.py +126 -0
  39. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/execute_js.py +88 -0
  40. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/find_element.py +73 -0
  41. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/input_value.py +328 -0
  42. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/js_templates.py +122 -0
  43. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/math.py +152 -0
  44. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/navigate.py +113 -0
  45. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/navigation.py +29 -0
  46. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/network.py +197 -0
  47. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/scroll.py +186 -0
  48. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/smart_wait.py +256 -0
  49. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/smartui.py +7 -0
  50. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/snapshot.py +134 -0
  51. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/tabs.py +120 -0
  52. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/textual_query.py +225 -0
  53. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/url.py +15 -0
  54. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/vision_query.py +93 -0
  55. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_helpers/wait.py +117 -0
  56. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_route_failure.py +71 -0
  57. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_session.py +206 -0
  58. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_step.py +66 -0
  59. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_test_config.py +83 -0
  60. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/_vars.py +496 -0
  61. testmuai_selenium_bindings-0.1.0b1/testmu_selenium/condition.py +888 -0
  62. testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/PKG-INFO +59 -0
  63. testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/SOURCES.txt +126 -0
  64. testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/dependency_links.txt +1 -0
  65. testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/requires.txt +15 -0
  66. testmuai_selenium_bindings-0.1.0b1/testmuai_selenium_bindings.egg-info/top_level.txt +1 -0
  67. testmuai_selenium_bindings-0.1.0b1/tests/test_action_clear.py +228 -0
  68. testmuai_selenium_bindings-0.1.0b1/tests/test_action_click.py +162 -0
  69. testmuai_selenium_bindings-0.1.0b1/tests/test_action_drag_drop.py +83 -0
  70. testmuai_selenium_bindings-0.1.0b1/tests/test_action_element_drag.py +144 -0
  71. testmuai_selenium_bindings-0.1.0b1/tests/test_action_engine.py +552 -0
  72. testmuai_selenium_bindings-0.1.0b1/tests/test_action_hover.py +134 -0
  73. testmuai_selenium_bindings-0.1.0b1/tests/test_action_scroll_into_view.py +207 -0
  74. testmuai_selenium_bindings-0.1.0b1/tests/test_action_scroll_until_element.py +185 -0
  75. testmuai_selenium_bindings-0.1.0b1/tests/test_action_search.py +221 -0
  76. testmuai_selenium_bindings-0.1.0b1/tests/test_action_set_input_files.py +337 -0
  77. testmuai_selenium_bindings-0.1.0b1/tests/test_action_textual_query.py +471 -0
  78. testmuai_selenium_bindings-0.1.0b1/tests/test_action_type.py +198 -0
  79. testmuai_selenium_bindings-0.1.0b1/tests/test_assertion_parity.py +99 -0
  80. testmuai_selenium_bindings-0.1.0b1/tests/test_capability.py +316 -0
  81. testmuai_selenium_bindings-0.1.0b1/tests/test_check_until_condition.py +96 -0
  82. testmuai_selenium_bindings-0.1.0b1/tests/test_clear_at_coordinate.py +29 -0
  83. testmuai_selenium_bindings-0.1.0b1/tests/test_click.py +118 -0
  84. testmuai_selenium_bindings-0.1.0b1/tests/test_condition.py +234 -0
  85. testmuai_selenium_bindings-0.1.0b1/tests/test_config_run_target.py +58 -0
  86. testmuai_selenium_bindings-0.1.0b1/tests/test_config_url_resolution.py +153 -0
  87. testmuai_selenium_bindings-0.1.0b1/tests/test_config_was_set.py +50 -0
  88. testmuai_selenium_bindings-0.1.0b1/tests/test_configure.py +90 -0
  89. testmuai_selenium_bindings-0.1.0b1/tests/test_decorator.py +55 -0
  90. testmuai_selenium_bindings-0.1.0b1/tests/test_drag_at_coordinate.py +146 -0
  91. testmuai_selenium_bindings-0.1.0b1/tests/test_execute_api.py +100 -0
  92. testmuai_selenium_bindings-0.1.0b1/tests/test_execute_db.py +83 -0
  93. testmuai_selenium_bindings-0.1.0b1/tests/test_find_element.py +150 -0
  94. testmuai_selenium_bindings-0.1.0b1/tests/test_handle_alert.py +68 -0
  95. testmuai_selenium_bindings-0.1.0b1/tests/test_heal_browser_coordinate.py +228 -0
  96. testmuai_selenium_bindings-0.1.0b1/tests/test_heal_cascade.py +521 -0
  97. testmuai_selenium_bindings-0.1.0b1/tests/test_heal_helpers.py +133 -0
  98. testmuai_selenium_bindings-0.1.0b1/tests/test_heal_init.py +227 -0
  99. testmuai_selenium_bindings-0.1.0b1/tests/test_heal_list_xpaths.py +258 -0
  100. testmuai_selenium_bindings-0.1.0b1/tests/test_heal_textual_query.py +291 -0
  101. testmuai_selenium_bindings-0.1.0b1/tests/test_heal_vision_query.py +127 -0
  102. testmuai_selenium_bindings-0.1.0b1/tests/test_helpers.py +333 -0
  103. testmuai_selenium_bindings-0.1.0b1/tests/test_http.py +124 -0
  104. testmuai_selenium_bindings-0.1.0b1/tests/test_input_value.py +296 -0
  105. testmuai_selenium_bindings-0.1.0b1/tests/test_math.py +50 -0
  106. testmuai_selenium_bindings-0.1.0b1/tests/test_navigate.py +202 -0
  107. testmuai_selenium_bindings-0.1.0b1/tests/test_navigation.py +27 -0
  108. testmuai_selenium_bindings-0.1.0b1/tests/test_network_assertion.py +53 -0
  109. testmuai_selenium_bindings-0.1.0b1/tests/test_network_query.py +251 -0
  110. testmuai_selenium_bindings-0.1.0b1/tests/test_public_api.py +71 -0
  111. testmuai_selenium_bindings-0.1.0b1/tests/test_resolve_variable.py +121 -0
  112. testmuai_selenium_bindings-0.1.0b1/tests/test_route_failure.py +197 -0
  113. testmuai_selenium_bindings-0.1.0b1/tests/test_scroll.py +235 -0
  114. testmuai_selenium_bindings-0.1.0b1/tests/test_search_root.py +83 -0
  115. testmuai_selenium_bindings-0.1.0b1/tests/test_session.py +241 -0
  116. testmuai_selenium_bindings-0.1.0b1/tests/test_session_local.py +133 -0
  117. testmuai_selenium_bindings-0.1.0b1/tests/test_session_options.py +55 -0
  118. testmuai_selenium_bindings-0.1.0b1/tests/test_session_pending_failures.py +127 -0
  119. testmuai_selenium_bindings-0.1.0b1/tests/test_smart_wait.py +233 -0
  120. testmuai_selenium_bindings-0.1.0b1/tests/test_smartui.py +61 -0
  121. testmuai_selenium_bindings-0.1.0b1/tests/test_step.py +70 -0
  122. testmuai_selenium_bindings-0.1.0b1/tests/test_tabs.py +169 -0
  123. testmuai_selenium_bindings-0.1.0b1/tests/test_test_config.py +59 -0
  124. testmuai_selenium_bindings-0.1.0b1/tests/test_textual_query.py +208 -0
  125. testmuai_selenium_bindings-0.1.0b1/tests/test_url.py +16 -0
  126. testmuai_selenium_bindings-0.1.0b1/tests/test_vars.py +265 -0
  127. testmuai_selenium_bindings-0.1.0b1/tests/test_vision_query_param_resolution.py +32 -0
  128. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)