testaro 71.1.0 → 72.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.
@@ -6,5 +6,6 @@
6
6
  "Bash(git commit -m ' *)",
7
7
  "Bash(git push *)"
8
8
  ]
9
- }
9
+ },
10
+ "outputStyle": "Explanatory"
10
11
  }
package/CLAUDE.md CHANGED
@@ -59,6 +59,8 @@ Two implementation patterns exist:
59
59
 
60
60
  ### Validation
61
61
 
62
+ Validation is currently broken. A reconsideration of its architecture is anticipated, but in the meantime the next paragraph describes its current inoperative design.
63
+
62
64
  Validation tests live in `validation/tests/jobs/` (jobs with `expect` arrays) and `validation/tests/targets/` (static HTML pages served locally as test targets). Running `npm test <ruleID>` executes `validation/executors/test.js` → `validation/validateTest.js`, which runs the job and compares `result` fields against `expect` clauses.
63
65
 
64
66
  ### Tool XPath strategy
@@ -87,6 +89,8 @@ Key variables:
87
89
 
88
90
  ESLint (`eslintrc.json`): 2-space indent, single quotes, semicolons, Stroustrup brace style (`else`/`catch` on a new line after `}`), `no-use-before-define`. The `htmlcs/HTMLCS.js` file uses a separate, looser ESLint config and must not be reformatted.
89
91
 
92
+ Long comments are not broken into multiple lines per paragraph.
93
+
90
94
  ## Adding a new Testaro rule
91
95
 
92
96
  1. Add an entry to `allRules` in `tests/testaro.js`.
@@ -97,7 +101,7 @@ ESLint (`eslintrc.json`): 2-space indent, single quotes, semicolons, Stroustrup
97
101
  ## Key files
98
102
 
99
103
  | File | Purpose |
100
- |------|---------|
104
+ | ---- | ------- |
101
105
  | `run.js` | `doJob()` — the main entry point |
102
106
  | `call.js` | CLI wrapper for `run`, `dirWatch`, `netWatch` |
103
107
  | `procs/doActs.js` | Iterates acts; forks child for each tool |
package/README.md CHANGED
@@ -502,7 +502,9 @@ The `testaro` tool (like the `ibm` tool) has a `withItems` property. If you set
502
502
 
503
503
  Unlike any other tool, the `testaro` tool requires a `stopOnFail` property, which specifies whether a failure to conform to any rule (i.e. any value of `totals` other than `[0, 0, 0, 0]`) should terminate the execution of tests for the remaining rules.
504
504
 
505
- Tests of the `testaro` tests (i.e. _validation_) can be performed as documented in the `VALIDATION.md` file.
505
+ Tests of the `testaro` tests (i.e. _validation_) could previously be performed as documented in the `VALIDATION.md` file. This functionality has broken and its redesign is planned.
506
+
507
+ One Testaro rule, `allCaps`, is currently being tested for in part with the assistance of the Claude Haiku artificial intelligence (AI) model. To obtain that assistance, you need an Anthropic API key, and its value must be assigned to the `ANTHROPIC_API_KEY` environment variable in the `.env` file. If no valid API key is set there, the rule will be tested for, but without AI assistance.
506
508
 
507
509
  ### WAVE
508
510
 
package/UPGRADES.md CHANGED
@@ -809,7 +809,7 @@ You're absolutely right about `nuVal`'s limitations. Since it requires publicly
809
809
 
810
810
  1. **`htmlcs`** - Pure JavaScript, no external dependencies, works on any HTML
811
811
  2. **`wave`** - Also API-based, so same limitations as `nuVal`
812
- 4. **`axe`** - Self-contained, works on any page, excellent for establishing patterns
812
+ 3. **`axe`** - Self-contained, works on any page, excellent for establishing patterns
813
813
 
814
814
  **I recommend starting with `axe`** because:
815
815
 
