Skip to content

Commit 97fd362

Browse files
tlambert03claude
andcommitted
fix: remove duplicate snapshot fixtures from conftest.py
During rebase conflict resolution, snapshot testing fixtures were accidentally duplicated in conftest.py when they should only exist in snapshot_plugin.py (which is already registered via pytest_plugins). Removed duplicates: - pytest_addoption (snapshot CLI options) - _visual_snapshots_enabled helper - SnapshotPaths class - _cleanup_snapshot_failures fixture - assert_snapshot fixture Also removed now-unused imports: shutil, Any, Callable 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 5a95ee2 commit 97fd362

File tree

1 file changed

+1
-210
lines changed

1 file changed

+1
-210
lines changed

backend/tests_e2e/conftest.py

Lines changed: 1 addition & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,13 @@
2828
import json
2929
import os
3030
import re
31-
import shutil
3231
import subprocess
3332
import sys
3433
import warnings
3534
from collections import defaultdict
3635
from contextlib import contextmanager, suppress
3736
from pathlib import Path
38-
from typing import TYPE_CHECKING, Any, Callable
37+
from typing import TYPE_CHECKING
3938

4039
import django.conf
4140
import pytest
@@ -77,22 +76,6 @@
7776
]
7877

7978

80-
def pytest_addoption(parser: pytest.Parser) -> None:
81-
"""Add custom command line options for e2e tests."""
82-
parser.addoption(
83-
"--visual-snapshots",
84-
action="store_true",
85-
default=False,
86-
help="Enable visual snapshot testing (disabled by default, never runs on CI)",
87-
)
88-
parser.addoption(
89-
"--update-snapshots",
90-
action="store_true",
91-
default=False,
92-
help="Update visual snapshots instead of comparing (for use with --visual-snapshots)",
93-
)
94-
95-
9679
def pytest_configure(config: pytest.Config) -> None:
9780
"""Build frontend assets before test collection when running e2e tests.
9881
@@ -149,15 +132,6 @@ def _color_text(text: str, color_code: int) -> str:
149132
print(_color_text("✅ Frontend assets are up to date", GREEN), flush=True)
150133

151134

152-
def _visual_snapshots_enabled(config: pytest.Config) -> bool:
153-
"""Check if visual snapshots are enabled via CLI flag or environment variable."""
154-
return config.getoption("--visual-snapshots", False) or os.environ.get("VISUAL_SNAPSHOTS", "").lower() in (
155-
"1",
156-
"true",
157-
"yes",
158-
)
159-
160-
161135
def _frontend_assets_need_rebuild(manifest_file) -> bool:
162136
"""Check if frontend assets need to be rebuilt."""
163137
if not manifest_file.is_file():
@@ -286,186 +260,3 @@ def auth_page(auth_context: BrowserContext) -> Iterator[Page]:
286260
with console_errors_raised(page):
287261
yield page
288262
page.close()
289-
290-
291-
# Add a data store for computed paths
292-
class SnapshotPaths:
293-
snapshots_path: Path | None = None
294-
failures_path: Path | None = None
295-
296-
297-
@pytest.fixture(scope="session", autouse=True)
298-
def _cleanup_snapshot_failures(pytestconfig: pytest.Config):
299-
"""
300-
Clean up snapshot failures directory once at the beginning of test session.
301-
302-
The snapshot storage path is relative to each test folder, modeling after the React snapshot locations
303-
"""
304-
305-
root_dir = Path(pytestconfig.rootdir)
306-
307-
# Compute paths once
308-
SnapshotPaths.snapshots_path = root_dir / "__snapshots__"
309-
310-
SnapshotPaths.failures_path = root_dir / "snapshot_failures"
311-
312-
# Clean up the entire failures directory at session start so past failures don't clutter the result
313-
if SnapshotPaths.failures_path.exists():
314-
shutil.rmtree(SnapshotPaths.failures_path, ignore_errors=True)
315-
316-
# Create the directory to ensure it exists
317-
with suppress(FileExistsError):
318-
SnapshotPaths.failures_path.mkdir(parents=True, exist_ok=True)
319-
320-
yield
321-
322-
323-
@pytest.fixture
324-
def assert_snapshot(pytestconfig: pytest.Config, request: pytest.FixtureRequest) -> Callable:
325-
if not _visual_snapshots_enabled(pytestconfig):
326-
327-
def noop(*args: Any, **kwargs: Any) -> None:
328-
pass
329-
330-
noop.NOOP = True # type: ignore[attr-defined]s
331-
return noop
332-
333-
from io import BytesIO
334-
335-
import pytest
336-
from PIL import Image
337-
from pixelmatch.contrib.PIL import pixelmatch
338-
339-
test_function_name = request.node.name
340-
SNAPSHOT_MESSAGE_PREFIX = "[playwright-visual-snapshot]"
341-
342-
test_name_without_params = test_function_name.split("[", 1)[0]
343-
test_name = f"{test_function_name}[{sys.platform!s}]"
344-
345-
current_test_file_path = Path(request.node.fspath)
346-
current_test_file_path.parent.resolve()
347-
348-
# Use global paths if available, otherwise calculate per test
349-
snapshots_path = SnapshotPaths.snapshots_path
350-
assert snapshots_path
351-
352-
snapshot_failures_path = SnapshotPaths.failures_path
353-
assert snapshot_failures_path
354-
355-
# we know this exists because of the default value on ini
356-
global_snapshot_threshold = 0.1
357-
358-
mask_selectors = []
359-
update_snapshot = pytestconfig.getoption("--update-snapshots", False)
360-
361-
# for automatically naming multiple assertions
362-
counter = 0
363-
# Collection to store failures
364-
failures = []
365-
366-
def _create_locators_from_selectors(page: Page, selectors: list[str]):
367-
"""
368-
Convert a list of CSS selector strings to locator objects
369-
"""
370-
return [page.locator(selector) for selector in selectors]
371-
372-
def compare(
373-
img_or_page: bytes | Any,
374-
*,
375-
threshold: float | None = None,
376-
name=None,
377-
fail_fast=False,
378-
mask_elements: list[str] | None = None,
379-
) -> None:
380-
nonlocal counter
381-
382-
if not name:
383-
if counter > 0:
384-
name = f"{test_name}_{counter}.png"
385-
else:
386-
name = f"{test_name}.png"
387-
388-
# Use global threshold if no local threshold provided
389-
if not threshold:
390-
threshold = global_snapshot_threshold
391-
392-
# If page reference is passed, use screenshot
393-
if isinstance(img_or_page, Page):
394-
# Combine configured mask elements with any provided in the function call
395-
all_mask_selectors = list(mask_selectors)
396-
if mask_elements:
397-
all_mask_selectors.extend(mask_elements)
398-
399-
# Convert selectors to locators
400-
masks = _create_locators_from_selectors(img_or_page, all_mask_selectors) if all_mask_selectors else []
401-
402-
img = img_or_page.screenshot(
403-
animations="disabled",
404-
type="png",
405-
mask=masks,
406-
# TODO only for jpeg
407-
# quality=100,
408-
)
409-
else:
410-
img = img_or_page
411-
412-
# test file without the extension
413-
test_file_name_without_extension = current_test_file_path.stem
414-
415-
# Created a nested folder to store screenshots: snapshot/test_file_name/test_name/
416-
test_file_snapshot_dir = snapshots_path / test_file_name_without_extension / test_name_without_params
417-
test_file_snapshot_dir.mkdir(parents=True, exist_ok=True)
418-
419-
screenshot_file = test_file_snapshot_dir / name
420-
421-
# Create a dir where all snapshot test failures will go
422-
# ex: snapshot_failures/test_file_name/test_name
423-
failure_results_dir = snapshot_failures_path / test_file_name_without_extension / test_name
424-
425-
# increment counter before any failures are recorded
426-
counter += 1
427-
428-
if update_snapshot:
429-
screenshot_file.write_bytes(img)
430-
failures.append(f"{SNAPSHOT_MESSAGE_PREFIX} Snapshots updated. Please review images. {screenshot_file}")
431-
return
432-
433-
if not screenshot_file.exists():
434-
screenshot_file.write_bytes(img)
435-
failures.append(
436-
f"{SNAPSHOT_MESSAGE_PREFIX} New snapshot(s) created. Please review images. {screenshot_file}"
437-
)
438-
return
439-
440-
img_a = Image.open(BytesIO(img))
441-
img_b = Image.open(screenshot_file)
442-
img_diff = Image.new("RGBA", img_a.size)
443-
mismatch = pixelmatch(img_a, img_b, img_diff, threshold=threshold, fail_fast=fail_fast)
444-
445-
if mismatch == 0:
446-
return
447-
448-
# Create new test_results folder
449-
failure_results_dir.mkdir(parents=True, exist_ok=True)
450-
img_diff.save(f"{failure_results_dir}/diff_{name}")
451-
img_a.save(f"{failure_results_dir}/actual_{name}")
452-
img_b.save(f"{failure_results_dir}/expected_{name}")
453-
454-
# on ci, update the existing screenshots in place so we can download them
455-
if os.getenv("CI"):
456-
screenshot_file.write_bytes(img)
457-
458-
# Still honor fail_fast if specifically requested
459-
if fail_fast:
460-
pytest.fail(f"{SNAPSHOT_MESSAGE_PREFIX} Snapshots DO NOT match! {name}")
461-
462-
failures.append(f"{SNAPSHOT_MESSAGE_PREFIX} Snapshots DO NOT match! {name}")
463-
464-
# Register finalizer to report all failures at the end of the test
465-
def finalize():
466-
if failures:
467-
pytest.fail("\n".join(failures))
468-
469-
request.addfinalizer(finalize)
470-
471-
return compare

0 commit comments

Comments
 (0)