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,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PLANNER AGENT
|
|
3
|
+
=============
|
|
4
|
+
Works like Playwright's planner agent, for Selenium Python:
|
|
5
|
+
|
|
6
|
+
1. Opens a real (headless) browser and scans the live DOM of the target
|
|
7
|
+
URL — optionally exploring same-origin pages — so plans use REAL
|
|
8
|
+
locators, never guessed ones.
|
|
9
|
+
2. Asks the LLM for a structured JSON plan (scenarios, page objects,
|
|
10
|
+
locators, wait strategy).
|
|
11
|
+
3. The Orchestrator persists the plan to specs/<slug>.md (human-readable)
|
|
12
|
+
and specs/<slug>.json (Generator input).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from selenium_agent.utils.logger import setup_logger
|
|
16
|
+
from selenium_agent.utils.llm import create_llm_client, DEFAULT_PROVIDER, get_default_model
|
|
17
|
+
from selenium_agent.utils.json_utils import extract_json_object, LLMJSONError
|
|
18
|
+
from selenium_agent.selenium.locator_scanner import (
|
|
19
|
+
scan_site_locators, format_site_for_llm,
|
|
20
|
+
)
|
|
21
|
+
from selenium_agent.bdd.gherkin_advisor import GherkinAdvisor
|
|
22
|
+
|
|
23
|
+
logger = setup_logger("PlannerAgent")
|
|
24
|
+
|
|
25
|
+
_SYSTEM_BASE = """
|
|
26
|
+
You are an expert QA Test Planner specializing in Selenium Python automation.
|
|
27
|
+
|
|
28
|
+
══ PAGE OBJECT ARCHITECTURE — MOST IMPORTANT RULE ══
|
|
29
|
+
|
|
30
|
+
Every distinct page MUST have its OWN class. NEVER mix pages.
|
|
31
|
+
Class names and locators come from the actual app under test.
|
|
32
|
+
|
|
33
|
+
EXAMPLE PATTERN (illustrative only — use actual app's pages and locators):
|
|
34
|
+
|
|
35
|
+
class LoginPage(BasePage): ← only login elements
|
|
36
|
+
class DashboardPage(BasePage): ← only dashboard elements
|
|
37
|
+
class ProductPage(BasePage): ← only product page elements
|
|
38
|
+
class CartPage(BasePage): ← only cart elements
|
|
39
|
+
class CheckoutPage(BasePage): ← only checkout elements
|
|
40
|
+
|
|
41
|
+
Each class contains ONLY the locators for that page.
|
|
42
|
+
FORBIDDEN: putting page B locators inside page A's class.
|
|
43
|
+
|
|
44
|
+
══ MULTI-STEP FLOW RULE ══
|
|
45
|
+
|
|
46
|
+
For flows that span multiple pages (login → inventory → cart → checkout → logout):
|
|
47
|
+
- Plan SEPARATE page objects for EACH page
|
|
48
|
+
- page_objects_needed must list ALL pages: ["LoginPage", "InventoryPage", "CartPage", "CheckoutPage", "MenuPage"]
|
|
49
|
+
- Each locator entry must specify which page_object it belongs to
|
|
50
|
+
- wait_for_url() must be called after every navigation click
|
|
51
|
+
|
|
52
|
+
══ LOCATOR RULES ══
|
|
53
|
+
1. CSS preferred over XPath
|
|
54
|
+
2. XPath only when CSS cannot express it
|
|
55
|
+
3. NEVER absolute XPath, NEVER index-based XPath
|
|
56
|
+
4. Priority: data-test > data-testid > id > name > css
|
|
57
|
+
5. Use ONLY locators from DOM scan — never invent
|
|
58
|
+
|
|
59
|
+
══ WAIT STRATEGY ══
|
|
60
|
+
- Input fields → fluent_wait(locator, 'visible')
|
|
61
|
+
- Buttons/links → fluent_wait(locator, 'clickable')
|
|
62
|
+
- After navigation → wait_for_url('partial-url-string')
|
|
63
|
+
- Loaders → fluent_wait(locator, 'invisible')
|
|
64
|
+
No time.sleep(). No hardcoded waits.
|
|
65
|
+
|
|
66
|
+
══ FORM FILLING ══
|
|
67
|
+
For React/SPA forms always use safe_type() — not type():
|
|
68
|
+
safe_type(page, page.FIRST_NAME_INPUT, "John")
|
|
69
|
+
|
|
70
|
+
══ SCENARIO QUALITY (enterprise test design) ══
|
|
71
|
+
- Cover the happy path FIRST, then negative/edge cases relevant to the instruction
|
|
72
|
+
- Every scenario independent — no shared state between scenarios
|
|
73
|
+
- Each scenario has explicit expected_result that a machine can assert
|
|
74
|
+
- Use realistic test_data (from the instruction when given)
|
|
75
|
+
- DOM-scan text shows the page BEFORE any action. expected_result must
|
|
76
|
+
describe the state AFTER the action (what changes/appears), never the
|
|
77
|
+
pre-action text you saw in the scan
|
|
78
|
+
- If the flow CREATES an entity (account, record), mark its test_data
|
|
79
|
+
values as "GENERATE_UNIQUE_AT_RUNTIME" — hardcoded values collide with
|
|
80
|
+
previous runs (e.g. "email already taken")
|
|
81
|
+
- When a scanned page contains a form, plan locators and fill-steps for
|
|
82
|
+
ALL its inputs/selects — apps silently reject submissions with missing
|
|
83
|
+
required fields, even if the instruction names only a few
|
|
84
|
+
|
|
85
|
+
OTHER RULES:
|
|
86
|
+
- Use EXACT target URL — never example.com
|
|
87
|
+
- headless comes from config
|
|
88
|
+
- Respond with valid JSON only — no markdown fences
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
_PYTEST_SCHEMA = """
|
|
92
|
+
{
|
|
93
|
+
"mode": "pytest",
|
|
94
|
+
"url": "<EXACT URL from instruction>",
|
|
95
|
+
"browser": "chrome",
|
|
96
|
+
"headless": false,
|
|
97
|
+
"test_scenarios": [
|
|
98
|
+
{
|
|
99
|
+
"id": "TC001",
|
|
100
|
+
"name": "descriptive scenario name",
|
|
101
|
+
"description": "what this test verifies",
|
|
102
|
+
"steps": [
|
|
103
|
+
"open <PageName>.URL",
|
|
104
|
+
"type <field> on <PageName>",
|
|
105
|
+
"click <button> on <PageName>",
|
|
106
|
+
"wait_for_url('<url-fragment>')",
|
|
107
|
+
"interact with <NextPageName>"
|
|
108
|
+
],
|
|
109
|
+
"expected_result": "what success looks like",
|
|
110
|
+
"test_data": {"key": "value"}
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"page_objects_needed": ["<Page1Name>", "<Page2Name>"],
|
|
114
|
+
"locators": [
|
|
115
|
+
{
|
|
116
|
+
"page_object": "<PageName>",
|
|
117
|
+
"element": "descriptive element name",
|
|
118
|
+
"css": "<css selector>",
|
|
119
|
+
"xpath": "<xpath>",
|
|
120
|
+
"preferred": "css",
|
|
121
|
+
"wait_condition": "visible"
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"pytest_markers": ["smoke"],
|
|
125
|
+
"notes": ""
|
|
126
|
+
}
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
_BDD_SCHEMA = """
|
|
130
|
+
{
|
|
131
|
+
"mode": "bdd",
|
|
132
|
+
"url": "<EXACT URL from instruction>",
|
|
133
|
+
"feature_name": "<feature name>",
|
|
134
|
+
"feature_title": "<Feature Title>",
|
|
135
|
+
"role": "<user role>",
|
|
136
|
+
"goal": "<goal>",
|
|
137
|
+
"benefit": "<benefit>",
|
|
138
|
+
"browser": "chrome",
|
|
139
|
+
"headless": false,
|
|
140
|
+
"scenarios": [
|
|
141
|
+
{
|
|
142
|
+
"id": "TC001",
|
|
143
|
+
"name": "descriptive scenario name",
|
|
144
|
+
"tags": ["smoke"],
|
|
145
|
+
"steps": [
|
|
146
|
+
{"type": "Given", "text": "I am on the <page> page"},
|
|
147
|
+
{"type": "When", "text": "I perform <action>"},
|
|
148
|
+
{"type": "Then", "text": "I should see <result>"}
|
|
149
|
+
],
|
|
150
|
+
"test_data": {}
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"page_objects_needed": ["<Page1Name>", "<Page2Name>"],
|
|
154
|
+
"locators": [
|
|
155
|
+
{
|
|
156
|
+
"page_object": "<PageName>",
|
|
157
|
+
"element": "descriptive element name",
|
|
158
|
+
"css": "<css selector>",
|
|
159
|
+
"preferred": "css",
|
|
160
|
+
"wait_condition": "visible"
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
"notes": ""
|
|
164
|
+
}
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
_MULTI_PAGE_KEYWORDS = [
|
|
168
|
+
"checkout", "cart", "logout", "login and", "then", "after",
|
|
169
|
+
"navigate", "flow", "end to end", "e2e", "journey",
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class PlannerAgent:
|
|
174
|
+
def __init__(self, api_key: str, provider: str = DEFAULT_PROVIDER,
|
|
175
|
+
model: str | None = None):
|
|
176
|
+
resolved_model = model or get_default_model(provider)
|
|
177
|
+
self.client = create_llm_client(
|
|
178
|
+
provider=provider, api_key=api_key, model=resolved_model
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def plan(self, user_instruction: str, mode: str = "pytest",
|
|
182
|
+
project_profile=None, headless: bool = False,
|
|
183
|
+
target_url: str | None = None,
|
|
184
|
+
explore_pages: int = 0) -> dict:
|
|
185
|
+
"""
|
|
186
|
+
Build a test plan.
|
|
187
|
+
|
|
188
|
+
explore_pages: how many same-origin pages beyond the target URL to
|
|
189
|
+
scan for locators (0 = target page only). Multi-page instructions
|
|
190
|
+
automatically get exploration even when not requested.
|
|
191
|
+
"""
|
|
192
|
+
logger.info(f"📋 Planning [{mode.upper()}] for: {user_instruction}")
|
|
193
|
+
|
|
194
|
+
schema = _BDD_SCHEMA if mode == "bdd" else _PYTEST_SCHEMA
|
|
195
|
+
system = _SYSTEM_BASE + f"\nRespond using this JSON schema:\n{schema}"
|
|
196
|
+
|
|
197
|
+
is_multi_page = (
|
|
198
|
+
sum(1 for kw in _MULTI_PAGE_KEYWORDS if kw in user_instruction.lower()) >= 2
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# ── DOM scan BEFORE planning — real locators, never guessed ──
|
|
202
|
+
dom_context = ""
|
|
203
|
+
if target_url:
|
|
204
|
+
extra = explore_pages or (2 if is_multi_page else 0)
|
|
205
|
+
logger.info(f"🔍 Pre-plan DOM scan: {target_url} (explore={extra} extra pages)")
|
|
206
|
+
site = scan_site_locators(target_url, headless=True, max_extra_pages=extra,
|
|
207
|
+
instruction=user_instruction)
|
|
208
|
+
dom_context = format_site_for_llm(site, context="planning")
|
|
209
|
+
total = sum(len(v) for v in site.values())
|
|
210
|
+
if total:
|
|
211
|
+
logger.info(f"✅ {total} real locators from {len(site)} page(s) injected into plan prompt")
|
|
212
|
+
else:
|
|
213
|
+
logger.warning("⚠️ DOM scan returned nothing — LLM will infer locators")
|
|
214
|
+
|
|
215
|
+
multi_page_hint = ""
|
|
216
|
+
if is_multi_page:
|
|
217
|
+
multi_page_hint = (
|
|
218
|
+
"\n\n🚨 MULTI-PAGE FLOW DETECTED — MANDATORY RULES:\n"
|
|
219
|
+
" 1. Create a SEPARATE page object class for EACH page visited\n"
|
|
220
|
+
" 2. page_objects_needed must list ALL pages with meaningful names based on the app\n"
|
|
221
|
+
" 3. Each locator entry must have 'page_object' field specifying which class it belongs to\n"
|
|
222
|
+
" 4. NEVER put locators from one page inside another page's class\n"
|
|
223
|
+
" 5. Add wait_for_url() after every navigation step\n"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
url_block = ""
|
|
227
|
+
if target_url:
|
|
228
|
+
url_block = (
|
|
229
|
+
f"\n\n🚨 TARGET URL — use EXACTLY:\n"
|
|
230
|
+
f" URL = '{target_url}'\n"
|
|
231
|
+
f" NEVER use example.com or any other URL.\n"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
project_context = ""
|
|
235
|
+
if project_profile:
|
|
236
|
+
project_context = (
|
|
237
|
+
f"\n\nFit into this existing project:\n"
|
|
238
|
+
f"{project_profile.to_llm_context()}\n"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
user_prompt = (
|
|
242
|
+
f"Create a Selenium Python {mode} test plan for: {user_instruction}\n"
|
|
243
|
+
f"headless: {str(headless).lower()}"
|
|
244
|
+
f"{url_block}"
|
|
245
|
+
f"{multi_page_hint}"
|
|
246
|
+
f"\n\n{dom_context}"
|
|
247
|
+
f"{project_context}"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
max_tokens = 8000 if is_multi_page else 4000
|
|
251
|
+
raw = self.client.generate_text(
|
|
252
|
+
system_prompt=system,
|
|
253
|
+
user_prompt=user_prompt,
|
|
254
|
+
max_tokens=max_tokens,
|
|
255
|
+
json_mode=True,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
plan = extract_json_object(raw)
|
|
260
|
+
except LLMJSONError:
|
|
261
|
+
logger.warning("⚠️ Plan JSON unparseable — retrying once with strict reminder")
|
|
262
|
+
raw = self.client.generate_text(
|
|
263
|
+
system_prompt=system,
|
|
264
|
+
user_prompt=user_prompt + "\n\nIMPORTANT: respond with ONLY the JSON object. "
|
|
265
|
+
"No prose, no markdown fences.",
|
|
266
|
+
max_tokens=max_tokens,
|
|
267
|
+
json_mode=True,
|
|
268
|
+
)
|
|
269
|
+
plan = extract_json_object(raw)
|
|
270
|
+
|
|
271
|
+
plan["mode"] = mode
|
|
272
|
+
plan["headless"] = headless
|
|
273
|
+
if target_url:
|
|
274
|
+
plan["url"] = target_url
|
|
275
|
+
|
|
276
|
+
count = len(plan.get("test_scenarios") or plan.get("scenarios", []))
|
|
277
|
+
pages = plan.get("page_objects_needed", [])
|
|
278
|
+
logger.info(f"✅ Plan ready: {count} scenario(s) | pages={pages} | url={plan.get('url')}")
|
|
279
|
+
return plan
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from selenium_agent.bdd.gherkin_advisor import GherkinAdvisor
|
|
2
|
+
from selenium_agent.bdd.templates import (
|
|
3
|
+
FEATURE_FILE_TEMPLATE,
|
|
4
|
+
SCENARIO_TEMPLATE,
|
|
5
|
+
STEP_DEFINITION_TEMPLATE,
|
|
6
|
+
CONFTEST_TEMPLATE,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"GherkinAdvisor",
|
|
11
|
+
"FEATURE_FILE_TEMPLATE",
|
|
12
|
+
"SCENARIO_TEMPLATE",
|
|
13
|
+
"STEP_DEFINITION_TEMPLATE",
|
|
14
|
+
"CONFTEST_TEMPLATE",
|
|
15
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GHERKIN ADVISOR
|
|
3
|
+
===============
|
|
4
|
+
pytest-bdd specific intelligence for writing good BDD tests.
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
- Gherkin best practices guide (injected into LLM prompts)
|
|
8
|
+
- Step naming conventions
|
|
9
|
+
- Anti-pattern detection
|
|
10
|
+
- Feature file structure rules
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class StepValidation:
|
|
19
|
+
"""Result of validating a Gherkin step."""
|
|
20
|
+
valid: bool
|
|
21
|
+
warnings: list[str]
|
|
22
|
+
suggestions: list[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GherkinAdvisor:
|
|
26
|
+
"""
|
|
27
|
+
Advises on writing good Gherkin feature files and step definitions.
|
|
28
|
+
Used by Planner and Coder agents when BDD mode is enabled.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
BEST_PRACTICES_GUIDE = """
|
|
32
|
+
PYTEST-BDD / GHERKIN BEST PRACTICES:
|
|
33
|
+
|
|
34
|
+
FEATURE FILE RULES:
|
|
35
|
+
1. One Feature per file — named <feature>.feature
|
|
36
|
+
2. Feature title describes the business capability, not the UI
|
|
37
|
+
3. Scenarios should be independent — no shared state
|
|
38
|
+
4. Use Scenario Outline for data-driven tests
|
|
39
|
+
5. Tags (@smoke, @regression) go above Scenario
|
|
40
|
+
|
|
41
|
+
STEP WRITING RULES:
|
|
42
|
+
1. Given → System state / precondition
|
|
43
|
+
✅ Given I am on the login page
|
|
44
|
+
✅ Given the user "admin" exists in the system
|
|
45
|
+
❌ Given I click the login button (that's a When)
|
|
46
|
+
|
|
47
|
+
2. When → User action
|
|
48
|
+
✅ When I enter username "standard_user"
|
|
49
|
+
✅ When I click the submit button
|
|
50
|
+
❌ When the page shows an error (that's a Then)
|
|
51
|
+
|
|
52
|
+
3. Then → Expected outcome / assertion
|
|
53
|
+
✅ Then I should see the dashboard
|
|
54
|
+
✅ Then the error message "Invalid credentials" should be displayed
|
|
55
|
+
❌ Then I click OK (that's a When)
|
|
56
|
+
|
|
57
|
+
4. And/But → Continuation of previous step type
|
|
58
|
+
✅ When I enter username "user"
|
|
59
|
+
And I enter password "pass"
|
|
60
|
+
And I click Login
|
|
61
|
+
|
|
62
|
+
STEP DEFINITION RULES:
|
|
63
|
+
1. Use parsers.parse() for parameterized steps:
|
|
64
|
+
@when(parsers.parse('I enter username "{username}"'))
|
|
65
|
+
def enter_username(login_page, username):
|
|
66
|
+
login_page.type(LoginPage.USERNAME_INPUT, username)
|
|
67
|
+
|
|
68
|
+
2. Never put assertions in Given/When steps
|
|
69
|
+
3. Always use page object methods — never raw driver calls
|
|
70
|
+
4. Fixtures inject page objects, not driver directly
|
|
71
|
+
|
|
72
|
+
ANTI-PATTERNS TO AVOID:
|
|
73
|
+
❌ UI-centric steps: "When I click the blue button at position 3"
|
|
74
|
+
❌ Steps with implementation details: "When I send POST to /api/login"
|
|
75
|
+
❌ Chained UI actions in one step: "When I login and navigate to profile"
|
|
76
|
+
❌ Assertions in Given: "Given the error message is shown"
|
|
77
|
+
❌ Vague Then: "Then it works"
|
|
78
|
+
|
|
79
|
+
GOOD SCENARIO EXAMPLE:
|
|
80
|
+
@smoke @login
|
|
81
|
+
Scenario: Successful login with valid credentials
|
|
82
|
+
Given I am on the login page
|
|
83
|
+
When I enter username "standard_user"
|
|
84
|
+
And I enter password "secret_sauce"
|
|
85
|
+
And I click the login button
|
|
86
|
+
Then I should be redirected to the products page
|
|
87
|
+
And the page title should be "Products"
|
|
88
|
+
|
|
89
|
+
SCENARIO OUTLINE EXAMPLE (data-driven):
|
|
90
|
+
Scenario Outline: Login with multiple users
|
|
91
|
+
Given I am on the login page
|
|
92
|
+
When I enter username "<username>"
|
|
93
|
+
And I enter password "<password>"
|
|
94
|
+
Then I should see "<expected_result>"
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
| username | password | expected_result |
|
|
98
|
+
| standard_user | secret_sauce | Products |
|
|
99
|
+
| locked_out_user | secret_sauce | Epic sadface logo |
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def get_guide(cls) -> str:
|
|
104
|
+
"""Return full Gherkin best practices guide for LLM prompt injection."""
|
|
105
|
+
return cls.BEST_PRACTICES_GUIDE
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def validate_step(cls, step: str) -> StepValidation:
|
|
109
|
+
"""
|
|
110
|
+
Validate a single Gherkin step for common anti-patterns.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
step: Full step string e.g. "When I click the blue button at index 3"
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
StepValidation with warnings and suggestions
|
|
117
|
+
"""
|
|
118
|
+
warnings = []
|
|
119
|
+
suggestions = []
|
|
120
|
+
step_lower = step.lower()
|
|
121
|
+
|
|
122
|
+
# UI-centric anti-patterns
|
|
123
|
+
ui_terms = ["click", "button", "input", "field", "textbox", "dropdown"]
|
|
124
|
+
if any(t in step_lower for t in ui_terms) and step_lower.startswith("given"):
|
|
125
|
+
warnings.append("Given steps should describe state, not UI actions")
|
|
126
|
+
suggestions.append("Rewrite as: Given the user is authenticated")
|
|
127
|
+
|
|
128
|
+
# Index-based references
|
|
129
|
+
if any(w in step_lower for w in ["index", "position", "number", "nth"]):
|
|
130
|
+
warnings.append("Index-based steps are brittle")
|
|
131
|
+
suggestions.append("Reference elements by label or role instead")
|
|
132
|
+
|
|
133
|
+
# Assertion in Given/When
|
|
134
|
+
if step_lower.startswith(("given", "when")) and any(
|
|
135
|
+
w in step_lower for w in ["should", "must", "verify", "assert", "check"]
|
|
136
|
+
):
|
|
137
|
+
warnings.append("Assertions belong in Then steps, not Given/When")
|
|
138
|
+
suggestions.append("Move verification to a Then step")
|
|
139
|
+
|
|
140
|
+
# Vague Then
|
|
141
|
+
if step_lower.startswith("then") and any(
|
|
142
|
+
w in step_lower for w in ["it works", "success", "ok", "done"]
|
|
143
|
+
):
|
|
144
|
+
warnings.append("Vague Then step — be specific about what is verified")
|
|
145
|
+
suggestions.append("Specify exact element, message, or URL expected")
|
|
146
|
+
|
|
147
|
+
# Too many actions in one step
|
|
148
|
+
action_words = ["and", "then", "also", "afterwards"]
|
|
149
|
+
if sum(1 for w in action_words if f" {w} " in step_lower) >= 2:
|
|
150
|
+
warnings.append("Step contains multiple actions — split into separate steps")
|
|
151
|
+
suggestions.append("Use And/But to chain related steps")
|
|
152
|
+
|
|
153
|
+
return StepValidation(
|
|
154
|
+
valid=len(warnings) == 0,
|
|
155
|
+
warnings=warnings,
|
|
156
|
+
suggestions=suggestions,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def validate_feature(cls, feature_content: str) -> list[StepValidation]:
|
|
161
|
+
"""Validate all steps in a feature file."""
|
|
162
|
+
results = []
|
|
163
|
+
for line in feature_content.splitlines():
|
|
164
|
+
line = line.strip()
|
|
165
|
+
if any(line.startswith(kw) for kw in ("Given", "When", "Then", "And", "But")):
|
|
166
|
+
results.append(cls.validate_step(line))
|
|
167
|
+
return results
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def get_folder_structure(cls) -> str:
|
|
171
|
+
"""Return recommended BDD folder structure for generated tests."""
|
|
172
|
+
return """
|
|
173
|
+
RECOMMENDED pytest-bdd FOLDER STRUCTURE:
|
|
174
|
+
|
|
175
|
+
generated_tests/
|
|
176
|
+
├── features/ ← .feature files (Gherkin)
|
|
177
|
+
│ └── login.feature
|
|
178
|
+
├── step_definitions/ ← pytest-bdd step implementations
|
|
179
|
+
│ └── test_login_steps.py ← must start with test_ for pytest discovery
|
|
180
|
+
├── pages/ ← Page Objects (BasePage subclasses)
|
|
181
|
+
│ └── login_page.py
|
|
182
|
+
└── conftest.py ← shared fixtures (driver, etc.)
|
|
183
|
+
|
|
184
|
+
IMPORTANT:
|
|
185
|
+
- step_definitions files MUST start with test_ for pytest to discover them
|
|
186
|
+
- Feature files go in features/ folder
|
|
187
|
+
- conftest.py must be at root of generated_tests/
|
|
188
|
+
- scenarios() call in step file links to feature file
|
|
189
|
+
"""
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BDD TEMPLATES
|
|
3
|
+
=============
|
|
4
|
+
Pre-built templates for pytest-bdd feature files and step definitions.
|
|
5
|
+
Coder Agent uses these as base when generating BDD test code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
FEATURE_FILE_TEMPLATE = '''\
|
|
9
|
+
# {feature_name}.feature
|
|
10
|
+
# Generated by selenium-python-ai-agent
|
|
11
|
+
|
|
12
|
+
Feature: {feature_title}
|
|
13
|
+
As a {role}
|
|
14
|
+
I want to {goal}
|
|
15
|
+
So that {benefit}
|
|
16
|
+
|
|
17
|
+
{scenarios}
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
SCENARIO_TEMPLATE = '''\
|
|
21
|
+
Scenario: {scenario_name}
|
|
22
|
+
{steps}
|
|
23
|
+
'''
|
|
24
|
+
|
|
25
|
+
STEP_DEFINITION_TEMPLATE = '''\
|
|
26
|
+
"""
|
|
27
|
+
Step Definitions for: {feature_name}
|
|
28
|
+
Generated by selenium-python-ai-agent
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import pytest
|
|
32
|
+
from pytest_bdd import given, when, then, parsers, scenarios
|
|
33
|
+
from selenium_agent.selenium.driver_factory import DriverFactory
|
|
34
|
+
from pages.{page_module} import {page_class}
|
|
35
|
+
|
|
36
|
+
# Link to feature file
|
|
37
|
+
scenarios("../features/{feature_name}.feature")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── Fixtures ──────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def driver():
|
|
44
|
+
d = DriverFactory.create(browser="chrome", headless=True)
|
|
45
|
+
yield d
|
|
46
|
+
d.quit()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def {page_fixture}(driver):
|
|
51
|
+
page = {page_class}(driver)
|
|
52
|
+
page.open({page_class}.URL)
|
|
53
|
+
return page
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── Step Definitions ─────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
{steps}
|
|
59
|
+
'''
|
|
60
|
+
|
|
61
|
+
GIVEN_STEP_TEMPLATE = '''\
|
|
62
|
+
@given("{step_text}")
|
|
63
|
+
def {func_name}({params}):
|
|
64
|
+
"""{step_text}"""
|
|
65
|
+
{body}
|
|
66
|
+
'''
|
|
67
|
+
|
|
68
|
+
WHEN_STEP_TEMPLATE = '''\
|
|
69
|
+
@when("{step_text}")
|
|
70
|
+
def {func_name}({params}):
|
|
71
|
+
"""{step_text}"""
|
|
72
|
+
{body}
|
|
73
|
+
'''
|
|
74
|
+
|
|
75
|
+
THEN_STEP_TEMPLATE = '''\
|
|
76
|
+
@then("{step_text}")
|
|
77
|
+
def {func_name}({params}):
|
|
78
|
+
"""{step_text}"""
|
|
79
|
+
{body}
|
|
80
|
+
'''
|
|
81
|
+
|
|
82
|
+
CONFTEST_TEMPLATE = '''\
|
|
83
|
+
"""
|
|
84
|
+
conftest.py — pytest-bdd shared fixtures
|
|
85
|
+
Generated by selenium-python-ai-agent
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
import pytest
|
|
89
|
+
from selenium_agent.selenium.driver_factory import DriverFactory
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.fixture(scope="function")
|
|
93
|
+
def driver():
|
|
94
|
+
"""Create WebDriver instance for each test."""
|
|
95
|
+
d = DriverFactory.create(browser="chrome", headless=True)
|
|
96
|
+
yield d
|
|
97
|
+
d.quit()
|
|
98
|
+
'''
|