@@ -0,0 +1,3 @@
1
+ # Memory Index
2
+
3
+ - [Validation system paused](project_validation_pause.md) — new validation tests on hold pending redesign of broken validation architecture
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: Validation system paused
3
+ description: All new validation test work is on hold pending a redesign of the currently broken validation architecture
4
+ type: project
5
+ ---
6
+
7
+ New validation tests and updates to existing validation tests are paused. The validation system is currently broken and a redesign is anticipated. Do not create or modify files in validation/tests/ until the user indicates the redesign is underway.
8
+
9
+ **Why:** The validation architecture is broken and being reconsidered.
10
+ **How to apply:** Skip validation file creation/updates when adding or modifying Testaro rules until further notice.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "71.1.0",
3
+ "version": "72.1.0",
4
4
  "description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -10,31 +10,180 @@
10
10
  /*
11
11
  allCaps
12
12
  Related to Tenon rule 153.
13
- This test reports elements with native upper-case text at least 8 characters long. Blocks of upper-case text are difficult to read.
13
+ This test reports elements whose text contains upper-case strings that are not intrinsically upper-case (i.e., not acronyms, abbreviations, or terms whose standard form is all-capitals). Claude Haiku classifies qualifying catalog entries and estimates the probability of a rule violation. If the AI call fails, the test falls back to a rule-based check for 8+ consecutive upper-case letters.
14
14
  */
15
15
 
16
16
  // IMPORTS
17
17
 
18
- const {doTest} = require('../procs/testaro');
18
+ const https = require('https');
19
+
20
+ // PARAMETERS
21
+
22
+ const MIN_CONFIDENCE = 0.5;
23
+ const MAX_MARGIN = 100;
24
+ const MAX_TOTAL = 2000;
25
+ const MAX_QUALIFYING = 100;
26
+
27
+ // CONSTANTS
28
+
29
+ const ruleID = 'allCaps';
30
+ const whats = 'Elements have all-capital text';
19
31
 
20
32
  // FUNCTIONS
21
33
 
