@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,485 @@
1
+ "use strict";
2
+
3
+ const { describe, it } = require("node:test");
4
+ const assert = require("node:assert");
5
+
6
+ const { diagnose, RULES } = require("../src/tools/diagnose.js");
7
+
8
+ // Create a test bundle with DOM snapshot
9
+ function createTestBundle(nodes) {
10
+ const domContent = JSON.stringify({ nodes, root: nodes[0] });
11
+
12
+ return {
13
+ bundle_id: "test-bundle",
14
+ artifacts: [
15
+ {
16
+ artifact_id: "artifact:dom:test",
17
+ media_type: "application/json",
18
+ labels: ["dom-snapshot"],
19
+ _content: domContent,
20
+ },
21
+ ],
22
+ provenance: {
23
+ record_id: "prov:test",
24
+ methods: ["engine.capture.html_v0_1"],
25
+ inputs: ["test.html"],
26
+ outputs: ["artifact:dom:test"],
27
+ },
28
+ };
29
+ }
30
+
31
+ describe("a11y.diagnose", () => {
32
+ describe("lang rule", () => {
33
+ it("should detect missing lang attribute", async () => {
34
+ const bundle = createTestBundle([
35
+ {
36
+ type: "element",
37
+ tagName: "html",
38
+ attrs: {},
39
+ index: 0,
40
+ children: [],
41
+ },
42
+ ]);
43
+
44
+ const result = await diagnose({
45
+ bundle,
46
+ rules: { include: ["lang"] },
47
+ output: { include_fix_guidance: true },
48
+ });
49
+
50
+ assert.ok(result.findings.length > 0);
51
+ assert.strictEqual(result.findings[0].id, "a11y.lang.missing");
52
+ assert.ok(result.findings[0].fix);
53
+ });
54
+
55
+ it("should detect empty lang attribute", async () => {
56
+ const bundle = createTestBundle([
57
+ {
58
+ type: "element",
59
+ tagName: "html",
60
+ attrs: { lang: "" },
61
+ index: 0,
62
+ children: [],
63
+ },
64
+ ]);
65
+
66
+ const result = await diagnose({
67
+ bundle,
68
+ rules: { include: ["lang"] },
69
+ });
70
+
71
+ assert.ok(result.findings.length > 0);
72
+ });
73
+
74
+ it("should pass when lang is present", async () => {
75
+ const bundle = createTestBundle([
76
+ {
77
+ type: "element",
78
+ tagName: "html",
79
+ attrs: { lang: "en" },
80
+ index: 0,
81
+ children: [],
82
+ },
83
+ ]);
84
+
85
+ const result = await diagnose({
86
+ bundle,
87
+ rules: { include: ["lang"] },
88
+ });
89
+
90
+ assert.strictEqual(result.findings.length, 0);
91
+ });
92
+ });
93
+
94
+ describe("alt rule", () => {
95
+ it("should detect missing alt on img", async () => {
96
+ const bundle = createTestBundle([
97
+ {
98
+ type: "element",
99
+ tagName: "img",
100
+ attrs: { src: "test.png" },
101
+ index: 0,
102
+ children: [],
103
+ },
104
+ ]);
105
+
106
+ const result = await diagnose({
107
+ bundle,
108
+ rules: { include: ["alt"] },
109
+ output: { include_fix_guidance: true },
110
+ });
111
+
112
+ assert.ok(result.findings.length > 0);
113
+ assert.strictEqual(result.findings[0].id, "a11y.img.missing_alt");
114
+ });
115
+
116
+ it("should skip decorative images (role=presentation)", async () => {
117
+ const bundle = createTestBundle([
118
+ {
119
+ type: "element",
120
+ tagName: "img",
121
+ attrs: { src: "test.png", role: "presentation" },
122
+ index: 0,
123
+ children: [],
124
+ },
125
+ ]);
126
+
127
+ const result = await diagnose({
128
+ bundle,
129
+ rules: { include: ["alt"] },
130
+ });
131
+
132
+ assert.strictEqual(result.findings.length, 0);
133
+ });
134
+
135
+ it("should skip hidden images (aria-hidden=true)", async () => {
136
+ const bundle = createTestBundle([
137
+ {
138
+ type: "element",
139
+ tagName: "img",
140
+ attrs: { src: "test.png", "aria-hidden": "true" },
141
+ index: 0,
142
+ children: [],
143
+ },
144
+ ]);
145
+
146
+ const result = await diagnose({
147
+ bundle,
148
+ rules: { include: ["alt"] },
149
+ });
150
+
151
+ assert.strictEqual(result.findings.length, 0);
152
+ });
153
+ });
154
+
155
+ describe("button-name rule", () => {
156
+ it("should detect button without accessible name", async () => {
157
+ const bundle = createTestBundle([
158
+ {
159
+ type: "element",
160
+ tagName: "button",
161
+ attrs: {},
162
+ index: 0,
163
+ children: [],
164
+ },
165
+ ]);
166
+
167
+ const result = await diagnose({
168
+ bundle,
169
+ rules: { include: ["button-name"] },
170
+ });
171
+
172
+ assert.ok(result.findings.length > 0);
173
+ assert.strictEqual(result.findings[0].id, "a11y.button.missing_name");
174
+ });
175
+
176
+ it("should pass when button has text content", async () => {
177
+ const bundle = createTestBundle([
178
+ {
179
+ type: "element",
180
+ tagName: "button",
181
+ attrs: {},
182
+ index: 0,
183
+ children: [{ type: "text", content: "Click me", index: 1 }],
184
+ },
185
+ ]);
186
+
187
+ const result = await diagnose({
188
+ bundle,
189
+ rules: { include: ["button-name"] },
190
+ });
191
+
192
+ assert.strictEqual(result.findings.length, 0);
193
+ });
194
+
195
+ it("should pass when button has aria-label", async () => {
196
+ const bundle = createTestBundle([
197
+ {
198
+ type: "element",
199
+ tagName: "button",
200
+ attrs: { "aria-label": "Close dialog" },
201
+ index: 0,
202
+ children: [],
203
+ },
204
+ ]);
205
+
206
+ const result = await diagnose({
207
+ bundle,
208
+ rules: { include: ["button-name"] },
209
+ });
210
+
211
+ assert.strictEqual(result.findings.length, 0);
212
+ });
213
+ });
214
+
215
+ describe("link-name rule", () => {
216
+ it("should detect link without accessible name", async () => {
217
+ const bundle = createTestBundle([
218
+ {
219
+ type: "element",
220
+ tagName: "a",
221
+ attrs: { href: "/page" },
222
+ index: 0,
223
+ children: [],
224
+ },
225
+ ]);
226
+
227
+ const result = await diagnose({
228
+ bundle,
229
+ rules: { include: ["link-name"] },
230
+ });
231
+
232
+ assert.ok(result.findings.length > 0);
233
+ assert.strictEqual(result.findings[0].id, "a11y.link.missing_name");
234
+ });
235
+
236
+ it("should skip anchors without href", async () => {
237
+ const bundle = createTestBundle([
238
+ {
239
+ type: "element",
240
+ tagName: "a",
241
+ attrs: { name: "anchor" },
242
+ index: 0,
243
+ children: [],
244
+ },
245
+ ]);
246
+
247
+ const result = await diagnose({
248
+ bundle,
249
+ rules: { include: ["link-name"] },
250
+ });
251
+
252
+ assert.strictEqual(result.findings.length, 0);
253
+ });
254
+ });
255
+
256
+ describe("label rule", () => {
257
+ it("should detect input without label", async () => {
258
+ const bundle = createTestBundle([
259
+ {
260
+ type: "element",
261
+ tagName: "input",
262
+ attrs: { type: "text" },
263
+ index: 0,
264
+ children: [],
265
+ },
266
+ ]);
267
+
268
+ const result = await diagnose({
269
+ bundle,
270
+ rules: { include: ["label"] },
271
+ });
272
+
273
+ assert.ok(result.findings.length > 0);
274
+ assert.strictEqual(result.findings[0].id, "a11y.input.missing_label");
275
+ });
276
+
277
+ it("should skip hidden inputs", async () => {
278
+ const bundle = createTestBundle([
279
+ {
280
+ type: "element",
281
+ tagName: "input",
282
+ attrs: { type: "hidden" },
283
+ index: 0,
284
+ children: [],
285
+ },
286
+ ]);
287
+
288
+ const result = await diagnose({
289
+ bundle,
290
+ rules: { include: ["label"] },
291
+ });
292
+
293
+ assert.strictEqual(result.findings.length, 0);
294
+ });
295
+
296
+ it("should pass when input has aria-label", async () => {
297
+ const bundle = createTestBundle([
298
+ {
299
+ type: "element",
300
+ tagName: "input",
301
+ attrs: { type: "text", "aria-label": "Search" },
302
+ index: 0,
303
+ children: [],
304
+ },
305
+ ]);
306
+
307
+ const result = await diagnose({
308
+ bundle,
309
+ rules: { include: ["label"] },
310
+ });
311
+
312
+ assert.strictEqual(result.findings.length, 0);
313
+ });
314
+
315
+ it("should pass when input has associated label", async () => {
316
+ const bundle = createTestBundle([
317
+ {
318
+ type: "element",
319
+ tagName: "label",
320
+ attrs: { for: "email" },
321
+ index: 0,
322
+ children: [],
323
+ },
324
+ {
325
+ type: "element",
326
+ tagName: "input",
327
+ attrs: { type: "email", id: "email" },
328
+ index: 1,
329
+ children: [],
330
+ },
331
+ ]);
332
+
333
+ const result = await diagnose({
334
+ bundle,
335
+ rules: { include: ["label"] },
336
+ });
337
+
338
+ assert.strictEqual(result.findings.length, 0);
339
+ });
340
+ });
341
+
342
+ describe("output structure", () => {
343
+ it("should include summary with severity counts", async () => {
344
+ const bundle = createTestBundle([
345
+ {
346
+ type: "element",
347
+ tagName: "html",
348
+ attrs: {},
349
+ index: 0,
350
+ children: [],
351
+ },
352
+ {
353
+ type: "element",
354
+ tagName: "img",
355
+ attrs: { src: "test.png" },
356
+ index: 1,
357
+ children: [],
358
+ },
359
+ ]);
360
+
361
+ const result = await diagnose({ bundle });
362
+
363
+ assert.ok(result.summary);
364
+ assert.ok(result.summary.severity_counts);
365
+ assert.strictEqual(result.summary.findings_total, result.findings.length);
366
+ });
367
+
368
+ it("should include provenance record", async () => {
369
+ const bundle = createTestBundle([
370
+ {
371
+ type: "element",
372
+ tagName: "html",
373
+ attrs: {},
374
+ index: 0,
375
+ children: [],
376
+ },
377
+ ]);
378
+
379
+ const result = await diagnose({ bundle });
380
+
381
+ assert.ok(result.provenance);
382
+ assert.ok(result.provenance.methods);
383
+ assert.ok(result.provenance.inputs);
384
+ });
385
+
386
+ it("should include evidence anchors in findings", async () => {
387
+ const bundle = createTestBundle([
388
+ {
389
+ type: "element",
390
+ tagName: "html",
391
+ attrs: {},
392
+ index: 0,
393
+ children: [],
394
+ },
395
+ ]);
396
+
397
+ const result = await diagnose({
398
+ bundle,
399
+ rules: { include: ["lang"] },
400
+ });
401
+
402
+ assert.ok(result.findings[0].targets);
403
+ assert.ok(result.findings[0].targets[0].artifact_id);
404
+ assert.ok(result.findings[0].targets[0].json_pointer);
405
+ });
406
+
407
+ it("should include fix guidance when requested", async () => {
408
+ const bundle = createTestBundle([
409
+ {
410
+ type: "element",
411
+ tagName: "html",
412
+ attrs: {},
413
+ index: 0,
414
+ children: [],
415
+ },
416
+ ]);
417
+
418
+ const result = await diagnose({
419
+ bundle,
420
+ rules: { include: ["lang"] },
421
+ output: { include_fix_guidance: true },
422
+ });
423
+
424
+ assert.ok(result.findings[0].fix);
425
+ assert.strictEqual(result.findings[0].fix.safe, true);
426
+ assert.ok(result.findings[0].fix.patch);
427
+ });
428
+ });
429
+
430
+ describe("rule filtering", () => {
431
+ it("should only run included rules", async () => {
432
+ const bundle = createTestBundle([
433
+ {
434
+ type: "element",
435
+ tagName: "html",
436
+ attrs: {},
437
+ index: 0,
438
+ children: [],
439
+ },
440
+ {
441
+ type: "element",
442
+ tagName: "img",
443
+ attrs: { src: "test.png" },
444
+ index: 1,
445
+ children: [],
446
+ },
447
+ ]);
448
+
449
+ const result = await diagnose({
450
+ bundle,
451
+ rules: { include: ["lang"] },
452
+ });
453
+
454
+ // Should only find lang issue, not alt issue
455
+ assert.ok(result.findings.every((f) => f.id === "a11y.lang.missing"));
456
+ });
457
+
458
+ it("should exclude specified rules", async () => {
459
+ const bundle = createTestBundle([
460
+ {
461
+ type: "element",
462
+ tagName: "html",
463
+ attrs: {},
464
+ index: 0,
465
+ children: [],
466
+ },
467
+ {
468
+ type: "element",
469
+ tagName: "img",
470
+ attrs: { src: "test.png" },
471
+ index: 1,
472
+ children: [],
473
+ },
474
+ ]);
475
+
476
+ const result = await diagnose({
477
+ bundle,
478
+ rules: { exclude: ["lang"] },
479
+ });
480
+
481
+ // Should not find lang issue
482
+ assert.ok(result.findings.every((f) => f.id !== "a11y.lang.missing"));
483
+ });
484
+ });
485
+ });
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+
3
+ const { describe, it, before, after } = require("node:test");
4
+ const assert = require("node:assert");
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const {
9
+ captureEvidence,
10
+ canonicalizeHtml,
11
+ createDomSnapshot,
12
+ } = require("../src/tools/evidence.js");
13
+
14
+ const FIXTURES_DIR = path.join(__dirname, "fixtures");
15
+
16
+ describe("a11y.evidence", () => {
17
+ before(() => {
18
+ // Create test fixtures
19
+ fs.mkdirSync(FIXTURES_DIR, { recursive: true });
20
+
21
+ fs.writeFileSync(
22
+ path.join(FIXTURES_DIR, "test.html"),
23
+ `<!DOCTYPE html>
24
+ <html>
25
+ <head><title>Test</title></head>
26
+ <body>
27
+ <img src="test.png">
28
+ <button></button>
29
+ </body>
30
+ </html>`
31
+ );
32
+
33
+ fs.writeFileSync(
34
+ path.join(FIXTURES_DIR, "test.log"),
35
+ "Some CLI output\nAnother line"
36
+ );
37
+ });
38
+
39
+ after(() => {
40
+ // Clean up
41
+ fs.rmSync(FIXTURES_DIR, { recursive: true, force: true });
42
+ });
43
+
44
+ describe("captureEvidence", () => {
45
+ it("should capture HTML file and create artifact", async () => {
46
+ const result = await captureEvidence({
47
+ targets: [{ kind: "file", path: path.join(FIXTURES_DIR, "test.html") }],
48
+ capture: { html: { canonicalize: false } },
49
+ });
50
+
51
+ assert.ok(result.bundle_id);
52
+ assert.ok(result.artifacts.length >= 1);
53
+
54
+ const htmlArtifact = result.artifacts.find((a) =>
55
+ a.artifact_id.includes("html")
56
+ );
57
+ assert.ok(htmlArtifact);
58
+ assert.strictEqual(htmlArtifact.media_type, "text/html");
59
+ assert.ok(htmlArtifact.digest.hex);
60
+ assert.strictEqual(htmlArtifact.digest.alg, "sha256");
61
+ });
62
+
63
+ it("should create DOM snapshot when requested", async () => {
64
+ const result = await captureEvidence({
65
+ targets: [{ kind: "file", path: path.join(FIXTURES_DIR, "test.html") }],
66
+ capture: {
67
+ html: { canonicalize: false },
68
+ dom: { snapshot: true },
69
+ },
70
+ });
71
+
72
+ const domArtifact = result.artifacts.find((a) =>
73
+ a.artifact_id.includes("dom")
74
+ );
75
+ assert.ok(domArtifact);
76
+ assert.strictEqual(domArtifact.media_type, "application/json");
77
+ assert.ok(domArtifact.labels.includes("dom-snapshot"));
78
+ });
79
+
80
+ it("should capture CLI log", async () => {
81
+ const result = await captureEvidence({
82
+ targets: [{ kind: "cli_log", path: path.join(FIXTURES_DIR, "test.log") }],
83
+ });
84
+
85
+ const logArtifact = result.artifacts.find((a) =>
86
+ a.artifact_id.includes("log")
87
+ );
88
+ assert.ok(logArtifact);
89
+ assert.ok(logArtifact.labels.includes("cli-log"));
90
+ });
91
+
92
+ it("should add labels to artifacts", async () => {
93
+ const result = await captureEvidence({
94
+ targets: [{ kind: "file", path: path.join(FIXTURES_DIR, "test.html") }],
95
+ labels: ["baseline", "wcag-2.2-aa"],
96
+ });
97
+
98
+ const artifact = result.artifacts[0];
99
+ assert.ok(artifact.labels.includes("baseline"));
100
+ assert.ok(artifact.labels.includes("wcag-2.2-aa"));
101
+ });
102
+
103
+ it("should include provenance record", async () => {
104
+ const result = await captureEvidence({
105
+ targets: [{ kind: "file", path: path.join(FIXTURES_DIR, "test.html") }],
106
+ });
107
+
108
+ assert.ok(result.provenance);
109
+ assert.ok(result.provenance.record_id);
110
+ assert.ok(result.provenance.methods.length > 0);
111
+ assert.ok(result.provenance.inputs.length > 0);
112
+ assert.ok(result.provenance.outputs.length > 0);
113
+ });
114
+
115
+ it("should capture environment info when requested", async () => {
116
+ const result = await captureEvidence({
117
+ targets: [{ kind: "file", path: path.join(FIXTURES_DIR, "test.html") }],
118
+ capture: {
119
+ environment: { include: ["os", "node"] },
120
+ },
121
+ });
122
+
123
+ assert.ok(result.environment);
124
+ assert.ok(result.environment.os);
125
+ assert.ok(result.environment.node);
126
+ });
127
+ });
128
+
129
+ describe("canonicalizeHtml", () => {
130
+ it("should sort attributes alphabetically", () => {
131
+ const html = '<div class="foo" id="bar">text</div>';
132
+ const result = canonicalizeHtml(html);
133
+ // Should have class before id alphabetically? Actually id comes before class
134
+ assert.ok(result.includes("class="));
135
+ assert.ok(result.includes("id="));
136
+ });
137
+
138
+ it("should normalize whitespace", () => {
139
+ const html = "<p> Multiple spaces </p>";
140
+ const result = canonicalizeHtml(html);
141
+ assert.ok(!result.includes(" ")); // No double spaces
142
+ });
143
+
144
+ it("should lowercase tags", () => {
145
+ const html = "<DIV>content</DIV>";
146
+ const result = canonicalizeHtml(html);
147
+ assert.ok(result.includes("<div>"));
148
+ assert.ok(result.includes("</div>"));
149
+ });
150
+ });
151
+
152
+ describe("createDomSnapshot", () => {
153
+ it("should create flat nodes array", () => {
154
+ const html = "<html><body><p>Text</p></body></html>";
155
+ const result = createDomSnapshot(html);
156
+
157
+ assert.ok(result.nodes);
158
+ assert.ok(Array.isArray(result.nodes));
159
+ assert.ok(result.nodes.length > 0);
160
+ });
161
+
162
+ it("should assign indices to nodes", () => {
163
+ const html = "<html><body><p>Text</p></body></html>";
164
+ const result = createDomSnapshot(html);
165
+
166
+ for (let i = 0; i < result.nodes.length; i++) {
167
+ assert.strictEqual(result.nodes[i].index, i);
168
+ }
169
+ });
170
+
171
+ it("should include CSS selectors when requested", () => {
172
+ const html = '<div id="main" class="container">text</div>';
173
+ const result = createDomSnapshot(html, { include_css_selectors: true });
174
+
175
+ const divNode = result.nodes.find(
176
+ (n) => n.type === "element" && n.tagName === "div"
177
+ );
178
+ assert.ok(divNode);
179
+ assert.ok(divNode.selector);
180
+ assert.ok(divNode.selector.includes("#main"));
181
+ });
182
+ });
183
+ });