File size: 21,061 Bytes
0b1188e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 |
# ai_test_generator/app.py
import gradio as gr
import pandas as pd
import json
import logging
import os
import html # Keep for potential future use, though not strictly needed now
from scraper import extract_elements
from genai_handler import generate_test_cases, generate_selenium_script
from utils import save_elements_to_json, save_test_cases_to_excel, save_scripts_to_excel
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Ensure output directory exists
OUTPUT_DIR = "outputs"
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
# --- Custom CSS (Keep the enhanced CSS from the previous version) ---
custom_css = """
/* === Body and General Styles === */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(to bottom right, #e0f2f7, #ffffff); /* Light blue gradient */
color: #333;
margin: 0;
padding: 0;
}
/* Gradio container adjustments */
.gradio-container {
max-width: 1200px; /* Increase max width */
margin: 0 auto !important; /* Center the container */
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
background-color: #ffffff; /* White background for content area */
overflow: hidden; /* Ensure shadows are contained */
}
/* === Header Simulation === */
.app-header {
background: linear-gradient(to right, #007bff, #0056b3); /* Blue gradient header */
color: white;
padding: 20px 30px;
text-align: center;
border-bottom: 3px solid #004085;
}
.app-header h1 {
margin: 0;
font-size: 2.2em;
font-weight: 600;
letter-spacing: 1px;
}
.app-header p {
margin-top: 8px;
font-size: 1.1em;
opacity: 0.9;
}
/* === Content Area Styling === */
.control-section {
padding: 25px 30px;
background-color: #f8f9fa; /* Light grey for control section */
border-bottom: 1px solid #dee2e6;
border-radius: 8px;
margin: 20px;
box-shadow: 0 2px 5px rgba(0,0,0, 0.05);
}
.control-section label {
font-weight: 600;
color: #0056b3; /* Darker blue for labels */
margin-bottom: 8px !important;
display: block;
}
/* Input fields styling */
.gradio-textbox input[type="text"], .gradio-slider input[type="number"] {
border: 1px solid #ced4da;
border-radius: 5px;
padding: 10px 12px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.gradio-textbox input[type="text"]:focus, .gradio-slider input[type="number"]:focus {
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
outline: none;
}
/* Button Styling */
#generate-button { /* Use ID selector for more specificity */
background: linear-gradient(to right, #28a745, #218838); /* Green gradient */
color: white !important; /* Ensure text is white */
font-weight: bold;
border-radius: 25px !important; /* More rounded */
padding: 12px 25px !important;
border: none !important;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
transition: background 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
cursor: pointer;
font-size: 1.1em !important;
display: block !important; /* Center button */
margin: 15px auto 0 auto !important; /* Center with margin */
width: fit-content !important; /* Fit content width */
}
#generate-button:hover {
background: linear-gradient(to right, #218838, #1e7e34);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* Status/Logs Accordion */
.gradio-accordion {
border: 1px solid #e0e0e0;
border-radius: 8px;
margin: 20px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0, 0.05);
}
.gradio-accordion > .gr-button { /* Targeting the accordion header button more specifically */
background-color: #f1f3f5 !important;
border-bottom: 1px solid #e0e0e0 !important;
font-weight: 600 !important;
color: #495057 !important;
padding: 12px 20px !important;
}
.gradio-accordion > div { /* Targeting the accordion content */
padding: 15px 20px;
}
/* === Results Section === */
.results-section {
padding: 10px 30px 30px 30px; /* Less top padding, more bottom */
}
.results-section h2 {
text-align: center;
color: #0056b3;
margin-bottom: 25px;
font-size: 1.8em;
}
/* Tab Styling */
.gradio-tabs > .tab-nav button { /* More specific selector for tab buttons */
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-bottom: none;
border-radius: 8px 8px 0 0;
padding: 12px 20px;
font-weight: 600;
color: #495057;
transition: background-color 0.2s ease, color 0.2s ease;
}
.gradio-tabs > .tab-nav button.selected {
background-color: #ffffff;
border-color: #dee2e6;
color: #007bff;
border-bottom: 1px solid #ffffff; /* Hide bottom border of selected tab */
position: relative;
top: 1px; /* Align with content border */
}
.tabitem { /* Style the content area of the tab */
border: 1px solid #dee2e6;
border-radius: 0 0 8px 8px;
padding: 20px;
background-color: #ffffff;
margin-top: -1px; /* Overlap with tab navigation border */
}
/* Specific Component Styling within Tabs */
/* JSON Output Code Block */
.json-output-code .cm-editor { /* Target CodeMirror instance used by gr.Code */
max-height: 400px; /* Limit height */
border: 1px solid #ced4da;
border-radius: 5px;
}
/* Force horizontal scroll on the container Gradio puts around the CodeMirror editor */
.json-output-code > div:first-of-type {
overflow-x: auto !important;
}
.json-output-code pre { /* Ensure preformatted text scrolls */
/* white-space: pre; Remove this as CodeMirror handles it */
/* overflow-x: auto !important; Move overflow to container */
word-wrap: normal; /* Prevent wrapping */
background-color: #f8f9fa; /* Slight background for code */
padding: 10px;
}
/* DataFrame Styling */
.gradio-dataframe table {
width: 100%;
border-collapse: collapse;
box-shadow: 0 1px 3px rgba(0,0,0, 0.1);
border-radius: 5px;
overflow: hidden; /* Clip shadows */
}
.gradio-dataframe th, .gradio-dataframe td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
vertical-align: top; /* Align content to top */
}
.gradio-dataframe th {
background-color: #e9ecef; /* Header background */
font-weight: 600;
color: #495057;
}
.gradio-dataframe tr:last-child td {
border-bottom: none;
}
.gradio-dataframe tr:hover {
background-color: #f1f3f5; /* Row hover effect */
}
/* DataFrame: Test Cases - Steps Column Formatting */
/* Assumes 'Steps to Execute' is the 4th column (index, ID, Scenario, Steps) */
.test-cases-df td:nth-child(4) { /* Check index if needed */
white-space: pre-wrap; /* Wrap text and respect newlines */
word-break: break-word; /* Break long words if needed */
min-width: 300px; /* Ensure minimum width */
max-width: 500px; /* Add max width */
}
/* DataFrame: Scripts - Code Column Formatting */
/* Assumes 'Python Selenium Code' is the 3rd column (index, ID, Code) */
.scripts-df td:nth-child(3) { /* Check index if needed */
white-space: pre; /* Preserve whitespace (indentation, newlines) */
overflow-x: auto; /* Allow horizontal scrolling for long lines */
max-width: 600px; /* Limit max width before scrolling */
font-family: 'Courier New', Courier, monospace; /* Monospace font for code */
background-color: #fdfdfe; /* Very light background for code cell */
font-size: 0.9em;
display: block; /* Treat cell content as a block for scrolling */
}
/* Download Buttons */
.gradio-file button {
background-color: #6c757d; /* Grey */
color: white;
border: none;
border-radius: 5px;
padding: 8px 15px;
font-size: 0.9em;
transition: background-color 0.2s ease;
margin-top: 10px; /* Add some space above download buttons */
}
.gradio-file button:hover {
background-color: #5a6268;
}
/* === Footer Simulation === */
.app-footer {
text-align: center;
padding: 15px;
margin-top: 30px;
font-size: 0.9em;
color: #6c757d;
border-top: 1px solid #dee2e6;
}
"""
# Corrected process_website function
def process_website(url: str, num_test_cases: int):
"""
Main processing function - Corrected for dynamic updates using yield.
Orchestrates scraping, test case generation, and script generation.
"""
# --- Initial State ---
elements_json_str = "{}" # Start with empty JSON string representation
elements_filepath = None
test_cases_df = pd.DataFrame(columns=['Test Case ID', 'Test Scenario', 'Steps to Execute', 'Expected Result'])
test_cases_filepath = None
scripts_df = pd.DataFrame(columns=['Test Case ID', 'Python Selenium Code'])
scripts_filepath = None
status_updates = []
def current_outputs():
# Helper to package the current state for yielding
return (
elements_json_str,
test_cases_df,
scripts_df,
elements_filepath,
test_cases_filepath,
scripts_filepath,
"\n".join(status_updates)
)
# --- Input Validation ---
if not url or not url.startswith(('http://', 'https://')):
status_updates.append("❌ Error: Please enter a valid URL starting with http:// or https://")
yield current_outputs()
return
try:
status_updates.append("▶️ Processing started...")
yield current_outputs()
# --- Task 1: Web Scraping ---
status_updates.append(f"\n🔄 [1/3] Scraping UI elements from {url}...")
yield current_outputs() # Update status BEFORE the long call
elements_data = extract_elements(url)
if not elements_data:
status_updates.append("❌ Error: Failed to extract elements. Check URL or website structure.")
yield current_outputs() # Show error
return
elements_json_str = json.dumps(elements_data, indent=4)
temp_elements_filename = os.path.join(OUTPUT_DIR, f"elements_{os.path.basename(url).split('.')[0]}.json")
elements_filepath = save_elements_to_json(elements_data, filename=os.path.basename(temp_elements_filename))
status_updates.append(f" ✅ Extracted {len(elements_data)} elements. Saved to {elements_filepath}")
yield current_outputs() # Update status AND show elements JSON
# Limit element data size sent to AI
max_elements_for_ai = 100
if len(elements_data) > max_elements_for_ai:
elements_json_str_for_ai = json.dumps(elements_data[:max_elements_for_ai], indent=2)
status_updates.append(f" ℹ️ Note: Using first {max_elements_for_ai} elements for AI analysis due to size.")
yield current_outputs() # Show the note immediately
else:
elements_json_str_for_ai = json.dumps(elements_data, indent=2)
# --- Task 2: Test Case Generation ---
status_updates.append(f"\n🧠 [2/3] Generating {num_test_cases} test cases using GenAI...")
yield current_outputs() # Update status BEFORE the long call
generated_tc_df = generate_test_cases(elements_json_str_for_ai, url, num_test_cases)
# Check for generation errors reflected in the DataFrame
generation_failed = generated_tc_df.empty or \
generated_tc_df['Test Case ID'].isin(['PARSE_ERROR', 'API_ERROR', 'ERROR']).any()
if generation_failed:
status_updates.append(" ⚠️ Warning: Failed to generate valid test cases or encountered an error. See table for details.")
if not generated_tc_df.empty:
test_cases_df = generated_tc_df # Display the error DF
temp_tc_filename = os.path.join(OUTPUT_DIR, f"test_cases_{os.path.basename(url).split('.')[0]}_error.xlsx")
test_cases_filepath = save_test_cases_to_excel(test_cases_df, filename=os.path.basename(temp_tc_filename))
else:
test_cases_filepath = None # No file if df is completely empty
yield current_outputs() # Update status and show error DF
# Stop if parsing failed completely or DF is empty
if test_cases_df.empty or 'PARSE_ERROR' in test_cases_df['Test Case ID'].values:
status_updates.append(" 🛑 Stopping process due to critical test case generation failure.")
yield current_outputs()
return
else:
test_cases_df = generated_tc_df # Update the main DF with successful results
temp_tc_filename = os.path.join(OUTPUT_DIR, f"test_cases_{os.path.basename(url).split('.')[0]}.xlsx")
test_cases_filepath = save_test_cases_to_excel(test_cases_df, filename=os.path.basename(temp_tc_filename))
status_updates.append(f" ✅ Generated {len(test_cases_df)} test cases. Saved to {test_cases_filepath}")
yield current_outputs() # Update status and show test cases DF
# --- Task 3: Selenium Script Generation ---
status_updates.append(f"\n🐍 [3/3] Generating Selenium scripts...")
yield current_outputs() # Update status BEFORE starting script generation
scripts_data = []
# Filter out potential error rows before iterating
valid_test_cases_df = test_cases_df[~test_cases_df['Test Case ID'].isin(['API_ERROR', 'ERROR', 'PARSE_ERROR'])]
if valid_test_cases_df.empty and not generation_failed:
status_updates.append(" ℹ️ No valid test cases found to generate scripts for (check previous warnings).")
yield current_outputs()
elif not valid_test_cases_df.empty:
status_updates.append(f" Mapping {len(valid_test_cases_df)} test cases to scripts...")
yield current_outputs() # Show count before starting loop
for index, test_case in valid_test_cases_df.iterrows():
tc_id = test_case.get('Test Case ID', f'Row_{index}')
status_updates.append(f" ⏳ Generating script for: {tc_id}...")
# Yield BEFORE the AI call for this script to show "Generating..." status
yield current_outputs()
script_code = generate_selenium_script(test_case.to_dict(), elements_json_str_for_ai, url)
scripts_data.append({
'Test Case ID': tc_id,
'Python Selenium Code': script_code
})
# Note: We collect all scripts first, then update the DataFrame *once* after the loop
# This prevents the DataFrame UI from flickering heavily during the loop.
# The status log will show progress for each script.
# After the loop, create and yield the final scripts DataFrame
scripts_df = pd.DataFrame(scripts_data)
temp_scripts_filename = os.path.join(OUTPUT_DIR, f"test_scripts_{os.path.basename(url).split('.')[0]}.xlsx")
scripts_filepath = save_scripts_to_excel(scripts_data, filename=os.path.basename(temp_scripts_filename))
status_updates.append(f" ✅ Generated {len(scripts_data)} scripts. Saved to {scripts_filepath}")
yield current_outputs() # Update status AND show the scripts DF
else: # Case where TC generation had errors but didn't stop the process
status_updates.append(" ℹ️ Skipping script generation due to previous errors in test case generation.")
yield current_outputs()
status_updates.append("\n\n🎉 Processing finished successfully!")
yield current_outputs() # Final successful state
except Exception as e:
logging.error(f"Critical error in process_website for {url}: {e}", exc_info=True)
status_updates.append(f"\n\n❌ CRITICAL ERROR: An unexpected error occurred: {str(e)}")
yield current_outputs() # Yield final state with error message
# --- Gradio Interface Definition (Keep the layout from the previous version) ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), css=custom_css) as demo:
# --- Header ---
with gr.Row():
gr.HTML("""
<div class="app-header">
<h1>🤖 AI-Driven Test Generation Prototype 🧪</h1>
<p>Extract UI Elements → Generate Test Cases → Create Selenium Scripts</p>
</div>
""")
# --- Input Controls ---
with gr.Group(elem_classes="control-section"):
with gr.Row():
with gr.Column(scale=3):
url_input = gr.Textbox(
label="Website URL",
placeholder="e.g., https://demoblaze.com",
info="Enter the full public URL of the website to analyze."
)
with gr.Column(scale=1):
num_cases_input = gr.Slider(
minimum=1, maximum=10, value=3, step=1, # Default to 3
label="Number of Test Cases",
info="How many test cases should the AI generate?"
)
# Use elem_id for more specific CSS targeting if needed
start_button = gr.Button("✨ Analyze Website and Generate Tests ✨", variant="primary", elem_id="generate-button")
# --- Status / Logs ---
with gr.Accordion("📊 Status & Logs", open=True): # Open by default
status_output = gr.Textbox(
label="Processing Log",
lines=12,
interactive=False,
show_copy_button=True
)
# --- Results Section ---
with gr.Column(elem_classes="results-section"):
gr.Markdown("## Results")
with gr.Tabs():
with gr.TabItem("📄 Extracted UI Elements"):
elements_output = gr.Code(
label="elements.json (Preview - scroll horizontally if needed)",
language="json",
interactive=False,
elem_classes="json-output-code" # Apply class for CSS targeting
)
download_elements = gr.File(label="Download elements.json", scale=0) # Make button smaller
with gr.TabItem("✅ Generated Test Cases"):
test_cases_output = gr.DataFrame(
label="Test Cases (Steps column supports multi-line)",
interactive=False,
wrap=True, # Allow text wrapping generally
elem_classes="test-cases-df" # Apply class for CSS targeting
)
download_test_cases = gr.File(label="Download test_cases.xlsx", scale=0)
with gr.TabItem("🐍 Generated Selenium Scripts"):
scripts_output = gr.DataFrame(
label="Selenium Scripts (Code column preserves formatting & scrolls)",
interactive=False,
wrap=False, # Disable wrapping for code column
elem_classes="scripts-df" # Apply class for CSS targeting
)
download_scripts = gr.File(label="Download test_scripts.xlsx", scale=0)
# --- Footer ---
with gr.Row():
gr.HTML("""
<div class="app-footer">
AI Test Generator Prototype | Using OpenAI Compatible API
</div>
""")
# --- Event Handling (Ensure outputs match the yielded tuple order) ---
start_button.click(
fn=process_website,
inputs=[url_input, num_cases_input],
outputs=[
elements_output, # 1st element in yielded tuple
test_cases_output, # 2nd
scripts_output, # 3rd
download_elements, # 4th
download_test_cases, # 5th
download_scripts, # 6th
status_output # 7th
]
)
# --- Examples ---
gr.Examples(
examples=[
["https://demoblaze.com", 5],
["http://the-internet.herokuapp.com/login", 4],
["https://www.wikipedia.org/", 3]
],
inputs=[url_input, num_cases_input],
label="Example Websites",
elem_id="examples-container" # Optional ID for styling
)
# --- Launch the Application ---
if __name__ == "__main__":
print("Starting Gradio app with enhanced UI and corrected dynamic updates...")
demo.launch(debug=True) # debug=True helps see errors
|