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 +73 -83
- package/package.json +3 -2
- package/procs/identify.js +176 -0
- package/procs/standardize.js +11 -8
- package/procs/testaro.js +13 -2
- package/run.js +12 -0
- package/tests/alfa.js +2 -1
- package/tests/ed11y.js +91 -71
- package/tests/nuVal.js +1 -1
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
+
##### Standard result
|
|
247
221
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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
|
+
};
|
package/procs/standardize.js
CHANGED
|
@@ -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 >
|
|
34
|
-
return `${string.slice(0,
|
|
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
|
-
&& ['
|
|
495
|
+
&& ['imageAlts', 'violations', 'errorCount', 'warningCount']
|
|
496
|
+
.every(key => result[key] !== undefined)
|
|
496
497
|
) {
|
|
497
|
-
// For each
|
|
498
|
-
result.
|
|
499
|
-
const {test, content, tagName, id, loc, excerpt} =
|
|
500
|
-
if (['test', 'content'].every(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 {
|
|
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
|
|
43
|
+
// Get the tool script.
|
|
42
44
|
const script = await fs.readFile(`${__dirname}/../ed11y/editoria11y.min.js`, 'utf8');
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
58
|
+
// When the script has been executed, creating data in an Ed11y object:
|
|
53
59
|
document.addEventListener('ed11yResults', () => {
|
|
54
|
-
//
|
|
55
|
-
const
|
|
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
|
-
|
|
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
|
|
74
|
-
|
|
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
|
|
79
|
-
delete
|
|
80
|
-
delete
|
|
81
|
-
delete
|
|
82
|
-
// Initialize the
|
|
83
|
-
|
|
84
|
-
// For each rule violation:
|
|
85
|
-
Ed11y.results.forEach(
|
|
86
|
-
// If rules were not selected or they were and include
|
|
87
|
-
if (! rulesToTest || rulesToTest.includes(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
133
|
-
const
|
|
138
|
+
// Add the tool script to the page.
|
|
139
|
+
const toolScript = document.createElement('script');
|
|
134
140
|
if (scriptNonce) {
|
|
135
|
-
|
|
136
|
-
console.log(`Added nonce ${scriptNonce} to script`);
|
|
141
|
+
toolScript.nonce = scriptNonce;
|
|
142
|
+
console.log(`Added nonce ${scriptNonce} to tool script`);
|
|
137
143
|
}
|
|
138
|
-
|
|
139
|
-
document.body.insertAdjacentElement('beforeend',
|
|
140
|
-
//
|
|
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(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
resolve({
|
|
154
|
+
facts: {
|
|
155
|
+
prevented: true,
|
|
156
|
+
error: error.message
|
|
157
|
+
}
|
|
158
|
+
});
|
|
151
159
|
};
|
|
152
160
|
}), {scriptNonce, script, rulesToTest: act.rules});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
|
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
|