reporting-labs 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. reporting_labs-0.1.0/LICENSE +21 -0
  2. reporting_labs-0.1.0/PKG-INFO +163 -0
  3. reporting_labs-0.1.0/README.md +127 -0
  4. reporting_labs-0.1.0/pyproject.toml +50 -0
  5. reporting_labs-0.1.0/setup.cfg +4 -0
  6. reporting_labs-0.1.0/src/reporting_labs/__init__.py +163 -0
  7. reporting_labs-0.1.0/src/reporting_labs/_version.py +1 -0
  8. reporting_labs-0.1.0/src/reporting_labs/capture/__init__.py +0 -0
  9. reporting_labs-0.1.0/src/reporting_labs/capture/http.py +181 -0
  10. reporting_labs-0.1.0/src/reporting_labs/core/__init__.py +0 -0
  11. reporting_labs-0.1.0/src/reporting_labs/core/collector.py +391 -0
  12. reporting_labs-0.1.0/src/reporting_labs/core/context.py +44 -0
  13. reporting_labs-0.1.0/src/reporting_labs/core/envdetect.py +194 -0
  14. reporting_labs-0.1.0/src/reporting_labs/core/explain.py +414 -0
  15. reporting_labs-0.1.0/src/reporting_labs/core/history.py +45 -0
  16. reporting_labs-0.1.0/src/reporting_labs/core/logo.py +26 -0
  17. reporting_labs-0.1.0/src/reporting_labs/core/mask.py +263 -0
  18. reporting_labs-0.1.0/src/reporting_labs/core/model.py +175 -0
  19. reporting_labs-0.1.0/src/reporting_labs/core/options.py +230 -0
  20. reporting_labs-0.1.0/src/reporting_labs/core/render.py +78 -0
  21. reporting_labs-0.1.0/src/reporting_labs/core/writer.py +79 -0
  22. reporting_labs-0.1.0/src/reporting_labs/playwright_support.py +448 -0
  23. reporting_labs-0.1.0/src/reporting_labs/py.typed +0 -0
  24. reporting_labs-0.1.0/src/reporting_labs/pytest_plugin.py +407 -0
  25. reporting_labs-0.1.0/src/reporting_labs/robot/__init__.py +3 -0
  26. reporting_labs-0.1.0/src/reporting_labs/robot/listener.py +293 -0
  27. reporting_labs-0.1.0/src/reporting_labs/selenium_support.py +234 -0
  28. reporting_labs-0.1.0/src/reporting_labs/template.html +1694 -0
  29. reporting_labs-0.1.0/src/reporting_labs/transport.py +64 -0
  30. reporting_labs-0.1.0/src/reporting_labs.egg-info/PKG-INFO +163 -0
  31. reporting_labs-0.1.0/src/reporting_labs.egg-info/SOURCES.txt +38 -0
  32. reporting_labs-0.1.0/src/reporting_labs.egg-info/dependency_links.txt +1 -0
  33. reporting_labs-0.1.0/src/reporting_labs.egg-info/entry_points.txt +2 -0
  34. reporting_labs-0.1.0/src/reporting_labs.egg-info/requires.txt +14 -0
  35. reporting_labs-0.1.0/src/reporting_labs.egg-info/top_level.txt +1 -0
  36. reporting_labs-0.1.0/tests/test_collector.py +83 -0
  37. reporting_labs-0.1.0/tests/test_explain.py +91 -0
  38. reporting_labs-0.1.0/tests/test_mask.py +81 -0
  39. reporting_labs-0.1.0/tests/test_options_env.py +63 -0
  40. reporting_labs-0.1.0/tests/test_plugin.py +111 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Naveen Khunteta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: reporting-labs
