selenium-python-ai-agent 0.2.0__py3-none-any.whl

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 (35) hide show
  1. selenium_agent/__init__.py +20 -0
  2. selenium_agent/agents/__init__.py +5 -0
  3. selenium_agent/agents/coder.py +505 -0
  4. selenium_agent/agents/definitions.py +152 -0
  5. selenium_agent/agents/healer.py +638 -0
  6. selenium_agent/agents/planner.py +279 -0
  7. selenium_agent/bdd/__init__.py +15 -0
  8. selenium_agent/bdd/gherkin_advisor.py +189 -0
  9. selenium_agent/bdd/templates.py +98 -0
  10. selenium_agent/cli.py +314 -0
  11. selenium_agent/core/__init__.py +3 -0
  12. selenium_agent/core/orchestrator.py +189 -0
  13. selenium_agent/scanner/__init__.py +3 -0
  14. selenium_agent/scanner/project_scanner.py +452 -0
  15. selenium_agent/selenium/__init__.py +6 -0
  16. selenium_agent/selenium/base_page.py +447 -0
  17. selenium_agent/selenium/driver_factory.py +222 -0
  18. selenium_agent/selenium/error_map.py +235 -0
  19. selenium_agent/selenium/locator_advisor.py +247 -0
  20. selenium_agent/selenium/locator_scanner.py +502 -0
  21. selenium_agent/utils/__init__.py +26 -0
  22. selenium_agent/utils/code_validator.py +96 -0
  23. selenium_agent/utils/config_manager.py +81 -0
  24. selenium_agent/utils/json_utils.py +103 -0
  25. selenium_agent/utils/llm.py +240 -0
  26. selenium_agent/utils/logger.py +37 -0
  27. selenium_agent/utils/paths.py +57 -0
  28. selenium_agent/utils/spec_writer.py +126 -0
  29. selenium_agent/utils/url_extractor.py +52 -0
  30. selenium_python_ai_agent-0.2.0.dist-info/METADATA +412 -0
  31. selenium_python_ai_agent-0.2.0.dist-info/RECORD +35 -0
  32. selenium_python_ai_agent-0.2.0.dist-info/WHEEL +5 -0
  33. selenium_python_ai_agent-0.2.0.dist-info/entry_points.txt +2 -0
  34. selenium_python_ai_agent-0.2.0.dist-info/licenses/LICENSE +21 -0
  35. selenium_python_ai_agent-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,235 @@
