testaro 60.18.2 → 61.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  - **Main dirs**: `/tests` (tool test definitions), `/procs` (shared procedures), `/validation` (test validators), root (core modules)
15
15
  - **Key files**: `run.js` (main executor), `actSpecs.js` (act specifications), `call.js` (CLI entry), `tests/testaro.js` (Testaro tool rules)
16
- - **Tools**: Integrates 11 a11y tools (Axe, Alfa, IBM Checker, QualWeb, ASLint, WAVE, WallyAX, Ed11y, HTML CodeSniffer, Nu Validator, Testaro)
16
+ - **Tools**: Integrates 12 accessibility tools (Axe, Alfa, IBM Checker, QualWeb, ASLint, WAVE, WallyAX, Ed11y, HTML CodeSniffer, Nu Validator API, Nu Html Checker, Testaro)
17
17
  - **Data flow**: Jobs (JSON) → run.js → tool tests → reports (with standardized results)
18
18
  - **Env vars**: Required for WallyAX (`WAX_KEY`), WAVE (`WAVE_KEY`); optional `DEBUG`, `WAITS`, `JOBDIR`, `REPORTDIR`, `AGENT`
19
19
 
package/README.md CHANGED
@@ -342,9 +342,9 @@ That means that a test act (i.e. an act with a `type` property having the value
342
342
 
343
343
  If a particular test act either must have or may have any other properties, those properties are specified in the `tools` property in `actSpecs.js`.
344
344
 
345
- When you include a `rules` property, you limit the tests of the tool that are performed or reported. For some tools (`alfa`, `axe`, `htmlcs`, `qualWeb`, `testaro`, and `wax`), only the specified tests are performed. Other tools (`aslint`, `ed11y`, `ibm`, `nuVal`, and `wave`) do not allow such a limitation, so, for those tools, all tests are performed but results are reported from only the specified tests.
345
+ When you include a `rules` property, you limit the tests of the tool that are performed or reported. For some tools (`alfa`, `axe`, `htmlcs`, `qualWeb`, `testaro`, and `wax`), only the specified tests are performed. Other tools (`aslint`, `ed11y`, `ibm`, `nuVal`, `nuVnu`, and `wave`) do not allow such a limitation, so, for those tools, all tests are performed but results are reported from only the specified tests.
346
346
 
347
- The `nuVal`, `qualWeb`, and `testaro` tools require specific formats for the `rules` property. Those formats are described below in the sections about those tools.
347
+ The `nuVal`, `nuVnu`, `qualWeb`, and `testaro` tools require specific formats for the `rules` property. Those formats are described below in the sections about those tools.
348
348
 
349
349
  ##### Examples of test acts
350
350
 
@@ -490,9 +490,9 @@ Experimentation indicates that the `ibm` tool emits untrappable errors for some
490
490
 
491
491
  #### Nu Html Checker
492
492
 
493
- The `nuVal` tool performs the tests of the Nu Html Checker.
493
+ The `nuVal` and `nuVnu` tools perform the tests of the Nu Html Checker.
494
494
 
495
- Its `rules` argument is **not** an array of rule IDs, but instead is an array of rule _specifications_. A rule specification for `nuVal` is a string with the format `=ruleID` or `~ruleID`. The `=` prefix indicates that the rule ID is invariable. The `~` prefix indicates that the rule ID is variable, in which case the `ruleID` part of the specification is a matching regular expression, rather than the exact text of a message. This `rules` format arises from the fact that `nuVal` generates customized messages and does not accompany them with rule identifiers.
495
+ Its `rules` argument is **not** an array of rule IDs, but instead is an array of rule _specifications_. A rule specification for `nuVal` or `nuVnu` is a string with the format `=ruleID` or `~ruleID`. The `=` prefix indicates that the rule ID is invariable. The `~` prefix indicates that the rule ID is variable, in which case the `ruleID` part of the specification is a matching regular expression, rather than the exact text of a message. This `rules` format arises from the fact that `nuVal` and `nuVnu` generate customized messages and do not accompany them with rule identifiers.
496
496
 
497
497
  #### QualWeb
498
498
 
@@ -731,7 +731,7 @@ This instance says that a `button` element violates a rule named `rule01`.
731
731
 
732
732
  The element has no `id` attribute to distinguish it from other `button` elements, but the tool describes its location. This tool uses an XPath to do that. Tools use various methods for location description, namely:
733
733
 
734
- - `line` (line number in the code of the page): Nu Html Checker
734
+ - `code` (line, starting column, and ending column): Nu Html Checker (API and installed)
735
735
  - `selector` (CSS selector): Axe, QualWeb, WAVE
736
736
  - `xpath`: Alfa, ASLint, Equal Access
737
737
  - `box` (coordinates, width, and height of the element box): Editoria11y, Testaro
@@ -771,7 +771,7 @@ This standard format reflects some judgments. For example:
771
771
 
772
772
  - The `ordinalSeverity` property of an instance involves interpretation. Tools may report severity, certainty, priority, or some combination of those. They may use ordinal or metric quantifications. If they quantify ordinally, their scales may have more or fewer than 4 ranks. Testaro coerces each tool’s severity, certainty, and/or priority classification into a 4-rank ordinal classification. This classification is deemed to express the most common pattern among the tools.
773
773
  - The `tagName` property of an instance may not always be obvious, because in some cases the rule being tested for requires a relationship among more than one element (e.g., “An X element may not have a Y element as its parent”).
774
- - The `ruleID` property of an instance is a matching rule if the tool issues a message but no rule identifier for each instance. The `nuVal` tool does this. In this case, Testaro is classifying the messages into rules.
774
+ - The `ruleID` property of an instance is a matching rule if the tool issues a message but no rule identifier for each instance. The `nuVal` and `nuVnu` tools do this. In this case, Testaro is classifying the messages into rules.
775
775
  - The `ruleID` property of an instance may reclassify tool rules. For example, if a tool rule covers multiple situations that are dissimilar, that rule may be split into multiple rules with distinct `ruleID` properties.
776
776
 
777
777
  You are not dependent on the judgments incorporated into the standard format, because Testaro can give you the original reports from the tools as the `result` property of a `test` act.
@@ -1,4 +1,4 @@
1
- # Validation README
1
+ # Validation
2
2
 
3
3
  Quick notes to run the Testaro validation tests locally (Windows PowerShell):
4
4
 
package/actSpecs.js CHANGED
@@ -133,7 +133,7 @@ exports.actSpecs = {
133
133
  {
134
134
  which: [true, 'string', 'isTest', 'tool name'],
135
135
  launch: [false, 'object', '', 'new target, browserID, and/or what, if any'],
136
- rules: [false, 'array', 'areStrings', 'rule IDs or (for testaro or nuVal) specifications, if not all']
136
+ rules: [false, 'array', 'areStrings', 'rule IDs or (for testaro, nuVal, or nuVnu) specifications, if not all']
137
137
  }
138
138
  ],
139
139
  text: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "60.18.2",
3
+ "version": "61.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": {
@@ -48,7 +48,8 @@
48
48
  "playwright-dompath": "*",
49
49
  "playwright-extra": "*",
50
50
  "playwright-extra-plugin-stealth": "*",
51
- "puppeteer-extra-plugin-stealth": "*"
51
+ "puppeteer-extra-plugin-stealth": "*",
52
+ "vnu-jar": "*"
52
53
  },
53
54
  "devDependencies": {
54
55
  "dom-accessibility-api": "*",
package/procs/job.js CHANGED
@@ -31,7 +31,8 @@ const tools = exports.tools = {
31
31
  ed11y: 'Editoria11y',
32
32
  htmlcs: 'HTML CodeSniffer WCAG 2.1 AA ruleset',
33
33
  ibm: 'IBM Accessibility Checker',
34
- nuVal: 'Nu Html Checker',
34
+ nuVal: 'Nu Html Checker API',
35
+ nuVnu: 'Nu Html Checker',
35
36
  qualWeb: 'QualWeb',
36
37
  testaro: 'Testaro',
37
38
  wax: 'WallyAX',
@@ -30,31 +30,24 @@ const cap = rawString => {
30
30
  };
31
31
  // Returns whether an id attribute value is valid without character escaping.
32
32
  const isBadID = id => /[^-\w]|^\d|^--|^-\d/.test(id);
33
- // Returns the tag name and the value of an id attribute from a substring of HTML code.
33
+ // Returns a tag name and the value of an id attribute from a substring of HTML code.
34
34
  const getIdentifiers = code => {
35
- let tagName = '';
36
- let id = '';
37
- // If the substring includes the start tag of an element:
38
- if (code && typeof code === 'string' && code.length && /<\s*[a-zA-Z]/.test(code)) {
39
- // Get the first start tag in the substring.
40
- const startTag = code.replace(/^.*?<(?=[a-zA-Z])/s, '').replace(/[^a-zA-Z0-9].*$/s, '').trim();
41
- // If it exists:
42
- if (startTag && startTag.length) {
43
- // Get its tag name, upper-cased.
44
- tagName = startTag.replace(/\s.*$/s, '').toUpperCase();
45
- // Get the value of the id attribute of the start tag, if any.
46
- const idArray = startTag.match(/\sid="([^"<>]+)"/);
47
- if (idArray && idArray.length === 2) {
48
- id = idArray[1];
49
- }
50
- // If the id value is invalid without character escaping:
51
- if (isBadID(id)) {
52
- // Remove it.
53
- id = '';
54
- }
55
- }
35
+ // Normalize the code.
36
+ code = code.replace(/\s+/g, ' ').replace(/\\"/g, '"');
37
+ // Get the first start tag of an element, if any.
38
+ const startTagData = code.match(/^.*?< ?([^>]*)/);
39
+ // If there is any:
40
+ if (startTagData) {
41
+ // Get the tag name.
42
+ const tagNameData = startTagData[1].match(/^[A-Za-z]+/);
43
+ const tagName = tagNameData ? tagNameData[0].toUpperCase() : '';
44
+ // Get the value of the id attribute, if any.
45
+ const idData = startTagData[1].match(/ id="([^"]+)"/);
46
+ const id = idData ? idData[1] : '';
47
+ // Return the tag name and the value of the id attribute, if any.
48
+ return [tagName, id];
56
49
  }
57
- return [tagName, id];
50
+ return ['', ''];
58
51
  };
59
52
  /*
60
53
  Differentiates some rule IDs of aslint.
@@ -221,30 +214,77 @@ const doNuVal = (result, standardResult, docType) => {
221
214
  const items = result[docType] && result[docType].messages;
222
215
  if (items && items.length) {
223
216
  items.forEach(item => {
224
- const identifiers = getIdentifiers(item.extract);
225
- if (! identifiers[0] && item.message) {
226
- const tagNameLCArray = item.message.match(
217
+ const {extract, firstColumn, lastColumn, lastLine, message, subType, type} = item;
218
+ const identifiers = getIdentifiers(extract);
219
+ if (! identifiers[0] && message) {
220
+ const tagNameLCArray = message.match(
227
221
  /^Element ([^ ]+)|^An (img) element| (meta|script) element| element (script)| tag (script)/
228
222
  );
229
223
  if (tagNameLCArray && tagNameLCArray[1]) {
230
224
  identifiers[0] = tagNameLCArray[1].toUpperCase();
231
225
  }
232
226
  }
233
- // Include the message twice. A scoring procedure may replace the ruleID with a pattern.
227
+ let spec = '';
228
+ const locationSegments = [lastLine, firstColumn, lastColumn];
229
+ if (locationSegments.every(segment => typeof segment === 'number')) {
230
+ spec = locationSegments.join(':');
231
+ }
234
232
  const instance = {
235
- ruleID: item.message,
236
- what: item.message,
233
+ ruleID: message,
234
+ what: message,
237
235
  ordinalSeverity: -1,
238
236
  tagName: identifiers[0],
239
237
  id: identifiers[1],
240
238
  location: {
241
239
  doc: docType === 'pageContent' ? 'dom' : 'source',
242
- type: 'line',
243
- spec: item && item.lastLine && item.lastLine.toString() || ''
240
+ type: 'code',
241
+ spec
244
242
  },
245
- excerpt: cap(item.extract)
243
+ excerpt: cap(extract)
244
+ };
245
+ if (type === 'info' && subType === 'warning') {
246
+ instance.ordinalSeverity = 0;
247
+ }
248
+ else if (type === 'error') {
249
+ instance.ordinalSeverity = subType === 'fatal' ? 3 : 2;
250
+ }
251
+ standardResult.instances.push(instance);
252
+ });
253
+ }
254
+ };
255
+ // Converts issue instances from a nuVnu document type.
256
+ const doNuVnu = (result, standardResult, docType) => {
257
+ const items = result[docType] && result[docType].messages;
258
+ if (items && items.length) {
259
+ items.forEach(item => {
260
+ const {extract, firstColumn, lastColumn, lastLine, message, subType, type} = item;
261
+ const identifiers = getIdentifiers(extract);
262
+ if (! identifiers[0] && message) {
263
+ const tagNameLCArray = message.match(
264
+ /^Element ([^ ]+)|^An (img) element| (meta|script) element| element (script)| tag (script)/
265
+ );
266
+ if (tagNameLCArray && tagNameLCArray[1]) {
267
+ identifiers[0] = tagNameLCArray[1].toUpperCase();
268
+ }
269
+ }
270
+ let spec = '';
271
+ const locationSegments = [lastLine, firstColumn, lastColumn];
272
+ if (locationSegments.every(segment => typeof segment === 'number')) {
273
+ spec = locationSegments.join(':');
274
+ }
275
+ const instance = {
276
+ ruleID: message,
277
+ what: message,
278
+ ordinalSeverity: -1,
279
+ tagName: identifiers[0],
280
+ id: identifiers[1],
281
+ location: {
282
+ doc: docType === 'pageContent' ? 'dom' : 'source',
283
+ type: 'code',
284
+ spec
285
+ },
286
+ excerpt: cap(extract)
246
287
  };
247
- const {type, subType} = item;
248
288
  if (type === 'info' && subType === 'warning') {
249
289
  instance.ordinalSeverity = 0;
250
290
  }
@@ -568,6 +608,15 @@ const convert = (toolName, data, result, standardResult) => {
568
608
  doNuVal(result, standardResult, 'rawPage');
569
609
  }
570
610
  }
611
+ // nuVnu
612
+ else if (toolName === 'nuVnu' && (result.pageContent || result.rawPage)) {
613
+ if (result.pageContent) {
614
+ doNuVnu(result, standardResult, 'pageContent');
615
+ }
616
+ if (result.rawPage) {
617
+ doNuVnu(result, standardResult, 'rawPage');
618
+ }
619
+ }
571
620
  // qualWeb
572
621
  else if (
573
622
  toolName === 'qualWeb'
package/run.js CHANGED
@@ -83,7 +83,7 @@ const timeLimits = {
83
83
  alfa: 20,
84
84
  ed11y: 30,
85
85
  ibm: 30,
86
- testaro: 200 + Math.round(6 * waits / 1000)
86
+ testaro: 150 + Math.round(6 * waits / 1000)
87
87
  };
88
88
  // Timeout multiplier.
89
89
  const timeoutMultiplier = Number.parseFloat(process.env.TIMEOUT_MULTIPLIER) || 1;
package/tests/nuVal.js CHANGED
@@ -9,13 +9,8 @@
9
9
 
10
10
  /*
11
11
  nuVal
12
- This tool subjects a page and its source to the Nu Html Checker, thereby testing scripted
13
- content found only in the loaded page and erroneous content before the browser corrects it.
14
- The API erratically replaces left and right double quotation marks with invalid UTF-8, which
15
- appears as 2 or 3 successive instances of the replacement character (U+fffd). Therefore, this
16
- test removes all such quotation marks and the replacement character. That causes
17
- 'Bad value “” for' to become 'Bad value for'. Since the corruption of quotation marks is
18
- erratic, no better solution is known.
12
+ This tool subjects a page and its source to the Nu Html Checker, thereby testing scripted content found only in the loaded page and erroneous content before the browser corrects it. The API erratically replaces left and right double quotation marks with invalid UTF-8, which appears as 2 or 3 successive instances of the replacement character (U+fffd). Therefore, this test removes all such quotation marks and the replacement character. That causes 'Bad value “” for' to become 'Bad value for'. Since the corruption of quotation marks is erratic, no better solution is known.
13
+ This tool is the API version of the Nu Html Checker. It is an alternative to the vnu tool, which uses the same validator as an installed dependency. Each tool has advantages and disadvantages. The main advantage of nuVal is that it does not require the Testaro host to provide a Java virtual machine. The main disadvantage of nuVal is that it returns 502 status errors when the content being uploaded is larger than about 80,000 bytes. The main advantage of the vnu tool is that it can evaluate pages larger than about 80,000 bytes and pages reachable from the host that Testaro runs on even if not reachable from the public Internet.
19
14
  */
20
15
 
21
16
  // IMPORTS
package/tests/nuVnu.js ADDED
@@ -0,0 +1,131 @@
1
+ /*
2
+ © 2022–2024 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Jonathan Robert Pool.
4
+
5
+ Licensed under the MIT License. See LICENSE file at the project root or
6
+ https://opensource.org/license/mit/ for details.
7
+
8
+ SPDX-License-Identifier: MIT
9
+ */
10
+
11
+ /*
12
+ This tool subjects a page and its source to the Nu Html Checker, thereby testing scripted content found only in the loaded page and erroneous content before the browser corrects it. The API erratically replaces left and right double quotation marks with invalid UTF-8, which appears as 2 or 3 successive instances of the replacement character (U+fffd). Therefore, this test removes all such quotation marks and the replacement character. That causes 'Bad value “” for' to become 'Bad value for'. Since the corruption of quotation marks is erratic, no better solution is known.
13
+ This tool is the installed version of the Nu Html Checker. It is an alternative to the nuVal tool, which uses the same validator as a web service of the World Wide Web Consortium (W3C). Each tool has advantages and disadvantages. The main advantage of the nuVnu tool is that it can evaluate pages larger than about 80,000 bytes and pages reachable from the host that Testaro runs on even if not reachable from the public Internet. The main advantage of nuVal is that it does not require the Testaro host to provide a Java virtual machine.
14
+ */
15
+
16
+ // IMPORTS
17
+
18
+ // Module to perform file operations.
19
+ const fs = require('fs/promises');
20
+ // Module to define the operating-system temporary-file directory.
21
+ const os = require('os');
22
+ // Module to run tests.
23
+ const {vnu} = require('vnu-jar');
24
+ // Module to get the document source.
25
+ const {getSource} = require('../procs/getSource');
26
+
27
+ // CONSTANTS
28
+
29
+ const tmpDir = os.tmpdir();
30
+
31
+ // FUNCTIONS
32
+
33
+ // Conducts and reports the Nu Html Checker tests.
34
+ exports.reporter = async (page, report, actIndex) => {
35
+ const act = report.acts[actIndex];
36
+ const {rules} = act;
37
+ // Get the browser-parsed page.
38
+ const pageContent = await page.content();
39
+ const pagePath = `${tmpDir}/nuVnu-page-${report.id}.html`;
40
+ // Save it.
41
+ await fs.writeFile(pagePath, pageContent);
42
+ // Get the source.
43
+ const sourceData = await getSource(page);
44
+ const data = {
45
+ docTypes: {
46
+ pageContent: {},
47
+ rawPage: {}
48
+ }
49
+ };
50
+ const result = {};
51
+ // If the source was not obtained:
52
+ if (sourceData.prevented) {
53
+ // Report this.
54
+ data.prevented = true;
55
+ data.error = sourceData.error;
56
+ }
57
+ // Otherwise, i.e. if it was obtained:
58
+ else {
59
+ const sourcePath = `${tmpDir}/nuVnu-source-${report.id}.html`;
60
+ // Save the source.
61
+ await fs.writeFile(sourcePath, sourceData.source);
62
+ const pageTypes = [['pageContent', pagePath], ['rawPage', sourcePath]];
63
+ // For each page type:
64
+ for (const page of pageTypes) {
65
+ let nuResult;
66
+ try {
67
+ // Get Nu Html Checker output on it.
68
+ const nuOutput = await vnu.check(['--format', 'json', '--stdout', page[1]]);
69
+ // Consider the JSON output to be the result.
70
+ nuResult = nuOutput;
71
+ }
72
+ // If any error was thrown:
73
+ catch (error) {
74
+ let errorMessage = error.message;
75
+ // If it was due to an incompatible Java version:
76
+ if (errorMessage.includes('Unsupported major.minor version')) {
77
+ // Revise the error messageand report this.
78
+ errorMessage = `Installed version of Java is incompatible. Details: ${errorMessage}`;
79
+ data.docTypes[page[0]].prevented = true;
80
+ data.docTypes[page[0]].error = errorMessage;
81
+ }
82
+ // Otherwise, i.e. if it was not due to an incompatible Java version:
83
+ else {
84
+ try {
85
+ // Treat the output as a JSON result reporting rule violations.
86
+ nuResult = JSON.parse(error.message);
87
+ // But, if parsing it as JSON fails:
88
+ } catch (error) {
89
+ // Report this.
90
+ data.docTypes[page[0]].prevented = true;
91
+ data.docTypes[page[0]].error = `Error getting result (${error.message.slice(0, 300)});`;
92
+ }
93
+ }
94
+ }
95
+ // Delete the temporary file.
96
+ await fs.unlink(page[1]);
97
+ // If a result with or without reported violations was obtained:
98
+ if (nuResult) {
99
+ // Delete left and right quotation marks and their erratic invalid replacements.
100
+ const nuResultClean = JSON.parse(JSON.stringify(nuResult).replace(/[\u{fffd}“”]/ug, ''));
101
+ result[page[0]] = nuResultClean;
102
+ // If there is a report and rules were specified:
103
+ if (! result[page[0]].error && rules && Array.isArray(rules) && rules.length) {
104
+ // Remove all messages except those specified.
105
+ result[page[0]].messages = result[page[0]].messages.filter(message => rules.some(rule => {
106
+ if (rule[0] === '=') {
107
+ return message.message === rule.slice(1);
108
+ }
109
+ else if (rule[0] === '~') {
110
+ return new RegExp(rule.slice(1)).test(message.message);
111
+ }
112
+ else {
113
+ console.log(`ERROR: Invalid nuVal rule ${rule}`);
114
+ return false;
115
+ }
116
+ }));
117
+ }
118
+ }
119
+ }
120
+ // If both page types prevented testing:
121
+ if (pageTypes.every(pageType => data.docTypes[pageType[0]].prevented)) {
122
+ // Report this.
123
+ data.prevented = true;
124
+ data.error = 'Both doc types prevented';
125
+ }
126
+ }
127
+ return {
128
+ data,
129
+ result
130
+ };
131
+ };