color-ux-access / conftest.py
salgadev's picture
Sync from GitHub 3f8aaa87
5f67b0d
Raw History Blame Contribute Delete
6.13 kB
"""
Shared pytest fixtures for color-ux-access.
=============================================
All tests should import from here rather than doing sys.path gymnastics.
Fixture hierarchy
-----------------
img_factory β€” PIL Image, no CVD simulation
cvd_img_factory β€” Image + callable(cv_type) -> simulated PIL Image
mock_vlm_factory β€” Returns deterministic WCAG JSON (no API call)
browser_page β€” Chromium page for screenshot capture tests
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import pytest
import numpy as np
from dotenv import load_dotenv
from PIL import Image
# Ensure project root is on path
_project_root = Path(__file__).resolve().parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
# Load .env so env-gated integration tests (modal endpoints, etc.) work locally
load_dotenv(_project_root / ".env")
# ── Image Fixtures ────────────────────────────────────────────────────────────
@pytest.fixture
def img_factory():
"""Factory: makes random RGB PIL Images of a given size."""
def _make(width: int = 640, height: int = 480, seed: int | None = None) -> Image.Image:
if seed is not None:
rng = np.random.default_rng(seed)
arr = rng.integers(0, 255, (height, width, 3), dtype=np.uint8)
else:
arr = np.random.randint(0, 255, (height, width, 3), dtype=np.uint8)
return Image.fromarray(arr)
return _make
@pytest.fixture
def cvd_img_factory(img_factory):
"""
Factory: makes PIL Image AND a simulate() callable.
Usage:
img, simulate = cvd_img_factory(100, 100)
deut_img = simulate(img, 'deuteranopia')
"""
from color_ux_access.cvd_sim import simulate_cvd
def _make(width: int = 640, height: int = 480, seed: int = 42) -> tuple[Image.Image, callable]:
img = img_factory(width, height, seed=seed)
return img, lambda cv_type: simulate_cvd(img, cv_type)
return _make
@pytest.fixture
def sample_button_panel(img_factory) -> Image.Image:
"""
Synthetic button panel β€” green Confirm button (left), red Cancel button (right).
Tests that CVD simulation makes the two buttons indistinguishable.
"""
arr = np.zeros((120, 300, 3), dtype=np.uint8)
arr[:, :] = [240, 240, 240] # gray background
arr[20:100, 20:145] = [34, 139, 34] # green confirm (left half)
arr[20:100, 155:280] = [205, 92, 92] # red cancel (right half)
return Image.fromarray(arr)
# ── VLM Fixtures ──────────────────────────────────────────────────────────────
@pytest.fixture
def mock_vlm_factory():
"""
Returns a deterministic WCAG JSON response for testing without API calls.
Use as a patch target for analyze_with_vlm() in app_space.py.
"""
def make_findings(count: int = 2) -> list[dict]:
return [
{
"type": "Low Contrast",
"wcag_criterion": "1.4.3",
"description": "Button text contrast ratio is 2.8:1 β€” below the 4.5:1 AA minimum",
"severity": "serious",
"location": "Submit button, top-right",
},
{
"type": "Color Only Information",
"wcag_criterion": "1.4.1",
"description": "Form validation relies on red/green borders with no secondary indicator",
"severity": "critical",
"location": "Form section, center-left",
},
][:count]
def mock_vlm_result(passes: bool = False, findings_count: int = 1) -> dict:
findings = make_findings(findings_count) if not passes else []
return {
"perception_summary": "The UI is mostly readable but some color-dependent elements may be hard to distinguish." if not passes else "The interface is clear and readable from this perspective.",
"findings": findings,
"summary": f"{findings_count} critical issue(s) found" if findings else "No issues found",
"passes": passes,
}
return mock_vlm_result
# ── CVD Gallery Fixture ───────────────────────────────────────────────────────
@pytest.fixture
def ten_type_gallery() -> list[str]:
"""
The 10 canonical CVD type names the UI gallery must produce.
"""
return [
'deuteranopia',
'protanopia',
'tritanopia',
'achromatopsia',
'deuteranomaly',
'protanomaly',
'tritanomaly',
]
# ── Token Fixture ─────────────────────────────────────────────────────────────
@pytest.fixture(scope="module")
def hf_token():
"""
Read HF_TOKEN from environment or ~/.cache/huggingface/token.
Order: HF_TOKEN env var β†’ HF_API_TOKEN env var β†’ token file.
Skips all E2E tests if no token is available.
Matches how color_ux_access.modal_app and vlm.vlm_inference resolve tokens.
"""
token = os.environ.get("HF_TOKEN") or os.environ.get("HF_API_TOKEN")
if token is None:
token_file = os.path.expanduser("~/.cache/huggingface/token")
if not os.path.exists(token_file):
pytest.skip("HF_TOKEN not found (no env var and no ~/.cache/huggingface/token)")
with open(token_file) as f:
token = f.read().strip()
if not token or len(token) < 10:
pytest.skip("HF_TOKEN is empty or too short to be valid")
return token
# Playwright fixtures removed β€” HF Space containers cannot run a browser.
# The capture module (color_ux_access.capture.take_screenshot) uses playwright
# locally for screenshots, but tests use source inspection only."