"""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]