3
+ Version: 0.1.0
4
+ Summary: A beautiful test report in one HTML file, for pytest, Playwright, Selenium and Robot Framework. It tells you what broke, who owns it, and whether it is new.
5
+ Author-email: Naveen Khunteta <naveenanimation20@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://reportinglabs.dev
8
+ Project-URL: Documentation, https://reportinglabs.dev/get-started/python
9
+ Project-URL: Repository, https://github.com/naveenautomationlabs/reporting-labs-python
10
+ Project-URL: Issues, https://github.com/naveenautomationlabs/reporting-labs-python/issues
11
+ Keywords: pytest,playwright,selenium,robotframework,test-report,html-report,reporting,allure-alternative
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Framework :: Robot Framework
14
+ Classifier: Framework :: Robot Framework :: Tool
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: pytest
24
+ Requires-Dist: pytest>=7.0; extra == "pytest"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: pytest-xdist>=3.0; extra == "dev"
28
+ Requires-Dist: pytest-rerunfailures>=12.0; extra == "dev"
29
+ Requires-Dist: pytest-timeout>=2.1; extra == "dev"
30
+ Requires-Dist: requests>=2.25; extra == "dev"
31
+ Requires-Dist: httpx>=0.23; extra == "dev"
32
+ Requires-Dist: playwright>=1.40; extra == "dev"
33
+ Requires-Dist: selenium>=4.10; extra == "dev"
34
+ Requires-Dist: robotframework>=6.0; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # reportingLabs for Python
38
+
39
+ **One beautiful HTML report for pytest, Playwright, Selenium and Robot Framework.** It tells you what broke,
40
+ who owns it, and whether it is new. The whole report is a single self-contained HTML file: no server, no
41
+ upload, no login. Open it in a browser, attach it to a CI job, or drop it in Slack.
42
+
43
+ The report is byte-for-byte the same layout as the [Node.js](https://github.com/naveenautomationlabs/reporting-labs)
44
+ and [Java](https://github.com/naveenautomationlabs/reporting-labs-java) reporters, so one triage habit works
45
+ across a whole company. Nothing from Node is needed at run time: the report is built entirely in Python.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install reporting-labs
51
+ ```
52
+
53
+ That is all. The pytest plugin turns on the moment the package is installed. For Robot Framework, add one
54
+ listener flag (below).
55
+
56
+ ## pytest
57
+
58
+ Run your tests as you always do:
59
+
60
+ ```bash
61
+ pytest
62
+ open reporting-labs/index.html # run again to see the trend and flaky history
63
+ ```
64
+
65
+ Add detail with a few optional helpers:
66
+
67
+ ```python
68
+ import pytest
69
+ from reporting_labs import meta, log, test_data, step, api, attach
70
+
71
+ @pytest.mark.meta(priority="P1", owner="asha", feature="checkout", story="SHOP-231")
72
+ def test_checkout(page):
73
+ test_data({"username": "demo", "password": "S3cret"}, "Login") # sensitive keys are masked
74
+ log("cart total before coupons: 99.00")
75
+ with step("Apply coupon"):
76
+ ...
77
+ ```
78
+
79
+ | What you get | How |
80
+ |---|---|
81
+ | A row per test: passed, failed, skipped (with the reason), xfail / xpass | automatic |
82
+ | Flaky, with every attempt | `pytest-rerunfailures` |
83
+ | Timed out | `pytest-timeout` |
84
+ | Parallel runs merged into one report | `pytest-xdist` (`-n auto`) |
85
+ | Priority, owner, feature charts and filters | `@pytest.mark.meta(...)` or `meta(...)` |
86
+ | Every `requests` / `httpx` call in the API tab | automatic |
87
+ | Captured stdout / stderr, with secrets masked | automatic |
88
+
89
+ Turn it off for a run with `-p no:reporting_labs` or `--no-rl`.
90
+
91
+ ## Playwright
92
+
93
+ Install `pytest-playwright` and write tests as usual. Every action, every `expect()` and a screenshot of the
94
+ failing page land in the report with no extra code. pytest-playwright's trace and video are attached too:
95
+
96
+ ```bash
97
+ pytest --tracing retain-on-failure --video retain-on-failure
98
+ ```
99
+
100
+ The sync and async APIs both work. See `examples/pytest-playwright`.
101
+
102
+ ## Selenium
103
+
104
+ Install `selenium` and write tests as usual. Every driver command (open, find, click, type, run script,
105
+ perform actions, Appium taps) becomes a step, with the element named by the locator that found it, and a
106
+ screenshot of the browser is attached when a test fails. No wrappers. See `examples/pytest-selenium`.
107
+
108
+ ## Robot Framework
109
+
110
+ Add one listener flag:
111
+
112
+ ```bash
113
+ robot --listener reporting_labs.RobotListener tests/
114
+ ```
115
+
116
+ One row per test case, suites as the path, every keyword a step, setup and teardown as hooks, tags as
117
+ filters, and the screenshots SeleniumLibrary or the Browser library embed in the log attached to the test.
118
+ Options go after a colon: `--listener reporting_labs.RobotListener:title=Checkout:output=reports/rl`.
119
+ See `examples/robot-selenium`.
120
+
121
+ ## Configuration
122
+
123
+ Every option is optional. Put them in `reporting-labs.config.json` next to where you run, or under
124
+ `[tool.reporting-labs]` in `pyproject.toml`. Keys may be written `camelCase` (as in the Node reporter) or
125
+ `snake_case`.
126
+
127
+ ```json
128
+ {
129
+ "title": "Checkout regression",
130
+ "project": { "name": "ShopLite", "version": "2.4.0", "team": "QA Platform" },
131
+ "metadata": { "env": "local" },
132
+ "logo": "logo.png",
133
+ "palette": "lab",
134
+ "links": { "story": "https://acme.atlassian.net/browse/{id}" },
135
+ "maskKeys": ["otp", "pan"],
136
+ "history": { "enabled": true, "keep": 30 }
137
+ }
138
+ ```
139
+
140
+ Common options: `title`, `logo`, `accent`, `theme` (`auto` / `light` / `dark`), `palette`
141
+ (`lab` / `ocean` / `ember` / `mono`), `outputFolder`, `metadata`, `project`, `env`, `dimensions`,
142
+ `links`, `maskKeys`, `maskValues`, `maskFromEnv`, `history`, `open` (`on-failure` / `always` / `never`).
143
+
144
+ **The environment chip** is found for you from `ENV`, `TEST_ENV`, `APP_ENV`, `TARGET_ENV`,
145
+ `CI_ENVIRONMENT_NAME`, or any variable ending in `_ENV`, so a config that says `local` still labels the
146
+ pipeline's reports `dev`, `qa`, `stage`. Point it at your own variable with `"envVar": "TARGET"`.
147
+
148
+ **Runtime overrides**, read from the environment and winning over the config, so CI can label a run without
149
+ touching the file: `REPORTING_LABS_METADATA_<KEY>` sets a header chip
150
+ (`REPORTING_LABS_METADATA_ENV=qa`), and `REPORTING_LABS_TITLE`, `_THEME`, `_PALETTE`, `_ACCENT`, `_LOGO`
151
+ set the matching option.
152
+
153
+ ## Secrets are masked
154
+
155
+ Passwords, tokens, cookies, auth headers, API keys (Stripe, GitHub, AWS, Google, GitLab, npm, SendGrid),
156
+ JWTs and Luhn-valid card numbers are blanked in logs, request and response bodies, error messages, test
157
+ data and step titles. A value seen once as a secret (`password=...`, a `PASSWORD` env var, or `maskValues`)
158
+ is blanked everywhere it later appears. `maskKeys` adds your own keys; `maskFromEnv` (on by default) learns
159
+ the values of `PASSWORD` / `*_TOKEN` / `*_SECRET` environment variables.
160
+
161
+ ## License
162
+
163
+ MIT. The report is a file you own; nothing leaves your machine or your CI.
@@ -0,0 +1,127 @@
1
+ # reportingLabs for Python
2
+
3
+ **One beautiful HTML report for pytest, Playwright, Selenium and Robot Framework.** It tells you what broke,
4
+ who owns it, and whether it is new. The whole report is a single self-contained HTML file: no server, no
5
+ upload, no login. Open it in a browser, attach it to a CI job, or drop it in Slack.
6
+
7
+ The report is byte-for-byte the same layout as the [Node.js](https://github.com/naveenautomationlabs/reporting-labs)
8
+ and [Java](https://github.com/naveenautomationlabs/reporting-labs-java) reporters, so one triage habit works
9
+ across a whole company. Nothing from Node is needed at run time: the report is built entirely in Python.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install reporting-labs
15
+ ```
16
+
17
+ That is all. The pytest plugin turns on the moment the package is installed. For Robot Framework, add one
18
+ listener flag (below).
19
+
20
+ ## pytest
21
+
22
+ Run your tests as you always do:
23
+
24
+ ```bash
25
+ pytest
26
+ open reporting-labs/index.html # run again to see the trend and flaky history
27
+ ```
28
+
29
+ Add detail with a few optional helpers:
30
+
31
+ ```python
32
+ import pytest
33
+ from reporting_labs import meta, log, test_data, step, api, attach
34
+
35
+ @pytest.mark.meta(priority="P1", owner="asha", feature="checkout", story="SHOP-231")
36
+ def test_checkout(page):
37
+ test_data({"username": "demo", "password": "S3cret"}, "Login") # sensitive keys are masked
38
+ log("cart total before coupons: 99.00")
39
+ with step("Apply coupon"):
40
+ ...
41
+ ```
42
+
43
+ | What you get | How |
44
+ |---|---|
45
+ | A row per test: passed, failed, skipped (with the reason), xfail / xpass | automatic |
46
+ | Flaky, with every attempt | `pytest-rerunfailures` |
47
+ | Timed out | `pytest-timeout` |
48
+ | Parallel runs merged into one report | `pytest-xdist` (`-n auto`) |
49
+ | Priority, owner, feature charts and filters | `@pytest.mark.meta(...)` or `meta(...)` |
50
+ | Every `requests` / `httpx` call in the API tab | automatic |
51
+ | Captured stdout / stderr, with secrets masked | automatic |
52
+
53
+ Turn it off for a run with `-p no:reporting_labs` or `--no-rl`.
54
+
55
+ ## Playwright
56
+
57
+ Install `pytest-playwright` and write tests as usual. Every action, every `expect()` and a screenshot of the
58
+ failing page land in the report with no extra code. pytest-playwright's trace and video are attached too:
59
+
60
+ ```bash
61
+ pytest --tracing retain-on-failure --video retain-on-failure
62
+ ```
63
+
64
+ The sync and async APIs both work. See `examples/pytest-playwright`.
65
+
66
+ ## Selenium
67
+
68
+ Install `selenium` and write tests as usual. Every driver command (open, find, click, type, run script,
69
+ perform actions, Appium taps) becomes a step, with the element named by the locator that found it, and a
70
+ screenshot of the browser is attached when a test fails. No wrappers. See `examples/pytest-selenium`.
71
+
72
+ ## Robot Framework
73
+
74
+ Add one listener flag:
75
+
76
+ ```bash
77
+ robot --listener reporting_labs.RobotListener tests/
78
+ ```
79
+
80
+ One row per test case, suites as the path, every keyword a step, setup and teardown as hooks, tags as
81
+ filters, and the screenshots SeleniumLibrary or the Browser library embed in the log attached to the test.
82
+ Options go after a colon: `--listener reporting_labs.RobotListener:title=Checkout:output=reports/rl`.
83
+ See `examples/robot-selenium`.
84
+
85
+ ## Configuration
86
+
87
+ Every option is optional. Put them in `reporting-labs.config.json` next to where you run, or under
88
+ `[tool.reporting-labs]` in `pyproject.toml`. Keys may be written `camelCase` (as in the Node reporter) or
89
+ `snake_case`.
90
+
91
+ ```json
92
+ {
93
+ "title": "Checkout regression",
94
+ "project": { "name": "ShopLite", "version": "2.4.0", "team": "QA Platform" },
95
+ "metadata": { "env": "local" },
96
+ "logo": "logo.png",
97
+ "palette": "lab",
98
+ "links": { "story": "https://acme.atlassian.net/browse/{id}" },
99
+ "maskKeys": ["otp", "pan"],
100
+ "history": { "enabled": true, "keep": 30 }
101
+ }
102
+ ```
103
+
104
+ Common options: `title`, `logo`, `accent`, `theme` (`auto` / `light` / `dark`), `palette`
105
+ (`lab` / `ocean` / `ember` / `mono`), `outputFolder`, `metadata`, `project`, `env`, `dimensions`,
106
+ `links`, `maskKeys`, `maskValues`, `maskFromEnv`, `history`, `open` (`on-failure` / `always` / `never`).
107
+
108
+ **The environment chip** is found for you from `ENV`, `TEST_ENV`, `APP_ENV`, `TARGET_ENV`,
109
+ `CI_ENVIRONMENT_NAME`, or any variable ending in `_ENV`, so a config that says `local` still labels the
110
+ pipeline's reports `dev`, `qa`, `stage`. Point it at your own variable with `"envVar": "TARGET"`.
111
+
112
+ **Runtime overrides**, read from the environment and winning over the config, so CI can label a run without
113
+ touching the file: `REPORTING_LABS_METADATA_<KEY>` sets a header chip
114
+ (`REPORTING_LABS_METADATA_ENV=qa`), and `REPORTING_LABS_TITLE`, `_THEME`, `_PALETTE`, `_ACCENT`, `_LOGO`
115
+ set the matching option.
116
+
117
+ ## Secrets are masked
118
+
119
+ Passwords, tokens, cookies, auth headers, API keys (Stripe, GitHub, AWS, Google, GitLab, npm, SendGrid),
120
+ JWTs and Luhn-valid card numbers are blanked in logs, request and response bodies, error messages, test
121
+ data and step titles. A value seen once as a secret (`password=...`, a `PASSWORD` env var, or `maskValues`)
122
+ is blanked everywhere it later appears. `maskKeys` adds your own keys; `maskFromEnv` (on by default) learns
123
+ the values of `PASSWORD` / `*_TOKEN` / `*_SECRET` environment variables.
124
+
125
+ ## License
126
+
127
+ MIT. The report is a file you own; nothing leaves your machine or your CI.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "reporting-labs"
7
+ dynamic = ["version"]
8
+ description = "A beautiful test report in one HTML file, for pytest, Playwright, Selenium and Robot Framework. It tells you what broke, who owns it, and whether it is new."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Naveen Khunteta", email = "naveenanimation20@gmail.com" }]
13
+ keywords = ["pytest", "playwright", "selenium", "robotframework", "test-report", "html-report", "reporting", "allure-alternative"]
14
+ classifiers = [
15
+ "Framework :: Pytest",
16
+ "Framework :: Robot Framework",
17
+ "Framework :: Robot Framework :: Tool",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Topic :: Software Development :: Testing",
23
+ ]
24
+ dependencies = []
25
+
26
+ [project.optional-dependencies]
27
+ pytest = ["pytest>=7.0"]
28
+ dev = ["pytest>=7.0", "pytest-xdist>=3.0", "pytest-rerunfailures>=12.0", "pytest-timeout>=2.1", "requests>=2.25", "httpx>=0.23", "playwright>=1.40", "selenium>=4.10", "robotframework>=6.0"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://reportinglabs.dev"
32
+ Documentation = "https://reportinglabs.dev/get-started/python"
33
+ Repository = "https://github.com/naveenautomationlabs/reporting-labs-python"
34
+ Issues = "https://github.com/naveenautomationlabs/reporting-labs-python/issues"
35
+
36
+ [project.entry-points.pytest11]
37
+ reporting_labs = "reporting_labs.pytest_plugin"
38
+
39
+ [tool.setuptools.dynamic]
40
+ version = { attr = "reporting_labs._version.__version__" }
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
44
+
45
+ [tool.setuptools.package-data]
46
+ reporting_labs = ["template.html", "py.typed"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ addopts = "-p no:reporting_labs"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,163 @@
1
+ """reportingLabs for Python: one HTML report for pytest, Playwright, Selenium and Robot Framework.
2
+
3
+ Helpers to call from a test:
4
+
5
+ from reporting_labs import meta, log, test_data, step, api, attach
6
+
7
+ meta(priority="P1", owner="asha", feature="checkout")
8
+ log("cart total before coupons: 99.00")
9
+ test_data({"username": "demo", "password": "x"}, "Login")
10
+ with step("Apply coupon"):
11
+ ...
12
+ api(method="GET", url="/v1/orders", status=200, duration=120, response_body=body)
13
+ attach("screenshot", png_bytes, "image/png")
14
+
15
+ Secrets (password=..., Bearer ..., tokens) are masked in the report.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import functools
20
+ import inspect
21
+ import json
22
+ from contextlib import contextmanager
23
+ from typing import Any, Callable, Dict, Iterator, Optional, Union
24
+
25
+ from ._version import __version__
26
+ from .core import context
27
+ from .core.model import ErrorInfo, now_ms
28
+
29
+ __all__ = ["meta", "log", "test_data", "step", "api", "attach", "RobotListener", "__version__"]
30
+
31
+
32
+ def __getattr__(name: str) -> Any:
33
+ # `robot --listener reporting_labs.RobotListener`: imported lazily so a pytest project never loads Robot code
34
+ if name == "RobotListener":
35
+ from .robot.listener import ReportingLabsListener
36
+ return ReportingLabsListener
37
+ raise AttributeError(name)
38
+
39
+
40
+ def meta(**values: Any) -> None:
41
+ """Attach report metadata to the current test: priority, severity, owner, feature, epic, story, issue... or any key.
42
+ A value can be a list (one chip per value) or a dict (the fields of a multi-parameter link, see `links`)."""
43
+ a = context.current()
44
+ if a is None:
45
+ return
46
+ for k, v in values.items():
47
+ if v is not None:
48
+ a.meta[str(k).lower()] = v
49
+
50
+
51
+ def log(message: Any, *rest: Any) -> None:
52
+ """Add a timestamped log line to the current test. Lines containing "error"/"fail" show red, "warn" amber."""
53
+ a = context.current()
54
+ if a is None:
55
+ return
56
+ parts = [str(message)] + [r if isinstance(r, str) else json.dumps(r, default=str) for r in rest]
57
+ a.log(" ".join(parts))
58
+
59
+
60
+ def test_data(data: Any, name: str = "Test data") -> None:
61
+ """Attach the data this test uses: a dict (key/value block), a list of dicts (table) or a CSV string (table).
62
+ Sensitive keys are masked."""
63
+ a = context.current()
64
+ if a is not None:
65
+ a.test_data(data, name)
66
+
67
+
68
+ def api(method: str, url: str, status: Optional[int] = None, duration: Optional[float] = None, name: Optional[str] = None,
69
+ request_headers: Optional[Dict[str, str]] = None, request_body: Any = None,
70
+ response_headers: Optional[Dict[str, str]] = None, response_body: Any = None) -> None:
71
+ """Record an API call by hand. requests and httpx calls are recorded automatically."""
72
+ a = context.current()
73
+ if a is None:
74
+ return
75
+ call: Dict[str, Any] = {"method": str(method).upper(), "url": str(url)}
76
+ if status is not None:
77
+ call["status"] = int(status)
78
+ if duration is not None:
79
+ call["duration"] = float(duration)
80
+ if name:
81
+ call["name"] = name
82
+ if request_headers:
83
+ call["requestHeaders"] = dict(request_headers)
84
+ if request_body is not None:
85
+ call["requestBody"] = request_body
86
+ if response_headers:
87
+ call["responseHeaders"] = dict(response_headers)
88
+ if response_body is not None:
89
+ call["responseBody"] = response_body
90
+ a.api_call(call)
91
+
92
+
93
+ def attach(name: str, body: Union[bytes, str, None] = None, content_type: Optional[str] = None, path: Optional[str] = None) -> None:
94
+ """Attach a file or bytes to the current test: a screenshot, a downloaded PDF, a log. Images are shown inline."""
95
+ a = context.current()
96
+ if a is None:
97
+ return
98
+ if isinstance(body, str):
99
+ body = body.encode("utf-8")
100
+ content_type = content_type or "text/plain"
101
+ if content_type is None:
102
+ ext = (path or name).lower().rsplit(".", 1)[-1] if "." in (path or name) else ""
103
+ content_type = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "webp": "image/webp", "gif": "image/gif", "webm": "video/webm",
104
+ "mp4": "video/mp4", "txt": "text/plain", "log": "text/plain", "json": "application/json", "html": "text/html", "csv": "text/csv",
105
+ "zip": "application/zip", "pdf": "application/pdf"}.get(ext, "application/octet-stream")
106
+ a.attach(name, body, content_type, path)
107
+
108
+
109
+ test_data.__test__ = False # pytest must not collect the helper when a test module imports it
110
+
111
+
112
+ class _StepContext:
113
+ """`with step("title"):` and `@step("title")` on a function (sync or async)."""
114
+
115
+ def __init__(self, title: str, category: str = "test.step") -> None:
116
+ self.title, self.category = title, category
117
+ self._st = None
118
+ self._attempt = None
119
+
120
+ def __enter__(self) -> "_StepContext":
121
+ self._attempt = context.current()
122
+ if self._attempt is not None:
123
+ self._st = self._attempt.begin_step(self.title, self.category)
124
+ return self
125
+
126
+ def __exit__(self, et, ev, tb) -> bool:
127
+ if self._attempt is not None and self._st is not None:
128
+ self._attempt.end_step(self._st, f"{et.__name__}: {ev}" if et else None)
129
+ return False
130
+
131
+ async def __aenter__(self) -> "_StepContext":
132
+ return self.__enter__()
133
+
134
+ async def __aexit__(self, et, ev, tb) -> bool:
135
+ return self.__exit__(et, ev, tb)
136
+
137
+ def __call__(self, fn: Callable) -> Callable:
138
+ if inspect.iscoroutinefunction(fn):
139
+ @functools.wraps(fn)
140
+ async def aw(*args: Any, **kw: Any) -> Any:
141
+ async with _StepContext(self.title or fn.__name__, self.category):
142
+ return await fn(*args, **kw)
143
+ return aw
144
+
145
+ @functools.wraps(fn)
146
+ def w(*args: Any, **kw: Any) -> Any:
147
+ with _StepContext(self.title or fn.__name__, self.category):
148
+ return fn(*args, **kw)
149
+ return w
150
+
151
+
152
+ def step(title: Union[str, Callable, None] = None, category: str = "test.step") -> Any:
153
+ """A named step in the report. Use as a context manager or a decorator; steps nest.
154
+
155
+ with step("Log in as admin"):
156
+ page.fill("#user", "admin")
157
+
158
+ @step("Open the cart")
159
+ def open_cart(page): ...
160
+ """
161
+ if callable(title):
162
+ return _StepContext(title.__name__, category)(title)
163
+ return _StepContext(str(title or "step"), category)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"