testaro 36.1.0 → 36.1.2
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/getSource.js +2 -1
- package/procs/isInlineLink.js +9 -3
- package/procs/testaro.js +13 -15
- package/procs/visChange.js +6 -4
- package/testaro/buttonMenu.js +21 -6
- package/testaro/dupAtt.js +2 -1
- package/testaro/focOp.js +3 -3
- package/testaro/headEl.js +2 -1
- package/testaro/hover.js +12 -4
- package/testaro/opFoc.js +3 -3
- package/testaro/tabNav.js +323 -326
- package/testaro/textNodes.js +1 -1
- package/tests/testaro.js +26 -5
package/package.json
CHANGED
package/procs/getSource.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2023 CVS Health and/or one of its affiliates. All rights reserved.
|
|
2
|
+
© 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
3
|
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -54,6 +54,7 @@ exports.getSource = async page => {
|
|
|
54
54
|
catch(error) {
|
|
55
55
|
console.log(`ERROR getting source of page (${error.message})`);
|
|
56
56
|
data.prevented = true;
|
|
57
|
+
data.error = 'ERROR getting source of page';
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
return data;
|
package/procs/isInlineLink.js
CHANGED
|
@@ -28,11 +28,17 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
exports.isInlineLink = async loc => await loc.evaluate(element => {
|
|
31
|
+
let listAncestor;
|
|
31
32
|
// Initialize the link as inline.
|
|
32
33
|
let result = true;
|
|
33
|
-
// If
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
34
|
+
// If its display style property is block:
|
|
35
|
+
const styleDec = window.getComputedStyle(element);
|
|
36
|
+
if (styleDec.display === 'block') {
|
|
37
|
+
// Reclassify the link as non-inline.
|
|
38
|
+
result = false;
|
|
39
|
+
}
|
|
40
|
+
// Otherwise, if it is in a list item in a list of at least 2 links:
|
|
41
|
+
else if (listAncestor = element.closest('ul, ol')) {
|
|
36
42
|
if (listAncestor.children.length > 1 && Array.from(listAncestor.children).every(child => {
|
|
37
43
|
const isValidListItem = child.tagName === 'LI';
|
|
38
44
|
const has1Link = child.querySelectorAll('a').length === 1;
|
package/procs/testaro.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2023 CVS Health and/or one of its affiliates. All rights reserved.
|
|
2
|
+
© 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
3
|
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -34,8 +34,8 @@ const {getLocatorData} = require('../procs/getLocatorData');
|
|
|
34
34
|
|
|
35
35
|
// ########## FUNCTIONS
|
|
36
36
|
|
|
37
|
-
// Initializes locators and a result.
|
|
38
|
-
const init = async (sampleMax, page, locAllSelector, options = {}) => {
|
|
37
|
+
// Initializes violation locators and a result and returns them in an object.
|
|
38
|
+
const init = exports.init = async (sampleMax, page, locAllSelector, options = {}) => {
|
|
39
39
|
// Get locators for the specified elements.
|
|
40
40
|
const locPop = page.locator(locAllSelector, options);
|
|
41
41
|
const locPops = await locPop.all();
|
|
@@ -60,11 +60,11 @@ const init = async (sampleMax, page, locAllSelector, options = {}) => {
|
|
|
60
60
|
};
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
// Populates a result.
|
|
64
|
-
const report = async (withItems, all, ruleID, whats, ordinalSeverity, tagName = '') => {
|
|
63
|
+
// Populates and returns a result.
|
|
64
|
+
const report = exports.report = async (withItems, all, ruleID, whats, ordinalSeverity, tagName = '') => {
|
|
65
65
|
const {locs, result} = all;
|
|
66
66
|
const {data, totals, standardInstances} = result;
|
|
67
|
-
// For each
|
|
67
|
+
// For each violation locator:
|
|
68
68
|
for (const locItem of locs) {
|
|
69
69
|
// Get data on its element.
|
|
70
70
|
let loc, whatParam;
|
|
@@ -76,7 +76,7 @@ const report = async (withItems, all, ruleID, whats, ordinalSeverity, tagName =
|
|
|
76
76
|
loc = locItem;
|
|
77
77
|
}
|
|
78
78
|
const elData = await getLocatorData(loc);
|
|
79
|
-
//
|
|
79
|
+
// Increment the totals.
|
|
80
80
|
totals[ordinalSeverity] += data.populationRatio;
|
|
81
81
|
// If itemization is required:
|
|
82
82
|
if (withItems) {
|
|
@@ -114,11 +114,11 @@ const report = async (withItems, all, ruleID, whats, ordinalSeverity, tagName =
|
|
|
114
114
|
return result;
|
|
115
115
|
};
|
|
116
116
|
// Performs a simplifiable test.
|
|
117
|
-
|
|
117
|
+
exports.simplify = async (page, withItems, ruleData) => {
|
|
118
118
|
const {
|
|
119
119
|
ruleID, selector, pruner, isDestructive, complaints, ordinalSeverity, summaryTagName
|
|
120
120
|
} = ruleData;
|
|
121
|
-
//
|
|
121
|
+
// Get an object with initialized violation locators and result as properties.
|
|
122
122
|
const all = await init(100, page, selector);
|
|
123
123
|
// For each locator:
|
|
124
124
|
for (const loc of all.allLocs) {
|
|
@@ -126,7 +126,7 @@ const simplify = async (page, withItems, ruleData) => {
|
|
|
126
126
|
const isBad = await pruner(loc);
|
|
127
127
|
// If it does:
|
|
128
128
|
if (isBad) {
|
|
129
|
-
// Add the locator to the array of
|
|
129
|
+
// Add the locator of the element to the array of violation locators.
|
|
130
130
|
all.locs.push(loc);
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -135,7 +135,7 @@ const simplify = async (page, withItems, ruleData) => {
|
|
|
135
135
|
complaints.instance,
|
|
136
136
|
complaints.summary
|
|
137
137
|
];
|
|
138
|
-
const
|
|
138
|
+
const result = await report(withItems, all, ruleID, whats, ordinalSeverity, summaryTagName);
|
|
139
139
|
// If the pruner modifies the page:
|
|
140
140
|
if (isDestructive) {
|
|
141
141
|
// Reload the page.
|
|
@@ -146,8 +146,6 @@ const simplify = async (page, withItems, ruleData) => {
|
|
|
146
146
|
console.log('ERROR: page reload timed out');
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
|
|
149
|
+
// Return the result.
|
|
150
|
+
return result;
|
|
150
151
|
};
|
|
151
|
-
exports.init = init;
|
|
152
|
-
exports.report = report;
|
|
153
|
-
exports.simplify = simplify;
|
package/procs/visChange.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2023 CVS Health and/or one of its affiliates. All rights reserved.
|
|
2
|
+
© 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
3
|
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -71,7 +71,7 @@ exports.visChange = async (page, options = {}) => {
|
|
|
71
71
|
}
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
// Make a screenshot, excluding an element if specified.
|
|
74
|
+
// Make and get a screenshot, excluding an element if specified.
|
|
75
75
|
const shot0 = await shoot(page, exclusion);
|
|
76
76
|
// If it succeeded:
|
|
77
77
|
if (shot0.length) {
|
|
@@ -93,7 +93,7 @@ exports.visChange = async (page, options = {}) => {
|
|
|
93
93
|
}
|
|
94
94
|
// Wait as specified, or 3 seconds.
|
|
95
95
|
await page.waitForTimeout(delayBetween || 3000);
|
|
96
|
-
// Make another screenshot.
|
|
96
|
+
// Make and get another screenshot.
|
|
97
97
|
const shot1 = await shoot(page, exclusion);
|
|
98
98
|
// If it succeeded:
|
|
99
99
|
if (shot1.length) {
|
|
@@ -119,15 +119,17 @@ exports.visChange = async (page, options = {}) => {
|
|
|
119
119
|
// Return this.
|
|
120
120
|
return {
|
|
121
121
|
success: false,
|
|
122
|
+
prevented: true,
|
|
122
123
|
error: 'Second screenshot failed'
|
|
123
124
|
};
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
// Otherwise, i.e. if the screenshot failed:
|
|
127
|
+
// Otherwise, i.e. if the first screenshot failed:
|
|
127
128
|
else {
|
|
128
129
|
// Return this.
|
|
129
130
|
return {
|
|
130
131
|
success: false,
|
|
132
|
+
prevented: true,
|
|
131
133
|
error: 'First screenshot failed'
|
|
132
134
|
};
|
|
133
135
|
}
|
package/testaro/buttonMenu.js
CHANGED
|
@@ -122,8 +122,8 @@ exports.reporter = async (page, withItems, trialKeySpecs = []) => {
|
|
|
122
122
|
// Get a locator for its menu.
|
|
123
123
|
const menuID = await mbLoc.getAttribute('aria-controls');
|
|
124
124
|
const menuLoc = page.locator(`[id=${menuID}][role=menu], [id=${menuID}][role=menubar]`);
|
|
125
|
-
// If the button controls
|
|
126
|
-
if (menuLoc) {
|
|
125
|
+
// If the button controls exactly 1 menu:
|
|
126
|
+
if (menuLoc && await menuLoc.count() === 1) {
|
|
127
127
|
// Get data on the menu.
|
|
128
128
|
const elData = await getLocatorData(menuLoc);
|
|
129
129
|
// If data were obtained:
|
|
@@ -251,10 +251,25 @@ exports.reporter = async (page, withItems, trialKeySpecs = []) => {
|
|
|
251
251
|
console.log('ERROR: Menu data not obtained');
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
|
-
// Otherwise, i.e. if
|
|
254
|
+
// Otherwise, i.e. if it does not control exactly 1 menu:
|
|
255
255
|
else {
|
|
256
|
-
//
|
|
257
|
-
|
|
256
|
+
// Add to the totals.
|
|
257
|
+
totals[2]++;
|
|
258
|
+
// If itemization is required:
|
|
259
|
+
if (withItems) {
|
|
260
|
+
// Get data on the menu button.
|
|
261
|
+
const mbData = await getLocatorData(mbLoc);
|
|
262
|
+
// Add an instance to the result.
|
|
263
|
+
standardInstances.push({
|
|
264
|
+
ruleID: 'buttonMenu',
|
|
265
|
+
what: `Menu button does not control exactly 1 menu`,
|
|
266
|
+
ordinalSeverity: 2,
|
|
267
|
+
tagName: 'BUTTON',
|
|
268
|
+
id: await mbData.id,
|
|
269
|
+
location: mbData.location,
|
|
270
|
+
excerpt: mbData.excerpt
|
|
271
|
+
});
|
|
272
|
+
}
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
275
|
// If itemization is not required and there are any instances:
|
|
@@ -262,7 +277,7 @@ exports.reporter = async (page, withItems, trialKeySpecs = []) => {
|
|
|
262
277
|
// Add a summary instance to the result.
|
|
263
278
|
standardInstances.push({
|
|
264
279
|
ruleID: 'buttonMenu',
|
|
265
|
-
what: '
|
|
280
|
+
what: 'Menu buttons and menus behave nonstandardly',
|
|
266
281
|
count: totals[2],
|
|
267
282
|
ordinalSeverity: 2,
|
|
268
283
|
tagName: '',
|
package/testaro/dupAtt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2023 CVS Health and/or one of its affiliates. All rights reserved.
|
|
2
|
+
© 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
3
|
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -47,6 +47,7 @@ exports.reporter = async (page, withItems) => {
|
|
|
47
47
|
if (sourceData.prevented) {
|
|
48
48
|
// Report this.
|
|
49
49
|
data.prevented = true;
|
|
50
|
+
data.error = sourceData.error;
|
|
50
51
|
}
|
|
51
52
|
// Otherwise, i.e. if it was obtained:
|
|
52
53
|
else {
|
package/testaro/focOp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2021–
|
|
2
|
+
© 2021–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
3
|
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -67,7 +67,7 @@ exports.reporter = async (page, withItems) => {
|
|
|
67
67
|
const whats = [
|
|
68
68
|
'Element is Tab-focusable but not operable', 'Elements are Tab-focusable but not operable'
|
|
69
69
|
];
|
|
70
|
-
const
|
|
70
|
+
const result = await report(withItems, all, 'focOp', whats, 2);
|
|
71
71
|
// Reload the page, because isOperable() modified it.
|
|
72
72
|
try {
|
|
73
73
|
await page.reload({timeout: 15000});
|
|
@@ -75,5 +75,5 @@ exports.reporter = async (page, withItems) => {
|
|
|
75
75
|
catch(error) {
|
|
76
76
|
console.log('ERROR: page reload timed out');
|
|
77
77
|
}
|
|
78
|
-
return
|
|
78
|
+
return result;
|
|
79
79
|
};
|
package/testaro/headEl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2023 CVS Health and/or one of its affiliates. All rights reserved.
|
|
2
|
+
© 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
3
|
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -48,6 +48,7 @@ exports.reporter = async page => {
|
|
|
48
48
|
if (sourceData.prevented) {
|
|
49
49
|
// Report this.
|
|
50
50
|
data.prevented = true;
|
|
51
|
+
data.error = 'ERROR getting page source';
|
|
51
52
|
}
|
|
52
53
|
// Otherwise, i.e. if it was obtained:
|
|
53
54
|
else {
|
package/testaro/hover.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2021–
|
|
2
|
+
© 2021–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
3
|
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -70,6 +70,7 @@ exports.reporter = async (page, withItems) => {
|
|
|
70
70
|
await page.mouse.move(0, 0);
|
|
71
71
|
const loc0 = page.locator('body *:visible');
|
|
72
72
|
const elementCount0 = await loc0.count();
|
|
73
|
+
// Hover over the element, whether or not covered.
|
|
73
74
|
try {
|
|
74
75
|
await loc.hover({
|
|
75
76
|
force: true,
|
|
@@ -78,16 +79,23 @@ exports.reporter = async (page, withItems) => {
|
|
|
78
79
|
const loc1 = page.locator('body *:visible');
|
|
79
80
|
const elementCount1 = await loc1.count();
|
|
80
81
|
const additions = elementCount1 - elementCount0;
|
|
81
|
-
// If any elements are:
|
|
82
|
+
// If any elements are added or subtracted:
|
|
82
83
|
if (additions !== 0) {
|
|
83
|
-
// Add the locator and the change of element count to the array of
|
|
84
|
+
// Add the locator and the change of element count to the array of violation locators.
|
|
84
85
|
const impact = additions > 0
|
|
85
86
|
? `added ${additions} elements to the page`
|
|
86
87
|
: `subtracted ${- additions} from the page`;
|
|
87
88
|
all.locs.push([loc, impact]);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
// If hovering times out:
|
|
92
|
+
catch(error) {
|
|
93
|
+
// Report the test prevented.
|
|
94
|
+
const {data} = all.result;
|
|
95
|
+
data.prevented = true;
|
|
96
|
+
data.error = 'ERROR hovering over an element';
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
91
99
|
}
|
|
92
100
|
// Populate and return the result.
|
|
93
101
|
const whats = [
|
package/testaro/opFoc.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2023 CVS Health and/or one of its affiliates. All rights reserved.
|
|
2
|
+
© 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
|
|
3
3
|
|
|
4
4
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
5
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -68,7 +68,7 @@ exports.reporter = async (page, withItems) => {
|
|
|
68
68
|
'Element is operable (__param__) but not Tab-focusable',
|
|
69
69
|
'Elements are operable but not Tab-focusable'
|
|
70
70
|
];
|
|
71
|
-
const
|
|
71
|
+
const result = await report(withItems, all, 'opFoc', whats, 3);
|
|
72
72
|
// Reload the page, because isOperable() modified it.
|
|
73
73
|
try {
|
|
74
74
|
await page.reload({timeout: 15000});
|
|
@@ -76,5 +76,5 @@ exports.reporter = async (page, withItems) => {
|
|
|
76
76
|
catch(error) {
|
|
77
77
|
console.log('ERROR: page reload timed out');
|
|
78
78
|
}
|
|
79
|
-
return
|
|
79
|
+
return result;
|
|
80
80
|
};
|
package/testaro/tabNav.js
CHANGED
|
@@ -28,6 +28,329 @@
|
|
|
28
28
|
|
|
29
29
|
// FUNCTIONS
|
|
30
30
|
|
|
31
|
+
// Returns the text associated with an element.
|
|
32
|
+
const allText = async (page, elementHandle) => await page.evaluate(element => {
|
|
33
|
+
// Identify the element, if specified, or else the focused element.
|
|
34
|
+
const el = element || document.activeElement;
|
|
35
|
+
// Initialize an array of its texts.
|
|
36
|
+
const texts = [];
|
|
37
|
+
// FUNCTION DEFINITION START
|
|
38
|
+
// Removes excess spacing from a string.
|
|
39
|
+
const debloat = text => text.trim().replace(/\s+/g, ' ');
|
|
40
|
+
// FUNCTION DEFINITION END
|
|
41
|
+
// Add any attribute label to the array.
|
|
42
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
43
|
+
if (ariaLabel) {
|
|
44
|
+
const trimmedLabel = debloat(ariaLabel);
|
|
45
|
+
if (trimmedLabel) {
|
|
46
|
+
texts.push(trimmedLabel);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Add any explicit and implicit labels to the array.
|
|
50
|
+
const labelNodeList = el.labels;
|
|
51
|
+
if (labelNodeList && labelNodeList.length) {
|
|
52
|
+
const labels = Array.from(labelNodeList);
|
|
53
|
+
const labelTexts = labels
|
|
54
|
+
.map(label => label.textContent && debloat(label.textContent))
|
|
55
|
+
.filter(text => text);
|
|
56
|
+
if (labelTexts.length) {
|
|
57
|
+
texts.push(...labelTexts);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Add any referenced labels to the array.
|
|
61
|
+
if (el.hasAttribute('aria-labelledby')) {
|
|
62
|
+
const labelerIDs = el.getAttribute('aria-labelledby').split(/\s+/);
|
|
63
|
+
labelerIDs.forEach(id => {
|
|
64
|
+
const labeler = document.getElementById(id);
|
|
65
|
+
if (labeler) {
|
|
66
|
+
const labelerText = debloat(labeler.textContent);
|
|
67
|
+
if (labelerText) {
|
|
68
|
+
texts.push(labelerText);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Add any image text alternatives to the array.
|
|
74
|
+
const altTexts = Array
|
|
75
|
+
.from(element.querySelectorAll('img[alt]:not([alt=""])'))
|
|
76
|
+
.map(img => debloat(img.alt))
|
|
77
|
+
.join('; ');
|
|
78
|
+
if (altTexts.length) {
|
|
79
|
+
texts.push(altTexts);
|
|
80
|
+
}
|
|
81
|
+
// Add the first 100 characters of any text content of the element to the array.
|
|
82
|
+
const ownText = element.textContent;
|
|
83
|
+
if (ownText) {
|
|
84
|
+
const minText = debloat(ownText);
|
|
85
|
+
if (minText) {
|
|
86
|
+
texts.push(minText.slice(0, 100));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Add any ID of the element to the array.
|
|
90
|
+
const id = element.id;
|
|
91
|
+
if (id) {
|
|
92
|
+
texts.push(`#${id}`);
|
|
93
|
+
}
|
|
94
|
+
// Identify a concatenation of the texts.
|
|
95
|
+
let textChain = texts.join('; ');
|
|
96
|
+
// If it is empty:
|
|
97
|
+
if (! textChain) {
|
|
98
|
+
// Substitute the HTML of the element.
|
|
99
|
+
textChain = `{${debloat(element.outerHTML)}}`;
|
|
100
|
+
if (textChain === '{}') {
|
|
101
|
+
textChain = '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Return a concatenation of the texts in the array.
|
|
105
|
+
return textChain;
|
|
106
|
+
}, elementHandle);
|
|
107
|
+
// Returns the index of the focused tab in an array of tabs.
|
|
108
|
+
const focusedTab = async tabs => await page.evaluate(tabs => {
|
|
109
|
+
const focus = document.activeElement;
|
|
110
|
+
return tabs.indexOf(focus);
|
|
111
|
+
}, tabs)
|
|
112
|
+
.catch(error => {
|
|
113
|
+
console.log(`ERROR: could not find focused tab (${error.message})`);
|
|
114
|
+
return -1;
|
|
115
|
+
});
|
|
116
|
+
// Tests a navigation on a tab element.
|
|
117
|
+
const testKey = async (
|
|
118
|
+
tabs, tabElement, keyName, keyProp, goodIndex, elementIsCorrect, itemData
|
|
119
|
+
) => {
|
|
120
|
+
let pressed = true;
|
|
121
|
+
// Click the tab element, to make the focus on it effective.
|
|
122
|
+
await tabElement.click({
|
|
123
|
+
timeout: 500
|
|
124
|
+
})
|
|
125
|
+
.catch(async error => {
|
|
126
|
+
console.log(
|
|
127
|
+
`ERROR clicking tab element ${itemData.text} (${error.message.replace(/\n.+/s, '')})`
|
|
128
|
+
);
|
|
129
|
+
await tabElement.click({
|
|
130
|
+
force: true
|
|
131
|
+
});
|
|
132
|
+
})
|
|
133
|
+
.catch(error => {
|
|
134
|
+
console.log(
|
|
135
|
+
`ERROR force-clicking tab element ${itemData.text} (${error.message.replace(/\n.+/s, '')})`
|
|
136
|
+
);
|
|
137
|
+
pressed = false;
|
|
138
|
+
});
|
|
139
|
+
// Increment the counts of navigations and key navigations.
|
|
140
|
+
data.totals.navigations.all.total++;
|
|
141
|
+
data.totals.navigations.specific[keyProp].total++;
|
|
142
|
+
const {navigationErrors} = itemData;
|
|
143
|
+
// If the click succeeded:
|
|
144
|
+
if (pressed) {
|
|
145
|
+
// Refocus the tab element and press the specified key (page.keyboard.press may fail).
|
|
146
|
+
await tabElement.press(keyName, {
|
|
147
|
+
timeout: 1000
|
|
148
|
+
})
|
|
149
|
+
.catch(error => {
|
|
150
|
+
console.log(`ERROR: could not press ${keyName} (${error.message})`);
|
|
151
|
+
pressed = false;
|
|
152
|
+
});
|
|
153
|
+
// If the refocus and keypress succeeded:
|
|
154
|
+
if (pressed) {
|
|
155
|
+
// Identify which tab element is now focused, if any.
|
|
156
|
+
const focusIndex = await focusedTab(tabs);
|
|
157
|
+
// If the focus is correct:
|
|
158
|
+
if (focusIndex === goodIndex) {
|
|
159
|
+
// Increment the counts of correct navigations and correct key navigations.
|
|
160
|
+
data.totals.navigations.all.correct++;
|
|
161
|
+
data.totals.navigations.specific[keyProp].correct++;
|
|
162
|
+
}
|
|
163
|
+
// Otherwise, i.e. if the focus is incorrect:
|
|
164
|
+
else {
|
|
165
|
+
// Increment the counts of incorrect navigations and incorrect key navigations.
|
|
166
|
+
data.totals.navigations.all.incorrect++;
|
|
167
|
+
data.totals.navigations.specific[keyProp].incorrect++;
|
|
168
|
+
// Update the element status to incorrect.
|
|
169
|
+
elementIsCorrect = false;
|
|
170
|
+
// If itemization is required:
|
|
171
|
+
if (withItems) {
|
|
172
|
+
// Update the element report.
|
|
173
|
+
navigationErrors.push(keyName);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return elementIsCorrect;
|
|
177
|
+
}
|
|
178
|
+
// Otherwise, i.e. if the refocus or keypress failed:
|
|
179
|
+
else {
|
|
180
|
+
// Increment the counts of incorrect navigations and incorrect key navigations.
|
|
181
|
+
data.totals.navigations.all.incorrect++;
|
|
182
|
+
data.totals.navigations.specific[keyProp].incorrect++;
|
|
183
|
+
// If itemization is required and a focus failure has not yet been reported:
|
|
184
|
+
if (withItems && ! navigationErrors.includes('focus')) {
|
|
185
|
+
// Update the element report.
|
|
186
|
+
navigationErrors.push('focus');
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Otherwise, i.e. if the click failed:
|
|
192
|
+
else {
|
|
193
|
+
// Increment the counts of incorrect navigations and incorrect key navigations.
|
|
194
|
+
data.totals.navigations.all.incorrect++;
|
|
195
|
+
data.totals.navigations.specific[keyProp].incorrect++;
|
|
196
|
+
// If itemization is required and a click failure has not yet been reported:
|
|
197
|
+
if (withItems && ! navigationErrors.includes('click')) {
|
|
198
|
+
// Update the element report.
|
|
199
|
+
navigationErrors.push('click');
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
// Returns the index to which an arrow key should move the focus.
|
|
205
|
+
const arrowTarget = (startIndex, tabCount, orientation, direction) => {
|
|
206
|
+
if (orientation === 'horizontal') {
|
|
207
|
+
if (['up', 'down'].includes(direction)) {
|
|
208
|
+
return startIndex;
|
|
209
|
+
}
|
|
210
|
+
else if (direction === 'left') {
|
|
211
|
+
return startIndex ? startIndex - 1 : tabCount - 1;
|
|
212
|
+
}
|
|
213
|
+
else if (direction === 'right') {
|
|
214
|
+
return startIndex === tabCount - 1 ? 0 : startIndex + 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (orientation === 'vertical') {
|
|
218
|
+
if (['left', 'right'].includes(direction)) {
|
|
219
|
+
return startIndex;
|
|
220
|
+
}
|
|
221
|
+
else if (direction === 'up') {
|
|
222
|
+
return startIndex ? startIndex - 1 : tabCount - 1;
|
|
223
|
+
}
|
|
224
|
+
else if (direction === 'down') {
|
|
225
|
+
return startIndex === tabCount - 1 ? 0 : startIndex + 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
/*
|
|
230
|
+
Recursively tests tablist tab elements (per
|
|
231
|
+
https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel)
|
|
232
|
+
*/
|
|
233
|
+
const testTabs = async (tabs, index, listOrientation, listIsCorrect) => {
|
|
234
|
+
const tabCount = tabs.length;
|
|
235
|
+
// If any tab elements remain to be tested:
|
|
236
|
+
if (index < tabCount) {
|
|
237
|
+
// Increment the reported count of tab elements.
|
|
238
|
+
data.totals.tabElements.total++;
|
|
239
|
+
// Identify the tab element to be tested.
|
|
240
|
+
const currentTab = tabs[index];
|
|
241
|
+
// Initialize it as correct.
|
|
242
|
+
let isCorrect = true;
|
|
243
|
+
const itemData = {};
|
|
244
|
+
// If itemization is required:
|
|
245
|
+
if (withItems) {
|
|
246
|
+
let found = true;
|
|
247
|
+
// Initialize a report on the element.
|
|
248
|
+
const moreItemData = await page.evaluate(element => ({
|
|
249
|
+
tagName: element.tagName,
|
|
250
|
+
id: element.id
|
|
251
|
+
}), currentTab)
|
|
252
|
+
.catch(error => {
|
|
253
|
+
console.log(`ERROR: could not get tag name (${error.message})`);
|
|
254
|
+
found = false;
|
|
255
|
+
data.prevented = true;
|
|
256
|
+
return 'ERROR: not found';
|
|
257
|
+
});
|
|
258
|
+
if (found) {
|
|
259
|
+
itemData.tagName = moreItemData.tagName;
|
|
260
|
+
itemData.id = moreItemData.id;
|
|
261
|
+
itemData.text = await allText(page, currentTab);
|
|
262
|
+
itemData.navigationErrors = [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Test the element with each navigation key.
|
|
266
|
+
isCorrect = await testKey(tabs, currentTab, 'Tab', 'tab', -1, isCorrect, itemData);
|
|
267
|
+
isCorrect = await testKey(
|
|
268
|
+
tabs,
|
|
269
|
+
currentTab,
|
|
270
|
+
'ArrowLeft',
|
|
271
|
+
'left',
|
|
272
|
+
arrowTarget(index, tabCount, listOrientation, 'left'),
|
|
273
|
+
isCorrect,
|
|
274
|
+
itemData
|
|
275
|
+
);
|
|
276
|
+
isCorrect = await testKey(
|
|
277
|
+
tabs,
|
|
278
|
+
currentTab,
|
|
279
|
+
'ArrowRight',
|
|
280
|
+
'right',
|
|
281
|
+
arrowTarget(index, tabCount, listOrientation, 'right'),
|
|
282
|
+
isCorrect,
|
|
283
|
+
itemData
|
|
284
|
+
);
|
|
285
|
+
isCorrect = await testKey(
|
|
286
|
+
tabs,
|
|
287
|
+
currentTab,
|
|
288
|
+
'ArrowUp',
|
|
289
|
+
'up',
|
|
290
|
+
arrowTarget(index, tabCount, listOrientation, 'up'),
|
|
291
|
+
isCorrect,
|
|
292
|
+
itemData
|
|
293
|
+
);
|
|
294
|
+
isCorrect = await testKey(
|
|
295
|
+
tabs,
|
|
296
|
+
currentTab,
|
|
297
|
+
'ArrowDown',
|
|
298
|
+
'down',
|
|
299
|
+
arrowTarget(index, tabCount, listOrientation, 'down'),
|
|
300
|
+
isCorrect,
|
|
301
|
+
itemData
|
|
302
|
+
);
|
|
303
|
+
isCorrect = await testKey(tabs, currentTab, 'Home', 'home', 0, isCorrect, itemData);
|
|
304
|
+
isCorrect = await testKey(
|
|
305
|
+
tabs, currentTab, 'End', 'end', tabCount - 1, isCorrect, itemData
|
|
306
|
+
);
|
|
307
|
+
// Update the tablist status (Node 14 does not support the ES 2021 &&= operator).
|
|
308
|
+
listIsCorrect = listIsCorrect && isCorrect;
|
|
309
|
+
// Increment the data.
|
|
310
|
+
data.totals.tabElements[isCorrect ? 'correct' : 'incorrect']++;
|
|
311
|
+
if (withItems) {
|
|
312
|
+
data.tabElements[isCorrect ? 'correct' : 'incorrect'].push(itemData);
|
|
313
|
+
}
|
|
314
|
+
// Process the next tab element.
|
|
315
|
+
return await testTabs(tabs, index + 1, listOrientation, listIsCorrect);
|
|
316
|
+
}
|
|
317
|
+
// Otherwise, i.e. if all tab elements have been tested:
|
|
318
|
+
else {
|
|
319
|
+
// Return whether the tablist is correct.
|
|
320
|
+
return listIsCorrect;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
// Recursively tests tablists.
|
|
324
|
+
const testTabLists = async tabLists => {
|
|
325
|
+
// If any tablists remain to be tested:
|
|
326
|
+
if (tabLists.length) {
|
|
327
|
+
const firstTabList = tabLists[0];
|
|
328
|
+
let orientation = await firstTabList.getAttribute('aria-orientation')
|
|
329
|
+
.catch(error=> {
|
|
330
|
+
console.log(`ERROR: could not get tab-list orientation (${error.message})`);
|
|
331
|
+
return 'ERROR';
|
|
332
|
+
});
|
|
333
|
+
if (! orientation) {
|
|
334
|
+
orientation = 'horizontal';
|
|
335
|
+
}
|
|
336
|
+
if (orientation === 'ERROR') {
|
|
337
|
+
data.prevented = true;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
const tabs = await firstTabList.$$('[role=tab]');
|
|
341
|
+
// If the tablist contains at least 2 tab elements:
|
|
342
|
+
if (tabs.length > 1) {
|
|
343
|
+
// Test them.
|
|
344
|
+
const isCorrect = await testTabs(tabs, 0, orientation, true);
|
|
345
|
+
// Increment the data.
|
|
346
|
+
data.totals.tabLists.total++;
|
|
347
|
+
data.totals.tabLists[isCorrect ? 'correct' : 'incorrect']++;
|
|
348
|
+
// Process the remaining tablists.
|
|
349
|
+
await testTabLists(tabLists.slice(1));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
31
354
|
// Tests tab-list navigation and reports results.
|
|
32
355
|
exports.reporter = async (page, withItems) => {
|
|
33
356
|
// Initialize the results.
|
|
@@ -98,332 +421,6 @@ exports.reporter = async (page, withItems) => {
|
|
|
98
421
|
// Get an array of element handles for the visible tablists.
|
|
99
422
|
const tabLists = await page.$$('[role=tablist]:visible');
|
|
100
423
|
if (tabLists.length) {
|
|
101
|
-
// FUNCTION DEFINITIONS START
|
|
102
|
-
// Returns text associated with an element.
|
|
103
|
-
// Returns the text associated with an element.
|
|
104
|
-
const allText = async (page, elementHandle) => await page.evaluate(element => {
|
|
105
|
-
// Identify the element, if specified, or else the focused element.
|
|
106
|
-
const el = element || document.activeElement;
|
|
107
|
-
// Initialize an array of its texts.
|
|
108
|
-
const texts = [];
|
|
109
|
-
// FUNCTION DEFINITION START
|
|
110
|
-
// Removes excess spacing from a string.
|
|
111
|
-
const debloat = text => text.trim().replace(/\s+/g, ' ');
|
|
112
|
-
// FUNCTION DEFINITION END
|
|
113
|
-
// Add any attribute label to the array.
|
|
114
|
-
const ariaLabel = el.getAttribute('aria-label');
|
|
115
|
-
if (ariaLabel) {
|
|
116
|
-
const trimmedLabel = debloat(ariaLabel);
|
|
117
|
-
if (trimmedLabel) {
|
|
118
|
-
texts.push(trimmedLabel);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
// Add any explicit and implicit labels to the array.
|
|
122
|
-
const labelNodeList = el.labels;
|
|
123
|
-
if (labelNodeList && labelNodeList.length) {
|
|
124
|
-
const labels = Array.from(labelNodeList);
|
|
125
|
-
const labelTexts = labels
|
|
126
|
-
.map(label => label.textContent && debloat(label.textContent))
|
|
127
|
-
.filter(text => text);
|
|
128
|
-
if (labelTexts.length) {
|
|
129
|
-
texts.push(...labelTexts);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Add any referenced labels to the array.
|
|
133
|
-
if (el.hasAttribute('aria-labelledby')) {
|
|
134
|
-
const labelerIDs = el.getAttribute('aria-labelledby').split(/\s+/);
|
|
135
|
-
labelerIDs.forEach(id => {
|
|
136
|
-
const labeler = document.getElementById(id);
|
|
137
|
-
if (labeler) {
|
|
138
|
-
const labelerText = debloat(labeler.textContent);
|
|
139
|
-
if (labelerText) {
|
|
140
|
-
texts.push(labelerText);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
// Add any image text alternatives to the array.
|
|
146
|
-
const altTexts = Array
|
|
147
|
-
.from(element.querySelectorAll('img[alt]:not([alt=""])'))
|
|
148
|
-
.map(img => debloat(img.alt))
|
|
149
|
-
.join('; ');
|
|
150
|
-
if (altTexts.length) {
|
|
151
|
-
texts.push(altTexts);
|
|
152
|
-
}
|
|
153
|
-
// Add the first 100 characters of any text content of the element to the array.
|
|
154
|
-
const ownText = element.textContent;
|
|
155
|
-
if (ownText) {
|
|
156
|
-
const minText = debloat(ownText);
|
|
157
|
-
if (minText) {
|
|
158
|
-
texts.push(minText.slice(0, 100));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
// Add any ID of the element to the array.
|
|
162
|
-
const id = element.id;
|
|
163
|
-
if (id) {
|
|
164
|
-
texts.push(`#${id}`);
|
|
165
|
-
}
|
|
166
|
-
// Identify a concatenation of the texts.
|
|
167
|
-
let textChain = texts.join('; ');
|
|
168
|
-
// If it is empty:
|
|
169
|
-
if (! textChain) {
|
|
170
|
-
// Substitute the HTML of the element.
|
|
171
|
-
textChain = `{${debloat(element.outerHTML)}}`;
|
|
172
|
-
if (textChain === '{}') {
|
|
173
|
-
textChain = '';
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// Return a concatenation of the texts in the array.
|
|
177
|
-
return textChain;
|
|
178
|
-
}, elementHandle);
|
|
179
|
-
// Returns the index of the focused tab in an array of tabs.
|
|
180
|
-
const focusedTab = async tabs => await page.evaluate(tabs => {
|
|
181
|
-
const focus = document.activeElement;
|
|
182
|
-
return tabs.indexOf(focus);
|
|
183
|
-
}, tabs)
|
|
184
|
-
.catch(error => {
|
|
185
|
-
console.log(`ERROR: could not find focused tab (${error.message})`);
|
|
186
|
-
return -1;
|
|
187
|
-
});
|
|
188
|
-
// Tests a navigation on a tab element.
|
|
189
|
-
const testKey = async (
|
|
190
|
-
tabs, tabElement, keyName, keyProp, goodIndex, elementIsCorrect, itemData
|
|
191
|
-
) => {
|
|
192
|
-
let pressed = true;
|
|
193
|
-
// Click the tab element, to make the focus on it effective.
|
|
194
|
-
await tabElement.click({
|
|
195
|
-
timeout: 500
|
|
196
|
-
})
|
|
197
|
-
.catch(async error => {
|
|
198
|
-
console.log(
|
|
199
|
-
`ERROR clicking tab element ${itemData.text} (${error.message.replace(/\n.+/s, '')})`
|
|
200
|
-
);
|
|
201
|
-
await tabElement.click({
|
|
202
|
-
force: true
|
|
203
|
-
});
|
|
204
|
-
})
|
|
205
|
-
.catch(error => {
|
|
206
|
-
console.log(
|
|
207
|
-
`ERROR force-clicking tab element ${itemData.text} (${error.message.replace(/\n.+/s, '')})`
|
|
208
|
-
);
|
|
209
|
-
pressed = false;
|
|
210
|
-
});
|
|
211
|
-
// Increment the counts of navigations and key navigations.
|
|
212
|
-
data.totals.navigations.all.total++;
|
|
213
|
-
data.totals.navigations.specific[keyProp].total++;
|
|
214
|
-
const {navigationErrors} = itemData;
|
|
215
|
-
// If the click succeeded:
|
|
216
|
-
if (pressed) {
|
|
217
|
-
// Refocus the tab element and press the specified key (page.keyboard.press may fail).
|
|
218
|
-
await tabElement.press(keyName, {
|
|
219
|
-
timeout: 1000
|
|
220
|
-
})
|
|
221
|
-
.catch(error => {
|
|
222
|
-
console.log(`ERROR: could not press ${keyName} (${error.message})`);
|
|
223
|
-
pressed = false;
|
|
224
|
-
});
|
|
225
|
-
// If the refocus and keypress succeeded:
|
|
226
|
-
if (pressed) {
|
|
227
|
-
// Identify which tab element is now focused, if any.
|
|
228
|
-
const focusIndex = await focusedTab(tabs);
|
|
229
|
-
// If the focus is correct:
|
|
230
|
-
if (focusIndex === goodIndex) {
|
|
231
|
-
// Increment the counts of correct navigations and correct key navigations.
|
|
232
|
-
data.totals.navigations.all.correct++;
|
|
233
|
-
data.totals.navigations.specific[keyProp].correct++;
|
|
234
|
-
}
|
|
235
|
-
// Otherwise, i.e. if the focus is incorrect:
|
|
236
|
-
else {
|
|
237
|
-
// Increment the counts of incorrect navigations and incorrect key navigations.
|
|
238
|
-
data.totals.navigations.all.incorrect++;
|
|
239
|
-
data.totals.navigations.specific[keyProp].incorrect++;
|
|
240
|
-
// Update the element status to incorrect.
|
|
241
|
-
elementIsCorrect = false;
|
|
242
|
-
// If itemization is required:
|
|
243
|
-
if (withItems) {
|
|
244
|
-
// Update the element report.
|
|
245
|
-
navigationErrors.push(keyName);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return elementIsCorrect;
|
|
249
|
-
}
|
|
250
|
-
// Otherwise, i.e. if the refocus or keypress failed:
|
|
251
|
-
else {
|
|
252
|
-
// Increment the counts of incorrect navigations and incorrect key navigations.
|
|
253
|
-
data.totals.navigations.all.incorrect++;
|
|
254
|
-
data.totals.navigations.specific[keyProp].incorrect++;
|
|
255
|
-
// If itemization is required and a focus failure has not yet been reported:
|
|
256
|
-
if (withItems && ! navigationErrors.includes('focus')) {
|
|
257
|
-
// Update the element report.
|
|
258
|
-
navigationErrors.push('focus');
|
|
259
|
-
}
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
// Otherwise, i.e. if the click failed:
|
|
264
|
-
else {
|
|
265
|
-
// Increment the counts of incorrect navigations and incorrect key navigations.
|
|
266
|
-
data.totals.navigations.all.incorrect++;
|
|
267
|
-
data.totals.navigations.specific[keyProp].incorrect++;
|
|
268
|
-
// If itemization is required and a click failure has not yet been reported:
|
|
269
|
-
if (withItems && ! navigationErrors.includes('click')) {
|
|
270
|
-
// Update the element report.
|
|
271
|
-
navigationErrors.push('click');
|
|
272
|
-
}
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
// Returns the index to which an arrow key should move the focus.
|
|
277
|
-
const arrowTarget = (startIndex, tabCount, orientation, direction) => {
|
|
278
|
-
if (orientation === 'horizontal') {
|
|
279
|
-
if (['up', 'down'].includes(direction)) {
|
|
280
|
-
return startIndex;
|
|
281
|
-
}
|
|
282
|
-
else if (direction === 'left') {
|
|
283
|
-
return startIndex ? startIndex - 1 : tabCount - 1;
|
|
284
|
-
}
|
|
285
|
-
else if (direction === 'right') {
|
|
286
|
-
return startIndex === tabCount - 1 ? 0 : startIndex + 1;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
else if (orientation === 'vertical') {
|
|
290
|
-
if (['left', 'right'].includes(direction)) {
|
|
291
|
-
return startIndex;
|
|
292
|
-
}
|
|
293
|
-
else if (direction === 'up') {
|
|
294
|
-
return startIndex ? startIndex - 1 : tabCount - 1;
|
|
295
|
-
}
|
|
296
|
-
else if (direction === 'down') {
|
|
297
|
-
return startIndex === tabCount - 1 ? 0 : startIndex + 1;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
/*
|
|
302
|
-
Recursively tests tablist tab elements (per
|
|
303
|
-
https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel)
|
|
304
|
-
*/
|
|
305
|
-
const testTabs = async (tabs, index, listOrientation, listIsCorrect) => {
|
|
306
|
-
const tabCount = tabs.length;
|
|
307
|
-
// If any tab elements remain to be tested:
|
|
308
|
-
if (index < tabCount) {
|
|
309
|
-
// Increment the reported count of tab elements.
|
|
310
|
-
data.totals.tabElements.total++;
|
|
311
|
-
// Identify the tab element to be tested.
|
|
312
|
-
const currentTab = tabs[index];
|
|
313
|
-
// Initialize it as correct.
|
|
314
|
-
let isCorrect = true;
|
|
315
|
-
const itemData = {};
|
|
316
|
-
// If itemization is required:
|
|
317
|
-
if (withItems) {
|
|
318
|
-
let found = true;
|
|
319
|
-
// Initialize a report on the element.
|
|
320
|
-
const moreItemData = await page.evaluate(element => ({
|
|
321
|
-
tagName: element.tagName,
|
|
322
|
-
id: element.id
|
|
323
|
-
}), currentTab)
|
|
324
|
-
.catch(error => {
|
|
325
|
-
console.log(`ERROR: could not get tag name (${error.message})`);
|
|
326
|
-
found = false;
|
|
327
|
-
data.prevented = true;
|
|
328
|
-
return 'ERROR: not found';
|
|
329
|
-
});
|
|
330
|
-
if (found) {
|
|
331
|
-
itemData.tagName = moreItemData.tagName;
|
|
332
|
-
itemData.id = moreItemData.id;
|
|
333
|
-
itemData.text = await allText(page, currentTab);
|
|
334
|
-
itemData.navigationErrors = [];
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// Test the element with each navigation key.
|
|
338
|
-
isCorrect = await testKey(tabs, currentTab, 'Tab', 'tab', -1, isCorrect, itemData);
|
|
339
|
-
isCorrect = await testKey(
|
|
340
|
-
tabs,
|
|
341
|
-
currentTab,
|
|
342
|
-
'ArrowLeft',
|
|
343
|
-
'left',
|
|
344
|
-
arrowTarget(index, tabCount, listOrientation, 'left'),
|
|
345
|
-
isCorrect,
|
|
346
|
-
itemData
|
|
347
|
-
);
|
|
348
|
-
isCorrect = await testKey(
|
|
349
|
-
tabs,
|
|
350
|
-
currentTab,
|
|
351
|
-
'ArrowRight',
|
|
352
|
-
'right',
|
|
353
|
-
arrowTarget(index, tabCount, listOrientation, 'right'),
|
|
354
|
-
isCorrect,
|
|
355
|
-
itemData
|
|
356
|
-
);
|
|
357
|
-
isCorrect = await testKey(
|
|
358
|
-
tabs,
|
|
359
|
-
currentTab,
|
|
360
|
-
'ArrowUp',
|
|
361
|
-
'up',
|
|
362
|
-
arrowTarget(index, tabCount, listOrientation, 'up'),
|
|
363
|
-
isCorrect,
|
|
364
|
-
itemData
|
|
365
|
-
);
|
|
366
|
-
isCorrect = await testKey(
|
|
367
|
-
tabs,
|
|
368
|
-
currentTab,
|
|
369
|
-
'ArrowDown',
|
|
370
|
-
'down',
|
|
371
|
-
arrowTarget(index, tabCount, listOrientation, 'down'),
|
|
372
|
-
isCorrect,
|
|
373
|
-
itemData
|
|
374
|
-
);
|
|
375
|
-
isCorrect = await testKey(tabs, currentTab, 'Home', 'home', 0, isCorrect, itemData);
|
|
376
|
-
isCorrect = await testKey(
|
|
377
|
-
tabs, currentTab, 'End', 'end', tabCount - 1, isCorrect, itemData
|
|
378
|
-
);
|
|
379
|
-
// Update the tablist status (Node 14 does not support the ES 2021 &&= operator).
|
|
380
|
-
listIsCorrect = listIsCorrect && isCorrect;
|
|
381
|
-
// Increment the data.
|
|
382
|
-
data.totals.tabElements[isCorrect ? 'correct' : 'incorrect']++;
|
|
383
|
-
if (withItems) {
|
|
384
|
-
data.tabElements[isCorrect ? 'correct' : 'incorrect'].push(itemData);
|
|
385
|
-
}
|
|
386
|
-
// Process the next tab element.
|
|
387
|
-
return await testTabs(tabs, index + 1, listOrientation, listIsCorrect);
|
|
388
|
-
}
|
|
389
|
-
// Otherwise, i.e. if all tab elements have been tested:
|
|
390
|
-
else {
|
|
391
|
-
// Return whether the tablist is correct.
|
|
392
|
-
return listIsCorrect;
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
// Recursively tests tablists.
|
|
396
|
-
const testTabLists = async tabLists => {
|
|
397
|
-
// If any tablists remain to be tested:
|
|
398
|
-
if (tabLists.length) {
|
|
399
|
-
const firstTabList = tabLists[0];
|
|
400
|
-
let orientation = await firstTabList.getAttribute('aria-orientation')
|
|
401
|
-
.catch(error=> {
|
|
402
|
-
console.log(`ERROR: could not get tab-list orientation (${error.message})`);
|
|
403
|
-
return 'ERROR';
|
|
404
|
-
});
|
|
405
|
-
if (! orientation) {
|
|
406
|
-
orientation = 'horizontal';
|
|
407
|
-
}
|
|
408
|
-
if (orientation === 'ERROR') {
|
|
409
|
-
data.prevented = true;
|
|
410
|
-
}
|
|
411
|
-
else {
|
|
412
|
-
const tabs = await firstTabList.$$('[role=tab]');
|
|
413
|
-
// If the tablist contains at least 2 tab elements:
|
|
414
|
-
if (tabs.length > 1) {
|
|
415
|
-
// Test them.
|
|
416
|
-
const isCorrect = await testTabs(tabs, 0, orientation, true);
|
|
417
|
-
// Increment the data.
|
|
418
|
-
data.totals.tabLists.total++;
|
|
419
|
-
data.totals.tabLists[isCorrect ? 'correct' : 'incorrect']++;
|
|
420
|
-
// Process the remaining tablists.
|
|
421
|
-
await testTabLists(tabLists.slice(1));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
// FUNCTION DEFINITIONS END
|
|
427
424
|
await testTabLists(tabLists);
|
|
428
425
|
// Reload the page, because keyboard navigation may have triggered content changes.
|
|
429
426
|
try {
|
package/testaro/textNodes.js
CHANGED
|
@@ -154,7 +154,7 @@ exports.reporter = async (page, withItems, detailLevel, text = '') => {
|
|
|
154
154
|
console.log(`ERROR performing test (${error.message.replace(/\n.+/s, '')})`);
|
|
155
155
|
data = {
|
|
156
156
|
prevented: true,
|
|
157
|
-
error: '
|
|
157
|
+
error: 'Test failed'
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
160
|
// Return the result.
|
package/tests/testaro.js
CHANGED
|
@@ -52,10 +52,23 @@ const futureEvalRulesCleanRoom = {
|
|
|
52
52
|
phOnly: 'input elements with placeholders but no accessible names'
|
|
53
53
|
};
|
|
54
54
|
*/
|
|
55
|
+
const futureRules = new Set([
|
|
56
|
+
'altScheme',
|
|
57
|
+
'captionLoc',
|
|
58
|
+
'dataListRef',
|
|
59
|
+
'secHeading',
|
|
60
|
+
'textSem',
|
|
61
|
+
'adbID',
|
|
62
|
+
'imageLink',
|
|
63
|
+
'legendLoc',
|
|
64
|
+
'optRoleSel',
|
|
65
|
+
'phOnly'
|
|
66
|
+
]);
|
|
55
67
|
const evalRules = {
|
|
56
68
|
allCaps: 'leaf elements with entirely upper-case text longer than 7 characters',
|
|
57
69
|
allHidden: 'page that is entirely or mostly hidden',
|
|
58
70
|
allSlanted: 'leaf elements with entirely italic or oblique text longer than 39 characters',
|
|
71
|
+
attVal: 'duplicate attribute values',
|
|
59
72
|
autocomplete: 'name and email inputs without autocomplete attributes',
|
|
60
73
|
bulk: 'large count of visible elements',
|
|
61
74
|
buttonMenu: 'nonstandard keyboard navigation between items of button-controlled menus',
|
|
@@ -75,8 +88,8 @@ const evalRules = {
|
|
|
75
88
|
hr: 'hr element instead of styles used for vertical segmentation',
|
|
76
89
|
labClash: 'labeling inconsistencies',
|
|
77
90
|
lineHeight: 'text with a line height less than 1.5 times its font size',
|
|
78
|
-
linkExt: 'links that automatically open new windows',
|
|
79
91
|
linkAmb: 'links with identical texts but different destinations',
|
|
92
|
+
linkExt: 'links that automatically open new windows',
|
|
80
93
|
linkOldAtt: 'links with deprecated attributes',
|
|
81
94
|
linkTitle: 'links with title attributes repeating text content',
|
|
82
95
|
linkTo: 'links without destinations',
|
|
@@ -148,16 +161,24 @@ exports.reporter = async (page, options) => {
|
|
|
148
161
|
if (
|
|
149
162
|
rules.length > 1
|
|
150
163
|
&& ['y', 'n'].includes(rules[0])
|
|
151
|
-
&& rules.slice(1).every(rule =>
|
|
164
|
+
&& rules.slice(1).every(rule => {
|
|
165
|
+
if (evalRules[rule] || etcRules[rule] || futureRules.has(rule)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.log(`ERROR: Testaro rule ${rule} invalid`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
})
|
|
152
173
|
) {
|
|
153
174
|
// Wait 1 second to prevent out-of-order logging with granular reporting.
|
|
154
175
|
await wait(1000);
|
|
155
|
-
// For each rule invoked:
|
|
176
|
+
// For each rule invoked except future rules:
|
|
156
177
|
const calledRules = rules[0] === 'y'
|
|
157
178
|
? rules.slice(1)
|
|
158
179
|
: Object.keys(evalRules).filter(ruleID => ! rules.slice(1).includes(ruleID));
|
|
159
180
|
const testTimes = [];
|
|
160
|
-
for (const rule of calledRules) {
|
|
181
|
+
for (const rule of calledRules.filter(rule => ! futureRules.has(rule))) {
|
|
161
182
|
// Initialize an argument array.
|
|
162
183
|
const ruleArgs = [page, withItems];
|
|
163
184
|
// If the rule is defined with JavaScript or JSON but not both:
|
|
@@ -217,7 +238,7 @@ exports.reporter = async (page, options) => {
|
|
|
217
238
|
// Otherwise, i.e. if the rule is undefined or doubly defined:
|
|
218
239
|
else {
|
|
219
240
|
// Report this.
|
|
220
|
-
data.
|
|
241
|
+
data.rulesInvalid.push(rule);
|
|
221
242
|
console.log(`ERROR: Rule ${rule} not validly defined`);
|
|
222
243
|
}
|
|
223
244
|
}
|