testaro 38.0.2 → 39.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
@@ -40,6 +40,7 @@ One software product that performs some such functions is [Testilo](https://www.
40
40
 
41
41
  Testaro uses:
42
42
  - [Playwright](https://playwright.dev/) to launch browsers, perform user actions in them, and perform tests
43
+ - [plywright-dompath](https://www.npmjs.com/package/playwright-dompath) to retrieve XPaths of elements
43
44
  - [pixelmatch](https://www.npmjs.com/package/pixelmatch) to measure motion
44
45
 
45
46
  Testaro performs tests of these tools:
@@ -212,96 +213,79 @@ Testaro also generates some data about the job and adds those data to the job, i
212
213
 
213
214
  The tools listed above as dependencies write their tool reports in various formats. They differ in how they organize multiple instances of the same problem, how they classify severity and certainty, how they point to the locations of problems, how they name problems, etc.
214
215
 
215
- ##### Standard format
216
-
217
- Testaro helps overcome this format diversity by offering to represent the main facts in each tool report in a single standardized format.
218
-
219
- In the conceptual scheme underlying the format standardization of Testaro, each tool has its own set of _rules_, where a rule is an algorithm for evaluating a target and determining whether instances of some kind of problem exist in it. With standardization, Testaro reports, in a uniform way, the outcomes from the application of rules by tools to a target.
220
-
221
- Each job can specify how Testaro is to handle report standardization. A job can contain a `standard` property. If the value of that property is `'also'` or `'only'`, Testaro converts some data in each tool report to the standard format. That permits you to ignore the format idiosyncrasies of the tools. If `standard` has the value `'also'`, the job report includes both formats. If the value is `'only'`, or there is no value, the job report includes only the standard format. If the value is `'no'`, the job report includes only the original format of each tool report.
222
-
223
- The standard format of each tool report has these properties:
224
- - `prevented`: `true` if the tool failed to run on the page, or otherwise omitted.
225
- - `totals`: an array of 4 integers, representing the counts of problem instances classified by the tool into 4 ordinal degrees of severity. For example, `[2, 13, 0, 5]` would mean that the tool reported 2 instances at the lowest severity, 13 at the next-lowest, none at the third-lowest, and 5 at the highest.
226
- - `instances`: an array of objects describing facts about issue instances reported by the tool. This object has these properties, some of which have empty strings as values when the tool does not provide values:
227
- - `ruleID`: a code identifying a rule
228
- - `what`: a description of the rule
229
- - `count` (optional): the count of instances if this instance represents multiple instances
230
- - `ordinalSeverity`: how the tool ranks the severity of the instance, on a 4-point ordinal scale from 0 to 3
231
- - `tagName`: upper-case tagName of the affected element
232
- - `id`: value of the `id` property of that element
233
- - `location`: an object with three properties:
234
- - `doc`: whether the source (`source`) or the browser rendition (`dom`) was tested
235
- - `type`: the type of location information provided by the tool (`line`, `selector`, `xpath`, or `box`)
236
- - `spec`: the location information
237
- - `excerpt`: some or all of the code
238
-
239
- The most common location types reported by the tools are:
240
- - `line`: Nu Html Checker
241
- - `selector`: Axe, QualWeb, WAVE
242
- - `xpath`: Alfa, ASLint, Equal Access
243
- - `box`: Editoria11y, Testaro
244
- - none: HTML CodeSniffer
216
+ A Testaro report can include, for each tool, either or both of these properties:
217
+ - `result`: the result in the native tool format.
218
+ - `standardResult`: the result in a standard format identical for all tools.
245
219
 
246
- The original result of a test act is recorded as the value of a `result` property of the act. The standard-format result is recorded as the value of the `standardResult` property of the act. Its format is shown by this example:
220
+ ##### Standard result
247
221
 
248
- ``` javascript
249
- standardResult: {
250
- totals: [2, 0, 17, 0],
251
- instances: [
252
- {
253
- ruleID: 'rule01',
254
- what: 'Button type invalid',
255
- ordinalSeverity: 2,
256
- tagName: 'BUTTON'
257
- id: '',
258
- location: {
259
- doc: 'dom',
260
- type: 'box',
261
- spec: {
262
- x: 12,
263
- y: 340,
264
- width: 46,
265
- height: 50
266
- }
267
- },
268
- excerpt: '<button type="link"></button>'
269
- },
270
- {
271
- ruleID: 'rule01',
272
- what: 'Button type invalid',
273
- ordinalSeverity: 1,
274
- tagName: 'BUTTON',
275
- id: 'submitbutton',
276
- location: {
277
- doc: 'dom',
278
- type: 'line',
279
- spec: 145
280
- },
281
- excerpt: '<button type="important">Submit</button>'
282
- },
283
- {
284
- ruleID: 'rule02',
285
- what: 'Links have empty href attributes',
286
- count: 17,
287
- ordinalSeverity: 3,
288
- tagName: 'A',
289
- id: '',
290
- location: {
291
- doc: '',
292
- type: '',
293
- spec: ''
294
- },
295
- excerpt: ''
296
- }
297
- ]
222
+ ###### Properties
223
+
224
+ The standard result includes three properties:
225
+ - `prevented`: a boolean (`true` or `false`) value, stating whether the page prevented the tool from performing its tests.
226
+ - `totals`: an array of numbers representing how many instances of rule violations at each level of severity the tool reported. There are 4 ordinal severity levels. For example, the array `[3, 0, 14, 10]` would report that there were 3 violations at level 0, 0 at level 1, 14 at level 2, and 10 at level 3.
227
+ - `instances`: an array of objects describing the rule violations. An instance can describe a single violation, usually by one element in the page, or can summarize multiple violations of the same rule.
228
+
229
+ ###### Instances
230
+
231
+ Here is an example of a standard instance:
232
+
233
+ ```javascript
234
+ {
235
+ ruleID: 'rule01',
236
+ what: 'Button type invalid',
237
+ ordinalSeverity: 2,
238
+ count: 1,
239
+ tagName: 'BUTTON'
240
+ id: '',
241
+ location: {
242
+ doc: 'dom',
243
+ type: 'xpath',
244
+ spec: '/html[1]/body[1]/section[1]/div[1]/div[1]/ul[1]/li[1]/a[1]'
245
+ },
246
+ excerpt: '<button type="link"></button>',
247
+ boxID: '12:340:46:50',
248
+ pathID: '/html[1]/body[1]/section[1]/div[1]/div[1]/ul[1]/li[1]/a[1]'
298
249
  }
299
250
  ```
300
251
 
301
- If a tool has the option to be used without itemization and is being so used, the `instances` array may be empty, or, as shown above, may contain one or more summary instances. Summary instances disclose the numbers of instances that they summarize with a `count` property.
252
+ This instance describes a violation of a rule named `rule01` by a `button` element.
253
+
254
+ 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:
255
+ - `line` (line number in the code of the page): Nu Html Checker
256
+ - `selector` (CSS selector): Axe, QualWeb, WAVE
257
+ - `xpath`: Alfa, ASLint, Equal Access
258
+ - `box` (coordinates, width, and height of the element box): Editoria11y, Testaro
259
+ - none: HTML CodeSniffer
260
+ The tool also reproduces an excerpt of the element code.
261
+
262
+ ###### Element identification
263
+
264
+ While the above properties can help you find the offending element, Testaro makes this easier by adding, where practical, two standard element identifiers to each standard instance:
265
+ - `boxID`: a compact representation of the x, y, width, and height of the element bounding box, if the element can be identified and is visible.
266
+ - `pathID`: the XPath of the element, if the element can be identified.
267
+
268
+ These standard identifiers can help you determine whether violations reported by different tools belong to the same element or different elements. The `boxID` property can also support the making of images of the violating elements.
269
+
270
+ Some tools limit the efficacy of the current algorithm for standard identifiers:
271
+ - HTML CodeSniffer does not report element locations, and the reported code excerpts exclude all text content.
272
+ - Nu Html Checker reports line and column boundaries of element start tags and truncates element text content in reported code excerpts.
273
+
274
+ Testing can change the pages being tested, and such changes can cause a particular element to change its physical or logical location. In such cases, an element may appear multiple times in a tool report with different `boxID` or `pathID` values, even though it is, for practical purposes, the same element.
275
+
276
+ ###### Standardization configuration
277
+
278
+ Each job can specify how Testaro is to handle report standardization. A job can contain a `standard` property, with one of the following values to determine which results the report will include:
279
+ - `'also'`: original and standard.
280
+ - `'only'`: standard only.
281
+ - `'no'`: original only.
282
+
283
+ If a tool has the option to be used without itemization and is being so used, the `instances` array may be empty, or may contain one or more summary instances. Summary instances disclose the numbers of instances that they summarize with the `count` property. They typically summarize violations by multiple elements, in which case their `id`, `location`, `excerpt`, `boxID`, and `pathID` properties will have empty values.
284
+
285
+ ###### Standardization opinionation
302
286
 
303
287
  This standard format reflects some judgments. For example:
304
- - The `ordinalSeverity` property of an instance may have required 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.
288
+ - 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.
305
289
  - 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”).
306
290
  - 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.
307
291
  - 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.
@@ -905,6 +889,12 @@ To deal with the above problems, you can:
905
889
 
906
890
  Some measures of these kinds are included in the scoring and reporting features of the Testilo package.
907
891
 
892
+ ### Tool malfunctions
893
+
894
+ Tools can become faulty. For example, Alfa stopped reporting any rule violations in mid-April 2024 and resumed doing so at the end of April. In some cases, such as this, the tool maker corrects the fault. In others, the tool changes and forces Testaro to change its handling of the tool.
895
+
896
+ Testaro would become more reliable if the behavior of its tools were monitored for suspect changes.
897
+
908
898
  ## Repository exclusions
909
899
 
910
900
  The files in the `temp` directory are presumed ephemeral and are not tracked by `git`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "38.0.2",
3
+ "version": "39.0.0",
4
4
  "description": "Run 1000 web accessibility tests from 10 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -37,7 +37,8 @@
37
37
  "axe-playwright": "*",
38
38
  "dotenv": "*",
39
39
  "pixelmatch": "*",
40
- "playwright": "*"
40
+ "playwright": "*",
41
+ "playwright-dompath": "*"
41
42
  },
42
43
  "devDependencies": {
43
44
  "eslint": "*"
@@ -0,0 +1,176 @@
1
+ /*
2
+ © 2024 CVS Health and/or one of its affiliates. All rights reserved.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ */
22
+
23
+ /*
24
+ identify.js
25
+ Identifies the element of a standard instance.
26
+ */
27
+
28
+ // IMPORTS
29
+
30
+ // Module to get the XPath of an element.
31
+ const {xPath} = require('playwright-dompath');
32
+
33
+ // FUNCTIONS
34
+
35
+ // Returns the bounding box of a locator.
36
+ const boxOf = exports.boxOf = async locator => {
37
+ try {
38
+ const isVisible = await locator.isVisible();
39
+ if (isVisible) {
40
+ const box = await locator.boundingBox({
41
+ timeout: 50
42
+ });
43
+ if (box) {
44
+ Object.keys(box).forEach(dim => {
45
+ box[dim] = Math.round(box[dim], 0);
46
+ });
47
+ return box;
48
+ }
49
+ else {
50
+ return null;
51
+ }
52
+ }
53
+ else {
54
+ return null;
55
+ }
56
+ }
57
+ catch(error) {
58
+ return null;
59
+ }
60
+ }
61
+ // Returns a string representation of a bounding box.
62
+ const boxToString = exports.boxToString = box => {
63
+ if (box) {
64
+ return ['x', 'y', 'width', 'height'].map(dim => box[dim]).join(':');
65
+ }
66
+ else {
67
+ return '';
68
+ }
69
+ };
70
+ // Adds a box ID and a path ID to an object.
71
+ const addIDs = async (locators, recipient) => {
72
+ // If there is exactly 1 of them:
73
+ const locatorCount = await locators.count();
74
+ if (locatorCount === 1) {
75
+ // Add the box ID of the element to the result if none exists yet.
76
+ if (! recipient.boxID) {
77
+ const box = await boxOf(locators);
78
+ recipient.boxID = boxToString(box);
79
+ }
80
+ // Add the path ID of the element to the result if none exists yet.
81
+ if (! recipient.pathID) {
82
+ recipient.pathID = await xPath(locators);
83
+ }
84
+ }
85
+ };
86
+ // Returns the XPath and box ID of the element of a standard instance.
87
+ exports.identify = async (instance, page) => {
88
+ // If the instance does not yet have both boxID and pathID properties:
89
+ if (['boxID', 'pathID'].some(key => instance[key] === undefined)) {
90
+ // Initialize a result.
91
+ const elementID = {
92
+ boxID: '',
93
+ pathID: ''
94
+ };
95
+ const {excerpt, id, location, tagName} = instance;
96
+ const {type, spec} = location;
97
+ // If the instance specifies a CSS selector or XPath location:
98
+ if (['selector', 'xpath'].includes(type)) {
99
+ // Get a locator of the element.
100
+ let specifier = spec;
101
+ if (type === 'xpath') {
102
+ specifier = spec.replace(/\/text\(\)\[\d+\]$/, '');
103
+ }
104
+ if (specifier) {
105
+ if (type === 'xpath') {
106
+ specifier = `xpath=${specifier}`;
107
+ }
108
+ console.log(`Specifier is ${specifier}`);
109
+ try {
110
+ const locators = page.locator(specifier);
111
+ const locatorCount = await locators.count();
112
+ console.log(`Locator count is ${locatorCount}`);
113
+ // If the count of matching elements is 1:
114
+ if (locatorCount === 1) {
115
+ // Add a box ID and a path ID to the result.
116
+ await addIDs(locators, elementID);
117
+ }
118
+ // Otherwise, if the count is not 1 and the instance specifies an XPath location:
119
+ else if (type === 'xpath') {
120
+ // Use the XPath location as the path ID.
121
+ elementID.pathID = spec;
122
+ }
123
+ }
124
+ catch(error) {
125
+ console.log(`ERROR locating element by CSS selector or XPath (${error.message})`);
126
+ }
127
+ }
128
+ }
129
+ // If either ID remains undefined and the instance specifies an element ID:
130
+ if (id && ! (elementID.boxID && elementID.pathID)) {
131
+ // Get the first of the locators for elements with the ID.
132
+ try {
133
+ let locator = page.locator(`#${id.replace(/([-&;/]|^\d)/g, '\\$1')}`).first();
134
+ // Add a box ID and a path ID to the result.
135
+ await addIDs(locator, elementID);
136
+ }
137
+ catch(error) {
138
+ console.log(`ERROR locating element by ID (${error.message})`);
139
+ }
140
+ }
141
+ // If either ID remains undefined and the instance specifies a tag name:
142
+ if (tagName && ! (elementID.boxID && elementID.pathID)) {
143
+ // Get locators for elements with the tag name.
144
+ let locators = page.locator(tagName.toLowerCase());
145
+ // If there is exactly 1 of them:
146
+ let locatorCount = await locators.count();
147
+ if (locatorCount === 1) {
148
+ // Add a box ID and a path ID to the result.
149
+ await addIDs(locators, elementID);
150
+ }
151
+ // If either ID remains undefined an the instance also specifies an excerpt:
152
+ if (excerpt && ! (elementID.boxID && elementID.pathID)) {
153
+ // Get the plain text parts of the excerpt, converting ... to an empty string.
154
+ const minTagExcerpt = excerpt.replace(/<[^>]+>/g, '<>');
155
+ const plainParts = (minTagExcerpt.match(/[^<>]+/g) || [])
156
+ .map(part => part === '...' ? '' : part);
157
+ // Get the longest of them.
158
+ const sortedPlainParts = plainParts.sort((a, b) => b.length - a.length);
159
+ const mainPart = sortedPlainParts.length ? sortedPlainParts[0] : '';
160
+ // If there is one:
161
+ if (mainPart.trim().replace(/\s+/g, '').length) {
162
+ // Get locators for elements with the tag name and the text.
163
+ const locators = page.locator(tagName.toLowerCase(), {hasText: mainPart});
164
+ // If there is exactly 1 of them:
165
+ const locatorCount = await locators.count();
166
+ if (locatorCount === 1) {
167
+ // Add a box ID and a path ID to the result.
168
+ await addIDs(locators, elementID);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ // Return the result (not yet getting IDs from Nu Html Checker lines and columns).
174
+ return elementID;
175
+ }
176
+ };
@@ -30,8 +30,8 @@
30
30
  // Limits the length of and unilinearizes a string.
31
31
  const cap = rawString => {
32
32
  const string = (rawString || '').replace(/[\s\u2028\u2029]+/g, ' ');
33
- if (string && string.length > 400) {
34
- return `${string.slice(0, 200)} ... ${string.slice(-200)}`;
33
+ if (string && string.length > 600) {
34
+ return `${string.slice(0, 300)} ${string.slice(-300)}`;
35
35
  }
36
36
  else if (string) {
37
37
  return string;
@@ -492,12 +492,13 @@ const convert = (toolName, data, result, standardResult) => {
492
492
  else if (
493
493
  toolName === 'ed11y'
494
494
  && result
495
- && ['results', 'errorCount', 'warningCount'].every(key => result[key] !== undefined)
495
+ && ['imageAlts', 'violations', 'errorCount', 'warningCount']
496
+ .every(key => result[key] !== undefined)
496
497
  ) {
497
- // For each result:
498
- result.results.forEach(result => {
499
- const {test, content, tagName, id, loc, excerpt} = result;
500
- if (['test', 'content'].every(key => result[key])) {
498
+ // For each violation:
499
+ result.violations.forEach(violation => {
500
+ const {test, content, tagName, id, loc, excerpt, boxID, pathID} = violation;
501
+ if (['test', 'content'].every(key => key)) {
501
502
  // Standardize the what property.
502
503
  let what = '';
503
504
  if (content.includes('<p>This')) {
@@ -518,7 +519,9 @@ const convert = (toolName, data, result, standardResult) => {
518
519
  type: 'box',
519
520
  spec: loc
520
521
  },
521
- excerpt
522
+ excerpt,
523
+ boxID,
524
+ pathID
522
525
  });
523
526
  }
524
527
  });
package/procs/testaro.js CHANGED
@@ -31,6 +31,10 @@
31
31
  const {getSample} = require('../procs/sample');
32
32
  // Module to get locator data.
33
33
  const {getLocatorData} = require('../procs/getLocatorData');
34
+ // Module to get element IDs.
35
+ const {boxOf, boxToString} = require('./identify');
36
+ // Module to get the XPath of an element.
37
+ const {xPath} = require('playwright-dompath');
34
38
 
35
39
  // ########## FUNCTIONS
36
40
 
@@ -80,6 +84,9 @@ const report = exports.report = async (withItems, all, ruleID, whats, ordinalSev
80
84
  totals[ordinalSeverity] += data.populationRatio;
81
85
  // If itemization is required:
82
86
  if (withItems) {
87
+ // Get the bounding box of the element.
88
+ const {location} = elData;
89
+ const box = location.type === 'box' ? location.spec : await boxOf(loc);
83
90
  // Add a standard instance to the result.
84
91
  standardInstances.push({
85
92
  ruleID,
@@ -88,7 +95,9 @@ const report = exports.report = async (withItems, all, ruleID, whats, ordinalSev
88
95
  tagName: elData.tagName,
89
96
  id: elData.id,
90
97
  location: elData.location,
91
- excerpt: elData.excerpt
98
+ excerpt: elData.excerpt,
99
+ boxID: boxToString(box),
100
+ pathID: await xPath(loc)
92
101
  });
93
102
  }
94
103
  }
@@ -107,7 +116,9 @@ const report = exports.report = async (withItems, all, ruleID, whats, ordinalSev
107
116
  type: '',
108
117
  spec: ''
109
118
  },
110
- excerpt: ''
119
+ excerpt: '',
120
+ boxID: '',
121
+ pathID: ''
111
122
  });
112
123
  }
113
124
  // Return the result.
package/run.js CHANGED
@@ -33,6 +33,8 @@ require('dotenv').config();
33
33
  const {actSpecs} = require('./actSpecs');
34
34
  // Module to standardize report formats.
35
35
  const {standardize} = require('./procs/standardize');
36
+ // Module to identify element bounding boxes.
37
+ const {identify} = require('./procs/identify');
36
38
  // Module to send a notice to an observer.
37
39
  const {tellServer} = require('./procs/tellServer');
38
40
 
@@ -1118,6 +1120,16 @@ const doActs = async (report, actIndex, page) => {
1118
1120
  };
1119
1121
  // Populate it.
1120
1122
  standardize(act);
1123
+ // Add a box ID and a path ID to each of its standard instances if missing.
1124
+ for (const instance of act.standardResult.instances) {
1125
+ const elementID = await identify(instance, page);
1126
+ if (! instance.boxID) {
1127
+ instance.boxID = elementID ? elementID.boxID : '';
1128
+ }
1129
+ if (! instance.pathID) {
1130
+ instance.pathID = elementID ? elementID.pathID : '';
1131
+ }
1132
+ };
1121
1133
  // If the original-format result is not to be included in the report:
1122
1134
  if (standard === 'only') {
1123
1135
  // Remove it.
package/tests/alfa.js CHANGED
@@ -35,7 +35,8 @@ let alfaRules = require('@siteimprove/alfa-rules').default;
35
35
 
36
36
  // Conducts and reports the alfa tests.
37
37
  exports.reporter = async (page, options) => {
38
- const {rules} = options;
38
+ const {act} = options;
39
+ const {rules} = act;
39
40
  // If only some rules are to be employed:
40
41
  if (rules && rules.length) {
41
42
  // Remove the other rules.
package/tests/ed11y.js CHANGED
@@ -29,6 +29,8 @@
29
29
 
30
30
  // Module to handle files.
31
31
  const fs = require('fs/promises');
32
+ // Module to get the XPath of an element.
33
+ const {xPath} = require('playwright-dompath');
32
34
 
33
35
  // FUNCTIONS
34
36
 
@@ -38,21 +40,30 @@ exports.reporter = async (page, options) => {
38
40
  const {act, report} = options;
39
41
  const {jobData} = report;
40
42
  const scriptNonce = jobData && jobData.lastScriptNonce;
41
- // Get the test script.
43
+ // Get the tool script.
42
44
  const script = await fs.readFile(`${__dirname}/../ed11y/editoria11y.min.js`, 'utf8');
43
- const rawResultJSON = await page.evaluate(args => new Promise(async resolve => {
44
- // Impose a timeout on obtaining a result.
45
+ // Run the tests and get the violating elements and violation facts.
46
+ const reportJSHandle = await page.evaluateHandle(args => new Promise(async resolve => {
47
+ // If the report is incomplete after 20 seconds:
45
48
  const timer = setTimeout(() => {
46
- resolve(JSON.stringify({
47
- prevented: true,
48
- error: 'ed11y timed out'
49
- }));
49
+ // Return this as the report.
50
+ resolve({
51
+ facts: {
52
+ prevented: true,
53
+ error: 'ed11y timed out'
54
+ }
55
+ });
50
56
  }, 20000);
51
57
  const {scriptNonce, script, rulesToTest} = args;
52
- // When the script is executed:
58
+ // When the script has been executed, creating data in an Ed11y object:
53
59
  document.addEventListener('ed11yResults', () => {
54
- // Get the result.
55
- const resultObj = {};
60
+ // Initialize a report containing violating elements and violation facts.
61
+ const report = {
62
+ elements: [],
63
+ facts: {}
64
+ };
65
+ const {elements, facts} = report;
66
+ // Populate the global facts.
56
67
  [
57
68
  'version',
58
69
  'options',
@@ -64,42 +75,45 @@ exports.reporter = async (page, options) => {
64
75
  ]
65
76
  .forEach(key => {
66
77
  try {
67
- resultObj[key] = Ed11y[key];
78
+ facts[key] = Ed11y[key];
68
79
  }
69
80
  catch(error) {
70
81
  console.log(`ERROR: invalid value of ${key} property of Ed11y (${error.message})`);
71
82
  }
72
83
  });
73
- // Get data on the text alternatives of images from the result.
74
- resultObj.imageAlts = Ed11y
84
+ // Get data on violating text alternatives of images from Ed11y.
85
+ facts.imageAlts = Ed11y
75
86
  .imageAlts
76
87
  .filter(item => item[3] !== 'pass')
77
88
  .map(item => item.slice(1));
78
- // Delete useless properties from the result.
79
- delete resultObj.options.sleekTheme;
80
- delete resultObj.options.darkTheme;
81
- delete resultObj.options.lightTheme;
82
- // Initialize the element results.
83
- const results = resultObj.results = [];
84
- // For each rule violation:
85
- Ed11y.results.forEach(elResult => {
86
- // If rules were not selected or they were and include this one:
87
- if (! rulesToTest || rulesToTest.includes(elResult.test)) {
88
- // Create a violation record.
89
- const result = {};
90
- result.test = elResult.test || '';
91
- if (elResult.content) {
92
- result.content = elResult.content.replace(/\s+/g, ' ');
89
+ // Delete useless facts.
90
+ delete facts.options.sleekTheme;
91
+ delete facts.options.darkTheme;
92
+ delete facts.options.lightTheme;
93
+ // Initialize the violation facts.
94
+ facts.violations = [];
95
+ // For each rule violation by an element:
96
+ Ed11y.results.forEach(violation => {
97
+ // If rules were not selected or they were and include the violated rule:
98
+ if (! rulesToTest || rulesToTest.includes(violation.test)) {
99
+ const violationFacts = {};
100
+ violationFacts.test = violation.test || '';
101
+ // If the element is in the page:
102
+ if (violation.content) {
103
+ violationFacts.content = violation.content.replace(/\s+/g, ' ');
93
104
  }
94
- if (elResult.element) {
95
- const{element} = elResult;
96
- result.tagName = element.tagName || '';
97
- result.id = element.id || '';
98
- result.loc = {};
105
+ const {element} = violation;
106
+ if (element.outerHTML) {
107
+ // Add the element to the report.
108
+ elements.push(element);
109
+ // Add its violation facts to the report.
110
+ violationFacts.tagName = element.tagName || '';
111
+ violationFacts.id = element.id || '';
112
+ violationFacts.loc = {};
99
113
  const locRect = element.getBoundingClientRect();
100
114
  if (locRect) {
101
115
  ['x', 'y', 'width', 'height'].forEach(dim => {
102
- result.loc[dim] = Math.round(locRect[dim]);
116
+ violationFacts.loc[dim] = Math.round(locRect[dim], 0);
103
117
  });
104
118
  }
105
119
  let elText = element.textContent.replace(/\s+/g, ' ').trim();
@@ -109,59 +123,65 @@ exports.reporter = async (page, options) => {
109
123
  if (elText.length > 400) {
110
124
  elText = `${elText.slice(0, 200)}…${elText.slice(-200)}`;
111
125
  }
112
- result.excerpt = elText.replace(/\s+/g, ' ');
126
+ violationFacts.excerpt = elText.replace(/\s+/g, ' ');
127
+ violationFacts.boxID = ['x', 'y', 'width', 'height']
128
+ .map(dim => violationFacts.loc[dim])
129
+ .join(':');
130
+ facts.violations.push(violationFacts);
113
131
  }
114
- // Add it to the result.
115
- results.push(result);
116
132
  }
117
133
  });
118
- // Return the result.
119
- try {
120
- const resultJSON = JSON.stringify(resultObj);
121
- clearTimeout(timer);
122
- resolve(resultJSON);
123
- }
124
- catch(error) {
125
- clearTimeout(timer);
126
- resolve(JSON.stringify({
127
- prevented: true,
128
- error: `Result object not stringified (${error.message})`
129
- }));
130
- }
134
+ // Return the report.
135
+ clearTimeout(timer);
136
+ resolve(report);
131
137
  });
132
- // Add the test script to the page.
133
- const testScript = document.createElement('script');
138
+ // Add the tool script to the page.
139
+ const toolScript = document.createElement('script');
134
140
  if (scriptNonce) {
135
- testScript.nonce = scriptNonce;
136
- console.log(`Added nonce ${scriptNonce} to script`);
141
+ toolScript.nonce = scriptNonce;
142
+ console.log(`Added nonce ${scriptNonce} to tool script`);
137
143
  }
138
- testScript.textContent = script;
139
- document.body.insertAdjacentElement('beforeend', testScript);
140
- // Run the script.
144
+ toolScript.textContent = script;
145
+ document.body.insertAdjacentElement('beforeend', toolScript);
146
+ // Execute the tool script, creating Ed11y and triggering the event listener.
141
147
  try {
142
148
  await new Ed11y({
143
149
  alertMode: 'headless'
144
150
  });
145
151
  }
146
152
  catch(error) {
147
- resolve(JSON.stringify({
148
- prevented: true,
149
- error: error.message
150
- }));
153
+ resolve({
154
+ facts: {
155
+ prevented: true,
156
+ error: error.message
157
+ }
158
+ });
151
159
  };
152
160
  }), {scriptNonce, script, rulesToTest: act.rules});
153
- const result = JSON.parse(rawResultJSON);
154
- let data = {};
155
- if (result.prevented) {
156
- data.success = false;
157
- data.prevented = true;
158
- data.error = result.error;
159
- delete result.prevented;
160
- delete result.error;
161
+ // Add the violation facts to the result.
162
+ const factsJSHandle = await reportJSHandle.getProperty('facts');
163
+ const facts = await factsJSHandle.jsonValue();
164
+ const result = facts;
165
+ // If there were any violations:
166
+ const {violations} = facts;
167
+ if (violations && violations.length) {
168
+ // Get the violating elements.
169
+ const elementsJSHandle = await reportJSHandle.getProperty('elements');
170
+ const elementJSHandles = await elementsJSHandle.getProperties();
171
+ // For each violation:
172
+ for (const index in violations) {
173
+ // Get its path ID.
174
+ const elementHandle = elementJSHandles.get(index).asElement();
175
+ const pathID = await xPath(elementHandle);
176
+ // Add it to the violation facts.
177
+ violations[index].pathID = pathID;
178
+ };
161
179
  }
162
- // Return the act report.
180
+ // Return the report.
163
181
  return {
164
- data,
182
+ data: {
183
+ prevented: facts.prevented
184
+ },
165
185
  result
166
186
  };
167
187
  };
package/tests/nuVal.js CHANGED
@@ -22,7 +22,7 @@
22
22
 
23
23
  /*
24
24
  nuVal
25
- This test subjects a page and its source to the Nu Html Checker, thereby testing scripted
25
+ This tool subjects a page and its source to the Nu Html Checker, thereby testing scripted
26
26
  content found only in the loaded page and erroneous content before the browser corrects it.
27
27
  The API erratically replaces left and right double quotation marks with invalid UTF-8, which
28
28
  appears as 2 or 3 successive instances of the replacement character (U+fffd). Therefore, this