Some checks failed
Architecture Lint / Lint Repository (push) Has been cancelled
Architecture Lint / Linter Tests (push) Has been cancelled
Smoke Test / smoke (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Python Test Suite (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Validate Config / YAML Lint (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Merge PR #531
302 lines
9.6 KiB
Python
302 lines
9.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for foundation_accessibility_audit.py — verifies WCAG checks."""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
|
|
|
from foundation_accessibility_audit import (
|
|
A11yHTMLParser, Severity, A11yViolation,
|
|
parse_color, contrast_ratio, relative_luminance,
|
|
run_programmatic_checks, check_page_title, check_images_alt_text,
|
|
check_heading_hierarchy, check_lang_attribute, check_landmarks,
|
|
check_skip_nav, check_form_labels, check_link_text,
|
|
_parse_json_response, format_report, A11yAuditReport, A11yPageResult,
|
|
)
|
|
|
|
|
|
# === Color Utilities ===
|
|
|
|
def test_parse_color_hex6():
|
|
assert parse_color("#ff0000") == (255, 0, 0)
|
|
assert parse_color("#000000") == (0, 0, 0)
|
|
assert parse_color("#ffffff") == (255, 255, 255)
|
|
print(" PASS: test_parse_color_hex6")
|
|
|
|
|
|
def test_parse_color_hex3():
|
|
assert parse_color("#f00") == (255, 0, 0)
|
|
assert parse_color("#abc") == (170, 187, 204)
|
|
print(" PASS: test_parse_color_hex3")
|
|
|
|
|
|
def test_parse_color_rgb():
|
|
assert parse_color("rgb(255, 0, 0)") == (255, 0, 0)
|
|
assert parse_color("rgb( 128 , 64 , 32 )") == (128, 64, 32)
|
|
print(" PASS: test_parse_color_rgb")
|
|
|
|
|
|
def test_parse_color_named():
|
|
assert parse_color("white") == (255, 255, 255)
|
|
assert parse_color("black") == (0, 0, 0)
|
|
print(" PASS: test_parse_color_named")
|
|
|
|
|
|
def test_parse_color_invalid():
|
|
assert parse_color("not-a-color") is None
|
|
assert parse_color("") is None
|
|
print(" PASS: test_parse_color_invalid")
|
|
|
|
|
|
def test_contrast_ratio_black_white():
|
|
ratio = contrast_ratio((0, 0, 0), (255, 255, 255))
|
|
assert ratio > 20 # Should be 21:1
|
|
print(f" PASS: test_contrast_ratio_black_white ({ratio:.1f}:1)")
|
|
|
|
|
|
def test_contrast_ratio_same():
|
|
ratio = contrast_ratio((128, 128, 128), (128, 128, 128))
|
|
assert ratio == 1.0
|
|
print(" PASS: test_contrast_ratio_same")
|
|
|
|
|
|
def test_contrast_ratio_wcag_aa():
|
|
# #767676 on white = 4.54:1 (WCAG AA pass for normal text)
|
|
ratio = contrast_ratio((118, 118, 118), (255, 255, 255))
|
|
assert ratio >= 4.5
|
|
print(f" PASS: test_contrast_ratio_wcag_aa ({ratio:.2f}:1)")
|
|
|
|
|
|
# === HTML Parser ===
|
|
|
|
def test_parser_title():
|
|
parser = A11yHTMLParser()
|
|
parser.feed("<html><head><title>Test Page</title></head></html>")
|
|
assert parser.title == "Test Page"
|
|
print(" PASS: test_parser_title")
|
|
|
|
|
|
def test_parser_images():
|
|
parser = A11yHTMLParser()
|
|
parser.feed('<html><body><img src="a.png" alt="Alt text"><img src="b.png"></body></html>')
|
|
assert len(parser.images) == 2
|
|
assert parser.images[0]["alt"] == "Alt text"
|
|
assert parser.images[1]["alt"] is None
|
|
print(" PASS: test_parser_images")
|
|
|
|
|
|
def test_parser_headings():
|
|
parser = A11yHTMLParser()
|
|
parser.feed("<html><body><h1>Main</h1><h2>Sub</h2><h4>Skip</h4></body></html>")
|
|
assert len(parser.headings) == 3
|
|
assert parser.headings[0] == {"level": 1, "text": "Main"}
|
|
assert parser.headings[2] == {"level": 4, "text": "Skip"}
|
|
print(" PASS: test_parser_headings")
|
|
|
|
|
|
def test_parser_lang():
|
|
parser = A11yHTMLParser()
|
|
parser.feed('<html lang="en"><body></body></html>')
|
|
assert parser.lang == "en"
|
|
print(" PASS: test_parser_lang")
|
|
|
|
|
|
def test_parser_landmarks():
|
|
parser = A11yHTMLParser()
|
|
parser.feed("<html><body><nav>Links</nav><main>Content</main></body></html>")
|
|
tags = {lm["tag"] for lm in parser.landmarks}
|
|
assert "nav" in tags
|
|
assert "main" in tags
|
|
print(" PASS: test_parser_landmarks")
|
|
|
|
|
|
# === Programmatic Checks ===
|
|
|
|
def test_check_page_title_empty():
|
|
parser = A11yHTMLParser()
|
|
parser.title = ""
|
|
violations = check_page_title(parser)
|
|
assert len(violations) == 1
|
|
assert violations[0].criterion == "2.4.2"
|
|
assert violations[0].severity == Severity.MAJOR
|
|
print(" PASS: test_check_page_title_empty")
|
|
|
|
|
|
def test_check_page_title_present():
|
|
parser = A11yHTMLParser()
|
|
parser.title = "My Great Page"
|
|
violations = check_page_title(parser)
|
|
assert len(violations) == 0
|
|
print(" PASS: test_check_page_title_present")
|
|
|
|
|
|
def test_check_lang_missing():
|
|
parser = A11yHTMLParser()
|
|
parser.lang = ""
|
|
violations = check_lang_attribute(parser)
|
|
assert len(violations) == 1
|
|
assert violations[0].criterion == "3.1.1"
|
|
print(" PASS: test_check_lang_missing")
|
|
|
|
|
|
def test_check_images_missing_alt():
|
|
parser = A11yHTMLParser()
|
|
parser.images = [{"src": "photo.jpg", "alt": None}]
|
|
violations = check_images_alt_text(parser)
|
|
assert len(violations) == 1
|
|
assert violations[0].severity == Severity.CRITICAL
|
|
print(" PASS: test_check_images_missing_alt")
|
|
|
|
|
|
def test_check_images_with_alt():
|
|
parser = A11yHTMLParser()
|
|
parser.images = [{"src": "photo.jpg", "alt": "A photo"}]
|
|
violations = check_images_alt_text(parser)
|
|
assert len(violations) == 0
|
|
print(" PASS: test_check_images_with_alt")
|
|
|
|
|
|
def test_check_images_decorative():
|
|
parser = A11yHTMLParser()
|
|
parser.images = [{"src": "deco.png", "alt": "", "role": "presentation"}]
|
|
violations = check_images_alt_text(parser)
|
|
assert len(violations) == 0
|
|
print(" PASS: test_check_images_decorative")
|
|
|
|
|
|
def test_check_headings_no_h1():
|
|
parser = A11yHTMLParser()
|
|
parser.headings = [{"level": 2, "text": "Sub"}, {"level": 3, "text": "Sub sub"}]
|
|
violations = check_heading_hierarchy(parser)
|
|
assert any(v.criterion == "1.3.1" and "h1" in v.description.lower() for v in violations)
|
|
print(" PASS: test_check_headings_no_h1")
|
|
|
|
|
|
def test_check_headings_skip():
|
|
parser = A11yHTMLParser()
|
|
parser.headings = [{"level": 1, "text": "Main"}, {"level": 4, "text": "Skipped"}]
|
|
violations = check_heading_hierarchy(parser)
|
|
assert any("skipped" in v.description.lower() for v in violations)
|
|
print(" PASS: test_check_headings_skip")
|
|
|
|
|
|
def test_check_skip_nav_missing():
|
|
parser = A11yHTMLParser()
|
|
parser.skip_nav = False
|
|
parser.links = [{"text": "Home", "href": "/"}, {"text": "About", "href": "/about"}]
|
|
violations = check_skip_nav(parser)
|
|
assert len(violations) == 1
|
|
assert violations[0].criterion == "2.4.1"
|
|
print(" PASS: test_check_skip_nav_missing")
|
|
|
|
|
|
def test_check_link_text_empty():
|
|
parser = A11yHTMLParser()
|
|
parser.links = [{"text": "", "href": "/page", "aria_label": ""}]
|
|
violations = check_link_text(parser)
|
|
assert len(violations) == 1
|
|
assert violations[0].criterion == "2.4.4"
|
|
print(" PASS: test_check_link_text_empty")
|
|
|
|
|
|
def test_check_link_text_generic():
|
|
parser = A11yHTMLParser()
|
|
parser.links = [{"text": "Click here", "href": "/page"}]
|
|
violations = check_link_text(parser)
|
|
assert any("non-descriptive" in v.description.lower() for v in violations)
|
|
print(" PASS: test_check_link_text_generic")
|
|
|
|
|
|
def test_run_programmatic_checks_full():
|
|
html = """<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head><title>Good Page</title></head>
|
|
<body>
|
|
<nav><a href="#main">Skip to content</a></nav>
|
|
<main>
|
|
<h1>Welcome</h1>
|
|
<h2>Section</h2>
|
|
<img src="hero.jpg" alt="Hero image">
|
|
<a href="/about">About Us</a>
|
|
</main>
|
|
</body>
|
|
</html>"""
|
|
violations = run_programmatic_checks(html)
|
|
# This page should have very few or no violations
|
|
criticals = [v for v in violations if v.severity == Severity.CRITICAL]
|
|
assert len(criticals) == 0
|
|
print(f" PASS: test_run_programmatic_checks_full ({len(violations)} minor issues)")
|
|
|
|
|
|
# === JSON Parsing ===
|
|
|
|
def test_parse_json_clean():
|
|
result = _parse_json_response('{"violations": [], "overall_score": 100}')
|
|
assert result["overall_score"] == 100
|
|
print(" PASS: test_parse_json_clean")
|
|
|
|
|
|
def test_parse_json_fenced():
|
|
result = _parse_json_response('```json\n{"overall_score": 80}\n```')
|
|
assert result["overall_score"] == 80
|
|
print(" PASS: test_parse_json_fenced")
|
|
|
|
|
|
# === Formatting ===
|
|
|
|
def test_format_json():
|
|
report = A11yAuditReport(site="test.com", pages_audited=1, overall_score=90)
|
|
output = format_report(report, "json")
|
|
parsed = json.loads(output)
|
|
assert parsed["site"] == "test.com"
|
|
assert parsed["overall_score"] == 90
|
|
print(" PASS: test_format_json")
|
|
|
|
|
|
def test_format_text():
|
|
report = A11yAuditReport(site="test.com", pages_audited=1, overall_score=90,
|
|
summary="Test complete")
|
|
output = format_report(report, "text")
|
|
assert "ACCESSIBILITY AUDIT" in output
|
|
assert "test.com" in output
|
|
print(" PASS: test_format_text")
|
|
|
|
|
|
# === Run All ===
|
|
|
|
def run_all():
|
|
print("=== foundation_accessibility_audit tests ===")
|
|
tests = [
|
|
test_parse_color_hex6, test_parse_color_hex3, test_parse_color_rgb,
|
|
test_parse_color_named, test_parse_color_invalid,
|
|
test_contrast_ratio_black_white, test_contrast_ratio_same, test_contrast_ratio_wcag_aa,
|
|
test_parser_title, test_parser_images, test_parser_headings,
|
|
test_parser_lang, test_parser_landmarks,
|
|
test_check_page_title_empty, test_check_page_title_present,
|
|
test_check_lang_missing,
|
|
test_check_images_missing_alt, test_check_images_with_alt, test_check_images_decorative,
|
|
test_check_headings_no_h1, test_check_headings_skip,
|
|
test_check_skip_nav_missing,
|
|
test_check_link_text_empty, test_check_link_text_generic,
|
|
test_run_programmatic_checks_full,
|
|
test_parse_json_clean, test_parse_json_fenced,
|
|
test_format_json, test_format_text,
|
|
]
|
|
passed = 0
|
|
failed = 0
|
|
for test in tests:
|
|
try:
|
|
test()
|
|
passed += 1
|
|
except Exception as e:
|
|
print(f" FAIL: {test.__name__} — {e}")
|
|
failed += 1
|
|
print(f"\n{'ALL PASSED' if failed == 0 else f'{failed} FAILED'}: {passed}/{len(tests)}")
|
|
return failed == 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(0 if run_all() else 1)
|