| """ |
| Tests for server_app.py API endpoints — FastAPI TestClient against gr.Server. |
| """ |
|
|
| import sys |
| from pathlib import Path |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) |
|
|
| import pytest |
| from unittest.mock import patch |
|
|
| FIXTURE_PATH = Path(__file__).resolve().parent / "fixtures" / "button_panel_accessible.png" |
|
|
|
|
| |
|
|
| def image_to_base64(path: Path) -> str: |
| """Encode a fixture image as base64 for JSON POST body.""" |
| import base64 |
|
|
| with open(path, "rb") as f: |
| return base64.b64encode(f.read()).decode("utf-8") |
|
|
|
|
| |
|
|
| class TestGenerateGalleryEndpoint: |
| """POST /api/generate_gallery should return 8 base64 PNGs with labels.""" |
|
|
| def test_generate_gallery_endpoint_returns_200(self): |
| """Endpoint should respond with HTTP 200.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| b64 = image_to_base64(FIXTURE_PATH) |
| response = client.post("/api/generate_gallery", json={"image": b64}) |
|
|
| assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" |
|
|
| def test_generate_gallery_endpoint_returns_8_items(self): |
| """Response should contain exactly 8 gallery items.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| b64 = image_to_base64(FIXTURE_PATH) |
| response = client.post("/api/generate_gallery", json={"image": b64}) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "items" in data, f"Response missing 'items' key: {list(data.keys())}" |
| assert len(data["items"]) == 8, f"Expected 8 items, got {len(data['items'])}" |
|
|
| def test_generate_gallery_endpoint_items_have_image_and_label(self): |
| """Each item should have 'image' (base64 str) and 'label' (str).""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| b64 = image_to_base64(FIXTURE_PATH) |
| response = client.post("/api/generate_gallery", json={"image": b64}) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| for i, item in enumerate(data["items"]): |
| assert "image" in item, f"Item {i} missing 'image' key: {list(item.keys())}" |
| assert "label" in item, f"Item {i} missing 'label' key: {list(item.keys())}" |
| assert isinstance(item["image"], str), f"Item {i} 'image' not str: {type(item['image'])}" |
| assert isinstance(item["label"], str), f"Item {i} 'label' not str: {type(item['label'])}" |
| assert len(item["image"]) > 0, f"Item {i} 'image' is empty" |
| assert len(item["label"]) > 0, f"Item {i} 'label' is empty" |
|
|
|
|
| class TestAnalyzeVlmEndpoint: |
| """POST /api/analyze_vlm should accept gallery JSON and return VLM result.""" |
|
|
| def test_analyze_vlm_endpoint_returns_200(self): |
| """Endpoint should respond with HTTP 200 when given a valid gallery.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| |
| |
| synthetic_gallery = [ |
| {"image": image_to_base64(FIXTURE_PATH), "label": "Protanopia (red-blind)"} |
| ] |
| response = client.post("/api/analyze_vlm", json={"items": synthetic_gallery}) |
| |
| assert response.status_code in (200, 400, 422), f"Unexpected status {response.status_code}" |
|
|
| @patch("backend_api._analyze_all_perspectives") |
| def test_analyze_vlm_endpoint_with_mocked_vlm(self, mock_analyze): |
| """Mocked VLM should return consistent JSON shape in response.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| mock_analyze.return_value = { |
| "findings": [ |
| { |
| "type": "Low Contrast", |
| "wcag_criterion": "1.4.3", |
| "description": "Button has insufficient contrast.", |
| "severity": "serious", |
| "location": "Bottom-right", |
| } |
| ], |
| "summary": "1 issue found.", |
| "passes": False, |
| } |
|
|
| client = TestClient(app) |
| |
| synthetic_gallery = [ |
| {"image": image_to_base64(FIXTURE_PATH), "label": "Deuteranopia (green-blind)"} |
| ] |
| response = client.post("/api/analyze_vlm", json={"items": synthetic_gallery}) |
|
|
| assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" |
| data = response.json() |
| assert "findings" in data, f"Missing 'findings' in response: {list(data.keys())}" |
| assert "passes" in data, f"Missing 'passes' in response: {list(data.keys())}" |
| assert data["passes"] is False |
|
|
| def test_analyze_vlm_endpoint_rejects_empty_gallery(self): |
| """Empty gallery list should be handled gracefully (returns error JSON, HTTP 200).""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| response = client.post("/api/analyze_vlm", json={"items": []}) |
|
|
| |
| assert response.status_code == 200, f"Expected 200, got {response.status_code}" |
| data = response.json() |
| assert "error" in data or data.get("findings") == [], "Empty gallery should return error or empty findings" |
|
|
|
|
| class TestWcagReportEndpoint: |
| """POST /api/wcag_report should accept VLM JSON and return markdown.""" |
|
|
| def test_wcag_report_endpoint_returns_200(self): |
| """Endpoint should respond with HTTP 200 for valid VLM JSON.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| vlm_json = { |
| "findings": [ |
| { |
| "type": "Low Contrast", |
| "wcag_criterion": "1.4.3", |
| "description": "Submit button contrast is below minimum.", |
| "severity": "serious", |
| "location": "Bottom-right", |
| } |
| ], |
| "summary": "1 issue.", |
| "passes": False, |
| } |
| client = TestClient(app) |
| response = client.post("/api/wcag_report", json=vlm_json) |
|
|
| assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" |
|
|
| def test_wcag_report_endpoint_returns_markdown_string(self): |
| """Response should be a markdown string containing report structure.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| vlm_json = { |
| "findings": [ |
| { |
| "type": "Low Contrast", |
| "wcag_criterion": "1.4.3", |
| "description": "Submit button contrast ratio is 2.1:1.", |
| "severity": "serious", |
| "location": "Bottom-right", |
| } |
| ], |
| "summary": "1 issue found.", |
| "passes": False, |
| } |
| client = TestClient(app) |
| response = client.post("/api/wcag_report", json=vlm_json) |
|
|
| assert response.status_code == 200 |
| body = response.text |
| assert isinstance(body, str), f"Expected str response, got {type(body)}" |
| assert "Overall:" in body, "Markdown should contain 'Overall:'" |
| assert "Fail" in body, "Markdown should indicate failure" |
| assert "Submit button" in body |
|
|
| def test_wcag_report_endpoint_handles_error_json(self): |
| """VLM JSON with 'error' key should return warning message in markdown.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| vlm_json = { |
| "error": "MiniCPM endpoint timed out (cold-start may need ~90s). Try again.", |
| "findings": [], |
| "passes": False, |
| } |
| client = TestClient(app) |
| response = client.post("/api/wcag_report", json=vlm_json) |
|
|
| assert response.status_code == 200 |
| assert "Warning" in response.text or "error" in response.text.lower() |
|
|
|
|
| class TestCarouselRoute: |
| """GET / should return HTML carousel with an iframe embedding /access.""" |
|
|
| def test_root_route_returns_200(self): |
| """GET / should return HTTP 200.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| response = client.get("/") |
|
|
| assert response.status_code == 200, f"Expected 200, got {response.status_code}" |
|
|
| def test_root_route_returns_html(self): |
| """GET / should return Content-Type text/html.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| response = client.get("/") |
|
|
| assert response.status_code == 200 |
| assert "text/html" in response.headers.get("content-type", ""), \ |
| f"Expected text/html, got {response.headers.get('content-type')}" |
|
|
| def test_root_route_contains_iframe_to_access(self): |
| """HTML should contain an iframe src pointing to /access.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| response = client.get("/") |
|
|
| assert response.status_code == 200 |
| html = response.text |
| assert 'src="/access"' in html or 'src="/access "' in html, \ |
| "HTML should contain <iframe src=\"/access\">" |
|
|
| def test_root_route_contains_carousel_buttons(self): |
| """HTML should contain Next/Prev carousel button text or handlers.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| response = client.get("/") |
|
|
| html = response.text.lower() |
| assert "next" in html and "prev" in html, \ |
| "HTML should contain Next and Prev carousel controls" |
|
|
|
|
| class TestAccessRoute: |
| """GET /access should serve the embedded Color-UX-Access Gradio app.""" |
|
|
| def test_access_route_returns_200_or_404(self): |
| """GET /access should return HTTP 200 or 404 (Gradio may redirect to /access/).""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| try: |
| response = client.get("/access") |
| assert response.status_code in (200, 404), \ |
| f"Expected 200 or 404, got {response.status_code}" |
| except Exception: |
| |
| |
| pass |
|
|
| def test_access_route_is_gradio_app_on_200(self): |
| """When GET /access returns 200, it should contain Gradio HTML.""" |
| from fastapi.testclient import TestClient |
|
|
| from server_app import app |
|
|
| client = TestClient(app) |
| try: |
| response = client.get("/access") |
| if response.status_code == 200: |
| html = response.text.lower() |
| assert "gradio" in html or "color-ux-access" in html or "blocks" in html, \ |
| "Expected Gradio app HTML at /access" |
| except Exception: |
| |
| pass |