testaro 59.2.5 → 59.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/procs/standardize.js +7 -15
- package/procs/visChange.js +1 -2
- package/run.js +44 -21
- package/testaro/hovInd.js +83 -92
- package/testaro/imageLink.js +4 -6
- package/testaro/legendLoc.js +11 -13
- package/testaro/lineHeight.js +4 -1
- package/testaro/optRoleSel.js +3 -5
- package/tests/alfa.js +8 -8
- package/tests/testaro.js +124 -48
package/package.json
CHANGED
package/procs/standardize.js
CHANGED
|
@@ -611,24 +611,16 @@ const convert = (toolName, data, result, standardResult) => {
|
|
|
611
611
|
rules.forEach(rule => {
|
|
612
612
|
// Copy its instances to the standard result.
|
|
613
613
|
const ruleResult = result[rule];
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
console.log(`ERROR: Testaro rule ${rule} result has no standardInstances property`);
|
|
619
|
-
}
|
|
614
|
+
ruleResult.standardInstances ??= [];
|
|
615
|
+
standardResult.instances.push(... ruleResult.standardInstances);
|
|
620
616
|
// Initialize a record of its sample-ratio-weighted totals.
|
|
621
617
|
data.ruleTotals[rule] = [0, 0, 0, 0];
|
|
622
618
|
// Add those totals to the record and to the standard result.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
console.log(`ERROR: Testaro rule ${rule} result has no totals property`);
|
|
619
|
+
ruleResult.totals ??= [0, 0, 0, 0];
|
|
620
|
+
for (const index in ruleResult.totals) {
|
|
621
|
+
const ruleTotal = ruleResult.totals[index];
|
|
622
|
+
data.ruleTotals[rule][index] += ruleTotal;
|
|
623
|
+
standardResult.totals[index] += ruleTotal;
|
|
632
624
|
}
|
|
633
625
|
});
|
|
634
626
|
const preventionCount = result.preventions && result.preventions.length;
|
package/procs/visChange.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
// IMPORTS
|
|
36
36
|
|
|
37
|
+
const pixelmatch = require('pixelmatch').default;
|
|
37
38
|
const {PNG} = require('pngjs');
|
|
38
39
|
|
|
39
40
|
// FUNCTIONS
|
|
@@ -103,8 +104,6 @@ exports.visChange = async (page, options = {}) => {
|
|
|
103
104
|
// Get their dimensions.
|
|
104
105
|
const {width, height} = pngs[0];
|
|
105
106
|
// Get the count of differing pixels between the shots.
|
|
106
|
-
const pixelmatchModule = await import('pixelmatch');
|
|
107
|
-
const pixelmatch = pixelmatchModule.default;
|
|
108
107
|
const pixelChanges = pixelmatch(pngs[0].data, pngs[1].data, null, width, height);
|
|
109
108
|
// Get the ratio of differing to all pixels as a percentage.
|
|
110
109
|
const changePercent = 100 * pixelChanges / (width * height);
|
package/run.js
CHANGED
|
@@ -205,7 +205,9 @@ const goTo = async (report, page, url, timeout, waitUntil) => {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
catch(error) {
|
|
208
|
-
|
|
208
|
+
if (debug) {
|
|
209
|
+
console.log(`ERROR visiting ${url} (${error.message.slice(0, 200)})`);
|
|
210
|
+
}
|
|
209
211
|
return {
|
|
210
212
|
success: false,
|
|
211
213
|
error: 'noVisit'
|
|
@@ -258,7 +260,9 @@ const addError = (alsoLog, alsoAbort, report, actIndex, message) => {
|
|
|
258
260
|
}
|
|
259
261
|
};
|
|
260
262
|
// Launches a browser and navigates to a URL.
|
|
261
|
-
const launch = exports.launch = async (
|
|
263
|
+
const launch = exports.launch = async (
|
|
264
|
+
report, debug, waits, tempBrowserID, tempURL, retries = 2
|
|
265
|
+
) => {
|
|
262
266
|
const act = report.acts[actIndex];
|
|
263
267
|
const {device} = report;
|
|
264
268
|
const deviceID = device && device.id;
|
|
@@ -276,22 +280,21 @@ const launch = exports.launch = async (report, debug, waits, tempBrowserID, temp
|
|
|
276
280
|
const browserOptions = {
|
|
277
281
|
logger: {
|
|
278
282
|
isEnabled: () => false,
|
|
279
|
-
log: (name, severity, message) =>
|
|
280
|
-
|
|
283
|
+
log: (name, severity, message) => {
|
|
284
|
+
if (['warning', 'error'].includes(severity)) {
|
|
285
|
+
console.log(`${severity.toUpperCase()}: ${message.slice(0, 200)}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
headless: ! debug,
|
|
290
|
+
slowMo: waits || 0,
|
|
291
|
+
args: ['--disable-dev-shm-usage']
|
|
281
292
|
};
|
|
282
|
-
browserOptions.headless = ! debug;
|
|
283
|
-
browserOptions.slowMo = waits || 0;
|
|
284
293
|
try {
|
|
285
294
|
// Replace the browser with a new one.
|
|
286
295
|
browser = await browserType.launch(browserOptions);
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
// Create a diagnostic listener for its unintentional closing.
|
|
290
|
-
browserContext.on('close', () => {
|
|
291
|
-
if (! browserCloseIntentional) {
|
|
292
|
-
console.log('ERROR: Browser context unexpectedly closed');
|
|
293
|
-
}
|
|
294
|
-
});
|
|
296
|
+
// Redefine the context (i.e. browser window).
|
|
297
|
+
browserContext = await browser.newContext(device.windowOptions);
|
|
295
298
|
// Prevent default timeouts.
|
|
296
299
|
browserContext.setDefaultTimeout(0);
|
|
297
300
|
// When a page (i.e. browser tab) is added to the browser context (i.e. browser window):
|
|
@@ -380,18 +383,38 @@ const launch = exports.launch = async (report, debug, waits, tempBrowserID, temp
|
|
|
380
383
|
// Report this.
|
|
381
384
|
addError(true, false, report, actIndex, 'status429');
|
|
382
385
|
}
|
|
383
|
-
// Otherwise, i.e. if the launch or navigation failed:
|
|
386
|
+
// Otherwise, i.e. if the launch or navigation failed for another reason:
|
|
384
387
|
else {
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
page = null;
|
|
388
|
+
// Cause another attempt to launch and navigate, if retries remain.
|
|
389
|
+
throw new Error(`Navigation failed (${navResult.error})`);
|
|
388
390
|
}
|
|
389
391
|
}
|
|
390
392
|
// If an error occurred:
|
|
391
393
|
catch(error) {
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
394
|
+
// If retries remain:
|
|
395
|
+
if (retries > 0) {
|
|
396
|
+
console.log(`WARNING: Retrying launch (${retries} retries left)`);
|
|
397
|
+
await wait(2000);
|
|
398
|
+
return launch(report, debug, waits, tempBrowserID, tempURL, retries - 1);
|
|
399
|
+
}
|
|
400
|
+
// Otherwise, i.e. if no retries remain:
|
|
401
|
+
else {
|
|
402
|
+
// Report this.
|
|
403
|
+
addError(
|
|
404
|
+
true, false, report, actIndex, `FINAL ERROR launching or navigating (${error.message})`
|
|
405
|
+
);
|
|
406
|
+
// If the browser was created, and thus not a context of it:
|
|
407
|
+
if (browser) {
|
|
408
|
+
// Report this.
|
|
409
|
+
console.log('ERROR: Browser was created but context creation failed');
|
|
410
|
+
// Close the browser.
|
|
411
|
+
await browser.close().catch(() => {
|
|
412
|
+
console.log('ERROR: Could not close browser after context creation failure');
|
|
413
|
+
});
|
|
414
|
+
browser = null;
|
|
415
|
+
}
|
|
416
|
+
page = null;
|
|
417
|
+
}
|
|
395
418
|
};
|
|
396
419
|
}
|
|
397
420
|
// Otherwise, i.e. if the browser or device ID is invalid:
|
package/testaro/hovInd.js
CHANGED
|
@@ -140,113 +140,104 @@ exports.reporter = async (page, withItems, sampleSize = 20) => {
|
|
|
140
140
|
const sample = locsAll.filter((loc, index) => sampleIndexes.includes(index));
|
|
141
141
|
// For each trigger in the sample:
|
|
142
142
|
for (const loc of sample) {
|
|
143
|
-
// Get its style properties.
|
|
144
|
-
const preStyles = await getHoverStyles(loc);
|
|
145
|
-
// Try to focus it.
|
|
146
143
|
try {
|
|
144
|
+
// Get its style properties.
|
|
145
|
+
const preStyles = await getHoverStyles(loc);
|
|
146
|
+
// Focus it.
|
|
147
147
|
await loc.focus({timeout: 500});
|
|
148
148
|
// If focusing succeeds, get its style properties.
|
|
149
149
|
const focStyles = await getHoverStyles(loc);
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
excerpt: elData.excerpt
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// If the element is a button and the hover and default states are not distinct:
|
|
183
|
-
if (hovStyles.tagName === 'BUTTON' && areAlike(preStyles, hovStyles)) {
|
|
184
|
-
// Add to the totals.
|
|
185
|
-
totals[1] += psRatio;
|
|
186
|
-
data.typeTotals.hoverLikeDefault += psRatio;
|
|
187
|
-
// If itemization is required:
|
|
188
|
-
if (withItems) {
|
|
189
|
-
// Add an instance to the result.
|
|
190
|
-
standardInstances.push({
|
|
191
|
-
ruleID: 'hovInd',
|
|
192
|
-
what: 'Element border, outline, color, and background color do not change when hovered over',
|
|
193
|
-
ordinalSeverity: 1,
|
|
194
|
-
tagName: elData.tagName,
|
|
195
|
-
id: elData.id,
|
|
196
|
-
location: elData.location,
|
|
197
|
-
excerpt: elData.excerpt
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// If the hover and focus states are indistinct but differ from the default state:
|
|
202
|
-
if (areAlike(hovStyles, focStyles) && ! areAlike(hovStyles, preStyles)) {
|
|
203
|
-
// Add to the totals.
|
|
204
|
-
totals[1] += psRatio;
|
|
205
|
-
data.typeTotals.hoverLikeFocus += psRatio;
|
|
206
|
-
// If itemization is required:
|
|
207
|
-
if (withItems) {
|
|
208
|
-
// Add an instance to the result.
|
|
209
|
-
standardInstances.push({
|
|
210
|
-
ruleID: 'hovInd',
|
|
211
|
-
what: 'Element border, outline, color, and background color are alike on hover and focus',
|
|
212
|
-
ordinalSeverity: 1,
|
|
213
|
-
tagName: elData.tagName,
|
|
214
|
-
id: elData.id,
|
|
215
|
-
location: elData.location,
|
|
216
|
-
excerpt: elData.excerpt
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
150
|
+
// Blur it.
|
|
151
|
+
await loc.blur({timeout: 500});
|
|
152
|
+
// If blurring succeeds, try to hover over it.
|
|
153
|
+
await loc.hover({timeout: 500});
|
|
154
|
+
// If hovering succeeds, get its style properties.
|
|
155
|
+
const hovStyles = await getHoverStyles(loc);
|
|
156
|
+
// If all 3 style declarations belong to the same element:
|
|
157
|
+
if ([focStyles, hovStyles].every(style => style.code === preStyles.code)) {
|
|
158
|
+
// Get data on the element if itemization is required.
|
|
159
|
+
const elData = withItems ? await getLocatorData(loc) : null;
|
|
160
|
+
// If the hover cursor is nonstandard:
|
|
161
|
+
const cursorData = getCursorData(hovStyles);
|
|
162
|
+
if (! cursorData.ok) {
|
|
163
|
+
// Add to the totals.
|
|
164
|
+
totals[2] += psRatio;
|
|
165
|
+
data.typeTotals.badCursor += psRatio;
|
|
166
|
+
// If itemization is required:
|
|
167
|
+
if (withItems) {
|
|
168
|
+
// Add an instance to the result.
|
|
169
|
+
standardInstances.push({
|
|
170
|
+
ruleID: 'hovInd',
|
|
171
|
+
what: `Element has a nonstandard hover cursor (${cursorData.cursor})`,
|
|
172
|
+
ordinalSeverity: 2,
|
|
173
|
+
tagName: elData.tagName,
|
|
174
|
+
id: elData.id,
|
|
175
|
+
location: elData.location,
|
|
176
|
+
excerpt: elData.excerpt
|
|
177
|
+
});
|
|
220
178
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
179
|
+
}
|
|
180
|
+
// If the element is a button and the hover and default states are not distinct:
|
|
181
|
+
if (hovStyles.tagName === 'BUTTON' && areAlike(preStyles, hovStyles)) {
|
|
182
|
+
// Add to the totals.
|
|
183
|
+
totals[1] += psRatio;
|
|
184
|
+
data.typeTotals.hoverLikeDefault += psRatio;
|
|
185
|
+
// If itemization is required:
|
|
186
|
+
if (withItems) {
|
|
187
|
+
// Add an instance to the result.
|
|
188
|
+
standardInstances.push({
|
|
189
|
+
ruleID: 'hovInd',
|
|
190
|
+
what: 'Element border, outline, color, and background color do not change when hovered over',
|
|
191
|
+
ordinalSeverity: 1,
|
|
192
|
+
tagName: elData.tagName,
|
|
193
|
+
id: elData.id,
|
|
194
|
+
location: elData.location,
|
|
195
|
+
excerpt: elData.excerpt
|
|
196
|
+
});
|
|
227
197
|
}
|
|
228
198
|
}
|
|
229
|
-
// If
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
data.
|
|
234
|
-
|
|
199
|
+
// If the hover and focus states are indistinct but differ from the default state:
|
|
200
|
+
if (areAlike(hovStyles, focStyles) && ! areAlike(hovStyles, preStyles)) {
|
|
201
|
+
// Add to the totals.
|
|
202
|
+
totals[1] += psRatio;
|
|
203
|
+
data.typeTotals.hoverLikeFocus += psRatio;
|
|
204
|
+
// If itemization is required:
|
|
205
|
+
if (withItems) {
|
|
206
|
+
// Add an instance to the result.
|
|
207
|
+
standardInstances.push({
|
|
208
|
+
ruleID: 'hovInd',
|
|
209
|
+
what: 'Element border, outline, color, and background color are alike on hover and focus',
|
|
210
|
+
ordinalSeverity: 1,
|
|
211
|
+
tagName: elData.tagName,
|
|
212
|
+
id: elData.id,
|
|
213
|
+
location: elData.location,
|
|
214
|
+
excerpt: elData.excerpt
|
|
215
|
+
});
|
|
216
|
+
}
|
|
235
217
|
}
|
|
236
218
|
}
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
// Report this.
|
|
219
|
+
// Otherwise, i.e. if the style properties do not all belong to the same element:
|
|
220
|
+
else {
|
|
221
|
+
// Report this and quit.
|
|
240
222
|
data.prevented = true;
|
|
241
|
-
data.error = 'ERROR:
|
|
223
|
+
data.error = 'ERROR: Page changes on focus or hover prevent test';
|
|
242
224
|
break;
|
|
243
225
|
}
|
|
244
226
|
}
|
|
245
|
-
// If focusing fails:
|
|
246
227
|
catch(error) {
|
|
247
|
-
//
|
|
228
|
+
// If the page closed:
|
|
229
|
+
if (
|
|
230
|
+
['Target page', 'detached', 'null', 'closed'].some(string => error.message.includes(string))
|
|
231
|
+
) {
|
|
232
|
+
data.error = `ERROR during hovInd test: ${error.message}`;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const elementText = loc ? await loc.textContent({timeout: 200}) : '';
|
|
236
|
+
const excerpt = elementText ? elementText.trim().slice(0, 100) : '<no text>';
|
|
237
|
+
data.error = `ERROR manipulating element (${excerpt}) during hovInd test`;
|
|
238
|
+
}
|
|
248
239
|
data.prevented = true;
|
|
249
|
-
|
|
240
|
+
// Abort this test.
|
|
250
241
|
break;
|
|
251
242
|
}
|
|
252
243
|
}
|
package/testaro/imageLink.js
CHANGED
|
@@ -34,12 +34,10 @@ exports.reporter = async (page, withItems) => {
|
|
|
34
34
|
const ruleData = {
|
|
35
35
|
ruleID: 'imageLink',
|
|
36
36
|
selector: 'a[href]',
|
|
37
|
-
pruner: async
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
},
|
|
37
|
+
pruner: async loc => await loc.evaluate(el => {
|
|
38
|
+
const href = el.getAttribute('href') || '';
|
|
39
|
+
return /\.(?:png|jpe?g|gif|svg|webp|ico)(?:$|[?#])/i.test(href);
|
|
40
|
+
}),
|
|
43
41
|
complaints: {
|
|
44
42
|
instance: 'Link destination is an image file',
|
|
45
43
|
summary: 'Links have image files as their destinations'
|
package/testaro/legendLoc.js
CHANGED
|
@@ -34,20 +34,18 @@ exports.reporter = async (page, withItems) => {
|
|
|
34
34
|
const ruleData = {
|
|
35
35
|
ruleID: 'legendLoc',
|
|
36
36
|
selector: 'legend',
|
|
37
|
-
pruner: async (loc) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return child !== el; // true if not first child
|
|
46
|
-
}
|
|
37
|
+
pruner: async (loc) => await loc.evaluate(el => {
|
|
38
|
+
const parent = el.parentElement;
|
|
39
|
+
if (!parent) return true;
|
|
40
|
+
if (parent.tagName.toUpperCase() !== 'FIELDSET') return true;
|
|
41
|
+
// Check if this legend is the first element child of the fieldset
|
|
42
|
+
for (const child of parent.children) {
|
|
43
|
+
if (child.nodeType === 1) {
|
|
44
|
+
return child !== el; // true if not first child
|
|
47
45
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}),
|
|
51
49
|
complaints: {
|
|
52
50
|
instance: 'Element is not the first child of a fieldset element',
|
|
53
51
|
summary: 'legend elements are not the first children of fieldset elements'
|
package/testaro/lineHeight.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
© 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
|
+
© 2025 Jonathan Robert Pool. All rights reserved.
|
|
3
4
|
|
|
4
5
|
MIT License
|
|
5
6
|
|
|
@@ -31,10 +32,12 @@
|
|
|
31
32
|
their subtrees are excluded.
|
|
32
33
|
*/
|
|
33
34
|
|
|
35
|
+
// IMPORTS
|
|
36
|
+
|
|
34
37
|
// Module to perform common operations.
|
|
35
38
|
const {init, report} = require('../procs/testaro');
|
|
36
39
|
|
|
37
|
-
//
|
|
40
|
+
// FUNCTIONS
|
|
38
41
|
|
|
39
42
|
// Runs the test and returns the result.
|
|
40
43
|
exports.reporter = async (page, withItems) => {
|
package/testaro/optRoleSel.js
CHANGED
|
@@ -34,11 +34,9 @@ exports.reporter = async (page, withItems) => {
|
|
|
34
34
|
const ruleData = {
|
|
35
35
|
ruleID: 'optRoleSel',
|
|
36
36
|
selector: '[role="option"]',
|
|
37
|
-
pruner: async (loc) => {
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
},
|
|
37
|
+
pruner: async (loc) => await loc.evaluate(el => {
|
|
38
|
+
return ! el.hasAttribute('aria-selected');
|
|
39
|
+
}),
|
|
42
40
|
complaints: {
|
|
43
41
|
instance: 'Element has an explicit option role but no aria-selected attribute',
|
|
44
42
|
summary: 'Elements with explicit option roles have no aria-selected attributes'
|
package/tests/alfa.js
CHANGED
|
@@ -27,14 +27,18 @@
|
|
|
27
27
|
This test implements the alfa ruleset for accessibility.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
// IMPORTS
|
|
31
|
+
|
|
32
|
+
const {Audit} = require('@siteimprove/alfa-act');
|
|
33
|
+
const {Playwright} = require('@siteimprove/alfa-playwright');
|
|
34
|
+
let alfaRules = require('@siteimprove/alfa-rules').default;
|
|
35
|
+
|
|
30
36
|
// FUNCTIONS
|
|
31
37
|
|
|
32
38
|
// Conducts and reports the alfa tests.
|
|
33
|
-
exports.reporter = async (page, report, actIndex
|
|
39
|
+
exports.reporter = async (page, report, actIndex) => {
|
|
34
40
|
const act = report.acts[actIndex];
|
|
35
41
|
const {rules} = act;
|
|
36
|
-
const alfaRulesModule = await import('@siteimprove/alfa-rules');
|
|
37
|
-
const alfaRules = alfaRulesModule.default;
|
|
38
42
|
// If only some rules are to be employed:
|
|
39
43
|
if (rules && rules.length) {
|
|
40
44
|
// Remove the other rules.
|
|
@@ -84,11 +88,7 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
|
|
|
84
88
|
}
|
|
85
89
|
// Test the page content with the specified rules.
|
|
86
90
|
const doc = await page.evaluateHandle('document');
|
|
87
|
-
const alfaPlaywrightModule = await import('@siteimprove/alfa-playwright');
|
|
88
|
-
const {Playwright} = alfaPlaywrightModule;
|
|
89
91
|
const alfaPage = await Playwright.toPage(doc);
|
|
90
|
-
const alfaActModule = await import('@siteimprove/alfa-act');
|
|
91
|
-
const {Audit} = alfaActModule;
|
|
92
92
|
const audit = Audit.of(alfaPage, alfaRules);
|
|
93
93
|
const outcomes = Array.from(await audit.evaluate());
|
|
94
94
|
// For each failure or warning:
|
|
@@ -161,7 +161,7 @@ exports.reporter = async (page, report, actIndex, timeLimit) => {
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
catch(error) {
|
|
164
|
-
console.log(`ERROR:
|
|
164
|
+
console.log(`ERROR: Navigation to URL timed out (${error})`);
|
|
165
165
|
data.prevented = true;
|
|
166
166
|
data.error = 'ERROR: Act failed';
|
|
167
167
|
}
|
package/tests/testaro.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
This test implements the Testaro evaluative rules.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// IMPORTS
|
|
32
32
|
|
|
33
33
|
// Module to perform common operations.
|
|
34
34
|
const {init, report} = require('../procs/testaro');
|
|
@@ -37,7 +37,7 @@ const {launch} = require('../run');
|
|
|
37
37
|
// Module to handle files.
|
|
38
38
|
const fs = require('fs/promises');
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// CONSTANTS
|
|
41
41
|
|
|
42
42
|
// The validation job data for the tests listed below are in the pending directory.
|
|
43
43
|
/*
|
|
@@ -146,12 +146,18 @@ const slowTestLimits = {
|
|
|
146
146
|
hovInd: 10,
|
|
147
147
|
labClash: 10,
|
|
148
148
|
lineHeight: 10,
|
|
149
|
+
linkUl: 10,
|
|
149
150
|
motion: 15,
|
|
150
151
|
opFoc: 10,
|
|
151
152
|
tabNav: 10,
|
|
152
153
|
textSem: 10
|
|
153
154
|
};
|
|
154
155
|
|
|
156
|
+
// ERROR HANDLER
|
|
157
|
+
process.on('unhandledRejection', reason => {
|
|
158
|
+
console.error(`ERROR: Unhandled Promise Rejection (${reason})`);
|
|
159
|
+
});
|
|
160
|
+
|
|
155
161
|
// ######## FUNCTIONS
|
|
156
162
|
|
|
157
163
|
// Conducts a JSON-defined test.
|
|
@@ -182,6 +188,7 @@ const wait = ms => {
|
|
|
182
188
|
};
|
|
183
189
|
// Conducts and reports Testaro tests.
|
|
184
190
|
exports.reporter = async (page, report, actIndex) => {
|
|
191
|
+
// Report page crashes.
|
|
185
192
|
const url = await page.url();
|
|
186
193
|
const act = report.acts[actIndex];
|
|
187
194
|
const {args, stopOnFail, withItems} = act;
|
|
@@ -216,17 +223,15 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
216
223
|
let calledRules = rules[0] === 'y'
|
|
217
224
|
? rules.slice(1)
|
|
218
225
|
: Object.keys(evalRules).filter(ruleID => ! rules.slice(1).includes(ruleID));
|
|
219
|
-
const calledContaminators = calledRules.filter(rule => contaminators.includes(rule))
|
|
226
|
+
const calledContaminators = calledRules.filter(rule => contaminators.includes(rule));
|
|
220
227
|
const firstCalledContaminator = calledContaminators[0];
|
|
221
|
-
const calledBenignRules = calledRules.filter(rule => ! contaminators.includes(rule))
|
|
228
|
+
const calledBenignRules = calledRules.filter(rule => ! contaminators.includes(rule));
|
|
222
229
|
const testTimes = [];
|
|
223
230
|
let contaminatorsStarted = false;
|
|
224
231
|
// Starting with the noncontaminators, for each rule invoked:
|
|
225
232
|
for (const rule of calledBenignRules.concat(calledContaminators)) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
console.log(' It is the first contaminator');
|
|
229
|
-
}
|
|
233
|
+
const contaminatorSuffix = rule === firstCalledContaminator ? ' (first contaminator)' : '';
|
|
234
|
+
console.log(`Starting rule ${rule}${contaminatorSuffix}`);
|
|
230
235
|
const pageClosed = page ? page.isClosed() : true;
|
|
231
236
|
const isContaminator = contaminators.includes(rule);
|
|
232
237
|
// If it is a contaminator other than the first one or the page has closed:
|
|
@@ -250,6 +255,22 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
250
255
|
if (isContaminator) {
|
|
251
256
|
contaminatorsStarted = true;
|
|
252
257
|
}
|
|
258
|
+
// Report crashes and disconnections during this test.
|
|
259
|
+
let crashHandler;
|
|
260
|
+
let disconnectHandler;
|
|
261
|
+
const {browser} = require('../run');
|
|
262
|
+
if (page && ! page.isClosed()) {
|
|
263
|
+
crashHandler = () => {
|
|
264
|
+
console.log(`ERROR: Page crashed during ${rule} test`);
|
|
265
|
+
};
|
|
266
|
+
page.on('crash', crashHandler);
|
|
267
|
+
}
|
|
268
|
+
if (browser) {
|
|
269
|
+
disconnectHandler = () => {
|
|
270
|
+
console.log(`ERROR: Browser disconnected during ${rule} test`);
|
|
271
|
+
};
|
|
272
|
+
browser.on('disconnected', disconnectHandler);
|
|
273
|
+
}
|
|
253
274
|
// Initialize an argument array.
|
|
254
275
|
const ruleArgs = [page, withItems];
|
|
255
276
|
const ruleFileNames = await fs.readdir(`${__dirname}/../testaro`);
|
|
@@ -269,52 +290,103 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
269
290
|
const what = evalRules[rule] || etcRules[rule];
|
|
270
291
|
result[rule].what = what;
|
|
271
292
|
const startTime = Date.now();
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
293
|
+
let timeout;
|
|
294
|
+
let testRetries = 2;
|
|
295
|
+
let testSuccess = false;
|
|
296
|
+
while (testRetries > 0 && ! testSuccess) {
|
|
297
|
+
try {
|
|
298
|
+
// Apply a time limit to the test.
|
|
299
|
+
const timeLimit = 1000 * (slowTestLimits[rule] ?? 5);
|
|
300
|
+
// If the time limit expires during the test:
|
|
301
|
+
const timer = new Promise(resolve => {
|
|
302
|
+
timeout = setTimeout(() => {
|
|
303
|
+
// Add data about the test, including its prevention, to the result.
|
|
304
|
+
const endTime = Date.now();
|
|
305
|
+
testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
|
|
306
|
+
data.rulePreventions.push(rule);
|
|
307
|
+
result[rule].totals = [0, 0, 0, 0];
|
|
308
|
+
result[rule].standardInstances = [];
|
|
309
|
+
console.log(`ERROR: Test of testaro rule ${rule} timed out`);
|
|
310
|
+
resolve({timedOut: true});
|
|
311
|
+
}, timeLimit);
|
|
312
|
+
});
|
|
313
|
+
// Perform the test, subject to the time limit.
|
|
314
|
+
const ruleReport = isJS
|
|
315
|
+
? require(`../testaro/${rule}`).reporter(... ruleArgs)
|
|
316
|
+
: jsonTest(rule, ruleArgs);
|
|
317
|
+
// Get the test result or a timeout result.
|
|
318
|
+
const ruleOrTimeoutReport = await Promise.race([timer, ruleReport]);
|
|
319
|
+
// If the test was completed:
|
|
320
|
+
if (! ruleOrTimeoutReport.timedOut) {
|
|
321
|
+
// Add data from the test to the result.
|
|
280
322
|
const endTime = Date.now();
|
|
281
323
|
testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
|
|
324
|
+
Object.keys(ruleOrTimeoutReport).forEach(key => {
|
|
325
|
+
result[rule][key] = ruleOrTimeoutReport[key];
|
|
326
|
+
});
|
|
327
|
+
result[rule].totals = result[rule].totals.map(total => Math.round(total));
|
|
328
|
+
// Prevent a retry of the test.
|
|
329
|
+
testSuccess = true;
|
|
330
|
+
// If testing is to stop after a failure and the page failed the test:
|
|
331
|
+
if (stopOnFail && ruleOrTimeoutReport.totals.some(total => total)) {
|
|
332
|
+
// Stop testing.
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// If an error is thrown by the test:
|
|
338
|
+
catch(error) {
|
|
339
|
+
const isPageClosed = ['closed', 'Protocol error', 'Target page'].some(phrase =>
|
|
340
|
+
error.message.includes(phrase)
|
|
341
|
+
);
|
|
342
|
+
// If the page has closed and there are retries left:
|
|
343
|
+
if (isPageClosed && testRetries) {
|
|
344
|
+
// Report this and decrement the allowed retry count.
|
|
345
|
+
console.log(
|
|
346
|
+
`WARNING: Retry ${3 - testRetries--} of test ${rule} starting after page closed`
|
|
347
|
+
);
|
|
348
|
+
await wait(2000);
|
|
349
|
+
// Replace the browser and the page and navigate to the target.
|
|
350
|
+
await launch(
|
|
351
|
+
report,
|
|
352
|
+
process.env.DEBUG === 'true',
|
|
353
|
+
Number.parseInt(process.env.WAITS) || 0,
|
|
354
|
+
report.browserID,
|
|
355
|
+
url
|
|
356
|
+
);
|
|
357
|
+
page = require('../run').page;
|
|
358
|
+
// If the page replacement failed:
|
|
359
|
+
if (! page) {
|
|
360
|
+
// Report this.
|
|
361
|
+
console.log(`ERROR: Browser relaunch to retry test ${rule} failed`);
|
|
362
|
+
data.rulePreventions.push(rule);
|
|
363
|
+
data.rulePreventionMessages[rule] = 'Retry failure due to browser relaunch failure';
|
|
364
|
+
// Stop retrying the test.
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
// Update the rule arguments with the current page.
|
|
368
|
+
ruleArgs[0] = page;
|
|
369
|
+
}
|
|
370
|
+
// Otherwise, i.e. if the page is open or it is closed but no retries are left:
|
|
371
|
+
else {
|
|
372
|
+
// Treat the test as prevented.
|
|
282
373
|
data.rulePreventions.push(rule);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
resolve({timedOut: true});
|
|
287
|
-
}, timeLimit);
|
|
288
|
-
});
|
|
289
|
-
// Perform the test, subject to the time limit.
|
|
290
|
-
const ruleReport = isJS
|
|
291
|
-
? require(`../testaro/${rule}`).reporter(... ruleArgs)
|
|
292
|
-
: jsonTest(rule, ruleArgs);
|
|
293
|
-
// Get the test result or a timeout result.
|
|
294
|
-
const ruleOrTimeoutReport = await Promise.race([timer, ruleReport]);
|
|
295
|
-
clearTimeout(timeout);
|
|
296
|
-
// If the test was completed:
|
|
297
|
-
if (! ruleOrTimeoutReport.timedOut) {
|
|
298
|
-
// Add data from the test to the result.
|
|
299
|
-
const endTime = Date.now();
|
|
300
|
-
testTimes.push([rule, Math.round((endTime - startTime) / 1000)]);
|
|
301
|
-
Object.keys(ruleOrTimeoutReport).forEach(key => {
|
|
302
|
-
result[rule][key] = ruleOrTimeoutReport[key];
|
|
303
|
-
});
|
|
304
|
-
result[rule].totals = result[rule].totals.map(total => Math.round(total));
|
|
305
|
-
// If testing is to stop after a failure and the page failed the test:
|
|
306
|
-
if (stopOnFail && ruleOrTimeoutReport.totals.some(total => total)) {
|
|
307
|
-
// Stop testing.
|
|
374
|
+
data.rulePreventionMessages[rule] = error.message;
|
|
375
|
+
console.log(`ERROR: Test of testaro rule ${rule} prevented (${error.message})`);
|
|
376
|
+
// Do not retry the test even if retries are left.
|
|
308
377
|
break;
|
|
309
378
|
}
|
|
310
379
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
380
|
+
finally {
|
|
381
|
+
// Clear the timeout and the error listeners.
|
|
382
|
+
clearTimeout(timeout);
|
|
383
|
+
if (page && ! page.isClosed() && crashHandler) {
|
|
384
|
+
page.off('crash', crashHandler);
|
|
385
|
+
}
|
|
386
|
+
if (browser && disconnectHandler) {
|
|
387
|
+
browser.off('disconnected', disconnectHandler);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
318
390
|
}
|
|
319
391
|
}
|
|
320
392
|
// Otherwise, i.e. if the rule is undefined or doubly defined:
|
|
@@ -322,6 +394,10 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
322
394
|
// Report this.
|
|
323
395
|
data.rulesInvalid.push(rule);
|
|
324
396
|
console.log(`ERROR: Rule ${rule} not validly defined`);
|
|
397
|
+
// Clear the crash listener.
|
|
398
|
+
if (page && ! page.isClosed()) {
|
|
399
|
+
page.off('crash', crashHandler);
|
|
400
|
+
}
|
|
325
401
|
}
|
|
326
402
|
}
|
|
327
403
|
// Record the test times in descending order.
|