pytest-visionspec 0.2.0__tar.gz → 0.2.1__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.
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/PKG-INFO +1 -1
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pyproject.toml +1 -1
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec/__init__.py +38 -17
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec.egg-info/PKG-INFO +1 -1
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/README.md +0 -0
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec.egg-info/SOURCES.txt +0 -0
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec.egg-info/dependency_links.txt +0 -0
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec.egg-info/entry_points.txt +0 -0
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec.egg-info/requires.txt +0 -0
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec.egg-info/top_level.txt +0 -0
- {pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytest-visionspec"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Pytest plugin that auto-reports test results with screenshots to VisionSpec"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Usage:
|
|
4
4
|
pip install pytest-visionspec
|
|
5
5
|
Set env vars: VS_API_KEY, VS_API_URL
|
|
6
|
-
Tag tests: @pytest.mark.vs("journey-id")
|
|
6
|
+
Tag tests: @pytest.mark.vs("journey-id", revision="slug")
|
|
7
7
|
|
|
8
8
|
The plugin:
|
|
9
|
-
1. Detects @pytest.mark.vs("journey-id") tags on tests
|
|
9
|
+
1. Detects @pytest.mark.vs("journey-id", revision="slug") tags on tests
|
|
10
10
|
2. After each test, captures a Playwright screenshot (if a page fixture exists)
|
|
11
11
|
3. Uploads screenshots to VisionSpec storage
|
|
12
|
-
4. Posts the result (pass/fail + screenshot_urls +
|
|
12
|
+
4. Posts the result (pass/fail + screenshot_urls + screenshot_metadata) to VisionSpec
|
|
13
13
|
|
|
14
14
|
Labeled screenshots:
|
|
15
15
|
Use the vs_screenshot fixture to capture labeled screenshots mid-test:
|
|
@@ -20,7 +20,9 @@ Labeled screenshots:
|
|
|
20
20
|
vs_screenshot("dark mode") # captures + labels
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
+
import hashlib
|
|
23
24
|
import os
|
|
25
|
+
import struct
|
|
24
26
|
import sys
|
|
25
27
|
import glob
|
|
26
28
|
from pathlib import Path
|
|
@@ -124,9 +126,10 @@ def pytest_runtest_makereport(item, call):
|
|
|
124
126
|
# Upload any captured screenshots/videos
|
|
125
127
|
screenshot_urls = []
|
|
126
128
|
screenshot_labels = []
|
|
129
|
+
screenshot_metadata = []
|
|
127
130
|
video_url = None
|
|
128
131
|
try:
|
|
129
|
-
screenshot_urls, screenshot_labels, video_url = _collect_and_upload_artifacts(item, api_key, api_url)
|
|
132
|
+
screenshot_urls, screenshot_labels, screenshot_metadata, video_url = _collect_and_upload_artifacts(item, api_key, api_url)
|
|
130
133
|
except Exception:
|
|
131
134
|
pass
|
|
132
135
|
|
|
@@ -158,6 +161,7 @@ def pytest_runtest_makereport(item, call):
|
|
|
158
161
|
"versions": _get_versions(),
|
|
159
162
|
"screenshot_urls": screenshot_urls,
|
|
160
163
|
"screenshot_labels": screenshot_labels,
|
|
164
|
+
"screenshot_metadata": screenshot_metadata,
|
|
161
165
|
"video_url": video_url,
|
|
162
166
|
"revision_slug": revision,
|
|
163
167
|
})
|
|
@@ -184,22 +188,39 @@ def _safe_filename(name: str) -> str:
|
|
|
184
188
|
return name.replace("[", "-").replace("]", "").replace("/", "-").replace(":", "-")
|
|
185
189
|
|
|
186
190
|
|
|
191
|
+
def _png_dimensions(data: bytes) -> tuple:
|
|
192
|
+
"""Extract width and height from PNG header without Pillow."""
|
|
193
|
+
if len(data) < 24 or data[:8] != b'\x89PNG\r\n\x1a\n':
|
|
194
|
+
return (0, 0)
|
|
195
|
+
# IHDR chunk starts at byte 8: 4 bytes length + 4 bytes "IHDR" + 4 bytes width + 4 bytes height
|
|
196
|
+
width, height = struct.unpack('>II', data[16:24])
|
|
197
|
+
return (width, height)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _file_metadata(data: bytes, url: str) -> dict:
|
|
201
|
+
"""Build screenshot_metadata entry from file bytes and uploaded URL."""
|
|
202
|
+
width, height = _png_dimensions(data)
|
|
203
|
+
return {
|
|
204
|
+
"url": url,
|
|
205
|
+
"width": width,
|
|
206
|
+
"height": height,
|
|
207
|
+
"size_bytes": len(data),
|
|
208
|
+
"sha256": hashlib.sha256(data).hexdigest(),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
187
212
|
def _collect_and_upload_artifacts(item, api_key, api_url):
|
|
188
213
|
"""Find screenshots/videos in test-results/ and upload them.
|
|
189
214
|
|
|
190
|
-
Returns (screenshot_urls, screenshot_labels, video_url).
|
|
191
|
-
Labels are extracted from filenames: 'labeled-N-some-label.png' -> 'some label'.
|
|
192
|
-
The final end-of-test screenshot gets label 'end'.
|
|
215
|
+
Returns (screenshot_urls, screenshot_labels, screenshot_metadata, video_url).
|
|
193
216
|
"""
|
|
194
217
|
screenshot_urls = []
|
|
195
218
|
screenshot_labels = []
|
|
219
|
+
screenshot_metadata = []
|
|
196
220
|
video_url = None
|
|
197
221
|
|
|
198
222
|
safe_name = _safe_filename(item.name)
|
|
199
223
|
|
|
200
|
-
# Get labeled captures from the vs_screenshot fixture (if used)
|
|
201
|
-
vs_labels = getattr(item, "_vs_screenshot_labels", [])
|
|
202
|
-
|
|
203
224
|
patterns = [
|
|
204
225
|
f"test-results/{safe_name}/*.png",
|
|
205
226
|
f"test-results/{safe_name}/*.jpg",
|
|
@@ -228,9 +249,10 @@ def _collect_and_upload_artifacts(item, api_key, api_url):
|
|
|
228
249
|
"webm": "video/webm", "mp4": "video/mp4",
|
|
229
250
|
}.get(ext, "application/octet-stream")
|
|
230
251
|
|
|
252
|
+
file_bytes = path.read_bytes()
|
|
231
253
|
upload_r = httpx.post(f"{api_url}/api/upload",
|
|
232
254
|
headers={"X-API-Key": api_key, "Content-Type": mime, "X-Filename": path.name},
|
|
233
|
-
content=
|
|
255
|
+
content=file_bytes,
|
|
234
256
|
timeout=30)
|
|
235
257
|
|
|
236
258
|
if upload_r.status_code == 200:
|
|
@@ -238,21 +260,20 @@ def _collect_and_upload_artifacts(item, api_key, api_url):
|
|
|
238
260
|
if mime.startswith("video/"):
|
|
239
261
|
video_url = data["url"]
|
|
240
262
|
else:
|
|
241
|
-
|
|
263
|
+
url = data["url"]
|
|
264
|
+
screenshot_urls.append(url)
|
|
265
|
+
screenshot_metadata.append(_file_metadata(file_bytes, url))
|
|
242
266
|
# Derive label from filename
|
|
243
267
|
stem = path.stem
|
|
244
268
|
if stem.startswith("labeled-") and len(stem.split("-", 2)) >= 3:
|
|
245
|
-
# labeled-0-some-label -> some label
|
|
246
269
|
label = stem.split("-", 2)[2].replace("-", " ")
|
|
247
|
-
elif stem
|
|
270
|
+
elif stem in ("end", "after"):
|
|
248
271
|
label = "end"
|
|
249
|
-
elif stem == "after":
|
|
250
|
-
label = "end" # legacy
|
|
251
272
|
else:
|
|
252
273
|
label = ""
|
|
253
274
|
screenshot_labels.append(label)
|
|
254
275
|
|
|
255
|
-
return screenshot_urls, screenshot_labels, video_url
|
|
276
|
+
return screenshot_urls, screenshot_labels, screenshot_metadata, video_url
|
|
256
277
|
|
|
257
278
|
|
|
258
279
|
def _get_versions():
|
|
File without changes
|
|
File without changes
|
{pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{pytest_visionspec-0.2.0 → pytest_visionspec-0.2.1}/pytest_visionspec.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|