190 lines
6.1 KiB
Python
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}"
|