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