22
- // Runs the test and returns the result.
23
- exports.reporter = async (page, catalog, withItems) => {
24
- const getBadWhat = element => {
25
- // Get the child text nodes of the element.
26
- const childTextNodes = Array.from(element.childNodes).filter(
27
- node => node.nodeType === Node.TEXT_NODE
28
- );
29
- // If any of them contains 8 or more consecutive capital letters:
30
- if (childTextNodes.some(node => /[A-Z]{8,}/.test(node.nodeValue))) {
31
- // Return a violation description.
32
- return 'Element contains all-capital text';
34
+ // Returns text limited to MAX_MARGIN characters before the first and after the last uppercase match, capped at MAX_TOTAL characters.
35
+ const getContext = text => {
36
+ const matches = [...text.matchAll(/\p{Lu}{2,}/gu)];
37
+ const first = matches[0];
38
+ const last = matches[matches.length - 1];
39
+ const start = Math.max(0, first.index - MAX_MARGIN);
40
+ const end = Math.min(text.length, last.index + last[0].length + MAX_MARGIN);
41
+ return text.slice(start, end).slice(0, MAX_TOTAL);
42
+ };
43
+
44
+ // Returns violations using the rule-based fallback (8+ consecutive uppercase letters).
45
+ const getRuleBasedViolations = catalog =>
46
+ Object.entries(catalog)
47
+ .filter(([, entry]) => entry.text && /\p{Lu}{8,}/u.test(entry.text))
48
+ .map(([index]) => ({
49
+ catalogIndex: index,
50
+ what: '[No AI available] Element contains all-capital text'
51
+ }));
52
+
53
+ // Sends qualifying entries to Claude Haiku and returns confidence scores.
54
+ const classifyWithAI = entries => new Promise((resolve, reject) => {
55
+ const apiKey = process.env.ANTHROPIC_API_KEY;
56
+ if (!apiKey) {
57
+ reject(new Error('ANTHROPIC_API_KEY not set'));
58
+ return;
59
+ }
60
+ const prompt =
61
+ 'Classify HTML elements for an accessibility rule. All-capital text violates the rule UNLESS it is an acronym, abbreviation, or term whose standard form is all-capitals (NASA, WHO, etc.).\n\n'
62
+ + 'For each element, give a confidence score (0.0–1.0, rounded to one decimal place) for the probability that the element VIOLATES the rule (its all-caps text is NOT intrinsically all-caps).\n\n'
63
+ + 'Respond with ONLY a JSON array. Each element: {"index": <number>, "confidence": <number>}\n\n'
64
+ + 'Elements:\n'
65
+ + JSON.stringify(entries);
66
+ const payload = JSON.stringify({
67
+ model: 'claude-haiku-4-5-20251001',
68
+ max_tokens: 2048,
69
+ messages: [{role: 'user', content: prompt}]
70
+ });
71
+ const options = {
72
+ hostname: 'api.anthropic.com',
73
+ path: '/v1/messages',
74
+ method: 'POST',
75
+ headers: {
76
+ 'x-api-key': apiKey,
77
+ 'anthropic-version': '2023-06-01',
78
+ 'content-type': 'application/json',
79
+ 'content-length': Buffer.byteLength(payload)
33
80
  }
34
81
  };
35
- const selector = 'body, body *:not(style, script, svg)';
36
- const whats = 'Elements have all-capital text';
37
- return await doTest(
38
- page, catalog, withItems, 'allCaps', selector, whats, 0, getBadWhat.toString()
39
- );
82
+ const req = https.request(options, res => {
83
+ let body = '';
84
+ res.on('data', chunk => { body += chunk; });
85
+ res.on('end', () => {
86
+ try {
87
+ const parsed = JSON.parse(body);
88
+ if (parsed.error) {
89
+ reject(new Error(parsed.error.message));
90
+ return;
91
+ }
92
+ const text = parsed.content[0].text;
93
+ resolve(
94
+ [...text.matchAll(/\{"index":\s*\d+,\s*"confidence":\s*[01]\.\d{1,2}\}/g)]
95
+ .map(m => {
96
+ const {index, confidence} = JSON.parse(m[0]);
97
+ return {index, confidence: Math.round(confidence * 10) / 10};
98
+ })
99
+ );
100
+ }
101
+ catch(error) {
102
+ reject(new Error(`Haiku response error: ${error.message}`));
103
+ }
104
+ });
105
+ });
106
+ req.on('error', reject);
107
+ req.setTimeout(20000, () => req.destroy(new Error('Haiku API timeout')));
108
+ req.write(payload);
109
+ req.end();
110
+ });
111
+
112
+ // Runs the test and returns the result.
113
+ exports.reporter = async (_, catalog, withItems) => {
114
+ const data = {};
115
+ const totals = [0, 0, 0, 0];
116
+ const standardInstances = [];
117
+ // Get data on the catalog entries whose text values contain 2+ consecutive capital letters.
118
+ const qualifying = Object.entries(catalog)
119
+ .filter(([, entry]) => entry.text && /\p{Lu}{2,}/u.test(entry.text))
120
+ .map(([index, entry]) => ({
121
+ index: Number(index),
122
+ tagName: entry.tagName,
123
+ text: getContext(entry.text)
124
+ }));
125
+ // If there are none:
126
+ if (!qualifying.length) {
127
+ // Report this.
128
+ return {data, totals, standardInstances};
129
+ }
130
+ // Sort them, with unbiased (Fisher–Yates) randomization.
131
+ const sample = qualifying.slice();
132
+ for (let i = sample.length - 1; i > 0; i--) {
133
+ const j = Math.floor(Math.random() * (i + 1));
134
+ [sample[i], sample[j]] = [sample[j], sample[i]];
135
+ }
136
+ // If their count exceeds the limit on AI assistance:
137
+ if (sample.length > MAX_QUALIFYING) {
138
+ // Truncate them.
139
+ sample.length = MAX_QUALIFYING;
140
+ }
141
+ let violations;
142
+ try {
143
+ // Get AI estimates of the probabilities of their violating the rule.
144
+ const classifications = await classifyWithAI(sample);
145
+ // Treat the entries with above-minimum violation confidence levels as violations.
146
+ violations = classifications
147
+ .filter(({confidence}) => confidence >= MIN_CONFIDENCE)
148
+ .map(({index, confidence}) => ({
149
+ catalogIndex: String(index),
150
+ what: `Element contains unnecessarily (with confidence ${Math.round(confidence * 100)}%) all-capital text`
151
+ }));
152
+ const evaluated = classifications.length;
153
+ const leftOut = qualifying.length - evaluated;
154
+ // If any entries were truncated out and any AI confidence levels were above-minimum:
155
+ if (leftOut > 0 && evaluated > 0) {
156
+ // Add data about the estimated violation rate among those entries.
157
+ data.leftOut = {
158
+ count: leftOut,
159
+ estimatedViolations: Math.round((violations.length / evaluated) * leftOut)
160
+ };
161
+ }
162
+ }
163
+ catch(error) {
164
+ data.aiError = error.message;
165
+ violations = getRuleBasedViolations(catalog);
166
+ }
167
+ const estimatedLeftOut = data.leftOut?.estimatedViolations ?? 0;
168
+ // Add the estimated violation count to the totals.
169
+ totals[0] = violations.length + estimatedLeftOut;
170
+ // If itemization is required:
171
+ if (withItems) {
172
+ // For each entry deemed a violation:
173
+ for (const {catalogIndex, what} of violations) {
174
+ // Add an instance to the standard instances.
175
+ standardInstances.push({ruleID, what, ordinalSeverity: 0, count: 1, catalogIndex});
176
+ }
177
+ // If any entries were truncated:
178
+ if (estimatedLeftOut) {
179
+ // Add a summary instance for them.
180
+ standardInstances.push({ruleID, what: whats, ordinalSeverity: 0, count: estimatedLeftOut});
181
+ }
182
+ }
183
+ // Otherwise, i.e. if itemization is not required, and if any violations exist:
184
+ else if (totals[0]) {
185
+ // Add a summary instance for them.
186
+ standardInstances.push({ruleID, what: whats, ordinalSeverity: 0, count: totals[0]});
187
+ }
188
+ return {data, totals, standardInstances};
40
189
  };
@@ -38,6 +38,20 @@ exports.reporter = async (page, catalog, withItems) => {
38
38
  const isBad = lineHeightNum < 1.495 * fontSizeNum;
39
39
  // If it does:
40
40
  if (isBad) {
41
+ const parent = element.parentElement;
42
+ // If the element has a parent:
43
+ if (parent) {
44
+ // Get the style properties of the parent.
45
+ const parentStyleDec = window.getComputedStyle(parent);
46
+ const {fontSize: parentFontSize, lineHeight: parentLineHeight} = parentStyleDec;
47
+ const parentFontSizeNum = Number.parseFloat(parentFontSize);
48
+ const parentLineHeightNum = Number.parseFloat(parentLineHeight);
49
+ // If the parent also violates the rule:
50
+ if (parentLineHeightNum < 1.495 * parentFontSizeNum) {
51
+ // Do not report a violation, because the line height may be inherited.
52
+ return null;
53
+ }
54
+ }
41
55
  const whatFontSize = `font size (${fontSizeNum.toFixed(1)}px)`;
42
56
  const whatLineHeight = `line height (${lineHeightNum.toFixed(1)}px)`;
43
57
  // Return a violation description.
@@ -40,6 +40,17 @@ exports.reporter = async (page, catalog, withItems) => {
40
40
  const fontSize = Number.parseFloat(fontSizeString);
41
41
  // If its font size is smaller than 11 pixels:
42
42
  if (fontSize < 11) {
43
+ const parent = element.parentElement;
44
+ // If the element has a parent:
45
+ if (parent) {
46
+ const parentStyleDec = window.getComputedStyle(parent);
47
+ const parentFontSize = Number.parseFloat(parentStyleDec.fontSize);
48
+ // If the parent also has a font size smaller than 11 pixels:
49
+ if (parentFontSize < 11) {
50
+ // Do not report a violation, because the font size may be inherited.
51
+ return null;
52
+ }
53
+ }
43
54
  // Return a violation description.
44
55
  return `Element is visible but its font size is ${fontSize}px, smaller than 11px`;
45
56
  }
package/tests/testaro.js CHANGED
@@ -41,10 +41,10 @@ const allRules = [
41
41
  },
42
42
  {
43
43
  id: 'allCaps',
44
- what: 'leaf elements with entirely upper-case text longer than 7 characters',
44
+ what: 'elements with unnecessarily all-capital text substrings',
45
45
  contaminates: false,
46
46
  needsAccessibleName: false,
47
- timeOut: 5,
47
+ timeOut: 30,
48
48
  defaultOn: true
49
49
  },
50
50
  {
@@ -1,88 +0,0 @@
1
- /*
2
- © 2025 Jonathan Robert Pool.
3
- Licensed under the MIT License. See LICENSE file for details.
4
- */
5
-
6
- // aslint.config.js
7
- const { defineConfig } = require('aslint');
8
-
9
- module.exports = defineConfig({
10
- // Configure ASLint rules
11
- rules: {
12
- 'image-alt': 'error',
13
- 'button-name': 'error',
14
- 'html-has-lang': 'error',
15
- 'link-name': 'error',
16
- 'page-title': 'error'
17
- }
18
- });
19
-
20
- // test.spec.js
21
- const { test, expect } = require('@playwright/test');
22
- const { createASLinter } = require('aslint');
23
-
24
- test.describe('Accessibility Tests', () => {
25
- let linter;
26
-
27
- test.beforeAll(async () => {
28
- linter = await createASLinter();
29
- });
30
-
31
- test('should pass accessibility checks', async ({ page }) => {
32
- // Navigate to your page
33
- await page.goto('https://your-website.com');
34
-
35
- // Get the page content
36
- const content = await page.content();
37
-
38
- // Run ASLint
39
- const results = await linter.lint(content);
40
-
41
- // Assert no accessibility violations
42
- expect(results.violations).toHaveLength(0);
43
-
44
- // Optional: Log detailed results
45
- if (results.violations.length > 0) {
46
- console.log('Accessibility violations found:',
47
- results.violations.map(v => ({
48
- rule: v.rule,
49
- message: v.message,
50
- element: v.element
51
- }))
52
- );
53
- }
54
- });
55
-
56
- // Test specific components
57
- test('check specific component accessibility', async ({ page }) => {
58
- await page.goto('https://your-website.com/component');
59
-
60
- // Wait for specific component to be visible
61
- await page.waitForSelector('.your-component');
62
-
63
- // Get component HTML
64
- const componentHTML = await page.$eval(
65
- '.your-component',
66
- el => el.outerHTML
67
- );
68
-
69
- const results = await linter.lint(componentHTML);
70
- expect(results.violations).toHaveLength(0);
71
- });
72
- });
73
-
74
- // playwright.config.js
75
- const { defineConfig } = require('@playwright/test');
76
-
77
- module.exports = defineConfig({
78
- testDir: './tests',
79
- /* Other Playwright configs */
80
- use: {
81
- // Enable automatic accessibility violations logging
82
- trace: 'retain-on-failure'
83
- },
84
- reporter: [
85
- ['html'],
86
- ['list']
87
- ]
88
- });