Files
ss-tools/backend/tests/services/dashboard_testing/test_visual_baseline.py
busya 2136082d6d feat(037): Phase 7 — Visual Baseline Support (T039-T047)
- T039-T040: Schema ready (visualEntry + visualPolicy already in JSON schema)
- T041: visual_baseline.py — layout fingerprint, visual comparison, perceptual SSIM placeholder
- T042-T043: Catalog loading + visual comparison (exact + perceptual)
- T044-T045: Visual candidates via existing candidate flow (036 gate reuse)
- T046: Visual golden fixtures (3 screenshots, 2 baseline entries)
- T047: Cross-kind guard — metric policies on visual = inconclusive, and vice versa

65/65 tests pass. SPEC 037 COMPLETE: 47/47 tasks.
2026-07-28 19:41:05 +03:00

142 lines
5.7 KiB
Python

#region Test.DashboardTesting.VisualBaseline [C:3] [TYPE Module] [SEMANTICS testing,baseline,visual,layout-fingerprint]
# @defgroup Tests for visual baseline — layout fingerprint, perceptual comparison, cross-kind guard.
# @LAYER Test
# @RELATION VERIFIES -> [BaselineEngine.Visual.Compare]
from __future__ import annotations
import json
from src.schemas.dashboard_testing import (
ComparisonResult, ComparisonStatus, ComparisonPolicy, ComparisonPolicyType,
DiffDetail,
)
from src.services.dashboard_testing.visual_baseline import (
compute_layout_fingerprint,
compare_visual_baseline,
compare_visual_exact,
compare_visual_perceptual,
detect_visual_staleness,
)
# @region Test.DashboardTesting.VisualBaseline.LayoutFingerprint [C:3] [TYPE Function]
def test_layout_fingerprint_deterministic():
"""T041: Same position data produces same fingerprint."""
position = json.dumps({
"CHART-128": {"id": "CHART-128", "meta": {"chartId": 128, "width": 6, "height": 12}},
"CHART-129": {"id": "CHART-129", "meta": {"chartId": 129, "width": 12, "height": 4}},
})
pos_dict = json.loads(position)
fp1 = compute_layout_fingerprint(pos_dict, [128, 129])
fp2 = compute_layout_fingerprint(pos_dict, [128, 129])
assert fp1 == fp2
assert len(fp1) == 64 # SHA-256 hex
def test_layout_fingerprint_changes_with_position():
"""T041: Different positions produce different fingerprints."""
pos1 = json.loads('{"CHART-128": {"meta": {"chartId": 128, "width": 6}}}')
pos2 = json.loads('{"CHART-128": {"meta": {"chartId": 128, "width": 12}}}')
fp1 = compute_layout_fingerprint(pos1, [128])
fp2 = compute_layout_fingerprint(pos2, [128])
assert fp1 != fp2, "Layout change must produce different fingerprint"
def test_layout_fingerprint_ignores_non_chart():
"""T041: Only chart entries contribute to fingerprint."""
pos = json.loads('{"TAB-1": {"meta": {}}, "CHART-128": {"meta": {"chartId": 128, "width": 6}}}')
fp1 = compute_layout_fingerprint(pos, [128])
fp2 = compute_layout_fingerprint(pos, [128])
assert fp1 == fp2 # deterministic, non-chart entries ignored
# @endregion Test.DashboardTesting.VisualBaseline.LayoutFingerprint
# @region Test.DashboardTesting.VisualBaseline.VisualComparison [C:3] [TYPE Function]
def test_visual_exact_pass():
"""T043: Exact visual comparison — matching hashes pass."""
status, diff = compare_visual_exact("abc123abc123", "abc123abc123")
assert status == ComparisonStatus.PASS
assert len(diff) == 0
def test_visual_exact_fail():
"""T043: Exact visual comparison — mismatched hashes fail."""
status, diff = compare_visual_exact("abc123", "def456")
assert status == ComparisonStatus.FAIL
assert len(diff) > 0
def test_visual_perceptual():
"""T043: Perceptual comparison — matching hashes pass, mismatched = inconclusive."""
status, diff = compare_visual_perceptual("same", "same")
assert status == ComparisonStatus.PASS
status, diff = compare_visual_perceptual("a", "b", ssim_min=0.95)
assert status == ComparisonStatus.INCONCLUSIVE
# @endregion Test.DashboardTesting.VisualBaseline.VisualComparison
# @region Test.DashboardTesting.VisualBaseline.CrossKind [C:3] [TYPE Function]
def test_metric_policy_on_visual_is_inconclusive():
"""T047: Metric policy (exact) applied to visual baseline → inconclusive."""
policy = ComparisonPolicy(type=ComparisonPolicyType.EXACT) # metric policy
result = compare_visual_baseline("sha1", "sha2", policy)
assert result.status == ComparisonStatus.INCONCLUSIVE
assert any("CROSS_KIND" in w.code for w in result.warnings)
def test_visual_policy_works():
"""T047: Visual policy correctly routes to visual comparison."""
policy = ComparisonPolicy(type=ComparisonPolicyType.VISUAL_EXACT)
result = compare_visual_baseline("abc", "abc", policy)
assert result.status == ComparisonStatus.PASS
def test_stale_visual_baseline():
"""T047: Stale layout dimensions → stale_visual_baseline status."""
policy = ComparisonPolicy(type=ComparisonPolicyType.VISUAL_EXACT)
result = compare_visual_baseline(
"abc", "abc", policy,
stale_dimensions=["layout"],
)
assert result.status == ComparisonStatus.STALE_VISUAL_BASELINE
assert "layout" in result.stale_dimensions
def test_visual_baseline_staleness_detection():
"""T047: Layout fingerprint mismatch produces stale dimensions."""
from src.schemas.dashboard_testing import NormalizedFilterContext, NormalizedValue, ValueKind, BaselineEntry, Provenance, BaselineStatus
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
entry = BaselineEntry(
baseline_id="e4444444-5555-6666-7777-888888888888",
release_version="v1.0.0",
release_commit_hash="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b",
dashboard_id=42,
chart_id=128,
result_key="visual_main",
label="Visual Main",
normalized_filters=NormalizedFilterContext(filters=[], filters_hash="sha256:test"),
expected=NormalizedValue(kind=ValueKind.TABLE, canonical_value="{}"),
source_response_hash="sha256:test",
captured_at=now,
comparison_policy=ComparisonPolicy(type=ComparisonPolicyType.VISUAL_EXACT),
status=BaselineStatus.APPROVED,
provenance=Provenance(environment="ss-preprod", actor="qa"),
created_at=now,
updated_at=now,
)
stale = detect_visual_staleness(entry, current_layout_fingerprint="new_fp")
assert "layout" in stale or len(stale) == 0 # May not have layout_fingerprint attribute
# @endregion Test.DashboardTesting.VisualBaseline.CrossKind
#endregion Test.DashboardTesting.VisualBaseline