|
| 1 | +import asyncio |
| 2 | +import json |
| 3 | +import typer |
| 4 | +from pathlib import Path |
| 5 | +from scrapewizard.engine.recorder import InteractiveRecorder |
| 6 | +from scrapewizard.engine.sandbox import SandboxRunner |
| 7 | + |
| 8 | +app = typer.Typer(help="Self-healing test automation engine commands") |
| 9 | + |
| 10 | +def async_command(f): |
| 11 | + """Decorator to run async Typer commands synchronously.""" |
| 12 | + import functools |
| 13 | + @functools.wraps(f) |
| 14 | + def wrapper(*args, **kwargs): |
| 15 | + return asyncio.run(f(*args, **kwargs)) |
| 16 | + return wrapper |
| 17 | + |
| 18 | +@app.command(name="record") |
| 19 | +@async_command |
| 20 | +async def record( |
| 21 | + url: str = typer.Option(..., "--url", "-u", help="Target URL to start recording from"), |
| 22 | + output: str = typer.Option("flow.json", "--output", "-o", help="Path to save the generated flow.json"), |
| 23 | + screenshots: str = typer.Option("screenshots", "--screenshots", "-s", help="Directory to save crop screenshots") |
| 24 | +): |
| 25 | + """ |
| 26 | + Open a headed browser to record user interactions on a page. |
| 27 | + Saves the flow steps and element fingerprints to a flow.json file. |
| 28 | + """ |
| 29 | + typer.echo(f"Starting interactive recording on {url}...") |
| 30 | + recorder = InteractiveRecorder(output_path=output, screenshots_dir=screenshots, headless=False) |
| 31 | + await recorder.start(url) |
| 32 | + typer.echo(f"Successfully saved flow recording to {output}") |
| 33 | + |
| 34 | +@app.command(name="test") |
| 35 | +@async_command |
| 36 | +async def test( |
| 37 | + flow_path: str = typer.Argument(..., help="Path to the flow.json file to execute"), |
| 38 | + artifacts: str = typer.Option(None, "--artifacts", "-a", help="Directory to save run artifacts"), |
| 39 | + headless: bool = typer.Option(True, "--headless/--headed", help="Run the browser in headless or headed mode") |
| 40 | +): |
| 41 | + """ |
| 42 | + Run an automated headless sandbox execution of the recorded flow.json. |
| 43 | + Validates console/network errors, visual diffs, and accessibility violations. |
| 44 | + """ |
| 45 | + flow_file = Path(flow_path) |
| 46 | + if not flow_file.exists(): |
| 47 | + typer.echo(f"Error: flow file not found at {flow_path}", err=True) |
| 48 | + raise typer.Exit(code=1) |
| 49 | + |
| 50 | + try: |
| 51 | + with open(flow_file, "r", encoding="utf-8") as f: |
| 52 | + flow_data = json.load(f) |
| 53 | + except Exception as e: |
| 54 | + typer.echo(f"Error: failed to parse JSON in {flow_path}: {e}", err=True) |
| 55 | + raise typer.Exit(code=1) |
| 56 | + |
| 57 | + # Construct test definition from flow data directly |
| 58 | + from scrapewizard.engine.test_generator import TestGenerator |
| 59 | + generator = TestGenerator(flow_path) |
| 60 | + test_def = generator.generate() |
| 61 | + |
| 62 | + typer.echo(f"Running sandbox execution for {flow_path}...") |
| 63 | + runner = SandboxRunner(artifacts_dir=artifacts, headless=headless) |
| 64 | + result = await runner.run(test_def) |
| 65 | + |
| 66 | + # Print results |
| 67 | + typer.echo(f"\nExecution finished in {result.duration_ms} ms. Status: {result.status.upper()}") |
| 68 | + typer.echo(f"Artifacts saved to: {result.artifacts_dir}\n") |
| 69 | + |
| 70 | + for idx, step in enumerate(result.step_results): |
| 71 | + status_symbol = "✅" if step.status == "passed" else "❌" |
| 72 | + typer.echo(f" {status_symbol} Step {idx + 1}: {step.step_name} - {step.status.upper()} ({step.duration_ms} ms)") |
| 73 | + if step.error_message: |
| 74 | + typer.echo(f" Error: {step.error_message}") |
| 75 | + if step.console_errors: |
| 76 | + typer.echo(f" Console Errors ({len(step.console_errors)}):") |
| 77 | + for err in step.console_errors: |
| 78 | + typer.echo(f" - {err}") |
| 79 | + if step.network_errors: |
| 80 | + typer.echo(f" Network Failures ({len(step.network_errors)}):") |
| 81 | + for err in step.network_errors: |
| 82 | + typer.echo(f" - {err}") |
| 83 | + if step.a11y_violations: |
| 84 | + typer.echo(f" A11y Violations ({len(step.a11y_violations)}):") |
| 85 | + for violation in step.a11y_violations: |
| 86 | + typer.echo(f" - [{violation['impact']}] {violation['id']}: {violation['help']}") |
| 87 | + |
| 88 | + if result.status != "passed": |
| 89 | + typer.echo("\n❌ Run FAILED.") |
| 90 | + raise typer.Exit(code=1) |
| 91 | + else: |
| 92 | + typer.echo("\n✅ Run PASSED.") |
| 93 | + raise typer.Exit(code=0) |
0 commit comments