Files
soleprint/station/tools/tester/playwright/runner.py
2025-12-24 05:38:37 -03:00

190 lines
6.1 KiB
Python

"""
Execute Playwright tests and capture artifacts.
"""
import subprocess
import json
import time
from pathlib import Path
from typing import Optional
from dataclasses import dataclass, field
@dataclass
class PlaywrightResult:
"""Result of a Playwright test execution."""
test_id: str
name: str
status: str # "passed", "failed", "skipped"
duration: float
error_message: Optional[str] = None
traceback: Optional[str] = None
artifacts: list[dict] = field(default_factory=list)
class PlaywrightRunner:
"""Run Playwright tests and collect artifacts."""
def __init__(self, tests_dir: Path, artifacts_dir: Path):
self.tests_dir = tests_dir
self.artifacts_dir = artifacts_dir
self.videos_dir = artifacts_dir / "videos"
self.screenshots_dir = artifacts_dir / "screenshots"
self.traces_dir = artifacts_dir / "traces"
# Ensure artifact directories exist
self.videos_dir.mkdir(parents=True, exist_ok=True)
self.screenshots_dir.mkdir(parents=True, exist_ok=True)
self.traces_dir.mkdir(parents=True, exist_ok=True)
def run_tests(
self,
test_files: Optional[list[str]] = None,
run_id: Optional[str] = None
) -> list[PlaywrightResult]:
"""
Run Playwright tests and collect results.
Args:
test_files: List of test file paths to run (relative to tests_dir).
If None, runs all tests.
run_id: Optional run ID to namespace artifacts.
Returns:
List of PlaywrightResult objects.
"""
if not self.tests_dir.exists():
return []
# Build playwright command
cmd = ["npx", "playwright", "test"]
# Add specific test files if provided
if test_files:
cmd.extend(test_files)
# Add reporter for JSON output
results_file = self.artifacts_dir / f"results_{run_id or 'latest'}.json"
cmd.extend([
"--reporter=json",
f"--output={results_file}"
])
# Configure artifact collection
# Videos and screenshots are configured in playwright.config.ts
# We'll assume config is set to capture on failure
# Run tests
start_time = time.time()
try:
result = subprocess.run(
cmd,
cwd=self.tests_dir,
capture_output=True,
text=True,
timeout=600 # 10 minute timeout
)
# Parse results
if results_file.exists():
with open(results_file) as f:
results_data = json.load(f)
return self._parse_results(results_data, run_id)
else:
# No results file - likely error
return self._create_error_result(result.stderr)
except subprocess.TimeoutExpired:
return self._create_error_result("Tests timed out after 10 minutes")
except Exception as e:
return self._create_error_result(str(e))
def _parse_results(
self,
results_data: dict,
run_id: Optional[str]
) -> list[PlaywrightResult]:
"""Parse Playwright JSON results."""
parsed_results = []
# Playwright JSON reporter structure:
# {
# "suites": [...],
# "tests": [...],
# }
tests = results_data.get("tests", [])
for test in tests:
test_id = test.get("testId", "unknown")
title = test.get("title", "Unknown test")
status = test.get("status", "unknown") # passed, failed, skipped
duration = test.get("duration", 0) / 1000.0 # Convert ms to seconds
error_message = None
traceback = None
# Extract error if failed
if status == "failed":
error = test.get("error", {})
error_message = error.get("message", "Test failed")
traceback = error.get("stack", "")
# Collect artifacts
artifacts = []
for attachment in test.get("attachments", []):
artifact_type = attachment.get("contentType", "")
artifact_path = attachment.get("path", "")
if artifact_path:
artifact_file = Path(artifact_path)
if artifact_file.exists():
# Determine type
if "video" in artifact_type:
type_label = "video"
elif "image" in artifact_type:
type_label = "screenshot"
elif "trace" in artifact_type:
type_label = "trace"
else:
type_label = "attachment"
artifacts.append({
"type": type_label,
"filename": artifact_file.name,
"path": str(artifact_file),
"size": artifact_file.stat().st_size,
"mimetype": artifact_type,
})
parsed_results.append(PlaywrightResult(
test_id=test_id,
name=title,
status=status,
duration=duration,
error_message=error_message,
traceback=traceback,
artifacts=artifacts,
))
return parsed_results
def _create_error_result(self, error_msg: str) -> list[PlaywrightResult]:
"""Create an error result when test execution fails."""
return [
PlaywrightResult(
test_id="playwright_error",
name="Playwright Execution Error",
status="failed",
duration=0.0,
error_message=error_msg,
traceback="",
artifacts=[],
)
]
def get_artifact_url(self, run_id: str, artifact_filename: str) -> str:
"""Generate URL for streaming an artifact."""
return f"/tools/tester/api/artifact/{run_id}/{artifact_filename}"