@mcptoolshop/accessibility-suite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +63 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/docs/prov-spec/.github/workflows/ci.yml +68 -0
- package/docs/prov-spec/CHANGELOG.md +69 -0
- package/docs/prov-spec/CODE_OF_CONDUCT.md +129 -0
- package/docs/prov-spec/CONFORMANCE_LEVELS.md +223 -0
- package/docs/prov-spec/CONTRIBUTING.md +145 -0
- package/docs/prov-spec/IMPLEMENTER_CHECKLIST.md +137 -0
- package/docs/prov-spec/LICENSE +21 -0
- package/docs/prov-spec/PRESS_RELEASE.md +74 -0
- package/docs/prov-spec/README.md +182 -0
- package/docs/prov-spec/SETUP.md +135 -0
- package/docs/prov-spec/WHY.md +86 -0
- package/docs/prov-spec/examples/artifact.example.json +14 -0
- package/docs/prov-spec/examples/artifact.ref.example.json +9 -0
- package/docs/prov-spec/examples/evidence.example.json +6 -0
- package/docs/prov-spec/examples/mcp.envelope.example.json +97 -0
- package/docs/prov-spec/examples/mcp.request.example.json +28 -0
- package/docs/prov-spec/examples/prov.record.example.json +35 -0
- package/docs/prov-spec/interop/PROOF_NODE_ENGINE.md +114 -0
- package/docs/prov-spec/spec/MCP_COMPATIBILITY.md +241 -0
- package/docs/prov-spec/spec/PROV_METHODS_CATALOG.md +142 -0
- package/docs/prov-spec/spec/PROV_METHODS_SPEC.md +397 -0
- package/docs/prov-spec/spec/methods.json +213 -0
- package/docs/prov-spec/spec/schemas/artifact.ref.schema.v0.1.json +58 -0
- package/docs/prov-spec/spec/schemas/artifact.schema.v0.1.json +61 -0
- package/docs/prov-spec/spec/schemas/assist.request.schema.v0.1.json +52 -0
- package/docs/prov-spec/spec/schemas/assist.response.schema.v0.1.json +70 -0
- package/docs/prov-spec/spec/schemas/cli.error.schema.v0.1.json +78 -0
- package/docs/prov-spec/spec/schemas/evidence.schema.v0.1.json +37 -0
- package/docs/prov-spec/spec/schemas/mcp.envelope.schema.v0.1.json +141 -0
- package/docs/prov-spec/spec/schemas/mcp.request.schema.v0.1.json +79 -0
- package/docs/prov-spec/spec/schemas/methods.schema.json +93 -0
- package/docs/prov-spec/spec/schemas/prov-capabilities.schema.json +122 -0
- package/docs/prov-spec/spec/schemas/prov.record.schema.v0.1.json +133 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/expected.json +4 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/input.json +1 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/double_wrapped.json +14 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/wrong_schema_version.json +11 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/expected.json +24 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/input.json +8 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/expected.json +7 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/input.json +1 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/non_hex_chars.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/uppercase_hex.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/wrong_length.json +16 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/hyphen_separator.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/reserved_namespace.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/starts_with_digit.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/uppercase.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/positive/valid_ids.json +18 -0
- package/docs/prov-spec/tools/python/prov_validator.py +428 -0
- package/examples/a11y-demo-site/.github/workflows/a11y-artifacts.yml +81 -0
- package/examples/a11y-demo-site/.github/workflows/a11y.yml +34 -0
- package/examples/a11y-demo-site/CODE_OF_CONDUCT.md +129 -0
- package/examples/a11y-demo-site/CONTRIBUTING.md +83 -0
- package/examples/a11y-demo-site/LICENSE +21 -0
- package/examples/a11y-demo-site/README.md +155 -0
- package/examples/a11y-demo-site/html/contact.html +15 -0
- package/examples/a11y-demo-site/html/index.html +20 -0
- package/examples/a11y-demo-site/scripts/a11y.sh +20 -0
- package/package.json +26 -0
- package/src/a11y-assist/.github/workflows/publish.yml +52 -0
- package/src/a11y-assist/.github/workflows/test.yml +30 -0
- package/src/a11y-assist/A11Y_ASSIST_TEST_COVERAGE_REQUIREMENTS.md +104 -0
- package/src/a11y-assist/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-assist/CONTRIBUTING.md +98 -0
- package/src/a11y-assist/ENGINE.md +363 -0
- package/src/a11y-assist/LICENSE +21 -0
- package/src/a11y-assist/PRESS_RELEASE.md +71 -0
- package/src/a11y-assist/QUICKSTART.md +101 -0
- package/src/a11y-assist/README.md +192 -0
- package/src/a11y-assist/RELEASE_NOTES.md +319 -0
- package/src/a11y-assist/a11y_assist/__init__.py +3 -0
- package/src/a11y-assist/a11y_assist/cli.py +599 -0
- package/src/a11y-assist/a11y_assist/from_cli_error.py +149 -0
- package/src/a11y-assist/a11y_assist/guard.py +444 -0
- package/src/a11y-assist/a11y_assist/ingest.py +407 -0
- package/src/a11y-assist/a11y_assist/methods.py +137 -0
- package/src/a11y-assist/a11y_assist/parse_raw.py +71 -0
- package/src/a11y-assist/a11y_assist/profiles/__init__.py +29 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load.py +245 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load_render.py +86 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia.py +144 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia_render.py +77 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language.py +119 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language_render.py +66 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader.py +348 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader_render.py +89 -0
- package/src/a11y-assist/a11y_assist/render.py +95 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.request.schema.v0.1.json +52 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.response.schema.v0.1.json +70 -0
- package/src/a11y-assist/a11y_assist/schemas/cli.error.schema.v0.1.json +78 -0
- package/src/a11y-assist/a11y_assist/storage.py +31 -0
- package/src/a11y-assist/pyproject.toml +60 -0
- package/src/a11y-assist/tests/__init__.py +1 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_high.json +18 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_medium.json +16 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/raw_text_low.txt +3 -0
- package/src/a11y-assist/tests/fixtures/cli_error_good.json +9 -0
- package/src/a11y-assist/tests/fixtures/cli_error_missing_id.json +7 -0
- package/src/a11y-assist/tests/fixtures/cli_error_string_format.json +7 -0
- package/src/a11y-assist/tests/fixtures/expected/cognitive_load_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/dyslexia_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/lowvision_high.txt +18 -0
- package/src/a11y-assist/tests/fixtures/expected/plain_language_high.txt +14 -0
- package/src/a11y-assist/tests/fixtures/expected/screen_reader_high.txt +19 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_cli_error.txt +16 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_no_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_with_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/raw_good.txt +11 -0
- package/src/a11y-assist/tests/fixtures/raw_no_id.txt +2 -0
- package/src/a11y-assist/tests/test_cognitive_load.py +469 -0
- package/src/a11y-assist/tests/test_dyslexia.py +337 -0
- package/src/a11y-assist/tests/test_explain.py +74 -0
- package/src/a11y-assist/tests/test_golden.py +127 -0
- package/src/a11y-assist/tests/test_guard.py +819 -0
- package/src/a11y-assist/tests/test_guard_integration.py +457 -0
- package/src/a11y-assist/tests/test_ingest.py +311 -0
- package/src/a11y-assist/tests/test_methods_metadata.py +236 -0
- package/src/a11y-assist/tests/test_plain_language.py +348 -0
- package/src/a11y-assist/tests/test_render.py +117 -0
- package/src/a11y-assist/tests/test_screen_reader.py +703 -0
- package/src/a11y-assist/tests/test_storage_last.py +61 -0
- package/src/a11y-assist/tests/test_triage.py +86 -0
- package/src/a11y-ci/.github/workflows/ci.yml +43 -0
- package/src/a11y-ci/.github/workflows/test.yml +30 -0
- package/src/a11y-ci/A11Y_CI_TEST_COVERAGE_REQUIREMENTS.md +94 -0
- package/src/a11y-ci/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-ci/CONTRIBUTING.md +142 -0
- package/src/a11y-ci/LICENSE +21 -0
- package/src/a11y-ci/README.md +105 -0
- package/src/a11y-ci/a11y_ci/__init__.py +3 -0
- package/src/a11y-ci/a11y_ci/allowlist.py +83 -0
- package/src/a11y-ci/a11y_ci/cli.py +145 -0
- package/src/a11y-ci/a11y_ci/gate.py +131 -0
- package/src/a11y-ci/a11y_ci/render.py +48 -0
- package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +24 -0
- package/src/a11y-ci/a11y_ci/scorecard.py +99 -0
- package/src/a11y-ci/npm/package.json +35 -0
- package/src/a11y-ci/pyproject.toml +64 -0
- package/src/a11y-ci/tests/__init__.py +1 -0
- package/src/a11y-ci/tests/fixtures/allowlist_expired.json +10 -0
- package/src/a11y-ci/tests/fixtures/allowlist_ok.json +10 -0
- package/src/a11y-ci/tests/fixtures/baseline_ok.json +7 -0
- package/src/a11y-ci/tests/fixtures/current_fail.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_ok.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_regresses.json +7 -0
- package/src/a11y-ci/tests/test_gate.py +134 -0
- package/src/a11y-evidence-engine/.github/workflows/ci.yml +53 -0
- package/src/a11y-evidence-engine/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-evidence-engine/CONTRIBUTING.md +128 -0
- package/src/a11y-evidence-engine/LICENSE +21 -0
- package/src/a11y-evidence-engine/README.md +71 -0
- package/src/a11y-evidence-engine/bin/a11y-engine.js +11 -0
- package/src/a11y-evidence-engine/fixtures/bad/button-no-name.html +30 -0
- package/src/a11y-evidence-engine/fixtures/bad/img-missing-alt.html +19 -0
- package/src/a11y-evidence-engine/fixtures/bad/input-missing-label.html +26 -0
- package/src/a11y-evidence-engine/fixtures/bad/missing-lang.html +11 -0
- package/src/a11y-evidence-engine/fixtures/good/index.html +29 -0
- package/src/a11y-evidence-engine/package-lock.json +109 -0
- package/src/a11y-evidence-engine/package.json +45 -0
- package/src/a11y-evidence-engine/src/cli.js +74 -0
- package/src/a11y-evidence-engine/src/evidence/canonicalize.js +52 -0
- package/src/a11y-evidence-engine/src/evidence/json_pointer.js +34 -0
- package/src/a11y-evidence-engine/src/evidence/prov_emit.js +153 -0
- package/src/a11y-evidence-engine/src/fswalk.js +56 -0
- package/src/a11y-evidence-engine/src/html_parse.js +117 -0
- package/src/a11y-evidence-engine/src/ids.js +53 -0
- package/src/a11y-evidence-engine/src/rules/document_missing_lang.js +50 -0
- package/src/a11y-evidence-engine/src/rules/form_control_missing_label.js +105 -0
- package/src/a11y-evidence-engine/src/rules/img_missing_alt.js +77 -0
- package/src/a11y-evidence-engine/src/rules/index.js +37 -0
- package/src/a11y-evidence-engine/src/rules/interactive_missing_name.js +129 -0
- package/src/a11y-evidence-engine/src/scan.js +128 -0
- package/src/a11y-evidence-engine/test/scan.test.js +149 -0
- package/src/a11y-evidence-engine/test/vectors.test.js +200 -0
- package/src/a11y-lint/.github/workflows/ci.yml +46 -0
- package/src/a11y-lint/.github/workflows/test.yml +34 -0
- package/src/a11y-lint/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-lint/CONTRIBUTING.md +70 -0
- package/src/a11y-lint/GOVERNANCE.md +57 -0
- package/src/a11y-lint/LICENSE +21 -0
- package/src/a11y-lint/PRESS_RELEASE.md +50 -0
- package/src/a11y-lint/README.md +276 -0
- package/src/a11y-lint/RELEASE_NOTES.md +57 -0
- package/src/a11y-lint/RELEASING.md +57 -0
- package/src/a11y-lint/a11y_lint/__init__.py +64 -0
- package/src/a11y-lint/a11y_lint/cli.py +319 -0
- package/src/a11y-lint/a11y_lint/errors.py +252 -0
- package/src/a11y-lint/a11y_lint/render.py +293 -0
- package/src/a11y-lint/a11y_lint/report_md.py +289 -0
- package/src/a11y-lint/a11y_lint/scan_cli_text.py +434 -0
- package/src/a11y-lint/a11y_lint/schemas/cli.error.schema.v0.1.json +83 -0
- package/src/a11y-lint/a11y_lint/scorecard.py +244 -0
- package/src/a11y-lint/a11y_lint/validate.py +225 -0
- package/src/a11y-lint/pyproject.toml +75 -0
- package/src/a11y-lint/tests/__init__.py +1 -0
- package/src/a11y-lint/tests/test_cli.py +200 -0
- package/src/a11y-lint/tests/test_errors.py +188 -0
- package/src/a11y-lint/tests/test_render.py +202 -0
- package/src/a11y-lint/tests/test_report_md.py +188 -0
- package/src/a11y-lint/tests/test_scan_cli_text.py +290 -0
- package/src/a11y-lint/tests/test_scorecard.py +195 -0
- package/src/a11y-lint/tests/test_validate.py +257 -0
- package/src/a11y-mcp-tools/.github/workflows/ci.yml +53 -0
- package/src/a11y-mcp-tools/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-mcp-tools/CONTRIBUTING.md +136 -0
- package/src/a11y-mcp-tools/LICENSE +21 -0
- package/src/a11y-mcp-tools/PROV_METHODS_CATALOG.md +104 -0
- package/src/a11y-mcp-tools/README.md +168 -0
- package/src/a11y-mcp-tools/bin/cli.js +452 -0
- package/src/a11y-mcp-tools/bin/server.js +244 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.diagnose.ok.json +27 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.evidence.ok.json +25 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.ok.json +139 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.evidence.ok.json +88 -0
- package/src/a11y-mcp-tools/package-lock.json +189 -0
- package/src/a11y-mcp-tools/package.json +49 -0
- package/src/a11y-mcp-tools/src/envelope.js +197 -0
- package/src/a11y-mcp-tools/src/index.js +9 -0
- package/src/a11y-mcp-tools/src/schemas/artifact.js +85 -0
- package/src/a11y-mcp-tools/src/schemas/diagnosis.schema.v0.1.json +137 -0
- package/src/a11y-mcp-tools/src/schemas/envelope.schema.v0.1.json +108 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.js +97 -0
- package/src/a11y-mcp-tools/src/schemas/index.js +11 -0
- package/src/a11y-mcp-tools/src/schemas/provenance.js +140 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/tools/diagnose.js +597 -0
- package/src/a11y-mcp-tools/src/tools/evidence.js +481 -0
- package/src/a11y-mcp-tools/src/tools/index.js +10 -0
- package/src/a11y-mcp-tools/test/contract.test.mjs +154 -0
- package/src/a11y-mcp-tools/test/diagnose.test.js +485 -0
- package/src/a11y-mcp-tools/test/evidence.test.js +183 -0
- package/src/a11y-mcp-tools/test/schema.test.js +327 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Tests for report_md module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from io import StringIO
|
|
5
|
+
|
|
6
|
+
from a11y_lint.report_md import (
|
|
7
|
+
render_message_md,
|
|
8
|
+
render_scorecard_md,
|
|
9
|
+
render_report_md,
|
|
10
|
+
MarkdownReporter,
|
|
11
|
+
generate_badge_md,
|
|
12
|
+
)
|
|
13
|
+
from a11y_lint.errors import A11yMessage, Level, Location
|
|
14
|
+
from a11y_lint.scorecard import Scorecard
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestRenderMessageMd:
|
|
18
|
+
"""Tests for render_message_md function."""
|
|
19
|
+
|
|
20
|
+
def test_ok_message(self) -> None:
|
|
21
|
+
msg = A11yMessage.ok("TST001", "Test passed")
|
|
22
|
+
md = render_message_md(msg)
|
|
23
|
+
assert "### " in md
|
|
24
|
+
assert "[OK]" in md
|
|
25
|
+
assert "TST001" in md
|
|
26
|
+
assert "Test passed" in md
|
|
27
|
+
|
|
28
|
+
def test_error_message_has_why_fix(self) -> None:
|
|
29
|
+
msg = A11yMessage.error("TST001", "Error", "This is why", "This is fix")
|
|
30
|
+
md = render_message_md(msg)
|
|
31
|
+
assert "**Why:**" in md
|
|
32
|
+
assert "This is why" in md
|
|
33
|
+
assert "**Fix:**" in md
|
|
34
|
+
assert "This is fix" in md
|
|
35
|
+
|
|
36
|
+
def test_message_with_location(self) -> None:
|
|
37
|
+
msg = A11yMessage.ok(
|
|
38
|
+
"TST001",
|
|
39
|
+
"Test",
|
|
40
|
+
location=Location(file="test.py", line=10, column=5),
|
|
41
|
+
)
|
|
42
|
+
md = render_message_md(msg)
|
|
43
|
+
assert "**Location:**" in md
|
|
44
|
+
assert "test.py" in md
|
|
45
|
+
assert "line 10" in md
|
|
46
|
+
|
|
47
|
+
def test_message_with_context(self) -> None:
|
|
48
|
+
msg = A11yMessage.ok(
|
|
49
|
+
"TST001",
|
|
50
|
+
"Test",
|
|
51
|
+
location=Location(context="some code here"),
|
|
52
|
+
)
|
|
53
|
+
md = render_message_md(msg)
|
|
54
|
+
assert "```" in md
|
|
55
|
+
assert "some code here" in md
|
|
56
|
+
|
|
57
|
+
def test_message_with_rule(self) -> None:
|
|
58
|
+
msg = A11yMessage.ok("TST001", "Test", rule="test-rule")
|
|
59
|
+
md = render_message_md(msg)
|
|
60
|
+
assert "*Rule: `test-rule`*" in md
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestRenderScorecardMd:
|
|
64
|
+
"""Tests for render_scorecard_md function."""
|
|
65
|
+
|
|
66
|
+
def test_empty_scorecard(self) -> None:
|
|
67
|
+
card = Scorecard(name="Test Card")
|
|
68
|
+
md = render_scorecard_md(card)
|
|
69
|
+
assert "# Test Card" in md
|
|
70
|
+
assert "100.0% (A)" in md
|
|
71
|
+
|
|
72
|
+
def test_scorecard_with_rules(self) -> None:
|
|
73
|
+
card = Scorecard(name="Test")
|
|
74
|
+
card.add_message(A11yMessage.ok("TST001", "Test", rule="rule-a"))
|
|
75
|
+
card.add_message(A11yMessage.warn("TST002", "Test", "Why", rule="rule-b"))
|
|
76
|
+
md = render_scorecard_md(card)
|
|
77
|
+
assert "## Rules" in md
|
|
78
|
+
assert "`rule-a`" in md
|
|
79
|
+
assert "`rule-b`" in md
|
|
80
|
+
|
|
81
|
+
def test_scorecard_summary_table(self) -> None:
|
|
82
|
+
card = Scorecard(name="Test")
|
|
83
|
+
card.add_message(A11yMessage.ok("TST001", "Test", rule="r"))
|
|
84
|
+
md = render_scorecard_md(card)
|
|
85
|
+
assert "| Metric | Count |" in md
|
|
86
|
+
assert "| Total Checks | 1 |" in md
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestRenderReportMd:
|
|
90
|
+
"""Tests for render_report_md function."""
|
|
91
|
+
|
|
92
|
+
def test_empty_report(self) -> None:
|
|
93
|
+
md = render_report_md([])
|
|
94
|
+
assert "# Accessibility Report" in md
|
|
95
|
+
assert "## Summary" in md
|
|
96
|
+
assert "Passing" in md
|
|
97
|
+
|
|
98
|
+
def test_report_with_errors(self) -> None:
|
|
99
|
+
messages = [
|
|
100
|
+
A11yMessage.error("TST001", "Error 1", "Why", "Fix", rule="r"),
|
|
101
|
+
]
|
|
102
|
+
md = render_report_md(messages)
|
|
103
|
+
assert "Failing" in md
|
|
104
|
+
assert "## Errors" in md
|
|
105
|
+
assert "Errors: 1" in md
|
|
106
|
+
|
|
107
|
+
def test_report_with_warnings(self) -> None:
|
|
108
|
+
messages = [
|
|
109
|
+
A11yMessage.warn("TST001", "Warning 1", "Why", rule="r"),
|
|
110
|
+
]
|
|
111
|
+
md = render_report_md(messages)
|
|
112
|
+
assert "## Warnings" in md
|
|
113
|
+
assert "Warnings: 1" in md
|
|
114
|
+
|
|
115
|
+
def test_report_with_passed(self) -> None:
|
|
116
|
+
messages = [
|
|
117
|
+
A11yMessage.ok("TST001", "Passed 1", rule="r"),
|
|
118
|
+
]
|
|
119
|
+
md = render_report_md(messages)
|
|
120
|
+
assert "## Passed" in md
|
|
121
|
+
assert "TST001" in md
|
|
122
|
+
|
|
123
|
+
def test_custom_title(self) -> None:
|
|
124
|
+
md = render_report_md([], title="Custom Title")
|
|
125
|
+
assert "# Custom Title" in md
|
|
126
|
+
|
|
127
|
+
def test_timestamp(self) -> None:
|
|
128
|
+
md = render_report_md([], include_timestamp=True)
|
|
129
|
+
assert "*Generated:" in md
|
|
130
|
+
|
|
131
|
+
def test_no_timestamp(self) -> None:
|
|
132
|
+
md = render_report_md([], include_timestamp=False)
|
|
133
|
+
assert "*Generated:" not in md
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestMarkdownReporter:
|
|
137
|
+
"""Tests for MarkdownReporter class."""
|
|
138
|
+
|
|
139
|
+
def test_render(self) -> None:
|
|
140
|
+
reporter = MarkdownReporter(title="Test Report")
|
|
141
|
+
messages = [A11yMessage.ok("TST001", "Test")]
|
|
142
|
+
md = reporter.render(messages)
|
|
143
|
+
assert "# Test Report" in md
|
|
144
|
+
|
|
145
|
+
def test_render_scorecard(self) -> None:
|
|
146
|
+
reporter = MarkdownReporter()
|
|
147
|
+
card = Scorecard(name="Test Card")
|
|
148
|
+
md = reporter.render_scorecard(card)
|
|
149
|
+
assert "# Test Card" in md
|
|
150
|
+
|
|
151
|
+
def test_write_to_stream(self) -> None:
|
|
152
|
+
reporter = MarkdownReporter()
|
|
153
|
+
messages = [A11yMessage.ok("TST001", "Test")]
|
|
154
|
+
stream = StringIO()
|
|
155
|
+
reporter.write(messages, stream)
|
|
156
|
+
output = stream.getvalue()
|
|
157
|
+
assert "# Accessibility Report" in output
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TestGenerateBadgeMd:
|
|
161
|
+
"""Tests for generate_badge_md function."""
|
|
162
|
+
|
|
163
|
+
def test_high_score_green(self) -> None:
|
|
164
|
+
badge = generate_badge_md(95)
|
|
165
|
+
assert "brightgreen" in badge
|
|
166
|
+
assert "95%25" in badge
|
|
167
|
+
|
|
168
|
+
def test_medium_score_yellow(self) -> None:
|
|
169
|
+
badge = generate_badge_md(75)
|
|
170
|
+
assert "yellow" in badge
|
|
171
|
+
|
|
172
|
+
def test_low_score_orange(self) -> None:
|
|
173
|
+
badge = generate_badge_md(55)
|
|
174
|
+
assert "orange" in badge
|
|
175
|
+
|
|
176
|
+
def test_very_low_score_red(self) -> None:
|
|
177
|
+
badge = generate_badge_md(40)
|
|
178
|
+
assert "red" in badge
|
|
179
|
+
|
|
180
|
+
def test_custom_label(self) -> None:
|
|
181
|
+
badge = generate_badge_md(90, label="accessibility")
|
|
182
|
+
assert "accessibility" in badge
|
|
183
|
+
|
|
184
|
+
def test_shields_io_format(self) -> None:
|
|
185
|
+
badge = generate_badge_md(90)
|
|
186
|
+
assert "shields.io/badge" in badge
|
|
187
|
+
assert "
|
|
20
|
+
from a11y_lint.errors import Level, ErrorCodes
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestCheckLineLength:
|
|
24
|
+
"""Tests for line length check."""
|
|
25
|
+
|
|
26
|
+
def test_short_line_ok(self) -> None:
|
|
27
|
+
result = check_line_length("Short line", None, 1)
|
|
28
|
+
assert result is None
|
|
29
|
+
|
|
30
|
+
def test_long_line_warns(self) -> None:
|
|
31
|
+
long_line = "x" * (MAX_LINE_LENGTH + 1)
|
|
32
|
+
result = check_line_length(long_line, None, 1)
|
|
33
|
+
assert result is not None
|
|
34
|
+
assert result.level == Level.WARN
|
|
35
|
+
assert result.code == ErrorCodes.LINE_TOO_LONG
|
|
36
|
+
|
|
37
|
+
def test_exactly_max_length_ok(self) -> None:
|
|
38
|
+
line = "x" * MAX_LINE_LENGTH
|
|
39
|
+
result = check_line_length(line, None, 1)
|
|
40
|
+
assert result is None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestCheckAllCaps:
|
|
44
|
+
"""Tests for all caps check."""
|
|
45
|
+
|
|
46
|
+
def test_normal_text_ok(self) -> None:
|
|
47
|
+
result = check_all_caps("This is normal text", None, 1)
|
|
48
|
+
assert result is None
|
|
49
|
+
|
|
50
|
+
def test_all_caps_warns(self) -> None:
|
|
51
|
+
result = check_all_caps("THIS IS SHOUTING TEXT", None, 1)
|
|
52
|
+
assert result is not None
|
|
53
|
+
assert result.level == Level.WARN
|
|
54
|
+
assert result.code == ErrorCodes.ALL_CAPS_MESSAGE
|
|
55
|
+
|
|
56
|
+
def test_allowed_acronyms_ok(self) -> None:
|
|
57
|
+
result = check_all_caps("ERROR: Something went wrong", None, 1)
|
|
58
|
+
assert result is None
|
|
59
|
+
|
|
60
|
+
def test_short_caps_ok(self) -> None:
|
|
61
|
+
# Words with 4 or fewer caps are allowed
|
|
62
|
+
result = check_all_caps("The HTML page", None, 1)
|
|
63
|
+
assert result is None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestCheckJargon:
|
|
67
|
+
"""Tests for jargon check."""
|
|
68
|
+
|
|
69
|
+
def test_plain_text_ok(self) -> None:
|
|
70
|
+
result = check_jargon("File not found", None, 1)
|
|
71
|
+
assert result is None
|
|
72
|
+
|
|
73
|
+
def test_jargon_warns(self) -> None:
|
|
74
|
+
result = check_jargon("Received EOF unexpectedly", None, 1)
|
|
75
|
+
assert result is not None
|
|
76
|
+
assert result.level == Level.WARN
|
|
77
|
+
assert result.code == ErrorCodes.JARGON_DETECTED
|
|
78
|
+
|
|
79
|
+
def test_stdin_jargon(self) -> None:
|
|
80
|
+
result = check_jargon("Reading from STDIN", None, 1)
|
|
81
|
+
assert result is not None
|
|
82
|
+
assert "STDIN" in result.what
|
|
83
|
+
|
|
84
|
+
def test_pid_jargon(self) -> None:
|
|
85
|
+
result = check_jargon("Process PID: 12345", None, 1)
|
|
86
|
+
assert result is not None
|
|
87
|
+
assert "PID" in result.what
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestCheckColorOnly:
|
|
91
|
+
"""Tests for color-only information check."""
|
|
92
|
+
|
|
93
|
+
def test_normal_text_ok(self) -> None:
|
|
94
|
+
result = check_color_only("Error: File not found", None, 1)
|
|
95
|
+
assert result is None
|
|
96
|
+
|
|
97
|
+
def test_color_only_errors(self) -> None:
|
|
98
|
+
result = check_color_only("Errors are shown in red", None, 1)
|
|
99
|
+
assert result is not None
|
|
100
|
+
assert result.level == Level.ERROR
|
|
101
|
+
assert result.code == ErrorCodes.COLOR_ONLY_INFO
|
|
102
|
+
|
|
103
|
+
def test_color_indicates_errors(self) -> None:
|
|
104
|
+
result = check_color_only("Green indicates success", None, 1)
|
|
105
|
+
assert result is not None
|
|
106
|
+
assert result.level == Level.ERROR
|
|
107
|
+
|
|
108
|
+
def test_highlighted_in_color(self) -> None:
|
|
109
|
+
result = check_color_only("Errors are highlighted in yellow", None, 1)
|
|
110
|
+
assert result is not None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestCheckEmojiOveruse:
|
|
114
|
+
"""Tests for emoji overuse check."""
|
|
115
|
+
|
|
116
|
+
def test_no_emoji_ok(self) -> None:
|
|
117
|
+
result = check_emoji_overuse("Normal text", None, 1)
|
|
118
|
+
assert result is None
|
|
119
|
+
|
|
120
|
+
def test_few_emoji_ok(self) -> None:
|
|
121
|
+
result = check_emoji_overuse("Hello \U0001F600\U0001F600\U0001F600", None, 1)
|
|
122
|
+
assert result is None # 3 or fewer is OK
|
|
123
|
+
|
|
124
|
+
def test_many_emoji_warns(self) -> None:
|
|
125
|
+
result = check_emoji_overuse(
|
|
126
|
+
"Hello \U0001F600\U0001F600\U0001F600\U0001F600\U0001F600", None, 1
|
|
127
|
+
)
|
|
128
|
+
assert result is not None
|
|
129
|
+
assert result.level == Level.WARN
|
|
130
|
+
assert result.code == ErrorCodes.EMOJI_OVERUSE
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestCheckMissingPunctuation:
|
|
134
|
+
"""Tests for missing punctuation check."""
|
|
135
|
+
|
|
136
|
+
def test_normal_text_not_checked(self) -> None:
|
|
137
|
+
# Only error-like messages are checked
|
|
138
|
+
result = check_missing_punctuation("Normal text without punctuation", None, 1)
|
|
139
|
+
assert result is None
|
|
140
|
+
|
|
141
|
+
def test_error_with_punctuation_ok(self) -> None:
|
|
142
|
+
result = check_missing_punctuation("ERROR: File not found.", None, 1)
|
|
143
|
+
assert result is None
|
|
144
|
+
|
|
145
|
+
def test_error_without_punctuation_warns(self) -> None:
|
|
146
|
+
result = check_missing_punctuation("ERROR: File not found", None, 1)
|
|
147
|
+
assert result is not None
|
|
148
|
+
assert result.level == Level.WARN
|
|
149
|
+
assert result.code == ErrorCodes.NO_PUNCTUATION
|
|
150
|
+
|
|
151
|
+
def test_colon_counts_as_punctuation(self) -> None:
|
|
152
|
+
result = check_missing_punctuation("ERROR:", None, 1)
|
|
153
|
+
assert result is None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestCheckErrorStructure:
|
|
157
|
+
"""Tests for error structure check."""
|
|
158
|
+
|
|
159
|
+
def test_normal_text_not_checked(self) -> None:
|
|
160
|
+
result = check_error_structure("Normal text", None, 1)
|
|
161
|
+
assert result is None
|
|
162
|
+
|
|
163
|
+
def test_error_with_explanation_ok(self) -> None:
|
|
164
|
+
result = check_error_structure(
|
|
165
|
+
"ERROR: Failed because the file was not found", None, 1
|
|
166
|
+
)
|
|
167
|
+
assert result is None
|
|
168
|
+
|
|
169
|
+
def test_error_with_fix_ok(self) -> None:
|
|
170
|
+
result = check_error_structure(
|
|
171
|
+
"ERROR: Failed. Try running as administrator.", None, 1
|
|
172
|
+
)
|
|
173
|
+
assert result is None
|
|
174
|
+
|
|
175
|
+
def test_error_without_context_warns(self) -> None:
|
|
176
|
+
result = check_error_structure("ERROR: Operation failed", None, 1)
|
|
177
|
+
assert result is not None
|
|
178
|
+
assert result.level == Level.WARN
|
|
179
|
+
assert result.code == ErrorCodes.MISSING_WHY
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TestCheckAmbiguousPronouns:
|
|
183
|
+
"""Tests for ambiguous pronoun check."""
|
|
184
|
+
|
|
185
|
+
def test_clear_text_ok(self) -> None:
|
|
186
|
+
result = check_ambiguous_pronouns("The file was not found", None, 1)
|
|
187
|
+
assert result is None
|
|
188
|
+
|
|
189
|
+
def test_it_failed_warns(self) -> None:
|
|
190
|
+
result = check_ambiguous_pronouns("It failed", None, 1)
|
|
191
|
+
assert result is not None
|
|
192
|
+
assert result.level == Level.WARN
|
|
193
|
+
assert result.code == ErrorCodes.AMBIGUOUS_PRONOUN
|
|
194
|
+
|
|
195
|
+
def test_this_is_invalid_warns(self) -> None:
|
|
196
|
+
result = check_ambiguous_pronouns("This is invalid", None, 1)
|
|
197
|
+
assert result is not None
|
|
198
|
+
|
|
199
|
+
def test_pronoun_mid_sentence_ok(self) -> None:
|
|
200
|
+
# Only check at start of line
|
|
201
|
+
result = check_ambiguous_pronouns("The process failed because it ran out of memory", None, 1)
|
|
202
|
+
assert result is None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TestScanner:
|
|
206
|
+
"""Tests for Scanner class."""
|
|
207
|
+
|
|
208
|
+
def test_scan_empty_text(self) -> None:
|
|
209
|
+
scanner = Scanner()
|
|
210
|
+
messages = scanner.scan_text("")
|
|
211
|
+
assert messages == []
|
|
212
|
+
|
|
213
|
+
def test_scan_clean_text(self) -> None:
|
|
214
|
+
scanner = Scanner()
|
|
215
|
+
# Use text that doesn't start with pronouns and has proper structure
|
|
216
|
+
messages = scanner.scan_text("File processed successfully.\nAll checks passed.")
|
|
217
|
+
assert len(messages) == 0
|
|
218
|
+
|
|
219
|
+
def test_scan_problematic_text(self) -> None:
|
|
220
|
+
scanner = Scanner()
|
|
221
|
+
messages = scanner.scan_text("ERROR: It failed")
|
|
222
|
+
# Should find: ambiguous pronoun, no punctuation, no explanation
|
|
223
|
+
assert len(messages) >= 2
|
|
224
|
+
|
|
225
|
+
def test_scan_with_source_file(self) -> None:
|
|
226
|
+
scanner = Scanner()
|
|
227
|
+
messages = scanner.scan_text("ERROR: It failed", file="test.txt")
|
|
228
|
+
for msg in messages:
|
|
229
|
+
if msg.location:
|
|
230
|
+
assert msg.location.file == "test.txt"
|
|
231
|
+
|
|
232
|
+
def test_disable_rule(self) -> None:
|
|
233
|
+
scanner = Scanner()
|
|
234
|
+
scanner.disable_rule("no-ambiguous-pronouns")
|
|
235
|
+
messages = scanner.scan_text("It failed")
|
|
236
|
+
# Should not find ambiguous pronoun warning
|
|
237
|
+
assert not any(m.code == ErrorCodes.AMBIGUOUS_PRONOUN for m in messages)
|
|
238
|
+
|
|
239
|
+
def test_enable_only_rule(self) -> None:
|
|
240
|
+
scanner = Scanner()
|
|
241
|
+
scanner.rules = []
|
|
242
|
+
scanner.enable_rule("no-all-caps")
|
|
243
|
+
messages = scanner.scan_text("THIS IS SHOUTING")
|
|
244
|
+
assert len(messages) == 1
|
|
245
|
+
assert messages[0].code == ErrorCodes.ALL_CAPS_MESSAGE
|
|
246
|
+
|
|
247
|
+
def test_error_and_warn_counts(self) -> None:
|
|
248
|
+
scanner = Scanner()
|
|
249
|
+
# Color-only is an error, others are warnings
|
|
250
|
+
scanner.scan_text("Errors are shown in red. THIS IS SHOUTING.")
|
|
251
|
+
assert scanner.error_count >= 1
|
|
252
|
+
assert scanner.warn_count >= 1
|
|
253
|
+
|
|
254
|
+
def test_has_errors(self) -> None:
|
|
255
|
+
scanner = Scanner()
|
|
256
|
+
scanner.scan_text("Errors are shown in red")
|
|
257
|
+
assert scanner.has_errors is True
|
|
258
|
+
|
|
259
|
+
scanner2 = Scanner()
|
|
260
|
+
scanner2.scan_text("Normal text.")
|
|
261
|
+
assert scanner2.has_errors is False
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class TestScanFunction:
|
|
265
|
+
"""Tests for scan convenience function."""
|
|
266
|
+
|
|
267
|
+
def test_scan_returns_messages(self) -> None:
|
|
268
|
+
messages = scan("ERROR: It failed")
|
|
269
|
+
assert len(messages) >= 1
|
|
270
|
+
|
|
271
|
+
def test_scan_with_file(self) -> None:
|
|
272
|
+
messages = scan("ERROR: It failed", file="test.py")
|
|
273
|
+
for msg in messages:
|
|
274
|
+
if msg.location and msg.location.file:
|
|
275
|
+
assert msg.location.file == "test.py"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class TestGetRuleNames:
|
|
279
|
+
"""Tests for get_rule_names function."""
|
|
280
|
+
|
|
281
|
+
def test_returns_all_rules(self) -> None:
|
|
282
|
+
names = get_rule_names()
|
|
283
|
+
assert len(names) == len(RULES)
|
|
284
|
+
|
|
285
|
+
def test_known_rules_present(self) -> None:
|
|
286
|
+
names = get_rule_names()
|
|
287
|
+
assert "line-length" in names
|
|
288
|
+
assert "no-all-caps" in names
|
|
289
|
+
assert "plain-language" in names
|
|
290
|
+
assert "no-color-only" in names
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Tests for scorecard module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from a11y_lint.scorecard import (
|
|
6
|
+
RuleScore,
|
|
7
|
+
Scorecard,
|
|
8
|
+
ScorecardBuilder,
|
|
9
|
+
create_scorecard,
|
|
10
|
+
)
|
|
11
|
+
from a11y_lint.errors import A11yMessage, Level
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestRuleScore:
|
|
15
|
+
"""Tests for RuleScore dataclass."""
|
|
16
|
+
|
|
17
|
+
def test_empty_score(self) -> None:
|
|
18
|
+
score = RuleScore(rule="test")
|
|
19
|
+
assert score.total == 0
|
|
20
|
+
assert score.score == 100.0 # No failures = perfect
|
|
21
|
+
assert score.grade == "A"
|
|
22
|
+
|
|
23
|
+
def test_all_passed(self) -> None:
|
|
24
|
+
score = RuleScore(rule="test", passed=10)
|
|
25
|
+
assert score.total == 10
|
|
26
|
+
assert score.score == 100.0
|
|
27
|
+
assert score.grade == "A"
|
|
28
|
+
|
|
29
|
+
def test_all_errors(self) -> None:
|
|
30
|
+
score = RuleScore(rule="test", errors=10)
|
|
31
|
+
assert score.total == 10
|
|
32
|
+
assert score.score == 0.0
|
|
33
|
+
assert score.grade == "F"
|
|
34
|
+
|
|
35
|
+
def test_all_warnings(self) -> None:
|
|
36
|
+
score = RuleScore(rule="test", warnings=10)
|
|
37
|
+
assert score.total == 10
|
|
38
|
+
assert score.score == 50.0 # Warnings = half points
|
|
39
|
+
assert score.grade == "F"
|
|
40
|
+
|
|
41
|
+
def test_mixed_results(self) -> None:
|
|
42
|
+
score = RuleScore(rule="test", passed=7, warnings=2, errors=1)
|
|
43
|
+
assert score.total == 10
|
|
44
|
+
# 7 + (2 * 0.5) + 0 = 8 / 10 = 80%
|
|
45
|
+
assert score.score == 80.0
|
|
46
|
+
assert score.grade == "B"
|
|
47
|
+
|
|
48
|
+
def test_grade_boundaries(self) -> None:
|
|
49
|
+
# A: >= 90
|
|
50
|
+
assert RuleScore(rule="t", passed=9, errors=1).grade == "A"
|
|
51
|
+
# B: >= 80
|
|
52
|
+
assert RuleScore(rule="t", passed=8, errors=2).grade == "B"
|
|
53
|
+
# C: >= 70
|
|
54
|
+
assert RuleScore(rule="t", passed=7, errors=3).grade == "C"
|
|
55
|
+
# D: >= 60
|
|
56
|
+
assert RuleScore(rule="t", passed=6, errors=4).grade == "D"
|
|
57
|
+
# F: < 60
|
|
58
|
+
assert RuleScore(rule="t", passed=5, errors=5).grade == "F"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestScorecard:
|
|
62
|
+
"""Tests for Scorecard class."""
|
|
63
|
+
|
|
64
|
+
def test_empty_scorecard(self) -> None:
|
|
65
|
+
card = Scorecard(name="Test")
|
|
66
|
+
assert card.total_checks == 0
|
|
67
|
+
assert card.overall_score == 100.0
|
|
68
|
+
assert card.is_passing is True
|
|
69
|
+
|
|
70
|
+
def test_add_ok_message(self) -> None:
|
|
71
|
+
card = Scorecard(name="Test")
|
|
72
|
+
card.add_message(A11yMessage.ok("TST001", "Passed", rule="test-rule"))
|
|
73
|
+
assert card.total_passed == 1
|
|
74
|
+
assert card.total_warnings == 0
|
|
75
|
+
assert card.total_errors == 0
|
|
76
|
+
assert "test-rule" in card.rule_scores
|
|
77
|
+
|
|
78
|
+
def test_add_warn_message(self) -> None:
|
|
79
|
+
card = Scorecard(name="Test")
|
|
80
|
+
card.add_message(A11yMessage.warn("TST001", "Warning", "Why", rule="test-rule"))
|
|
81
|
+
assert card.total_warnings == 1
|
|
82
|
+
assert card.is_passing is True # Warnings don't fail
|
|
83
|
+
|
|
84
|
+
def test_add_error_message(self) -> None:
|
|
85
|
+
card = Scorecard(name="Test")
|
|
86
|
+
card.add_message(
|
|
87
|
+
A11yMessage.error("TST001", "Error", "Why", "Fix", rule="test-rule")
|
|
88
|
+
)
|
|
89
|
+
assert card.total_errors == 1
|
|
90
|
+
assert card.is_passing is False
|
|
91
|
+
|
|
92
|
+
def test_add_messages_batch(self) -> None:
|
|
93
|
+
card = Scorecard(name="Test")
|
|
94
|
+
messages = [
|
|
95
|
+
A11yMessage.ok("TST001", "Test 1", rule="rule-a"),
|
|
96
|
+
A11yMessage.warn("TST002", "Test 2", "Why", rule="rule-b"),
|
|
97
|
+
A11yMessage.error("TST003", "Test 3", "Why", "Fix", rule="rule-a"),
|
|
98
|
+
]
|
|
99
|
+
card.add_messages(messages)
|
|
100
|
+
assert card.total_checks == 3
|
|
101
|
+
assert len(card.rule_scores) == 2
|
|
102
|
+
|
|
103
|
+
def test_overall_score_calculation(self) -> None:
|
|
104
|
+
card = Scorecard(name="Test")
|
|
105
|
+
card.add_messages(
|
|
106
|
+
[
|
|
107
|
+
A11yMessage.ok("TST001", "Test 1", rule="r"),
|
|
108
|
+
A11yMessage.ok("TST002", "Test 2", rule="r"),
|
|
109
|
+
A11yMessage.warn("TST003", "Test 3", "Why", rule="r"),
|
|
110
|
+
A11yMessage.error("TST004", "Test 4", "Why", "Fix", rule="r"),
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
# 2 passed + 0.5 warn + 0 error = 2.5 / 4 = 62.5%
|
|
114
|
+
assert card.overall_score == 62.5
|
|
115
|
+
assert card.overall_grade == "D"
|
|
116
|
+
|
|
117
|
+
def test_summary(self) -> None:
|
|
118
|
+
card = Scorecard(name="Test Card")
|
|
119
|
+
card.add_message(A11yMessage.ok("TST001", "Test", rule="test"))
|
|
120
|
+
summary = card.summary()
|
|
121
|
+
assert "Test Card" in summary
|
|
122
|
+
assert "Passed: 1" in summary
|
|
123
|
+
|
|
124
|
+
def test_to_dict(self) -> None:
|
|
125
|
+
card = Scorecard(name="Test")
|
|
126
|
+
card.add_message(A11yMessage.ok("TST001", "Test", rule="test-rule"))
|
|
127
|
+
d = card.to_dict()
|
|
128
|
+
assert d["name"] == "Test"
|
|
129
|
+
assert d["overall_score"] == 100.0
|
|
130
|
+
assert d["overall_grade"] == "A"
|
|
131
|
+
assert d["is_passing"] is True
|
|
132
|
+
assert d["totals"]["passed"] == 1
|
|
133
|
+
assert "test-rule" in d["rules"]
|
|
134
|
+
assert len(d["messages"]) == 1
|
|
135
|
+
|
|
136
|
+
def test_unknown_rule(self) -> None:
|
|
137
|
+
card = Scorecard(name="Test")
|
|
138
|
+
# Message without rule gets assigned to "unknown"
|
|
139
|
+
card.add_message(A11yMessage.ok("TST001", "Test"))
|
|
140
|
+
assert "unknown" in card.rule_scores
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TestScorecardBuilder:
|
|
144
|
+
"""Tests for ScorecardBuilder class."""
|
|
145
|
+
|
|
146
|
+
def test_build_empty(self) -> None:
|
|
147
|
+
builder = ScorecardBuilder()
|
|
148
|
+
card = builder.build()
|
|
149
|
+
assert card.total_checks == 0
|
|
150
|
+
|
|
151
|
+
def test_custom_name(self) -> None:
|
|
152
|
+
builder = ScorecardBuilder(name="Custom Name")
|
|
153
|
+
card = builder.build()
|
|
154
|
+
assert card.name == "Custom Name"
|
|
155
|
+
|
|
156
|
+
def test_add_scan_result(self) -> None:
|
|
157
|
+
messages = [
|
|
158
|
+
A11yMessage.ok("TST001", "Test 1", rule="r"),
|
|
159
|
+
A11yMessage.ok("TST002", "Test 2", rule="r"),
|
|
160
|
+
]
|
|
161
|
+
builder = ScorecardBuilder()
|
|
162
|
+
builder.add_scan_result(messages)
|
|
163
|
+
card = builder.build()
|
|
164
|
+
assert card.total_checks == 2
|
|
165
|
+
|
|
166
|
+
def test_add_ok_check(self) -> None:
|
|
167
|
+
builder = ScorecardBuilder()
|
|
168
|
+
builder.add_ok_check("test-rule", "TST001", "Test passed")
|
|
169
|
+
card = builder.build()
|
|
170
|
+
assert card.total_passed == 1
|
|
171
|
+
|
|
172
|
+
def test_chaining(self) -> None:
|
|
173
|
+
card = (
|
|
174
|
+
ScorecardBuilder(name="Chained")
|
|
175
|
+
.add_ok_check("rule-1", "TST001", "Test 1")
|
|
176
|
+
.add_ok_check("rule-2", "TST002", "Test 2")
|
|
177
|
+
.build()
|
|
178
|
+
)
|
|
179
|
+
assert card.total_checks == 2
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TestCreateScorecard:
|
|
183
|
+
"""Tests for create_scorecard convenience function."""
|
|
184
|
+
|
|
185
|
+
def test_create_from_messages(self) -> None:
|
|
186
|
+
messages = [
|
|
187
|
+
A11yMessage.ok("TST001", "Test", rule="r"),
|
|
188
|
+
A11yMessage.warn("TST002", "Test", "Why", rule="r"),
|
|
189
|
+
]
|
|
190
|
+
card = create_scorecard(messages)
|
|
191
|
+
assert card.total_checks == 2
|
|
192
|
+
|
|
193
|
+
def test_custom_name(self) -> None:
|
|
194
|
+
card = create_scorecard([], name="Custom")
|
|
195
|
+
assert card.name == "Custom"
|