Spaces:
Sleeping
Sleeping
File size: 12,597 Bytes
cc036ff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 | """
E2E UI Test Configuration with Pytest and Playwright.
This module provides pytest fixtures for Playwright browser automation
including browser context, page, and base URL configuration.
"""
import os
import shutil
import sys
from datetime import datetime
import pytest
from playwright.sync_api import BrowserContext as SyncBrowserContext
# Make allure optional - only import if available
try:
import allure
ALLURE_AVAILABLE = True
except ImportError:
ALLURE_AVAILABLE = False
def is_ci_environment():
"""Detect if running in CI environment."""
return os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" or os.getenv("GITLAB_CI") == "true"
# Import base fixtures for direct use (optional, fixtures available via plugins)
from .fixtures import auth_fixtures
from .fixtures import database_fixtures
from .fixtures import api_fixtures
from .fixtures import test_data_factory # Factory functions module
# Re-export commonly used fixtures for backward compatibility
from .fixtures.auth_fixtures import authenticated_page, authenticated_page_api, test_user, authenticated_user
from .fixtures.database_fixtures import db_session
from .fixtures.api_fixtures import setup_test_user, setup_test_project, api_client, api_base_url, test_user_data
@pytest.fixture(scope="session")
def worker_id():
"""
Provide worker_id for pytest-xdist compatibility.
Returns 'master' when not running under xdist (single worker mode).
Yields:
str: Worker ID ('master' for single worker, 'gw0', 'gw1', etc. for xdist)
"""
return "master"
def pytest_configure(config):
"""
Pytest configuration hook.
Register custom markers and configure CI-only retries.
"""
# Register markers
config.addinivalue_line(
"markers", "e2e: mark test as end-to-end UI test"
)
# Enable retries only in CI - set environment for pytest-rerunfailures
if is_ci_environment():
# Add --reruns to sys.argv so pytest-rerunfailures picks it up
if "--reruns" not in sys.argv and "-r" not in sys.argv:
reruns = os.getenv("PYTEST_RERUNS", "2")
sys.argv.extend(["--reruns", reruns])
print(f"\nCI environment: Enabled {reruns} retries on failure")
else:
print("\nLocal development: Test retries disabled (fast feedback)")
@pytest.fixture(scope="session")
def browser_type_launch_args(browser_type_launch_args):
"""
Configure browser launch arguments.
Args:
browser_type_launch_args: Default launch arguments from pytest-playwright
Returns:
Updated launch arguments with headless mode
"""
return {
**browser_type_launch_args,
"headless": True, # Run in headless mode for CI/CD
}
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
"""
Configure browser context arguments with CI-aware video recording.
Args:
browser_context_args: Default context arguments from pytest-playwright
Returns:
Updated context arguments with accept downloads, bypass CSP, and conditional video recording
"""
context_args = {
**browser_context_args,
"accept_downloads": True,
"bypass_csp": True, # Bypass Content Security Policy for testing
"ignore_https_errors": True, # Allow self-signed certificates
}
# Enable video recording only in CI
if is_ci_environment():
video_dir = "backend/tests/e2e_ui/artifacts/videos"
os.makedirs(video_dir, exist_ok=True)
context_args["record_video_dir"] = video_dir
return context_args
@pytest.fixture(scope="session", autouse=True)
def clean_allure_results():
"""
Clean Allure results directory before test run.
Prevents old test results from polluting current run.
Yields:
None: Allows test session to proceed
"""
if ALLURE_AVAILABLE:
allure_dir = "allure-results"
if os.path.exists(allure_dir):
shutil.rmtree(allure_dir)
yield
# Don't clean after (let user review results)
@pytest.fixture(scope="session")
def base_url():
"""
Base URL for E2E UI tests.
Uses port 3001 to avoid conflict with dev frontend (port 3000).
Returns:
str: Base URL for test application
"""
return "http://localhost:3001"
@pytest.fixture(scope="function")
def page(browser, base_url):
"""
Create a new page with base URL.
Args:
browser: Playwright browser fixture (session-scoped)
base_url: Base URL fixture
Yields:
Page: Playwright page object
"""
# Create a new browser context
context = browser.new_context()
page = context.new_page()
# Set base URL for relative navigation
page.goto(base_url)
yield page
# Cleanup: close page and context
page.close()
context.close()
@pytest.fixture(scope="function")
def screenshot_page(page, request):
"""
Capture screenshot on test failure.
Args:
page: Playwright page fixture
request: Pytest request node
Returns:
Page: Same page object for chaining
"""
yield page
# Capture screenshot if test failed
if request.node.rep_call.failed:
screenshot_path = f"screenshots/{request.node.name}.png"
page.screenshot(path=screenshot_path)
print(f"\nScreenshot saved: {screenshot_path}")
@pytest.fixture(scope="function")
def video_page(browser, base_url, request):
"""
Capture video on test failure.
Args:
browser: Playwright browser fixture
base_url: Base URL fixture
request: Pytest request node
Yields:
Page: Page object with video recording enabled
"""
context = browser.new_context(record_video_dir="videos/")
page = context.new_page()
yield page
# Save video if test failed
if request.node.rep_call.failed:
video_path = page.video.path()
print(f"\nVideo saved: {video_path}")
page.close()
context.close()
@pytest.fixture(autouse=True)
def track_page_for_screenshots(request):
"""
Track page object for automatic screenshot capture on test failure.
This autouse fixture stores a reference to the page object in the
test node, allowing the pytest_runtest_makereport hook to capture
screenshots when tests fail.
Args:
request: Pytest request object
Yields:
None: Allows test to execute
"""
# Skip tracking for unit tests marked with no_browser
if request.node.get_closest_marker('no_browser'):
yield
return
# Only track if page fixture is available in the test
if hasattr(request, "funcargs"):
page = request.funcargs.get("page") or request.funcargs.get("authenticated_page") or request.funcargs.get("authenticated_page_api")
if page and hasattr(request, "node"):
request.node._page = page
yield
# Pytest hooks for screenshot/video capture
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""
Pytest hook to capture test results for screenshot/video capture.
Automatically captures screenshots on test failure and saves them
to artifacts/screenshots/ with descriptive filenames including timestamp
and test name for easy debugging in CI and local development.
Args:
item: Pytest test item
call: Pytest call info
Returns:
Test report with outcome information
"""
outcome = yield
rep = outcome.get_result()
# Store test outcome in request.node for fixtures to access
setattr(item, "rep_" + rep.when, rep)
# Capture screenshot on test failure
if rep.when == "call" and rep.failed:
# Get page fixture if available
page = getattr(item, "_page", None)
if page is None:
# Try to get page from function args
if hasattr(item, "funcargs"):
page = item.funcargs.get("page") or item.funcargs.get("authenticated_page")
if page is not None:
# Create screenshots directory if not exists
screenshot_dir = "backend/tests/e2e_ui/artifacts/screenshots"
os.makedirs(screenshot_dir, exist_ok=True)
# Generate descriptive filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
test_name = item.name.replace("::", "_").replace("/", "_")[:100]
screenshot_path = f"{screenshot_dir}/{timestamp}_{test_name}.png"
# Capture full page screenshot
page.screenshot(path=screenshot_path, full_page=True)
print(f"\nScreenshot saved: {screenshot_path}")
# Attach screenshot to Allure report
if ALLURE_AVAILABLE:
try:
allure.attach.file(
screenshot_path,
name=f"Screenshot: {item.name}",
attachment_type=allure.attachment_type.PNG
)
except Exception as e:
print(f"Failed to attach screenshot to Allure: {e}")
# Save video if in CI environment
if is_ci_environment():
video_path = page.video.path()
if video_path and os.path.exists(video_path):
# Rename video with test name and timestamp
video_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
video_test_name = item.name.replace("::", "_").replace("/", "_")[:100]
named_video_path = f"backend/tests/e2e_ui/artifacts/videos/{video_timestamp}_{video_test_name}.webm"
os.rename(video_path, named_video_path)
print(f"\nVideo saved: {named_video_path}")
# Attach video to Allure report
if ALLURE_AVAILABLE:
try:
allure.attach.file(
named_video_path,
name=f"Video: {item.name}",
attachment_type=allure.attachment_type.WEBM
)
except Exception as e:
print(f"Failed to attach video to Allure: {e}")
# ============================================================================
# Pytest-HTML Report Hooks
# ============================================================================
# Only register pytest-html hooks if the plugin is available
try:
# Check if pytest-html is installed by importing the plugin
import pytest_html
PYTEST_HTML_AVAILABLE = True
except ImportError:
PYTEST_HTML_AVAILABLE = False
if PYTEST_HTML_AVAILABLE:
def pytest_html_results_summary(prefix, summary, postfix):
"""
Add custom content to pytest HTML report summary.
Args:
prefix: List of HTML elements to insert before summary
summary: Summary data
postfix: List of HTML elements to insert after summary
"""
prefix.extend([
"<h2>Atom E2E UI Test Report</h2>",
"<p>Generated on: {}</p>".format(
datetime.now().strftime('%Y-%m-%d %H:%M:%S')
),
])
def pytest_html_results_table_row(report, cells):
"""
Add screenshot link to failed test rows in HTML report.
Args:
report: Pytest test report
cells: List of table cells for this test row
"""
if report.failed:
# Check if screenshot exists
screenshot_dir = "backend/tests/e2e_ui/artifacts/screenshots"
test_name = report.nodeid.replace("::", "_").replace("/", "_")[:100]
# Look for matching screenshot files
if os.path.exists(screenshot_dir):
for filename in sorted(os.listdir(screenshot_dir), reverse=True):
if test_name in filename and filename.endswith(".png"):
screenshot_path = os.path.join(screenshot_dir, filename)
# Add screenshot cell
cells.append(
f'<td><a href="{screenshot_path}">Screenshot</a></td>'
)
break
def pytest_html_results_table_header(cells):
"""
Add screenshot column header to HTML report.
Args:
cells: List of table header cells
"""
cells.append("<th>Screenshot</th>")
|