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,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
+ '''