testaro 64.5.0 → 64.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "64.5.0",
3
+ "version": "64.6.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": {
package/procs/nu.js CHANGED
@@ -14,6 +14,8 @@
14
14
 
15
15
  // ########## IMPORTS
16
16
 
17
+ // Module to add Testaro IDs to elements.
18
+ const {addTestaroIDs, getLocationData} = require('./testaro');
17
19
  // Module to get the document source.
18
20
  const {getSource} = require('./getSource');
19
21
 
@@ -43,13 +45,8 @@ exports.getContent = async (page, withSource) => {
43
45
  }
44
46
  // Otherwise, i.e. if the specified content type was the Playwright page content:
45
47
  else {
46
- // Annotate all elements in the page with unique identifiers.
47
- await page.evaluate(() => {
48
- let serialID = 0;
49
- for (const element of Array.from(document.querySelectorAll('*'))) {
50
- element.setAttribute('data-testaro-id', `${serialID++}#`);
51
- }
52
- });
48
+ // Annotate all elements on the page with unique identifiers.
49
+ await addTestaroIDs(page);
53
50
  // Add the annotated page content to the data.
54
51
  data.testTarget = await page.content();
55
52
  }
@@ -94,43 +91,8 @@ exports.curate = async (page, data, nuData, rules) => {
94
91
  // For each message:
95
92
  for (const message of result.messages) {
96
93
  const {extract} = message;
97
- const testaroIDArray = extract.match(/data-testaro-id="(\d+)#"/);
98
- // If its extract contains a Testaro identifier:
99
- if (testaroIDArray) {
100
- const testaroID = message.testaroID = testaroIDArray[1];
101
- // Add location data for the element to the message.
102
- message.elementLocation = await page.evaluate(testaroID => {
103
- const element = document.querySelector(`[data-testaro-id="${testaroID}#"]`);
104
- // If any element has that identifier:
105
- if (element) {
106
- // Get a box specification and an XPath for the element.
107
- const box = {};
108
- const boundingBox = element.getBoundingClientRect() || {};
109
- if (boundingBox.x) {
110
- ['x', 'y', 'width', 'height'].forEach(coordinate => {
111
- box[coordinate] = Math.round(boundingBox[coordinate]);
112
- });
113
- }
114
- const xPath = window.getXPath(element) || '';
115
- // Treat them as the element location.
116
- return {
117
- box,
118
- xPath
119
- };
120
- }
121
- // Otherwise, i.e. if no element has it, make the location data empty.
122
- return {};
123
- }, testaroID);
124
- }
125
- // Otherwise, i.e. if its extract contains no Testaro identifier:
126
- else {
127
- // Add a non-DOM location to the message.
128
- message.elementLocation = {
129
- notInDOM: true,
130
- box: {},
131
- xPath: ''
132
- };
133
- }
94
+ // Add location data for the element to the message.
95
+ message.elementLocation = await getLocationData(page, extract);
134
96
  }
135
97
  }
136
98
  // Return the result.
@@ -189,7 +189,7 @@ const doHTMLCS = (result, standardResult, severity) => {
189
189
  const ruleData = result[severity][ruleID];
190
190
  Object.keys(ruleData).forEach(what => {
191
191
  ruleData[what].forEach(item => {
192
- const {tagName, id, code} = item;
192
+ const {tagName, id, excerpt, notInDOM, boxID, pathID} = item;
193
193
  const instance = {
194
194
  ruleID,
195
195
  what,
@@ -197,11 +197,13 @@ const doHTMLCS = (result, standardResult, severity) => {
197
197
  tagName: tagName.toUpperCase(),
198
198
  id: isBadID(id.slice(1)) ? '' : id.slice(1),
199
199
  location: {
200
- doc: 'dom',
200
+ doc: notInDOM ? 'notInDOM' : 'dom',
201
201
  type: '',
202
202
  spec: ''
203
203
  },
204
- excerpt: cap(code)
204
+ excerpt: cap(excerpt),
205
+ boxID,
206
+ pathID
205
207
  };
206
208
  standardResult.instances.push(instance);
207
209
  });
@@ -253,8 +255,8 @@ const doNu = (withSource, result, standardResult) => {
253
255
  spec
254
256
  },
255
257
  excerpt: cap(extract),
256
- boxID: elementLocation?.box || {},
257
- pathID: elementLocation?.xPath || ''
258
+ boxID: elementLocation?.boxID || '',
259
+ pathID: elementLocation?.pathID || ''
258
260
  };
259
261
  if (type === 'info' && subType === 'warning') {
260
262
  instance.ordinalSeverity = 0;
package/procs/testaro.js CHANGED
@@ -225,3 +225,56 @@ exports.getVisibleCountChange = async (
225
225
  elapsedTime
226
226
  };
227
227
  };
228
+ // Annotates every element on a page with a unique identifier.
229
+ exports.addTestaroIDs = async page => {
230
+ await page.evaluate(() => {
231
+ let serialID = 0;
232
+ for (const element of Array.from(document.querySelectorAll('*'))) {
233
+ element.setAttribute('data-testaro-id', `${serialID++}#`);
234
+ }
235
+ });
236
+ };
237
+ // Returns location data from the extract of a standard instance.
238
+ exports.getLocationData = async (page, extract) => {
239
+ const testaroIDArray = extract.match(/data-testaro-id="(\d+)#"/);
240
+ // If the extract contains a Testaro identifier:
241
+ if (testaroIDArray) {
242
+ const testaroID = testaroIDArray[1];
243
+ // Return location data for the element.
244
+ return await page.evaluate(testaroID => {
245
+ const element = document.querySelector(`[data-testaro-id="${testaroID}#"]`);
246
+ // If any element has that identifier:
247
+ if (element) {
248
+ // Get box and path IDs for the element.
249
+ const box = {};
250
+ let boxID = '';
251
+ const boundingBox = element.getBoundingClientRect() || {};
252
+ if (boundingBox.x) {
253
+ ['x', 'y', 'width', 'height'].forEach(coordinate => {
254
+ box[coordinate] = Math.round(boundingBox[coordinate]);
255
+ });
256
+ }
257
+ if (typeof box.x === 'number') {
258
+ boxID = Object.values(box).join(':');
259
+ }
260
+ const pathID = window.getXPath(element) || '';
261
+ // Return them.
262
+ return {
263
+ boxID,
264
+ pathID
265
+ };
266
+ }
267
+ // Otherwise, i.e. if no element has it, return empty location data.
268
+ return {};
269
+ }, testaroID);
270
+ }
271
+ // Otherwise, i.e. if the extract contains no Testaro identifier:
272
+ else {
273
+ // Return a non-DOM location.
274
+ return {
275
+ notInDOM: true,
276
+ boxID: '',
277
+ pathID: ''
278
+ };
279
+ }
280
+ };
package/run.js CHANGED
@@ -414,9 +414,10 @@ const launch = exports.launch = async (
414
414
  });
415
415
  });
416
416
  const isTestaroTest = act.type === 'test' && act.which === 'testaro';
417
+ const isHTMLCSTest = act.type === 'test' && act.which === 'htmlcs';
417
418
  const isNuTest = act.type === 'test' && (['nuVal', 'nuVnu'].some(id => act.which === id));
418
- // If the launch is for a testaro or Nu test act:
419
- if (isTestaroTest || isNuTest) {
419
+ // If the launch is for a testaro,3 htmlcs, or Nu test act:
420
+ if (isTestaroTest || isHTMLCSTest || isNuTest) {
420
421
  // Add a script to the page to add a window method to get the XPath of an element.
421
422
  await page.addInitScript(() => {
422
423
  window.getXPath = element => {
@@ -1674,6 +1675,40 @@ const doActs = async (report, opts = {}) => {
1674
1675
  // Close the last browser launched for standardization.
1675
1676
  await browserClose();
1676
1677
  console.log('Standardization completed');
1678
+ // XXX
1679
+ const {acts} = report;
1680
+ const idData = {};
1681
+ for (const act of acts) {
1682
+ if (act.type === 'test') {
1683
+ const {which} = act;
1684
+ idData[which] ??= {
1685
+ instanceCount: 0,
1686
+ boxIDCount: 0,
1687
+ pathIDCount: 0,
1688
+ boxIDPercent: null,
1689
+ pathIDPercent: null
1690
+ };
1691
+ const actIDData = idData[which];
1692
+ const {standardResult} = act;
1693
+ const {instances} = standardResult;
1694
+ for (const instance of instances) {
1695
+ const {boxID, pathID} = instance;
1696
+ actIDData.instanceCount++;
1697
+ if (boxID) {
1698
+ actIDData.boxIDCount++;
1699
+ }
1700
+ if (pathID) {
1701
+ actIDData.pathIDCount++;
1702
+ }
1703
+ }
1704
+ const {instanceCount, boxIDCount, pathIDCount} = actIDData;
1705
+ if (instanceCount) {
1706
+ actIDData.boxIDPercent = Math.round(100 * boxIDCount / instanceCount);
1707
+ actIDData.pathIDPercent = Math.round(100 * pathIDCount / instanceCount);
1708
+ }
1709
+ }
1710
+ }
1711
+ report.jobData.idData = idData;
1677
1712
  }
1678
1713
  // Delete the temporary report file.
1679
1714
  await fs.rm(reportPath, {force: true});
package/tests/htmlcs.js CHANGED
@@ -14,6 +14,8 @@
14
14
 
15
15
  // IMPORTS
16
16
 
17
+ // Module to add Testaro IDs to elements.
18
+ const {addTestaroIDs, getLocationData} = require('../procs/testaro');
17
19
  // Module to handle files.
18
20
  const fs = require('fs/promises');
19
21
 
@@ -28,10 +30,12 @@ exports.reporter = async (page, report, actIndex) => {
28
30
  // Get the HTMLCS script.
29
31
  const scriptText = await fs.readFile(`${__dirname}/../htmlcs/HTMLCS.js`, 'utf8');
30
32
  const scriptNonce = report.jobData && report.jobData.lastScriptNonce;
31
- // Define the rules to be employed as those of WCAG 2 level AAA.
33
+ // Annotate all elements on the page with unique identifiers.
34
+ await addTestaroIDs(page);
32
35
  let messageStrings = [];
36
+ // Define the rules to be employed as those of WCAG 2 level AAA.
33
37
  for (const actStandard of ['WCAG2AAA']) {
34
- const nextIssues = await page.evaluate(args => {
38
+ const nextViolations = await page.evaluate(args => {
35
39
  // Add the HTMLCS script to the page.
36
40
  const scriptText = args[2];
37
41
  const scriptNonce = args[3];
@@ -49,18 +53,18 @@ exports.reporter = async (page, report, actIndex) => {
49
53
  }
50
54
  window.HTMLCS_WCAG2AAA.sniffs = rules;
51
55
  }
56
+ let violations = null;
52
57
  // Run the tests.
53
- let issues = null;
54
58
  try {
55
- issues = window.HTMLCS_RUNNER.run(actStandard);
59
+ violations = window.HTMLCS_RUNNER.run(actStandard);
56
60
  }
57
61
  catch(error) {
58
62
  console.log(`ERROR executing HTMLCS_RUNNER on ${document.URL} (${error.message})`);
59
63
  }
60
- return issues;
64
+ return violations;
61
65
  }, [actStandard, rules, scriptText, scriptNonce]);
62
- if (nextIssues && nextIssues.every(issue => typeof issue === 'string')) {
63
- messageStrings.push(... nextIssues);
66
+ if (nextViolations && nextViolations.every(violation => typeof violation === 'string')) {
67
+ messageStrings.push(... nextViolations);
64
68
  }
65
69
  else {
66
70
  data.prevented = true;
@@ -69,39 +73,41 @@ exports.reporter = async (page, report, actIndex) => {
69
73
  }
70
74
  }
71
75
  if (! data.prevented) {
72
- // Sort the issues by class and standard.
76
+ // Sort the violations by class and standard.
73
77
  messageStrings.sort();
74
- // Remove any duplicate issues.
78
+ // Remove any duplicate violations.
75
79
  messageStrings = [... new Set(messageStrings)];
76
80
  // Initialize the result.
77
81
  result.Error = {};
78
82
  result.Warning = {};
79
- // For each issue:
80
- messageStrings.forEach(string => {
83
+ // For each violation:
84
+ messageStrings.forEach(async string => {
85
+ // Split its message into severity class, rule ID, tagname, ID, rule description, and excerpt.
81
86
  const parts = string.split(/\|/, 6);
82
87
  const partCount = parts.length;
83
88
  if (partCount < 6) {
84
- console.log(`ERROR: Issue string ${string} has too few parts`);
89
+ console.log(`ERROR: Violation string ${string} has too few parts`);
85
90
  }
86
91
  // If it is an error or a warning (not a notice):
87
92
  else if (['Error', 'Warning'].includes(parts[0])) {
88
93
  /*
89
- Add the issue to an issueClass.issueCode.description array in the result.
90
- This saves space, because, although some descriptions are issue-specific, such as
94
+ Add the violation to an violationClass.violationCode.description array in the result.
95
+ This saves space, because, although some descriptions are violation-specific, such as
91
96
  descriptions that state the contrast ratio of an element, most descriptions are
92
97
  generic, so typically many violations share a description.
93
98
  */
94
- const issueCode = parts[1].replace(/^WCAG2|\.Principle\d\.Guideline[\d_]+/g, '');
95
- if (! result[parts[0]][issueCode]) {
96
- result[parts[0]][issueCode] = {};
97
- }
98
- if (! result[parts[0]][issueCode][parts[4]]) {
99
- result[parts[0]][issueCode][parts[4]] = [];
100
- }
101
- result[parts[0]][issueCode][parts[4]].push({
99
+ const ruleID = parts[1].replace(/^WCAG2|\.Principle\d\.Guideline[\d_]+/g, '');
100
+ result[parts[0]][ruleID] ??= {};
101
+ result[parts[0]][ruleID][parts[4]] ??= [];
102
+ const elementLocation = await getLocationData(page, parts[5]);
103
+ const {boxID, notInDOM, pathID} = elementLocation;
104
+ result[parts[0]][ruleID][parts[4]].push({
102
105
  tagName: parts[2],
103
106
  id: parts[3],
104
- code: parts[5]
107
+ notInDOM,
108
+ excerpt: parts[5],
109
+ boxID,
110
+ pathID
105
111
  });
106
112
  }
107
113
  });