@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.
Files changed (241) hide show
  1. package/.github/workflows/ci.yml +63 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -0
  4. package/docs/prov-spec/.github/workflows/ci.yml +68 -0
  5. package/docs/prov-spec/CHANGELOG.md +69 -0
  6. package/docs/prov-spec/CODE_OF_CONDUCT.md +129 -0
  7. package/docs/prov-spec/CONFORMANCE_LEVELS.md +223 -0
  8. package/docs/prov-spec/CONTRIBUTING.md +145 -0
  9. package/docs/prov-spec/IMPLEMENTER_CHECKLIST.md +137 -0
  10. package/docs/prov-spec/LICENSE +21 -0
  11. package/docs/prov-spec/PRESS_RELEASE.md +74 -0
  12. package/docs/prov-spec/README.md +182 -0
  13. package/docs/prov-spec/SETUP.md +135 -0
  14. package/docs/prov-spec/WHY.md +86 -0
  15. package/docs/prov-spec/examples/artifact.example.json +14 -0
  16. package/docs/prov-spec/examples/artifact.ref.example.json +9 -0
  17. package/docs/prov-spec/examples/evidence.example.json +6 -0
  18. package/docs/prov-spec/examples/mcp.envelope.example.json +97 -0
  19. package/docs/prov-spec/examples/mcp.request.example.json +28 -0
  20. package/docs/prov-spec/examples/prov.record.example.json +35 -0
  21. package/docs/prov-spec/interop/PROOF_NODE_ENGINE.md +114 -0
  22. package/docs/prov-spec/spec/MCP_COMPATIBILITY.md +241 -0
  23. package/docs/prov-spec/spec/PROV_METHODS_CATALOG.md +142 -0
  24. package/docs/prov-spec/spec/PROV_METHODS_SPEC.md +397 -0
  25. package/docs/prov-spec/spec/methods.json +213 -0
  26. package/docs/prov-spec/spec/schemas/artifact.ref.schema.v0.1.json +58 -0
  27. package/docs/prov-spec/spec/schemas/artifact.schema.v0.1.json +61 -0
  28. package/docs/prov-spec/spec/schemas/assist.request.schema.v0.1.json +52 -0
  29. package/docs/prov-spec/spec/schemas/assist.response.schema.v0.1.json +70 -0
  30. package/docs/prov-spec/spec/schemas/cli.error.schema.v0.1.json +78 -0
  31. package/docs/prov-spec/spec/schemas/evidence.schema.v0.1.json +37 -0
  32. package/docs/prov-spec/spec/schemas/mcp.envelope.schema.v0.1.json +141 -0
  33. package/docs/prov-spec/spec/schemas/mcp.request.schema.v0.1.json +79 -0
  34. package/docs/prov-spec/spec/schemas/methods.schema.json +93 -0
  35. package/docs/prov-spec/spec/schemas/prov-capabilities.schema.json +122 -0
  36. package/docs/prov-spec/spec/schemas/prov.record.schema.v0.1.json +133 -0
  37. package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/expected.json +4 -0
  38. package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/input.json +1 -0
  39. package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/double_wrapped.json +14 -0
  40. package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/wrong_schema_version.json +11 -0
  41. package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/expected.json +24 -0
  42. package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/input.json +8 -0
  43. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/expected.json +7 -0
  44. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/input.json +1 -0
  45. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/non_hex_chars.json +16 -0
  46. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/uppercase_hex.json +16 -0
  47. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/wrong_length.json +16 -0
  48. package/docs/prov-spec/spec/vectors/method_id_syntax/negative/hyphen_separator.json +8 -0
  49. package/docs/prov-spec/spec/vectors/method_id_syntax/negative/reserved_namespace.json +8 -0
  50. package/docs/prov-spec/spec/vectors/method_id_syntax/negative/starts_with_digit.json +8 -0
  51. package/docs/prov-spec/spec/vectors/method_id_syntax/negative/uppercase.json +8 -0
  52. package/docs/prov-spec/spec/vectors/method_id_syntax/positive/valid_ids.json +18 -0
  53. package/docs/prov-spec/tools/python/prov_validator.py +428 -0
  54. package/examples/a11y-demo-site/.github/workflows/a11y-artifacts.yml +81 -0
  55. package/examples/a11y-demo-site/.github/workflows/a11y.yml +34 -0
  56. package/examples/a11y-demo-site/CODE_OF_CONDUCT.md +129 -0
  57. package/examples/a11y-demo-site/CONTRIBUTING.md +83 -0
  58. package/examples/a11y-demo-site/LICENSE +21 -0
  59. package/examples/a11y-demo-site/README.md +155 -0
  60. package/examples/a11y-demo-site/html/contact.html +15 -0
  61. package/examples/a11y-demo-site/html/index.html +20 -0
  62. package/examples/a11y-demo-site/scripts/a11y.sh +20 -0
  63. package/package.json +26 -0
  64. package/src/a11y-assist/.github/workflows/publish.yml +52 -0
  65. package/src/a11y-assist/.github/workflows/test.yml +30 -0
  66. package/src/a11y-assist/A11Y_ASSIST_TEST_COVERAGE_REQUIREMENTS.md +104 -0
  67. package/src/a11y-assist/CODE_OF_CONDUCT.md +129 -0
  68. package/src/a11y-assist/CONTRIBUTING.md +98 -0
  69. package/src/a11y-assist/ENGINE.md +363 -0
  70. package/src/a11y-assist/LICENSE +21 -0
  71. package/src/a11y-assist/PRESS_RELEASE.md +71 -0
  72. package/src/a11y-assist/QUICKSTART.md +101 -0
  73. package/src/a11y-assist/README.md +192 -0
  74. package/src/a11y-assist/RELEASE_NOTES.md +319 -0
  75. package/src/a11y-assist/a11y_assist/__init__.py +3 -0
  76. package/src/a11y-assist/a11y_assist/cli.py +599 -0
  77. package/src/a11y-assist/a11y_assist/from_cli_error.py +149 -0
  78. package/src/a11y-assist/a11y_assist/guard.py +444 -0
  79. package/src/a11y-assist/a11y_assist/ingest.py +407 -0
  80. package/src/a11y-assist/a11y_assist/methods.py +137 -0
  81. package/src/a11y-assist/a11y_assist/parse_raw.py +71 -0
  82. package/src/a11y-assist/a11y_assist/profiles/__init__.py +29 -0
  83. package/src/a11y-assist/a11y_assist/profiles/cognitive_load.py +245 -0
  84. package/src/a11y-assist/a11y_assist/profiles/cognitive_load_render.py +86 -0
  85. package/src/a11y-assist/a11y_assist/profiles/dyslexia.py +144 -0
  86. package/src/a11y-assist/a11y_assist/profiles/dyslexia_render.py +77 -0
  87. package/src/a11y-assist/a11y_assist/profiles/plain_language.py +119 -0
  88. package/src/a11y-assist/a11y_assist/profiles/plain_language_render.py +66 -0
  89. package/src/a11y-assist/a11y_assist/profiles/screen_reader.py +348 -0
  90. package/src/a11y-assist/a11y_assist/profiles/screen_reader_render.py +89 -0
  91. package/src/a11y-assist/a11y_assist/render.py +95 -0
  92. package/src/a11y-assist/a11y_assist/schemas/assist.request.schema.v0.1.json +52 -0
  93. package/src/a11y-assist/a11y_assist/schemas/assist.response.schema.v0.1.json +70 -0
  94. package/src/a11y-assist/a11y_assist/schemas/cli.error.schema.v0.1.json +78 -0
  95. package/src/a11y-assist/a11y_assist/storage.py +31 -0
  96. package/src/a11y-assist/pyproject.toml +60 -0
  97. package/src/a11y-assist/tests/__init__.py +1 -0
  98. package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_high.json +18 -0
  99. package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_medium.json +16 -0
  100. package/src/a11y-assist/tests/fixtures/base_inputs/raw_text_low.txt +3 -0
  101. package/src/a11y-assist/tests/fixtures/cli_error_good.json +9 -0
  102. package/src/a11y-assist/tests/fixtures/cli_error_missing_id.json +7 -0
  103. package/src/a11y-assist/tests/fixtures/cli_error_string_format.json +7 -0
  104. package/src/a11y-assist/tests/fixtures/expected/cognitive_load_high.txt +20 -0
  105. package/src/a11y-assist/tests/fixtures/expected/dyslexia_high.txt +20 -0
  106. package/src/a11y-assist/tests/fixtures/expected/lowvision_high.txt +18 -0
  107. package/src/a11y-assist/tests/fixtures/expected/plain_language_high.txt +14 -0
  108. package/src/a11y-assist/tests/fixtures/expected/screen_reader_high.txt +19 -0
  109. package/src/a11y-assist/tests/fixtures/golden_screen_reader_cli_error.txt +16 -0
  110. package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_no_id.txt +14 -0
  111. package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_with_id.txt +14 -0
  112. package/src/a11y-assist/tests/fixtures/raw_good.txt +11 -0
  113. package/src/a11y-assist/tests/fixtures/raw_no_id.txt +2 -0
  114. package/src/a11y-assist/tests/test_cognitive_load.py +469 -0
  115. package/src/a11y-assist/tests/test_dyslexia.py +337 -0
  116. package/src/a11y-assist/tests/test_explain.py +74 -0
  117. package/src/a11y-assist/tests/test_golden.py +127 -0
  118. package/src/a11y-assist/tests/test_guard.py +819 -0
  119. package/src/a11y-assist/tests/test_guard_integration.py +457 -0
  120. package/src/a11y-assist/tests/test_ingest.py +311 -0
  121. package/src/a11y-assist/tests/test_methods_metadata.py +236 -0
  122. package/src/a11y-assist/tests/test_plain_language.py +348 -0
  123. package/src/a11y-assist/tests/test_render.py +117 -0
  124. package/src/a11y-assist/tests/test_screen_reader.py +703 -0
  125. package/src/a11y-assist/tests/test_storage_last.py +61 -0
  126. package/src/a11y-assist/tests/test_triage.py +86 -0
  127. package/src/a11y-ci/.github/workflows/ci.yml +43 -0
  128. package/src/a11y-ci/.github/workflows/test.yml +30 -0
  129. package/src/a11y-ci/A11Y_CI_TEST_COVERAGE_REQUIREMENTS.md +94 -0
  130. package/src/a11y-ci/CODE_OF_CONDUCT.md +129 -0
  131. package/src/a11y-ci/CONTRIBUTING.md +142 -0
  132. package/src/a11y-ci/LICENSE +21 -0
  133. package/src/a11y-ci/README.md +105 -0
  134. package/src/a11y-ci/a11y_ci/__init__.py +3 -0
  135. package/src/a11y-ci/a11y_ci/allowlist.py +83 -0
  136. package/src/a11y-ci/a11y_ci/cli.py +145 -0
  137. package/src/a11y-ci/a11y_ci/gate.py +131 -0
  138. package/src/a11y-ci/a11y_ci/render.py +48 -0
  139. package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +24 -0
  140. package/src/a11y-ci/a11y_ci/scorecard.py +99 -0
  141. package/src/a11y-ci/npm/package.json +35 -0
  142. package/src/a11y-ci/pyproject.toml +64 -0
  143. package/src/a11y-ci/tests/__init__.py +1 -0
  144. package/src/a11y-ci/tests/fixtures/allowlist_expired.json +10 -0
  145. package/src/a11y-ci/tests/fixtures/allowlist_ok.json +10 -0
  146. package/src/a11y-ci/tests/fixtures/baseline_ok.json +7 -0
  147. package/src/a11y-ci/tests/fixtures/current_fail.json +6 -0
  148. package/src/a11y-ci/tests/fixtures/current_ok.json +6 -0
  149. package/src/a11y-ci/tests/fixtures/current_regresses.json +7 -0
  150. package/src/a11y-ci/tests/test_gate.py +134 -0
  151. package/src/a11y-evidence-engine/.github/workflows/ci.yml +53 -0
  152. package/src/a11y-evidence-engine/CODE_OF_CONDUCT.md +129 -0
  153. package/src/a11y-evidence-engine/CONTRIBUTING.md +128 -0
  154. package/src/a11y-evidence-engine/LICENSE +21 -0
  155. package/src/a11y-evidence-engine/README.md +71 -0
  156. package/src/a11y-evidence-engine/bin/a11y-engine.js +11 -0
  157. package/src/a11y-evidence-engine/fixtures/bad/button-no-name.html +30 -0
  158. package/src/a11y-evidence-engine/fixtures/bad/img-missing-alt.html +19 -0
  159. package/src/a11y-evidence-engine/fixtures/bad/input-missing-label.html +26 -0
  160. package/src/a11y-evidence-engine/fixtures/bad/missing-lang.html +11 -0
  161. package/src/a11y-evidence-engine/fixtures/good/index.html +29 -0
  162. package/src/a11y-evidence-engine/package-lock.json +109 -0
  163. package/src/a11y-evidence-engine/package.json +45 -0
  164. package/src/a11y-evidence-engine/src/cli.js +74 -0
  165. package/src/a11y-evidence-engine/src/evidence/canonicalize.js +52 -0
  166. package/src/a11y-evidence-engine/src/evidence/json_pointer.js +34 -0
  167. package/src/a11y-evidence-engine/src/evidence/prov_emit.js +153 -0
  168. package/src/a11y-evidence-engine/src/fswalk.js +56 -0
  169. package/src/a11y-evidence-engine/src/html_parse.js +117 -0
  170. package/src/a11y-evidence-engine/src/ids.js +53 -0
  171. package/src/a11y-evidence-engine/src/rules/document_missing_lang.js +50 -0
  172. package/src/a11y-evidence-engine/src/rules/form_control_missing_label.js +105 -0
  173. package/src/a11y-evidence-engine/src/rules/img_missing_alt.js +77 -0
  174. package/src/a11y-evidence-engine/src/rules/index.js +37 -0
  175. package/src/a11y-evidence-engine/src/rules/interactive_missing_name.js +129 -0
  176. package/src/a11y-evidence-engine/src/scan.js +128 -0
  177. package/src/a11y-evidence-engine/test/scan.test.js +149 -0
  178. package/src/a11y-evidence-engine/test/vectors.test.js +200 -0
  179. package/src/a11y-lint/.github/workflows/ci.yml +46 -0
  180. package/src/a11y-lint/.github/workflows/test.yml +34 -0
  181. package/src/a11y-lint/CODE_OF_CONDUCT.md +129 -0
  182. package/src/a11y-lint/CONTRIBUTING.md +70 -0
  183. package/src/a11y-lint/GOVERNANCE.md +57 -0
  184. package/src/a11y-lint/LICENSE +21 -0
  185. package/src/a11y-lint/PRESS_RELEASE.md +50 -0
  186. package/src/a11y-lint/README.md +276 -0
  187. package/src/a11y-lint/RELEASE_NOTES.md +57 -0
  188. package/src/a11y-lint/RELEASING.md +57 -0
  189. package/src/a11y-lint/a11y_lint/__init__.py +64 -0
  190. package/src/a11y-lint/a11y_lint/cli.py +319 -0
  191. package/src/a11y-lint/a11y_lint/errors.py +252 -0
  192. package/src/a11y-lint/a11y_lint/render.py +293 -0
  193. package/src/a11y-lint/a11y_lint/report_md.py +289 -0
  194. package/src/a11y-lint/a11y_lint/scan_cli_text.py +434 -0
  195. package/src/a11y-lint/a11y_lint/schemas/cli.error.schema.v0.1.json +83 -0
  196. package/src/a11y-lint/a11y_lint/scorecard.py +244 -0
  197. package/src/a11y-lint/a11y_lint/validate.py +225 -0
  198. package/src/a11y-lint/pyproject.toml +75 -0
  199. package/src/a11y-lint/tests/__init__.py +1 -0
  200. package/src/a11y-lint/tests/test_cli.py +200 -0
  201. package/src/a11y-lint/tests/test_errors.py +188 -0
  202. package/src/a11y-lint/tests/test_render.py +202 -0
  203. package/src/a11y-lint/tests/test_report_md.py +188 -0
  204. package/src/a11y-lint/tests/test_scan_cli_text.py +290 -0
  205. package/src/a11y-lint/tests/test_scorecard.py +195 -0
  206. package/src/a11y-lint/tests/test_validate.py +257 -0
  207. package/src/a11y-mcp-tools/.github/workflows/ci.yml +53 -0
  208. package/src/a11y-mcp-tools/CODE_OF_CONDUCT.md +129 -0
  209. package/src/a11y-mcp-tools/CONTRIBUTING.md +136 -0
  210. package/src/a11y-mcp-tools/LICENSE +21 -0
  211. package/src/a11y-mcp-tools/PROV_METHODS_CATALOG.md +104 -0
  212. package/src/a11y-mcp-tools/README.md +168 -0
  213. package/src/a11y-mcp-tools/bin/cli.js +452 -0
  214. package/src/a11y-mcp-tools/bin/server.js +244 -0
  215. package/src/a11y-mcp-tools/fixtures/requests/a11y.diagnose.ok.json +27 -0
  216. package/src/a11y-mcp-tools/fixtures/requests/a11y.evidence.ok.json +25 -0
  217. package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.ok.json +139 -0
  218. package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
  219. package/src/a11y-mcp-tools/fixtures/responses/a11y.evidence.ok.json +88 -0
  220. package/src/a11y-mcp-tools/package-lock.json +189 -0
  221. package/src/a11y-mcp-tools/package.json +49 -0
  222. package/src/a11y-mcp-tools/src/envelope.js +197 -0
  223. package/src/a11y-mcp-tools/src/index.js +9 -0
  224. package/src/a11y-mcp-tools/src/schemas/artifact.js +85 -0
  225. package/src/a11y-mcp-tools/src/schemas/diagnosis.schema.v0.1.json +137 -0
  226. package/src/a11y-mcp-tools/src/schemas/envelope.schema.v0.1.json +108 -0
  227. package/src/a11y-mcp-tools/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
  228. package/src/a11y-mcp-tools/src/schemas/evidence.js +97 -0
  229. package/src/a11y-mcp-tools/src/schemas/index.js +11 -0
  230. package/src/a11y-mcp-tools/src/schemas/provenance.js +140 -0
  231. package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
  232. package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
  233. package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
  234. package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
  235. package/src/a11y-mcp-tools/src/tools/diagnose.js +597 -0
  236. package/src/a11y-mcp-tools/src/tools/evidence.js +481 -0
  237. package/src/a11y-mcp-tools/src/tools/index.js +10 -0
  238. package/src/a11y-mcp-tools/test/contract.test.mjs +154 -0
  239. package/src/a11y-mcp-tools/test/diagnose.test.js +485 -0
  240. package/src/a11y-mcp-tools/test/evidence.test.js +183 -0
  241. 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 "![" in badge
188
+ assert "](" in badge
@@ -0,0 +1,290 @@
1
+ """Tests for scan_cli_text module."""
2
+
3
+ import pytest
4
+
5
+ from a11y_lint.scan_cli_text import (
6
+ Scanner,
7
+ scan,
8
+ get_rule_names,
9
+ RULES,
10
+ check_line_length,
11
+ check_all_caps,
12
+ check_jargon,
13
+ check_color_only,
14
+ check_emoji_overuse,
15
+ check_missing_punctuation,
16
+ check_error_structure,
17
+ check_ambiguous_pronouns,
18
+ MAX_LINE_LENGTH,
19
+ )
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"