From b99be67b63cf8a0ddc562aa4220a1d62890daf05 Mon Sep 17 00:00:00 2001 From: Hasan Alp Zengin Date: Fri, 31 Jul 2026 16:16:08 +0200 Subject: [PATCH] chore: enable playwright-trace for allure_pytest and allure_robotframework #877 --- allure-pytest/src/listener.py | 35 ++++++ .../src/allure_commons/types.py | 1 + .../src/listener/robot_listener.py | 36 ++++++ .../externals/pytest_playwright/__init__.py | 0 .../externals/pytest_playwright/conftest.py | 7 ++ .../pytest_playwright_test.py | 92 +++++++++++++++ .../externals/__init__.py | 0 .../robotframework_browser/__init__.py | 0 .../browser_traces_test.py | 111 ++++++++++++++++++ .../fake_browser_library.py | 33 ++++++ 10 files changed, 315 insertions(+) create mode 100644 tests/allure_pytest/externals/pytest_playwright/__init__.py create mode 100644 tests/allure_pytest/externals/pytest_playwright/conftest.py create mode 100644 tests/allure_pytest/externals/pytest_playwright/pytest_playwright_test.py create mode 100644 tests/allure_robotframework/externals/__init__.py create mode 100644 tests/allure_robotframework/externals/robotframework_browser/__init__.py create mode 100644 tests/allure_robotframework/externals/robotframework_browser/browser_traces_test.py create mode 100644 tests/allure_robotframework/externals/robotframework_browser/fake_browser_library.py diff --git a/allure-pytest/src/listener.py b/allure-pytest/src/listener.py index 10ec29df..ba827547 100644 --- a/allure-pytest/src/listener.py +++ b/allure-pytest/src/listener.py @@ -1,5 +1,6 @@ import pytest import doctest +from pathlib import Path import allure_commons from allure_commons.utils import now @@ -242,6 +243,40 @@ def pytest_runtest_makereport(self, item, call): if report.capstderr: self.attach_data(report.capstderr, "stderr", AttachmentType.TEXT, None) + self._attach_playwright_traces(item) + + def _attach_playwright_traces(self, item): + if item.config.pluginmanager.get_plugin("playwright") is None: + return + if getattr(item.config.option, "tracing", "off") == "off": + return + + output = Path(item.config.getoption("--output", default="test-results")).absolute() + + try: + from slugify import slugify as _slugify + except ImportError: + return + + slug = _slugify(item.nodeid) + # pytest-playwright truncates slugs longer than 255 chars + if len(slug) >= 256: + import hashlib + slug = f"{slug[:100]}-{hashlib.sha256(slug.encode()).hexdigest()[:7]}-{slug[-100:]}" + + test_folder = output / slug + + if not test_folder.is_dir(): + return + + for trace_file in sorted(test_folder.glob("trace*.zip")): + self.allure_logger.attach_file( + uuid4(), + str(trace_file), + name=trace_file.name, + attachment_type=AttachmentType.PLAYWRIGHT_TRACE, + ) + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_logfinish(self, nodeid, location): yield diff --git a/allure-python-commons/src/allure_commons/types.py b/allure-python-commons/src/allure_commons/types.py index 43b8d5ed..bffa5973 100644 --- a/allure-python-commons/src/allure_commons/types.py +++ b/allure-python-commons/src/allure_commons/types.py @@ -51,6 +51,7 @@ def __init__(self, mime_type, extension): YAML = ("application/yaml", "yaml") PCAP = ("application/vnd.tcpdump.pcap", "pcap") ZIP = ("application/zip", "zip") + PLAYWRIGHT_TRACE = ("application/vnd.allure.playwright-trace", "zip") PNG = ("image/png", "png") JPG = ("image/jpg", "jpg") diff --git a/allure-robotframework/src/listener/robot_listener.py b/allure-robotframework/src/listener/robot_listener.py index acc0624c..35a8db98 100644 --- a/allure-robotframework/src/listener/robot_listener.py +++ b/allure-robotframework/src/listener/robot_listener.py @@ -1,8 +1,11 @@ import os +from pathlib import Path import allure_commons from allure_commons.lifecycle import AllureLifecycle from allure_commons.logger import AllureFileLogger +from allure_commons.types import AttachmentType +from allure_commons.utils import uuid4 from allure_robotframework.allure_listener import AllureListener from allure_robotframework.types import RobotKeywordType @@ -39,12 +42,45 @@ def start_test(self, name, attributes): self.messages.start_context() self.listener.start_test_container(name, attributes) self.listener.start_test(name, {**attributes, "titlepath": self.title_path}) + self._browser_traces_before = self._snapshot_browser_traces() def end_test(self, name, attributes): messages = self.messages.stop_context() self.listener.stop_test(name, attributes, messages) + self._attach_new_browser_traces() self.listener.stop_test_container(name, attributes) + def _browser_folder(self): + try: + from robot.libraries.BuiltIn import BuiltIn + outputdir = BuiltIn().get_variable_value("${OUTPUTDIR}") + except Exception: + return None + if not outputdir: + return None + folder = Path(outputdir) / "browser" + return folder if folder.is_dir() else None + + def _snapshot_browser_traces(self): + folder = self._browser_folder() + if folder is None: + return set() + return set(folder.glob("**/*.zip")) + + def _attach_new_browser_traces(self): + folder = self._browser_folder() + if folder is None: + return + before = getattr(self, "_browser_traces_before", set()) + for trace_file in sorted(folder.glob("**/*.zip")): + if trace_file not in before: + self.lifecycle.attach_file( + uuid4(), + str(trace_file), + name=trace_file.name, + attachment_type=AttachmentType.PLAYWRIGHT_TRACE, + ) + def start_keyword(self, name, attributes): self.messages.start_context() keyword_type = attributes.get("type") diff --git a/tests/allure_pytest/externals/pytest_playwright/__init__.py b/tests/allure_pytest/externals/pytest_playwright/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_pytest/externals/pytest_playwright/conftest.py b/tests/allure_pytest/externals/pytest_playwright/conftest.py new file mode 100644 index 00000000..7976fe67 --- /dev/null +++ b/tests/allure_pytest/externals/pytest_playwright/conftest.py @@ -0,0 +1,7 @@ +collect_ignore_glob = [] + +def pytest_configure(config): + # Disable the outer playwright plugin to avoid _soft_errors global leaking + # into nested in-process pytester runs. + config.pluginmanager.set_blocked("playwright") + diff --git a/tests/allure_pytest/externals/pytest_playwright/pytest_playwright_test.py b/tests/allure_pytest/externals/pytest_playwright/pytest_playwright_test.py new file mode 100644 index 00000000..80a7c57c --- /dev/null +++ b/tests/allure_pytest/externals/pytest_playwright/pytest_playwright_test.py @@ -0,0 +1,92 @@ +import os +import pytest +from hamcrest import assert_that, not_ + +from tests.allure_pytest.pytest_runner import AllurePytestRunner +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_attachment + +pytest.importorskip("playwright", reason="playwright is not installed") + +# Capture the real home dir before pytester changes HOME to a tmpdir. +_REAL_HOME = os.path.expanduser("~") +_BROWSERS_PATH = os.path.join(_REAL_HOME, ".cache", "ms-playwright") + + +@pytest.fixture +def playwright_runner(allure_pytest_runner: AllurePytestRunner, monkeypatch): + monkeypatch.setenv("PLAYWRIGHT_BROWSERS_PATH", _BROWSERS_PATH) + allure_pytest_runner.enable_plugins("playwright", "base_url") + yield allure_pytest_runner + + +_PAGE_TEST = """ +def test_playwright_trace(page): + page.goto("about:blank") +""" + +_PAGE_TEST_FAILING = """ +def test_playwright_trace_failing(page): + page.goto("about:blank") + assert False +""" + + +def test_trace_attached_when_tracing_on(playwright_runner: AllurePytestRunner): + output = playwright_runner.run_pytest( + _PAGE_TEST, + cli_args=["--tracing", "on", "--browser", "chromium"], + ) + + assert_that( + output, + has_test_case( + "test_playwright_trace", + has_attachment(attach_type="application/vnd.allure.playwright-trace"), + ) + ) + + +def test_trace_attached_on_failure_with_retain_on_failure(playwright_runner: AllurePytestRunner): + output = playwright_runner.run_pytest( + _PAGE_TEST_FAILING, + cli_args=["--tracing", "retain-on-failure", "--browser", "chromium"], + ) + + assert_that( + output, + has_test_case( + "test_playwright_trace_failing", + has_attachment(attach_type="application/vnd.allure.playwright-trace"), + ) + ) + + +def test_trace_not_attached_on_pass_with_retain_on_failure(playwright_runner: AllurePytestRunner): + output = playwright_runner.run_pytest( + _PAGE_TEST, + cli_args=["--tracing", "retain-on-failure", "--browser", "chromium"], + ) + + assert_that( + output, + has_test_case( + "test_playwright_trace", + not_(has_attachment(attach_type="application/vnd.allure.playwright-trace")), + ) + ) + + +def test_trace_not_attached_when_tracing_off(playwright_runner: AllurePytestRunner): + output = playwright_runner.run_pytest( + _PAGE_TEST, + cli_args=["--tracing", "off", "--browser", "chromium"], + ) + + assert_that( + output, + has_test_case( + "test_playwright_trace", + not_(has_attachment(attach_type="application/vnd.allure.playwright-trace")), + ) + ) diff --git a/tests/allure_robotframework/externals/__init__.py b/tests/allure_robotframework/externals/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_robotframework/externals/robotframework_browser/__init__.py b/tests/allure_robotframework/externals/robotframework_browser/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_robotframework/externals/robotframework_browser/browser_traces_test.py b/tests/allure_robotframework/externals/robotframework_browser/browser_traces_test.py new file mode 100644 index 00000000..977cc044 --- /dev/null +++ b/tests/allure_robotframework/externals/robotframework_browser/browser_traces_test.py @@ -0,0 +1,111 @@ +from hamcrest import assert_that, not_ + +from tests.allure_robotframework.robot_runner import AllureRobotRunner +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_attachment + + +_SUITE_WITH_TRACE = """\ +*** Settings *** +Library fake_browser_library.py + +*** Test Cases *** +Browser Trace Test + Open Browser about:blank + Create Trace + Close Browser +""" + +_SUITE_WITHOUT_TRACE = """\ +*** Settings *** +Library fake_browser_library.py + +*** Test Cases *** +Browser No Trace Test + Open Browser about:blank + Do Nothing + Close Browser +""" + +_SUITE_WITH_TRACE_IN_SUBDIR = """\ +*** Settings *** +Library fake_browser_library.py + +*** Test Cases *** +Browser Subdir Trace Test + Open Browser about:blank + Create Trace In Subdir + Close Browser +""" + +_SUITE_FAILING_WITH_TRACE = """\ +*** Settings *** +Library fake_browser_library.py + +*** Test Cases *** +Browser Failing Test + Open Browser about:blank + Create Trace + Fail intentional failure + Close Browser +""" + + +def test_trace_attached_when_browser_creates_zip(robot_runner: AllureRobotRunner): + robot_runner.run_robotframework( + suite_literals={"browser_trace.robot": _SUITE_WITH_TRACE}, + library_paths=["fake_browser_library.py"], + ) + + assert_that( + robot_runner.allure_results, + has_test_case( + "Browser Trace Test", + has_attachment(attach_type="application/vnd.allure.playwright-trace"), + ), + ) + + +def test_trace_not_attached_when_no_zip_created(robot_runner: AllureRobotRunner): + robot_runner.run_robotframework( + suite_literals={"browser_no_trace.robot": _SUITE_WITHOUT_TRACE}, + library_paths=["fake_browser_library.py"], + ) + + assert_that( + robot_runner.allure_results, + has_test_case( + "Browser No Trace Test", + not_(has_attachment(attach_type="application/vnd.allure.playwright-trace")), + ), + ) + + +def test_trace_attached_when_browser_creates_zip_in_subdir(robot_runner: AllureRobotRunner): + robot_runner.run_robotframework( + suite_literals={"browser_trace_subdir.robot": _SUITE_WITH_TRACE_IN_SUBDIR}, + library_paths=["fake_browser_library.py"], + ) + + assert_that( + robot_runner.allure_results, + has_test_case( + "Browser Subdir Trace Test", + has_attachment(attach_type="application/vnd.allure.playwright-trace"), + ), + ) + + +def test_trace_attached_on_failing_test(robot_runner: AllureRobotRunner): + robot_runner.run_robotframework( + suite_literals={"browser_fail.robot": _SUITE_FAILING_WITH_TRACE}, + library_paths=["fake_browser_library.py"], + ) + + assert_that( + robot_runner.allure_results, + has_test_case( + "Browser Failing Test", + has_attachment(attach_type="application/vnd.allure.playwright-trace"), + ), + ) diff --git a/tests/allure_robotframework/externals/robotframework_browser/fake_browser_library.py b/tests/allure_robotframework/externals/robotframework_browser/fake_browser_library.py new file mode 100644 index 00000000..75ca2869 --- /dev/null +++ b/tests/allure_robotframework/externals/robotframework_browser/fake_browser_library.py @@ -0,0 +1,33 @@ +import zipfile +from pathlib import Path +from robot.libraries.BuiltIn import BuiltIn + + +def _outputdir(): + return Path(BuiltIn().get_variable_value("${OUTPUTDIR}")) + + +def open_browser(url="about:blank"): + pass + + +def close_browser(): + pass + + +def create_trace(): + trace_path = _outputdir() / "browser" / "trace-1.zip" + trace_path.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(str(trace_path), "w") as zf: + zf.writestr("trace.json", "{}") + + +def create_trace_in_subdir(): + trace_path = _outputdir() / "browser" / "traces" / "sometrace.zip" + trace_path.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(str(trace_path), "w") as zf: + zf.writestr("trace.json", "{}") + + +def do_nothing(): + pass