1
+ """
2
+ SELENIUM ERROR MAP
3
+ ==================
4
+ Maps known Selenium exceptions to root causes and exact fixes.
5
+
6
+ The Healer Agent uses this to:
7
+ - Identify error type from pytest output
8
+ - Apply targeted fix before asking the LLM
9
+ - Reduce LLM calls for common/known errors
10
+
11
+ This is what makes the Healer truly Selenium-specific,
12
+ not just a generic "ask LLM to fix" agent.
13
+ """
14
+
15
+ from __future__ import annotations
16
+ from dataclasses import dataclass
17
+
18
+
19
+ @dataclass
20
+ class ErrorFix:
21
+ """A known Selenium error with its cause and fix."""
22
+ exception: str # Exception class name
23
+ pattern: str # String to search for in error output
24
+ cause: str # What causes this error
25
+ fix: str # How to fix it in code
26
+ code_before: str # Example of broken code
27
+ code_after: str # Example of fixed code
28
+ severity: str # low | medium | high
29
+
30
+
31
+ class SeleniumErrorMap:
32
+ """
33
+ Lookup table of known Selenium errors → targeted fixes.
34
+
35
+ Used by the Healer Agent to apply precise fixes
36
+ before falling back to LLM healing.
37
+ """
38
+
39
+ KNOWN_ERRORS: list[ErrorFix] = [
40
+
41
+ ErrorFix(
42
+ exception="NoSuchElementException",
43
+ pattern="no such element",
44
+ cause="Element not found in DOM — wrong locator or page not loaded",
45
+ fix="Add WebDriverWait for element presence, or fix the locator",
46
+ code_before='driver.find_element(By.ID, "username")',
47
+ code_after='WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "username")))',
48
+ severity="high",
49
+ ),
50
+
51
+ ErrorFix(
52
+ exception="TimeoutException (wait_for_url)",
53
+ pattern="wait_for_url",
54
+ cause="The URL never changed after the action — many SPAs show the result "
55
+ "on the SAME page (status badge, toast, message) without navigating",
56
+ fix="Do NOT increase the timeout. Remove wait_for_url and instead assert an "
57
+ "in-page success indicator from the DOM scan (a status element whose text "
58
+ "changes, a success message, etc.)",
59
+ code_before="page.click(page.LOGIN_BUTTON)\npage.wait_for_url('dashboard')",
60
+ code_after="page.click(page.LOGIN_BUTTON)\n"
61
+ "# SPA: success shows on the same page — assert the status element\n"
62
+ "page.wait_for_text(page.LOGIN_STATUS, 'AUTHENTICATED')\n"
63
+ "assert 'AUTHENTICATED' in page.get_text(page.LOGIN_STATUS)",
64
+ severity="high",
65
+ ),
66
+
67
+ ErrorFix(
68
+ exception="TimeoutException",
69
+ pattern="timeout",
70
+ cause="Element did not appear within the wait timeout",
71
+ fix="Increase timeout, verify locator is correct, or check if page loaded",
72
+ code_before='WebDriverWait(driver, 5).until(EC.visibility_of_element_located((By.ID, "result")))',
73
+ code_after='WebDriverWait(driver, 15).until(EC.visibility_of_element_located((By.ID, "result")))',
74
+ severity="medium",
75
+ ),
76
+
77
+ ErrorFix(
78
+ exception="StaleElementReferenceException",
79
+ pattern="stale element reference",
80
+ cause="Element was found, then DOM was refreshed/updated — reference is now stale",
81
+ fix="Re-fetch the element inside the action, or use a retry wrapper",
82
+ code_before="""element = driver.find_element(By.ID, "btn")
83
+ page.load_more()
84
+ element.click() # stale!""",
85
+ code_after="""page.load_more()
86
+ # Re-fetch after DOM change
87
+ WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "btn"))).click()""",
88
+ severity="high",
89
+ ),
90
+
91
+ ErrorFix(
92
+ exception="ElementNotInteractableException",
93
+ pattern="element not interactable",
94
+ cause="Element exists but is hidden, disabled, or off-screen",
95
+ fix="Scroll element into view, wait for it to be visible, or use JS click",
96
+ code_before='driver.find_element(By.ID, "submit").click()',
97
+ code_after="""element = WebDriverWait(driver, 10).until(
98
+ EC.element_to_be_clickable((By.ID, "submit"))
99
+ )
100
+ driver.execute_script("arguments[0].scrollIntoView(true);", element)
101
+ element.click()""",
102
+ severity="medium",
103
+ ),
104
+
105
+ ErrorFix(
106
+ exception="ElementClickInterceptedException",
107
+ pattern="element click intercepted",
108
+ cause="Another element (overlay, modal, cookie banner) is blocking the click",
109
+ fix="Dismiss the overlay first, or use JS click as fallback",
110
+ code_before='driver.find_element(By.ID, "checkout").click()',
111
+ code_after="""# Option 1: Dismiss overlay if present
112
+ try:
113
+ WebDriverWait(driver, 3).until(
114
+ EC.element_to_be_clickable((By.CSS_SELECTOR, ".cookie-accept"))
115
+ ).click()
116
+ except TimeoutException:
117
+ pass
118
+
119
+ # Option 2: JS click fallback
120
+ element = WebDriverWait(driver, 10).until(
121
+ EC.presence_of_element_located((By.ID, "checkout"))
122
+ )
123
+ driver.execute_script("arguments[0].click();", element)""",
124
+ severity="medium",
125
+ ),
126
+
127
+ ErrorFix(
128
+ exception="MoveTargetOutOfBoundsException",
129
+ pattern="move target out of bounds",
130
+ cause="Element is outside the current viewport",
131
+ fix="Scroll element into view before hover/click",
132
+ code_before='ActionChains(driver).move_to_element(element).perform()',
133
+ code_after="""driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
134
+ ActionChains(driver).move_to_element(element).perform()""",
135
+ severity="low",
136
+ ),
137
+
138
+ ErrorFix(
139
+ exception="WebDriverException (session)",
140
+ pattern="invalid session id",
141
+ cause="WebDriver session was closed or crashed",
142
+ fix="Ensure driver.quit() is in teardown, add session check in fixture",
143
+ code_before="""# Missing teardown
144
+ def test_login(driver):
145
+ driver.get("https://example.com")""",
146
+ code_after="""@pytest.fixture
147
+ def driver():
148
+ d = DriverFactory.create(browser="chrome", headless=True)
149
+ yield d
150
+ d.quit() # Always called, even on failure""",
151
+ severity="high",
152
+ ),
153
+
154
+ ErrorFix(
155
+ exception="ImplicitWaitConflict",
156
+ pattern="implicit wait",
157
+ cause="Mixing implicit and explicit waits causes unpredictable timeouts",
158
+ fix="Remove implicit waits — use only WebDriverWait (explicit) everywhere",
159
+ code_before='driver.implicitly_wait(10) # conflicts with WebDriverWait',
160
+ code_after="""# Remove implicit wait
161
+ # Use explicit wait everywhere:
162
+ WebDriverWait(driver, 10).until(EC.visibility_of_element_located(locator))""",
163
+ severity="medium",
164
+ ),
165
+
166
+ ErrorFix(
167
+ exception="NoAlertPresentException",
168
+ pattern="no alert",
169
+ cause="Trying to switch to alert before it appears",
170
+ fix="Wait for alert to be present using EC.alert_is_present()",
171
+ code_before='alert = driver.switch_to.alert',
172
+ code_after="""WebDriverWait(driver, 5).until(EC.alert_is_present())
173
+ alert = driver.switch_to.alert""",
174
+ severity="low",
175
+ ),
176
+
177
+ ErrorFix(
178
+ exception="UnexpectedAlertPresentException",
179
+ pattern="unexpected alert",
180
+ cause="An unexpected JS alert appeared and blocked the driver",
181
+ fix="Handle alert in teardown or dismiss it before continuing",
182
+ code_before="# No alert handling",
183
+ code_after="""try:
184
+ WebDriverWait(driver, 2).until(EC.alert_is_present())
185
+ driver.switch_to.alert.dismiss()
186
+ except TimeoutException:
187
+ pass""",
188
+ severity="medium",
189
+ ),
190
+ ]
191
+
192
+ @classmethod
193
+ def find_fix(cls, error_output: str) -> ErrorFix | None:
194
+ """
195
+ Search error output for known patterns and return fix.
196
+
197
+ Args:
198
+ error_output: Full pytest error/traceback string
199
+
200
+ Returns:
201
+ ErrorFix if a known error is found, None otherwise
202
+ """
203
+ lower_output = error_output.lower()
204
+ for error in cls.KNOWN_ERRORS:
205
+ if error.pattern.lower() in lower_output:
206
+ return error
207
+ return None
208
+
209
+ @classmethod
210
+ def find_all_fixes(cls, error_output: str) -> list[ErrorFix]:
211
+ """Find ALL matching errors in the output."""
212
+ lower_output = error_output.lower()
213
+ return [e for e in cls.KNOWN_ERRORS if e.pattern.lower() in lower_output]
214
+
215
+ @classmethod
216
+ def get_fix_summary(cls, error_output: str) -> str:
217
+ """Return a human-readable fix summary for the Healer Agent prompt."""
218
+ fixes = cls.find_all_fixes(error_output)
219
+ if not fixes:
220
+ return "No known Selenium error pattern matched. LLM analysis required."
221
+
222
+ lines = ["KNOWN SELENIUM ERRORS DETECTED:\n"]
223
+ for fix in fixes:
224
+ lines.append(f"❌ {fix.exception}")
225
+ lines.append(f" Cause: {fix.cause}")
226
+ lines.append(f" Fix: {fix.fix}")
227
+ lines.append(f" Before:\n{fix.code_before}")
228
+ lines.append(f" After:\n{fix.code_after}\n")
229
+
230
+ return "\n".join(lines)
231
+
232
+ @classmethod
233
+ def list_all(cls) -> list[str]:
234
+ """Return list of all known exception names."""
235
+ return [e.exception for e in cls.KNOWN_ERRORS]
@@ -0,0 +1,247 @@
1
+ """
2
+ LOCATOR ADVISOR
3
+ ===============
4
+ Selenium-specific intelligence for choosing the best locator strategy.
5
+
6
+ The Planner and Coder agents use this to:
7
+ - Rank locator strategies by reliability
8
+ - Detect anti-patterns in XPath/CSS
9
+ - Suggest better alternatives
10
+ - Embed locator guidance in generated code
11
+
12
+ Locator Priority (most → least reliable):
13
+ 1. By.ID — unique, fast, stable
14
+ 2. By.NAME — good for form fields
15
+ 3. By.CSS_SELECTOR — flexible, fast, readable
16
+ 4. By.XPATH — powerful but fragile if absolute
17
+ 5. By.LINK_TEXT — brittle, language-dependent
18
+ 6. By.CLASS_NAME — often not unique
19
+ 7. By.TAG_NAME — almost never unique
20
+ """
21
+
22
+ from __future__ import annotations
23
+ from dataclasses import dataclass
24
+ from enum import IntEnum
25
+
26
+
27
+ class LocatorScore(IntEnum):
28
+ """Reliability score for locator strategies (higher = better)."""
29
+ ID = 100
30
+ NAME = 90
31
+ CSS_SELECTOR = 80
32
+ XPATH_RELATIVE = 70
33
+ DATA_TESTID = 95 # data-testid is best practice
34
+ ARIA_LABEL = 75
35
+ LINK_TEXT = 50
36
+ PARTIAL_LINK_TEXT = 45
37
+ CLASS_NAME = 40
38
+ TAG_NAME = 10
39
+ XPATH_ABSOLUTE = 5 # Absolute XPath — worst
40
+
41
+
42
+ @dataclass
43
+ class LocatorSuggestion:
44
+ """A single locator suggestion with explanation."""
45
+ strategy: str # By.ID, By.CSS_SELECTOR, etc.
46
+ value: str # The locator value
47
+ score: int # Reliability score
48
+ reason: str # Why this was suggested
49
+ warning: str = "" # Any anti-pattern warning
50
+
51
+
52
+ class LocatorAdvisor:
53
+ """
54
+ Advises on the best Selenium locator strategy.
55
+
56
+ Used by Planner Agent to embed locator guidance in test plans,
57
+ and by Coder Agent to generate reliable locators.
58
+ """
59
+
60
+ # Anti-patterns to warn about
61
+ ANTI_PATTERNS = {
62
+ "absolute_xpath": {
63
+ "pattern": r"^/html/body",
64
+ "warning": "Absolute XPath is extremely fragile — breaks on any DOM change.",
65
+ "suggestion": "Use By.ID or By.CSS_SELECTOR instead."
66
+ },
67
+ "index_xpath": {
68
+ "pattern": r"\[\d+\]",
69
+ "warning": "Index-based XPath (e.g. [2]) breaks when new elements are added.",
70
+ "suggestion": "Use a unique attribute like id, name, or data-testid."
71
+ },
72
+ "dynamic_class": {
73
+ "pattern": r"[a-z0-9]{8,}", # Random-looking class names
74
+ "warning": "This class name looks auto-generated and may change on rebuild.",
75
+ "suggestion": "Ask developers to add stable data-testid attributes."
76
+ },
77
+ }
78
+
79
+ # Locator strategy priority list (for Coder Agent prompt injection)
80
+ PRIORITY_GUIDE = """
81
+ SELENIUM LOCATOR STRATEGY — Priority Order:
82
+
83
+ 1. data-testid attribute (BEST)
84
+ css = "[data-testid='login-button']"
85
+ → Stable, developer-intended for testing
86
+
87
+ 2. By.ID (EXCELLENT)
88
+ By.ID, "username"
89
+ → Fast, unique per page spec
90
+
91
+ 3. By.NAME (GOOD for forms)
92
+ By.NAME, "email"
93
+ → Stable for form inputs
94
+
95
+ 4. By.CSS_SELECTOR (GOOD)
96
+ By.CSS_SELECTOR, "button.login-btn"
97
+ By.CSS_SELECTOR, "input[placeholder='Email']"
98
+ By.CSS_SELECTOR, "form > button[type='submit']"
99
+ → Readable, faster than XPath
100
+
101
+ 5. Relative By.XPATH (ACCEPTABLE)
102
+ By.XPATH, "//button[contains(text(),'Login')]"
103
+ By.XPATH, "//input[@placeholder='Email']"
104
+ → Use only when CSS won't work
105
+
106
+ 6. By.LINK_TEXT (LIMITED)
107
+ By.LINK_TEXT, "Forgot Password"
108
+ → Only for anchor tags, breaks on text change
109
+
110
+ NEVER USE:
111
+ ❌ Absolute XPath: /html/body/div[1]/form/input[2]
112
+ ❌ Index-based: (//button)[3]
113
+ ❌ Auto-generated classes: .sc-bdXHXe.hQDDGZ
114
+ """
115
+
116
+ @classmethod
117
+ def get_priority_guide(cls) -> str:
118
+ """Return the full locator priority guide for injection into LLM prompts."""
119
+ return cls.PRIORITY_GUIDE
120
+
121
+ @classmethod
122
+ def rank_locators(cls, locators: list[dict]) -> list[LocatorSuggestion]:
123
+ """
124
+ Rank a list of locator options by reliability.
125
+
126
+ Args:
127
+ locators: List of dicts with 'strategy' and 'value' keys
128
+
129
+ Returns:
130
+ Sorted list of LocatorSuggestion (best first)
131
+
132
+ Example:
133
+ ranked = LocatorAdvisor.rank_locators([
134
+ {"strategy": "xpath", "value": "/html/body/div/input"},
135
+ {"strategy": "id", "value": "username"},
136
+ {"strategy": "css", "value": "input.login-field"},
137
+ ])
138
+ """
139
+ strategy_scores = {
140
+ "id": LocatorScore.ID,
141
+ "name": LocatorScore.NAME,
142
+ "css": LocatorScore.CSS_SELECTOR,
143
+ "css_selector": LocatorScore.CSS_SELECTOR,
144
+ "xpath": LocatorScore.XPATH_RELATIVE,
145
+ "link_text": LocatorScore.LINK_TEXT,
146
+ "class_name": LocatorScore.CLASS_NAME,
147
+ "tag_name": LocatorScore.TAG_NAME,
148
+ "data_testid": LocatorScore.DATA_TESTID,
149
+ "aria_label": LocatorScore.ARIA_LABEL,
150
+ }
151
+
152
+ suggestions = []
153
+ for loc in locators:
154
+ strategy = loc.get("strategy", "").lower().replace(" ", "_")
155
+ value = loc.get("value", "")
156
+ score = strategy_scores.get(strategy, 30)
157
+
158
+ # Penalize absolute XPath
159
+ if strategy == "xpath" and value.startswith("/html"):
160
+ score = LocatorScore.XPATH_ABSOLUTE
161
+ warning = "⚠️ Absolute XPath — extremely fragile!"
162
+ elif strategy == "xpath" and "[" in value and value[value.index("[") + 1].isdigit():
163
+ score = max(score - 30, 10)
164
+ warning = "⚠️ Index-based XPath — fragile!"
165
+ elif "[data-testid" in value:
166
+ score = LocatorScore.DATA_TESTID
167
+ warning = ""
168
+ else:
169
+ warning = ""
170
+
171
+ reason = cls._get_reason(strategy, score)
172
+ suggestions.append(LocatorSuggestion(
173
+ strategy=strategy,
174
+ value=value,
175
+ score=score,
176
+ reason=reason,
177
+ warning=warning,
178
+ ))
179
+
180
+ return sorted(suggestions, key=lambda s: s.score, reverse=True)
181
+
182
+ @classmethod
183
+ def best_locator(cls, locators: list[dict]) -> LocatorSuggestion:
184
+ """Return the single best locator from a list."""
185
+ ranked = cls.rank_locators(locators)
186
+ return ranked[0] if ranked else None
187
+
188
+ @classmethod
189
+ def _get_reason(cls, strategy: str, score: int) -> str:
190
+ reasons = {
191
+ "id": "Unique per-page, fastest lookup, most stable",
192
+ "name": "Good for form fields, stable in most frameworks",
193
+ "css": "Readable, fast, flexible — preferred over XPath",
194
+ "css_selector": "Readable, fast, flexible — preferred over XPath",
195
+ "xpath": "Use only when CSS/ID not available",
196
+ "link_text": "Brittle — breaks on text change or i18n",
197
+ "class_name": "Often not unique — combine with other strategies",
198
+ "tag_name": "Almost never unique — avoid",
199
+ "data_testid": "Best practice — developer-defined test hook",
200
+ "aria_label": "Good for accessibility-first apps",
201
+ }
202
+ return reasons.get(strategy, "No specific guidance available")
203
+
204
+ @classmethod
205
+ def validate(cls, strategy: str, value: str) -> dict:
206
+ """
207
+ Validate a single locator and return warnings.
208
+
209
+ Returns:
210
+ dict with 'valid', 'score', 'warnings', 'suggestions'
211
+ """
212
+ import re
213
+ warnings = []
214
+ suggestions = []
215
+
216
+ if strategy.lower() == "xpath":
217
+ if value.startswith("/html"):
218
+ warnings.append("Absolute XPath is extremely fragile")
219
+ suggestions.append("Convert to By.ID or By.CSS_SELECTOR")
220
+
221
+ if re.search(r"\[\d+\]", value):
222
+ warnings.append("Index-based XPath breaks when DOM changes")
223
+ suggestions.append("Use unique attribute selectors instead")
224
+
225
+ if strategy.lower() in ("class_name", "css_selector"):
226
+ if re.search(r"\b[a-z0-9]{8,}\b", value):
227
+ warnings.append("Possible auto-generated class name detected")
228
+ suggestions.append("Request data-testid from developers")
229
+
230
+ score_map = {
231
+ "id": LocatorScore.ID,
232
+ "name": LocatorScore.NAME,
233
+ "css_selector": LocatorScore.CSS_SELECTOR,
234
+ "xpath": LocatorScore.XPATH_RELATIVE if not value.startswith("/html") else LocatorScore.XPATH_ABSOLUTE,
235
+ "link_text": LocatorScore.LINK_TEXT,
236
+ "class_name": LocatorScore.CLASS_NAME,
237
+ "tag_name": LocatorScore.TAG_NAME,
238
+ }
239
+
240
+ score = score_map.get(strategy.lower(), 30)
241
+
242
+ return {
243
+ "valid": len(warnings) == 0,
244
+ "score": score,
245
+ "warnings": warnings,
246
+ "suggestions": suggestions,
247
+ }