testaro 5.7.2 → 5.7.6
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 +30 -24
- package/package.json +1 -1
- package/run.js +21 -4
- package/tests/hover.js +181 -141
- package/tests/nuVal.js +26 -7
package/README.md
CHANGED
|
@@ -93,6 +93,7 @@ A script is a JSON file with the properties:
|
|
|
93
93
|
|
|
94
94
|
```json
|
|
95
95
|
{
|
|
96
|
+
"id": "string consisting of lower-case ASCII letters and digits",
|
|
96
97
|
"what": "string: description of the script",
|
|
97
98
|
"strict": "boolean: whether redirections should be treated as failures",
|
|
98
99
|
"timeLimit": "number: limit in seconds on the execution of this script",
|
|
@@ -100,32 +101,33 @@ A script is a JSON file with the properties:
|
|
|
100
101
|
}
|
|
101
102
|
```
|
|
102
103
|
|
|
103
|
-
The `timeLimit` property is optional. If it is omitted, a default of 300 seconds (5 minutes) is set.
|
|
104
|
+
The `timeLimit` property is optional. If it is omitted, a default of 300 seconds (5 minutes) is set.
|
|
104
105
|
|
|
105
106
|
### Example
|
|
106
107
|
|
|
107
108
|
Here is an example of a script:
|
|
108
109
|
|
|
109
|
-
```
|
|
110
|
+
```json
|
|
110
111
|
{
|
|
111
|
-
|
|
112
|
+
"id": "samplescript",
|
|
113
|
+
what: "Test example.com with alfa",
|
|
112
114
|
strict: true,
|
|
113
|
-
timeLimit:
|
|
115
|
+
timeLimit: 65,
|
|
114
116
|
commands: [
|
|
115
117
|
{
|
|
116
|
-
type:
|
|
117
|
-
which:
|
|
118
|
-
what:
|
|
118
|
+
type: "launch",
|
|
119
|
+
which: "chromium",
|
|
120
|
+
what: "Chromium browser"
|
|
119
121
|
},
|
|
120
122
|
{
|
|
121
|
-
type:
|
|
122
|
-
which:
|
|
123
|
-
what:
|
|
123
|
+
type: "url",
|
|
124
|
+
which: "https://example.com/",
|
|
125
|
+
what: "page with a few accessibility defects"
|
|
124
126
|
},
|
|
125
127
|
{
|
|
126
|
-
type:
|
|
127
|
-
which:
|
|
128
|
-
what:
|
|
128
|
+
type: "test",
|
|
129
|
+
which: "alfa",
|
|
130
|
+
what: "Siteimprove alfa package"
|
|
129
131
|
}
|
|
130
132
|
]
|
|
131
133
|
}
|
|
@@ -302,6 +304,10 @@ In case you want to perform more than one `tenon` test with the same script, you
|
|
|
302
304
|
|
|
303
305
|
Tenon recommends giving it a public URL rather than giving it the content of a page, if possible. So, it is best to give the `withNewContent` property of the `tenonRequest` command the value `true`, unless the page is not public.
|
|
304
306
|
|
|
307
|
+
###### Continuum
|
|
308
|
+
|
|
309
|
+
The `continuum` test makes use of the files in the `continuum` directory. The test inserts the contents of all three files into the page as scripts and then uses them to perform the tests of the Continuum package.
|
|
310
|
+
|
|
305
311
|
###### HTML CodeSniffer
|
|
306
312
|
|
|
307
313
|
The `htmlcs` test makes use of the`htmlcs/HTMLCS.js` file. That file was created, and can be recreated if necessary, as follows:
|
|
@@ -336,7 +342,7 @@ The changes in `htmlcs/HTMLCS.js` are:
|
|
|
336
342
|
|
|
337
343
|
###### BBC Accessibility Standards Checker
|
|
338
344
|
|
|
339
|
-
The BBC Accessibility Standards Checker has obsolete dependencies with security vulnerabilities. Therefore, it is not used as a dependency of Testaro. Instead, 6 of its tests
|
|
345
|
+
The BBC Accessibility Standards Checker has obsolete dependencies with security vulnerabilities. Therefore, it is not used as a dependency of Testaro. Instead, 6 of its tests are reimplemented, in some case with revisions, as Testaro tests. They are drawn from the 18 automated tests of the Checker. The other 12 tests were found too duplicative of other tests to justify reimplementation.
|
|
340
346
|
|
|
341
347
|
##### Branching
|
|
342
348
|
|
|
@@ -436,18 +442,18 @@ A typical use for an `expect` property is checking the correctness of a Testaro
|
|
|
436
442
|
|
|
437
443
|
You may wish to have Testaro perform the same sequence of tests on multiple web pages. In that case, you can create a _batch_, with the following structure:
|
|
438
444
|
|
|
439
|
-
```
|
|
445
|
+
```json
|
|
440
446
|
{
|
|
441
|
-
what:
|
|
447
|
+
what: "Web leaders",
|
|
442
448
|
hosts: {
|
|
443
|
-
id:
|
|
444
|
-
which:
|
|
445
|
-
what:
|
|
449
|
+
id: "w3c",
|
|
450
|
+
which: "https://www.w3.org/",
|
|
451
|
+
what: "W3C"
|
|
446
452
|
},
|
|
447
453
|
{
|
|
448
|
-
id:
|
|
449
|
-
which:
|
|
450
|
-
what:
|
|
454
|
+
id: "wikimedia",
|
|
455
|
+
which: "https://www.wikimedia.org/",
|
|
456
|
+
what: "Wikimedia"
|
|
451
457
|
}
|
|
452
458
|
}
|
|
453
459
|
```
|
|
@@ -487,7 +493,7 @@ The argument of `require` is a path relative to the directory of the module in w
|
|
|
487
493
|
|
|
488
494
|
Another Node.js package that has Testaro as a dependency can execute the same statements, except changing `'./run'` to `'testaro/run'`.
|
|
489
495
|
|
|
490
|
-
Testaro will run the script and
|
|
496
|
+
Testaro will run the script and modify the properties of the `report` object. When Testaro finishes, the `log`, `acts`, and other properties of `report` will contain the results. The final statement can further process the `report` object as desired in the `then` callback.
|
|
491
497
|
|
|
492
498
|
#### High-level
|
|
493
499
|
|
|
@@ -500,7 +506,7 @@ Relative paths must be relative to the Testaro project directory. For example, i
|
|
|
500
506
|
|
|
501
507
|
Also ensure that Testaro can read all those directories and write to `REPORTDIR`.
|
|
502
508
|
|
|
503
|
-
Place a script into `SCRIPTDIR` and, optionally, a batch into `BATCHDIR`. Each should be named `
|
|
509
|
+
Place a script into `SCRIPTDIR` and, optionally, a batch into `BATCHDIR`. Each should be named with a `.json` extension., where `idvalue` is replaced with the value of its `id` property. That value must consist of only lower-case ASCII letters and digits.
|
|
504
510
|
|
|
505
511
|
Then execute the statement `node high scriptID` or `node high scriptID batchID`, replacing `scriptID` and `batchID` with the `id` values of the script and the batch, respectively.
|
|
506
512
|
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
2
|
+
run.js
|
|
3
3
|
testaro main script.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -103,8 +103,10 @@ let errorLogSize = 0;
|
|
|
103
103
|
let prohibitedCount = 0;
|
|
104
104
|
let visitTimeoutCount = 0;
|
|
105
105
|
let visitRejectionCount = 0;
|
|
106
|
+
let visitLatency = 0;
|
|
106
107
|
let actCount = 0;
|
|
107
108
|
// Facts about the current browser.
|
|
109
|
+
let browser;
|
|
108
110
|
let browserContext;
|
|
109
111
|
let browserTypeName;
|
|
110
112
|
let requestedURL = '';
|
|
@@ -256,12 +258,24 @@ const isValidReport = async report => {
|
|
|
256
258
|
|
|
257
259
|
// ########## OTHER FUNCTIONS
|
|
258
260
|
|
|
261
|
+
// Closes the current browser.
|
|
262
|
+
const browserClose = async () => {
|
|
263
|
+
if (browser) {
|
|
264
|
+
const contexts = browser.contexts();
|
|
265
|
+
for (const context of contexts) {
|
|
266
|
+
await context.close();
|
|
267
|
+
}
|
|
268
|
+
await browser.close();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
259
271
|
// Launches a browser.
|
|
260
272
|
const launch = async typeName => {
|
|
261
273
|
const browserType = require('playwright')[typeName];
|
|
262
274
|
// If the specified browser type exists:
|
|
263
275
|
if (browserType) {
|
|
264
|
-
//
|
|
276
|
+
// Close the current browser, if any.
|
|
277
|
+
await browserClose();
|
|
278
|
+
// Launch a browser of that type.
|
|
265
279
|
const browserOptions = {};
|
|
266
280
|
if (debug) {
|
|
267
281
|
browserOptions.headless = false;
|
|
@@ -270,7 +284,7 @@ const launch = async typeName => {
|
|
|
270
284
|
browserOptions.slowMo = waits;
|
|
271
285
|
}
|
|
272
286
|
let healthy = true;
|
|
273
|
-
|
|
287
|
+
browser = await browserType.launch(browserOptions)
|
|
274
288
|
.catch(error => {
|
|
275
289
|
healthy = false;
|
|
276
290
|
console.log(`ERROR launching browser: ${error.message.replace(/\n.+/s, '')}`);
|
|
@@ -511,6 +525,7 @@ const goto = async (page, url, timeout, waitUntil, isStrict) => {
|
|
|
511
525
|
url = url.replace('file://', `file://${__dirname}/`);
|
|
512
526
|
}
|
|
513
527
|
// Visit the URL.
|
|
528
|
+
const startTime = Date.now();
|
|
514
529
|
const response = await page.goto(url, {
|
|
515
530
|
timeout,
|
|
516
531
|
waitUntil
|
|
@@ -520,6 +535,7 @@ const goto = async (page, url, timeout, waitUntil, isStrict) => {
|
|
|
520
535
|
visitTimeoutCount++;
|
|
521
536
|
return 'error';
|
|
522
537
|
});
|
|
538
|
+
visitLatency += Math.round((Date.now() - startTime) / 1000);
|
|
523
539
|
// If the visit succeeded:
|
|
524
540
|
if (typeof response !== 'string') {
|
|
525
541
|
const httpStatus = response.status();
|
|
@@ -733,7 +749,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
733
749
|
// If the command is a url:
|
|
734
750
|
if (act.type === 'url') {
|
|
735
751
|
// Visit it and wait until it is stable.
|
|
736
|
-
page = await visit(act, page, report.
|
|
752
|
+
page = await visit(act, page, report.isStrict);
|
|
737
753
|
}
|
|
738
754
|
// Otherwise, if the act is a wait for text:
|
|
739
755
|
else if (act.type === 'wait') {
|
|
@@ -1335,6 +1351,7 @@ const doScript = async (report) => {
|
|
|
1335
1351
|
report.prohibitedCount = prohibitedCount;
|
|
1336
1352
|
report.visitTimeoutCount = visitTimeoutCount;
|
|
1337
1353
|
report.visitRejectionCount = visitRejectionCount;
|
|
1354
|
+
report.visitLatency = visitLatency;
|
|
1338
1355
|
// Add the end time and duration to the report.
|
|
1339
1356
|
const endTime = new Date();
|
|
1340
1357
|
report.endTime = endTime.toISOString().slice(0, 19);
|
package/tests/hover.js
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
value in the same location being the target.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
// VARIABLES
|
|
32
|
+
|
|
33
|
+
let hasTimedOut = false;
|
|
34
|
+
|
|
31
35
|
// FUNCTIONS
|
|
32
36
|
|
|
33
37
|
// Samples a population and returns the sample.
|
|
@@ -59,153 +63,172 @@ const textOf = async (element, limit) => {
|
|
|
59
63
|
};
|
|
60
64
|
// Recursively reports impacts of hovering over triggers.
|
|
61
65
|
const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
62
|
-
// If any potential triggers remain:
|
|
63
|
-
if (sample.length) {
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
},
|
|
82
|
-
firstTrigger
|
|
83
|
-
);
|
|
84
|
-
root = rootJSHandle.asElement();
|
|
85
|
-
}
|
|
86
|
-
// Identify all the descendants of the root.
|
|
87
|
-
const preDescendants = await root.$$(':visible');
|
|
88
|
-
// Identify their opacities.
|
|
89
|
-
const preOpacities = await page.evaluate(elements => elements.map(
|
|
90
|
-
element => window.getComputedStyle(element).opacity
|
|
91
|
-
), preDescendants);
|
|
92
|
-
try {
|
|
93
|
-
// Hover over the trigger.
|
|
94
|
-
await firstTrigger.hover({
|
|
95
|
-
timeout: 500,
|
|
96
|
-
noWaitAfter: true
|
|
97
|
-
});
|
|
98
|
-
// Repeatedly seeks impacts.
|
|
99
|
-
const getImpacts = async (interval, triesLeft) => {
|
|
100
|
-
// If the allowed trial count has not yet been exhausted:
|
|
101
|
-
if (triesLeft--) {
|
|
102
|
-
// Get the collection of descendants of the root.
|
|
103
|
-
const postDescendants = await root.$$(':visible');
|
|
104
|
-
// Identify the prior descandants of the root still in existence.
|
|
105
|
-
const remainerIndexes = await page.evaluate(args => {
|
|
106
|
-
const preDescendants = args[0];
|
|
107
|
-
const postDescendants = args[1];
|
|
108
|
-
const remainerIndexes = preDescendants
|
|
109
|
-
.map((element, index) => postDescendants.includes(element) ? index : -1)
|
|
110
|
-
.filter(index => index > -1);
|
|
111
|
-
return remainerIndexes;
|
|
112
|
-
}, [preDescendants, postDescendants]);
|
|
113
|
-
// Get the count of elements added by the hover event.
|
|
114
|
-
const additionCount = postDescendants.length - remainerIndexes.length;
|
|
115
|
-
const removalCount = preDescendants.length - remainerIndexes.length;
|
|
116
|
-
const remainers = [];
|
|
117
|
-
for (const index of remainerIndexes) {
|
|
118
|
-
remainers.push({
|
|
119
|
-
element: preDescendants[index],
|
|
120
|
-
preOpacity: preOpacities[index],
|
|
121
|
-
postOpacity: await page.evaluate(
|
|
122
|
-
element => window.getComputedStyle(element).opacity, preDescendants[index]
|
|
123
|
-
)
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
const opacityChangers = remainers
|
|
127
|
-
.filter(remainer => remainer.postOpacity !== remainer.preOpacity);
|
|
128
|
-
const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
|
|
129
|
-
(total, current) => total + current.element.querySelectorAll('*').length, 0
|
|
130
|
-
), opacityChangers) : 0;
|
|
131
|
-
if (additionCount || removalCount || opacityChangers.length) {
|
|
132
|
-
return {
|
|
133
|
-
additionCount,
|
|
134
|
-
removalCount,
|
|
135
|
-
opacityChangers,
|
|
136
|
-
opacityImpact
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
return await new Promise(resolve => {
|
|
141
|
-
setTimeout(() => {
|
|
142
|
-
resolve(getImpacts(interval, triesLeft));
|
|
143
|
-
}, interval);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
// Repeatedly seek impacts of the hover at intervals.
|
|
152
|
-
const impacts = await getImpacts(300, 4);
|
|
153
|
-
// If there were any:
|
|
154
|
-
if (impacts) {
|
|
155
|
-
// Hover over the upper-left corner of the page, to undo any impacts.
|
|
156
|
-
await page.hover('body', {
|
|
157
|
-
position: {
|
|
158
|
-
x: 0,
|
|
159
|
-
y: 0
|
|
66
|
+
// If any potential triggers remain and the test has not timed out:
|
|
67
|
+
if (sample.length && ! hasTimedOut) {
|
|
68
|
+
// Get and report the impacts until and unless the test times out.
|
|
69
|
+
try {
|
|
70
|
+
// Identify the first of them.
|
|
71
|
+
const firstTrigger = sample[0];
|
|
72
|
+
const tagNameJSHandle = await firstTrigger.getProperty('tagName')
|
|
73
|
+
.catch(error => '');
|
|
74
|
+
if (tagNameJSHandle) {
|
|
75
|
+
const tagName = await tagNameJSHandle.jsonValue();
|
|
76
|
+
// Identify the root of a subtree likely to contain impacted elements.
|
|
77
|
+
let root = firstTrigger;
|
|
78
|
+
if (['A', 'BUTTON', 'LI'].includes(tagName)) {
|
|
79
|
+
const rootJSHandle = await page.evaluateHandle(
|
|
80
|
+
firstTrigger => {
|
|
81
|
+
const parent = firstTrigger.parentElement || firstTrigger;
|
|
82
|
+
const grandparent = parent.parentElement || parent;
|
|
83
|
+
const greatGrandparent = grandparent.parentElement || parent;
|
|
84
|
+
return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
|
|
160
85
|
},
|
|
86
|
+
firstTrigger
|
|
87
|
+
);
|
|
88
|
+
root = rootJSHandle.asElement();
|
|
89
|
+
}
|
|
90
|
+
// Identify all the visible descendants of the root.
|
|
91
|
+
const preDescendants = await root.$$(':visible');
|
|
92
|
+
// Identify their opacities.
|
|
93
|
+
const preOpacities = await page.evaluate(elements => elements.map(
|
|
94
|
+
element => window.getComputedStyle(element).opacity
|
|
95
|
+
), preDescendants);
|
|
96
|
+
try {
|
|
97
|
+
// Hover over the trigger.
|
|
98
|
+
await firstTrigger.hover({
|
|
161
99
|
timeout: 500,
|
|
162
|
-
force: true,
|
|
163
100
|
noWaitAfter: true
|
|
164
101
|
});
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
102
|
+
// FUNCTION DEFINITION START
|
|
103
|
+
// Repeatedly seeks impacts.
|
|
104
|
+
const getImpacts = async (interval, triesLeft) => {
|
|
105
|
+
// If the allowed trial count has not yet been exhausted:
|
|
106
|
+
if (triesLeft-- && ! hasTimedOut) {
|
|
107
|
+
// Get the collection of descendants of the root.
|
|
108
|
+
const postDescendants = await root.$$(':visible');
|
|
109
|
+
// Identify the prior descandants of the root still in existence.
|
|
110
|
+
const remainerIndexes = await page.evaluate(args => {
|
|
111
|
+
const preDescendants = args[0];
|
|
112
|
+
const postDescendants = args[1];
|
|
113
|
+
const remainerIndexes = preDescendants
|
|
114
|
+
.map((element, index) => postDescendants.includes(element) ? index : -1)
|
|
115
|
+
.filter(index => index > -1);
|
|
116
|
+
return remainerIndexes;
|
|
117
|
+
}, [preDescendants, postDescendants]);
|
|
118
|
+
// Get the count of elements added by the hover event.
|
|
119
|
+
const additionCount = postDescendants.length - remainerIndexes.length;
|
|
120
|
+
const removalCount = preDescendants.length - remainerIndexes.length;
|
|
121
|
+
const remainers = [];
|
|
122
|
+
for (const index of remainerIndexes) {
|
|
123
|
+
remainers.push({
|
|
124
|
+
element: preDescendants[index],
|
|
125
|
+
preOpacity: preOpacities[index],
|
|
126
|
+
postOpacity: await page.evaluate(
|
|
127
|
+
element => window.getComputedStyle(element).opacity, preDescendants[index]
|
|
128
|
+
)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const opacityChangers = remainers
|
|
132
|
+
.filter(remainer => remainer.postOpacity !== remainer.preOpacity);
|
|
133
|
+
const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
|
|
134
|
+
(total, current) => total + current.element.querySelectorAll('*').length, 0
|
|
135
|
+
), opacityChangers) : 0;
|
|
136
|
+
if (additionCount || removalCount || opacityChangers.length) {
|
|
137
|
+
return {
|
|
138
|
+
additionCount,
|
|
139
|
+
removalCount,
|
|
140
|
+
opacityChangers,
|
|
141
|
+
opacityImpact
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
return await new Promise(resolve => {
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
resolve(getImpacts(interval, triesLeft));
|
|
148
|
+
}, interval);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// FUNCTION DEFINITION END
|
|
157
|
+
// Repeatedly seek impacts of the hover at intervals.
|
|
158
|
+
const impacts = await getImpacts(300, 4);
|
|
159
|
+
// If there were any:
|
|
160
|
+
if (impacts) {
|
|
161
|
+
// Hover over the upper-left corner of the page, to undo any impacts.
|
|
162
|
+
await page.hover('body', {
|
|
163
|
+
position: {
|
|
164
|
+
x: 0,
|
|
165
|
+
y: 0
|
|
166
|
+
},
|
|
167
|
+
timeout: 500,
|
|
168
|
+
force: true,
|
|
169
|
+
noWaitAfter: true
|
|
185
170
|
});
|
|
171
|
+
// Wait for any delayed and/or slowed hover reaction.
|
|
172
|
+
await page.waitForTimeout(200);
|
|
173
|
+
await root.waitForElementState('stable');
|
|
174
|
+
// Increment the counts of triggers and impacts.
|
|
175
|
+
const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
|
|
176
|
+
if (hasTimedOut) {
|
|
177
|
+
return Promise.resolve('');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
data.totals.impactTriggers += popRatio;
|
|
181
|
+
data.totals.additions += popRatio * additionCount;
|
|
182
|
+
data.totals.removals += popRatio * removalCount;
|
|
183
|
+
data.totals.opacityChanges += popRatio * opacityChangers.length;
|
|
184
|
+
data.totals.opacityImpact += popRatio * opacityImpact;
|
|
185
|
+
// If details are to be reported:
|
|
186
|
+
if (withItems) {
|
|
187
|
+
// Report them.
|
|
188
|
+
data.items[region].impactTriggers.push({
|
|
189
|
+
tagName,
|
|
190
|
+
text: await textOf(firstTrigger, 50),
|
|
191
|
+
additions: additionCount,
|
|
192
|
+
removals: removalCount,
|
|
193
|
+
opacityChanges: opacityChangers.length,
|
|
194
|
+
opacityImpact
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
186
198
|
}
|
|
187
199
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (withItems) {
|
|
193
|
-
try {
|
|
194
|
-
const id = await firstTrigger.getAttribute('id');
|
|
195
|
-
data.items[region].unhoverables.push({
|
|
196
|
-
tagName,
|
|
197
|
-
id: id || '',
|
|
198
|
-
text: await textOf(firstTrigger, 50)
|
|
199
|
-
});
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.log(`ERROR hovering (${error.message.replace(/\n.+/s, '')})`);
|
|
202
|
+
if (hasTimedOut) {
|
|
203
|
+
return Promise.resolve('');
|
|
200
204
|
}
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
else {
|
|
206
|
+
data.totals.unhoverables++;
|
|
207
|
+
if (withItems) {
|
|
208
|
+
try {
|
|
209
|
+
const id = await firstTrigger.getAttribute('id');
|
|
210
|
+
data.items[region].unhoverables.push({
|
|
211
|
+
tagName,
|
|
212
|
+
id: id || '',
|
|
213
|
+
text: await textOf(firstTrigger, 50)
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch(error) {
|
|
217
|
+
console.log('ERROR itemizing unhoverable element');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
203
220
|
}
|
|
204
221
|
}
|
|
205
222
|
}
|
|
223
|
+
// Process the remaining potential triggers.
|
|
224
|
+
await find(data, withItems, page, region, sample.slice(1), popRatio);
|
|
206
225
|
}
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
catch(error) {
|
|
227
|
+
console.log(`ERROR: Test quit when remaining sample size was ${sample.length}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
return Promise.resolve('');
|
|
209
232
|
}
|
|
210
233
|
};
|
|
211
234
|
// Performs the hover test and reports results.
|
|
@@ -213,7 +236,7 @@ exports.reporter = async (
|
|
|
213
236
|
page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
|
|
214
237
|
) => {
|
|
215
238
|
// Initialize the result.
|
|
216
|
-
|
|
239
|
+
let data = {
|
|
217
240
|
totals: {
|
|
218
241
|
triggers: 0,
|
|
219
242
|
headTriggers: 0,
|
|
@@ -259,17 +282,34 @@ exports.reporter = async (
|
|
|
259
282
|
// Get the head and tail samples.
|
|
260
283
|
const headSample = getSample(headTriggers, headSampleSize);
|
|
261
284
|
const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
|
|
285
|
+
// Set a time limit to handle pages that slow the operations of this test.
|
|
286
|
+
const timeLimit = Math.round(1.3 * (headSample.length + tailSample.length));
|
|
287
|
+
const timeout = setTimeout(async () => {
|
|
288
|
+
await page.close();
|
|
289
|
+
console.log(
|
|
290
|
+
`ERROR: hover test timed out at ${timeLimit} seconds; page closed`
|
|
291
|
+
);
|
|
292
|
+
hasTimedOut = true;
|
|
293
|
+
data = {
|
|
294
|
+
prevented: true,
|
|
295
|
+
error: 'ERROR: hover test timed out'
|
|
296
|
+
};
|
|
297
|
+
clearTimeout(timeout);
|
|
298
|
+
}, 1000 * timeLimit);
|
|
262
299
|
// Find and document the impacts.
|
|
263
|
-
if (headSample.length) {
|
|
300
|
+
if (headSample.length && ! hasTimedOut) {
|
|
264
301
|
await find(data, withItems, page, 'head', headSample, headTriggerCount / headSample.length);
|
|
265
302
|
}
|
|
266
|
-
if (tailSample.length) {
|
|
303
|
+
if (tailSample.length && ! hasTimedOut) {
|
|
267
304
|
await find(data, withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
|
|
268
305
|
}
|
|
306
|
+
clearTimeout(timeout);
|
|
269
307
|
// Round the reported totals.
|
|
270
|
-
|
|
271
|
-
data.totals
|
|
272
|
-
|
|
308
|
+
if (! hasTimedOut) {
|
|
309
|
+
Object.keys(data.totals).forEach(key => {
|
|
310
|
+
data.totals[key] = Math.round(data.totals[key]);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
273
313
|
// Return the result.
|
|
274
314
|
return {result: data};
|
|
275
315
|
};
|
package/tests/nuVal.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/*
|
|
2
2
|
nuVal
|
|
3
3
|
This test subjects a page to the Nu Html Checker.
|
|
4
|
+
That API erratically replaces left and right double quotation marks with invalid UTF-8, which
|
|
5
|
+
appears as 2 or 3 successive instances of the replacement character (U+fffd). Therefore, this
|
|
6
|
+
test removes all such quotation marks and the replacement character. That causes
|
|
7
|
+
'Bad value “” for' to become 'Bad value for'. Since the corruption of quotation marks is
|
|
8
|
+
erratic, no better solution is known.
|
|
4
9
|
*/
|
|
5
10
|
const https = require('https');
|
|
6
11
|
exports.reporter = async page => {
|
|
7
12
|
const pageContent = await page.content();
|
|
8
13
|
// Get the data from a Nu validation.
|
|
9
|
-
const
|
|
14
|
+
const dataPromise = new Promise(resolve => {
|
|
10
15
|
try {
|
|
11
16
|
const request = https.request(
|
|
12
17
|
{
|
|
13
|
-
|
|
18
|
+
/*
|
|
19
|
+
Alternatives (more timeout-prone): host=validator.nu; path=/?parser=html@out=json
|
|
20
|
+
That host crashes instead of ending with a fatal error.
|
|
21
|
+
*/
|
|
14
22
|
host: 'validator.w3.org',
|
|
15
23
|
path: '/nu/?parser=html&out=json',
|
|
16
24
|
method: 'POST',
|
|
@@ -27,12 +35,12 @@ exports.reporter = async page => {
|
|
|
27
35
|
// When the data arrive:
|
|
28
36
|
response.on('end', async () => {
|
|
29
37
|
try {
|
|
30
|
-
// Delete
|
|
31
|
-
const result = JSON.parse(report);
|
|
38
|
+
// Delete left and right quotation marks and their erratic invalid replacements.
|
|
39
|
+
const result = JSON.parse(report.replace(/[\u{fffd}“”]/ug, ''));
|
|
32
40
|
return resolve(result);
|
|
33
41
|
}
|
|
34
42
|
catch (error) {
|
|
35
|
-
console.log(`Validation failed (${error.message})`);
|
|
43
|
+
console.log(`ERROR: Validation failed (${error.message})`);
|
|
36
44
|
return resolve({
|
|
37
45
|
prevented: true,
|
|
38
46
|
error: error.message,
|
|
@@ -46,7 +54,7 @@ exports.reporter = async page => {
|
|
|
46
54
|
request.end();
|
|
47
55
|
request.on('error', error => {
|
|
48
56
|
console.log(error.message);
|
|
49
|
-
return
|
|
57
|
+
return resolve({
|
|
50
58
|
prevented: true,
|
|
51
59
|
error: error.message
|
|
52
60
|
});
|
|
@@ -54,11 +62,22 @@ exports.reporter = async page => {
|
|
|
54
62
|
}
|
|
55
63
|
catch(error) {
|
|
56
64
|
console.log(error.message);
|
|
57
|
-
return
|
|
65
|
+
return resolve({
|
|
58
66
|
prevented: true,
|
|
59
67
|
error: error.message
|
|
60
68
|
});
|
|
61
69
|
}
|
|
62
70
|
});
|
|
71
|
+
const timeoutPromise = new Promise(resolve => {
|
|
72
|
+
const timeLimit = 12;
|
|
73
|
+
const timeoutID = setTimeout(() => {
|
|
74
|
+
resolve({
|
|
75
|
+
prevented: true,
|
|
76
|
+
error: `ERROR: Validation timed out at ${timeLimit} seconds`
|
|
77
|
+
});
|
|
78
|
+
clearTimeout(timeoutID);
|
|
79
|
+
}, 1000 * timeLimit);
|
|
80
|
+
});
|
|
81
|
+
const data = await Promise.race([dataPromise, timeoutPromise]);
|
|
63
82
|
return {result: data};
|
|
64
83
|
};
|