163 lines
5.2 KiB
Python
163 lines
5.2 KiB
Python
|
|
"""Base classes for Playwright scenario execution.
|
||
|
|
|
||
|
|
Scenarios are deterministic, reusable browser automation sequences
|
||
|
|
that execute specific UI workflows against tenant applications.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from abc import ABC, abstractmethod
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any, Optional
|
||
|
|
|
||
|
|
from playwright.async_api import Page
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class ScenarioOptions:
|
||
|
|
"""Configuration options for scenario execution.
|
||
|
|
|
||
|
|
Attributes:
|
||
|
|
timeout_ms: Default timeout for actions in milliseconds
|
||
|
|
screenshot_on_failure: Capture screenshot when scenario fails
|
||
|
|
screenshot_on_success: Capture screenshot when scenario succeeds
|
||
|
|
save_trace: Save Playwright trace for debugging
|
||
|
|
allowed_domains: List of domains the scenario can access (REQUIRED for security)
|
||
|
|
artifacts_dir: Directory to save screenshots and traces
|
||
|
|
"""
|
||
|
|
timeout_ms: int = 60000
|
||
|
|
screenshot_on_failure: bool = True
|
||
|
|
screenshot_on_success: bool = False
|
||
|
|
save_trace: bool = False
|
||
|
|
allowed_domains: list[str] = field(default_factory=list)
|
||
|
|
artifacts_dir: Optional[Path] = None
|
||
|
|
|
||
|
|
def __post_init__(self) -> None:
|
||
|
|
if self.artifacts_dir and isinstance(self.artifacts_dir, str):
|
||
|
|
self.artifacts_dir = Path(self.artifacts_dir)
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class ScenarioResult:
|
||
|
|
"""Result of a scenario execution.
|
||
|
|
|
||
|
|
Attributes:
|
||
|
|
success: Whether the scenario completed successfully
|
||
|
|
data: Scenario-specific result data
|
||
|
|
screenshots: List of paths to captured screenshots
|
||
|
|
error: Error message if scenario failed
|
||
|
|
trace_path: Path to trace file if tracing was enabled
|
||
|
|
"""
|
||
|
|
success: bool
|
||
|
|
data: dict[str, Any]
|
||
|
|
screenshots: list[str] = field(default_factory=list)
|
||
|
|
error: Optional[str] = None
|
||
|
|
trace_path: Optional[str] = None
|
||
|
|
|
||
|
|
|
||
|
|
class BaseScenario(ABC):
|
||
|
|
"""Abstract base class for Playwright scenarios.
|
||
|
|
|
||
|
|
Each scenario implements a specific UI automation workflow.
|
||
|
|
Scenarios are registered by name and dispatched by the PlaywrightExecutor.
|
||
|
|
|
||
|
|
Example implementation:
|
||
|
|
class NextcloudInitialSetup(BaseScenario):
|
||
|
|
@property
|
||
|
|
def name(self) -> str:
|
||
|
|
return "nextcloud_initial_setup"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def required_inputs(self) -> list[str]:
|
||
|
|
return ["base_url", "admin_username", "admin_password"]
|
||
|
|
|
||
|
|
async def execute(self, page, inputs, options) -> ScenarioResult:
|
||
|
|
# Perform setup steps...
|
||
|
|
return ScenarioResult(success=True, data={"setup": "complete"})
|
||
|
|
"""
|
||
|
|
|
||
|
|
@property
|
||
|
|
@abstractmethod
|
||
|
|
def name(self) -> str:
|
||
|
|
"""Unique name identifying this scenario.
|
||
|
|
|
||
|
|
This name is used in task payloads to select the scenario.
|
||
|
|
Convention: lowercase_with_underscores (e.g., 'nextcloud_initial_setup')
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
|
||
|
|
@property
|
||
|
|
@abstractmethod
|
||
|
|
def required_inputs(self) -> list[str]:
|
||
|
|
"""List of required input keys for this scenario.
|
||
|
|
|
||
|
|
The executor validates that all required inputs are present
|
||
|
|
before executing the scenario.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
|
||
|
|
@property
|
||
|
|
def optional_inputs(self) -> list[str]:
|
||
|
|
"""List of optional input keys for this scenario.
|
||
|
|
|
||
|
|
Override this property to declare optional inputs with defaults.
|
||
|
|
"""
|
||
|
|
return []
|
||
|
|
|
||
|
|
@property
|
||
|
|
def description(self) -> str:
|
||
|
|
"""Human-readable description of what this scenario does.
|
||
|
|
|
||
|
|
Override this property to provide documentation.
|
||
|
|
"""
|
||
|
|
return f"Scenario: {self.name}"
|
||
|
|
|
||
|
|
@abstractmethod
|
||
|
|
async def execute(
|
||
|
|
self,
|
||
|
|
page: Page,
|
||
|
|
inputs: dict[str, Any],
|
||
|
|
options: ScenarioOptions,
|
||
|
|
) -> ScenarioResult:
|
||
|
|
"""Execute the scenario against the provided page.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
page: Playwright Page object with domain restrictions applied
|
||
|
|
inputs: Dictionary of input values (validated by executor)
|
||
|
|
options: Scenario options including timeout and artifact settings
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ScenarioResult with success status and any result data
|
||
|
|
|
||
|
|
Note:
|
||
|
|
- Domain restrictions are already enforced by the executor
|
||
|
|
- Screenshots on failure are handled by the executor
|
||
|
|
- Focus on the business logic of the UI workflow
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
|
||
|
|
async def setup(self, page: Page, options: ScenarioOptions) -> None:
|
||
|
|
"""Optional setup hook called before execute().
|
||
|
|
|
||
|
|
Override to perform setup actions like setting viewport size,
|
||
|
|
configuring page settings, etc.
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
async def teardown(self, page: Page, options: ScenarioOptions) -> None:
|
||
|
|
"""Optional teardown hook called after execute().
|
||
|
|
|
||
|
|
Override to perform cleanup actions. Called even if execute() fails.
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
def validate_inputs(self, inputs: dict[str, Any]) -> list[str]:
|
||
|
|
"""Validate inputs and return list of missing required keys.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
inputs: Dictionary of inputs to validate
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of missing required input keys (empty if all present)
|
||
|
|
"""
|
||
|
|
return [key for key in self.required_inputs if key not in inputs]
|