@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,200 @@
1
+ """Tests for CLI module."""
2
+
3
+ import json
4
+ import pytest
5
+ import tempfile
6
+ from pathlib import Path
7
+ from click.testing import CliRunner
8
+
9
+ from a11y_lint.cli import main
10
+
11
+
12
+ @pytest.fixture
13
+ def runner() -> CliRunner:
14
+ """Create a CLI test runner."""
15
+ return CliRunner()
16
+
17
+
18
+ @pytest.fixture
19
+ def sample_file(tmp_path: Path) -> Path:
20
+ """Create a sample file with some text."""
21
+ file = tmp_path / "sample.txt"
22
+ file.write_text("This is a test file.\nERROR: Something went wrong.\n")
23
+ return file
24
+
25
+
26
+ @pytest.fixture
27
+ def clean_file(tmp_path: Path) -> Path:
28
+ """Create a clean file with no issues."""
29
+ file = tmp_path / "clean.txt"
30
+ file.write_text("This is a clean file with no accessibility issues.\n")
31
+ return file
32
+
33
+
34
+ @pytest.fixture
35
+ def json_file(tmp_path: Path) -> Path:
36
+ """Create a valid JSON messages file."""
37
+ file = tmp_path / "messages.json"
38
+ data = [
39
+ {"level": "OK", "code": "TST001", "what": "Test passed"},
40
+ {"level": "WARN", "code": "TST002", "what": "Test warning", "why": "Reason"},
41
+ ]
42
+ file.write_text(json.dumps(data))
43
+ return file
44
+
45
+
46
+ class TestMainCommand:
47
+ """Tests for main CLI group."""
48
+
49
+ def test_help(self, runner: CliRunner) -> None:
50
+ result = runner.invoke(main, ["--help"])
51
+ assert result.exit_code == 0
52
+ assert "Accessibility linter" in result.output
53
+
54
+ def test_version(self, runner: CliRunner) -> None:
55
+ result = runner.invoke(main, ["--version"])
56
+ assert result.exit_code == 0
57
+ assert "0.1.0" in result.output
58
+
59
+
60
+ class TestScanCommand:
61
+ """Tests for scan command."""
62
+
63
+ def test_scan_file(self, runner: CliRunner, sample_file: Path) -> None:
64
+ result = runner.invoke(main, ["scan", str(sample_file)])
65
+ # Should find issues with "ERROR: Something went wrong"
66
+ assert "WARN" in result.output or "ERROR" in result.output
67
+
68
+ def test_scan_clean_file(self, runner: CliRunner, clean_file: Path) -> None:
69
+ result = runner.invoke(main, ["scan", str(clean_file)])
70
+ assert result.exit_code == 0
71
+
72
+ def test_scan_stdin(self, runner: CliRunner) -> None:
73
+ result = runner.invoke(main, ["scan", "--stdin"], input="Clean text.\n")
74
+ assert result.exit_code == 0
75
+
76
+ def test_scan_json_output(self, runner: CliRunner, sample_file: Path) -> None:
77
+ result = runner.invoke(main, ["scan", "--json", str(sample_file)])
78
+ data = json.loads(result.output)
79
+ assert "messages" in data
80
+ assert "summary" in data
81
+
82
+ def test_scan_markdown_output(self, runner: CliRunner, sample_file: Path) -> None:
83
+ result = runner.invoke(main, ["scan", "--format=markdown", str(sample_file)])
84
+ assert "# Accessibility Report" in result.output
85
+
86
+ def test_scan_no_input(self, runner: CliRunner) -> None:
87
+ result = runner.invoke(main, ["scan"])
88
+ assert result.exit_code == 1
89
+ assert "Must specify" in result.output
90
+
91
+ def test_scan_disable_rule(self, runner: CliRunner, tmp_path: Path) -> None:
92
+ file = tmp_path / "test.txt"
93
+ file.write_text("ERROR: It failed")
94
+ result = runner.invoke(
95
+ main,
96
+ ["scan", "--disable=no-ambiguous-pronouns", str(file)],
97
+ )
98
+ # Should not find ambiguous pronoun warning
99
+ assert "LNG004" not in result.output
100
+
101
+ def test_scan_strict_mode(self, runner: CliRunner, sample_file: Path) -> None:
102
+ # Strict mode treats warnings as errors
103
+ result = runner.invoke(main, ["scan", "--strict", str(sample_file)])
104
+ # If there are warnings, exit code should be 1
105
+ if "WARN" in result.output:
106
+ assert result.exit_code == 1
107
+
108
+
109
+ class TestValidateCommand:
110
+ """Tests for validate command."""
111
+
112
+ def test_validate_valid_file(self, runner: CliRunner, json_file: Path) -> None:
113
+ result = runner.invoke(main, ["validate", str(json_file)])
114
+ assert result.exit_code == 0
115
+ assert "[OK]" in result.output
116
+
117
+ def test_validate_invalid_file(self, runner: CliRunner, tmp_path: Path) -> None:
118
+ file = tmp_path / "invalid.json"
119
+ file.write_text(json.dumps({"level": "INVALID", "code": "bad", "what": "x"}))
120
+ result = runner.invoke(main, ["validate", str(file)])
121
+ assert result.exit_code == 1
122
+ assert "[ERROR]" in result.output
123
+
124
+ def test_validate_verbose(self, runner: CliRunner, tmp_path: Path) -> None:
125
+ file = tmp_path / "invalid.json"
126
+ file.write_text(json.dumps({"level": "INVALID", "code": "bad", "what": "x"}))
127
+ result = runner.invoke(main, ["validate", "-v", str(file)])
128
+ assert result.exit_code == 1
129
+ # Verbose should show detailed errors
130
+ assert "level" in result.output.lower() or "code" in result.output.lower()
131
+
132
+
133
+ class TestScorecardCommand:
134
+ """Tests for scorecard command."""
135
+
136
+ def test_scorecard_file(self, runner: CliRunner, sample_file: Path) -> None:
137
+ result = runner.invoke(main, ["scorecard", str(sample_file)])
138
+ assert "Scorecard" in result.output
139
+ assert "Score:" in result.output
140
+
141
+ def test_scorecard_json(self, runner: CliRunner, sample_file: Path) -> None:
142
+ result = runner.invoke(main, ["scorecard", "--json", str(sample_file)])
143
+ data = json.loads(result.output)
144
+ assert "overall_score" in data
145
+ assert "rules" in data
146
+
147
+ def test_scorecard_badge(self, runner: CliRunner, clean_file: Path) -> None:
148
+ result = runner.invoke(main, ["scorecard", "--badge", str(clean_file)])
149
+ assert "shields.io" in result.output
150
+
151
+ def test_scorecard_custom_name(self, runner: CliRunner, clean_file: Path) -> None:
152
+ result = runner.invoke(
153
+ main, ["scorecard", "--name=Custom Name", str(clean_file)]
154
+ )
155
+ assert "Custom Name" in result.output
156
+
157
+
158
+ class TestReportCommand:
159
+ """Tests for report command."""
160
+
161
+ def test_report_stdout(self, runner: CliRunner, sample_file: Path) -> None:
162
+ result = runner.invoke(main, ["report", str(sample_file)])
163
+ assert "# Accessibility Report" in result.output
164
+
165
+ def test_report_to_file(
166
+ self, runner: CliRunner, sample_file: Path, tmp_path: Path
167
+ ) -> None:
168
+ output = tmp_path / "report.md"
169
+ result = runner.invoke(main, ["report", str(sample_file), "-o", str(output)])
170
+ assert result.exit_code == 0 or result.exit_code == 1 # Depends on errors
171
+ assert output.exists()
172
+ assert "# Accessibility Report" in output.read_text(encoding="utf-8")
173
+
174
+ def test_report_custom_title(self, runner: CliRunner, clean_file: Path) -> None:
175
+ result = runner.invoke(
176
+ main, ["report", "--title=Custom Title", str(clean_file)]
177
+ )
178
+ assert "# Custom Title" in result.output
179
+
180
+
181
+ class TestListRulesCommand:
182
+ """Tests for list-rules command."""
183
+
184
+ def test_list_rules(self, runner: CliRunner) -> None:
185
+ result = runner.invoke(main, ["list-rules"])
186
+ assert result.exit_code == 0
187
+ assert "line-length" in result.output
188
+ assert "no-all-caps" in result.output
189
+ assert "plain-language" in result.output
190
+
191
+
192
+ class TestSchemaCommand:
193
+ """Tests for schema command."""
194
+
195
+ def test_schema(self, runner: CliRunner) -> None:
196
+ result = runner.invoke(main, ["schema"])
197
+ assert result.exit_code == 0
198
+ data = json.loads(result.output)
199
+ assert data["title"] == "CLI Ground Truth Message"
200
+ assert "properties" in data
@@ -0,0 +1,188 @@
1
+ """Tests for errors module."""
2
+
3
+ import pytest
4
+
5
+ from a11y_lint.errors import A11yMessage, Level, Location, ErrorCodes, CODE_PATTERN
6
+
7
+
8
+ class TestLevel:
9
+ """Tests for Level enum."""
10
+
11
+ def test_level_values(self) -> None:
12
+ assert Level.OK.value == "OK"
13
+ assert Level.WARN.value == "WARN"
14
+ assert Level.ERROR.value == "ERROR"
15
+
16
+ def test_level_str(self) -> None:
17
+ assert str(Level.OK) == "OK"
18
+ assert str(Level.WARN) == "WARN"
19
+ assert str(Level.ERROR) == "ERROR"
20
+
21
+
22
+ class TestLocation:
23
+ """Tests for Location dataclass."""
24
+
25
+ def test_empty_location(self) -> None:
26
+ loc = Location()
27
+ assert str(loc) == "<unknown>"
28
+ assert loc.to_dict() == {}
29
+
30
+ def test_full_location(self) -> None:
31
+ loc = Location(file="test.py", line=10, column=5, context="some text")
32
+ assert "test.py" in str(loc)
33
+ assert "line 10" in str(loc)
34
+ assert "col 5" in str(loc)
35
+
36
+ d = loc.to_dict()
37
+ assert d["file"] == "test.py"
38
+ assert d["line"] == 10
39
+ assert d["column"] == 5
40
+ assert d["context"] == "some text"
41
+
42
+ def test_location_truncates_context(self) -> None:
43
+ long_context = "x" * 300
44
+ loc = Location(context=long_context)
45
+ d = loc.to_dict()
46
+ assert len(d["context"]) == 200
47
+
48
+
49
+ class TestCodePattern:
50
+ """Tests for error code pattern validation."""
51
+
52
+ def test_valid_codes(self) -> None:
53
+ assert CODE_PATTERN.match("A11Y001")
54
+ assert CODE_PATTERN.match("FMT123")
55
+ assert CODE_PATTERN.match("CLR999")
56
+ assert CODE_PATTERN.match("ABCD000")
57
+
58
+ def test_invalid_codes(self) -> None:
59
+ assert not CODE_PATTERN.match("A1") # Too short
60
+ assert not CODE_PATTERN.match("a11y001") # Lowercase
61
+ assert not CODE_PATTERN.match("A11Y01") # Only 2 digits
62
+ assert not CODE_PATTERN.match("A11Y0001") # 4 digits
63
+ assert not CODE_PATTERN.match("ABCDE001") # 5 letters
64
+
65
+
66
+ class TestA11yMessage:
67
+ """Tests for A11yMessage dataclass."""
68
+
69
+ def test_ok_message(self) -> None:
70
+ msg = A11yMessage.ok("TST001", "Test passed")
71
+ assert msg.level == Level.OK
72
+ assert msg.code == "TST001"
73
+ assert msg.what == "Test passed"
74
+ assert msg.why is None
75
+ assert msg.fix is None
76
+
77
+ def test_warn_message(self) -> None:
78
+ msg = A11yMessage.warn(
79
+ "TST002",
80
+ "Test warning",
81
+ "This is a warning reason",
82
+ fix="Consider fixing this",
83
+ )
84
+ assert msg.level == Level.WARN
85
+ assert msg.code == "TST002"
86
+ assert msg.what == "Test warning"
87
+ assert msg.why == "This is a warning reason"
88
+ assert msg.fix == "Consider fixing this"
89
+
90
+ def test_error_message(self) -> None:
91
+ msg = A11yMessage.error(
92
+ "TST003",
93
+ "Test error",
94
+ "This is an error reason",
95
+ "Fix it like this",
96
+ )
97
+ assert msg.level == Level.ERROR
98
+ assert msg.code == "TST003"
99
+ assert msg.what == "Test error"
100
+ assert msg.why == "This is an error reason"
101
+ assert msg.fix == "Fix it like this"
102
+
103
+ def test_error_requires_why(self) -> None:
104
+ with pytest.raises(ValueError, match="must include 'why'"):
105
+ A11yMessage(
106
+ level=Level.ERROR,
107
+ code="TST001",
108
+ what="Test",
109
+ fix="Some fix",
110
+ )
111
+
112
+ def test_error_requires_fix(self) -> None:
113
+ with pytest.raises(ValueError, match="must include 'fix'"):
114
+ A11yMessage(
115
+ level=Level.ERROR,
116
+ code="TST001",
117
+ what="Test",
118
+ why="Some reason",
119
+ )
120
+
121
+ def test_invalid_code_raises(self) -> None:
122
+ with pytest.raises(ValueError, match="Invalid error code"):
123
+ A11yMessage.ok("invalid", "Test")
124
+
125
+ def test_empty_what_raises(self) -> None:
126
+ with pytest.raises(ValueError, match="cannot be empty"):
127
+ A11yMessage.ok("TST001", "")
128
+
129
+ def test_whitespace_what_raises(self) -> None:
130
+ with pytest.raises(ValueError, match="cannot be empty"):
131
+ A11yMessage.ok("TST001", " ")
132
+
133
+ def test_to_dict(self) -> None:
134
+ msg = A11yMessage.error(
135
+ "TST001",
136
+ "Test error",
137
+ "Why",
138
+ "Fix",
139
+ rule="test-rule",
140
+ location=Location(file="test.py", line=1),
141
+ metadata={"key": "value"},
142
+ )
143
+ d = msg.to_dict()
144
+ assert d["level"] == "ERROR"
145
+ assert d["code"] == "TST001"
146
+ assert d["what"] == "Test error"
147
+ assert d["why"] == "Why"
148
+ assert d["fix"] == "Fix"
149
+ assert d["rule"] == "test-rule"
150
+ assert d["location"]["file"] == "test.py"
151
+ assert d["metadata"]["key"] == "value"
152
+
153
+ def test_from_dict(self) -> None:
154
+ data = {
155
+ "level": "WARN",
156
+ "code": "TST002",
157
+ "what": "Test warning",
158
+ "why": "Reason",
159
+ "location": {"file": "test.py", "line": 5},
160
+ }
161
+ msg = A11yMessage.from_dict(data)
162
+ assert msg.level == Level.WARN
163
+ assert msg.code == "TST002"
164
+ assert msg.what == "Test warning"
165
+ assert msg.why == "Reason"
166
+ assert msg.location is not None
167
+ assert msg.location.file == "test.py"
168
+ assert msg.location.line == 5
169
+
170
+ def test_truncates_long_what(self) -> None:
171
+ long_what = "x" * 300
172
+ msg = A11yMessage.ok("TST001", long_what)
173
+ assert len(msg.what) == 200
174
+
175
+ def test_truncates_long_why(self) -> None:
176
+ long_why = "y" * 600
177
+ msg = A11yMessage.warn("TST001", "Test", long_why)
178
+ assert len(msg.why) == 500
179
+
180
+
181
+ class TestErrorCodes:
182
+ """Tests for ErrorCodes constants."""
183
+
184
+ def test_all_codes_valid(self) -> None:
185
+ for attr in dir(ErrorCodes):
186
+ if not attr.startswith("_"):
187
+ code = getattr(ErrorCodes, attr)
188
+ assert CODE_PATTERN.match(code), f"{attr} = {code} is invalid"
@@ -0,0 +1,202 @@
1
+ """Tests for render module."""
2
+
3
+ import io
4
+ import pytest
5
+
6
+ from a11y_lint.render import (
7
+ render,
8
+ render_plain,
9
+ render_colored,
10
+ render_batch,
11
+ Renderer,
12
+ format_for_file,
13
+ Colors,
14
+ get_level_color,
15
+ )
16
+ from a11y_lint.errors import A11yMessage, Level, Location
17
+
18
+
19
+ class TestRenderPlain:
20
+ """Tests for render_plain function."""
21
+
22
+ def test_ok_message(self) -> None:
23
+ msg = A11yMessage.ok("TST001", "Test passed")
24
+ output = render_plain(msg)
25
+ assert "[OK] TST001: Test passed" in output
26
+
27
+ def test_warn_message(self) -> None:
28
+ msg = A11yMessage.warn("TST002", "Test warning", "This is why")
29
+ output = render_plain(msg)
30
+ assert "[WARN] TST002: Test warning" in output
31
+ assert "Why: This is why" in output
32
+
33
+ def test_error_message(self) -> None:
34
+ msg = A11yMessage.error("TST003", "Test error", "Reason", "Fix this")
35
+ output = render_plain(msg)
36
+ assert "[ERROR] TST003: Test error" in output
37
+ assert "Why: Reason" in output
38
+ assert "Fix: Fix this" in output
39
+
40
+ def test_message_with_location(self) -> None:
41
+ msg = A11yMessage.ok(
42
+ "TST001",
43
+ "Test",
44
+ location=Location(file="test.py", line=10, column=5),
45
+ )
46
+ output = render_plain(msg)
47
+ assert "at test.py" in output
48
+ assert "line 10" in output
49
+ assert "col 5" in output
50
+
51
+ def test_indentation(self) -> None:
52
+ msg = A11yMessage.ok("TST001", "Test")
53
+ output = render_plain(msg, indent=4)
54
+ assert output.startswith(" [OK]")
55
+
56
+
57
+ class TestRenderColored:
58
+ """Tests for render_colored function."""
59
+
60
+ def test_contains_color_codes(self) -> None:
61
+ msg = A11yMessage.ok("TST001", "Test")
62
+ output = render_colored(msg)
63
+ assert Colors.OK in output
64
+ assert Colors.RESET in output
65
+
66
+ def test_error_uses_red(self) -> None:
67
+ msg = A11yMessage.error("TST001", "Test", "Why", "Fix")
68
+ output = render_colored(msg)
69
+ assert Colors.ERROR in output
70
+
71
+ def test_warn_uses_yellow(self) -> None:
72
+ msg = A11yMessage.warn("TST001", "Test", "Why")
73
+ output = render_colored(msg)
74
+ assert Colors.WARN in output
75
+
76
+
77
+ class TestRender:
78
+ """Tests for render function."""
79
+
80
+ def test_default_no_color(self) -> None:
81
+ msg = A11yMessage.ok("TST001", "Test")
82
+ output = render(msg)
83
+ assert Colors.OK not in output
84
+
85
+ def test_color_enabled(self) -> None:
86
+ msg = A11yMessage.ok("TST001", "Test")
87
+ output = render(msg, color=True)
88
+ assert Colors.OK in output
89
+
90
+ def test_color_disabled(self) -> None:
91
+ msg = A11yMessage.ok("TST001", "Test")
92
+ output = render(msg, color=False)
93
+ assert Colors.OK not in output
94
+
95
+
96
+ class TestRenderBatch:
97
+ """Tests for render_batch function."""
98
+
99
+ def test_multiple_messages(self) -> None:
100
+ messages = [
101
+ A11yMessage.ok("TST001", "Test 1"),
102
+ A11yMessage.ok("TST002", "Test 2"),
103
+ ]
104
+ output = render_batch(messages)
105
+ assert "TST001" in output
106
+ assert "TST002" in output
107
+
108
+ def test_custom_separator(self) -> None:
109
+ messages = [
110
+ A11yMessage.ok("TST001", "Test 1"),
111
+ A11yMessage.ok("TST002", "Test 2"),
112
+ ]
113
+ output = render_batch(messages, separator="\n\n")
114
+ assert "\n\n" in output
115
+
116
+
117
+ class TestGetLevelColor:
118
+ """Tests for get_level_color function."""
119
+
120
+ def test_ok_green(self) -> None:
121
+ assert get_level_color(Level.OK) == Colors.OK
122
+
123
+ def test_warn_yellow(self) -> None:
124
+ assert get_level_color(Level.WARN) == Colors.WARN
125
+
126
+ def test_error_red(self) -> None:
127
+ assert get_level_color(Level.ERROR) == Colors.ERROR
128
+
129
+
130
+ class TestRenderer:
131
+ """Tests for Renderer class."""
132
+
133
+ def test_render_to_stream(self) -> None:
134
+ stream = io.StringIO()
135
+ renderer = Renderer(color=False, stream=stream)
136
+ msg = A11yMessage.ok("TST001", "Test")
137
+ renderer.write(msg)
138
+ output = stream.getvalue()
139
+ assert "[OK] TST001: Test" in output
140
+
141
+ def test_counts_messages(self) -> None:
142
+ stream = io.StringIO()
143
+ renderer = Renderer(color=False, stream=stream)
144
+
145
+ renderer.write(A11yMessage.ok("TST001", "Test 1"))
146
+ renderer.write(A11yMessage.warn("TST002", "Test 2", "Why"))
147
+ renderer.write(A11yMessage.error("TST003", "Test 3", "Why", "Fix"))
148
+
149
+ assert renderer.ok_count == 1
150
+ assert renderer.warn_count == 1
151
+ assert renderer.error_count == 1
152
+ assert renderer.total_count == 3
153
+
154
+ def test_summary_line(self) -> None:
155
+ renderer = Renderer(color=False, stream=io.StringIO())
156
+ assert renderer.summary_line() == "No issues found"
157
+
158
+ renderer.write(A11yMessage.ok("TST001", "Test"))
159
+ assert "1 passed" in renderer.summary_line()
160
+
161
+ renderer.write(A11yMessage.warn("TST002", "Test", "Why"))
162
+ assert "1 warnings" in renderer.summary_line()
163
+
164
+ renderer.write(A11yMessage.error("TST003", "Test", "Why", "Fix"))
165
+ assert "1 errors" in renderer.summary_line()
166
+
167
+ def test_write_batch(self) -> None:
168
+ stream = io.StringIO()
169
+ renderer = Renderer(color=False, stream=stream)
170
+ messages = [
171
+ A11yMessage.ok("TST001", "Test 1"),
172
+ A11yMessage.ok("TST002", "Test 2"),
173
+ ]
174
+ renderer.write_batch(messages)
175
+ output = stream.getvalue()
176
+ assert "TST001" in output
177
+ assert "TST002" in output
178
+ assert renderer.total_count == 2
179
+
180
+ def test_auto_detect_color(self) -> None:
181
+ # StringIO doesn't have isatty, so should default to no color
182
+ stream = io.StringIO()
183
+ renderer = Renderer(stream=stream)
184
+ assert renderer.color is False
185
+
186
+
187
+ class TestFormatForFile:
188
+ """Tests for format_for_file function."""
189
+
190
+ def test_no_colors(self) -> None:
191
+ messages = [A11yMessage.ok("TST001", "Test")]
192
+ output = format_for_file(messages)
193
+ assert Colors.OK not in output
194
+
195
+ def test_blank_lines_between(self) -> None:
196
+ messages = [
197
+ A11yMessage.ok("TST001", "Test 1"),
198
+ A11yMessage.ok("TST002", "Test 2"),
199
+ ]
200
+ output = format_for_file(messages)
201
+ # Should have blank line between messages
202
+ assert "\n\n" in output