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.
- selenium_agent/__init__.py +20 -0
- selenium_agent/agents/__init__.py +5 -0
- selenium_agent/agents/coder.py +505 -0
- selenium_agent/agents/definitions.py +152 -0
- selenium_agent/agents/healer.py +638 -0
- selenium_agent/agents/planner.py +279 -0
- selenium_agent/bdd/__init__.py +15 -0
- selenium_agent/bdd/gherkin_advisor.py +189 -0
- selenium_agent/bdd/templates.py +98 -0
- selenium_agent/cli.py +314 -0
- selenium_agent/core/__init__.py +3 -0
- selenium_agent/core/orchestrator.py +189 -0
- selenium_agent/scanner/__init__.py +3 -0
- selenium_agent/scanner/project_scanner.py +452 -0
- selenium_agent/selenium/__init__.py +6 -0
- selenium_agent/selenium/base_page.py +447 -0
- selenium_agent/selenium/driver_factory.py +222 -0
- selenium_agent/selenium/error_map.py +235 -0
- selenium_agent/selenium/locator_advisor.py +247 -0
- selenium_agent/selenium/locator_scanner.py +502 -0
- selenium_agent/utils/__init__.py +26 -0
- selenium_agent/utils/code_validator.py +96 -0
- selenium_agent/utils/config_manager.py +81 -0
- selenium_agent/utils/json_utils.py +103 -0
- selenium_agent/utils/llm.py +240 -0
- selenium_agent/utils/logger.py +37 -0
- selenium_agent/utils/paths.py +57 -0
- selenium_agent/utils/spec_writer.py +126 -0
- selenium_agent/utils/url_extractor.py +52 -0
- selenium_python_ai_agent-0.2.0.dist-info/METADATA +412 -0
- selenium_python_ai_agent-0.2.0.dist-info/RECORD +35 -0
- selenium_python_ai_agent-0.2.0.dist-info/WHEEL +5 -0
- selenium_python_ai_agent-0.2.0.dist-info/entry_points.txt +2 -0
- selenium_python_ai_agent-0.2.0.dist-info/licenses/LICENSE +21 -0
- selenium_python_ai_agent-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PROJECT SCANNER
|
|
3
|
+
===============
|
|
4
|
+
Scans an existing Selenium Python project and detects:
|
|
5
|
+
- Folder structure (pages/, page_objects/, src/, etc.)
|
|
6
|
+
- Base class (BasePage, BaseTest, PageBase, etc.)
|
|
7
|
+
- Test framework (pytest, unittest, pytest-bdd)
|
|
8
|
+
- Naming conventions (test_login.py vs LoginTest.py)
|
|
9
|
+
- Import style (relative vs absolute)
|
|
10
|
+
- Existing conftest.py, fixtures
|
|
11
|
+
- Driver setup pattern
|
|
12
|
+
|
|
13
|
+
This context is passed to Planner + Coder so generated code
|
|
14
|
+
fits INTO the existing project, not alongside it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ProjectProfile:
|
|
26
|
+
"""
|
|
27
|
+
Complete profile of a detected Selenium Python project.
|
|
28
|
+
Passed to Planner and Coder agents as context.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# Root directory
|
|
32
|
+
root: str = ""
|
|
33
|
+
|
|
34
|
+
# Folder structure
|
|
35
|
+
pages_dir: str = "pages" # where Page Objects live
|
|
36
|
+
tests_dir: str = "tests" # where test files live
|
|
37
|
+
steps_dir: str = "step_definitions" # BDD steps dir
|
|
38
|
+
features_dir: str = "features" # BDD features dir
|
|
39
|
+
utils_dir: str = "" # utils/helpers dir
|
|
40
|
+
base_dir: str = "" # base classes dir
|
|
41
|
+
|
|
42
|
+
# Framework
|
|
43
|
+
test_framework: str = "pytest" # pytest | unittest | pytest-bdd
|
|
44
|
+
has_bdd: bool = False
|
|
45
|
+
has_pytest: bool = True
|
|
46
|
+
has_conftest: bool = False
|
|
47
|
+
|
|
48
|
+
# Base classes
|
|
49
|
+
base_page_class: str = "BasePage" # existing base page name
|
|
50
|
+
base_page_import: str = "" # full import path
|
|
51
|
+
base_test_class: str = "" # existing base test name
|
|
52
|
+
base_test_import: str = ""
|
|
53
|
+
|
|
54
|
+
# Driver setup
|
|
55
|
+
driver_setup: str = "fixture" # fixture | setUp | class-level
|
|
56
|
+
driver_scope: str = "function" # function | class | module
|
|
57
|
+
browser: str = "chrome"
|
|
58
|
+
headless: bool = False
|
|
59
|
+
|
|
60
|
+
# Naming conventions
|
|
61
|
+
test_file_prefix: str = "test_" # test_login.py
|
|
62
|
+
test_file_suffix: str = "" # LoginTest.py → suffix=Test
|
|
63
|
+
page_file_suffix: str = "_page" # login_page.py
|
|
64
|
+
test_func_prefix: str = "test_"
|
|
65
|
+
|
|
66
|
+
# Import style
|
|
67
|
+
import_style: str = "absolute" # absolute | relative
|
|
68
|
+
project_package: str = "" # top-level package name
|
|
69
|
+
|
|
70
|
+
# Existing files (for context)
|
|
71
|
+
existing_page_files: list = field(default_factory=list)
|
|
72
|
+
existing_test_files: list = field(default_factory=list)
|
|
73
|
+
existing_conftest_files: list = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
# Raw snippets for LLM context
|
|
76
|
+
sample_page_code: str = "" # snippet from existing page object
|
|
77
|
+
sample_test_code: str = "" # snippet from existing test
|
|
78
|
+
sample_conftest_code: str = "" # snippet from conftest
|
|
79
|
+
|
|
80
|
+
def to_llm_context(self) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Format the project profile as a clear context string
|
|
83
|
+
for injection into Planner and Coder prompts.
|
|
84
|
+
"""
|
|
85
|
+
lines = [
|
|
86
|
+
"=== EXISTING PROJECT PROFILE ===",
|
|
87
|
+
f"Root : {self.root}",
|
|
88
|
+
f"Pages folder : {self.pages_dir}/",
|
|
89
|
+
f"Tests folder : {self.tests_dir}/",
|
|
90
|
+
f"Test framework : {self.test_framework}",
|
|
91
|
+
f"BDD detected : {self.has_bdd}",
|
|
92
|
+
f"Base Page class : {self.base_page_class}",
|
|
93
|
+
f"Base Page import : {self.base_page_import or 'not detected'}",
|
|
94
|
+
f"Driver setup : {self.driver_setup} (scope={self.driver_scope})",
|
|
95
|
+
f"Browser : {self.browser} (headless={self.headless})",
|
|
96
|
+
f"Test file pattern : {self.test_file_prefix}<name>.py",
|
|
97
|
+
f"Page file pattern : <name>{self.page_file_suffix}.py",
|
|
98
|
+
f"Import style : {self.import_style}",
|
|
99
|
+
f"Package : {self.project_package or 'none'}",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
if self.existing_page_files:
|
|
103
|
+
lines.append(f"\nExisting pages : {', '.join(self.existing_page_files[:5])}")
|
|
104
|
+
if self.existing_test_files:
|
|
105
|
+
lines.append(f"Existing tests : {', '.join(self.existing_test_files[:5])}")
|
|
106
|
+
|
|
107
|
+
if self.sample_page_code:
|
|
108
|
+
lines.append(f"\n--- SAMPLE PAGE OBJECT (follow this style) ---\n{self.sample_page_code}\n---")
|
|
109
|
+
if self.sample_test_code:
|
|
110
|
+
lines.append(f"\n--- SAMPLE TEST FILE (follow this style) ---\n{self.sample_test_code}\n---")
|
|
111
|
+
if self.sample_conftest_code:
|
|
112
|
+
lines.append(f"\n--- CONFTEST (follow this fixture pattern) ---\n{self.sample_conftest_code}\n---")
|
|
113
|
+
|
|
114
|
+
lines.append("\nINSTRUCTION: Generated code MUST follow this exact project structure,")
|
|
115
|
+
lines.append("import style, base class, and naming convention shown above.")
|
|
116
|
+
lines.append("=================================")
|
|
117
|
+
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ProjectScanner:
|
|
122
|
+
"""
|
|
123
|
+
Scans an existing Selenium Python project and returns a ProjectProfile.
|
|
124
|
+
|
|
125
|
+
Usage:
|
|
126
|
+
scanner = ProjectScanner("/path/to/existing/project")
|
|
127
|
+
profile = scanner.scan()
|
|
128
|
+
print(profile.to_llm_context())
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
# Known folder name patterns
|
|
132
|
+
PAGES_DIRS = ["pages", "page_objects", "pageobjects", "page_object",
|
|
133
|
+
"pom", "page_models", "screens", "views", "ui"]
|
|
134
|
+
TESTS_DIRS = ["tests", "test", "test_suite", "testing", "functional",
|
|
135
|
+
"e2e", "integration", "specs", "test_cases"]
|
|
136
|
+
FEATURES_DIRS = ["features", "feature", "bdd", "scenarios"]
|
|
137
|
+
STEPS_DIRS = ["step_definitions", "steps", "step_defs", "stepdefs"]
|
|
138
|
+
UTILS_DIRS = ["utils", "utilities", "helpers", "common", "lib"]
|
|
139
|
+
BASE_DIRS = ["base", "core", "foundation", "abstract"]
|
|
140
|
+
|
|
141
|
+
# Known base class name patterns
|
|
142
|
+
BASE_PAGE_NAMES = ["BasePage", "BasePageObject", "PageBase", "BaseUI",
|
|
143
|
+
"SeleniumBase", "WebPage", "AbstractPage", "PageObject",
|
|
144
|
+
"BaseDriver", "DriverBase"]
|
|
145
|
+
|
|
146
|
+
def __init__(self, project_root: str):
|
|
147
|
+
self.root = Path(project_root).resolve()
|
|
148
|
+
self.profile = ProjectProfile(root=str(self.root))
|
|
149
|
+
|
|
150
|
+
def scan(self) -> ProjectProfile:
|
|
151
|
+
"""
|
|
152
|
+
Full project scan. Returns populated ProjectProfile.
|
|
153
|
+
Safe to call on any Python project — read-only, no writes.
|
|
154
|
+
"""
|
|
155
|
+
if not self.root.exists():
|
|
156
|
+
raise ValueError(f"Project root does not exist: {self.root}")
|
|
157
|
+
|
|
158
|
+
self._detect_folders()
|
|
159
|
+
self._detect_framework()
|
|
160
|
+
self._detect_base_classes()
|
|
161
|
+
self._detect_driver_setup()
|
|
162
|
+
self._detect_naming_conventions()
|
|
163
|
+
self._detect_import_style()
|
|
164
|
+
self._collect_existing_files()
|
|
165
|
+
self._extract_code_samples()
|
|
166
|
+
|
|
167
|
+
return self.profile
|
|
168
|
+
|
|
169
|
+
# ── Detection methods ─────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
def _detect_folders(self):
|
|
172
|
+
"""Find pages, tests, features, steps directories."""
|
|
173
|
+
all_dirs = [
|
|
174
|
+
d for d in self.root.rglob("*")
|
|
175
|
+
if d.is_dir() and not self._is_ignored(d)
|
|
176
|
+
]
|
|
177
|
+
dir_names = {d.name.lower(): d for d in all_dirs}
|
|
178
|
+
|
|
179
|
+
for name in self.PAGES_DIRS:
|
|
180
|
+
if name in dir_names:
|
|
181
|
+
self.profile.pages_dir = str(
|
|
182
|
+
dir_names[name].relative_to(self.root)
|
|
183
|
+
)
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
for name in self.TESTS_DIRS:
|
|
187
|
+
if name in dir_names:
|
|
188
|
+
self.profile.tests_dir = str(
|
|
189
|
+
dir_names[name].relative_to(self.root)
|
|
190
|
+
)
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
for name in self.FEATURES_DIRS:
|
|
194
|
+
if name in dir_names:
|
|
195
|
+
self.profile.features_dir = str(
|
|
196
|
+
dir_names[name].relative_to(self.root)
|
|
197
|
+
)
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
for name in self.STEPS_DIRS:
|
|
201
|
+
if name in dir_names:
|
|
202
|
+
self.profile.steps_dir = str(
|
|
203
|
+
dir_names[name].relative_to(self.root)
|
|
204
|
+
)
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
for name in self.UTILS_DIRS:
|
|
208
|
+
if name in dir_names:
|
|
209
|
+
self.profile.utils_dir = str(
|
|
210
|
+
dir_names[name].relative_to(self.root)
|
|
211
|
+
)
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
for name in self.BASE_DIRS:
|
|
215
|
+
if name in dir_names:
|
|
216
|
+
self.profile.base_dir = str(
|
|
217
|
+
dir_names[name].relative_to(self.root)
|
|
218
|
+
)
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
def _detect_framework(self):
|
|
222
|
+
"""Detect test framework from imports and file contents."""
|
|
223
|
+
py_files = list(self.root.rglob("*.py"))[:50] # limit for speed
|
|
224
|
+
|
|
225
|
+
has_pytest_bdd = False
|
|
226
|
+
has_pytest = False
|
|
227
|
+
has_unittest = False
|
|
228
|
+
has_conftest = False
|
|
229
|
+
|
|
230
|
+
for f in py_files:
|
|
231
|
+
if self._is_ignored(f):
|
|
232
|
+
continue
|
|
233
|
+
try:
|
|
234
|
+
content = f.read_text(encoding="utf-8", errors="ignore")
|
|
235
|
+
if "pytest_bdd" in content or "from pytest_bdd" in content:
|
|
236
|
+
has_pytest_bdd = True
|
|
237
|
+
if "import pytest" in content or "from pytest" in content:
|
|
238
|
+
has_pytest = True
|
|
239
|
+
if "unittest" in content:
|
|
240
|
+
has_unittest = True
|
|
241
|
+
if f.name == "conftest.py":
|
|
242
|
+
has_conftest = True
|
|
243
|
+
except Exception:
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
self.profile.has_bdd = has_pytest_bdd
|
|
247
|
+
self.profile.has_pytest = has_pytest
|
|
248
|
+
self.profile.has_conftest = has_conftest
|
|
249
|
+
|
|
250
|
+
if has_pytest_bdd:
|
|
251
|
+
self.profile.test_framework = "pytest-bdd"
|
|
252
|
+
elif has_pytest:
|
|
253
|
+
self.profile.test_framework = "pytest"
|
|
254
|
+
elif has_unittest:
|
|
255
|
+
self.profile.test_framework = "unittest"
|
|
256
|
+
|
|
257
|
+
def _detect_base_classes(self):
|
|
258
|
+
"""Find existing BasePage / BaseTest class definitions."""
|
|
259
|
+
py_files = list(self.root.rglob("*.py"))[:100]
|
|
260
|
+
|
|
261
|
+
for f in py_files:
|
|
262
|
+
if self._is_ignored(f):
|
|
263
|
+
continue
|
|
264
|
+
try:
|
|
265
|
+
content = f.read_text(encoding="utf-8", errors="ignore")
|
|
266
|
+
except Exception:
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
# Look for class definitions matching known base names
|
|
270
|
+
class_matches = re.findall(r"class\s+(\w+)\s*[\(:]", content)
|
|
271
|
+
for cls_name in class_matches:
|
|
272
|
+
if any(base.lower() in cls_name.lower() for base in
|
|
273
|
+
["basepage", "pagebase", "pageobject", "baseui",
|
|
274
|
+
"seleniumbase", "webpage", "abstractpage", "basedriver"]):
|
|
275
|
+
self.profile.base_page_class = cls_name
|
|
276
|
+
rel = f.relative_to(self.root)
|
|
277
|
+
# Build import path
|
|
278
|
+
parts = list(rel.with_suffix("").parts)
|
|
279
|
+
self.profile.base_page_import = ".".join(parts)
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
if any(base.lower() in cls_name.lower() for base in
|
|
283
|
+
["basetest", "testbase", "seleniumtest", "webtest"]):
|
|
284
|
+
self.profile.base_test_class = cls_name
|
|
285
|
+
rel = f.relative_to(self.root)
|
|
286
|
+
parts = list(rel.with_suffix("").parts)
|
|
287
|
+
self.profile.base_test_import = ".".join(parts)
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
def _detect_driver_setup(self):
|
|
291
|
+
"""Detect how the driver is initialized (fixture/setUp/class)."""
|
|
292
|
+
conftest_files = list(self.root.rglob("conftest.py"))
|
|
293
|
+
test_files = list(self.root.rglob("test_*.py"))[:20]
|
|
294
|
+
|
|
295
|
+
for f in conftest_files + test_files:
|
|
296
|
+
if self._is_ignored(f):
|
|
297
|
+
continue
|
|
298
|
+
try:
|
|
299
|
+
content = f.read_text(encoding="utf-8", errors="ignore")
|
|
300
|
+
except Exception:
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# Fixture pattern
|
|
304
|
+
if "@pytest.fixture" in content and "driver" in content:
|
|
305
|
+
self.profile.driver_setup = "fixture"
|
|
306
|
+
scope_match = re.search(r'@pytest\.fixture\s*\(\s*scope=["\'](\w+)["\']', content)
|
|
307
|
+
if scope_match:
|
|
308
|
+
self.profile.driver_scope = scope_match.group(1)
|
|
309
|
+
|
|
310
|
+
# unittest setUp pattern
|
|
311
|
+
elif "def setUp(self)" in content:
|
|
312
|
+
self.profile.driver_setup = "setUp"
|
|
313
|
+
|
|
314
|
+
# Browser detection
|
|
315
|
+
for browser in ["chrome", "firefox", "edge", "safari"]:
|
|
316
|
+
if browser in content.lower():
|
|
317
|
+
self.profile.browser = browser
|
|
318
|
+
break
|
|
319
|
+
|
|
320
|
+
# Headless detection
|
|
321
|
+
if "headless" in content.lower():
|
|
322
|
+
self.profile.headless = True
|
|
323
|
+
|
|
324
|
+
def _detect_naming_conventions(self):
|
|
325
|
+
"""Detect test file and page file naming patterns."""
|
|
326
|
+
py_files = list(self.root.rglob("*.py"))
|
|
327
|
+
|
|
328
|
+
test_prefixed = 0 # test_login.py
|
|
329
|
+
test_suffixed = 0 # LoginTest.py
|
|
330
|
+
page_suffixed = 0 # login_page.py
|
|
331
|
+
page_prefixed = 0 # page_login.py
|
|
332
|
+
|
|
333
|
+
for f in py_files:
|
|
334
|
+
if self._is_ignored(f):
|
|
335
|
+
continue
|
|
336
|
+
name = f.stem # filename without extension
|
|
337
|
+
if name.startswith("test_"):
|
|
338
|
+
test_prefixed += 1
|
|
339
|
+
elif name.endswith("Test") or name.endswith("Tests"):
|
|
340
|
+
test_suffixed += 1
|
|
341
|
+
if name.endswith("_page") or name.endswith("_Page"):
|
|
342
|
+
page_suffixed += 1
|
|
343
|
+
elif name.endswith("Page") or name.endswith("PageObject"):
|
|
344
|
+
page_prefixed += 1
|
|
345
|
+
|
|
346
|
+
# Set based on majority
|
|
347
|
+
if test_suffixed > test_prefixed:
|
|
348
|
+
self.profile.test_file_prefix = ""
|
|
349
|
+
self.profile.test_file_suffix = "Test"
|
|
350
|
+
else:
|
|
351
|
+
self.profile.test_file_prefix = "test_"
|
|
352
|
+
self.profile.test_file_suffix = ""
|
|
353
|
+
|
|
354
|
+
if page_prefixed > page_suffixed:
|
|
355
|
+
self.profile.page_file_suffix = "Page"
|
|
356
|
+
else:
|
|
357
|
+
self.profile.page_file_suffix = "_page"
|
|
358
|
+
|
|
359
|
+
def _detect_import_style(self):
|
|
360
|
+
"""Detect relative vs absolute imports."""
|
|
361
|
+
py_files = list(self.root.rglob("*.py"))[:30]
|
|
362
|
+
relative = 0
|
|
363
|
+
absolute = 0
|
|
364
|
+
|
|
365
|
+
for f in py_files:
|
|
366
|
+
if self._is_ignored(f):
|
|
367
|
+
continue
|
|
368
|
+
try:
|
|
369
|
+
content = f.read_text(encoding="utf-8", errors="ignore")
|
|
370
|
+
except Exception:
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
if re.search(r"^from \.", content, re.MULTILINE):
|
|
374
|
+
relative += 1
|
|
375
|
+
if re.search(r"^from [a-zA-Z]", content, re.MULTILINE):
|
|
376
|
+
absolute += 1
|
|
377
|
+
|
|
378
|
+
self.profile.import_style = "relative" if relative > absolute else "absolute"
|
|
379
|
+
|
|
380
|
+
# Try to detect top-level package
|
|
381
|
+
init_files = [
|
|
382
|
+
f for f in self.root.glob("*/__init__.py") if not self._is_ignored(f)
|
|
383
|
+
]
|
|
384
|
+
if init_files:
|
|
385
|
+
self.profile.project_package = init_files[0].parent.name
|
|
386
|
+
|
|
387
|
+
def _collect_existing_files(self):
|
|
388
|
+
"""Collect lists of existing page and test files."""
|
|
389
|
+
pages_path = self.root / self.profile.pages_dir
|
|
390
|
+
tests_path = self.root / self.profile.tests_dir
|
|
391
|
+
|
|
392
|
+
if pages_path.exists():
|
|
393
|
+
self.profile.existing_page_files = [
|
|
394
|
+
f.name for f in pages_path.rglob("*.py")
|
|
395
|
+
if not f.name.startswith("__")
|
|
396
|
+
][:10]
|
|
397
|
+
|
|
398
|
+
if tests_path.exists():
|
|
399
|
+
self.profile.existing_test_files = [
|
|
400
|
+
f.name for f in tests_path.rglob("*.py")
|
|
401
|
+
if not f.name.startswith("__")
|
|
402
|
+
][:10]
|
|
403
|
+
|
|
404
|
+
self.profile.existing_conftest_files = [
|
|
405
|
+
str(f.relative_to(self.root))
|
|
406
|
+
for f in self.root.rglob("conftest.py")
|
|
407
|
+
][:5]
|
|
408
|
+
|
|
409
|
+
def _extract_code_samples(self):
|
|
410
|
+
"""Extract small code snippets as style examples for LLM."""
|
|
411
|
+
pages_path = self.root / self.profile.pages_dir
|
|
412
|
+
tests_path = self.root / self.profile.tests_dir
|
|
413
|
+
|
|
414
|
+
# Sample page object (first 40 lines)
|
|
415
|
+
if pages_path.exists():
|
|
416
|
+
for f in pages_path.rglob("*.py"):
|
|
417
|
+
if f.name.startswith("__"):
|
|
418
|
+
continue
|
|
419
|
+
try:
|
|
420
|
+
lines = f.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
421
|
+
self.profile.sample_page_code = "\n".join(lines[:40])
|
|
422
|
+
break
|
|
423
|
+
except Exception:
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
# Sample test file (first 40 lines)
|
|
427
|
+
if tests_path.exists():
|
|
428
|
+
for f in tests_path.rglob("*.py"):
|
|
429
|
+
if f.name.startswith("__"):
|
|
430
|
+
continue
|
|
431
|
+
try:
|
|
432
|
+
lines = f.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
433
|
+
self.profile.sample_test_code = "\n".join(lines[:40])
|
|
434
|
+
break
|
|
435
|
+
except Exception:
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
# Conftest sample
|
|
439
|
+
for conftest_path in self.root.rglob("conftest.py"):
|
|
440
|
+
try:
|
|
441
|
+
lines = conftest_path.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
442
|
+
self.profile.sample_conftest_code = "\n".join(lines[:30])
|
|
443
|
+
break
|
|
444
|
+
except Exception:
|
|
445
|
+
continue
|
|
446
|
+
|
|
447
|
+
def _is_ignored(self, path: Path) -> bool:
|
|
448
|
+
"""Skip venv, cache, git, node_modules etc."""
|
|
449
|
+
ignored = {".venv", "venv", "__pycache__", ".git", ".idea",
|
|
450
|
+
"node_modules", ".pytest_cache", "dist", "build",
|
|
451
|
+
".eggs", "*.egg-info", ".tox", ".mypy_cache"}
|
|
452
|
+
return any(part in ignored for part in path.parts)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from selenium_agent.selenium.driver_factory import DriverFactory
|
|
2
|
+
from selenium_agent.selenium.base_page import BasePage
|
|
3
|
+
from selenium_agent.selenium.locator_advisor import LocatorAdvisor
|
|
4
|
+
from selenium_agent.selenium.error_map import SeleniumErrorMap
|
|
5
|
+
|
|
6
|
+
__all__ = ["DriverFactory", "BasePage", "LocatorAdvisor", "SeleniumErrorMap"]
|