testaro 66.0.0 → 67.0.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/README.md CHANGED
@@ -48,7 +48,7 @@ Testaro uses:
48
48
  - [Playwright](https://playwright.dev/) to launch browsers, perform user actions in them, and perform tests
49
49
  - [playwright-extra](https://www.npmjs.com/package/playwright-extra) and [puppeteer-extra-plugin-stealth](https://www.npmjs.com/package/puppeteer-extra-plugin-stealth) to make a Playwright-controlled browser more indistinguishable from a human-operated browser and thus make their requests more likely to succeed
50
50
  - [playwright-dompath](https://www.npmjs.com/package/playwright-dompath) to retrieve XPaths of elements
51
- - [pixelmatch](https://www.npmjs.com/package/pixelmatch) to measure motion
51
+ - [BlazeDiff](https://blazediff.dev/) to measure motion
52
52
 
53
53
  Testaro performs tests of these _tools_:
54
54
 
package/UPGRADES.md CHANGED
@@ -54,7 +54,7 @@ I'll quickly scan for module patterns and JSON imports across the repo to tailor
54
54
  - Strategy for optional properties and progressive enrichment during execution.
55
55
 
56
56
  - **3rd‑party types and augmentations**
57
- - Verify/choose type packages for `playwright`, `@siteimprove/alfa-*`, `axe-playwright`, `@qualweb/*`, `accessibility-checker`, `playwright-dompath`, `pixelmatch`. Plan fallbacks if defs are missing.
57
+ - Verify/choose type packages for `playwright`, `@siteimprove/alfa-*`, `axe-playwright`, `@qualweb/*`, `accessibility-checker`, `playwright-dompath`. Plan fallbacks if defs are missing.
58
58
  - Decide on module augmentation vs casting for custom fields (e.g., `page.browserID` assignment requires augmenting `playwright.Page` or casting).
59
59
 
60
60
  - **Dynamic loading patterns**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "66.0.0",
3
+ "version": "67.0.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": {
@@ -30,6 +30,7 @@
30
30
  },
31
31
  "homepage": "https://github.com/jrpool/testaro#readme",
32
32
  "dependencies": {
33
+ "@blazediff/core": "*",
33
34
  "@qualweb/core": "*",
34
35
  "@qualweb/act-rules": "*",
35
36
  "@qualweb/wcag-techniques": "*",
@@ -43,7 +44,6 @@
43
44
  "aslint-testaro": "*",
44
45
  "axe-playwright": "*",
45
46
  "dotenv": "*",
46
- "pixelmatch": "*",
47
47
  "playwright": "*",
48
48
  "playwright-dompath": "*",
49
49
  "playwright-extra": "*",
@@ -101,20 +101,52 @@ exports.getLocatorData = async loc => {
101
101
  return null;
102
102
  }
103
103
  };
104
- // Returns location data from the extract of a standard instance.
105
- exports.getLocationData = async (page, excerpt) => {
104
+ // Returns properties of an element with an excerpt when Testaro identifiers have been added.
105
+ exports.getElementData = async (page, excerpt) => {
106
+ // Initialize the properties.
107
+ let data = {
108
+ tagName: '',
109
+ id: '',
110
+ text: '',
111
+ notInDOM: true,
112
+ boxID: '',
113
+ pathID: '',
114
+ originalExcerpt: excerpt,
115
+ };
116
+ let elementProperties = {};
106
117
  const testaroIDArray = excerpt.match(/data-testaro-id="(\d+)#([^"]*)"/);
107
- // If the extract contains a Testaro identifier:
118
+ // If the excerpt contains a Testaro identifier:
108
119
  if (testaroIDArray) {
109
- return await page.evaluate(testaroIDArray => {
120
+ // Remove the identifier.
121
+ originalExcerpt = excerpt.replace(/ data-testaro-id="\d+#[^" ]+"?/, '');
122
+ elementProperties = await page.evaluate(testaroIDArray => {
123
+ let tagName = '';
124
+ let id = '';
125
+ const text = [];
126
+ let notInDOM = false;
127
+ let boxID = '';
128
+ let pathID = '';
110
129
  const testaroID = `${testaroIDArray[1]}#${testaroIDArray[2]}`;
111
130
  const element = document.querySelector(`[data-testaro-id="${testaroID}"]`);
112
- // If any element has that identifier:
131
+ // If any element has the identifier:
113
132
  if (element) {
114
- // Get a box ID for the element.
133
+ // Get properties of the element.
134
+ ({tagName, id} = element);
135
+ const segments = element
136
+ .innerText
137
+ ?.trim()
138
+ .split(/[\t\n]+/)
139
+ .filter(segment => segment.length);
140
+ if (segments?.length) {
141
+ if (segments.length > 1) {
142
+ text.push(segments[0], segments[segments.length - 1]);
143
+ }
144
+ else {
145
+ text.push(segments[0]);
146
+ }
147
+ }
148
+ const boundingBox = element.getBoundingClientRect() ?? {};
115
149
  const box = {};
116
- let boxID = '';
117
- const boundingBox = element.getBoundingClientRect() || {};
118
150
  if (boundingBox.x) {
119
151
  ['x', 'y', 'width', 'height'].forEach(coordinate => {
120
152
  box[coordinate] = Math.round(boundingBox[coordinate]);
@@ -123,41 +155,38 @@ exports.getLocationData = async (page, excerpt) => {
123
155
  if (typeof box.x === 'number') {
124
156
  boxID = Object.values(box).join(':');
125
157
  }
126
- // Get a path ID from the Testaro identifier or, if necessary, the element.
127
- let pathID = testaroIDArray[2];
158
+ // Get a path ID from the identifier or, if necessary, the element.
159
+ pathID = testaroIDArray[2];
128
160
  if (! pathID) {
129
161
  pathID = window.getXPath(element);
130
162
  }
131
- // Return the box and path IDs.
132
- return {
133
- boxID,
134
- pathID
135
- };
136
163
  }
137
- // Otherwise, if no element has it but the identifier includes an XPath:
138
- else if (testaroIDArray[2]) {
139
- // Return an empty box ID and that XPath as a path ID.
140
- return {
141
- notInDOM: true,
142
- boxID: '',
143
- pathID: testaroIDArray[2]
144
- };
164
+ // Otherwise, i.e. if no element has the identifier:
165
+ else {
166
+ // Report this.
167
+ notInDOM = true;
145
168
  }
146
- // Otherwise, return empty location properties.
169
+ // Report the properties.
147
170
  return {
148
- notInDOM: true,
149
- boxID: '',
150
- pathID: ''
171
+ tagName,
172
+ id,
173
+ text,
174
+ notInDOM,
175
+ boxID,
176
+ pathID
151
177
  };
152
178
  }, testaroIDArray);
179
+ // Populate the data with any properties obtained from the element.
180
+ Object.assign(data, elementProperties);
153
181
  }
154
- // Otherwise, i.e. if the extract contains no Testaro identifier:
182
+ // Otherwise, i.e. if the excerpt contains no Testaro identifier:
155
183
  else {
156
- // Return a non-DOM location.
157
- return {
158
- notInDOM: true,
159
- boxID: '',
160
- pathID: ''
161
- };
184
+ // Get properties of the element that are gettable from the excerpt.
185
+ const tagNameArray = excerpt.match(/<([-a-z]+)/);
186
+ data.tagName = tagNameArray?.[1] ?? '';
187
+ const idArray = excerpt.match(/ id="([^"]*)"/);
188
+ data.id = idArray?.[1] ?? '';
162
189
  }
190
+ // Return the properties.
191
+ return data;
163
192
  };
package/procs/identify.js CHANGED
@@ -56,30 +56,35 @@ const boxToString = exports.boxToString = box => {
56
56
  };
57
57
  // Normalizes an XPath.
58
58
  const getNormalizedXPath = exports.getNormalizedXPath = xPath => {
59
- xPath = xPath.replace(/^\.\/\//, '/');
60
- const segments = xPath.split('/');
61
- // Initialize an array of normalized segments.
62
- const normalizedSegments = [];
63
- // For each segment of the XPath:
64
- segments.forEach(segment => {
65
- // If the segment is html[1] or body[1]:
66
- if (/html\[1\]|body\[1\]/.test(segment)) {
67
- // Add it without its subscript to the array.
68
- normalizedSegments.push(segment.replace(/\[1\]/, ''));
69
- }
70
- // Otherwise, if the segment is empty or html or body or ends with a subscript:
71
- else if (segment === '' || ['html', 'body'].includes(segment) || segment.endsWith(']')) {
72
- // Add it to the array.
73
- normalizedSegments.push(segment);
74
- }
75
- // Otherwise, i.e. if the segment is a tag name with no subscript:
76
- else {
77
- // Add it with a subscript 1 to the array.
78
- normalizedSegments.push(`${segment}[1]`);
79
- }
80
- });
81
- // Return the concatenated segments as the normalized XPath.
82
- return normalizedSegments.join('/');
59
+ if (xPath) {
60
+ xPath = xPath.replace(/^\.\/\//, '/');
61
+ const segments = xPath.split('/');
62
+ // Initialize an array of normalized segments.
63
+ const normalizedSegments = [];
64
+ // For each segment of the XPath:
65
+ segments.forEach(segment => {
66
+ // If the segment is html[1] or body[1]:
67
+ if (/html\[1\]|body\[1\]/.test(segment)) {
68
+ // Add it without its subscript to the array.
69
+ normalizedSegments.push(segment.replace(/\[1\]/, ''));
70
+ }
71
+ // Otherwise, if the segment is empty or html or body or ends with a subscript:
72
+ else if (segment === '' || ['html', 'body'].includes(segment) || segment.endsWith(']')) {
73
+ // Add it to the array.
74
+ normalizedSegments.push(segment);
75
+ }
76
+ // Otherwise, i.e. if the segment is a tag name with no subscript:
77
+ else {
78
+ // Add it with a subscript 1 to the array.
79
+ normalizedSegments.push(`${segment}[1]`);
80
+ }
81
+ });
82
+ // Return the concatenated segments as the normalized XPath.
83
+ return normalizedSegments.join('/');
84
+ }
85
+ else {
86
+ return '';
87
+ }
83
88
  };
84
89
  // Adds a box ID and a path ID to an object.
85
90
  const addIDs = async (locator, recipient) => {
package/procs/nu.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // Module to add Testaro IDs to elements.
18
18
  const {addTestaroIDs} = require('./testaro');
19
19
  // Module to get location data from an element.
20
- const {getLocationData} = require('./getLocatorData');
20
+ const {getElementData} = require('./getLocatorData');
21
21
  // Module to get the document source.
22
22
  const {getSource} = require('./getSource');
23
23
 
@@ -94,7 +94,7 @@ exports.curate = async (page, data, nuData, rules) => {
94
94
  for (const message of result.messages) {
95
95
  const {extract} = message;
96
96
  // Add location data for the element to the message.
97
- message.elementLocation = await getLocationData(page, extract);
97
+ message.elementLocation = await getElementData(page, extract);
98
98
  }
99
99
  }
100
100
  // Return the result.
@@ -16,7 +16,6 @@
16
16
  // IMPORTS
17
17
 
18
18
  const {cap} = require('./job');
19
- const {getNormalizedXPath} = require('./identify');
20
19
 
21
20
  // FUNCTIONS
22
21
 
@@ -41,103 +40,6 @@ const getIdentifiers = exports.getIdentifiers = code => {
41
40
  }
42
41
  return ['', ''];
43
42
  };
44
- /*
45
- Differentiates some rule IDs of aslint.
46
- If the purported rule ID is a key and the what property contains all of the strings except the
47
- last of any array item of the value of that key, then the final rule ID is the last item of that
48
- array item.
49
- */
50
- const aslintData = {
51
- 'misused_required_attribute': [
52
- ['not needed', 'misused_required_attributeR']
53
- ],
54
- 'accessible_svg': [
55
- ['associated', 'accessible_svgI'],
56
- ['tabindex', 'accessible_svgT']
57
- ],
58
- 'audio_alternative': [
59
- ['track', 'audio_alternativeT'],
60
- ['alternative', 'audio_alternativeA'],
61
- ['bgsound', 'audio_alternativeB']
62
- ],
63
- 'table_missing_description': [
64
- ['describedby', 'associated', 'table_missing_descriptionDM'],
65
- ['labeledby', 'associated', 'table_missing_descriptionLM'],
66
- ['caption', 'not been defined', 'table_missing_descriptionC'],
67
- ['summary', 'empty', 'table_missing_descriptionS'],
68
- ['describedby', 'empty', 'table_missing_descriptionDE'],
69
- ['labeledby', 'empty', 'table_missing_descriptionLE'],
70
- ['caption', 'no content', 'table_missing_descriptionE']
71
- ],
72
- 'label_implicitly_associated': [
73
- ['only whice spaces', 'label_implicitly_associatedW'],
74
- ['more than one', 'label_implicitly_associatedM']
75
- ],
76
- 'label_inappropriate_association': [
77
- ['Missing', 'label_inappropriate_associationM'],
78
- ['non-form', 'label_inappropriate_associationN']
79
- ],
80
- 'table_row_and_column_headers': [
81
- ['headers', 'table_row_and_column_headersRC'],
82
- ['Content', 'table_row_and_column_headersB'],
83
- ['head of the columns', 'table_row_and_column_headersH']
84
- ],
85
- 'color_contrast_state_pseudo_classes_abstract': [
86
- ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
87
- ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
88
- ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
89
- ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
90
- ],
91
- 'color_contrast_state_pseudo_classes_active': [
92
- ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
93
- ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
94
- ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
95
- ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
96
- ],
97
- 'color_contrast_state_pseudo_classes_focus': [
98
- ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
99
- ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
100
- ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
101
- ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
102
- ],
103
- 'color_contrast_state_pseudo_classes_hover': [
104
- ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
105
- ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
106
- ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
107
- ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
108
- ],
109
- 'color_contrast_aaa': [
110
- ['transparent', 'color_contrast_aaaB'],
111
- ['least 4.5:1', 'color_contrast_aaa4'],
112
- ['least 7:1', 'color_contrast_aaa7']
113
- ],
114
- 'animation': [
115
- ['duration', 'animationD'],
116
- ['iteration', 'animationI'],
117
- ['mechanism', 'animationM']
118
- ],
119
- 'page_title': [
120
- ['empty', 'page_titleN'],
121
- ['not identify', 'page_titleU']
122
- ],
123
- 'aria_labelledby_association': [
124
- ['exist', 'aria_labelledby_associationN'],
125
- ['empty', 'aria_labelledby_associationE']
126
- ],
127
- 'html_lang_attr': [
128
- ['parameters', 'html_lang_attrP'],
129
- ['nothing', 'html_lang_attrN'],
130
- ['empty', 'html_lang_attrE']
131
- ],
132
- 'missing_label': [
133
- ['associated', 'missing_labelI'],
134
- ['defined', 'missing_labelN'],
135
- ['multiple labels', 'missing_labelM']
136
- ],
137
- 'orientation': [
138
- ['loaded', 'orientationT']
139
- ]
140
- };
141
43
  // Converts issue instances at an axe certainty level.
142
44
  const doAxe = (result, standardResult, certainty) => {
143
45
  if (result.details && result.details[certainty]) {
@@ -379,95 +281,14 @@ const convert = (toolName, data, result, standardResult) => {
379
281
  // Add that to the standard result and disregard tool-specific conversions.
380
282
  standardResult.prevented = true;
381
283
  }
382
- // alfa
383
- else if (toolName === 'alfa' && result.standardResult) {
284
+ // alfa, aslint
285
+ else if (['alfa', 'aslint'].includes(toolName) && result.standardResult) {
384
286
  // Move the results to standard locations.
385
287
  Object.assign(result, result.nativeResult);
386
288
  Object.assign(standardResult, result.standardResult);
387
289
  delete result.nativeResult;
388
290
  delete result.standardResult;
389
291
  }
390
- // aslint
391
- else if (toolName === 'aslint' && result.summary && result.summary.byIssueType) {
392
- // For each rule:
393
- Object.keys(result.rules).forEach(ruleID => {
394
- // If it has a valid issue type:
395
- const {issueType} = result.rules[ruleID];
396
- if (issueType && ['warning', 'error'].includes(issueType)) {
397
- // If there are any violations:
398
- const ruleResults = result.rules[ruleID].results;
399
- if (ruleResults && ruleResults.length) {
400
- // For each violation:
401
- ruleResults.forEach(ruleResult => {
402
- // If it has a description:
403
- if (
404
- ruleResult.message
405
- && ruleResult.message.actual
406
- && ruleResult.message.actual.description
407
- ) {
408
- const what = ruleResult.message.actual.description;
409
- // Get the differentiated ID of the rule if any.
410
- const ruleData = aslintData[ruleID];
411
- let finalRuleID = ruleID;
412
- if (ruleData) {
413
- const changer = ruleData.find(
414
- specs => specs.slice(0, -1).every(matcher => what.includes(matcher))
415
- );
416
- if (changer) {
417
- finalRuleID = changer[changer.length - 1];
418
- }
419
- }
420
- // Initialize the path ID of the violating element as any normalized reported XPath.
421
- let pathID = getNormalizedXPath(ruleResult.element && ruleResult.element.xpath) || '';
422
- const {locationData} = ruleResult;
423
- // If an XPath was obtained from the excerpt:
424
- if (locationData && locationData.pathID) {
425
- // Replace the path ID with it, because some ASLint-reported XPaths are abbreviated.
426
- ({pathID} = locationData);
427
- }
428
- // Get and normalize the reported excerpt.
429
- const excerpt = ruleResult.element
430
- && ruleResult.element.html
431
- && ruleResult.element.html.replace(/\s+/g, ' ')
432
- || '';
433
- // Get the tag name from the XPath, if possible.
434
- let tagName = pathID && pathID.replace(/[^-\w].*$/, '').toUpperCase() || '';
435
- if (! tagName && finalRuleID.endsWith('_svg')) {
436
- tagName = 'SVG';
437
- }
438
- // If that was impossible but there is a tag name in the excerpt:
439
- if (! tagName && /^<[a-z]+[ >]/.test(excerpt)) {
440
- // Get it.
441
- tagName = excerpt.slice(1).replace(/[ >].+/, '').toUpperCase();
442
- }
443
- // Get the ID, if any.
444
- const idDraft = excerpt && excerpt.replace(/^[^[>]+id="/, 'id=').replace(/".*$/, '');
445
- const idFinal = idDraft && idDraft.length > 3 && idDraft.startsWith('id=')
446
- ? idDraft.slice(3)
447
- : '';
448
- const id = idFinal === '' || isBadID(idFinal) ? '' : idFinal;
449
- const instance = {
450
- ruleID: finalRuleID,
451
- what,
452
- ordinalSeverity: ['warning', 0, 0, 'error'].indexOf(issueType),
453
- tagName,
454
- id,
455
- location: {
456
- doc: 'dom',
457
- type: 'xpath',
458
- spec: pathID
459
- },
460
- excerpt,
461
- boxID: '',
462
- pathID
463
- };
464
- standardResult.instances.push(instance);
465
- }
466
- });
467
- }
468
- }
469
- });
470
- }
471
292
  // axe
472
293
  else if (
473
294
  toolName === 'axe'
package/run.js CHANGED
@@ -1871,8 +1871,8 @@ const doActs = async (report, opts = {}) => {
1871
1871
  // Increment the path ID count.
1872
1872
  actIDData.pathIDCount++;
1873
1873
  }
1874
- // If the instance has a text:
1875
- if (text) {
1874
+ // If the instance has any text segments:
1875
+ if (text?.[0]?.length) {
1876
1876
  // Increment the text count.
1877
1877
  actIDData.textCount++;
1878
1878
  }
package/testaro/motion.js CHANGED
@@ -19,7 +19,7 @@ const fs = require('fs/promises');
19
19
  // Module to get operating-system properties.
20
20
  const os = require('os');
21
21
  // Module to compare screenshots.
22
- const pixelmatch = require('pixelmatch').default;
22
+ const blazediff = require('@blazediff/core').diff;
23
23
  // Module to parse PNGs.
24
24
  const {PNG} = require('pngjs');
25
25
 
@@ -60,7 +60,7 @@ exports.reporter = async page => {
60
60
  // Otherwise, i.e. if their dimensions are identical:
61
61
  else {
62
62
  // Get the count of differing pixels between the shots.
63
- const pixelChanges = pixelmatch(shoot0PNG.data, shoot1PNG.data, null, width, height);
63
+ const pixelChanges = blazediff(shoot0PNG.data, shoot1PNG.data, null, width, height);
64
64
  // Get the ratio of differing to all pixels as a percentage.
65
65
  const changePercent = Math.round(100 * pixelChanges / (width * height));
66
66
  // Free the memory used by screenshots.
package/tests/alfa.js CHANGED
@@ -121,9 +121,18 @@ exports.reporter = async (page, report, actIndex) => {
121
121
  }
122
122
  }
123
123
  catch(error) {}
124
- let text = '';
124
+ const text = [];
125
125
  try {
126
- text = await targetLoc.innerText({timeout: 50});
126
+ const textRaw = await targetLoc.innerText({timeout: 50});
127
+ const segments = textRaw?.trim().split(/[\t\n]+/).filter(segment => segment.length);
128
+ if (segments?.length) {
129
+ if (segments.length > 1) {
130
+ text.push(segments[0], segments[segments.length - 1]);
131
+ }
132
+ else {
133
+ text.push(segments[0]);
134
+ }
135
+ }
127
136
  }
128
137
  catch(error) {}
129
138
  // Get rule-specific properties of a standard instance.
package/tests/aslint.js CHANGED
@@ -17,20 +17,138 @@
17
17
 
18
18
  // Function to add unique identifiers to the elements in the page.
19
19
  const {addTestaroIDs} = require('../procs/testaro');
20
+ // Module to simplify strings.
21
+ const {cap, tidy} = require('../procs/job');
20
22
  // Module to handle files.
21
23
  const fs = require('fs/promises');
22
- // Function to get location data from an element.
23
- const {getLocationData} = require('../procs/getLocatorData');
24
+ // Function to get location data with a Testaro identifier.
25
+ const {getElementData} = require('../procs/getLocatorData');
26
+ // Function to normalize an XPath.
27
+ const {getNormalizedXPath} = require('../procs/identify');
28
+
29
+ // CONSTANTS
30
+
31
+ /*
32
+ Differentiates some rule IDs of aslint.
33
+ If the purported rule ID is a key and the what property contains all of the strings except the
34
+ last of any array item of the value of that key, then the final rule ID is the last item of that
35
+ array item.
36
+ */
37
+ const aslintData = {
38
+ 'misused_required_attribute': [
39
+ ['not needed', 'misused_required_attributeR']
40
+ ],
41
+ 'accessible_svg': [
42
+ ['associated', 'accessible_svgI'],
43
+ ['tabindex', 'accessible_svgT']
44
+ ],
45
+ 'audio_alternative': [
46
+ ['track', 'audio_alternativeT'],
47
+ ['alternative', 'audio_alternativeA'],
48
+ ['bgsound', 'audio_alternativeB']
49
+ ],
50
+ 'table_missing_description': [
51
+ ['describedby', 'associated', 'table_missing_descriptionDM'],
52
+ ['labeledby', 'associated', 'table_missing_descriptionLM'],
53
+ ['caption', 'not been defined', 'table_missing_descriptionC'],
54
+ ['summary', 'empty', 'table_missing_descriptionS'],
55
+ ['describedby', 'empty', 'table_missing_descriptionDE'],
56
+ ['labeledby', 'empty', 'table_missing_descriptionLE'],
57
+ ['caption', 'no content', 'table_missing_descriptionE']
58
+ ],
59
+ 'label_implicitly_associated': [
60
+ ['only whice spaces', 'label_implicitly_associatedW'],
61
+ ['more than one', 'label_implicitly_associatedM']
62
+ ],
63
+ 'label_inappropriate_association': [
64
+ ['Missing', 'label_inappropriate_associationM'],
65
+ ['non-form', 'label_inappropriate_associationN']
66
+ ],
67
+ 'table_row_and_column_headers': [
68
+ ['headers', 'table_row_and_column_headersRC'],
69
+ ['Content', 'table_row_and_column_headersB'],
70
+ ['head of the columns', 'table_row_and_column_headersH']
71
+ ],
72
+ 'color_contrast_state_pseudo_classes_abstract': [
73
+ ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
74
+ ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
75
+ ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
76
+ ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
77
+ ],
78
+ 'color_contrast_state_pseudo_classes_active': [
79
+ ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
80
+ ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
81
+ ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
82
+ ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
83
+ ],
84
+ 'color_contrast_state_pseudo_classes_focus': [
85
+ ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
86
+ ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
87
+ ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
88
+ ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
89
+ ],
90
+ 'color_contrast_state_pseudo_classes_hover': [
91
+ ['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
92
+ ['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
93
+ ['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
94
+ ['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
95
+ ],
96
+ 'color_contrast_aaa': [
97
+ ['transparent', 'color_contrast_aaaB'],
98
+ ['least 4.5:1', 'color_contrast_aaa4'],
99
+ ['least 7:1', 'color_contrast_aaa7']
100
+ ],
101
+ 'animation': [
102
+ ['duration', 'animationD'],
103
+ ['iteration', 'animationI'],
104
+ ['mechanism', 'animationM']
105
+ ],
106
+ 'page_title': [
107
+ ['empty', 'page_titleN'],
108
+ ['not identify', 'page_titleU']
109
+ ],
110
+ 'aria_labelledby_association': [
111
+ ['exist', 'aria_labelledby_associationN'],
112
+ ['empty', 'aria_labelledby_associationE']
113
+ ],
114
+ 'html_lang_attr': [
115
+ ['parameters', 'html_lang_attrP'],
116
+ ['nothing', 'html_lang_attrN'],
117
+ ['empty', 'html_lang_attrE']
118
+ ],
119
+ 'missing_label': [
120
+ ['associated', 'missing_labelI'],
121
+ ['defined', 'missing_labelN'],
122
+ ['multiple labels', 'missing_labelM']
123
+ ],
124
+ 'orientation': [
125
+ ['loaded', 'orientationT']
126
+ ]
127
+ };
24
128
 
25
129
  // FUNCTIONS
26
130
 
27
131
  // Conducts and reports the ASLint tests.
28
- exports.reporter = async (page, report, actIndex, timeLimit) => {
29
- // Add unique identifiers to the elements in the page.
132
+ exports.reporter = async (page, report, actIndex) => {
133
+ // Add unique Testaro identifiers to the elements in the page.
30
134
  await addTestaroIDs(page);
31
135
  // Initialize the act report.
32
136
  let data = {};
33
- let result = {};
137
+ const result = {
138
+ nativeResult: {},
139
+ standardResult: {}
140
+ };
141
+ const standard = report.standard !== 'no';
142
+ // If standard results are to be reported:
143
+ if (standard) {
144
+ // Initialize the standard result.
145
+ result.standardResult = {
146
+ prevented: false,
147
+ totals: [0, 0, 0, 0],
148
+ instances: []
149
+ };
150
+ }
151
+ const {standardResult} = result;
34
152
  // Get the ASLint runner and bundle scripts.
35
153
  const aslintRunner = await fs.readFile(`${__dirname}/../procs/aslint.js`, 'utf8');
36
154
  const aslintBundlePath = require.resolve('aslint-testaro/aslint.bundle.js');
@@ -65,6 +183,9 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
65
183
  console.log(message);
66
184
  data.prevented = true;
67
185
  data.error = message;
186
+ if (standard) {
187
+ standardResult.prevented = true;
188
+ }
68
189
  });
69
190
  const reportLoc = page.locator('#aslintResult');
70
191
  // If the injection succeeded:
@@ -73,7 +194,7 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
73
194
  // Wait for the test results to be attached to the page.
74
195
  const waitOptions = {
75
196
  state: 'attached',
76
- timeout: 1000 * timeLimit
197
+ timeout: 20000
77
198
  };
78
199
  await reportLoc.waitFor(waitOptions);
79
200
  }
@@ -88,47 +209,102 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
88
209
  if (! data.prevented) {
89
210
  // Get their text.
90
211
  const actReport = await reportLoc.textContent();
91
- result = JSON.parse(actReport);
92
- // If any rules were reported violated:
93
- if (result.rules) {
94
- // For each such rule:
95
- for (const ruleID of Object.keys(result.rules)) {
96
- const excluded = act.rules && ! act.rules.includes(ruleID);
97
- const instanceType = result.rules[ruleID].status.type;
98
- // If rules to be tested were specified and exclude it or the rule was passed or skipped:
99
- if (excluded || ['passed', 'skipped'].includes(instanceType)) {
100
- // Delete the rule report.
101
- delete result.rules[ruleID];
212
+ try {
213
+ const nativeResult = result.nativeResult = JSON.parse(actReport);
214
+ // If any rules were reported violated:
215
+ if (nativeResult.rules) {
216
+ const {rules} = nativeResult;
217
+ // For each such rule:
218
+ for (const ruleID of Object.keys(rules)) {
219
+ const ruleData = rules[ruleID];
220
+ const {issueType, status} = ruleData;
221
+ const excluded = act.rules && ! act.rules.includes(ruleID);
222
+ const {type} = status;
223
+ // If rule is not an error or warning or is not to be tested:
224
+ if (
225
+ excluded
226
+ || ['passed', 'skipped'].includes(type)
227
+ || ! ['error', 'warning'].includes(issueType)
228
+ ) {
229
+ // Delete the rule report.
230
+ delete rules[ruleID];
231
+ }
102
232
  }
103
- // Otherwise, i.e. if the rule was violated:
104
- else {
105
- const ruleResults = result.rules[ruleID].results;
106
- // For each violation:
107
- for (const ruleResult of ruleResults) {
108
- const excerpt = ruleResult.element
109
- && ruleResult.element.html.replace(/\s+/g, ' ')
110
- || '';
111
- // If an element excerpt was reported:
112
- if (excerpt) {
113
- // Use it to add location data to the violation data in the result.
114
- ruleResult.locationData = await getLocationData(page, excerpt);
233
+ // If standard results are to be reported:
234
+ if (standard) {
235
+ const ruleIDs = Object.keys(rules);
236
+ // For each violated rule:
237
+ for (let ruleID of ruleIDs) {
238
+ const ruleData = rules[ruleID];
239
+ const {issueType, results} = ruleData;
240
+ // For each violation:
241
+ for (const result of results) {
242
+ const {message, element} = result;
243
+ const what = message?.actual?.description ?? '';
244
+ // Get the values of the properties required for a standard result.
245
+ if (ruleID) {
246
+ const changer = aslintData[ruleID]?.find(
247
+ specs => specs.slice(0, -1).every(matcher => what.includes(matcher))
248
+ );
249
+ if (changer) {
250
+ ruleID = changer[changer.length - 1];
251
+ }
252
+ }
253
+ const ordinalSeverity = issueType === 'warning' ? 1 : 2;
254
+ const {html, xpath} = element;
255
+ const excerpt = html?.replace(/\s+/g, ' ') ?? '';
256
+ let tagName = '';
257
+ let id = '';
258
+ let text = [];
259
+ let notInDOM = false;
260
+ let boxID = '';
261
+ let pathID = '';
262
+ if (excerpt) {
263
+ const elementData = await getElementData(page, excerpt);
264
+ ({tagName, id, text, notInDOM, boxID, pathID, originalExcerpt} = elementData);
265
+ }
266
+ if (! pathID) {
267
+ pathID = getNormalizedXPath(xpath);
268
+ }
269
+ if (pathID && ! tagName) {
270
+ tagName = pathID?.replace(/[^-\w].*$/, '').toUpperCase() ?? '';
271
+ }
272
+ if (ruleID.endsWith('_svg') && ! tagName) {
273
+ tagName = 'SVG';
274
+ }
275
+ // Add an instance to the standard result.
276
+ standardResult.instances.push({
277
+ ruleID,
278
+ what,
279
+ ordinalSeverity,
280
+ count: 1,
281
+ tagName,
282
+ id,
283
+ location: {
284
+ notInDOM,
285
+ doc: 'dom',
286
+ type: 'xpath',
287
+ spec: pathID
288
+ },
289
+ excerpt: cap(tidy(originalExcerpt)),
290
+ text,
291
+ boxID,
292
+ pathID
293
+ });
115
294
  }
116
- };
295
+ }
117
296
  }
118
- };
297
+ }
298
+ }
299
+ // If the results are not JSON:
300
+ catch(error) {
301
+ const message = `Result processing failed (${error.message})`;
302
+ console.log(`ERROR: ${message}`);
303
+ // Report this.
304
+ data.prevented = true;
305
+ data.error = message;
119
306
  }
120
307
  }
121
- // Return the act report.
122
- try {
123
- JSON.stringify(data);
124
- }
125
- catch(error) {
126
- const message = `ERROR: ASLint result cannot be made JSON (${error.message.slice(0, 200)})`;
127
- data = {
128
- prevented: true,
129
- error: message
130
- };
131
- };
132
308
  return {
133
309
  data,
134
310
  result
package/tests/htmlcs.js CHANGED
@@ -18,7 +18,7 @@
18
18
  // Module to add and use unique element IDs.
19
19
  const {addTestaroIDs} = require('../procs/testaro');
20
20
  // Module to get location data from an element.
21
- const {getLocationData} = require('../procs/getLocatorData');
21
+ const {getElementData} = require('../procs/getLocatorData');
22
22
  // Module to handle files.
23
23
  const fs = require('fs/promises');
24
24
 
@@ -102,7 +102,7 @@ exports.reporter = async (page, report, actIndex) => {
102
102
  const ruleID = parts[1].replace(/^WCAG2|\.Principle\d\.Guideline[\d_]+/g, '');
103
103
  result[parts[0]][ruleID] ??= {};
104
104
  result[parts[0]][ruleID][parts[4]] ??= [];
105
- const elementLocation = await getLocationData(page, parts[5]);
105
+ const elementLocation = await getElementData(page, parts[5]);
106
106
  const {boxID, notInDOM, pathID} = elementLocation;
107
107
  result[parts[0]][ruleID][parts[4]].push({
108
108
  tagName: parts[2],
package/tests/qualWeb.js CHANGED
@@ -20,7 +20,7 @@ const {ACTRules} = require('@qualweb/act-rules');
20
20
  const {WCAGTechniques} = require('@qualweb/wcag-techniques');
21
21
  const {BestPractices} = require('@qualweb/best-practices');
22
22
  const {addTestaroIDs} = require('../procs/testaro');
23
- const {getLocationData} = require('../procs/getLocatorData');
23
+ const {getElementData} = require('../procs/getLocatorData');
24
24
 
25
25
  // CONSTANTS
26
26
 
@@ -211,7 +211,7 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
211
211
  // For each violating element:
212
212
  for (const element of elements) {
213
213
  // Add location data from its excerpt to the element data.
214
- element.locationData = await getLocationData(page, element.htmlCode);
214
+ element.locationData = await getElementData(page, element.htmlCode);
215
215
  // Limit the size of its reported excerpt.
216
216
  if (element.htmlCode && element.htmlCode.length > 700) {
217
217
  element.htmlCode = `${element.htmlCode.slice(0, 700)} …`;
package/tests/wax.js CHANGED
@@ -18,7 +18,7 @@
18
18
  // Function to add and use unique element IDs.
19
19
  const {addTestaroIDs} = require('../procs/testaro');
20
20
  // Function to get location data from an element.
21
- const {getLocationData} = require('../procs/getLocatorData');
21
+ const {getElementData} = require('../procs/getLocatorData');
22
22
  // Modules to run WAX.
23
23
  const runWax = require('@wally-ax/wax-dev');
24
24
  const waxDev = {runWax};
@@ -63,7 +63,7 @@ exports.reporter = async (page, report, actIndex) => {
63
63
  // Add location data to its reported violations.
64
64
  for (const violation of actReport) {
65
65
  const {element} = violation;
66
- const elementLocation = await getLocationData(page, element);
66
+ const elementLocation = await getElementData(page, element);
67
67
  Object.assign(violation, elementLocation);
68
68
  }
69
69
  // Populate the act report.