Gen AI
Browse files- app.py +522 -0
- genai_handler.py +212 -0
- requirements.txt +9 -0
- scraper.py +150 -0
- utils.py +56 -0
app.py
ADDED
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ai_test_generator/app.py
|
2 |
+
import gradio as gr
|
3 |
+
import pandas as pd
|
4 |
+
import json
|
5 |
+
import logging
|
6 |
+
import os
|
7 |
+
import html # Keep for potential future use, though not strictly needed now
|
8 |
+
|
9 |
+
from scraper import extract_elements
|
10 |
+
from genai_handler import generate_test_cases, generate_selenium_script
|
11 |
+
from utils import save_elements_to_json, save_test_cases_to_excel, save_scripts_to_excel
|
12 |
+
|
13 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
14 |
+
|
15 |
+
# Ensure output directory exists
|
16 |
+
OUTPUT_DIR = "outputs"
|
17 |
+
if not os.path.exists(OUTPUT_DIR):
|
18 |
+
os.makedirs(OUTPUT_DIR)
|
19 |
+
|
20 |
+
# --- Custom CSS (Keep the enhanced CSS from the previous version) ---
|
21 |
+
custom_css = """
|
22 |
+
/* === Body and General Styles === */
|
23 |
+
body {
|
24 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
25 |
+
background: linear-gradient(to bottom right, #e0f2f7, #ffffff); /* Light blue gradient */
|
26 |
+
color: #333;
|
27 |
+
margin: 0;
|
28 |
+
padding: 0;
|
29 |
+
}
|
30 |
+
|
31 |
+
/* Gradio container adjustments */
|
32 |
+
.gradio-container {
|
33 |
+
max-width: 1200px; /* Increase max width */
|
34 |
+
margin: 0 auto !important; /* Center the container */
|
35 |
+
border-radius: 10px;
|
36 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
37 |
+
background-color: #ffffff; /* White background for content area */
|
38 |
+
overflow: hidden; /* Ensure shadows are contained */
|
39 |
+
}
|
40 |
+
|
41 |
+
/* === Header Simulation === */
|
42 |
+
.app-header {
|
43 |
+
background: linear-gradient(to right, #007bff, #0056b3); /* Blue gradient header */
|
44 |
+
color: white;
|
45 |
+
padding: 20px 30px;
|
46 |
+
text-align: center;
|
47 |
+
border-bottom: 3px solid #004085;
|
48 |
+
}
|
49 |
+
.app-header h1 {
|
50 |
+
margin: 0;
|
51 |
+
font-size: 2.2em;
|
52 |
+
font-weight: 600;
|
53 |
+
letter-spacing: 1px;
|
54 |
+
}
|
55 |
+
.app-header p {
|
56 |
+
margin-top: 8px;
|
57 |
+
font-size: 1.1em;
|
58 |
+
opacity: 0.9;
|
59 |
+
}
|
60 |
+
|
61 |
+
/* === Content Area Styling === */
|
62 |
+
.control-section {
|
63 |
+
padding: 25px 30px;
|
64 |
+
background-color: #f8f9fa; /* Light grey for control section */
|
65 |
+
border-bottom: 1px solid #dee2e6;
|
66 |
+
border-radius: 8px;
|
67 |
+
margin: 20px;
|
68 |
+
box-shadow: 0 2px 5px rgba(0,0,0, 0.05);
|
69 |
+
}
|
70 |
+
|
71 |
+
.control-section label {
|
72 |
+
font-weight: 600;
|
73 |
+
color: #0056b3; /* Darker blue for labels */
|
74 |
+
margin-bottom: 8px !important;
|
75 |
+
display: block;
|
76 |
+
}
|
77 |
+
|
78 |
+
/* Input fields styling */
|
79 |
+
.gradio-textbox input[type="text"], .gradio-slider input[type="number"] {
|
80 |
+
border: 1px solid #ced4da;
|
81 |
+
border-radius: 5px;
|
82 |
+
padding: 10px 12px;
|
83 |
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
84 |
+
}
|
85 |
+
.gradio-textbox input[type="text"]:focus, .gradio-slider input[type="number"]:focus {
|
86 |
+
border-color: #007bff;
|
87 |
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
88 |
+
outline: none;
|
89 |
+
}
|
90 |
+
|
91 |
+
/* Button Styling */
|
92 |
+
#generate-button { /* Use ID selector for more specificity */
|
93 |
+
background: linear-gradient(to right, #28a745, #218838); /* Green gradient */
|
94 |
+
color: white !important; /* Ensure text is white */
|
95 |
+
font-weight: bold;
|
96 |
+
border-radius: 25px !important; /* More rounded */
|
97 |
+
padding: 12px 25px !important;
|
98 |
+
border: none !important;
|
99 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
|
100 |
+
transition: background 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
|
101 |
+
cursor: pointer;
|
102 |
+
font-size: 1.1em !important;
|
103 |
+
display: block !important; /* Center button */
|
104 |
+
margin: 15px auto 0 auto !important; /* Center with margin */
|
105 |
+
width: fit-content !important; /* Fit content width */
|
106 |
+
}
|
107 |
+
#generate-button:hover {
|
108 |
+
background: linear-gradient(to right, #218838, #1e7e34);
|
109 |
+
transform: translateY(-1px);
|
110 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
111 |
+
}
|
112 |
+
|
113 |
+
/* Status/Logs Accordion */
|
114 |
+
.gradio-accordion {
|
115 |
+
border: 1px solid #e0e0e0;
|
116 |
+
border-radius: 8px;
|
117 |
+
margin: 20px;
|
118 |
+
overflow: hidden;
|
119 |
+
box-shadow: 0 1px 3px rgba(0,0,0, 0.05);
|
120 |
+
}
|
121 |
+
.gradio-accordion > .gr-button { /* Targeting the accordion header button more specifically */
|
122 |
+
background-color: #f1f3f5 !important;
|
123 |
+
border-bottom: 1px solid #e0e0e0 !important;
|
124 |
+
font-weight: 600 !important;
|
125 |
+
color: #495057 !important;
|
126 |
+
padding: 12px 20px !important;
|
127 |
+
}
|
128 |
+
.gradio-accordion > div { /* Targeting the accordion content */
|
129 |
+
padding: 15px 20px;
|
130 |
+
}
|
131 |
+
|
132 |
+
/* === Results Section === */
|
133 |
+
.results-section {
|
134 |
+
padding: 10px 30px 30px 30px; /* Less top padding, more bottom */
|
135 |
+
}
|
136 |
+
.results-section h2 {
|
137 |
+
text-align: center;
|
138 |
+
color: #0056b3;
|
139 |
+
margin-bottom: 25px;
|
140 |
+
font-size: 1.8em;
|
141 |
+
}
|
142 |
+
|
143 |
+
/* Tab Styling */
|
144 |
+
.gradio-tabs > .tab-nav button { /* More specific selector for tab buttons */
|
145 |
+
background-color: #f8f9fa;
|
146 |
+
border: 1px solid #dee2e6;
|
147 |
+
border-bottom: none;
|
148 |
+
border-radius: 8px 8px 0 0;
|
149 |
+
padding: 12px 20px;
|
150 |
+
font-weight: 600;
|
151 |
+
color: #495057;
|
152 |
+
transition: background-color 0.2s ease, color 0.2s ease;
|
153 |
+
}
|
154 |
+
.gradio-tabs > .tab-nav button.selected {
|
155 |
+
background-color: #ffffff;
|
156 |
+
border-color: #dee2e6;
|
157 |
+
color: #007bff;
|
158 |
+
border-bottom: 1px solid #ffffff; /* Hide bottom border of selected tab */
|
159 |
+
position: relative;
|
160 |
+
top: 1px; /* Align with content border */
|
161 |
+
}
|
162 |
+
.tabitem { /* Style the content area of the tab */
|
163 |
+
border: 1px solid #dee2e6;
|
164 |
+
border-radius: 0 0 8px 8px;
|
165 |
+
padding: 20px;
|
166 |
+
background-color: #ffffff;
|
167 |
+
margin-top: -1px; /* Overlap with tab navigation border */
|
168 |
+
}
|
169 |
+
|
170 |
+
/* Specific Component Styling within Tabs */
|
171 |
+
|
172 |
+
/* JSON Output Code Block */
|
173 |
+
.json-output-code .cm-editor { /* Target CodeMirror instance used by gr.Code */
|
174 |
+
max-height: 400px; /* Limit height */
|
175 |
+
border: 1px solid #ced4da;
|
176 |
+
border-radius: 5px;
|
177 |
+
}
|
178 |
+
/* Force horizontal scroll on the container Gradio puts around the CodeMirror editor */
|
179 |
+
.json-output-code > div:first-of-type {
|
180 |
+
overflow-x: auto !important;
|
181 |
+
}
|
182 |
+
.json-output-code pre { /* Ensure preformatted text scrolls */
|
183 |
+
/* white-space: pre; Remove this as CodeMirror handles it */
|
184 |
+
/* overflow-x: auto !important; Move overflow to container */
|
185 |
+
word-wrap: normal; /* Prevent wrapping */
|
186 |
+
background-color: #f8f9fa; /* Slight background for code */
|
187 |
+
padding: 10px;
|
188 |
+
}
|
189 |
+
|
190 |
+
|
191 |
+
/* DataFrame Styling */
|
192 |
+
.gradio-dataframe table {
|
193 |
+
width: 100%;
|
194 |
+
border-collapse: collapse;
|
195 |
+
box-shadow: 0 1px 3px rgba(0,0,0, 0.1);
|
196 |
+
border-radius: 5px;
|
197 |
+
overflow: hidden; /* Clip shadows */
|
198 |
+
}
|
199 |
+
.gradio-dataframe th, .gradio-dataframe td {
|
200 |
+
padding: 12px 15px;
|
201 |
+
text-align: left;
|
202 |
+
border-bottom: 1px solid #e0e0e0;
|
203 |
+
vertical-align: top; /* Align content to top */
|
204 |
+
}
|
205 |
+
.gradio-dataframe th {
|
206 |
+
background-color: #e9ecef; /* Header background */
|
207 |
+
font-weight: 600;
|
208 |
+
color: #495057;
|
209 |
+
}
|
210 |
+
.gradio-dataframe tr:last-child td {
|
211 |
+
border-bottom: none;
|
212 |
+
}
|
213 |
+
.gradio-dataframe tr:hover {
|
214 |
+
background-color: #f1f3f5; /* Row hover effect */
|
215 |
+
}
|
216 |
+
|
217 |
+
/* DataFrame: Test Cases - Steps Column Formatting */
|
218 |
+
/* Assumes 'Steps to Execute' is the 4th column (index, ID, Scenario, Steps) */
|
219 |
+
.test-cases-df td:nth-child(4) { /* Check index if needed */
|
220 |
+
white-space: pre-wrap; /* Wrap text and respect newlines */
|
221 |
+
word-break: break-word; /* Break long words if needed */
|
222 |
+
min-width: 300px; /* Ensure minimum width */
|
223 |
+
max-width: 500px; /* Add max width */
|
224 |
+
}
|
225 |
+
|
226 |
+
/* DataFrame: Scripts - Code Column Formatting */
|
227 |
+
/* Assumes 'Python Selenium Code' is the 3rd column (index, ID, Code) */
|
228 |
+
.scripts-df td:nth-child(3) { /* Check index if needed */
|
229 |
+
white-space: pre; /* Preserve whitespace (indentation, newlines) */
|
230 |
+
overflow-x: auto; /* Allow horizontal scrolling for long lines */
|
231 |
+
max-width: 600px; /* Limit max width before scrolling */
|
232 |
+
font-family: 'Courier New', Courier, monospace; /* Monospace font for code */
|
233 |
+
background-color: #fdfdfe; /* Very light background for code cell */
|
234 |
+
font-size: 0.9em;
|
235 |
+
display: block; /* Treat cell content as a block for scrolling */
|
236 |
+
}
|
237 |
+
|
238 |
+
/* Download Buttons */
|
239 |
+
.gradio-file button {
|
240 |
+
background-color: #6c757d; /* Grey */
|
241 |
+
color: white;
|
242 |
+
border: none;
|
243 |
+
border-radius: 5px;
|
244 |
+
padding: 8px 15px;
|
245 |
+
font-size: 0.9em;
|
246 |
+
transition: background-color 0.2s ease;
|
247 |
+
margin-top: 10px; /* Add some space above download buttons */
|
248 |
+
}
|
249 |
+
.gradio-file button:hover {
|
250 |
+
background-color: #5a6268;
|
251 |
+
}
|
252 |
+
|
253 |
+
/* === Footer Simulation === */
|
254 |
+
.app-footer {
|
255 |
+
text-align: center;
|
256 |
+
padding: 15px;
|
257 |
+
margin-top: 30px;
|
258 |
+
font-size: 0.9em;
|
259 |
+
color: #6c757d;
|
260 |
+
border-top: 1px solid #dee2e6;
|
261 |
+
}
|
262 |
+
"""
|
263 |
+
|
264 |
+
# Corrected process_website function
|
265 |
+
def process_website(url: str, num_test_cases: int):
|
266 |
+
"""
|
267 |
+
Main processing function - Corrected for dynamic updates using yield.
|
268 |
+
Orchestrates scraping, test case generation, and script generation.
|
269 |
+
"""
|
270 |
+
# --- Initial State ---
|
271 |
+
elements_json_str = "{}" # Start with empty JSON string representation
|
272 |
+
elements_filepath = None
|
273 |
+
test_cases_df = pd.DataFrame(columns=['Test Case ID', 'Test Scenario', 'Steps to Execute', 'Expected Result'])
|
274 |
+
test_cases_filepath = None
|
275 |
+
scripts_df = pd.DataFrame(columns=['Test Case ID', 'Python Selenium Code'])
|
276 |
+
scripts_filepath = None
|
277 |
+
status_updates = []
|
278 |
+
|
279 |
+
def current_outputs():
|
280 |
+
# Helper to package the current state for yielding
|
281 |
+
return (
|
282 |
+
elements_json_str,
|
283 |
+
test_cases_df,
|
284 |
+
scripts_df,
|
285 |
+
elements_filepath,
|
286 |
+
test_cases_filepath,
|
287 |
+
scripts_filepath,
|
288 |
+
"\n".join(status_updates)
|
289 |
+
)
|
290 |
+
|
291 |
+
# --- Input Validation ---
|
292 |
+
if not url or not url.startswith(('http://', 'https://')):
|
293 |
+
status_updates.append("❌ Error: Please enter a valid URL starting with http:// or https://")
|
294 |
+
yield current_outputs()
|
295 |
+
return
|
296 |
+
|
297 |
+
try:
|
298 |
+
status_updates.append("▶️ Processing started...")
|
299 |
+
yield current_outputs()
|
300 |
+
|
301 |
+
# --- Task 1: Web Scraping ---
|
302 |
+
status_updates.append(f"\n🔄 [1/3] Scraping UI elements from {url}...")
|
303 |
+
yield current_outputs() # Update status BEFORE the long call
|
304 |
+
|
305 |
+
elements_data = extract_elements(url)
|
306 |
+
|
307 |
+
if not elements_data:
|
308 |
+
status_updates.append("❌ Error: Failed to extract elements. Check URL or website structure.")
|
309 |
+
yield current_outputs() # Show error
|
310 |
+
return
|
311 |
+
|
312 |
+
elements_json_str = json.dumps(elements_data, indent=4)
|
313 |
+
temp_elements_filename = os.path.join(OUTPUT_DIR, f"elements_{os.path.basename(url).split('.')[0]}.json")
|
314 |
+
elements_filepath = save_elements_to_json(elements_data, filename=os.path.basename(temp_elements_filename))
|
315 |
+
status_updates.append(f" ✅ Extracted {len(elements_data)} elements. Saved to {elements_filepath}")
|
316 |
+
yield current_outputs() # Update status AND show elements JSON
|
317 |
+
|
318 |
+
# Limit element data size sent to AI
|
319 |
+
max_elements_for_ai = 100
|
320 |
+
if len(elements_data) > max_elements_for_ai:
|
321 |
+
elements_json_str_for_ai = json.dumps(elements_data[:max_elements_for_ai], indent=2)
|
322 |
+
status_updates.append(f" ℹ️ Note: Using first {max_elements_for_ai} elements for AI analysis due to size.")
|
323 |
+
yield current_outputs() # Show the note immediately
|
324 |
+
else:
|
325 |
+
elements_json_str_for_ai = json.dumps(elements_data, indent=2)
|
326 |
+
|
327 |
+
# --- Task 2: Test Case Generation ---
|
328 |
+
status_updates.append(f"\n🧠 [2/3] Generating {num_test_cases} test cases using GenAI...")
|
329 |
+
yield current_outputs() # Update status BEFORE the long call
|
330 |
+
|
331 |
+
generated_tc_df = generate_test_cases(elements_json_str_for_ai, url, num_test_cases)
|
332 |
+
|
333 |
+
# Check for generation errors reflected in the DataFrame
|
334 |
+
generation_failed = generated_tc_df.empty or \
|
335 |
+
generated_tc_df['Test Case ID'].isin(['PARSE_ERROR', 'API_ERROR', 'ERROR']).any()
|
336 |
+
|
337 |
+
if generation_failed:
|
338 |
+
status_updates.append(" ⚠️ Warning: Failed to generate valid test cases or encountered an error. See table for details.")
|
339 |
+
if not generated_tc_df.empty:
|
340 |
+
test_cases_df = generated_tc_df # Display the error DF
|
341 |
+
temp_tc_filename = os.path.join(OUTPUT_DIR, f"test_cases_{os.path.basename(url).split('.')[0]}_error.xlsx")
|
342 |
+
test_cases_filepath = save_test_cases_to_excel(test_cases_df, filename=os.path.basename(temp_tc_filename))
|
343 |
+
else:
|
344 |
+
test_cases_filepath = None # No file if df is completely empty
|
345 |
+
yield current_outputs() # Update status and show error DF
|
346 |
+
|
347 |
+
# Stop if parsing failed completely or DF is empty
|
348 |
+
if test_cases_df.empty or 'PARSE_ERROR' in test_cases_df['Test Case ID'].values:
|
349 |
+
status_updates.append(" 🛑 Stopping process due to critical test case generation failure.")
|
350 |
+
yield current_outputs()
|
351 |
+
return
|
352 |
+
else:
|
353 |
+
test_cases_df = generated_tc_df # Update the main DF with successful results
|
354 |
+
temp_tc_filename = os.path.join(OUTPUT_DIR, f"test_cases_{os.path.basename(url).split('.')[0]}.xlsx")
|
355 |
+
test_cases_filepath = save_test_cases_to_excel(test_cases_df, filename=os.path.basename(temp_tc_filename))
|
356 |
+
status_updates.append(f" ✅ Generated {len(test_cases_df)} test cases. Saved to {test_cases_filepath}")
|
357 |
+
yield current_outputs() # Update status and show test cases DF
|
358 |
+
|
359 |
+
|
360 |
+
# --- Task 3: Selenium Script Generation ---
|
361 |
+
status_updates.append(f"\n🐍 [3/3] Generating Selenium scripts...")
|
362 |
+
yield current_outputs() # Update status BEFORE starting script generation
|
363 |
+
|
364 |
+
scripts_data = []
|
365 |
+
# Filter out potential error rows before iterating
|
366 |
+
valid_test_cases_df = test_cases_df[~test_cases_df['Test Case ID'].isin(['API_ERROR', 'ERROR', 'PARSE_ERROR'])]
|
367 |
+
|
368 |
+
if valid_test_cases_df.empty and not generation_failed:
|
369 |
+
status_updates.append(" ℹ️ No valid test cases found to generate scripts for (check previous warnings).")
|
370 |
+
yield current_outputs()
|
371 |
+
elif not valid_test_cases_df.empty:
|
372 |
+
status_updates.append(f" Mapping {len(valid_test_cases_df)} test cases to scripts...")
|
373 |
+
yield current_outputs() # Show count before starting loop
|
374 |
+
|
375 |
+
for index, test_case in valid_test_cases_df.iterrows():
|
376 |
+
tc_id = test_case.get('Test Case ID', f'Row_{index}')
|
377 |
+
status_updates.append(f" ⏳ Generating script for: {tc_id}...")
|
378 |
+
# Yield BEFORE the AI call for this script to show "Generating..." status
|
379 |
+
yield current_outputs()
|
380 |
+
|
381 |
+
script_code = generate_selenium_script(test_case.to_dict(), elements_json_str_for_ai, url)
|
382 |
+
scripts_data.append({
|
383 |
+
'Test Case ID': tc_id,
|
384 |
+
'Python Selenium Code': script_code
|
385 |
+
})
|
386 |
+
# Note: We collect all scripts first, then update the DataFrame *once* after the loop
|
387 |
+
# This prevents the DataFrame UI from flickering heavily during the loop.
|
388 |
+
# The status log will show progress for each script.
|
389 |
+
|
390 |
+
# After the loop, create and yield the final scripts DataFrame
|
391 |
+
scripts_df = pd.DataFrame(scripts_data)
|
392 |
+
temp_scripts_filename = os.path.join(OUTPUT_DIR, f"test_scripts_{os.path.basename(url).split('.')[0]}.xlsx")
|
393 |
+
scripts_filepath = save_scripts_to_excel(scripts_data, filename=os.path.basename(temp_scripts_filename))
|
394 |
+
status_updates.append(f" ✅ Generated {len(scripts_data)} scripts. Saved to {scripts_filepath}")
|
395 |
+
yield current_outputs() # Update status AND show the scripts DF
|
396 |
+
|
397 |
+
else: # Case where TC generation had errors but didn't stop the process
|
398 |
+
status_updates.append(" ℹ️ Skipping script generation due to previous errors in test case generation.")
|
399 |
+
yield current_outputs()
|
400 |
+
|
401 |
+
|
402 |
+
status_updates.append("\n\n🎉 Processing finished successfully!")
|
403 |
+
yield current_outputs() # Final successful state
|
404 |
+
|
405 |
+
except Exception as e:
|
406 |
+
logging.error(f"Critical error in process_website for {url}: {e}", exc_info=True)
|
407 |
+
status_updates.append(f"\n\n❌ CRITICAL ERROR: An unexpected error occurred: {str(e)}")
|
408 |
+
yield current_outputs() # Yield final state with error message
|
409 |
+
|
410 |
+
|
411 |
+
# --- Gradio Interface Definition (Keep the layout from the previous version) ---
|
412 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), css=custom_css) as demo:
|
413 |
+
|
414 |
+
# --- Header ---
|
415 |
+
with gr.Row():
|
416 |
+
gr.HTML("""
|
417 |
+
<div class="app-header">
|
418 |
+
<h1>🤖 AI-Driven Test Generation Prototype 🧪</h1>
|
419 |
+
<p>Extract UI Elements → Generate Test Cases → Create Selenium Scripts</p>
|
420 |
+
</div>
|
421 |
+
""")
|
422 |
+
|
423 |
+
# --- Input Controls ---
|
424 |
+
with gr.Group(elem_classes="control-section"):
|
425 |
+
with gr.Row():
|
426 |
+
with gr.Column(scale=3):
|
427 |
+
url_input = gr.Textbox(
|
428 |
+
label="Website URL",
|
429 |
+
placeholder="e.g., https://demoblaze.com",
|
430 |
+
info="Enter the full public URL of the website to analyze."
|
431 |
+
)
|
432 |
+
with gr.Column(scale=1):
|
433 |
+
num_cases_input = gr.Slider(
|
434 |
+
minimum=1, maximum=10, value=3, step=1, # Default to 3
|
435 |
+
label="Number of Test Cases",
|
436 |
+
info="How many test cases should the AI generate?"
|
437 |
+
)
|
438 |
+
# Use elem_id for more specific CSS targeting if needed
|
439 |
+
start_button = gr.Button("✨ Analyze Website and Generate Tests ✨", variant="primary", elem_id="generate-button")
|
440 |
+
|
441 |
+
|
442 |
+
# --- Status / Logs ---
|
443 |
+
with gr.Accordion("📊 Status & Logs", open=True): # Open by default
|
444 |
+
status_output = gr.Textbox(
|
445 |
+
label="Processing Log",
|
446 |
+
lines=12,
|
447 |
+
interactive=False,
|
448 |
+
show_copy_button=True
|
449 |
+
)
|
450 |
+
|
451 |
+
# --- Results Section ---
|
452 |
+
with gr.Column(elem_classes="results-section"):
|
453 |
+
gr.Markdown("## Results")
|
454 |
+
with gr.Tabs():
|
455 |
+
with gr.TabItem("📄 Extracted UI Elements"):
|
456 |
+
elements_output = gr.Code(
|
457 |
+
label="elements.json (Preview - scroll horizontally if needed)",
|
458 |
+
language="json",
|
459 |
+
interactive=False,
|
460 |
+
elem_classes="json-output-code" # Apply class for CSS targeting
|
461 |
+
)
|
462 |
+
download_elements = gr.File(label="Download elements.json", scale=0) # Make button smaller
|
463 |
+
|
464 |
+
with gr.TabItem("✅ Generated Test Cases"):
|
465 |
+
test_cases_output = gr.DataFrame(
|
466 |
+
label="Test Cases (Steps column supports multi-line)",
|
467 |
+
interactive=False,
|
468 |
+
wrap=True, # Allow text wrapping generally
|
469 |
+
elem_classes="test-cases-df" # Apply class for CSS targeting
|
470 |
+
)
|
471 |
+
download_test_cases = gr.File(label="Download test_cases.xlsx", scale=0)
|
472 |
+
|
473 |
+
with gr.TabItem("🐍 Generated Selenium Scripts"):
|
474 |
+
scripts_output = gr.DataFrame(
|
475 |
+
label="Selenium Scripts (Code column preserves formatting & scrolls)",
|
476 |
+
interactive=False,
|
477 |
+
wrap=False, # Disable wrapping for code column
|
478 |
+
elem_classes="scripts-df" # Apply class for CSS targeting
|
479 |
+
)
|
480 |
+
download_scripts = gr.File(label="Download test_scripts.xlsx", scale=0)
|
481 |
+
|
482 |
+
# --- Footer ---
|
483 |
+
with gr.Row():
|
484 |
+
gr.HTML("""
|
485 |
+
<div class="app-footer">
|
486 |
+
AI Test Generator Prototype | Using OpenAI Compatible API
|
487 |
+
</div>
|
488 |
+
""")
|
489 |
+
|
490 |
+
|
491 |
+
# --- Event Handling (Ensure outputs match the yielded tuple order) ---
|
492 |
+
start_button.click(
|
493 |
+
fn=process_website,
|
494 |
+
inputs=[url_input, num_cases_input],
|
495 |
+
outputs=[
|
496 |
+
elements_output, # 1st element in yielded tuple
|
497 |
+
test_cases_output, # 2nd
|
498 |
+
scripts_output, # 3rd
|
499 |
+
download_elements, # 4th
|
500 |
+
download_test_cases, # 5th
|
501 |
+
download_scripts, # 6th
|
502 |
+
status_output # 7th
|
503 |
+
]
|
504 |
+
)
|
505 |
+
|
506 |
+
# --- Examples ---
|
507 |
+
gr.Examples(
|
508 |
+
examples=[
|
509 |
+
["https://demoblaze.com", 5],
|
510 |
+
["http://the-internet.herokuapp.com/login", 4],
|
511 |
+
["https://www.wikipedia.org/", 3]
|
512 |
+
],
|
513 |
+
inputs=[url_input, num_cases_input],
|
514 |
+
label="Example Websites",
|
515 |
+
elem_id="examples-container" # Optional ID for styling
|
516 |
+
)
|
517 |
+
|
518 |
+
# --- Launch the Application ---
|
519 |
+
if __name__ == "__main__":
|
520 |
+
print("Starting Gradio app with enhanced UI and corrected dynamic updates...")
|
521 |
+
demo.launch(debug=True) # debug=True helps see errors
|
522 |
+
|
genai_handler.py
ADDED
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ai_test_generator/genai_handler.py
|
2 |
+
import os
|
3 |
+
import json
|
4 |
+
import logging
|
5 |
+
import pandas as pd
|
6 |
+
from openai import OpenAI, APIError, RateLimitError
|
7 |
+
|
8 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
9 |
+
|
10 |
+
# --- Configuration ---
|
11 |
+
# It's better practice to use environment variables for API keys,
|
12 |
+
# but using the provided key directly for this specific case.
|
13 |
+
API_KEY = "ddc-beta-v7bjela50v-lI9ep55oPFJz7N06MjSh2Asj2AVGaubLqIC"
|
14 |
+
BASE_URL = "https://beta.sree.shop/v1"
|
15 |
+
MODEL_NAME = "Provider-5/gpt-4o" # Use the specified model
|
16 |
+
|
17 |
+
try:
|
18 |
+
client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
|
19 |
+
logging.info(f"OpenAI client initialized for model: {MODEL_NAME}")
|
20 |
+
except Exception as e:
|
21 |
+
logging.error(f"Failed to initialize OpenAI client: {e}", exc_info=True)
|
22 |
+
client = None # Ensure client is None if initialization fails
|
23 |
+
|
24 |
+
# --- Helper Function to Parse AI Response for Test Cases ---
|
25 |
+
def parse_ai_test_cases(response_content: str) -> list[dict]:
|
26 |
+
"""Attempts to parse the AI response string into a list of test case dictionaries."""
|
27 |
+
try:
|
28 |
+
# Try parsing directly as JSON if the AI follows instructions perfectly
|
29 |
+
parsed_data = json.loads(response_content)
|
30 |
+
if isinstance(parsed_data, list) and all(isinstance(item, dict) for item in parsed_data):
|
31 |
+
# Basic validation for expected keys (can be made more robust)
|
32 |
+
if parsed_data and all(k in parsed_data[0] for k in ['Test Case ID', 'Test Scenario', 'Steps to Execute', 'Expected Result']):
|
33 |
+
logging.info("Successfully parsed AI response as JSON list of test cases.")
|
34 |
+
return parsed_data
|
35 |
+
except json.JSONDecodeError:
|
36 |
+
logging.warning("AI response is not a direct JSON list. Trying to extract from markdown or other formats.")
|
37 |
+
# Add more sophisticated parsing here if needed (e.g., regex for markdown tables)
|
38 |
+
# For now, return empty if direct JSON parsing fails
|
39 |
+
pass # Fall through to return empty list
|
40 |
+
|
41 |
+
logging.error("Failed to parse AI response into the expected test case format.")
|
42 |
+
return []
|
43 |
+
|
44 |
+
|
45 |
+
# --- Test Case Generation ---
|
46 |
+
def generate_test_cases(elements_json_str: str, url: str, num_cases: int = 5) -> pd.DataFrame:
|
47 |
+
"""
|
48 |
+
Generates test cases using GenAI based on extracted UI elements.
|
49 |
+
|
50 |
+
Args:
|
51 |
+
elements_json_str: A JSON string representing the extracted UI elements.
|
52 |
+
url: The URL of the website being tested (for context).
|
53 |
+
num_cases: The desired number of test cases (default 3-5, AI might vary).
|
54 |
+
|
55 |
+
Returns:
|
56 |
+
A pandas DataFrame containing the generated test cases, or an empty DataFrame on failure.
|
57 |
+
"""
|
58 |
+
if not client:
|
59 |
+
logging.error("GenAI client is not available.")
|
60 |
+
return pd.DataFrame()
|
61 |
+
|
62 |
+
prompt = f"""
|
63 |
+
Analyze the following UI elements extracted from the website {url}:
|
64 |
+
```json
|
65 |
+
{elements_json_str}
|
66 |
+
```
|
67 |
+
|
68 |
+
Based on these elements, generate {num_cases} meaningful test cases covering common user interactions like navigation, form interaction (if any), viewing content, etc. Focus on positive and potentially simple negative scenarios relevant to the visible elements.
|
69 |
+
|
70 |
+
Present the test cases ONLY as a valid JSON list of objects. Each object must have the following keys:
|
71 |
+
- "Test Case ID": A unique identifier (e.g., TC001, TC002).
|
72 |
+
- "Test Scenario": A brief description of the test objective.
|
73 |
+
- "Steps to Execute": Numbered steps describing how to perform the test manually. Mention specific element identifiers (text, id, placeholder) where possible from the JSON above.
|
74 |
+
- "Expected Result": What should happen after executing the steps.
|
75 |
+
|
76 |
+
Example format for a single test case object:
|
77 |
+
{{
|
78 |
+
"Test Case ID": "TC001",
|
79 |
+
"Test Scenario": "Verify user can navigate to the 'Contact' page",
|
80 |
+
"Steps to Execute": "1. Go to the homepage.\n2. Click on the link with text 'Contact'.",
|
81 |
+
"Expected Result": "The contact page should load successfully, displaying contact information or a contact form."
|
82 |
+
}}
|
83 |
+
|
84 |
+
Ensure the entire output is *only* the JSON list, starting with '[' and ending with ']'. Do not include any introductory text, explanations, or markdown formatting outside the JSON structure itself.
|
85 |
+
"""
|
86 |
+
|
87 |
+
logging.info(f"Generating {num_cases} test cases for {url}...")
|
88 |
+
try:
|
89 |
+
response = client.chat.completions.create(
|
90 |
+
model=MODEL_NAME,
|
91 |
+
messages=[
|
92 |
+
{"role": "system", "content": "You are an expert QA engineer generating test cases from UI elements."},
|
93 |
+
{"role": "user", "content": prompt}
|
94 |
+
],
|
95 |
+
temperature=0.5, # Lower temperature for more predictable structure
|
96 |
+
max_tokens=1500 # Adjust as needed
|
97 |
+
)
|
98 |
+
|
99 |
+
response_content = response.choices[0].message.content.strip()
|
100 |
+
logging.info("Raw AI Response for Test Cases:\n" + response_content) # Log the raw response
|
101 |
+
|
102 |
+
# Parse the response
|
103 |
+
test_cases_list = parse_ai_test_cases(response_content)
|
104 |
+
|
105 |
+
if not test_cases_list:
|
106 |
+
logging.error("Failed to parse test cases from AI response.")
|
107 |
+
# Attempt to return raw response in a placeholder format if parsing fails
|
108 |
+
return pd.DataFrame([{'Test Case ID': 'PARSE_ERROR', 'Test Scenario': 'Failed to parse AI response', 'Steps to Execute': response_content, 'Expected Result': 'N/A'}])
|
109 |
+
|
110 |
+
|
111 |
+
df = pd.DataFrame(test_cases_list)
|
112 |
+
# Ensure standard columns exist, even if AI missed some
|
113 |
+
for col in ['Test Case ID', 'Test Scenario', 'Steps to Execute', 'Expected Result']:
|
114 |
+
if col not in df.columns:
|
115 |
+
df[col] = 'N/A' # Add missing columns with default value
|
116 |
+
df = df[['Test Case ID', 'Test Scenario', 'Steps to Execute', 'Expected Result']] # Enforce order
|
117 |
+
|
118 |
+
logging.info(f"Successfully generated and parsed {len(df)} test cases.")
|
119 |
+
return df
|
120 |
+
|
121 |
+
except RateLimitError as e:
|
122 |
+
logging.error(f"API Rate Limit Error: {e}")
|
123 |
+
return pd.DataFrame([{'Test Case ID': 'API_ERROR', 'Test Scenario': 'Rate Limit Reached', 'Steps to Execute': str(e), 'Expected Result': 'N/A'}])
|
124 |
+
except APIError as e:
|
125 |
+
logging.error(f"API Error during test case generation: {e}")
|
126 |
+
return pd.DataFrame([{'Test Case ID': 'API_ERROR', 'Test Scenario': 'API Communication Issue', 'Steps to Execute': str(e), 'Expected Result': 'N/A'}])
|
127 |
+
except Exception as e:
|
128 |
+
logging.error(f"Unexpected error during test case generation: {e}", exc_info=True)
|
129 |
+
return pd.DataFrame([{'Test Case ID': 'ERROR', 'Test Scenario': 'Unexpected Error', 'Steps to Execute': str(e), 'Expected Result': 'N/A'}])
|
130 |
+
|
131 |
+
|
132 |
+
# --- Selenium Script Generation ---
|
133 |
+
def generate_selenium_script(test_case: dict, elements_json_str: str, url: str) -> str:
|
134 |
+
"""
|
135 |
+
Generates a Python Selenium script for a single test case using GenAI.
|
136 |
+
|
137 |
+
Args:
|
138 |
+
test_case: A dictionary representing a single test case (needs 'Test Case ID', 'Steps to Execute').
|
139 |
+
elements_json_str: A JSON string of extracted UI elements for context.
|
140 |
+
url: The target website URL.
|
141 |
+
|
142 |
+
Returns:
|
143 |
+
A string containing the generated Python Selenium script, or an error message string.
|
144 |
+
"""
|
145 |
+
if not client:
|
146 |
+
logging.error("GenAI client is not available.")
|
147 |
+
return "Error: GenAI client not initialized."
|
148 |
+
|
149 |
+
test_case_id = test_case.get('Test Case ID', 'Unknown TC')
|
150 |
+
steps = test_case.get('Steps to Execute', 'No steps provided.')
|
151 |
+
scenario = test_case.get('Test Scenario', 'No scenario provided.')
|
152 |
+
|
153 |
+
prompt = f"""
|
154 |
+
Generate a complete, runnable Python Selenium script to automate the following test case for the website {url}.
|
155 |
+
|
156 |
+
Test Case ID: {test_case_id}
|
157 |
+
Test Scenario: {scenario}
|
158 |
+
Steps to Execute:
|
159 |
+
{steps}
|
160 |
+
|
161 |
+
Use the following UI element details extracted from the page for context when choosing selectors. Prefer using ID, then Name, then CSS Selector, then Link Text, then XPath. Handle potential waits for elements to be clickable or visible.
|
162 |
+
```json
|
163 |
+
{elements_json_str}
|
164 |
+
```
|
165 |
+
|
166 |
+
The script should:
|
167 |
+
1. Include necessary imports (Selenium webdriver, By, time, etc.).
|
168 |
+
2. Set up the ChromeDriver (using webdriver-manager is preferred). Run in headless mode.
|
169 |
+
3. Navigate to the base URL: {url}.
|
170 |
+
4. Implement the test steps described above using Selenium commands (find_element, click, send_keys, etc.). Use robust locators based on the provided element details. Include reasonable waits (e.g., `time.sleep(1)` or explicit waits) after actions like clicks or navigation.
|
171 |
+
5. Include a basic assertion relevant to the 'Expected Result' or the final step (e.g., check page title, check for an element's presence/text). If the expected result is vague, make a reasonable assertion based on the steps.
|
172 |
+
6. Print a success or failure message to the console based on the assertion.
|
173 |
+
7. Include teardown code to close the browser (`driver.quit()`) in a `finally` block.
|
174 |
+
8. Be fully contained within a single Python code block.
|
175 |
+
|
176 |
+
Output *only* the Python code for the script. Do not include any explanations, introductory text, or markdown formatting like ```python ... ```.
|
177 |
+
"""
|
178 |
+
|
179 |
+
logging.info(f"Generating Selenium script for Test Case ID: {test_case_id}...")
|
180 |
+
try:
|
181 |
+
response = client.chat.completions.create(
|
182 |
+
model=MODEL_NAME,
|
183 |
+
messages=[
|
184 |
+
{"role": "system", "content": "You are an expert QA automation engineer generating Python Selenium scripts."},
|
185 |
+
{"role": "user", "content": prompt}
|
186 |
+
],
|
187 |
+
temperature=0.3, # Low temperature for more deterministic code generation
|
188 |
+
max_tokens=2000 # Allow ample space for code
|
189 |
+
)
|
190 |
+
|
191 |
+
script_code = response.choices[0].message.content.strip()
|
192 |
+
|
193 |
+
# Basic cleanup: remove potential markdown fences if AI includes them
|
194 |
+
if script_code.startswith("```python"):
|
195 |
+
script_code = script_code[len("```python"):].strip()
|
196 |
+
if script_code.endswith("```"):
|
197 |
+
script_code = script_code[:-len("```")].strip()
|
198 |
+
|
199 |
+
logging.info(f"Successfully generated script for {test_case_id}.")
|
200 |
+
# Log first few lines of the script for verification
|
201 |
+
logging.debug(f"Generated script (first 100 chars): {script_code[:100]}...")
|
202 |
+
return script_code
|
203 |
+
|
204 |
+
except RateLimitError as e:
|
205 |
+
logging.error(f"API Rate Limit Error during script generation for {test_case_id}: {e}")
|
206 |
+
return f"# Error: API Rate Limit Reached\n# {e}"
|
207 |
+
except APIError as e:
|
208 |
+
logging.error(f"API Error during script generation for {test_case_id}: {e}")
|
209 |
+
return f"# Error: API Communication Issue\n# {e}"
|
210 |
+
except Exception as e:
|
211 |
+
logging.error(f"Unexpected error during script generation for {test_case_id}: {e}", exc_info=True)
|
212 |
+
return f"# Error: Unexpected error during script generation\n# {e}"
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
beautifulsoup4
|
2 |
+
selenium
|
3 |
+
webdriver-manager
|
4 |
+
openai
|
5 |
+
pandas
|
6 |
+
openpyxl
|
7 |
+
gradio
|
8 |
+
lxml
|
9 |
+
requests
|
scraper.py
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ai_test_generator/scraper.py
|
2 |
+
import time
|
3 |
+
import json
|
4 |
+
from selenium import webdriver
|
5 |
+
from selenium.webdriver.chrome.service import Service
|
6 |
+
from selenium.webdriver.chrome.options import Options
|
7 |
+
from webdriver_manager.chrome import ChromeDriverManager
|
8 |
+
from bs4 import BeautifulSoup
|
9 |
+
import logging
|
10 |
+
|
11 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
12 |
+
|
13 |
+
def extract_elements(url: str) -> list[dict]:
|
14 |
+
"""
|
15 |
+
Scrapes a website URL to extract buttons, links, input fields, and forms.
|
16 |
+
|
17 |
+
Args:
|
18 |
+
url: The public URL of the website to scrape.
|
19 |
+
|
20 |
+
Returns:
|
21 |
+
A list of dictionaries, each representing an extracted UI element.
|
22 |
+
Returns an empty list if scraping fails.
|
23 |
+
"""
|
24 |
+
logging.info(f"Starting scraping for URL: {url}")
|
25 |
+
extracted_elements = []
|
26 |
+
|
27 |
+
chrome_options = Options()
|
28 |
+
chrome_options.add_argument("--headless") # Run headless (no GUI)
|
29 |
+
chrome_options.add_argument("--no-sandbox")
|
30 |
+
chrome_options.add_argument("--disable-dev-shm-usage")
|
31 |
+
chrome_options.add_argument("--disable-gpu") # Recommended for headless
|
32 |
+
chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") # Set user agent
|
33 |
+
|
34 |
+
service = Service(ChromeDriverManager().install())
|
35 |
+
driver = None
|
36 |
+
|
37 |
+
try:
|
38 |
+
driver = webdriver.Chrome(service=service, options=chrome_options)
|
39 |
+
driver.set_page_load_timeout(30) # Set timeout for page load
|
40 |
+
driver.get(url)
|
41 |
+
# Allow some time for dynamic content to potentially load
|
42 |
+
# A more robust solution might use WebDriverWait
|
43 |
+
time.sleep(3)
|
44 |
+
|
45 |
+
page_source = driver.page_source
|
46 |
+
soup = BeautifulSoup(page_source, 'lxml') # Use lxml parser
|
47 |
+
|
48 |
+
# --- Extract Buttons ---
|
49 |
+
buttons = soup.find_all('button')
|
50 |
+
for btn in buttons:
|
51 |
+
element_data = {
|
52 |
+
'type': 'button',
|
53 |
+
'text': btn.get_text(strip=True),
|
54 |
+
'id': btn.get('id'),
|
55 |
+
'name': btn.get('name'),
|
56 |
+
'class': btn.get('class'),
|
57 |
+
'attributes': {k: v for k, v in btn.attrs.items() if k not in ['id', 'name', 'class']}
|
58 |
+
}
|
59 |
+
extracted_elements.append(element_data)
|
60 |
+
logging.info(f"Found {len(buttons)} buttons.")
|
61 |
+
|
62 |
+
# --- Extract Links ---
|
63 |
+
links = soup.find_all('a')
|
64 |
+
for link in links:
|
65 |
+
element_data = {
|
66 |
+
'type': 'link',
|
67 |
+
'text': link.get_text(strip=True),
|
68 |
+
'href': link.get('href'),
|
69 |
+
'id': link.get('id'),
|
70 |
+
'class': link.get('class'),
|
71 |
+
'attributes': {k: v for k, v in link.attrs.items() if k not in ['id', 'class', 'href']}
|
72 |
+
}
|
73 |
+
extracted_elements.append(element_data)
|
74 |
+
logging.info(f"Found {len(links)} links.")
|
75 |
+
|
76 |
+
# --- Extract Input Fields ---
|
77 |
+
inputs = soup.find_all('input')
|
78 |
+
for inp in inputs:
|
79 |
+
element_data = {
|
80 |
+
'type': 'input',
|
81 |
+
'input_type': inp.get('type', 'text'), # Default to 'text' if type not specified
|
82 |
+
'id': inp.get('id'),
|
83 |
+
'name': inp.get('name'),
|
84 |
+
'placeholder': inp.get('placeholder'),
|
85 |
+
'value': inp.get('value'),
|
86 |
+
'class': inp.get('class'),
|
87 |
+
'attributes': {k: v for k, v in inp.attrs.items() if k not in ['id', 'name', 'class', 'type', 'placeholder', 'value']}
|
88 |
+
}
|
89 |
+
extracted_elements.append(element_data)
|
90 |
+
logging.info(f"Found {len(inputs)} input fields.")
|
91 |
+
|
92 |
+
# --- Extract Forms ---
|
93 |
+
forms = soup.find_all('form')
|
94 |
+
for form in forms:
|
95 |
+
form_elements = []
|
96 |
+
# Find elements within this specific form
|
97 |
+
for child_input in form.find_all('input'):
|
98 |
+
form_elements.append({
|
99 |
+
'tag': 'input',
|
100 |
+
'type': child_input.get('type'),
|
101 |
+
'id': child_input.get('id'),
|
102 |
+
'name': child_input.get('name')
|
103 |
+
})
|
104 |
+
for child_button in form.find_all('button'):
|
105 |
+
form_elements.append({
|
106 |
+
'tag': 'button',
|
107 |
+
'type': child_button.get('type'),
|
108 |
+
'id': child_button.get('id'),
|
109 |
+
'name': child_button.get('name'),
|
110 |
+
'text': child_button.get_text(strip=True)
|
111 |
+
})
|
112 |
+
# Add other form element types if needed (select, textarea)
|
113 |
+
|
114 |
+
element_data = {
|
115 |
+
'type': 'form',
|
116 |
+
'id': form.get('id'),
|
117 |
+
'action': form.get('action'),
|
118 |
+
'method': form.get('method'),
|
119 |
+
'class': form.get('class'),
|
120 |
+
'contained_elements': form_elements,
|
121 |
+
'attributes': {k: v for k, v in form.attrs.items() if k not in ['id', 'class', 'action', 'method']}
|
122 |
+
}
|
123 |
+
extracted_elements.append(element_data)
|
124 |
+
logging.info(f"Found {len(forms)} forms.")
|
125 |
+
|
126 |
+
logging.info(f"Successfully extracted {len(extracted_elements)} elements in total.")
|
127 |
+
|
128 |
+
except Exception as e:
|
129 |
+
logging.error(f"Error during scraping URL {url}: {e}", exc_info=True)
|
130 |
+
# Return empty list on error, Gradio app will handle this
|
131 |
+
return []
|
132 |
+
finally:
|
133 |
+
if driver:
|
134 |
+
driver.quit()
|
135 |
+
logging.info("WebDriver closed.")
|
136 |
+
|
137 |
+
return extracted_elements
|
138 |
+
|
139 |
+
# Example usage (optional, for testing scraper independently)
|
140 |
+
# if __name__ == '__main__':
|
141 |
+
# test_url = "https://demoblaze.com/"
|
142 |
+
# elements = extract_elements(test_url)
|
143 |
+
# if elements:
|
144 |
+
# print(f"Extracted {len(elements)} elements.")
|
145 |
+
# # Save to a temporary file for inspection
|
146 |
+
# with open("temp_elements.json", "w", encoding="utf-8") as f:
|
147 |
+
# json.dump(elements, f, indent=4)
|
148 |
+
# print("Saved results to temp_elements.json")
|
149 |
+
# else:
|
150 |
+
# print("Scraping failed.")
|
utils.py
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ai_test_generator/utils.py
|
2 |
+
import json
|
3 |
+
import pandas as pd
|
4 |
+
import logging
|
5 |
+
import os
|
6 |
+
|
7 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
8 |
+
OUTPUT_DIR = "outputs" # Define output directory
|
9 |
+
|
10 |
+
def save_elements_to_json(elements: list[dict], filename: str = "elements.json") -> str:
|
11 |
+
"""Saves extracted elements to a JSON file."""
|
12 |
+
if not os.path.exists(OUTPUT_DIR):
|
13 |
+
os.makedirs(OUTPUT_DIR)
|
14 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
15 |
+
try:
|
16 |
+
with open(filepath, "w", encoding="utf-8") as f:
|
17 |
+
json.dump(elements, f, indent=4)
|
18 |
+
logging.info(f"Successfully saved elements to {filepath}")
|
19 |
+
return filepath
|
20 |
+
except Exception as e:
|
21 |
+
logging.error(f"Error saving elements to JSON file {filepath}: {e}", exc_info=True)
|
22 |
+
return ""
|
23 |
+
|
24 |
+
def save_test_cases_to_excel(test_cases_df: pd.DataFrame, filename: str = "test_cases.xlsx") -> str:
|
25 |
+
"""Saves test cases DataFrame to an Excel file."""
|
26 |
+
if not os.path.exists(OUTPUT_DIR):
|
27 |
+
os.makedirs(OUTPUT_DIR)
|
28 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
29 |
+
try:
|
30 |
+
test_cases_df.to_excel(filepath, index=False, engine='openpyxl')
|
31 |
+
logging.info(f"Successfully saved test cases to {filepath}")
|
32 |
+
return filepath
|
33 |
+
except Exception as e:
|
34 |
+
logging.error(f"Error saving test cases to Excel file {filepath}: {e}", exc_info=True)
|
35 |
+
return ""
|
36 |
+
|
37 |
+
def save_scripts_to_excel(scripts_data: list[dict], filename: str = "test_scripts.xlsx") -> str:
|
38 |
+
"""Saves generated scripts along with Test Case IDs to an Excel file."""
|
39 |
+
if not os.path.exists(OUTPUT_DIR):
|
40 |
+
os.makedirs(OUTPUT_DIR)
|
41 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
42 |
+
try:
|
43 |
+
df = pd.DataFrame(scripts_data)
|
44 |
+
# Ensure consistent column order
|
45 |
+
if not df.empty:
|
46 |
+
df = df[['Test Case ID', 'Python Selenium Code']]
|
47 |
+
else:
|
48 |
+
# Create empty DataFrame with columns if no scripts were generated
|
49 |
+
df = pd.DataFrame(columns=['Test Case ID', 'Python Selenium Code'])
|
50 |
+
|
51 |
+
df.to_excel(filepath, index=False, engine='openpyxl')
|
52 |
+
logging.info(f"Successfully saved scripts to {filepath}")
|
53 |
+
return filepath
|
54 |
+
except Exception as e:
|
55 |
+
logging.error(f"Error saving scripts to Excel file {filepath}: {e}", exc_info=True)
|
56 |
+
return ""
|