#!/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("Test Page") assert parser.title == "Test Page" print(" PASS: test_parser_title") def test_parser_images(): parser = A11yHTMLParser() parser.feed('Alt text') 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("

Main

Sub

Skip

") 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('') assert parser.lang == "en" print(" PASS: test_parser_lang") def test_parser_landmarks(): parser = A11yHTMLParser() parser.feed("
Content
") 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 = """ Good Page

Welcome

Section

Hero image About Us
""" 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)