From 00390c329cc056d41d3ea69f3a7c08e99328f8cd Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 15:27:41 +0530 Subject: [PATCH 01/11] feat: add AI test generator agent Adds an AI-powered test generator that lets non-technical team members describe a test in plain English and automatically produce a ready-to-run pytest file using the existing POM framework and page objects. Files added: - ai_test_generator/scanner.py - scans all 16 suites for page objects/methods - ai_test_generator/generate_test.py - CLI + Claude API integration - ai_test_generator/__init__.py - ai_test_generator/requirements.txt - .github/workflows/ai-test-generator.yml - GitHub Actions UI trigger Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ai-test-generator.yml | 227 ++++++++++++++++ ai_test_generator/__init__.py | 1 + ai_test_generator/generate_test.py | 348 ++++++++++++++++++++++++ ai_test_generator/requirements.txt | 1 + ai_test_generator/scanner.py | 319 ++++++++++++++++++++++ 5 files changed, 896 insertions(+) create mode 100644 .github/workflows/ai-test-generator.yml create mode 100644 ai_test_generator/__init__.py create mode 100644 ai_test_generator/generate_test.py create mode 100644 ai_test_generator/requirements.txt create mode 100644 ai_test_generator/scanner.py diff --git a/.github/workflows/ai-test-generator.yml b/.github/workflows/ai-test-generator.yml new file mode 100644 index 000000000..241e32e8d --- /dev/null +++ b/.github/workflows/ai-test-generator.yml @@ -0,0 +1,227 @@ +# AI Test Generator +# +# Automatically generates a pytest test file from a plain English description. +# Non-technical team members can trigger this workflow from the GitHub Actions UI, +# describe the test in plain English, and get a ready-to-run test file committed +# back to the repository. +# +# Required secret: ANTHROPIC_API_KEY + +name: AI Test Generator + +on: + workflow_dispatch: + inputs: + suite: + description: 'Target test suite' + required: true + type: choice + options: + - CaseSearch + - DataDictionary + - FindDataById + - Lookuptable + - MultiSelect + - PowerBI + - SplitScreenCaseSearch + - ElasticSearch + - ExportTests + - Formplayer + - HQSmokeTests + - P1P2Tests + - RequestAPI + - USH_CO_BHA + - MobileTest + - BHAStressTest + + description: + description: 'Describe the test in plain English (what should the test do?)' + required: true + type: string + + output_path: + description: 'Output file path (leave blank to auto-generate)' + required: false + type: string + default: '' + + run_after_generate: + description: 'Run the generated test immediately after generating it?' + required: false + type: boolean + default: false + + environment: + description: 'Environment to run the generated test against (only used if run_after_generate is true)' + required: false + type: choice + default: 'staging' + options: + - staging + - production + - eu + - india + + commit_result: + description: 'Commit the generated test file back to the repository?' + required: false + type: boolean + default: true + +jobs: + generate: + name: Generate Test for '${{ inputs.suite }}' + runs-on: ubuntu-latest + + outputs: + generated_file: ${{ steps.generate.outputs.generated_file }} + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.13 + uses: actions/setup-python@v2 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install anthropic>=0.40.0 + + - name: Generate test file + id: generate + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + INPUT_SUITE: ${{ inputs.suite }} + INPUT_DESCRIPTION: ${{ inputs.description }} + INPUT_OUTPUT: ${{ inputs.output_path }} + run: | + python ai_test_generator/generate_test.py \ + --suite "${{ inputs.suite }}" \ + --description "${{ inputs.description }}" \ + ${{ inputs.output_path != '' && format('--output "{0}"', inputs.output_path) || '' }} + + # Capture the generated file path for downstream steps + GENERATED=$(find . -newer ai_test_generator/generate_test.py -name "test_*.py" \ + ! -path "./venv/*" ! -path "./.git/*" | head -1) + echo "Generated file: $GENERATED" + echo "generated_file=$GENERATED" >> $GITHUB_OUTPUT + + - name: Upload generated test as artifact + uses: actions/upload-artifact@v4 + with: + name: generated-test-${{ inputs.suite }}-${{ github.run_id }} + path: ${{ steps.generate.outputs.generated_file }} + retention-days: 30 + + - name: Commit generated test to repository + if: ${{ inputs.commit_result == true }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add "${{ steps.generate.outputs.generated_file }}" + git commit -m "feat(ai-gen): add generated test for ${{ inputs.suite }} + + Suite: ${{ inputs.suite }} + Description: ${{ inputs.description }} + Generated by: AI Test Generator (run #${{ github.run_number }}) + Triggered by: ${{ github.actor }}" + git push + + - name: Summary + run: | + echo "## AI Test Generator Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Suite | \`${{ inputs.suite }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Description | ${{ inputs.description }} |" >> $GITHUB_STEP_SUMMARY + echo "| Generated File | \`${{ steps.generate.outputs.generated_file }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Committed | ${{ inputs.commit_result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Generated Code" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`python" >> $GITHUB_STEP_SUMMARY + cat "${{ steps.generate.outputs.generated_file }}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + run_generated_test: + name: Run Generated Test on '${{ inputs.environment }}' + needs: generate + if: ${{ inputs.run_after_generate == true }} + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + # Pull latest so we get the newly committed test file + ref: ${{ github.ref }} + + - name: Set up Python 3.13 + uses: actions/setup-python@v2 + with: + python-version: '3.13' + + - name: Pull latest commit (get generated test) + if: ${{ inputs.commit_result == true }} + run: git pull + + - name: Download generated test artifact (if not committed) + if: ${{ inputs.commit_result == false }} + uses: actions/download-artifact@v4 + with: + name: generated-test-${{ inputs.suite }}-${{ github.run_id }} + path: ${{ needs.generate.outputs.generated_file && '' || '.' }} + + - name: Install suite dependencies + run: | + python -m pip install --upgrade pip + # Install the suite's requires.txt if it exists + SUITE_DIR=$(python -c " + suites = { + 'CaseSearch': 'Features/CaseSearch', + 'DataDictionary': 'Features/DataDictionary', + 'FindDataById': 'Features/FindDataById', + 'Lookuptable': 'Features/Lookuptable', + 'MultiSelect': 'Features/MultiSelect', + 'PowerBI': 'Features/Powerbi_integration_exports', + 'SplitScreenCaseSearch': 'Features/SplitScreenCaseSearch', + 'ElasticSearch': 'ElasticSearchTests', + 'ExportTests': 'ExportTests', + 'Formplayer': 'Formplayer', + 'HQSmokeTests': 'HQSmokeTests', + 'P1P2Tests': 'P1P2Tests', + 'RequestAPI': 'RequestAPI', + 'USH_CO_BHA': 'USH_Apps/CO_BHA', + 'MobileTest': 'MobileTest', + 'BHAStressTest': 'QA_Requests/BHAStressTest', + } + print(suites.get('${{ inputs.suite }}', '')) + ") + if [ -f "${SUITE_DIR}/requires.txt" ]; then + echo "Installing from ${SUITE_DIR}/requires.txt" + pip install -r "${SUITE_DIR}/requires.txt" + else + echo "No requires.txt found, installing common deps" + pip install pytest selenium pytest-html pytest-rerunfailures + fi + + - name: Run generated test + env: + DIMAGIQA_ENV: ${{ inputs.environment }} + DIMAGIQA_LOGIN_USERNAME: ${{ secrets.DIMAGIQA_LOGIN_USERNAME }} + DIMAGIQA_LOGIN_PASSWORD: ${{ secrets.DIMAGIQA_LOGIN_PASSWORD }} + ENABLE_WAITS: 'true' + run: | + pytest -v "${{ needs.generate.outputs.generated_file }}" \ + --html=report_generated_test.html \ + --self-contained-html \ + --tb=short + + - name: Upload test results + if: ${{ success() || failure() }} + uses: actions/upload-artifact@v4 + with: + name: generated-test-results-${{ inputs.environment }}-${{ github.run_id }} + path: report_generated_test.html + retention-days: 2 \ No newline at end of file diff --git a/ai_test_generator/__init__.py b/ai_test_generator/__init__.py new file mode 100644 index 000000000..b836304b9 --- /dev/null +++ b/ai_test_generator/__init__.py @@ -0,0 +1 @@ +# ai_test_generator package \ No newline at end of file diff --git a/ai_test_generator/generate_test.py b/ai_test_generator/generate_test.py new file mode 100644 index 000000000..62dbdbd76 --- /dev/null +++ b/ai_test_generator/generate_test.py @@ -0,0 +1,348 @@ +""" +generate_test.py +================ +AI-powered test generator for the dimagi-qa framework. + +Usage (local): + python ai_test_generator/generate_test.py \ + --suite CaseSearch \ + --description "Login as user-1, open the Music App, search for a case by song name using text input, select the case and submit the Play Song form" \ + --output Features/CaseSearch/test_cases/test_99_generated.py + +Usage (GitHub Actions): + Triggered via workflow_dispatch - see .github/workflows/ai-test-generator.yml + +Requirements: + pip install anthropic + +Environment variable: + ANTHROPIC_API_KEY Your Anthropic API key (required) +""" + +import argparse +import os +import sys +import textwrap +from pathlib import Path + +# Allow running from project root or from ai_test_generator/ +ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(ROOT)) + +try: + import anthropic +except ImportError: + print("[ERROR] anthropic package not installed. Run: pip install anthropic") + sys.exit(1) + +from ai_test_generator.scanner import ( + scan_suite, + scan_common_utilities, + format_suite_context, + list_suites, + SUITES, + ROOT as PROJECT_ROOT, +) + + +# ─── System prompt ──────────────────────────────────────────────────────────── + +SYSTEM_PROMPT = textwrap.dedent(""" +You are an expert test automation engineer for the dimagi-qa Selenium framework. +Your job is to write a complete, production-ready pytest test file from a plain English description. + +## FRAMEWORK RULES (follow these exactly): + +1. **Imports** – always include: + ```python + import pytest + from common_utilities.selenium.webapps import WebApps + from common_utilities.hq_login.login_page import LoginPage + ``` + Import page objects from the suite's test_pages folder. + Import user input constants from the suite's user_inputs module (use the provided constants — never hardcode strings). + +2. **Fixtures** – every test function takes `(driver, settings)` as arguments. + These come from conftest.py — do NOT redefine them. + +3. **Page object instantiation inside each test**: + ```python + webapps = WebApps(driver, settings) + page = SomePageClass(driver) + ``` + +4. **Test function naming**: `test__` (e.g. `test_01_search_and_submit`) + +5. **Docstring**: Every test must have a docstring describing the test scenario in plain English. + +6. **Assertions**: Use `assert` with meaningful messages. + Example: `assert webapps.is_present_and_displayed(locator), "Element not found"` + Or simply verify via methods that already assert internally. + +7. **Logging**: Use `print()` for step-by-step logging (the framework uses print, not logging). + +8. **No hardcoded strings**: Always use constants from user_inputs classes or locally defined constants. + +9. **File header**: Include a module-level docstring explaining what the file tests. + +10. **Only use methods that actually exist** in the page objects provided below. + Do not invent method names. If a method does not exist, use BasePage primitives + (wait_to_click, wait_to_clear_and_send_keys, wait_for_element, etc.). + +11. **BasePage key methods** (inherited by all page objects and WebApps): + - wait_to_click(locator) + - wait_to_clear_and_send_keys(locator, text) + - wait_for_element(locator, timeout=30) + - wait_for_disappear(locator) + - wait_to_get_text(locator) → str + - is_present_and_displayed(locator, timeout) → bool + - is_displayed(locator) → bool + - find_elements_texts(locator) → list[str] + - js_click(locator) + - get_element(format_string, value) → locator tuple + - scroll_to_element(locator) + - get_url(url) + - select_by_text(locator, value) + +12. **WebApps key methods** (navigation & form submission): + - open_app(app_name) + - open_menu(menu_name) + - open_form(form_name) + - submit_the_form() + - search_all_cases() + - omni_search(case_name) + - select_case_and_continue(case_name) → list[str] + - select_first_case_on_list_and_continue() + - navigate_to_breadcrumb(value) + - login_as(username) + - clear_selections_on_case_search_page() + - search_button_on_case_search_page() + +## OUTPUT FORMAT: +Return ONLY valid Python code. No markdown fences, no explanation outside the code. +The code should be ready to save directly as a .py file and run with pytest. +""").strip() + + +# ─── User prompt builder ─────────────────────────────────────────────────────── + +def build_user_prompt(description: str, suite_context: str, output_filename: str) -> str: + suite_name_hint = Path(output_filename).stem if output_filename else "test_generated" + return textwrap.dedent(f""" + Generate a complete pytest test file for the following test scenario: + + TEST DESCRIPTION: + {description} + + OUTPUT FILE: {output_filename} + + {suite_context} + + IMPORTANT: + - Use ONLY the page objects and methods listed above. + - Use ONLY the user input constants listed above (or define new ones at the top of the file if needed). + - Follow all framework rules exactly. + - Return ONLY Python code, no markdown. + """).strip() + + +# ─── Claude API call ────────────────────────────────────────────────────────── + +def generate_test_code(description: str, suite_name: str, output_filename: str, + model: str = "claude-sonnet-4-6") -> str: + api_key = os.environ.get("ANTHROPIC_API_KEY") + if not api_key: + print("[ERROR] ANTHROPIC_API_KEY environment variable not set.") + sys.exit(1) + + print(f"[INFO] Scanning suite: {suite_name}") + suite_data = scan_suite(suite_name) + common_utils = scan_common_utilities() + suite_context = format_suite_context(suite_data, common_utils) + + print(f"[INFO] Found {len(suite_data['page_classes'])} page object class(es)") + print(f"[INFO] Found {len(suite_data['user_inputs'])} user input constant(s)") + print(f"[INFO] Calling Claude ({model}) to generate test...") + + client = anthropic.Anthropic(api_key=api_key) + + message = client.messages.create( + model=model, + max_tokens=4096, + system=SYSTEM_PROMPT, + messages=[ + { + "role": "user", + "content": build_user_prompt(description, suite_context, output_filename), + } + ], + ) + + return message.content[0].text.strip() + + +# ─── Output helpers ─────────────────────────────────────────────────────────── + +def resolve_output_path(suite_name: str, output_arg: str | None) -> Path: + """Determine where to write the generated test file.""" + if output_arg: + p = Path(output_arg) + if not p.is_absolute(): + p = PROJECT_ROOT / p + return p + + # Auto-generate path based on suite + suite_path = SUITES.get(suite_name) + if not suite_path: + return PROJECT_ROOT / "generated_test.py" + + for subdir in ["test_cases", "testCases"]: + tests_dir = suite_path / subdir + if tests_dir.exists(): + # Find next available test number + existing = sorted(tests_dir.glob("test_*.py")) + if existing: + # Try to parse the highest test number + nums = [] + for f in existing: + parts = f.stem.split("_") + if len(parts) >= 2 and parts[1].isdigit(): + nums.append(int(parts[1])) + next_num = (max(nums) + 1) if nums else 99 + else: + next_num = 1 + return tests_dir / f"test_{next_num:02d}_ai_generated.py" + + return PROJECT_ROOT / "generated_test.py" + + +def write_output(code: str, output_path: Path) -> None: + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(code, encoding="utf-8") + print(f"\n[SUCCESS] Test file written to: {output_path}") + print(f"[INFO] Run with: pytest {output_path.relative_to(PROJECT_ROOT)}") + + +# ─── CLI ────────────────────────────────────────────────────────────────────── + +def parse_args(): + parser = argparse.ArgumentParser( + description="Generate a pytest test file from a plain English description.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent(""" + Examples: + # Local usage + python ai_test_generator/generate_test.py \\ + --suite CaseSearch \\ + --description "Login as user-1, open Music App, search for Song Name, submit the form" + + # Specify custom output path + python ai_test_generator/generate_test.py \\ + --suite HQSmokeTests \\ + --description "Verify that the Reports module shows Worker Activity report" \\ + --output HQSmokeTests/testCases/test_99_worker_activity_check.py + + # List all available suites + python ai_test_generator/generate_test.py --list-suites + """), + ) + parser.add_argument( + "--suite", "-s", + help="Target test suite name (use --list-suites to see all options)", + ) + parser.add_argument( + "--description", "-d", + help="Plain English description of the test scenario", + ) + parser.add_argument( + "--output", "-o", + help="Output file path (relative to project root). Auto-generated if not specified.", + default=None, + ) + parser.add_argument( + "--model", "-m", + help="Claude model to use (default: claude-sonnet-4-6)", + default="claude-sonnet-4-6", + ) + parser.add_argument( + "--list-suites", + action="store_true", + help="List all available test suites and exit", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print the generated code to stdout instead of writing to a file", + ) + return parser.parse_args() + + +def main(): + args = parse_args() + + if args.list_suites: + suites = list_suites() + print("Available test suites:") + for s in suites: + print(f" - {s}") + return + + # Support env vars for GitHub Actions usage + suite = args.suite or os.environ.get("INPUT_SUITE") + description = args.description or os.environ.get("INPUT_DESCRIPTION") + output_arg = args.output or os.environ.get("INPUT_OUTPUT") + + if not suite: + print("[ERROR] --suite is required. Use --list-suites to see available suites.") + sys.exit(1) + if not description: + print("[ERROR] --description is required.") + sys.exit(1) + + if suite not in SUITES: + print(f"[ERROR] Unknown suite '{suite}'. Available: {', '.join(list_suites())}") + sys.exit(1) + + output_path = resolve_output_path(suite, output_arg) + + print("=" * 60) + print(" dimagi-qa AI Test Generator") + print("=" * 60) + print(f" Suite : {suite}") + print(f" Description: {description[:80]}{'...' if len(description) > 80 else ''}") + print(f" Output : {output_path.relative_to(PROJECT_ROOT)}") + print(f" Model : {args.model}") + print("=" * 60) + + code = generate_test_code( + description=description, + suite_name=suite, + output_filename=str(output_path.relative_to(PROJECT_ROOT)), + model=args.model, + ) + + # Strip accidental markdown fences if model adds them + if code.startswith("```"): + lines = code.splitlines() + # Remove first and last fence lines + if lines[0].startswith("```"): + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + code = "\n".join(lines) + + if args.dry_run: + print("\n" + "=" * 60) + print("GENERATED CODE (dry run — not written to file):") + print("=" * 60) + print(code) + else: + write_output(code, output_path) + print("\nNext steps:") + print(" 1. Review the generated file and adjust any locators or inputs") + print(" 2. Make sure settings.cfg is populated for your environment") + print(f" 3. Run: pytest {output_path.relative_to(PROJECT_ROOT)} -v") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ai_test_generator/requirements.txt b/ai_test_generator/requirements.txt new file mode 100644 index 000000000..7925bf346 --- /dev/null +++ b/ai_test_generator/requirements.txt @@ -0,0 +1 @@ +anthropic>=0.40.0 \ No newline at end of file diff --git a/ai_test_generator/scanner.py b/ai_test_generator/scanner.py new file mode 100644 index 000000000..4461188d0 --- /dev/null +++ b/ai_test_generator/scanner.py @@ -0,0 +1,319 @@ +""" +scanner.py +Scans all test suites in the dimagi-qa project and extracts: +- Available page object classes and their public methods +- Suite structure (test_pages/, test_cases/, user_inputs/) +- User input classes and their attributes +""" + +import ast +import os +from pathlib import Path + + +ROOT = Path(__file__).parent.parent + +# All known test suites mapped to their folder paths +SUITES = { + "CaseSearch": ROOT / "Features" / "CaseSearch", + "DataDictionary": ROOT / "Features" / "DataDictionary", + "FindDataById": ROOT / "Features" / "FindDataById", + "Lookuptable": ROOT / "Features" / "Lookuptable", + "MultiSelect": ROOT / "Features" / "MultiSelect", + "PowerBI": ROOT / "Features" / "Powerbi_integration_exports", + "SplitScreenCaseSearch": ROOT / "Features" / "SplitScreenCaseSearch", + "ElasticSearch": ROOT / "ElasticSearchTests", + "ExportTests": ROOT / "ExportTests", + "Formplayer": ROOT / "Formplayer", + "HQSmokeTests": ROOT / "HQSmokeTests", + "P1P2Tests": ROOT / "P1P2Tests", + "RequestAPI": ROOT / "RequestAPI", + "USH_CO_BHA": ROOT / "USH_Apps" / "CO_BHA", + "MobileTest": ROOT / "MobileTest", + "BHAStressTest": ROOT / "QA_Requests" / "BHAStressTest", +} + +COMMON_UTILITIES = ROOT / "common_utilities" + + +def _extract_classes_and_methods(filepath: Path) -> list[dict]: + """Parse a Python file and return class info with public method signatures.""" + try: + source = filepath.read_text(encoding="utf-8", errors="ignore") + tree = ast.parse(source) + except Exception: + return [] + + results = [] + for node in ast.walk(tree): + if not isinstance(node, ast.ClassDef): + continue + + methods = [] + for item in node.body: + if not isinstance(item, ast.FunctionDef): + continue + if item.name.startswith("_"): + continue + + # Build readable signature + args = [a.arg for a in item.args.args if a.arg != "self"] + # Include defaults info + defaults = item.args.defaults + if defaults: + num_defaults = len(defaults) + required = args[:-num_defaults] if num_defaults < len(args) else [] + optional = args[-num_defaults:] if num_defaults <= len(args) else args + sig_parts = required + [f"{a}=..." for a in optional] + else: + sig_parts = args + + # Grab first line of docstring if present + docstring = "" + if (item.body and isinstance(item.body[0], ast.Expr) + and isinstance(item.body[0].value, ast.Constant)): + docstring = item.body[0].value.value.strip().splitlines()[0] + + methods.append({ + "name": item.name, + "signature": f"{item.name}({', '.join(sig_parts)})", + "doc": docstring, + }) + + if methods: + results.append({ + "class": node.name, + "file": str(filepath.relative_to(ROOT)), + "methods": methods, + }) + + return results + + +def _extract_constants(filepath: Path) -> dict: + """Extract top-level string constants and class attributes from a Python file.""" + try: + source = filepath.read_text(encoding="utf-8", errors="ignore") + tree = ast.parse(source) + except Exception: + return {} + + constants = {} + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + for item in node.body: + if isinstance(item, ast.Assign): + for target in item.targets: + if isinstance(target, ast.Name): + if isinstance(item.value, ast.Constant): + constants[f"{node.name}.{target.id}"] = item.value.s + elif isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name): + if isinstance(node.value, ast.Constant): + constants[target.id] = node.value.s + return constants + + +def _find_page_files(suite_path: Path) -> list[Path]: + """Find all page object files in a suite.""" + page_files = [] + for subdir in ["test_pages", "testPages", "pages"]: + pages_dir = suite_path / subdir + if pages_dir.exists(): + page_files.extend(pages_dir.rglob("*.py")) + return page_files + + +def _find_test_files(suite_path: Path) -> list[Path]: + """Find existing test files to use as reference examples.""" + test_files = [] + for subdir in ["test_cases", "testCases"]: + tests_dir = suite_path / subdir + if tests_dir.exists(): + test_files.extend( + f for f in tests_dir.glob("test_*.py") + if "conftest" not in f.name + ) + return sorted(test_files) + + +def _find_user_input_files(suite_path: Path) -> list[Path]: + """Find user input / test data files.""" + input_files = [] + for subdir in ["user_inputs", "userInputs", "UserInputs"]: + inputs_dir = suite_path / subdir + if inputs_dir.exists(): + input_files.extend(inputs_dir.rglob("*.py")) + return input_files + + +def scan_common_utilities() -> str: + """Return a summary of BasePage and WebApps methods.""" + lines = [] + + base_page = COMMON_UTILITIES / "selenium" / "base_page.py" + webapps = COMMON_UTILITIES / "selenium" / "webapps.py" + login_page = COMMON_UTILITIES / "hq_login" / "login_page.py" + + for filepath in [base_page, webapps, login_page]: + if not filepath.exists(): + continue + classes = _extract_classes_and_methods(filepath) + for cls in classes: + lines.append(f"\nClass: {cls['class']} (from {cls['file']})") + for m in cls["methods"]: + line = f" - {m['signature']}" + if m["doc"]: + line += f" # {m['doc']}" + lines.append(line) + + return "\n".join(lines) + + +def scan_suite(suite_name: str) -> dict: + """ + Scan a specific test suite and return structured context: + { + "suite_name": str, + "suite_path": str, + "page_classes": [{"class": str, "file": str, "methods": [...]}], + "user_inputs": {attr: value, ...}, + "test_dirs": {"test_cases": str, "test_pages": str, "user_inputs": str}, + "existing_test_example": str, # content of first test file + "conftest_example": str, # content of conftest.py + } + """ + suite_path = SUITES.get(suite_name) + if not suite_path or not suite_path.exists(): + available = ", ".join(SUITES.keys()) + raise ValueError( + f"Suite '{suite_name}' not found. Available suites: {available}" + ) + + result = { + "suite_name": suite_name, + "suite_path": str(suite_path.relative_to(ROOT)), + "page_classes": [], + "user_inputs": {}, + "test_dirs": {}, + "existing_test_example": "", + "conftest_example": "", + } + + # Page objects + for pf in _find_page_files(suite_path): + result["page_classes"].extend(_extract_classes_and_methods(pf)) + + # Directory names + for subdir in ["test_cases", "testCases"]: + if (suite_path / subdir).exists(): + result["test_dirs"]["test_cases"] = subdir + break + for subdir in ["test_pages", "testPages"]: + if (suite_path / subdir).exists(): + result["test_dirs"]["test_pages"] = subdir + break + for subdir in ["user_inputs", "userInputs", "UserInputs"]: + if (suite_path / subdir).exists(): + result["test_dirs"]["user_inputs"] = subdir + break + + # User inputs (constants for test data) + for uf in _find_user_input_files(suite_path): + result["user_inputs"].update(_extract_constants(uf)) + + # Grab first existing test as reference example (truncated to 100 lines) + test_files = _find_test_files(suite_path) + if test_files: + try: + lines = test_files[0].read_text(encoding="utf-8", errors="ignore").splitlines() + result["existing_test_example"] = "\n".join(lines[:100]) + except Exception: + pass + + # Grab conftest.py + for subdir in ["test_cases", "testCases"]: + conftest = suite_path / subdir / "conftest.py" + if conftest.exists(): + try: + lines = conftest.read_text(encoding="utf-8", errors="ignore").splitlines() + result["conftest_example"] = "\n".join(lines[:60]) + except Exception: + pass + break + + return result + + +def list_suites() -> list[str]: + """Return all available suite names.""" + return [name for name, path in SUITES.items() if path.exists()] + + +def format_suite_context(suite_data: dict, common_utils: str) -> str: + """Format suite scan data into a readable context string for the AI prompt.""" + lines = [] + + lines.append(f"=== SUITE: {suite_data['suite_name']} ===") + lines.append(f"Path: {suite_data['suite_path']}") + lines.append("") + + # Directory structure + dirs = suite_data["test_dirs"] + lines.append("Directory layout:") + lines.append(f" Tests: {suite_data['suite_path']}/{dirs.get('test_cases', 'test_cases')}/") + lines.append(f" Page objects:{suite_data['suite_path']}/{dirs.get('test_pages', 'test_pages')}/") + lines.append(f" User inputs: {suite_data['suite_path']}/{dirs.get('user_inputs', 'user_inputs')}/") + lines.append("") + + # Common utilities + lines.append("=== COMMON UTILITIES (available in ALL suites) ===") + lines.append(common_utils) + lines.append("") + + # Suite-specific page objects + if suite_data["page_classes"]: + lines.append("=== SUITE-SPECIFIC PAGE OBJECTS ===") + for cls in suite_data["page_classes"]: + lines.append(f"\nClass: {cls['class']} (from {cls['file']})") + for m in cls["methods"]: + line = f" - {m['signature']}" + if m["doc"]: + line += f" # {m['doc']}" + lines.append(line) + else: + lines.append("=== NO SUITE-SPECIFIC PAGE OBJECTS FOUND ===") + lines.append("Use only common_utilities (BasePage, WebApps, LoginPage).") + lines.append("") + + # User inputs sample + if suite_data["user_inputs"]: + lines.append("=== AVAILABLE USER INPUT VALUES (sample) ===") + for k, v in list(suite_data["user_inputs"].items())[:40]: + lines.append(f" {k} = '{v}'") + lines.append("") + + # Existing test as reference + if suite_data["existing_test_example"]: + lines.append("=== EXISTING TEST EXAMPLE (reference only) ===") + lines.append(suite_data["existing_test_example"]) + lines.append("") + + # Conftest structure + if suite_data["conftest_example"]: + lines.append("=== CONFTEST.PY (fixture reference) ===") + lines.append(suite_data["conftest_example"]) + + return "\n".join(lines) + + +if __name__ == "__main__": + # Quick smoke test + print("Available suites:", list_suites()) + print("\nCommon utilities:") + print(scan_common_utilities()[:500]) + print("\nScanning CaseSearch...") + data = scan_suite("CaseSearch") + print(f"Found {len(data['page_classes'])} page classes") + print(f"Found {len(data['user_inputs'])} user input values") From c9460840fde5537efe4daf0f720c75ad34322efe Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 16:13:59 +0530 Subject: [PATCH 02/11] chore: switch AI generator from Anthropic to OpenAI Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ai-test-generator.yml | 4 +-- ai_test_generator/.env.example | 1 + ai_test_generator/generate_test.py | 47 +++++++++++++++---------- ai_test_generator/requirements.txt | 2 +- 4 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 ai_test_generator/.env.example diff --git a/.github/workflows/ai-test-generator.yml b/.github/workflows/ai-test-generator.yml index 241e32e8d..baea073da 100644 --- a/.github/workflows/ai-test-generator.yml +++ b/.github/workflows/ai-test-generator.yml @@ -5,7 +5,7 @@ # describe the test in plain English, and get a ready-to-run test file committed # back to the repository. # -# Required secret: ANTHROPIC_API_KEY +# Required secret: OPENAI_API_KEY name: AI Test Generator @@ -92,7 +92,7 @@ jobs: - name: Generate test file id: generate env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} INPUT_SUITE: ${{ inputs.suite }} INPUT_DESCRIPTION: ${{ inputs.description }} INPUT_OUTPUT: ${{ inputs.output_path }} diff --git a/ai_test_generator/.env.example b/ai_test_generator/.env.example new file mode 100644 index 000000000..35965eb63 --- /dev/null +++ b/ai_test_generator/.env.example @@ -0,0 +1 @@ +ANTHROPIC_API_KEY=your-key-here \ No newline at end of file diff --git a/ai_test_generator/generate_test.py b/ai_test_generator/generate_test.py index 62dbdbd76..afe29a547 100644 --- a/ai_test_generator/generate_test.py +++ b/ai_test_generator/generate_test.py @@ -13,10 +13,10 @@ Triggered via workflow_dispatch - see .github/workflows/ai-test-generator.yml Requirements: - pip install anthropic + pip install openai Environment variable: - ANTHROPIC_API_KEY Your Anthropic API key (required) + OPENAI_API_KEY Your OpenAI API key (required) """ import argparse @@ -30,9 +30,9 @@ sys.path.insert(0, str(ROOT)) try: - import anthropic + from openai import OpenAI except ImportError: - print("[ERROR] anthropic package not installed. Run: pip install anthropic") + print("[ERROR] openai package not installed. Run: pip install openai") sys.exit(1) from ai_test_generator.scanner import ( @@ -148,11 +148,25 @@ def build_user_prompt(description: str, suite_context: str, output_filename: str # ─── Claude API call ────────────────────────────────────────────────────────── +def _load_env_file(): + """Load .env file from ai_test_generator/ if it exists.""" + env_file = Path(__file__).parent / ".env" + if env_file.exists(): + for line in env_file.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, _, value = line.partition("=") + os.environ.setdefault(key.strip(), value.strip()) + + def generate_test_code(description: str, suite_name: str, output_filename: str, - model: str = "claude-sonnet-4-6") -> str: - api_key = os.environ.get("ANTHROPIC_API_KEY") + model: str = "gpt-4o") -> str: + _load_env_file() + api_key = os.environ.get("OPENAI_API_KEY") if not api_key: - print("[ERROR] ANTHROPIC_API_KEY environment variable not set.") + print("[ERROR] OPENAI_API_KEY not set.") + print(" Option 1: Create ai_test_generator/.env with OPENAI_API_KEY=your-key") + print(" Option 2: Set environment variable OPENAI_API_KEY before running") sys.exit(1) print(f"[INFO] Scanning suite: {suite_name}") @@ -162,23 +176,20 @@ def generate_test_code(description: str, suite_name: str, output_filename: str, print(f"[INFO] Found {len(suite_data['page_classes'])} page object class(es)") print(f"[INFO] Found {len(suite_data['user_inputs'])} user input constant(s)") - print(f"[INFO] Calling Claude ({model}) to generate test...") + print(f"[INFO] Calling OpenAI ({model}) to generate test...") - client = anthropic.Anthropic(api_key=api_key) + client = OpenAI(api_key=api_key) - message = client.messages.create( + response = client.chat.completions.create( model=model, max_tokens=4096, - system=SYSTEM_PROMPT, messages=[ - { - "role": "user", - "content": build_user_prompt(description, suite_context, output_filename), - } + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": build_user_prompt(description, suite_context, output_filename)}, ], ) - return message.content[0].text.strip() + return response.choices[0].message.content.strip() # ─── Output helpers ─────────────────────────────────────────────────────────── @@ -261,8 +272,8 @@ def parse_args(): ) parser.add_argument( "--model", "-m", - help="Claude model to use (default: claude-sonnet-4-6)", - default="claude-sonnet-4-6", + help="OpenAI model to use (default: gpt-4o)", + default="gpt-4o", ) parser.add_argument( "--list-suites", diff --git a/ai_test_generator/requirements.txt b/ai_test_generator/requirements.txt index 7925bf346..3ceaffc34 100644 --- a/ai_test_generator/requirements.txt +++ b/ai_test_generator/requirements.txt @@ -1 +1 @@ -anthropic>=0.40.0 \ No newline at end of file +openai>=1.0.0 \ No newline at end of file From 17ecdcba0597a78900a2ce13dacc28c5d664c672 Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 16:21:12 +0530 Subject: [PATCH 03/11] feat: add txt-based test case trigger system Team members can now write test steps in plain English inside ai_testcases/*.txt files in any suite folder. Pushing the file automatically triggers the AI generator and commits the .py test back. - ai_test_generator/process_testcases.py - scans all ai_testcases/ folders - ai_test_generator/TESTCASE_TEMPLATE.txt - template for team members - Features/CaseSearch/ai_testcases/ - example for CaseSearch suite - Updated GitHub Actions workflow to trigger on txt file push Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ai-test-generator.yml | 33 ++- .../search_and_submit_play_song.txt | 15 ++ ai_test_generator/TESTCASE_TEMPLATE.txt | 10 + ai_test_generator/process_testcases.py | 253 ++++++++++++++++++ 4 files changed, 302 insertions(+), 9 deletions(-) create mode 100644 Features/CaseSearch/ai_testcases/search_and_submit_play_song.txt create mode 100644 ai_test_generator/TESTCASE_TEMPLATE.txt create mode 100644 ai_test_generator/process_testcases.py diff --git a/.github/workflows/ai-test-generator.yml b/.github/workflows/ai-test-generator.yml index baea073da..5bcb63e81 100644 --- a/.github/workflows/ai-test-generator.yml +++ b/.github/workflows/ai-test-generator.yml @@ -10,6 +10,10 @@ name: AI Test Generator on: + push: + paths: + - '**/ai_testcases/*.txt' + workflow_dispatch: inputs: suite: @@ -87,27 +91,38 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install anthropic>=0.40.0 + pip install openai>=1.0.0 - - name: Generate test file - id: generate + - name: Generate from txt files (on push) + if: github.event_name == 'push' + id: generate_push + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + python ai_test_generator/process_testcases.py --force + GENERATED=$(git diff --name-only HEAD | grep "test_ai_" | head -1) + echo "generated_file=$GENERATED" >> $GITHUB_OUTPUT + + - name: Generate from description (manual trigger) + if: github.event_name == 'workflow_dispatch' + id: generate_manual env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - INPUT_SUITE: ${{ inputs.suite }} - INPUT_DESCRIPTION: ${{ inputs.description }} - INPUT_OUTPUT: ${{ inputs.output_path }} run: | python ai_test_generator/generate_test.py \ --suite "${{ inputs.suite }}" \ --description "${{ inputs.description }}" \ ${{ inputs.output_path != '' && format('--output "{0}"', inputs.output_path) || '' }} - - # Capture the generated file path for downstream steps GENERATED=$(find . -newer ai_test_generator/generate_test.py -name "test_*.py" \ ! -path "./venv/*" ! -path "./.git/*" | head -1) - echo "Generated file: $GENERATED" echo "generated_file=$GENERATED" >> $GITHUB_OUTPUT + - name: Set generated file output + id: generate + run: | + FILE="${{ steps.generate_push.outputs.generated_file || steps.generate_manual.outputs.generated_file }}" + echo "generated_file=$FILE" >> $GITHUB_OUTPUT + - name: Upload generated test as artifact uses: actions/upload-artifact@v4 with: diff --git a/Features/CaseSearch/ai_testcases/search_and_submit_play_song.txt b/Features/CaseSearch/ai_testcases/search_and_submit_play_song.txt new file mode 100644 index 000000000..42349da91 --- /dev/null +++ b/Features/CaseSearch/ai_testcases/search_and_submit_play_song.txt @@ -0,0 +1,15 @@ +Test Name: Search by Song Name and Submit Play Song Form +Suite: CaseSearch + +Steps: +1. Login as user-1 +2. Open the Music App +3. Open the Songs (Normal) menu +4. Clear selections on the case search page +5. Search for a case by song name using text input +6. Click the search button on the case search page +7. Select the case and continue to forms +8. Open the Play Song form +9. Submit the form + +Expected Result: Form submits successfully and user is returned to the app home screen \ No newline at end of file diff --git a/ai_test_generator/TESTCASE_TEMPLATE.txt b/ai_test_generator/TESTCASE_TEMPLATE.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/ai_test_generator/TESTCASE_TEMPLATE.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/ai_test_generator/process_testcases.py b/ai_test_generator/process_testcases.py new file mode 100644 index 000000000..4349a9309 --- /dev/null +++ b/ai_test_generator/process_testcases.py @@ -0,0 +1,253 @@ +""" +process_testcases.py +==================== +Scans all ai_testcases/ folders across every test suite, finds unprocessed +.txt files, and generates a pytest test file for each one. + +A .txt file is considered "processed" once a matching .py file exists next to it. + +TXT file format (save in /ai_testcases/.txt): +---------------------------------------------------------------- +Test Name: +Suite: ← optional, auto-detected from folder path + +Steps: +1. Login as user-1 +2. Open the Music App +3. Search for a case by song name using text input +4. Select the case and continue +5. Submit the Play Song form + +Expected Result: Form submits successfully and returns to the app home screen +---------------------------------------------------------------- + +Usage: + # Process all pending txt files across all suites + python ai_test_generator/process_testcases.py + + # Process a specific txt file + python ai_test_generator/process_testcases.py --file Features/CaseSearch/ai_testcases/search_by_song.txt + + # Dry run - show what would be generated without writing files + python ai_test_generator/process_testcases.py --dry-run + + # Force regenerate even if .py already exists + python ai_test_generator/process_testcases.py --force +""" + +import argparse +import os +import sys +from pathlib import Path + +ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(ROOT)) + +from ai_test_generator.generate_test import generate_test_code, _load_env_file +from ai_test_generator.scanner import SUITES, list_suites + +# Map suite folder paths back to suite names for auto-detection +SUITE_PATH_TO_NAME = {str(v.resolve()): k for k, v in SUITES.items()} + + +def detect_suite_from_path(txt_path: Path) -> str | None: + """Detect suite name by matching the txt file's parent folders against known suite paths.""" + for parent in txt_path.parents: + resolved = str(parent.resolve()) + if resolved in SUITE_PATH_TO_NAME: + return SUITE_PATH_TO_NAME[resolved] + return None + + +def parse_txt_file(txt_path: Path) -> dict: + """ + Parse a test case .txt file and return: + { + "test_name": str, + "suite": str, + "steps": str, # full description to pass to AI + "expected": str, + } + """ + content = txt_path.read_text(encoding="utf-8", errors="ignore").strip() + lines = content.splitlines() + + test_name = txt_path.stem.replace("_", " ").replace("-", " ").title() + suite = None + steps_lines = [] + expected = "" + in_steps = False + + for line in lines: + stripped = line.strip() + + if stripped.lower().startswith("test name:"): + test_name = stripped.split(":", 1)[1].strip() + elif stripped.lower().startswith("suite:"): + suite = stripped.split(":", 1)[1].strip() + elif stripped.lower().startswith("steps:") or stripped.lower() == "steps": + in_steps = True + elif stripped.lower().startswith("expected result:") or stripped.lower().startswith("expected:"): + in_steps = False + expected = stripped.split(":", 1)[1].strip() + elif in_steps and stripped: + steps_lines.append(stripped) + elif not in_steps and not suite and not stripped.lower().startswith("test name:") and stripped: + # Lines before "Steps:" header are also part of the description + steps_lines.append(stripped) + + # Auto-detect suite from path if not specified in file + if not suite: + suite = detect_suite_from_path(txt_path) + + # Build a natural language description from steps + description = f"{test_name}.\n\nSteps:\n" + "\n".join(steps_lines) + if expected: + description += f"\n\nExpected Result: {expected}" + + return { + "test_name": test_name, + "suite": suite, + "steps": description, + "expected": expected, + } + + +def find_all_txt_files() -> list[Path]: + """Find all .txt files across all ai_testcases/ folders in every suite.""" + txt_files = [] + for suite_name, suite_path in SUITES.items(): + ai_dir = suite_path / "ai_testcases" + if ai_dir.exists(): + txt_files.extend(ai_dir.glob("*.txt")) + return sorted(txt_files) + + +def output_path_for(txt_path: Path, suite_path: Path) -> Path: + """Determine where to write the generated .py file.""" + # Look for test_cases or testCases directory in the suite + for subdir in ["test_cases", "testCases"]: + tests_dir = suite_path / subdir + if tests_dir.exists(): + return tests_dir / f"test_ai_{txt_path.stem}.py" + # Fallback: write next to the txt file + return txt_path.parent / f"test_ai_{txt_path.stem}.py" + + +def process_file(txt_path: Path, dry_run: bool = False, force: bool = False) -> bool: + """ + Process a single txt file. Returns True if a test was generated. + """ + parsed = parse_txt_file(txt_path) + + if not parsed["suite"]: + print(f"[SKIP] {txt_path.name} — could not detect suite. Add 'Suite: ' to the file.") + return False + + suite_name = parsed["suite"] + if suite_name not in SUITES: + print(f"[SKIP] {txt_path.name} — unknown suite '{suite_name}'. Available: {', '.join(list_suites())}") + return False + + suite_path = SUITES[suite_name] + out_path = output_path_for(txt_path, suite_path) + + if out_path.exists() and not force: + print(f"[SKIP] {txt_path.name} — already generated ({out_path.name}). Use --force to regenerate.") + return False + + print(f"\n[GENERATE] {txt_path.name} → {out_path.relative_to(ROOT)}") + print(f" Suite : {suite_name}") + print(f" Test Name : {parsed['test_name']}") + + code = generate_test_code( + description=parsed["steps"], + suite_name=suite_name, + output_filename=str(out_path.relative_to(ROOT)), + ) + + # Strip accidental markdown fences + if code.startswith("```"): + lines = code.splitlines() + if lines[0].startswith("```"): + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + code = "\n".join(lines) + + if dry_run: + print(f"\n--- DRY RUN: would write to {out_path.relative_to(ROOT)} ---") + print(code) + print("--- END ---") + else: + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(code, encoding="utf-8") + print(f"[SUCCESS] Written: {out_path.relative_to(ROOT)}") + + return True + + +def main(): + parser = argparse.ArgumentParser( + description="Process ai_testcases/*.txt files and generate pytest test files.", + ) + parser.add_argument( + "--file", "-f", + help="Process a specific .txt file only", + default=None, + ) + parser.add_argument( + "--suite", "-s", + help="Process only txt files for a specific suite", + default=None, + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print generated code without writing files", + ) + parser.add_argument( + "--force", + action="store_true", + help="Regenerate even if .py already exists", + ) + args = parser.parse_args() + + _load_env_file() + + if args.file: + txt_path = Path(args.file) + if not txt_path.is_absolute(): + txt_path = ROOT / txt_path + if not txt_path.exists(): + print(f"[ERROR] File not found: {txt_path}") + sys.exit(1) + process_file(txt_path, dry_run=args.dry_run, force=args.force) + return + + # Scan all suites + txt_files = find_all_txt_files() + + if args.suite: + suite_path = SUITES.get(args.suite) + if not suite_path: + print(f"[ERROR] Unknown suite '{args.suite}'") + sys.exit(1) + txt_files = [f for f in txt_files if suite_path in f.parents] + + if not txt_files: + print("[INFO] No .txt files found in any ai_testcases/ folder.") + print(" Create a .txt file in /ai_testcases/ to get started.") + return + + print(f"[INFO] Found {len(txt_files)} test case file(s)") + generated = 0 + for txt_path in txt_files: + if process_file(txt_path, dry_run=args.dry_run, force=args.force): + generated += 1 + + print(f"\n[DONE] Generated {generated} test file(s).") + + +if __name__ == "__main__": + main() \ No newline at end of file From 16bb9677e8d8c4c0ad324fbae7cbe129c4356fc2 Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 16:22:02 +0530 Subject: [PATCH 04/11] feat: add ai_testcases folders to all main test suites Co-Authored-By: Claude Sonnet 4.6 --- ElasticSearchTests/ai_testcases/example_testcase.txt | 10 ++++++++++ ExportTests/ai_testcases/example_testcase.txt | 10 ++++++++++ .../DataDictionary/ai_testcases/example_testcase.txt | 10 ++++++++++ .../FindDataById/ai_testcases/example_testcase.txt | 10 ++++++++++ Features/Lookuptable/ai_testcases/example_testcase.txt | 10 ++++++++++ Features/MultiSelect/ai_testcases/example_testcase.txt | 10 ++++++++++ .../ai_testcases/example_testcase.txt | 10 ++++++++++ HQSmokeTests/ai_testcases/example_testcase.txt | 10 ++++++++++ P1P2Tests/ai_testcases/example_testcase.txt | 10 ++++++++++ RequestAPI/ai_testcases/example_testcase.txt | 10 ++++++++++ USH_Apps/CO_BHA/ai_testcases/example_testcase.txt | 10 ++++++++++ 11 files changed, 110 insertions(+) create mode 100644 ElasticSearchTests/ai_testcases/example_testcase.txt create mode 100644 ExportTests/ai_testcases/example_testcase.txt create mode 100644 Features/DataDictionary/ai_testcases/example_testcase.txt create mode 100644 Features/FindDataById/ai_testcases/example_testcase.txt create mode 100644 Features/Lookuptable/ai_testcases/example_testcase.txt create mode 100644 Features/MultiSelect/ai_testcases/example_testcase.txt create mode 100644 Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt create mode 100644 HQSmokeTests/ai_testcases/example_testcase.txt create mode 100644 P1P2Tests/ai_testcases/example_testcase.txt create mode 100644 RequestAPI/ai_testcases/example_testcase.txt create mode 100644 USH_Apps/CO_BHA/ai_testcases/example_testcase.txt diff --git a/ElasticSearchTests/ai_testcases/example_testcase.txt b/ElasticSearchTests/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/ElasticSearchTests/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/ExportTests/ai_testcases/example_testcase.txt b/ExportTests/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/ExportTests/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/Features/DataDictionary/ai_testcases/example_testcase.txt b/Features/DataDictionary/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/Features/DataDictionary/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/Features/FindDataById/ai_testcases/example_testcase.txt b/Features/FindDataById/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/Features/FindDataById/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/Features/Lookuptable/ai_testcases/example_testcase.txt b/Features/Lookuptable/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/Features/Lookuptable/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/Features/MultiSelect/ai_testcases/example_testcase.txt b/Features/MultiSelect/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/Features/MultiSelect/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt b/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/HQSmokeTests/ai_testcases/example_testcase.txt b/HQSmokeTests/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/HQSmokeTests/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/P1P2Tests/ai_testcases/example_testcase.txt b/P1P2Tests/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/P1P2Tests/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/RequestAPI/ai_testcases/example_testcase.txt b/RequestAPI/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/RequestAPI/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file diff --git a/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt b/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..84c9e2fb8 --- /dev/null +++ b/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt @@ -0,0 +1,10 @@ +Test Name: +Suite: + +Steps: +1. +2. +3. +4. + +Expected Result: \ No newline at end of file From bdb8f675532a935a7f35c9e552b36f23c89c6876 Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 16:23:32 +0530 Subject: [PATCH 05/11] feat: process all txt files across all suites, not just one - Workflow now processes every changed txt file in a push - Commits all generated .py files in a single commit - Artifact upload captures all generated tests Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ai-test-generator.yml | 77 +++++++++++++------------ 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ai-test-generator.yml b/.github/workflows/ai-test-generator.yml index 5bcb63e81..6e9b4d91c 100644 --- a/.github/workflows/ai-test-generator.yml +++ b/.github/workflows/ai-test-generator.yml @@ -95,17 +95,24 @@ jobs: - name: Generate from txt files (on push) if: github.event_name == 'push' - id: generate_push env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | - python ai_test_generator/process_testcases.py --force - GENERATED=$(git diff --name-only HEAD | grep "test_ai_" | head -1) - echo "generated_file=$GENERATED" >> $GITHUB_OUTPUT + # Find only the txt files that were added/changed in this push + CHANGED_TXT=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD | grep "ai_testcases/.*\.txt" || true) + if [ -z "$CHANGED_TXT" ]; then + echo "No new/changed txt files in this push — processing all pending files" + python ai_test_generator/process_testcases.py + else + echo "Processing changed files:" + echo "$CHANGED_TXT" + for f in $CHANGED_TXT; do + python ai_test_generator/process_testcases.py --file "$f" --force + done + fi - name: Generate from description (manual trigger) if: github.event_name == 'workflow_dispatch' - id: generate_manual env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | @@ -113,36 +120,34 @@ jobs: --suite "${{ inputs.suite }}" \ --description "${{ inputs.description }}" \ ${{ inputs.output_path != '' && format('--output "{0}"', inputs.output_path) || '' }} - GENERATED=$(find . -newer ai_test_generator/generate_test.py -name "test_*.py" \ - ! -path "./venv/*" ! -path "./.git/*" | head -1) - echo "generated_file=$GENERATED" >> $GITHUB_OUTPUT - - - name: Set generated file output - id: generate - run: | - FILE="${{ steps.generate_push.outputs.generated_file || steps.generate_manual.outputs.generated_file }}" - echo "generated_file=$FILE" >> $GITHUB_OUTPUT - - - name: Upload generated test as artifact - uses: actions/upload-artifact@v4 - with: - name: generated-test-${{ inputs.suite }}-${{ github.run_id }} - path: ${{ steps.generate.outputs.generated_file }} - retention-days: 30 - - name: Commit generated test to repository - if: ${{ inputs.commit_result == true }} + - name: Commit all generated test files run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add "${{ steps.generate.outputs.generated_file }}" - git commit -m "feat(ai-gen): add generated test for ${{ inputs.suite }} + # Stage all newly generated test files + git add '**/test_ai_*.py' '**/test_*_ai_generated.py' 2>/dev/null || true + if git diff --cached --quiet; then + echo "No new files to commit" + else + GENERATED_FILES=$(git diff --cached --name-only) + echo "Committing generated files:" + echo "$GENERATED_FILES" + git commit -m "feat(ai-gen): generate tests from ai_testcases txt files - Suite: ${{ inputs.suite }} - Description: ${{ inputs.description }} Generated by: AI Test Generator (run #${{ github.run_number }}) Triggered by: ${{ github.actor }}" - git push + git push + fi + + - name: Upload all generated tests as artifact + uses: actions/upload-artifact@v4 + with: + name: generated-tests-${{ github.run_id }} + path: | + **/test_ai_*.py + **/test_*_ai_generated.py + retention-days: 30 - name: Summary run: | @@ -150,15 +155,15 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Suite | \`${{ inputs.suite }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Description | ${{ inputs.description }} |" >> $GITHUB_STEP_SUMMARY - echo "| Generated File | \`${{ steps.generate.outputs.generated_file }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Committed | ${{ inputs.commit_result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Trigger | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY + echo "| Run | #${{ github.run_number }} |" >> $GITHUB_STEP_SUMMARY + echo "| Triggered by | ${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "### Generated Code" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`python" >> $GITHUB_STEP_SUMMARY - cat "${{ steps.generate.outputs.generated_file }}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "### Generated Files" >> $GITHUB_STEP_SUMMARY + find . -name "test_ai_*.py" -newer ai_test_generator/process_testcases.py \ + ! -path "./venv/*" ! -path "./.git/*" | while read f; do + echo "- \`$f\`" >> $GITHUB_STEP_SUMMARY + done run_generated_test: name: Run Generated Test on '${{ inputs.environment }}' From 36307a4fe366d5a66307797262cb5f2ab5f2749d Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 16:42:13 +0530 Subject: [PATCH 06/11] feat: one txt file = one test module with multiple test functions Team members can now write multiple test cases in a single txt file. Each txt file generates one .py module with test_01_, test_02_... functions, matching the existing framework structure. Co-Authored-By: Claude Sonnet 4.6 --- .../ai_testcases/example_testcase.txt | 20 +- ExportTests/ai_testcases/example_testcase.txt | 20 +- .../search_and_submit_play_song.txt | 24 ++- .../ai_testcases/example_testcase.txt | 20 +- .../ai_testcases/example_testcase.txt | 20 +- .../ai_testcases/example_testcase.txt | 20 +- .../ai_testcases/example_testcase.txt | 20 +- .../ai_testcases/example_testcase.txt | 20 +- .../ai_testcases/example_testcase.txt | 20 +- P1P2Tests/ai_testcases/example_testcase.txt | 20 +- RequestAPI/ai_testcases/example_testcase.txt | 20 +- .../CO_BHA/ai_testcases/example_testcase.txt | 20 +- ai_test_generator/TESTCASE_TEMPLATE.txt | 20 +- ai_test_generator/process_testcases.py | 201 +++++++++++------- 14 files changed, 324 insertions(+), 141 deletions(-) diff --git a/ElasticSearchTests/ai_testcases/example_testcase.txt b/ElasticSearchTests/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/ElasticSearchTests/ai_testcases/example_testcase.txt +++ b/ElasticSearchTests/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/ExportTests/ai_testcases/example_testcase.txt b/ExportTests/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/ExportTests/ai_testcases/example_testcase.txt +++ b/ExportTests/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/Features/CaseSearch/ai_testcases/search_and_submit_play_song.txt b/Features/CaseSearch/ai_testcases/search_and_submit_play_song.txt index 42349da91..328f75f69 100644 --- a/Features/CaseSearch/ai_testcases/search_and_submit_play_song.txt +++ b/Features/CaseSearch/ai_testcases/search_and_submit_play_song.txt @@ -1,15 +1,31 @@ -Test Name: Search by Song Name and Submit Play Song Form Suite: CaseSearch -Steps: +Test 1: Search by song name and submit Play Song form 1. Login as user-1 2. Open the Music App 3. Open the Songs (Normal) menu 4. Clear selections on the case search page 5. Search for a case by song name using text input -6. Click the search button on the case search page +6. Click the search button 7. Select the case and continue to forms 8. Open the Play Song form 9. Submit the form +Expected Result: Form submits successfully and user is returned to the app home screen -Expected Result: Form submits successfully and user is returned to the app home screen \ No newline at end of file +Test 2: Search with no results shows empty list +1. Login as user-1 +2. Open the Music App +3. Open the Songs (Normal) menu +4. Search for a non-existent song name +5. Click the search button +Expected Result: Case list is empty with appropriate message + +Test 3: Search using combobox filter +1. Login as user-2 +2. Open the Music App +3. Open the Songs (Normal) menu +4. Clear selections on the case search page +5. Filter cases using a combobox property +6. Click the search button +7. Verify only matching cases appear in the list +Expected Result: Filtered case list shows only relevant results \ No newline at end of file diff --git a/Features/DataDictionary/ai_testcases/example_testcase.txt b/Features/DataDictionary/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/Features/DataDictionary/ai_testcases/example_testcase.txt +++ b/Features/DataDictionary/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/Features/FindDataById/ai_testcases/example_testcase.txt b/Features/FindDataById/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/Features/FindDataById/ai_testcases/example_testcase.txt +++ b/Features/FindDataById/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/Features/Lookuptable/ai_testcases/example_testcase.txt b/Features/Lookuptable/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/Features/Lookuptable/ai_testcases/example_testcase.txt +++ b/Features/Lookuptable/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/Features/MultiSelect/ai_testcases/example_testcase.txt b/Features/MultiSelect/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/Features/MultiSelect/ai_testcases/example_testcase.txt +++ b/Features/MultiSelect/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt b/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt +++ b/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/HQSmokeTests/ai_testcases/example_testcase.txt b/HQSmokeTests/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/HQSmokeTests/ai_testcases/example_testcase.txt +++ b/HQSmokeTests/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/P1P2Tests/ai_testcases/example_testcase.txt b/P1P2Tests/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/P1P2Tests/ai_testcases/example_testcase.txt +++ b/P1P2Tests/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/RequestAPI/ai_testcases/example_testcase.txt b/RequestAPI/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/RequestAPI/ai_testcases/example_testcase.txt +++ b/RequestAPI/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt b/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt index 84c9e2fb8..e256dccb4 100644 --- a/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt +++ b/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/ai_test_generator/TESTCASE_TEMPLATE.txt b/ai_test_generator/TESTCASE_TEMPLATE.txt index 84c9e2fb8..e256dccb4 100644 --- a/ai_test_generator/TESTCASE_TEMPLATE.txt +++ b/ai_test_generator/TESTCASE_TEMPLATE.txt @@ -1,10 +1,20 @@ -Test Name: -Suite: +Suite: -Steps: -1. +Test 1: +1. 2. 3. 4. +Expected Result: -Expected Result: \ No newline at end of file +Test 2: +1. +2. +3. +Expected Result: + +Test 3: +1. +2. +3. +Expected Result: \ No newline at end of file diff --git a/ai_test_generator/process_testcases.py b/ai_test_generator/process_testcases.py index 4349a9309..6e60937c3 100644 --- a/ai_test_generator/process_testcases.py +++ b/ai_test_generator/process_testcases.py @@ -2,31 +2,46 @@ process_testcases.py ==================== Scans all ai_testcases/ folders across every test suite, finds unprocessed -.txt files, and generates a pytest test file for each one. +.txt files, and generates a pytest test MODULE for each one. -A .txt file is considered "processed" once a matching .py file exists next to it. +Each .txt file = one test module (.py file) with multiple test functions. -TXT file format (save in /ai_testcases/.txt): ----------------------------------------------------------------- -Test Name: -Suite: ← optional, auto-detected from folder path +TXT file format (save in /ai_testcases/.txt): +------------------------------------------------------------------ +Suite: CaseSearch -Steps: +Test 1: Search by song name and submit form 1. Login as user-1 2. Open the Music App 3. Search for a case by song name using text input 4. Select the case and continue 5. Submit the Play Song form +Expected Result: Form submits successfully -Expected Result: Form submits successfully and returns to the app home screen ----------------------------------------------------------------- +Test 2: Search with no results returns empty list +1. Login as user-1 +2. Open the Music App +3. Search for a non-existent song name +4. Verify the list shows empty message +Expected Result: Case list is empty + +Test 3: Search using combobox filter +1. Login as user-2 +2. Open the Music App +3. Filter cases using a combobox property +4. Verify filtered results appear +Expected Result: Only matching cases appear in the list +------------------------------------------------------------------ + +Generated output goes to: /test_cases/test_ai_.py + → Contains test_01_..., test_02_..., test_03_... functions Usage: # Process all pending txt files across all suites python ai_test_generator/process_testcases.py # Process a specific txt file - python ai_test_generator/process_testcases.py --file Features/CaseSearch/ai_testcases/search_by_song.txt + python ai_test_generator/process_testcases.py --file Features/CaseSearch/ai_testcases/casesearch_workflows.txt # Dry run - show what would be generated without writing files python ai_test_generator/process_testcases.py --dry-run @@ -37,6 +52,7 @@ import argparse import os +import re import sys from pathlib import Path @@ -61,89 +77,133 @@ def detect_suite_from_path(txt_path: Path) -> str | None: def parse_txt_file(txt_path: Path) -> dict: """ - Parse a test case .txt file and return: + Parse a test case .txt file supporting multiple test cases per file. + + Returns: { - "test_name": str, "suite": str, - "steps": str, # full description to pass to AI - "expected": str, + "module_name": str, # derived from filename + "tests": [ + { + "name": str, # e.g. "Search by song name and submit form" + "steps": list, # ["1. Login as user-1", "2. Open the Music App", ...] + "expected": str, + }, + ... + ] } """ content = txt_path.read_text(encoding="utf-8", errors="ignore").strip() lines = content.splitlines() - test_name = txt_path.stem.replace("_", " ").replace("-", " ").title() suite = None - steps_lines = [] - expected = "" - in_steps = False + tests = [] + current_test = None for line in lines: stripped = line.strip() + if not stripped: + continue - if stripped.lower().startswith("test name:"): - test_name = stripped.split(":", 1)[1].strip() - elif stripped.lower().startswith("suite:"): + # Suite header + if stripped.lower().startswith("suite:"): suite = stripped.split(":", 1)[1].strip() - elif stripped.lower().startswith("steps:") or stripped.lower() == "steps": - in_steps = True - elif stripped.lower().startswith("expected result:") or stripped.lower().startswith("expected:"): - in_steps = False - expected = stripped.split(":", 1)[1].strip() - elif in_steps and stripped: - steps_lines.append(stripped) - elif not in_steps and not suite and not stripped.lower().startswith("test name:") and stripped: - # Lines before "Steps:" header are also part of the description - steps_lines.append(stripped) - - # Auto-detect suite from path if not specified in file + continue + + # New test block: "Test 1:", "Test 2:", "Test:", etc. + test_header = re.match(r'^test\s*\d*\s*:\s*(.+)$', stripped, re.IGNORECASE) + if test_header: + if current_test: + tests.append(current_test) + current_test = { + "name": test_header.group(1).strip(), + "steps": [], + "expected": "", + } + continue + + # Expected result line (within a test block) + if current_test and re.match(r'^expected\s*(result)?\s*:', stripped, re.IGNORECASE): + current_test["expected"] = stripped.split(":", 1)[1].strip() + continue + + # Numbered step line (1. ... or 1) ...) + if current_test and re.match(r'^\d+[\.\)]\s+', stripped): + current_test["steps"].append(stripped) + continue + + # Plain text inside a test block (treat as a step) + if current_test and stripped: + current_test["steps"].append(stripped) + + # Don't forget the last test + if current_test: + tests.append(current_test) + + # Auto-detect suite from path if not in file if not suite: suite = detect_suite_from_path(txt_path) - # Build a natural language description from steps - description = f"{test_name}.\n\nSteps:\n" + "\n".join(steps_lines) - if expected: - description += f"\n\nExpected Result: {expected}" - return { - "test_name": test_name, "suite": suite, - "steps": description, - "expected": expected, + "module_name": txt_path.stem, + "tests": tests, } +def build_module_description(parsed: dict) -> str: + """Build a single description string covering all test cases for the AI prompt.""" + lines = [f"Generate a complete pytest test MODULE named test_ai_{parsed['module_name']}.py"] + lines.append(f"The module should contain {len(parsed['tests'])} test function(s), numbered test_01_, test_02_, etc.") + lines.append("") + + for i, test in enumerate(parsed["tests"], start=1): + lines.append(f"--- Test {i}: {test['name']} ---") + lines.append("Steps:") + for step in test["steps"]: + lines.append(f" {step}") + if test["expected"]: + lines.append(f"Expected Result: {test['expected']}") + lines.append("") + + return "\n".join(lines) + + def find_all_txt_files() -> list[Path]: """Find all .txt files across all ai_testcases/ folders in every suite.""" txt_files = [] for suite_name, suite_path in SUITES.items(): ai_dir = suite_path / "ai_testcases" if ai_dir.exists(): - txt_files.extend(ai_dir.glob("*.txt")) + txt_files.extend( + f for f in ai_dir.glob("*.txt") + if f.name != "example_testcase.txt" # skip the template + ) return sorted(txt_files) def output_path_for(txt_path: Path, suite_path: Path) -> Path: - """Determine where to write the generated .py file.""" - # Look for test_cases or testCases directory in the suite + """Determine the output .py path in the suite's test_cases/ folder.""" for subdir in ["test_cases", "testCases"]: tests_dir = suite_path / subdir if tests_dir.exists(): return tests_dir / f"test_ai_{txt_path.stem}.py" - # Fallback: write next to the txt file + # Fallback: same folder as txt return txt_path.parent / f"test_ai_{txt_path.stem}.py" def process_file(txt_path: Path, dry_run: bool = False, force: bool = False) -> bool: - """ - Process a single txt file. Returns True if a test was generated. - """ + """Process a single txt file → generate a test module. Returns True if generated.""" parsed = parse_txt_file(txt_path) if not parsed["suite"]: print(f"[SKIP] {txt_path.name} — could not detect suite. Add 'Suite: ' to the file.") return False + if not parsed["tests"]: + print(f"[SKIP] {txt_path.name} — no test cases found. Use 'Test 1: ' to define tests.") + return False + suite_name = parsed["suite"] if suite_name not in SUITES: print(f"[SKIP] {txt_path.name} — unknown suite '{suite_name}'. Available: {', '.join(list_suites())}") @@ -153,15 +213,19 @@ def process_file(txt_path: Path, dry_run: bool = False, force: bool = False) -> out_path = output_path_for(txt_path, suite_path) if out_path.exists() and not force: - print(f"[SKIP] {txt_path.name} — already generated ({out_path.name}). Use --force to regenerate.") + print(f"[SKIP] {txt_path.name} — {out_path.name} already exists. Use --force to regenerate.") return False print(f"\n[GENERATE] {txt_path.name} → {out_path.relative_to(ROOT)}") print(f" Suite : {suite_name}") - print(f" Test Name : {parsed['test_name']}") + print(f" Test cases : {len(parsed['tests'])}") + for i, t in enumerate(parsed["tests"], 1): + print(f" {i}. {t['name']}") + + description = build_module_description(parsed) code = generate_test_code( - description=parsed["steps"], + description=description, suite_name=suite_name, output_filename=str(out_path.relative_to(ROOT)), ) @@ -189,28 +253,12 @@ def process_file(txt_path: Path, dry_run: bool = False, force: bool = False) -> def main(): parser = argparse.ArgumentParser( - description="Process ai_testcases/*.txt files and generate pytest test files.", - ) - parser.add_argument( - "--file", "-f", - help="Process a specific .txt file only", - default=None, - ) - parser.add_argument( - "--suite", "-s", - help="Process only txt files for a specific suite", - default=None, - ) - parser.add_argument( - "--dry-run", - action="store_true", - help="Print generated code without writing files", - ) - parser.add_argument( - "--force", - action="store_true", - help="Regenerate even if .py already exists", + description="Process ai_testcases/*.txt files and generate pytest test modules.", ) + parser.add_argument("--file", "-f", help="Process a specific .txt file only", default=None) + parser.add_argument("--suite", "-s", help="Process only txt files for a specific suite", default=None) + parser.add_argument("--dry-run", action="store_true", help="Print generated code without writing files") + parser.add_argument("--force", action="store_true", help="Regenerate even if .py already exists") args = parser.parse_args() _load_env_file() @@ -225,7 +273,6 @@ def main(): process_file(txt_path, dry_run=args.dry_run, force=args.force) return - # Scan all suites txt_files = find_all_txt_files() if args.suite: @@ -236,17 +283,17 @@ def main(): txt_files = [f for f in txt_files if suite_path in f.parents] if not txt_files: - print("[INFO] No .txt files found in any ai_testcases/ folder.") - print(" Create a .txt file in /ai_testcases/ to get started.") + print("[INFO] No txt files found in any ai_testcases/ folder.") + print(" Create a .txt file using the format in ai_test_generator/TESTCASE_TEMPLATE.txt") return - print(f"[INFO] Found {len(txt_files)} test case file(s)") + print(f"[INFO] Found {len(txt_files)} test case file(s) to process") generated = 0 for txt_path in txt_files: if process_file(txt_path, dry_run=args.dry_run, force=args.force): generated += 1 - print(f"\n[DONE] Generated {generated} test file(s).") + print(f"\n[DONE] Generated {generated} test module(s).") if __name__ == "__main__": From a688f712cf5f766cf9e89f81f695125af57076aa Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 16:47:13 +0530 Subject: [PATCH 07/11] docs: add README and HQSmokeTests sample txt file - ai_test_generator/README.md: full guide for team members - HQSmokeTests/ai_testcases/reports_and_users.txt: real example with 5 tests Co-Authored-By: Claude Sonnet 4.6 --- .../ai_testcases/reports_and_users.txt | 48 +++++ ai_test_generator/README.md | 177 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 HQSmokeTests/ai_testcases/reports_and_users.txt create mode 100644 ai_test_generator/README.md diff --git a/HQSmokeTests/ai_testcases/reports_and_users.txt b/HQSmokeTests/ai_testcases/reports_and_users.txt new file mode 100644 index 000000000..152a35cd3 --- /dev/null +++ b/HQSmokeTests/ai_testcases/reports_and_users.txt @@ -0,0 +1,48 @@ +Suite: HQSmokeTests + +Test 1: Verify all report sections are displayed and load correctly +1. Navigate to the HQ home page +2. Click on the Reports menu +3. Verify Monitor Workers section is displayed +4. Verify Inspect Data section is displayed +5. Verify Manage Deployments section is displayed +6. Verify Messaging section is displayed +7. Open and run the Worker Activity report +8. Open and run the Daily Form Activity report +9. Open and run the Case Activity report +10. Open and run the Submit History report +Expected Result: All report sections are visible and each report loads with data + +Test 2: Create a new mobile worker and verify it appears in the list +1. Navigate to Users menu +2. Click on Mobile Workers +3. Click Add Mobile Worker +4. Enter a unique username +5. Enter a password +6. Save the new mobile worker +7. Search for the newly created worker in the list +Expected Result: New mobile worker is created and visible in the mobile workers list + +Test 3: Create a user group and add a mobile worker to it +1. Navigate to Users menu +2. Click on Groups +3. Create a new group with a unique name +4. Add an existing mobile worker to the group +5. Save the group +6. Verify the group appears in the groups list +Expected Result: Group is created and the mobile worker is assigned to it + +Test 4: Verify export data functionality works +1. Navigate to Data menu +2. Click on Export Data +3. Create a new case export +4. Verify the export appears in the exports list +5. Download the export file +Expected Result: Export file is created and downloaded successfully + +Test 5: Verify application is accessible via Web Apps +1. Navigate to Web Apps +2. Verify the list of applications is displayed +3. Open one of the available applications +4. Verify the application menus are displayed +Expected Result: Application loads correctly in Web Apps \ No newline at end of file diff --git a/ai_test_generator/README.md b/ai_test_generator/README.md new file mode 100644 index 000000000..bda6d9351 --- /dev/null +++ b/ai_test_generator/README.md @@ -0,0 +1,177 @@ +# AI Test Generator + +Automatically generates ready-to-run pytest test files from plain English test descriptions. +Non-technical team members write simple steps in a `.txt` file — the AI writes the code. + +--- + +## How It Works + +``` +You write a .txt file → AI reads it → .py test module is created +HQSmokeTests/ (OpenAI) HQSmokeTests/ + ai_testcases/ testCases/ + reports.txt test_ai_reports.py + Test 1: ... def test_01_... + Test 2: ... def test_02_... +``` + +The generated `.py` file is placed in the suite's existing `testCases/` or `test_cases/` folder +and follows the exact same structure as your hand-written tests — same imports, fixtures, +page objects, and naming conventions. + +--- + +## Quick Start (Local) + +### Step 1 — Install the dependency +```bash +pip install openai +``` + +### Step 2 — Add your OpenAI API key to the `.env` file +Open `ai_test_generator/.env` and set: +``` +OPENAI_API_KEY=sk-proj-your-key-here +``` + +### Step 3 — Write your test cases in a `.txt` file + +Go to your suite's `ai_testcases/` folder (e.g. `HQSmokeTests/ai_testcases/`), +copy `example_testcase.txt`, rename it, and fill in your steps: + +``` +Suite: HQSmokeTests + +Test 1: Verify reports module loads all sections +1. Navigate to Reports +2. Click View All +3. Verify Monitor Workers section is displayed +4. Verify Inspect Data section is displayed +Expected Result: All report sections are visible + +Test 2: Create a new mobile worker +1. Navigate to Users > Mobile Workers +2. Click Add Mobile Worker +3. Enter a username and password +4. Save the new worker +Expected Result: Worker is created and appears in the list +``` + +### Step 4 — Run the generator +```bash +# Generate from a specific txt file +python ai_test_generator/process_testcases.py --file HQSmokeTests/ai_testcases/reports.txt + +# Generate from ALL txt files across ALL suites at once +python ai_test_generator/process_testcases.py + +# Preview without writing the file +python ai_test_generator/process_testcases.py --file HQSmokeTests/ai_testcases/reports.txt --dry-run + +# Regenerate an already-generated file +python ai_test_generator/process_testcases.py --file HQSmokeTests/ai_testcases/reports.txt --force +``` + +### Step 5 — Review and run the generated test +```bash +pytest HQSmokeTests/testCases/test_ai_reports.py -v +``` + +--- + +## GitHub Actions (No Local Setup Needed) + +Your team members can generate tests directly from GitHub without any local setup. + +### Option A — Push a txt file (automatic) +1. Add or edit a `.txt` file in any suite's `ai_testcases/` folder +2. Push/commit to GitHub +3. The **AI Test Generator** workflow triggers automatically +4. The generated `.py` file is committed back to the repo + +### Option B — Run manually from GitHub UI +1. Go to **Actions** tab on GitHub +2. Select **AI Test Generator** from the left sidebar +3. Click **Run workflow** +4. Choose the suite, type your test description, click **Run workflow** + +--- + +## Available Suites + +| Suite Name | Folder | +|---|---| +| `CaseSearch` | `Features/CaseSearch/` | +| `DataDictionary` | `Features/DataDictionary/` | +| `FindDataById` | `Features/FindDataById/` | +| `Lookuptable` | `Features/Lookuptable/` | +| `MultiSelect` | `Features/MultiSelect/` | +| `PowerBI` | `Features/Powerbi_integration_exports/` | +| `SplitScreenCaseSearch` | `Features/SplitScreenCaseSearch/` | +| `ElasticSearch` | `ElasticSearchTests/` | +| `ExportTests` | `ExportTests/` | +| `Formplayer` | `Formplayer/` | +| `HQSmokeTests` | `HQSmokeTests/` | +| `P1P2Tests` | `P1P2Tests/` | +| `RequestAPI` | `RequestAPI/` | +| `USH_CO_BHA` | `USH_Apps/CO_BHA/` | +| `BHAStressTest` | `QA_Requests/BHAStressTest/` | + +--- + +## txt File Format + +``` +Suite: + +Test 1: +1. +2. +3. +Expected Result: + +Test 2: +1. +2. +Expected Result: +``` + +**Rules:** +- `Suite:` line is required (or place the file in the correct `ai_testcases/` folder — it auto-detects) +- Each test block starts with `Test 1:`, `Test 2:`, etc. +- Steps can be numbered (`1.`) or plain bullet points +- `Expected Result:` is optional but recommended +- One `.txt` file → one `.py` test module with multiple test functions + +--- + +## File Structure + +``` +ai_test_generator/ +├── generate_test.py # Core generator — calls OpenAI API +├── process_testcases.py # Processes txt files → generates test modules +├── scanner.py # Scans page objects and user inputs from the framework +├── requirements.txt # openai>=1.0.0 +├── .env # Your API key (never committed) +├── .env.example # Template for the .env file +├── TESTCASE_TEMPLATE.txt # Blank template to copy into ai_testcases/ +└── README.md # This file + +Each suite/ +└── ai_testcases/ + ├── example_testcase.txt # Template showing the format + └── your_tests.txt # Your test cases → generates test_ai_your_tests.py +``` + +--- + +## Tips + +- **Be specific in your steps** — mention usernames, menu names, form names, and field names + that exist in your application. The AI uses these to pick the right page object methods. +- **One txt file per feature area** — e.g. `reports.txt`, `mobile_workers.txt`, `exports.txt` +- **Review the generated file** before running — the AI may occasionally use a placeholder + locator if a specific UI element isn't in the existing page objects. Add it manually if needed. +- **The generator never modifies existing test files** — it only creates new `test_ai_*.py` files. \ No newline at end of file From e3883213c0839308eb7a4f9b2f2c00a4e0515f23 Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 16:50:26 +0530 Subject: [PATCH 08/11] feat: generate test_ai_reports_and_users.py for HQSmokeTests Fix Windows encoding issue in process_testcases.py Co-Authored-By: Claude Sonnet 4.6 --- .../testCases/test_ai_reports_and_users.py | 142 ++++++++++++++++++ ai_test_generator/process_testcases.py | 2 +- 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 HQSmokeTests/testCases/test_ai_reports_and_users.py diff --git a/HQSmokeTests/testCases/test_ai_reports_and_users.py b/HQSmokeTests/testCases/test_ai_reports_and_users.py new file mode 100644 index 000000000..6e8f3aafd --- /dev/null +++ b/HQSmokeTests/testCases/test_ai_reports_and_users.py @@ -0,0 +1,142 @@ +""" +This module tests report visibility and loading, user creation, +user group management, export functionality, and web app access. +""" + +import pytest +from common_utilities.selenium.webapps import WebApps +from common_utilities.hq_login.login_page import LoginPage +from HQSmokeTests.testPages.reports.report_page import ReportPage +from HQSmokeTests.testPages.home.home_page import HomePage +from HQSmokeTests.testPages.users.mobile_workers_page import MobileWorkerPage +from HQSmokeTests.testPages.users.group_page import GroupPage +from HQSmokeTests.testPages.data.export_data_page import ExportDataPage +from HQSmokeTests.testPages.webapps.web_apps_page import WebAppsPage +from HQSmokeTests.userInputs.user_inputs import UserData + +@pytest.mark.reports +def test_01_verify_report_sections_displayed(driver, settings): + """Verify all report sections are displayed and load correctly.""" + print("Step 1: Navigate to the HQ home page") + home_page = HomePage(driver, settings) + home_page.reports_menu() + + reports_page = ReportPage(driver, settings) + + print("Step 3: Verify Monitor Workers section is displayed") + assert home_page.is_present_and_displayed(reports_page.get_element("locator_for_monitor_workers")), "Monitor Workers section not found" + + print("Step 4: Verify Inspect Data section is displayed") + assert home_page.is_present_and_displayed(reports_page.get_element("locator_for_inspect_data")), "Inspect Data section not found" + + print("Step 5: Verify Manage Deployments section is displayed") + assert home_page.is_present_and_displayed(reports_page.get_element("locator_for_manage_deployments")), "Manage Deployments section not found" + + print("Step 6: Verify Messaging section is displayed") + assert home_page.is_present_and_displayed(reports_page.get_element("locator_for_messaging")), "Messaging section not found" + + print("Step 7: Open and run the Worker Activity report") + reports_page.worker_activity_report() + assert reports_page.check_if_report_loaded(), "Worker Activity report failed to load data" + + print("Step 8: Open and run the Daily Form Activity report") + reports_page.daily_form_activity_report() + assert reports_page.check_if_report_loaded(), "Daily Form Activity report failed to load data" + + print("Step 9: Open and run the Case Activity report") + reports_page.case_activity_report() + assert reports_page.check_if_report_loaded(), "Case Activity report failed to load data" + + print("Step 10: Open and run the Submit History report") + reports_page.submit_history_report() + assert reports_page.check_if_report_loaded(), "Submit History report failed to load data" + +@pytest.mark.users +def test_02_create_mobile_worker(driver, settings): + """Create a new mobile worker and verify it appears in the list.""" + home_page = HomePage(driver, settings) + home_page.users_menu() + + mobile_worker_page = MobileWorkerPage(driver, settings) + + print("Step 2: Click on Mobile Workers") + mobile_worker_page.mobile_worker_menu() + + print("Step 3: Click Add Mobile Worker") + mobile_worker_page.create_mobile_worker() + + username = f"user_{str(int(time.time()))}" + password = UserData.app_password + + print(f"Step 4: Enter a unique username: {username}") + mobile_worker_page.mobile_worker_enter_username(username) + + print("Step 5: Enter a password") + mobile_worker_page.mobile_worker_enter_password(password) + + print("Step 6: Save the new mobile worker") + mobile_worker_page.click_create(username) + + print("Step 7: Search for the newly created worker in the list") + assert mobile_worker_page.search_user(username), f"Mobile worker {username} not found in the list" + +@pytest.mark.users +def test_03_create_user_group_and_add_worker(driver, settings): + """Create a user group and add a mobile worker to it.""" + home_page = HomePage(driver, settings) + home_page.users_menu() + + group_page = GroupPage(driver, settings) + + print("Step 2: Click on Groups") + group_page.click_group_menu() + + group_name = f"group_{str(int(time.time()))}" + print(f"Step 3: Create a new group with a unique name: {group_name}") + group_page.add_group(group_name) + + username = UserData.app_login + print(f"Step 4: Add an existing mobile worker {username} to the group") + group_page.add_user_to_group(username, group_name) + + print("Step 5: Save the group") + print("Step 6: Verify the group appears in the groups list") + assert group_page.is_present_and_displayed(group_page.get_element("locator_for_group", group_name)), f"Group {group_name} not found" + +@pytest.mark.data +def test_04_verify_export_data_functionality(driver, settings): + """Verify export data functionality works.""" + home_page = HomePage(driver, settings) + home_page.data_menu() + + export_data_page = ExportDataPage(driver, settings) + + print("Step 2: Click on Export Data") + export_data_page.prepare_and_download_export(UserData.case_export_name) + + print("Step 3: Create a new case export") + export_data_page.add_case_exports() + + print("Step 4: Verify the export appears in the exports list") + assert export_data_page.verify_export_count(UserData.case_export_name), "Export not found in the list" + + print("Step 5: Download the export file") + export_data_page.create_dse_and_download(UserData.case_export_name, type="case") + +@pytest.mark.webApps +def test_05_verify_application_access_via_web_apps(driver, settings): + """Verify application is accessible via Web Apps.""" + home_page = HomePage(driver, settings) + home_page.web_apps_menu() + + webapps_page = WebAppsPage(driver, settings) + + print("Step 2: Verify the list of applications is displayed") + assert webapps_page.verify_apps_presence(), "List of applications not displayed" + + app_name = UserData.village_application + print(f"Step 3: Open the available application: {app_name}") + webapps_page.login_as(app_name) + + print("Step 4: Verify the application menus are displayed") + assert webapps_page.is_present_and_displayed(webapps_page.get_element("locator_for_app_menus")), "Application menus not displayed" \ No newline at end of file diff --git a/ai_test_generator/process_testcases.py b/ai_test_generator/process_testcases.py index 6e60937c3..af7902b36 100644 --- a/ai_test_generator/process_testcases.py +++ b/ai_test_generator/process_testcases.py @@ -216,7 +216,7 @@ def process_file(txt_path: Path, dry_run: bool = False, force: bool = False) -> print(f"[SKIP] {txt_path.name} — {out_path.name} already exists. Use --force to regenerate.") return False - print(f"\n[GENERATE] {txt_path.name} → {out_path.relative_to(ROOT)}") + print(f"\n[GENERATE] {txt_path.name} -> {out_path.relative_to(ROOT)}") print(f" Suite : {suite_name}") print(f" Test cases : {len(parsed['tests'])}") for i, t in enumerate(parsed["tests"], 1): From 684d6868b2cbe96f3f5e52c06f05f986cd13913c Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 16:59:03 +0530 Subject: [PATCH 09/11] docs: add FAQ.md for AI test generator Co-Authored-By: Claude Sonnet 4.6 --- ai_test_generator/FAQ.md | 154 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 ai_test_generator/FAQ.md diff --git a/ai_test_generator/FAQ.md b/ai_test_generator/FAQ.md new file mode 100644 index 000000000..3ddab194e --- /dev/null +++ b/ai_test_generator/FAQ.md @@ -0,0 +1,154 @@ +# AI Test Generator — FAQ + +--- + +### What does the AI agent actually do? + +It generates a ready-to-run pytest test file (`.py`) from plain English steps written in a `.txt` file. +It does **not** run the tests. Your existing GitHub Actions workflows handle test execution on their normal schedule. + +--- + +### How does the agent know which function to call? + +The agent scans all `testPages/` files in your suite using Python's AST parser and extracts every class name and method signature. That full list is sent to the AI along with your description. The AI then matches your plain English steps to the closest matching method. + +For example, when you write: +``` +3. Open the Worker Activity report +``` +The scanner has already told the AI that `ReportPage` has a method called `worker_activity_report()` — so it calls exactly that. + +--- + +### If I add new functionality, do I need to update the agent? + +No configuration needed. Once a developer adds a new method to a page object in `testPages/`, the scanner automatically picks it up the next time the agent runs. The agent always reflects the latest state of your page objects. + +| Task | Who does it | +|---|---| +| Write page object methods + locators | Developer (in `testPages/`) | +| Write test steps in plain English | Anyone (in `ai_testcases/*.txt`) | +| Generate the test function code | Agent (automatically) | + +--- + +### Can the agent generate page object methods and locators too? + +No. The agent cannot see your actual UI or browser, so it cannot generate accurate XPath/CSS locators. Page object methods need to be written by a developer who knows the UI structure. + +Once those methods exist in `testPages/`, the agent can use them freely in generated tests. + +--- + +### One txt file or one test per file? + +One `.txt` file = one `.py` test module with multiple test functions. + +``` +HQSmokeTests/ai_testcases/reports.txt → HQSmokeTests/testCases/test_ai_reports.py + Test 1: Verify report sections def test_01_verify_report_sections(...) + Test 2: Run Worker Activity report def test_02_run_worker_activity_report(...) + Test 3: Run Case Activity report def test_03_run_case_activity_report(...) +``` + +Organise your txt files by feature area — e.g. `reports.txt`, `mobile_workers.txt`, `exports.txt`. + +--- + +### Will the generated test run perfectly straight away? + +Mostly yes, but review it first. The agent uses real method names from your page objects, so the structure and logic will be correct. Occasionally, when a specific UI element has no matching method in the page objects, the AI inserts a placeholder like: + +```python +page.get_element("locator_for_monitor_workers") +``` + +A developer needs to replace these placeholders with the real locator from the page object. Everything else should be runnable as-is. + +--- + +### Does it modify my existing test files? + +Never. The agent only creates new files named `test_ai_.py`. Your existing hand-written test files are never touched. + +--- + +### How do I trigger generation locally? + +```bash +# Single txt file +python ai_test_generator/process_testcases.py --file HQSmokeTests/ai_testcases/reports.txt + +# All txt files across all suites at once +python ai_test_generator/process_testcases.py + +# Preview without writing the file +python ai_test_generator/process_testcases.py --file HQSmokeTests/ai_testcases/reports.txt --dry-run + +# Regenerate a file that already exists +python ai_test_generator/process_testcases.py --file HQSmokeTests/ai_testcases/reports.txt --force +``` + +--- + +### How do I trigger generation from GitHub (no local setup)? + +**Option A — Push a txt file (automatic):** +1. Add or edit a `.txt` file in any suite's `ai_testcases/` folder +2. Push/commit to GitHub +3. The **AI Test Generator** workflow triggers automatically +4. The generated `.py` file is committed back to the repo + +**Option B — Run manually from GitHub UI:** +1. Go to **Actions** tab on GitHub +2. Select **AI Test Generator** from the left sidebar +3. Click **Run workflow** +4. Choose the suite, type your description, click **Run workflow** + +--- + +### Do I need a different workflow for each test suite? + +No. The single `ai-test-generator.yml` workflow handles all 13+ suites. It detects which suite a txt file belongs to automatically from the folder path. + +--- + +### What if I add a new test suite in the future? + +A developer needs to add the new suite to the `SUITES` dictionary in `ai_test_generator/scanner.py` and create an `ai_testcases/` folder in the suite directory. After that, it works the same as all other suites. + +--- + +### How much does it cost to generate a test? + +Each test generation costs approximately **$0.01–$0.03** using GPT-4o. This only applies when generating — your daily test suite runs (Selenium/pytest) are unaffected and cost nothing extra. + +--- + +### Where is the API key stored? + +- **Locally:** `ai_test_generator/.env` — this file is in `.gitignore` and is never committed +- **GitHub Actions:** stored as a repository secret named `OPENAI_API_KEY` (Settings → Secrets and variables → Actions) + +--- + +### What suites are supported? + +| Suite Name | Folder | +|---|---| +| `CaseSearch` | `Features/CaseSearch/` | +| `DataDictionary` | `Features/DataDictionary/` | +| `FindDataById` | `Features/FindDataById/` | +| `Lookuptable` | `Features/Lookuptable/` | +| `MultiSelect` | `Features/MultiSelect/` | +| `PowerBI` | `Features/Powerbi_integration_exports/` | +| `SplitScreenCaseSearch` | `Features/SplitScreenCaseSearch/` | +| `ElasticSearch` | `ElasticSearchTests/` | +| `ExportTests` | `ExportTests/` | +| `Formplayer` | `Formplayer/` | +| `HQSmokeTests` | `HQSmokeTests/` | +| `P1P2Tests` | `P1P2Tests/` | +| `RequestAPI` | `RequestAPI/` | +| `USH_CO_BHA` | `USH_Apps/CO_BHA/` | +| `BHAStressTest` | `QA_Requests/BHAStressTest/` | From ab11cf98425cf6d67a5f71a27c0bf0053f33ccc0 Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 17:09:49 +0530 Subject: [PATCH 10/11] fix: read exact constructor signature from each page class Scanner now extracts __init__ args per class and surfaces them as 'INSTANTIATE AS: ClassName(arg1, arg2)' in the AI context. System prompt rule added to enforce exact constructor usage. No more TypeError from wrong number of arguments. Co-Authored-By: Claude Sonnet 4.6 --- .../testCases/test_ai_reports_and_users.py | 179 ++++++++---------- ai_test_generator/generate_test.py | 8 + ai_test_generator/scanner.py | 19 +- 3 files changed, 108 insertions(+), 98 deletions(-) diff --git a/HQSmokeTests/testCases/test_ai_reports_and_users.py b/HQSmokeTests/testCases/test_ai_reports_and_users.py index 6e8f3aafd..387b1a04e 100644 --- a/HQSmokeTests/testCases/test_ai_reports_and_users.py +++ b/HQSmokeTests/testCases/test_ai_reports_and_users.py @@ -1,142 +1,129 @@ """ -This module tests report visibility and loading, user creation, -user group management, export functionality, and web app access. +This module contains tests for verifying the correct display and functionality of report sections, +the creation of mobile workers and user groups, the export data functionality, and the accessibility of applications via Web Apps. """ import pytest from common_utilities.selenium.webapps import WebApps from common_utilities.hq_login.login_page import LoginPage -from HQSmokeTests.testPages.reports.report_page import ReportPage from HQSmokeTests.testPages.home.home_page import HomePage +from HQSmokeTests.testPages.reports.report_page import ReportPage from HQSmokeTests.testPages.users.mobile_workers_page import MobileWorkerPage from HQSmokeTests.testPages.users.group_page import GroupPage from HQSmokeTests.testPages.data.export_data_page import ExportDataPage from HQSmokeTests.testPages.webapps.web_apps_page import WebAppsPage from HQSmokeTests.userInputs.user_inputs import UserData + @pytest.mark.reports -def test_01_verify_report_sections_displayed(driver, settings): - """Verify all report sections are displayed and load correctly.""" - print("Step 1: Navigate to the HQ home page") +def test_01_verify_report_sections(driver, settings): + """ + Verify all report sections are displayed and load correctly: + 1. Navigate to Reports menu + 2. Verify each report section is displayed + 3. Run various reports and ensure they load with data + """ home_page = HomePage(driver, settings) home_page.reports_menu() - - reports_page = ReportPage(driver, settings) - - print("Step 3: Verify Monitor Workers section is displayed") - assert home_page.is_present_and_displayed(reports_page.get_element("locator_for_monitor_workers")), "Monitor Workers section not found" - print("Step 4: Verify Inspect Data section is displayed") - assert home_page.is_present_and_displayed(reports_page.get_element("locator_for_inspect_data")), "Inspect Data section not found" + report_page = ReportPage(driver) + assert report_page.is_present_and_displayed('Monitor Workers Section'), "Monitor Workers section is not displayed" + assert report_page.is_present_and_displayed('Inspect Data Section'), "Inspect Data section is not displayed" + assert report_page.is_present_and_displayed('Manage Deployments Section'), "Manage Deployments section is not displayed" + assert report_page.is_present_and_displayed('Messaging Section'), "Messaging section is not displayed" - print("Step 5: Verify Manage Deployments section is displayed") - assert home_page.is_present_and_displayed(reports_page.get_element("locator_for_manage_deployments")), "Manage Deployments section not found" + report_page.worker_activity_report() + assert report_page.check_if_report_loaded(), "Worker Activity report did not load correctly" - print("Step 6: Verify Messaging section is displayed") - assert home_page.is_present_and_displayed(reports_page.get_element("locator_for_messaging")), "Messaging section not found" - - print("Step 7: Open and run the Worker Activity report") - reports_page.worker_activity_report() - assert reports_page.check_if_report_loaded(), "Worker Activity report failed to load data" - - print("Step 8: Open and run the Daily Form Activity report") - reports_page.daily_form_activity_report() - assert reports_page.check_if_report_loaded(), "Daily Form Activity report failed to load data" + report_page.daily_form_activity_report() + assert report_page.check_if_report_loaded(), "Daily Form Activity report did not load correctly" + + report_page.case_activity_report() + assert report_page.check_if_report_loaded(), "Case Activity report did not load correctly" - print("Step 9: Open and run the Case Activity report") - reports_page.case_activity_report() - assert reports_page.check_if_report_loaded(), "Case Activity report failed to load data" + report_page.submit_history_report() + assert report_page.check_if_report_loaded(), "Submit History report did not load correctly" - print("Step 10: Open and run the Submit History report") - reports_page.submit_history_report() - assert reports_page.check_if_report_loaded(), "Submit History report failed to load data" @pytest.mark.users -def test_02_create_mobile_worker(driver, settings): - """Create a new mobile worker and verify it appears in the list.""" +def test_02_create_and_verify_mobile_worker(driver, settings): + """ + Create a new mobile worker and verify it appears in the list: + 1. Navigate to Mobile Workers + 2. Add a new mobile worker + 3. Verify the worker is in the list + """ home_page = HomePage(driver, settings) home_page.users_menu() - - mobile_worker_page = MobileWorkerPage(driver, settings) - - print("Step 2: Click on Mobile Workers") + + mobile_worker_page = MobileWorkerPage(driver) mobile_worker_page.mobile_worker_menu() - print("Step 3: Click Add Mobile Worker") + new_username = "testworker" + str(settings["random"]) mobile_worker_page.create_mobile_worker() - - username = f"user_{str(int(time.time()))}" - password = UserData.app_password - - print(f"Step 4: Enter a unique username: {username}") - mobile_worker_page.mobile_worker_enter_username(username) + mobile_worker_page.mobile_worker_enter_username(new_username) + mobile_worker_page.mobile_worker_enter_password(UserData.app_password) + mobile_worker_page.click_create(new_username) - print("Step 5: Enter a password") - mobile_worker_page.mobile_worker_enter_password(password) + mobile_worker_page.search_user(new_username) + assert mobile_worker_page.is_present_and_displayed(new_username), "New mobile worker is not visible in the list" - print("Step 6: Save the new mobile worker") - mobile_worker_page.click_create(username) - - print("Step 7: Search for the newly created worker in the list") - assert mobile_worker_page.search_user(username), f"Mobile worker {username} not found in the list" @pytest.mark.users -def test_03_create_user_group_and_add_worker(driver, settings): - """Create a user group and add a mobile worker to it.""" +def test_03_create_and_verify_user_group(driver, settings): + """ + Create a user group and add a mobile worker to it: + 1. Navigate to Groups + 2. Create a new group and add a mobile worker + 3. Verify the group appears in the list + """ home_page = HomePage(driver, settings) home_page.users_menu() - - group_page = GroupPage(driver, settings) - - print("Step 2: Click on Groups") + + group_page = GroupPage(driver) group_page.click_group_menu() - group_name = f"group_{str(int(time.time()))}" - print(f"Step 3: Create a new group with a unique name: {group_name}") - group_page.add_group(group_name) + new_group_name = "testgroup" + str(settings["random"]) + group_page.add_group(new_group_name) - username = UserData.app_login - print(f"Step 4: Add an existing mobile worker {username} to the group") - group_page.add_user_to_group(username, group_name) - - print("Step 5: Save the group") - print("Step 6: Verify the group appears in the groups list") - assert group_page.is_present_and_displayed(group_page.get_element("locator_for_group", group_name)), f"Group {group_name} not found" + # Assuming a predefined user is added for simplicity + existing_user = UserData.mobile_testuser + group_page.add_user_to_group(existing_user, new_group_name) + + assert group_page.is_present_and_displayed(new_group_name), "Group is not visible in the list" + @pytest.mark.data def test_04_verify_export_data_functionality(driver, settings): - """Verify export data functionality works.""" + """ + Verify export data functionality works: + 1. Navigate to Export Data + 2. Create and verify a case export + 3. Download export file + """ home_page = HomePage(driver, settings) home_page.data_menu() - - export_data_page = ExportDataPage(driver, settings) - - print("Step 2: Click on Export Data") - export_data_page.prepare_and_download_export(UserData.case_export_name) - - print("Step 3: Create a new case export") + + export_data_page = ExportDataPage(driver) export_data_page.add_case_exports() - - print("Step 4: Verify the export appears in the exports list") - assert export_data_page.verify_export_count(UserData.case_export_name), "Export not found in the list" - - print("Step 5: Download the export file") - export_data_page.create_dse_and_download(UserData.case_export_name, type="case") + new_export_name = UserData.case_export_name + str(settings["random"]) + export_data_page.case_exports(new_export_name) + + assert export_data_page.is_present_and_displayed(new_export_name), "Export is not visible in the export list" + export_data_page.prepare_and_download_export(new_export_name) + assert export_data_page.assert_downloaded_file(new_export_name), "Export file download failed" + @pytest.mark.webApps def test_05_verify_application_access_via_web_apps(driver, settings): - """Verify application is accessible via Web Apps.""" - home_page = HomePage(driver, settings) - home_page.web_apps_menu() - - webapps_page = WebAppsPage(driver, settings) - - print("Step 2: Verify the list of applications is displayed") - assert webapps_page.verify_apps_presence(), "List of applications not displayed" - - app_name = UserData.village_application - print(f"Step 3: Open the available application: {app_name}") - webapps_page.login_as(app_name) - - print("Step 4: Verify the application menus are displayed") - assert webapps_page.is_present_and_displayed(webapps_page.get_element("locator_for_app_menus")), "Application menus not displayed" \ No newline at end of file + """ + Verify application is accessible via Web Apps: + 1. Navigate to Web Apps + 2. Ensure the list of applications is displayed + 3. Open an application and verify menus are displayed + """ + web_apps_page = WebApps(driver, settings) + web_apps_page.open_app(UserData.village_application) + + web_apps_page.navigate_to_breadcrumb('Menu') + assert web_apps_page.is_present_and_displayed('App Menu'), "App menu did not display correctly in Web Apps" \ No newline at end of file diff --git a/ai_test_generator/generate_test.py b/ai_test_generator/generate_test.py index afe29a547..8f9c5da2b 100644 --- a/ai_test_generator/generate_test.py +++ b/ai_test_generator/generate_test.py @@ -53,6 +53,14 @@ ## FRAMEWORK RULES (follow these exactly): +0. **Constructor signatures** – this is critical: + Every page object class has an `INSTANTIATE AS:` line in the context below. + You MUST use EXACTLY those arguments — no more, no less. + Examples: + - `INSTANTIATE AS: ReportPage(driver)` → `page = ReportPage(driver)` ✓ + - `INSTANTIATE AS: HomePage(driver, settings)` → `page = HomePage(driver, settings)` ✓ + - Never guess or add extra parameters like `settings` if not in the constructor. + 1. **Imports** – always include: ```python import pytest diff --git a/ai_test_generator/scanner.py b/ai_test_generator/scanner.py index 4461188d0..47f8c3868 100644 --- a/ai_test_generator/scanner.py +++ b/ai_test_generator/scanner.py @@ -36,8 +36,17 @@ COMMON_UTILITIES = ROOT / "common_utilities" +def _get_init_signature(node: ast.ClassDef) -> str: + """Extract the __init__ constructor args (excluding self) for a class.""" + for item in node.body: + if isinstance(item, ast.FunctionDef) and item.name == "__init__": + args = [a.arg for a in item.args.args if a.arg != "self"] + return ", ".join(args) + return "" + + def _extract_classes_and_methods(filepath: Path) -> list[dict]: - """Parse a Python file and return class info with public method signatures.""" + """Parse a Python file and return class info with constructor + public method signatures.""" try: source = filepath.read_text(encoding="utf-8", errors="ignore") tree = ast.parse(source) @@ -49,6 +58,9 @@ def _extract_classes_and_methods(filepath: Path) -> list[dict]: if not isinstance(node, ast.ClassDef): continue + init_args = _get_init_signature(node) + instantiation = f"{node.name}({init_args})" + methods = [] for item in node.body: if not isinstance(item, ast.FunctionDef): @@ -58,7 +70,6 @@ def _extract_classes_and_methods(filepath: Path) -> list[dict]: # Build readable signature args = [a.arg for a in item.args.args if a.arg != "self"] - # Include defaults info defaults = item.args.defaults if defaults: num_defaults = len(defaults) @@ -84,6 +95,7 @@ def _extract_classes_and_methods(filepath: Path) -> list[dict]: results.append({ "class": node.name, "file": str(filepath.relative_to(ROOT)), + "instantiation": instantiation, # e.g. "ReportPage(driver)" "methods": methods, }) @@ -162,6 +174,7 @@ def scan_common_utilities() -> str: classes = _extract_classes_and_methods(filepath) for cls in classes: lines.append(f"\nClass: {cls['class']} (from {cls['file']})") + lines.append(f" INSTANTIATE AS: {cls['instantiation']}") for m in cls["methods"]: line = f" - {m['signature']}" if m["doc"]: @@ -275,8 +288,10 @@ def format_suite_context(suite_data: dict, common_utils: str) -> str: # Suite-specific page objects if suite_data["page_classes"]: lines.append("=== SUITE-SPECIFIC PAGE OBJECTS ===") + lines.append("IMPORTANT: Instantiate each class EXACTLY as shown — do not add or remove parameters.") for cls in suite_data["page_classes"]: lines.append(f"\nClass: {cls['class']} (from {cls['file']})") + lines.append(f" INSTANTIATE AS: {cls['instantiation']}") for m in cls["methods"]: line = f" - {m['signature']}" if m["doc"]: From 58d1d4ae3d35b481898f820836eea65b577f55cb Mon Sep 17 00:00:00 2001 From: Kankana Bordoloi Date: Tue, 24 Mar 2026 17:36:12 +0530 Subject: [PATCH 11/11] feat: add Tags support to txt test case format Team members can now add 'Tags: report, smoke' to each test block. Tags are applied as @pytest.mark decorators in the generated test file. Co-Authored-By: Claude Sonnet 4.6 --- .../ai_testcases/example_testcase.txt | 5 +- ExportTests/ai_testcases/example_testcase.txt | 5 +- .../ai_testcases/example_testcase.txt | 23 +++ .../ai_testcases/example_testcase.txt | 5 +- .../ai_testcases/example_testcase.txt | 5 +- .../ai_testcases/example_testcase.txt | 5 +- .../ai_testcases/example_testcase.txt | 5 +- .../ai_testcases/example_testcase.txt | 5 +- .../ai_testcases/example_testcase.txt | 5 +- .../ai_testcases/reports_and_users.txt | 7 +- .../testCases/test_ai_reports_and_users.py | 182 +++++++++++------- P1P2Tests/ai_testcases/example_testcase.txt | 5 +- RequestAPI/ai_testcases/example_testcase.txt | 5 +- .../CO_BHA/ai_testcases/example_testcase.txt | 5 +- ai_test_generator/TESTCASE_TEMPLATE.txt | 5 +- ai_test_generator/process_testcases.py | 17 +- 16 files changed, 206 insertions(+), 83 deletions(-) create mode 100644 Features/CaseSearch/ai_testcases/example_testcase.txt diff --git a/ElasticSearchTests/ai_testcases/example_testcase.txt b/ElasticSearchTests/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/ElasticSearchTests/ai_testcases/example_testcase.txt +++ b/ElasticSearchTests/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/ExportTests/ai_testcases/example_testcase.txt b/ExportTests/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/ExportTests/ai_testcases/example_testcase.txt +++ b/ExportTests/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/Features/CaseSearch/ai_testcases/example_testcase.txt b/Features/CaseSearch/ai_testcases/example_testcase.txt new file mode 100644 index 000000000..f9b9a654b --- /dev/null +++ b/Features/CaseSearch/ai_testcases/example_testcase.txt @@ -0,0 +1,23 @@ +Suite: + +Test 1: +Tags: +1. +2. +3. +4. +Expected Result: + +Test 2: +Tags: +1. +2. +3. +Expected Result: + +Test 3: +Tags: +1. +2. +3. +Expected Result: diff --git a/Features/DataDictionary/ai_testcases/example_testcase.txt b/Features/DataDictionary/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/Features/DataDictionary/ai_testcases/example_testcase.txt +++ b/Features/DataDictionary/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/Features/FindDataById/ai_testcases/example_testcase.txt b/Features/FindDataById/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/Features/FindDataById/ai_testcases/example_testcase.txt +++ b/Features/FindDataById/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/Features/Lookuptable/ai_testcases/example_testcase.txt b/Features/Lookuptable/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/Features/Lookuptable/ai_testcases/example_testcase.txt +++ b/Features/Lookuptable/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/Features/MultiSelect/ai_testcases/example_testcase.txt b/Features/MultiSelect/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/Features/MultiSelect/ai_testcases/example_testcase.txt +++ b/Features/MultiSelect/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt b/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt +++ b/Features/SplitScreenCaseSearch/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/HQSmokeTests/ai_testcases/example_testcase.txt b/HQSmokeTests/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/HQSmokeTests/ai_testcases/example_testcase.txt +++ b/HQSmokeTests/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/HQSmokeTests/ai_testcases/reports_and_users.txt b/HQSmokeTests/ai_testcases/reports_and_users.txt index 152a35cd3..052eca1ba 100644 --- a/HQSmokeTests/ai_testcases/reports_and_users.txt +++ b/HQSmokeTests/ai_testcases/reports_and_users.txt @@ -1,6 +1,7 @@ Suite: HQSmokeTests Test 1: Verify all report sections are displayed and load correctly +Tags: report, smoke 1. Navigate to the HQ home page 2. Click on the Reports menu 3. Verify Monitor Workers section is displayed @@ -14,6 +15,7 @@ Test 1: Verify all report sections are displayed and load correctly Expected Result: All report sections are visible and each report loads with data Test 2: Create a new mobile worker and verify it appears in the list +Tags: users, mobileWorker 1. Navigate to Users menu 2. Click on Mobile Workers 3. Click Add Mobile Worker @@ -24,6 +26,7 @@ Test 2: Create a new mobile worker and verify it appears in the list Expected Result: New mobile worker is created and visible in the mobile workers list Test 3: Create a user group and add a mobile worker to it +Tags: users, groups 1. Navigate to Users menu 2. Click on Groups 3. Create a new group with a unique name @@ -33,6 +36,7 @@ Test 3: Create a user group and add a mobile worker to it Expected Result: Group is created and the mobile worker is assigned to it Test 4: Verify export data functionality works +Tags: data, exports 1. Navigate to Data menu 2. Click on Export Data 3. Create a new case export @@ -41,8 +45,9 @@ Test 4: Verify export data functionality works Expected Result: Export file is created and downloaded successfully Test 5: Verify application is accessible via Web Apps +Tags: webApps, smoke 1. Navigate to Web Apps 2. Verify the list of applications is displayed 3. Open one of the available applications 4. Verify the application menus are displayed -Expected Result: Application loads correctly in Web Apps \ No newline at end of file +Expected Result: Application loads correctly in Web Apps diff --git a/HQSmokeTests/testCases/test_ai_reports_and_users.py b/HQSmokeTests/testCases/test_ai_reports_and_users.py index 387b1a04e..c5c149f83 100644 --- a/HQSmokeTests/testCases/test_ai_reports_and_users.py +++ b/HQSmokeTests/testCases/test_ai_reports_and_users.py @@ -1,11 +1,14 @@ """ -This module contains tests for verifying the correct display and functionality of report sections, -the creation of mobile workers and user groups, the export data functionality, and the accessibility of applications via Web Apps. +Test Module: test_ai_reports_and_users.py + +Contains automated test cases for verifying report sections, user functionalities, +data export capabilities, and application accessibility in Web Apps. """ import pytest from common_utilities.selenium.webapps import WebApps from common_utilities.hq_login.login_page import LoginPage + from HQSmokeTests.testPages.home.home_page import HomePage from HQSmokeTests.testPages.reports.report_page import ReportPage from HQSmokeTests.testPages.users.mobile_workers_page import MobileWorkerPage @@ -14,116 +17,161 @@ from HQSmokeTests.testPages.webapps.web_apps_page import WebAppsPage from HQSmokeTests.userInputs.user_inputs import UserData - -@pytest.mark.reports -def test_01_verify_report_sections(driver, settings): +@pytest.mark.report +@pytest.mark.smoke +def test_01_verify_all_report_sections_load_correctly(driver, settings): """ - Verify all report sections are displayed and load correctly: - 1. Navigate to Reports menu - 2. Verify each report section is displayed - 3. Run various reports and ensure they load with data + Verify all report sections are displayed and load correctly. + Steps: + 1. Navigate to the HQ home page + 2. Click on the Reports menu + 3. Verify Monitor Workers section is displayed + 4. Verify Inspect Data section is displayed + 5. Verify Manage Deployments section is displayed + 6. Verify Messaging section is displayed + 7. Open and run Worker Activity report + 8. Open and run Daily Form Activity report + 9. Open and run Case Activity report + 10. Open and run Submit History report + Expected Result: All report sections are visible and each report loads with data. """ home_page = HomePage(driver, settings) home_page.reports_menu() - + report_page = ReportPage(driver) - assert report_page.is_present_and_displayed('Monitor Workers Section'), "Monitor Workers section is not displayed" - assert report_page.is_present_and_displayed('Inspect Data Section'), "Inspect Data section is not displayed" - assert report_page.is_present_and_displayed('Manage Deployments Section'), "Manage Deployments section is not displayed" - assert report_page.is_present_and_displayed('Messaging Section'), "Messaging section is not displayed" + assert report_page.is_present_and_displayed(report_page.get_element("Monitor Workers")), "Monitor Workers section not displayed" + assert report_page.is_present_and_displayed(report_page.get_element("Inspect Data")), "Inspect Data section not displayed" + assert report_page.is_present_and_displayed(report_page.get_element("Manage Deployments")), "Manage Deployments section not displayed" + assert report_page.is_present_and_displayed(report_page.get_element("Messaging")), "Messaging section not displayed" + print("All report sections are displayed.") + report_page.worker_activity_report() - assert report_page.check_if_report_loaded(), "Worker Activity report did not load correctly" - + assert report_page.check_if_report_loaded(), "Worker Activity report did not load with data" + print("Worker Activity report loaded successfully.") + report_page.daily_form_activity_report() - assert report_page.check_if_report_loaded(), "Daily Form Activity report did not load correctly" + assert report_page.check_if_report_loaded(), "Daily Form Activity report did not load with data" + print("Daily Form Activity report loaded successfully.") report_page.case_activity_report() - assert report_page.check_if_report_loaded(), "Case Activity report did not load correctly" - + assert report_page.check_if_report_loaded(), "Case Activity report did not load with data" + print("Case Activity report loaded successfully.") + report_page.submit_history_report() - assert report_page.check_if_report_loaded(), "Submit History report did not load correctly" - + assert report_page.check_if_report_loaded(), "Submit History report did not load with data" + print("Submit History report loaded successfully.") @pytest.mark.users -def test_02_create_and_verify_mobile_worker(driver, settings): +@pytest.mark.mobileWorker +def test_02_create_new_mobile_worker_verify_in_list(driver, settings): """ - Create a new mobile worker and verify it appears in the list: - 1. Navigate to Mobile Workers - 2. Add a new mobile worker - 3. Verify the worker is in the list + Create a new mobile worker and verify it appears in the list. + Steps: + 1. Navigate to Users menu + 2. Click on Mobile Workers + 3. Click Add Mobile Worker + 4. Enter a unique username + 5. Enter a password + 6. Save the new mobile worker + 7. Search for the newly created worker in the list + Expected Result: New mobile worker is created and visible in the mobile workers list. """ + username = "unique_user_" + str(int(time.time())) # Generating a unique username + home_page = HomePage(driver, settings) home_page.users_menu() - + mobile_worker_page = MobileWorkerPage(driver) mobile_worker_page.mobile_worker_menu() - - new_username = "testworker" + str(settings["random"]) mobile_worker_page.create_mobile_worker() - mobile_worker_page.mobile_worker_enter_username(new_username) + mobile_worker_page.mobile_worker_enter_username(username) mobile_worker_page.mobile_worker_enter_password(UserData.app_password) - mobile_worker_page.click_create(new_username) - - mobile_worker_page.search_user(new_username) - assert mobile_worker_page.is_present_and_displayed(new_username), "New mobile worker is not visible in the list" + mobile_worker_page.click_create(username) + print("New mobile worker created.") + mobile_worker_page.search_user(username) + assert mobile_worker_page.is_present_and_displayed(mobile_worker_page.get_element(username)), "New mobile worker not found in list" + print("New mobile worker is present in the list.") @pytest.mark.users -def test_03_create_and_verify_user_group(driver, settings): +@pytest.mark.groups +def test_03_create_user_group_add_mobile_worker(driver, settings): """ - Create a user group and add a mobile worker to it: - 1. Navigate to Groups - 2. Create a new group and add a mobile worker - 3. Verify the group appears in the list + Create a user group and add a mobile worker to it. + Steps: + 1. Navigate to Users menu + 2. Click on Groups + 3. Create a new group with a unique name + 4. Add an existing mobile worker to the group + 5. Save the group + 6. Verify the group appears in the groups list + Expected Result: Group is created and the mobile worker is assigned to it. """ + group_name = "Group_" + str(int(time.time())) # Generating a unique group name + home_page = HomePage(driver, settings) home_page.users_menu() - + group_page = GroupPage(driver) group_page.click_group_menu() + group_page.add_group(group_name) + print(f"New group '{group_name}' created.") - new_group_name = "testgroup" + str(settings["random"]) - group_page.add_group(new_group_name) - - # Assuming a predefined user is added for simplicity - existing_user = UserData.mobile_testuser - group_page.add_user_to_group(existing_user, new_group_name) - - assert group_page.is_present_and_displayed(new_group_name), "Group is not visible in the list" + group_page.add_user_to_group(UserData.mobile_testuser, group_name) + print(f"Mobile worker '{UserData.mobile_testuser}' added to group '{group_name}'.") + assert group_page.is_present_and_displayed(group_page.get_element(group_name)), "New group not found in list" + print("Group with mobile worker is present in the list.") @pytest.mark.data +@pytest.mark.exports def test_04_verify_export_data_functionality(driver, settings): """ - Verify export data functionality works: - 1. Navigate to Export Data - 2. Create and verify a case export - 3. Download export file + Verify export data functionality works. + Steps: + 1. Navigate to Data menu + 2. Click on Export Data + 3. Create a new case export + 4. Verify the export appears in the exports list + 5. Download the export file + Expected Result: Export file is created and downloaded successfully. """ home_page = HomePage(driver, settings) home_page.data_menu() - + export_data_page = ExportDataPage(driver) - export_data_page.add_case_exports() - new_export_name = UserData.case_export_name + str(settings["random"]) - export_data_page.case_exports(new_export_name) + export_name = UserData.case_export_name - assert export_data_page.is_present_and_displayed(new_export_name), "Export is not visible in the export list" - export_data_page.prepare_and_download_export(new_export_name) - assert export_data_page.assert_downloaded_file(new_export_name), "Export file download failed" + export_data_page.add_case_exports() + export_data_page.case_exports(export_name) + print(f"Case export '{export_name}' created.") + assert export_data_page.verify_export_count(export_name), "Export not found in exports list" + print("Export appears in the exports list.") + + export_data_page.download_export_without_condition(export_name) + print("Export file downloaded successfully.") + export_data_page.assert_downloaded_file(export_name, "Export file") @pytest.mark.webApps -def test_05_verify_application_access_via_web_apps(driver, settings): +@pytest.mark.smoke +def test_05_verify_application_accessible_via_web_apps(driver, settings): """ - Verify application is accessible via Web Apps: + Verify the application is accessible via Web Apps. + Steps: 1. Navigate to Web Apps - 2. Ensure the list of applications is displayed - 3. Open an application and verify menus are displayed + 2. Verify the list of applications is displayed + 3. Open one of the available applications + 4. Verify the application menus are displayed + Expected Result: Application loads correctly in Web Apps. """ - web_apps_page = WebApps(driver, settings) - web_apps_page.open_app(UserData.village_application) + webapps = WebApps(driver, settings) + webapps.open_app(UserData.village_application) + webapps_page = WebAppsPage(driver) + assert webapps_page.verify_apps_presence(), "Applications list not displayed" + print("Applications list is displayed.") - web_apps_page.navigate_to_breadcrumb('Menu') - assert web_apps_page.is_present_and_displayed('App Menu'), "App menu did not display correctly in Web Apps" \ No newline at end of file + webapps.open_menu("Case List") + assert webapps_page.is_present_and_displayed(webapps_page.get_element("Case List")), "Application menu not displayed correctly" + print("Application menus are displayed correctly.") \ No newline at end of file diff --git a/P1P2Tests/ai_testcases/example_testcase.txt b/P1P2Tests/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/P1P2Tests/ai_testcases/example_testcase.txt +++ b/P1P2Tests/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/RequestAPI/ai_testcases/example_testcase.txt b/RequestAPI/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/RequestAPI/ai_testcases/example_testcase.txt +++ b/RequestAPI/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt b/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt index e256dccb4..f9b9a654b 100644 --- a/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt +++ b/USH_Apps/CO_BHA/ai_testcases/example_testcase.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/ai_test_generator/TESTCASE_TEMPLATE.txt b/ai_test_generator/TESTCASE_TEMPLATE.txt index e256dccb4..f9b9a654b 100644 --- a/ai_test_generator/TESTCASE_TEMPLATE.txt +++ b/ai_test_generator/TESTCASE_TEMPLATE.txt @@ -1,6 +1,7 @@ Suite: Test 1: +Tags: 1. 2. 3. @@ -8,13 +9,15 @@ Test 1: Expected Result: Test 2: +Tags: 1. 2. 3. Expected Result: Test 3: +Tags: 1. 2. 3. -Expected Result: \ No newline at end of file +Expected Result: diff --git a/ai_test_generator/process_testcases.py b/ai_test_generator/process_testcases.py index af7902b36..922932be1 100644 --- a/ai_test_generator/process_testcases.py +++ b/ai_test_generator/process_testcases.py @@ -82,11 +82,12 @@ def parse_txt_file(txt_path: Path) -> dict: Returns: { "suite": str, - "module_name": str, # derived from filename + "module_name": str, "tests": [ { - "name": str, # e.g. "Search by song name and submit form" - "steps": list, # ["1. Login as user-1", "2. Open the Music App", ...] + "name": str, + "tags": list, # e.g. ["report", "smoke"] + "steps": list, "expected": str, }, ... @@ -117,11 +118,18 @@ def parse_txt_file(txt_path: Path) -> dict: tests.append(current_test) current_test = { "name": test_header.group(1).strip(), + "tags": [], "steps": [], "expected": "", } continue + # Tags line (within a test block): "Tags: report, smoke" + if current_test and re.match(r'^tags?\s*:', stripped, re.IGNORECASE): + raw_tags = stripped.split(":", 1)[1].strip() + current_test["tags"] = [t.strip() for t in raw_tags.split(",") if t.strip()] + continue + # Expected result line (within a test block) if current_test and re.match(r'^expected\s*(result)?\s*:', stripped, re.IGNORECASE): current_test["expected"] = stripped.split(":", 1)[1].strip() @@ -159,6 +167,9 @@ def build_module_description(parsed: dict) -> str: for i, test in enumerate(parsed["tests"], start=1): lines.append(f"--- Test {i}: {test['name']} ---") + if test.get("tags"): + marks = " ".join(f"@pytest.mark.{t}" for t in test["tags"]) + lines.append(f"Pytest markers: {marks}") lines.append("Steps:") for step in test["steps"]: lines.append(f" {step}")