# 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("""

๐Ÿค– AI-Driven Test Generation Prototype ๐Ÿงช

Extract UI Elements โ†’ Generate Test Cases โ†’ Create Selenium Scripts

""") # --- 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(""" """) # --- 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