testaro 64.4.1 → 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.4.1",
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/identify.js CHANGED
@@ -209,7 +209,7 @@ exports.identify = async (instance, page) => {
209
209
  };
210
210
  }
211
211
  }
212
- // Return the result (not yet getting IDs from Nu Html Checker lines and columns).
212
+ // Return the result.
213
213
  return elementID;
214
214
  }
215
215
  };
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,14 +45,16 @@ 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
- // Add it to the data.
48
+ // Annotate all elements on the page with unique identifiers.
49
+ await addTestaroIDs(page);
50
+ // Add the annotated page content to the data.
47
51
  data.testTarget = await page.content();
48
52
  }
49
53
  // Return the data.
50
54
  return data;
51
55
  };
52
56
  // Postprocesses a result from nuVal or nuVnu tests.
53
- exports.curate = (data, nuData, rules) => {
57
+ exports.curate = async (page, data, nuData, rules) => {
54
58
  // Delete most of the test target from the data.
55
59
  data.testTarget = `${data.testTarget.slice(0, 200)}…`;
56
60
  let result;
@@ -76,11 +80,20 @@ exports.curate = (data, nuData, rules) => {
76
80
  return false;
77
81
  }
78
82
  }));
83
+ }
84
+ // If there is a result:
85
+ if (result) {
79
86
  // Remove messages reporting duplicate blank IDs.
80
87
  const badMessages = new Set(['Duplicate ID .', 'The first occurrence of ID was here.']);
81
88
  result.messages = result.messages.filter(
82
89
  message => ! badMessages.has(message.message)
83
90
  );
91
+ // For each message:
92
+ for (const message of result.messages) {
93
+ const {extract} = message;
94
+ // Add location data for the element to the message.
95
+ message.elementLocation = await getLocationData(page, extract);
96
+ }
84
97
  }
85
98
  // Return the result.
86
99
  return result;
@@ -18,8 +18,8 @@
18
18
  // Limits the length of and unilinearizes a string.
19
19
  const cap = rawString => {
20
20
  const string = (rawString || '').replace(/[\s\u2028\u2029]+/g, ' ');
21
- if (string && string.length > 600) {
22
- return `${string.slice(0, 300)} … ${string.slice(-300)}`;
21
+ if (string && string.length > 1000) {
22
+ return `${string.slice(0, 500)} … ${string.slice(-500)}`;
23
23
  }
24
24
  else if (string) {
25
25
  return string;
@@ -35,12 +35,12 @@ const getIdentifiers = code => {
35
35
  // Normalize the code.
36
36
  code = code.replace(/\s+/g, ' ').replace(/\\"/g, '"');
37
37
  // Get the first start tag of an element, if any.
38
- const startTagData = code.match(/^.*?< ?([^>]*)/);
38
+ const startTagData = code.match(/^.*?<([a-zA-][^>]*)/);
39
39
  // If there is any:
40
40
  if (startTagData) {
41
41
  // Get the tag name.
42
- const tagNameData = startTagData[1].match(/^[A-Za-z0-9]+/);
43
- const tagName = tagNameData ? tagNameData[0].toUpperCase() : '';
42
+ const tagNameArray = startTagData[1].match(/^[A-Za-z0-9]+/);
43
+ const tagName = tagNameArray ? tagNameArray[0].toUpperCase() : '';
44
44
  // Get the value of the id attribute, if any.
45
45
  const idData = startTagData[1].match(/ id="([^"]+)"/);
46
46
  const id = idData ? idData[1] : '';
@@ -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
  });
@@ -209,12 +211,23 @@ const doHTMLCS = (result, standardResult, severity) => {
209
211
  });
210
212
  }
211
213
  };
