selenium-selector-autocorrect 0.1.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.
@@ -0,0 +1,54 @@
1
+ """Selenium Selector AutoCorrect
2
+
3
+ A Python package that automatically corrects Selenium element selectors using AI
4
+ when they fail, reducing test maintenance and improving test reliability.
5
+
6
+ Usage:
7
+ from selenium_selector_autocorrect import install_auto_correct_hook
8
+
9
+ install_auto_correct_hook()
10
+
11
+ Environment Variables:
12
+ LOCAL_AI_API_URL: URL of local AI service (default: http://localhost:8765)
13
+ SELENIUM_AUTO_CORRECT: Enable auto-correction (default: "1")
14
+ SELENIUM_SUGGEST_BETTER: Suggest better selectors for found elements (default: "0")
15
+ SELENIUM_AUTO_UPDATE_TESTS: Auto-update test files with corrections (default: "0")
16
+ """
17
+
18
+ __version__ = "1.0.0"
19
+
20
+ from .ai_providers import AIProvider, LocalAIProvider, configure_provider, get_provider
21
+ from .auto_correct import (
22
+ SelectorAutoCorrect,
23
+ configure_auto_correct,
24
+ get_auto_correct,
25
+ set_auto_correct_enabled,
26
+ )
27
+ from .correction_tracker import (
28
+ CorrectionTracker,
29
+ apply_corrections_to_test_files,
30
+ export_corrections_report,
31
+ get_correction_tracker,
32
+ record_correction,
33
+ )
34
+ from .wait_hook import install_auto_correct_hook, uninstall_auto_correct_hook
35
+
36
+ __all__ = [
37
+ "__version__",
38
+ "SelectorAutoCorrect",
39
+ "get_auto_correct",
40
+ "set_auto_correct_enabled",
41
+ "configure_auto_correct",
42
+ "CorrectionTracker",
43
+ "get_correction_tracker",
44
+ "record_correction",
45
+ "apply_corrections_to_test_files",
46
+ "export_corrections_report",
47
+ "AIProvider",
48
+ "LocalAIProvider",
49
+ "get_provider",
50
+ "configure_provider",
51
+ "install_auto_correct_hook",
52
+ "uninstall_auto_correct_hook",
53
+ ]
54
+
@@ -0,0 +1,123 @@
1
+ """AI provider for selector auto-correction using local AI service."""
2
+
3
+ import logging
4
+ import os
5
+ from abc import ABC, abstractmethod
6
+ from typing import Optional
7
+
8
+ import requests
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class AIProvider(ABC):
14
+ """Abstract base class for AI providers."""
15
+
16
+ @abstractmethod
17
+ def suggest_selector(self, system_prompt: str, user_prompt: str) -> Optional[str]:
18
+ """Request a selector suggestion from the AI service.
19
+
20
+ Args:
21
+ system_prompt: System-level instructions for the AI
22
+ user_prompt: User query with failed selector and page context
23
+
24
+ Returns:
25
+ JSON string with suggestion or None if failed
26
+ """
27
+ pass
28
+
29
+ @abstractmethod
30
+ def is_available(self) -> bool:
31
+ """Check if the provider is available and operational."""
32
+ pass
33
+
34
+
35
+ class LocalAIProvider(AIProvider):
36
+ """AI provider using local AI service with OpenAI-compatible API.
37
+
38
+ Args:
39
+ base_url: URL of the local AI service. If None, reads from
40
+ LOCAL_AI_API_URL environment variable (default: http://localhost:8765)
41
+ """
42
+
43
+ def __init__(self, base_url: str = None):
44
+ self.base_url = base_url or os.environ.get("LOCAL_AI_API_URL", "http://localhost:8765")
45
+ self._available = None
46
+
47
+ def is_available(self) -> bool:
48
+ if self._available is not None:
49
+ return self._available
50
+ try:
51
+ response = requests.post(
52
+ f"{self.base_url}/v1/chat/completions",
53
+ json={"messages": [{"role": "user", "content": "test"}], "max_tokens": 1},
54
+ timeout=5
55
+ )
56
+ self._available = response.status_code in (200, 400)
57
+ except Exception as e:
58
+ logger.info(f"Local AI service not available at {self.base_url}: {e}")
59
+ self._available = False
60
+ return self._available
61
+
62
+ def suggest_selector(self, system_prompt: str, user_prompt: str) -> Optional[str]:
63
+ """Request a selector suggestion from the local AI service.
64
+
65
+ Args:
66
+ system_prompt: System message describing the task
67
+ user_prompt: User message with page context and failed selector
68
+
69
+ Returns:
70
+ AI response text or None if request fails
71
+ """
72
+ try:
73
+ response = requests.post(
74
+ f"{self.base_url}/v1/chat/completions",
75
+ json={
76
+ "messages": [
77
+ {"role": "system", "content": system_prompt},
78
+ {"role": "user", "content": user_prompt}
79
+ ],
80
+ "temperature": 0.3,
81
+ "max_tokens": 500
82
+ },
83
+ timeout=30
84
+ )
85
+ response.raise_for_status()
86
+ data = response.json()
87
+ return data.get("choices", [{}])[0].get("message", {}).get("content", "")
88
+ except requests.exceptions.HTTPError as e:
89
+ if e.response.status_code == 503:
90
+ logger.info(f"Local AI service unavailable (503). Disabling auto-correction.")
91
+ self._available = False
92
+ else:
93
+ logger.warning(f"Local AI HTTP error: {e}")
94
+ return None
95
+ except Exception as e:
96
+ logger.warning(f"Local AI request failed: {e}")
97
+ self._available = False
98
+ return None
99
+
100
+
101
+ def get_provider() -> AIProvider:
102
+ """Get the configured AI provider.
103
+
104
+ Returns:
105
+ LocalAIProvider instance
106
+ """
107
+ global _provider_instance
108
+ if _provider_instance is None:
109
+ _provider_instance = LocalAIProvider()
110
+ return _provider_instance
111
+
112
+
113
+ def configure_provider(provider: AIProvider):
114
+ """Set a custom AI provider.
115
+
116
+ Args:
117
+ provider: Instance of AIProvider to use
118
+ """
119
+ global _provider_instance
120
+ _provider_instance = provider
121
+
122
+
123
+ _provider_instance = None
@@ -0,0 +1,254 @@
1
+ """Core selector auto-correction functionality for Selenium WebDriver."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import re
7
+ from typing import Any, Dict, Optional, Tuple
8
+
9
+ from .ai_providers import get_provider
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class SelectorAutoCorrect:
15
+ """Auto-corrects element selectors by analyzing the page and requesting AI suggestions.
16
+
17
+ Args:
18
+ enabled: Whether auto-correction is enabled
19
+ """
20
+
21
+ def __init__(self, enabled: bool = True):
22
+ self.enabled = enabled
23
+ self._provider = None
24
+ self._correction_cache: Dict[str, str] = {}
25
+ self._suggestion_cache: Dict[str, str] = {}
26
+ self.suggest_better_selectors = os.environ.get("SELENIUM_SUGGEST_BETTER", "0").lower() in ("1", "true", "yes")
27
+ self._confidence_threshold = 50
28
+ self._cache_enabled = True
29
+
30
+ @property
31
+ def provider(self):
32
+ if self._provider is None:
33
+ self._provider = get_provider()
34
+ return self._provider
35
+
36
+ def set_provider(self, provider) -> None:
37
+ self._provider = provider
38
+
39
+ def is_service_available(self) -> bool:
40
+ if not self.enabled:
41
+ return False
42
+ provider = self.provider
43
+ return provider is not None and provider.is_available()
44
+
45
+ def get_visible_elements_summary(self, driver) -> str:
46
+ try:
47
+ script = """
48
+ function getElementSummary() {
49
+ const selectors = ['input', 'button', 'a', 'select', 'textarea',
50
+ '[role="button"]', '[role="link"]', '[data-testid]', '[data-test]', '[id]', '[name]'];
51
+ const elements = [];
52
+ selectors.forEach(selector => {
53
+ document.querySelectorAll(selector).forEach(el => {
54
+ if (el.offsetParent !== null) {
55
+ const info = {
56
+ tag: el.tagName.toLowerCase(),
57
+ id: el.id || null,
58
+ name: el.getAttribute('name') || null,
59
+ class: el.className || null,
60
+ type: el.getAttribute('type') || null,
61
+ text: (el.innerText || '').substring(0, 50),
62
+ placeholder: el.getAttribute('placeholder') || null,
63
+ ariaLabel: el.getAttribute('aria-label') || null,
64
+ dataTestId: el.getAttribute('data-testid') || el.getAttribute('data-test') || null,
65
+ role: el.getAttribute('role') || null
66
+ };
67
+ if (info.id || info.name || info.dataTestId || info.text || info.ariaLabel) {
68
+ elements.push(info);
69
+ }
70
+ }
71
+ });
72
+ });
73
+ const seen = new Set();
74
+ return elements.filter(el => {
75
+ const key = JSON.stringify(el);
76
+ if (seen.has(key)) return false;
77
+ seen.add(key);
78
+ return true;
79
+ }).slice(0, 100);
80
+ }
81
+ return JSON.stringify(getElementSummary());
82
+ """
83
+ result = driver.execute_script(script)
84
+ return result if result else "[]"
85
+ except Exception as e:
86
+ logger.warning(f"Failed to get element summary: {e}")
87
+ return "[]"
88
+
89
+ def suggest_selector(self, driver, failed_by: str, failed_value: str, error_message: str = "") -> Optional[Tuple[str, str]]:
90
+ if not self.enabled or not self.is_service_available():
91
+ return None
92
+
93
+ cache_key = f"{failed_by}:{failed_value}"
94
+ if self._cache_enabled and cache_key in self._correction_cache:
95
+ logger.info(f"[AUTO-CORRECT] Using cached correction for {failed_value[:50]}")
96
+ return self._parse_selector_suggestion(self._correction_cache[cache_key])
97
+
98
+ try:
99
+ elements_summary = self.get_visible_elements_summary(driver)
100
+ current_url = driver.current_url
101
+
102
+ system_prompt = """You are an expert at fixing Selenium element selectors.
103
+ When given a failed selector and available elements, suggest a working alternative.
104
+ ONLY respond with a JSON object containing:
105
+ - "by": the selector strategy ("css selector", "xpath", "id", "name", etc.)
106
+ - "value": the selector value
107
+ - "confidence": a number 0-100
108
+ - "reason": brief explanation
109
+
110
+ If no good alternative exists, respond with:
111
+ {"by": null, "value": null, "confidence": 0, "reason": "No suitable alternative found"}"""
112
+
113
+ user_prompt = f"""The following selector failed:
114
+ - Strategy: {failed_by}
115
+ - Value: {failed_value}
116
+ - URL: {current_url}
117
+ - Error: {error_message}
118
+
119
+ Available Elements:
120
+ ```json
121
+ {elements_summary}
122
+ ```
123
+
124
+ Suggest a working selector. Respond with ONLY a JSON object."""
125
+
126
+ logger.info(f"[AUTO-CORRECT] Requesting selector suggestion for: {failed_value[:50]}...")
127
+ response = self.provider.suggest_selector(system_prompt, user_prompt)
128
+
129
+ if response:
130
+ suggestion = self._parse_selector_suggestion(response)
131
+ if suggestion and self._cache_enabled:
132
+ self._correction_cache[cache_key] = response
133
+ return suggestion
134
+ else:
135
+ if self.provider and not self.provider.is_available():
136
+ logger.info("[AUTO-CORRECT] Service unavailable, auto-correction disabled for this session")
137
+ self.enabled = False
138
+ except Exception as e:
139
+ logger.warning(f"[AUTO-CORRECT] Failed to get suggestion: {e}")
140
+ return None
141
+
142
+ def suggest_better_selector(self, driver, current_by: str, current_value: str, element) -> Optional[Tuple[str, str]]:
143
+ if not self.suggest_better_selectors or not self.enabled or not self.is_service_available():
144
+ return None
145
+
146
+ if current_by in ("id", "name") or (current_by == "css selector" and "[data-testid=" in current_value):
147
+ return None
148
+
149
+ cache_key = f"{current_by}:{current_value}"
150
+ if self._cache_enabled and cache_key in self._suggestion_cache:
151
+ cached = self._suggestion_cache[cache_key]
152
+ if cached == "OPTIMAL":
153
+ return None
154
+ return self._parse_selector_suggestion(cached)
155
+
156
+ try:
157
+ element_info = self._get_element_info(element)
158
+ elements_summary = self.get_visible_elements_summary(driver)
159
+
160
+ system_prompt = """You are an expert at improving Selenium element selectors.
161
+ Suggest a better selector ONLY if the current one is fragile.
162
+ Preference: data-testid > id > name > css class > xpath
163
+
164
+ Respond with JSON: {"by": ..., "value": ..., "confidence": ..., "reason": ...}
165
+ If current is optimal: {"by": null, "value": null, "confidence": 100, "reason": "Current selector is optimal"}"""
166
+
167
+ user_prompt = f"""Current Selector: {current_by}='{current_value}'
168
+ Element attributes: {json.dumps(element_info, indent=2)}
169
+ Available Elements:
170
+ ```json
171
+ {elements_summary}
172
+ ```"""
173
+
174
+ response = self.provider.suggest_selector(system_prompt, user_prompt)
175
+ if response:
176
+ suggestion = self._parse_selector_suggestion(response)
177
+ if suggestion:
178
+ if self._cache_enabled:
179
+ self._suggestion_cache[cache_key] = response
180
+ return suggestion
181
+ else:
182
+ if self._cache_enabled:
183
+ self._suggestion_cache[cache_key] = "OPTIMAL"
184
+ except Exception as e:
185
+ logger.debug(f"[SUGGEST] Failed to get better selector: {e}")
186
+ return None
187
+
188
+ def _get_element_info(self, element) -> Dict[str, Any]:
189
+ try:
190
+ info = {
191
+ "tag": element.tag_name,
192
+ "id": element.get_attribute("id"),
193
+ "name": element.get_attribute("name"),
194
+ "class": element.get_attribute("class"),
195
+ "data-testid": element.get_attribute("data-testid") or element.get_attribute("data-test"),
196
+ "aria-label": element.get_attribute("aria-label"),
197
+ }
198
+ return {k: v for k, v in info.items() if v}
199
+ except Exception:
200
+ return {}
201
+
202
+ def _parse_selector_suggestion(self, content: str) -> Optional[Tuple[str, str]]:
203
+ try:
204
+ json_match = re.search(r'\{[^{}]*\}', content, re.DOTALL)
205
+ if json_match:
206
+ data = json.loads(json_match.group())
207
+ by = data.get("by")
208
+ value = data.get("value")
209
+ confidence = data.get("confidence", 0)
210
+ reason = data.get("reason", "")
211
+
212
+ if by and value and confidence >= self._confidence_threshold:
213
+ logger.info(f"[AUTO-CORRECT] Suggested selector (confidence: {confidence}%)")
214
+ logger.info(f" Strategy: {by}")
215
+ logger.info(f" Value: {value}")
216
+ logger.info(f" Reason: {reason}")
217
+ return (by, value)
218
+ elif reason:
219
+ logger.info(f"[AUTO-CORRECT] No good suggestion - {reason}")
220
+ except json.JSONDecodeError as e:
221
+ logger.warning(f"[AUTO-CORRECT] Failed to parse JSON: {e}")
222
+ except Exception as e:
223
+ logger.warning(f"[AUTO-CORRECT] Error parsing suggestion: {e}")
224
+ return None
225
+
226
+ def clear_cache(self):
227
+ self._correction_cache.clear()
228
+ self._suggestion_cache.clear()
229
+
230
+
231
+ _auto_correct_instance: Optional[SelectorAutoCorrect] = None
232
+
233
+
234
+ def get_auto_correct() -> SelectorAutoCorrect:
235
+ """Get the global SelectorAutoCorrect instance."""
236
+ global _auto_correct_instance
237
+ if _auto_correct_instance is None:
238
+ enabled = os.environ.get("SELENIUM_AUTO_CORRECT", "1").lower() in ("1", "true", "yes")
239
+ _auto_correct_instance = SelectorAutoCorrect(enabled=enabled)
240
+ return _auto_correct_instance
241
+
242
+
243
+ def set_auto_correct_enabled(enabled: bool):
244
+ """Enable or disable auto-correction globally."""
245
+ get_auto_correct().enabled = enabled
246
+
247
+
248
+ def configure_auto_correct(provider=None, enabled: bool = True, suggest_better: bool = False):
249
+ """Configure auto-correction settings."""
250
+ auto_correct = get_auto_correct()
251
+ auto_correct.enabled = enabled
252
+ auto_correct.suggest_better_selectors = suggest_better
253
+ if provider:
254
+ auto_correct.set_provider(provider)
@@ -0,0 +1,226 @@
1
+ """Correction tracker for recording and applying selector fixes."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import traceback
7
+ from datetime import datetime
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import requests
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class CorrectionTracker:
16
+ """Tracks selector corrections and manages test file updates."""
17
+
18
+ def __init__(self):
19
+ self._corrections: List[Dict[str, Any]] = []
20
+ self._local_ai_url = os.environ.get("LOCAL_AI_API_URL", "http://localhost:8765")
21
+ self._auto_update_enabled = os.environ.get("SELENIUM_AUTO_UPDATE_TESTS", "0").lower() in ("1", "true", "yes")
22
+
23
+ def record_correction(
24
+ self,
25
+ original_by: str,
26
+ original_value: str,
27
+ corrected_by: str,
28
+ corrected_value: str,
29
+ success: bool = True,
30
+ test_file: Optional[str] = None,
31
+ test_line: Optional[int] = None
32
+ ):
33
+ if test_file is None or test_line is None:
34
+ # Extract from stack trace, prioritizing actual test files
35
+ for frame in traceback.extract_stack():
36
+ filename = frame.filename.replace('\\', '/')
37
+ filename_lower = filename.lower()
38
+ # Skip selenium packages, pytest, and our autocorrect packages
39
+ # Be specific to avoid skipping directories with "selenium" in the name
40
+ if ('/selenium/' in filename_lower or
41
+ '\\selenium\\' in filename or
42
+ '/site-packages/selenium/' in filename_lower or
43
+ '/pytest' in filename_lower or
44
+ '/_pytest' in filename_lower or
45
+ '/selenium_selector_autocorrect/' in filename_lower or
46
+ '\\selenium_selector_autocorrect\\' in filename):
47
+ continue
48
+ # Prioritize test files, then page objects, then ui_client
49
+ if ('test_library' in filename or
50
+ 'test_' in filename or
51
+ 'page_factory' in filename_lower or
52
+ 'ui_client' in filename_lower):
53
+ test_file = filename
54
+ test_line = frame.lineno
55
+ # Don't break - keep looking for test files specifically
56
+ if 'test_' in filename or 'test_library' in filename:
57
+ break
58
+
59
+ correction = {
60
+ "original_by": original_by,
61
+ "original_value": original_value,
62
+ "corrected_by": corrected_by,
63
+ "corrected_value": corrected_value,
64
+ "success": success,
65
+ "test_file": test_file,
66
+ "test_line": test_line,
67
+ "timestamp": datetime.now().isoformat()
68
+ }
69
+ self._corrections.append(correction)
70
+
71
+ try:
72
+ logger.info(f"[CORRECTION TRACKED] {original_by}='{original_value[:30]}...' -> {corrected_by}='{corrected_value[:30]}...'")
73
+ if test_file:
74
+ logger.info(f"[CORRECTION SOURCE] File: {test_file}, Line: {test_line}")
75
+ except Exception:
76
+ pass
77
+
78
+ if self._auto_update_enabled and success and test_file:
79
+ logger.info(f"[AUTO-UPDATE] Attempting to update {test_file}...")
80
+ self._auto_update_test_file(correction)
81
+
82
+ def get_corrections(self) -> List[Dict[str, Any]]:
83
+ return self._corrections.copy()
84
+
85
+ def get_successful_corrections(self) -> List[Dict[str, Any]]:
86
+ return [c for c in self._corrections if c.get("success", False)]
87
+
88
+ def clear_corrections(self):
89
+ self._corrections.clear()
90
+
91
+ def _auto_update_test_file(self, correction: Dict[str, Any]):
92
+ try:
93
+ test_file = correction.get("test_file")
94
+ if not test_file:
95
+ return
96
+ result = self.update_test_file_via_service(
97
+ test_file,
98
+ correction["original_by"],
99
+ correction["original_value"],
100
+ correction["corrected_by"],
101
+ correction["corrected_value"]
102
+ )
103
+ if result.get("success"):
104
+ logger.info(f"[AUTO-UPDATE] Successfully updated {test_file}")
105
+ else:
106
+ logger.warning(f"[AUTO-UPDATE] Failed to update {test_file}: {result.get('errors', [])}")
107
+ except Exception as e:
108
+ logger.warning(f"[AUTO-UPDATE] Error updating test file: {e}")
109
+
110
+ def update_test_file_via_service(
111
+ self,
112
+ file_path: str,
113
+ original_by: str,
114
+ original_value: str,
115
+ corrected_by: str,
116
+ corrected_value: str
117
+ ) -> Dict[str, Any]:
118
+ try:
119
+ read_url = f"{self._local_ai_url}/v1/workspace/files/read"
120
+ read_response = requests.post(read_url, json={"filePath": file_path}, timeout=30)
121
+ read_response.raise_for_status()
122
+ file_content = read_response.json()
123
+
124
+ if not file_content.get("success"):
125
+ return {"success": False, "errors": ["Could not read file"]}
126
+
127
+ content = file_content.get("content", "")
128
+ old_patterns = [
129
+ f'"{original_value}"',
130
+ f"'{original_value}'",
131
+ ]
132
+
133
+ found_pattern = None
134
+ new_pattern = None
135
+ for old_pattern in old_patterns:
136
+ if old_pattern in content:
137
+ found_pattern = old_pattern
138
+ new_pattern = f'"{corrected_value}"' if old_pattern.startswith('"') else f"'{corrected_value}'"
139
+ break
140
+
141
+ if not found_pattern:
142
+ return {"success": False, "errors": [f"Could not find selector: {original_value[:50]}..."]}
143
+
144
+ edit_url = f"{self._local_ai_url}/v1/workspace/files/edit"
145
+ edit_response = requests.post(
146
+ edit_url,
147
+ json={"filePath": file_path, "oldString": found_pattern, "newString": new_pattern},
148
+ timeout=30
149
+ )
150
+ edit_response.raise_for_status()
151
+ return edit_response.json()
152
+ except requests.exceptions.ConnectionError:
153
+ logger.warning(f"[LOCAL AI SERVICE] Not available at {self._local_ai_url}")
154
+ return {"success": False, "errors": ["Local AI service not available"]}
155
+ except Exception as e:
156
+ logger.warning(f"[UPDATE ERROR] {e}")
157
+ return {"success": False, "errors": [str(e)]}
158
+
159
+ def export_corrections_report(self, output_file: str = "selector_corrections.json"):
160
+ with open(output_file, "w") as f:
161
+ json.dump({
162
+ "corrections": self._corrections,
163
+ "summary": {
164
+ "total": len(self._corrections),
165
+ "successful": len(self.get_successful_corrections()),
166
+ "generated_at": datetime.now().isoformat()
167
+ }
168
+ }, f, indent=2)
169
+ logger.info(f"[CORRECTIONS REPORT] Exported to {output_file}")
170
+
171
+ def apply_all_corrections_to_files(self) -> Dict[str, Any]:
172
+ results = {"total": 0, "success": 0, "failed": 0, "details": []}
173
+ for correction in self.get_successful_corrections():
174
+ test_file = correction.get("test_file")
175
+ if not test_file:
176
+ continue
177
+ results["total"] += 1
178
+ result = self.update_test_file_via_service(
179
+ test_file,
180
+ correction["original_by"],
181
+ correction["original_value"],
182
+ correction["corrected_by"],
183
+ correction["corrected_value"]
184
+ )
185
+ if result.get("success"):
186
+ results["success"] += 1
187
+ else:
188
+ results["failed"] += 1
189
+ results["details"].append({
190
+ "file": test_file,
191
+ "original": correction["original_value"][:50],
192
+ "corrected": correction["corrected_value"][:50],
193
+ "result": result
194
+ })
195
+ logger.info(f"[APPLIED CORRECTIONS] {results['success']}/{results['total']} successful")
196
+ return results
197
+
198
+
199
+ _correction_tracker: Optional[CorrectionTracker] = None
200
+
201
+
202
+ def get_correction_tracker() -> CorrectionTracker:
203
+ """Get the global CorrectionTracker instance."""
204
+ global _correction_tracker
205
+ if _correction_tracker is None:
206
+ _correction_tracker = CorrectionTracker()
207
+ return _correction_tracker
208
+
209
+
210
+ def record_correction(
211
+ original_by: str, original_value: str, corrected_by: str, corrected_value: str, success: bool = True
212
+ ):
213
+ """Record a selector correction."""
214
+ get_correction_tracker().record_correction(
215
+ original_by, original_value, corrected_by, corrected_value, success
216
+ )
217
+
218
+
219
+ def apply_corrections_to_test_files() -> Dict[str, Any]:
220
+ """Apply all successful corrections to their source test files."""
221
+ return get_correction_tracker().apply_all_corrections_to_files()
222
+
223
+
224
+ def export_corrections_report(output_file: str = "selector_corrections.json"):
225
+ """Export corrections report to JSON file."""
226
+ get_correction_tracker().export_corrections_report(output_file)
@@ -0,0 +1 @@
1
+ # Type hints marker for PEP 561
@@ -0,0 +1,140 @@
1
+ """WebDriverWait integration hook for selector auto-correction."""
2
+
3
+ import logging
4
+ import time
5
+ from typing import Callable, Optional
6
+
7
+ from selenium.common.exceptions import TimeoutException
8
+ from selenium.webdriver.remote.webelement import WebElement
9
+ from selenium.webdriver.support.wait import WebDriverWait
10
+
11
+ from .auto_correct import get_auto_correct
12
+ from .correction_tracker import record_correction
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _original_until = WebDriverWait.until
17
+
18
+
19
+ def _patched_until(self, method: Callable, message: str = ""):
20
+ """Patched until method with auto-correct support."""
21
+ screen = None
22
+ stacktrace = None
23
+
24
+ end_time = time.monotonic() + self._timeout
25
+ while True:
26
+ try:
27
+ value = method(self._driver)
28
+ if value:
29
+ return value
30
+ except self._ignored_exceptions as exc:
31
+ screen = getattr(exc, "screen", None)
32
+ stacktrace = getattr(exc, "stacktrace", None)
33
+ if time.monotonic() > end_time:
34
+ break
35
+ time.sleep(self._poll)
36
+
37
+ auto_correct = get_auto_correct()
38
+ if auto_correct.enabled:
39
+ locator = _extract_locator_from_method(method)
40
+ if locator:
41
+ by, value_str = locator
42
+ logger.warning(
43
+ f"[AUTO-CORRECT] Timeout waiting for element {by}='{value_str[:80]}...' - attempting auto-correction"
44
+ )
45
+
46
+ driver = self._driver
47
+ if isinstance(driver, WebElement):
48
+ driver = driver.parent
49
+
50
+ suggestion = auto_correct.suggest_selector(
51
+ driver,
52
+ failed_by=by,
53
+ failed_value=value_str,
54
+ error_message=message or f"Timeout waiting for element with {by}={value_str}",
55
+ )
56
+
57
+ if suggestion:
58
+ suggested_by, suggested_value = suggestion
59
+ logger.warning(f"[AUTO-CORRECT] Trying suggested selector: {suggested_by}='{suggested_value}'")
60
+
61
+ corrected_method = _create_corrected_method(method, suggested_by, suggested_value)
62
+ if corrected_method:
63
+ try:
64
+ result = corrected_method(self._driver)
65
+ if result:
66
+ logger.warning(f"[AUTO-CORRECT] SUCCESS! Element found with corrected selector")
67
+ record_correction(
68
+ original_by=by,
69
+ original_value=value_str,
70
+ corrected_by=suggested_by,
71
+ corrected_value=suggested_value,
72
+ success=True,
73
+ )
74
+ return result
75
+ except Exception as e:
76
+ logger.warning(f"[AUTO-CORRECT] Suggested selector also failed: {e}")
77
+
78
+ raise TimeoutException(message, screen, stacktrace)
79
+
80
+
81
+ def _extract_locator_from_method(method: Callable) -> Optional[tuple]:
82
+ """Extract locator tuple (by, value) from an expected_conditions method."""
83
+ try:
84
+ if hasattr(method, "locator"):
85
+ logger.debug(f"[AUTO-CORRECT] Found locator attribute: {method.locator}")
86
+ return method.locator
87
+
88
+ if hasattr(method, "__closure__") and method.__closure__:
89
+ for cell in method.__closure__:
90
+ cell_contents = cell.cell_contents
91
+ logger.debug(f"[AUTO-CORRECT] Checking closure cell: {type(cell_contents)} = {cell_contents}")
92
+ if isinstance(cell_contents, tuple) and len(cell_contents) == 2:
93
+ if isinstance(cell_contents[0], str) and isinstance(cell_contents[1], str):
94
+ logger.debug(f"[AUTO-CORRECT] Extracted locator from closure: {cell_contents}")
95
+ return cell_contents
96
+ logger.warning(f"[AUTO-CORRECT] Could not extract locator from method: {method}")
97
+ except Exception as e:
98
+ logger.exception(f"[AUTO-CORRECT] Error extracting locator: {e}")
99
+ return None
100
+
101
+
102
+ def _create_corrected_method(original_method: Callable, new_by: str, new_value: str) -> Optional[Callable]:
103
+ """Create a new expected condition method with corrected locator."""
104
+ try:
105
+ from selenium.webdriver.support import expected_conditions as EC
106
+
107
+ method_name = None
108
+ if hasattr(original_method, '__qualname__'):
109
+ qualname = original_method.__qualname__
110
+ if '.<locals>._predicate' in qualname:
111
+ method_name = qualname.split('.<locals>._predicate')[0]
112
+
113
+ if not method_name:
114
+ method_name = original_method.__class__.__name__
115
+
116
+ method_map = {
117
+ "visibility_of_element_located": EC.visibility_of_element_located,
118
+ "presence_of_element_located": EC.presence_of_element_located,
119
+ "element_to_be_clickable": EC.element_to_be_clickable,
120
+ "invisibility_of_element_located": EC.invisibility_of_element_located,
121
+ }
122
+
123
+ ec_method = method_map.get(method_name, EC.visibility_of_element_located)
124
+ return ec_method((new_by, new_value))
125
+
126
+ except Exception as e:
127
+ logger.exception(f"[AUTO-CORRECT] Error creating corrected method: {e}")
128
+ return None
129
+
130
+
131
+ def install_auto_correct_hook():
132
+ """Install the auto-correct hook into WebDriverWait."""
133
+ WebDriverWait.until = _patched_until
134
+ logger.info("[AUTO-CORRECT] Hook installed into WebDriverWait")
135
+
136
+
137
+ def uninstall_auto_correct_hook():
138
+ """Remove the auto-correct hook from WebDriverWait."""
139
+ WebDriverWait.until = _original_until
140
+ logger.info("[AUTO-CORRECT] Hook removed from WebDriverWait")
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.4
2
+ Name: selenium-selector-autocorrect
3
+ Version: 0.1.0
4
+ Summary: Automatic Selenium selector correction using AI when elements fail to be found
5
+ Author-email: Marty Zhou <marty.zhou@example.com>
6
+ Maintainer-email: Marty Zhou <marty.zhou@example.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/MartyZhou/selenium-selector-autocorrect
9
+ Project-URL: Documentation, https://github.com/MartyZhou/selenium-selector-autocorrect#readme
10
+ Project-URL: Repository, https://github.com/MartyZhou/selenium-selector-autocorrect
11
+ Project-URL: Issues, https://github.com/MartyZhou/selenium-selector-autocorrect/issues
12
+ Project-URL: Changelog, https://github.com/MartyZhou/selenium-selector-autocorrect/blob/main/CHANGELOG.md
13
+ Keywords: selenium,testing,automation,ai,web-testing,test-automation,selector,auto-correction
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Classifier: Topic :: Software Development :: Quality Assurance
26
+ Classifier: Framework :: Pytest
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: selenium>=4.0.0
31
+ Requires-Dist: requests>=2.25.0
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
34
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
35
+ Requires-Dist: black>=23.0.0; extra == "dev"
36
+ Requires-Dist: isort>=5.12.0; extra == "dev"
37
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
38
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # Selenium Selector AutoCorrect
42
+
43
+ A Python package that automatically corrects Selenium element selectors using AI when they fail, reducing test maintenance and improving test reliability.
44
+
45
+ ## Features
46
+
47
+ - **Automatic Selector Correction**: When a WebDriverWait times out, the package uses AI to analyze the page and suggest working alternatives
48
+ - **Local AI Integration**: Uses a local AI service with OpenAI-compatible API
49
+ - **Correction Tracking**: Records all corrections with source file and line information
50
+ - **Optional Auto-Update**: Can automatically update test files with corrected selectors
51
+ - **Zero Code Changes**: Works by hooking into Selenium's WebDriverWait
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install selenium-selector-autocorrect
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ```python
62
+ from selenium import webdriver
63
+ from selenium.webdriver.common.by import By
64
+ from selenium.webdriver.support.wait import WebDriverWait
65
+ from selenium.webdriver.support import expected_conditions as EC
66
+ from selenium_selector_autocorrect import install_auto_correct_hook
67
+
68
+ install_auto_correct_hook()
69
+
70
+ driver = webdriver.Chrome()
71
+ driver.get("https://example.com")
72
+
73
+ element = WebDriverWait(driver, 10).until(
74
+ EC.presence_of_element_located((By.ID, "some-element"))
75
+ )
76
+ ```
77
+
78
+ ## AI Service Setup
79
+
80
+ This package requires a local AI service with an OpenAI-compatible API. We recommend using **[VS Code Copilot as Service](https://marketplace.visualstudio.com/items?itemName=MartyZhou.vscode-copilot-as-service)**, which exposes GitHub Copilot through a local HTTP server.
81
+
82
+ ### Installing VS Code Copilot as Service
83
+
84
+ 1. Install from VS Code Marketplace or run:
85
+ ```bash
86
+ code --install-extension MartyZhou.vscode-copilot-as-service
87
+ ```
88
+
89
+ 2. The extension automatically starts a server on `http://localhost:8765`
90
+
91
+ 3. Requires an active GitHub Copilot subscription
92
+
93
+
94
+ ## Configuration
95
+
96
+ Configure via environment variables:
97
+
98
+ - `LOCAL_AI_API_URL`: URL of local AI service (default: `http://localhost:8765`)
99
+ - `SELENIUM_AUTO_CORRECT`: Enable/disable auto-correction (default: `"1"`)
100
+ - `SELENIUM_SUGGEST_BETTER`: Suggest better selectors for found elements (default: `"0"`)
101
+ - `SELENIUM_AUTO_UPDATE_TESTS`: Auto-update test files with corrections (default: `"0"`)
102
+
103
+ ### Example
104
+
105
+ ```python
106
+ import os
107
+
108
+ os.environ['LOCAL_AI_API_URL'] = 'http://localhost:8765'
109
+ os.environ['SELENIUM_AUTO_CORRECT'] = '1'
110
+ os.environ['SELENIUM_AUTO_UPDATE_TESTS'] = '1' # Enable auto-update
111
+ ```
112
+
113
+ ## Usage
114
+
115
+ ### Basic Usage
116
+
117
+ ```python
118
+ from selenium_selector_autocorrect import install_auto_correct_hook
119
+
120
+ install_auto_correct_hook()
121
+ ```
122
+
123
+ ### Advanced Usage
124
+
125
+ ```python
126
+ from selenium_selector_autocorrect import (
127
+ install_auto_correct_hook,
128
+ get_auto_correct,
129
+ get_correction_tracker,
130
+ export_corrections_report
131
+ )
132
+
133
+ install_auto_correct_hook()
134
+
135
+ auto_correct = get_auto_correct()
136
+ auto_correct.enabled = True
137
+ auto_correct.suggest_better_selectors = False
138
+
139
+ # Export corrections report at end of test run
140
+ tracker = get_correction_tracker()
141
+ export_corrections_report("corrections_report.json")
142
+ tracker = get_correction_tracker()
143
+ export_corrections_report("corrections_report.json")
144
+
145
+ print(f"Total corrections: {len(tracker.get_corrections())}")
146
+ print(f"Successful corrections: {len(tracker.get_successful_corrections())}")
147
+ ```
148
+
149
+ ### Custom AI Provider
150
+
151
+ ```python
152
+ from selenium_selector_autocorrect import AIProvider, configure_provider
153
+
154
+ class CustomAIProvider(AIProvider):
155
+ def is_available(self) -> bool:
156
+ return True
157
+
158
+ def suggest_selector(self, system_prompt: str, user_prompt: str):))
159
+ ```
160
+
161
+ ## How It Works
162
+
163
+ 1. **Hook Installation**: Patches `WebDriverWait.until()` to add auto-correction
164
+ 2. **Timeout Detection**: When a selector times out, the original exception is caught
165
+ 3. **Page Analysis**: JavaScript extracts visible elements and their attributes
166
+ 4. **AI Suggestion**: Sends page context to AI provider for selector suggestion
167
+ 5. **Verification**: Tests the suggested selector
168
+ 6. **Success Handling**: If successful, records the correction and optionally updates the test file
169
+ 7. **Fallback**: If correction fails, raises the original TimeoutException
170
+
171
+ ## AI Provider Setup
172
+
173
+ ### Local AI Service
174
+
175
+ The package requires a local AI service with OpenAI-compatible API:
176
+
177
+ ```bash
178
+ POST http://localhost:8765/v1/chat/completions
179
+ ```
180
+
181
+ For file auto-updates:
182
+ ```bash
183
+ POST http://localhost:8765/v1/workspace/files/read
184
+ POST http://localhost:8765/v1/workspace/files/edit
185
+ ## Correction Reports
186
+
187
+ Export correction reports in JSON format:
188
+
189
+ ```python
190
+ from selenium_selector_autocorrect import export_corrections_report
191
+
192
+ export_corrections_report("corrections_report.json")
193
+ ```
194
+
195
+ Report format:
196
+ ```json
197
+ {
198
+ "corrections": [
199
+ {
200
+ "original_by": "id",
201
+ "original_value": "old-selector",
202
+ "corrected_by": "css selector",
203
+ "corrected_value": ".new-selector",
204
+ "success": true,
205
+ "test_file": "/path/to/test.py",
206
+ "test_line": 42,
207
+ "timestamp": "2024-01-31T10:30:00"
208
+ }
209
+ ],
210
+ "summary": {
211
+ "total": 10,
212
+ "successful": 8,
213
+ "generated_at": "2024-01-31T10:35:00"
214
+ }
215
+ }
216
+ ```
217
+
218
+ ## Best Practices
219
+
220
+ 1. **Install Once**: Call `install_auto_correct_hook()` once at test suite startup (e.g., in `conftest.py`)
221
+ 2. **Review Corrections**: Regularly review correction reports to identify brittle selectors
222
+ 3. **Update Tests**: Use auto-update sparingly and review changes before committing
223
+ 4. **Monitor AI Service**: Ensure your AI service is running and responsive
224
+ 5. **Use Strong Selectors**: The tool helps with failures but writing robust selectors is still preferred
225
+
226
+ ## Requirements
227
+
228
+ - Python >= 3.8
229
+ - selenium >= 4.0.0
230
+ - requests >= 2.25.0
231
+
232
+ ## License
233
+
234
+ MITInstall hook once at test suite startup (e.g., in conftest.py)
235
+ 2. Review correction reports regularly to identify brittle selectors
236
+ 3. Use auto-update sparingly and review changes before committing
237
+ 4. Ensure your AI service is running and responsive
238
+ 5. Write robust selectors - the tool helps with failures but prevention is better
239
+
240
+ When contributing:
241
+ 1. Follow PEP 8 style guidelines
242
+ 2. Add tests for new features
243
+ 3. Update documentation
244
+ 4. No emojis in code or documentation
245
+
246
+ ## Troubleshooting
247
+
248
+ ### AI Service Not Available
249
+
250
+ Contributions are welcome! Please:
251
+ 1. Follow PEP 8 style guidelines
252
+ 2. Add tests for new features
253
+ 3. Update documentation
254
+ 4. Maintain consistency with existing code
255
+
256
+ **Possible causes**:
257
+ - `SELENIUM_AUTO_UPDATE_TESTS` not set to `"1"`
258
+ - Test file path not detected correctly
259
+ - Selector string not found in source file (check quotes)
260
+
261
+ ### No Corrections Happening
262
+ Solution: Ensure your local AI service is running on the configured port.
263
+
264
+ ### Test File Not Updated
265
+
266
+ Possible causes:
267
+ - `SELENIUM_AUTO_UPDATE_TESTS` not set to "1"
268
+ - Test file path not detected correctly
269
+ - Selector string not found in source file
270
+
271
+ ### No Corrections Happening
272
+
273
+ Check:
274
+ 1. Hook is installed - look for log message
275
+ 2. AI service is available - check `get_auto_correct().is_service_available()`
276
+ 3. Auto-correct is enabled - c
277
+ See CHANGELOG.md for version history and changes.
@@ -0,0 +1,11 @@
1
+ selenium_selector_autocorrect/__init__.py,sha256=WjF3o-hlNlt_-tlFxQqIQ4P3OT9ZB0MhBYyJc9Ef8Hg,1665
2
+ selenium_selector_autocorrect/ai_providers.py,sha256=c0DHw1Kyfsa6QNF_Yt2zhEGw7-yynZ2_YT7NAXEIn1Y,4095
3
+ selenium_selector_autocorrect/auto_correct.py,sha256=73PhryN0HnMTqhQXZGIy3ahC65IfW2i0IJqf-aOSEVM,10822
4
+ selenium_selector_autocorrect/correction_tracker.py,sha256=tvw7S9q8y-wuPYJ1W8pBP72r8dI0okbVHSjaYhnKGbA,9282
5
+ selenium_selector_autocorrect/py.typed,sha256=FPdf-6Jjb4gsSRLK88ZhHbrQnHD9pcZQTqaxHzO8neM,33
6
+ selenium_selector_autocorrect/wait_hook.py,sha256=U4xD3G3rx6sjcN72qTpH3ET-gY5fhS_X6ADI99CP5uc,5857
7
+ selenium_selector_autocorrect-0.1.0.dist-info/licenses/LICENSE,sha256=VRPy6YXF2wA_3MeTDnpa_-6Zgjt8c2C0D_iIyhDkduc,1095
8
+ selenium_selector_autocorrect-0.1.0.dist-info/METADATA,sha256=mhCjJVP7EoA_n6_VUnHffytUImc-A0kozBntx5TiiHk,9221
9
+ selenium_selector_autocorrect-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ selenium_selector_autocorrect-0.1.0.dist-info/top_level.txt,sha256=nQ78Mk-XHDhYBckP0tMZvoFAZmZGO4Ec4-e1i61Fdz0,30
11
+ selenium_selector_autocorrect-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ selenium_selector_autocorrect