212
- // Converts issue instances from a nuVal or nuVnu result..
214
+ // Converts issue instances from a nuVal or nuVnu result.
213
215
  const doNu = (withSource, result, standardResult) => {
214
216
  const items = result && result.messages;
217
+ // If there are any messages:
215
218
  if (items && items.length) {
219
+ // For each one:
216
220
  items.forEach(item => {
217
- const {extract, firstColumn, lastColumn, lastLine, message, subType, type} = item;
221
+ const {
222
+ extract,
223
+ firstColumn,
224
+ lastColumn,
225
+ lastLine,
226
+ message,
227
+ subType,
228
+ type,
229
+ elementLocation
230
+ } = item;
218
231
  const identifiers = getIdentifiers(extract);
219
232
  if (! identifiers[0] && message) {
220
233
  const tagNameLCArray = message.match(
@@ -229,6 +242,7 @@ const doNu = (withSource, result, standardResult) => {
229
242
  if (locationSegments.every(segment => typeof segment === 'number')) {
230
243
  spec = locationSegments.join(':');
231
244
  }
245
+ const {notInDOM} = elementLocation;
232
246
  const instance = {
233
247
  ruleID: message,
234
248
  what: message,
@@ -236,11 +250,13 @@ const doNu = (withSource, result, standardResult) => {
236
250
  tagName: identifiers[0],
237
251
  id: identifiers[1],
238
252
  location: {
239
- doc: withSource ? 'source' : 'dom',
253
+ doc: withSource ? 'source' : (notInDOM ? 'notInDOM' : 'dom'),
240
254
  type: 'code',
241
255
  spec
242
256
  },
243
- excerpt: cap(extract)
257
+ excerpt: cap(extract),
258
+ boxID: elementLocation?.boxID || '',
259
+ pathID: elementLocation?.pathID || ''
244
260
  };
245
261
  if (type === 'info' && subType === 'warning') {
246
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
@@ -377,23 +377,8 @@ const launch = exports.launch = async (
377
377
  let indentedMsg = '';
378
378
  // If debugging is on:
379
379
  if (debug) {
380
- // Log a summary of the message on the console.
381
- const parts = [msgText.slice(0, 75)];
382
- if (msgText.length > 75) {
383
- parts.push(msgText.slice(75, 150));
384
- if (msgText.length > 150) {
385
- const tail = msgText.slice(150).slice(-150);
386
- if (msgText.length > 300) {
387
- parts.push('...');
388
- }
389
- parts.push(tail.slice(0, 75));
390
- if (tail.length > 75) {
391
- parts.push(tail.slice(75));
392
- }
393
- }
394
- }
395
- indentedMsg = parts.map(part => ` | ${part}`).join('\n');
396
- console.log(`\n${indentedMsg}`);
380
+ // Log the start of the message on the console.
381
+ console.log(`\n${msgText.slice(0, 300)}`);
397
382
  }
398
383
  // Add statistics on the message to the report.
399
384
  const msgTextLC = msgText.toLowerCase();
@@ -429,8 +414,10 @@ const launch = exports.launch = async (
429
414
  });
430
415
  });
431
416
  const isTestaroTest = act.type === 'test' && act.which === 'testaro';
432
- // If the launch is for a testaro test act:
433
- if (isTestaroTest) {
417
+ const isHTMLCSTest = act.type === 'test' && act.which === 'htmlcs';
418
+ const isNuTest = act.type === 'test' && (['nuVal', 'nuVnu'].some(id => act.which === id));
419
+ // If the launch is for a testaro,3 htmlcs, or Nu test act:
420
+ if (isTestaroTest || isHTMLCSTest || isNuTest) {
434
421
  // Add a script to the page to add a window method to get the XPath of an element.
435
422
  await page.addInitScript(() => {
436
423
  window.getXPath = element => {
@@ -477,6 +464,9 @@ const launch = exports.launch = async (
477
464
  return `/${segments.join('/')}`;
478
465
  };
479
466
  });
467
+ }
468
+ // If the launch is for a testaro test act:
469
+ if (isTestaroTest) {
480
470
  // Add a script to the page to compute the accessible name of an element.
481
471
  await page.addInitScript({path: require.resolve('./dist/nameComputation.js')});
482
472
  // Add a script to the page to:
@@ -1685,6 +1675,40 @@ const doActs = async (report, opts = {}) => {
1685
1675
  // Close the last browser launched for standardization.
1686
1676
  await browserClose();
1687
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;
1688
1712
  }
1689
1713
  // Delete the temporary report file.
1690
1714
  await fs.rm(reportPath, {force: true});
package/tests/alfa.js CHANGED
@@ -17,7 +17,6 @@
17
17
 
18
18
  let alfaRules = require('@siteimprove/alfa-rules').default;
19
19
  const {Audit} = require('@siteimprove/alfa-act');
20
- const path = require('path');
21
20
  const {Playwright} = require('@siteimprove/alfa-playwright');
22
21
 
23
22
  // FUNCTIONS
@@ -88,7 +87,9 @@ exports.reporter = async (page, report, actIndex) => {
88
87
  type: targetJ.type,
89
88
  tagName: targetJ.name || '',
90
89
  path: target.path(),
91
- codeLines: codeLines.map(line => line.length > 300 ? `${line.slice(0, 300)}...` : line)
90
+ codeLines: codeLines.map(
91
+ line => line.length > 300 ? `${line.slice(0, 300)}...` : line
92
+ )
92
93
  }
93
94
  };
94
95
  // If the rule summary is missing:
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
  });
package/tests/nuVal.js CHANGED
@@ -65,7 +65,7 @@ exports.reporter = async (page, report, actIndex) => {
65
65
  data.error = message;
66
66
  };
67
67
  // Postprocess the response data.
68
- result = curate(data, nuData, rules);
68
+ result = await curate(page, data, nuData, rules);
69
69
  }
70
70
  // Otherwise, i.e. if the page content was not obtained:
71
71
  else {
package/tests/nuVnu.js CHANGED
@@ -69,7 +69,7 @@ exports.reporter = async (page, report, actIndex) => {
69
69
  // Delete the temporary file.
70
70
  await fs.unlink(pagePath);
71
71
  // Postprocess the result.
72
- result = curate(data, nuData, rules);
72
+ result = await curate(page, data, nuData, rules);
73
73
  }
74
74
  // Otherwise, i.e. if the content was not obtained:
75
75
  else {