testaro 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +502 -0
- package/aceconfig.js +7 -0
- package/commands.js +249 -0
- package/index.js +1248 -0
- package/package.json +39 -0
- package/procs/score/asp09.js +555 -0
- package/procs/test/allText.js +76 -0
- package/procs/test/allVis.js +17 -0
- package/procs/test/linksByType.js +90 -0
- package/procs/test/textOf.txt +73 -0
- package/scoring/correlation.js +74 -0
- package/scoring/correlations.json +327 -0
- package/scoring/data.json +26021 -0
- package/scoring/dupCounts.js +39 -0
- package/scoring/dupCounts.json +112 -0
- package/scoring/duplications.json +253 -0
- package/scoring/issues.json +304 -0
- package/scoring/packageData.js +171 -0
- package/scoring/packageIssues.js +34 -0
- package/scoring/rulesetData.json +15 -0
- package/tests/aatt.js +64 -0
- package/tests/alfa.js +107 -0
- package/tests/axe.js +109 -0
- package/tests/bulk.js +21 -0
- package/tests/embAc.js +36 -0
- package/tests/focAll.js +62 -0
- package/tests/focInd.js +99 -0
- package/tests/focOp.js +132 -0
- package/tests/hover.js +195 -0
- package/tests/ibm.js +89 -0
- package/tests/labClash.js +157 -0
- package/tests/linkUl.js +65 -0
- package/tests/menuNav.js +254 -0
- package/tests/motion.js +115 -0
- package/tests/radioSet.js +87 -0
- package/tests/role.js +164 -0
- package/tests/styleDiff.js +146 -0
- package/tests/tabNav.js +282 -0
- package/tests/wave.js +44 -0
- package/tests/zIndex.js +49 -0
- package/validation/batches/sample.json +13 -0
- package/validation/executors/sample.js +11 -0
- package/validation/scripts/app/sample.json +21 -0
- package/validation/scripts/test/bulk.json +39 -0
- package/validation/scripts/test/embAc.json +45 -0
- package/validation/scripts/test/focAll.json +59 -0
- package/validation/scripts/test/focInd.json +55 -0
- package/validation/scripts/test/focOp.json +53 -0
- package/validation/scripts/test/hover.json +47 -0
- package/validation/scripts/test/labClash.json +43 -0
- package/validation/scripts/test/linkUl.json +62 -0
- package/validation/scripts/test/menuNav.json +97 -0
- package/validation/scripts/test/motion.json +53 -0
- package/validation/scripts/test/radioSet.json +43 -0
- package/validation/scripts/test/role.json +42 -0
- package/validation/scripts/test/styleDiff.json +61 -0
- package/validation/scripts/test/tabNav.json +97 -0
- package/validation/scripts/test/zIndex.json +40 -0
- package/validation/targets/bulk/bad.html +48 -0
- package/validation/targets/bulk/good.html +15 -0
- package/validation/targets/embAc/bad.html +21 -0
- package/validation/targets/embAc/good.html +15 -0
- package/validation/targets/focAll/good.html +15 -0
- package/validation/targets/focAll/less.html +15 -0
- package/validation/targets/focAll/more.html +16 -0
- package/validation/targets/focInd/bad.html +31 -0
- package/validation/targets/focInd/good.html +22 -0
- package/validation/targets/focOp/bad.html +18 -0
- package/validation/targets/focOp/good.html +15 -0
- package/validation/targets/hover/bad.html +19 -0
- package/validation/targets/hover/good.html +15 -0
- package/validation/targets/labClash/bad.html +20 -0
- package/validation/targets/labClash/good.html +18 -0
- package/validation/targets/linkUl/bad.html +16 -0
- package/validation/targets/linkUl/good.html +30 -0
- package/validation/targets/linkUl/na.html +20 -0
- package/validation/targets/menuNav/bad.html +106 -0
- package/validation/targets/menuNav/bad.js +348 -0
- package/validation/targets/menuNav/good.html +106 -0
- package/validation/targets/menuNav/good.js +365 -0
- package/validation/targets/menuNav/style.css +22 -0
- package/validation/targets/motion/bad.css +15 -0
- package/validation/targets/motion/bad.html +16 -0
- package/validation/targets/motion/good.html +15 -0
- package/validation/targets/radioSet/bad.html +34 -0
- package/validation/targets/radioSet/good.html +27 -0
- package/validation/targets/role/bad.html +26 -0
- package/validation/targets/role/good.html +22 -0
- package/validation/targets/styleDiff/bad.html +35 -0
- package/validation/targets/styleDiff/good.html +36 -0
- package/validation/targets/tabNav/bad.html +51 -0
- package/validation/targets/tabNav/bad.js +35 -0
- package/validation/targets/tabNav/good.html +53 -0
- package/validation/targets/tabNav/good.js +83 -0
- package/validation/targets/tabNav/goodMoz.js +206 -0
- package/validation/targets/tabNav/style.css +34 -0
- package/validation/targets/zIndex/bad.html +17 -0
- package/validation/targets/zIndex/good.html +15 -0
package/tests/linkUl.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
linkUl
|
|
3
|
+
This test reports failures to underline inline links. Underlining and color are the
|
|
4
|
+
traditional style properties that identify links. Collections of links in blocks can be
|
|
5
|
+
recognized without underlines, but inline links are difficult or impossible to distinguish
|
|
6
|
+
visually from surrounding text if not underlined. Underlining inline links only on hover
|
|
7
|
+
provides an indicator valuable only to mouse users, and even they must traverse the text with
|
|
8
|
+
a mouse merely to discover which passages are links.
|
|
9
|
+
*/
|
|
10
|
+
exports.reporter = async (page, withItems) => {
|
|
11
|
+
// Identify the links in the page, by type.
|
|
12
|
+
const linkTypes = await require('../procs/test/linksByType').linksByType(page);
|
|
13
|
+
return await page.evaluate(args => {
|
|
14
|
+
const withItems = args[0];
|
|
15
|
+
const linkTypes = args[1];
|
|
16
|
+
// FUNCTION DEFINITION START
|
|
17
|
+
// Returns a space-minimized copy of a string.
|
|
18
|
+
const compact = string => string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim();
|
|
19
|
+
// FUNCTION DEFINITION END
|
|
20
|
+
// Identify the inline links.
|
|
21
|
+
const inLinks = linkTypes.inline;
|
|
22
|
+
const inLinkCount = inLinks.length;
|
|
23
|
+
let underlined = 0;
|
|
24
|
+
const ulInLinkTexts = [];
|
|
25
|
+
const nulInLinkTexts = [];
|
|
26
|
+
// For each of them:
|
|
27
|
+
inLinks.forEach(link => {
|
|
28
|
+
// Identify the text of the link if itemization is required.
|
|
29
|
+
const text = withItems ? compact(link.textContent) : '';
|
|
30
|
+
// If it is underlined:
|
|
31
|
+
if (window.getComputedStyle(link).textDecorationLine === 'underline') {
|
|
32
|
+
// Increment the count of underlined inline links.
|
|
33
|
+
underlined++;
|
|
34
|
+
// If required, add its text to the array of their texts.
|
|
35
|
+
if (withItems) {
|
|
36
|
+
ulInLinkTexts.push(text);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Otherwise, if it is not underlined and itemization is required:
|
|
40
|
+
else if (withItems) {
|
|
41
|
+
// Add its text to the array of texts of non-underlined inline links.
|
|
42
|
+
nulInLinkTexts.push(text);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
// Get the percentage of underlined links among all inline links.
|
|
46
|
+
const underlinedPercent = inLinkCount ? Math.floor(100 * underlined / inLinkCount) : 'N/A';
|
|
47
|
+
const data = {
|
|
48
|
+
totals: {
|
|
49
|
+
links: inLinks.length + linkTypes.block.length,
|
|
50
|
+
inline: {
|
|
51
|
+
total: inLinkCount,
|
|
52
|
+
underlined,
|
|
53
|
+
underlinedPercent
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
if (withItems) {
|
|
58
|
+
data.items = {
|
|
59
|
+
underlined: ulInLinkTexts,
|
|
60
|
+
notUnderlined: nulInLinkTexts
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {result: data};
|
|
64
|
+
}, [withItems, linkTypes]);
|
|
65
|
+
};
|
package/tests/menuNav.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/*
|
|
2
|
+
menuNav
|
|
3
|
+
This test reports nonstandard keyboard navigation among menu items in menus that manage true
|
|
4
|
+
focus. Menus that use pseudofocus with the 'aria-activedescendant' attribute are not tested.
|
|
5
|
+
Standards are based on https://www.w3.org/TR/wai-aria-practices-1.1/#menu.
|
|
6
|
+
*/
|
|
7
|
+
exports.reporter = async (page, withItems) => {
|
|
8
|
+
// Initialize a report.
|
|
9
|
+
const data = {
|
|
10
|
+
totals: {
|
|
11
|
+
navigations: {
|
|
12
|
+
all: {
|
|
13
|
+
total: 0,
|
|
14
|
+
correct: 0,
|
|
15
|
+
incorrect: 0
|
|
16
|
+
},
|
|
17
|
+
specific: {
|
|
18
|
+
tab: {
|
|
19
|
+
total: 0,
|
|
20
|
+
correct: 0,
|
|
21
|
+
incorrect: 0
|
|
22
|
+
},
|
|
23
|
+
left: {
|
|
24
|
+
total: 0,
|
|
25
|
+
correct: 0,
|
|
26
|
+
incorrect: 0
|
|
27
|
+
},
|
|
28
|
+
right: {
|
|
29
|
+
total: 0,
|
|
30
|
+
correct: 0,
|
|
31
|
+
incorrect: 0
|
|
32
|
+
},
|
|
33
|
+
up: {
|
|
34
|
+
total: 0,
|
|
35
|
+
correct: 0,
|
|
36
|
+
incorrect: 0
|
|
37
|
+
},
|
|
38
|
+
down: {
|
|
39
|
+
total: 0,
|
|
40
|
+
correct: 0,
|
|
41
|
+
incorrect: 0
|
|
42
|
+
},
|
|
43
|
+
home: {
|
|
44
|
+
total: 0,
|
|
45
|
+
correct: 0,
|
|
46
|
+
incorrect: 0
|
|
47
|
+
},
|
|
48
|
+
end: {
|
|
49
|
+
total: 0,
|
|
50
|
+
correct: 0,
|
|
51
|
+
incorrect: 0
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
menuItems: {
|
|
56
|
+
total: 0,
|
|
57
|
+
correct: 0,
|
|
58
|
+
incorrect: 0
|
|
59
|
+
},
|
|
60
|
+
menus: {
|
|
61
|
+
total: 0,
|
|
62
|
+
correct: 0,
|
|
63
|
+
incorrect: 0
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
if (withItems) {
|
|
68
|
+
data.menuItems = {
|
|
69
|
+
incorrect: [],
|
|
70
|
+
correct: []
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Identify an array of the true-focus menus.
|
|
74
|
+
const menus = await page.$$(
|
|
75
|
+
'[role=menu]:not([aria-activedescendant]), [role=menubar]:not([aria-activedescendant])'
|
|
76
|
+
);
|
|
77
|
+
if (menus.length) {
|
|
78
|
+
// FUNCTION DEFINITIONS START
|
|
79
|
+
// Returns text associated with an element.
|
|
80
|
+
const {allText} = require('../procs/test/allText');
|
|
81
|
+
// Returns the index of the focused menu item in an array of menu items.
|
|
82
|
+
const focusedMenuItem = async menuItems => await page.evaluate(menuItems => {
|
|
83
|
+
const focus = document.activeElement;
|
|
84
|
+
return menuItems.indexOf(focus);
|
|
85
|
+
}, menuItems);
|
|
86
|
+
// Tests a navigation on a menu item.
|
|
87
|
+
const testKey = async (
|
|
88
|
+
menu, menuItems, menuItem, keyName, keyProp, goodIndex, itemIsCorrect, itemData
|
|
89
|
+
) => {
|
|
90
|
+
// Make the menu visible and the menu item the active one.
|
|
91
|
+
await page.evaluate(args => {
|
|
92
|
+
const menu = args[0];
|
|
93
|
+
const menuItems = args[1];
|
|
94
|
+
const menuItem = args[2];
|
|
95
|
+
menu.style.display = 'revert';
|
|
96
|
+
menu.style.visibility = 'visible';
|
|
97
|
+
menu.style.opacity = 1;
|
|
98
|
+
menuItems.forEach(item => {
|
|
99
|
+
item.tabIndex = -1;
|
|
100
|
+
});
|
|
101
|
+
menuItem.tabIndex = 0;
|
|
102
|
+
}, [menu, menuItems, menuItem]);
|
|
103
|
+
// Focus it and press the specified key.
|
|
104
|
+
await menuItem.press(keyName);
|
|
105
|
+
// Increment the counts of navigations and key navigations.
|
|
106
|
+
data.totals.navigations.all.total++;
|
|
107
|
+
data.totals.navigations.specific[keyProp].total++;
|
|
108
|
+
// Identify which menu item is now focused, if any.
|
|
109
|
+
const focusIndex = await focusedMenuItem(menuItems);
|
|
110
|
+
// If the focus is correct:
|
|
111
|
+
if (
|
|
112
|
+
goodIndex === null
|
|
113
|
+
? [-1, menuItems.indexOf(menuItem)].includes(focusIndex)
|
|
114
|
+
: focusIndex === goodIndex
|
|
115
|
+
) {
|
|
116
|
+
// Increment the counts of correct navigations and correct key navigations.
|
|
117
|
+
data.totals.navigations.all.correct++;
|
|
118
|
+
data.totals.navigations.specific[keyProp].correct++;
|
|
119
|
+
}
|
|
120
|
+
// Otherwise, i.e. if the focus is incorrect:
|
|
121
|
+
else {
|
|
122
|
+
// Increment the counts of incorrect navigations and incorrect key navigations.
|
|
123
|
+
data.totals.navigations.all.incorrect++;
|
|
124
|
+
data.totals.navigations.specific[keyProp].incorrect++;
|
|
125
|
+
// Update the menu-item status to incorrect.
|
|
126
|
+
itemIsCorrect = false;
|
|
127
|
+
// If itemization is required:
|
|
128
|
+
if (withItems) {
|
|
129
|
+
// Update the menu-item report.
|
|
130
|
+
itemData.navigationErrors.push(keyName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return itemIsCorrect;
|
|
134
|
+
};
|
|
135
|
+
// Returns the index to which an arrow key should move the focus.
|
|
136
|
+
const arrowTarget = (startIndex, itemCount, orientation, direction) => {
|
|
137
|
+
if (orientation === 'horizontal') {
|
|
138
|
+
if (direction === 'left') {
|
|
139
|
+
return startIndex ? startIndex - 1 : itemCount - 1;
|
|
140
|
+
}
|
|
141
|
+
else if (direction === 'right') {
|
|
142
|
+
return startIndex === itemCount - 1 ? 0 : startIndex + 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (orientation === 'vertical') {
|
|
146
|
+
if (direction === 'up') {
|
|
147
|
+
return startIndex ? startIndex - 1 : itemCount - 1;
|
|
148
|
+
}
|
|
149
|
+
else if (direction === 'down') {
|
|
150
|
+
return startIndex === itemCount - 1 ? 0 : startIndex + 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
// Recursively tests menu items of a menu.
|
|
155
|
+
const testMenuItems = async (menu, menuItems, index, orientation, menuIsCorrect) => {
|
|
156
|
+
const itemCount = menuItems.length;
|
|
157
|
+
// If any menu items remain to be tested:
|
|
158
|
+
if (index < itemCount) {
|
|
159
|
+
// Increment the reported count of menu items.
|
|
160
|
+
data.totals.menuItems.total++;
|
|
161
|
+
// Identify the menu item to be tested.
|
|
162
|
+
const currentItem = menuItems[index];
|
|
163
|
+
// Initialize it as correct.
|
|
164
|
+
let isCorrect = true;
|
|
165
|
+
const itemData = {};
|
|
166
|
+
// If itemization is required:
|
|
167
|
+
if (withItems) {
|
|
168
|
+
// Initialize a report on the menu item.
|
|
169
|
+
itemData.tagName = await page.evaluate(element => element.tagName, currentItem);
|
|
170
|
+
itemData.text = await allText(page, currentItem);
|
|
171
|
+
itemData.navigationErrors = [];
|
|
172
|
+
}
|
|
173
|
+
// Test the element with each navigation key.
|
|
174
|
+
isCorrect = await testKey(
|
|
175
|
+
menu, menuItems, currentItem, 'Tab', 'tab', -1, isCorrect, itemData
|
|
176
|
+
);
|
|
177
|
+
// FUNCTION DEFINITION START
|
|
178
|
+
// Tests arrow-key navigation.
|
|
179
|
+
const testArrow = async (keyName, keyProp) => {
|
|
180
|
+
isCorrect = await testKey(
|
|
181
|
+
menu,
|
|
182
|
+
menuItems,
|
|
183
|
+
currentItem,
|
|
184
|
+
keyName,
|
|
185
|
+
keyProp,
|
|
186
|
+
arrowTarget(index, itemCount, orientation, keyProp),
|
|
187
|
+
isCorrect,
|
|
188
|
+
itemData
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
// FUNCTION DEFINITION END
|
|
192
|
+
if (orientation === 'vertical') {
|
|
193
|
+
await testArrow('ArrowUp', 'up');
|
|
194
|
+
await testArrow('ArrowDown', 'down');
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
await testArrow('ArrowRight', 'right');
|
|
198
|
+
await testArrow('ArrowLeft', 'left');
|
|
199
|
+
}
|
|
200
|
+
isCorrect = await testKey(
|
|
201
|
+
menu, menuItems, currentItem, 'Home', 'home', 0, isCorrect, itemData
|
|
202
|
+
);
|
|
203
|
+
isCorrect = await testKey(
|
|
204
|
+
menu, menuItems, currentItem, 'End', 'end', itemCount - 1, isCorrect, itemData
|
|
205
|
+
);
|
|
206
|
+
// Update the menu-item status (Node 14 does not support the ES 2021 &&= operator).
|
|
207
|
+
menuIsCorrect = menuIsCorrect && isCorrect;
|
|
208
|
+
// Increment the data.
|
|
209
|
+
data.totals.menuItems[isCorrect ? 'correct' : 'incorrect']++;
|
|
210
|
+
if (withItems) {
|
|
211
|
+
data.menuItems[isCorrect ? 'correct' : 'incorrect'].push(itemData);
|
|
212
|
+
}
|
|
213
|
+
// Process the next tab element.
|
|
214
|
+
return await testMenuItems(menu, menuItems, index + 1, orientation, menuIsCorrect);
|
|
215
|
+
}
|
|
216
|
+
// Otherwise, i.e. if all menu items have been tested:
|
|
217
|
+
else {
|
|
218
|
+
// Return whether the menu is correct.
|
|
219
|
+
return menuIsCorrect;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
// Recursively tests menus.
|
|
223
|
+
const testMenus = async menus => {
|
|
224
|
+
// If any menus remain to be tested:
|
|
225
|
+
if (menus.length) {
|
|
226
|
+
// Identify the first of them.
|
|
227
|
+
const firstMenu = menus[0];
|
|
228
|
+
// Identify its orientation.
|
|
229
|
+
const menuRole = await firstMenu.getAttribute('role');
|
|
230
|
+
const orientationAttribute = await firstMenu.getAttribute('aria-orientation');
|
|
231
|
+
const orientation = orientationAttribute || (
|
|
232
|
+
menuRole === 'menu' ? 'vertical' : 'horizontal'
|
|
233
|
+
);
|
|
234
|
+
// Identify its direct menu items.
|
|
235
|
+
const menuItems = await firstMenu.$$(
|
|
236
|
+
'[role=menuitem]:not([role=menu] [role=menuitem]):not([role=menubar] [role=menuitem])'
|
|
237
|
+
);
|
|
238
|
+
// If the menu contains at least 2 direct menu items:
|
|
239
|
+
if (menuItems.length > 1) {
|
|
240
|
+
// Test its menu items.
|
|
241
|
+
const isCorrect = await testMenuItems(firstMenu, menuItems, 0, orientation, true);
|
|
242
|
+
// Increment the data.
|
|
243
|
+
data.totals.menus.total++;
|
|
244
|
+
data.totals.menus[isCorrect ? 'correct' : 'incorrect']++;
|
|
245
|
+
// Process the remaining menus.
|
|
246
|
+
await testMenus(menus.slice(1));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
// FUNCTION DEFINITIONS END
|
|
251
|
+
await testMenus(menus);
|
|
252
|
+
}
|
|
253
|
+
return {result: data};
|
|
254
|
+
};
|
package/tests/motion.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/*
|
|
2
|
+
motion
|
|
3
|
+
This test reports motion in a page. For minimal accessibility, standards require motion to be
|
|
4
|
+
brief, or else stoppable by the user. But stopping motion can be difficult or impossible, and,
|
|
5
|
+
by the time a user manages to stop motion, the motion may have caused annoyance or harm. For
|
|
6
|
+
superior accessibility, a page contains no motion until and unless the user authorizes it. The
|
|
7
|
+
test compares screen shots of the part of the page within the visible viewport. You can specify
|
|
8
|
+
how many milliseconds to wait before the first screen shot (delay), how many milliseconds to wait
|
|
9
|
+
between screen shots (interval), and how many screen shots to make (count). The test compares the
|
|
10
|
+
screen shots and reports 9 statistics:
|
|
11
|
+
0. bytes: an array of the sizes of the screen shots, in bytes
|
|
12
|
+
1. localRatios: an array of the ratios of bytes of the larger to the smaller of adjacent pairs of screen shots
|
|
13
|
+
2. meanLocalRatio: the mean of the ratios in the localRatios array
|
|
14
|
+
3. maxLocalRatio: the greatest of the ratios in the localRatios array
|
|
15
|
+
4. globalRatio: the ratio of bytes of the largest to the smallest screen shot
|
|
16
|
+
5. pixelChanges: an array of counts of differing pixels between adjacent pairs of screen shots
|
|
17
|
+
6. meanPixelChange: the mean of the counts in the pixelChanges array
|
|
18
|
+
7. maxPixelChange: the greatest of the counts in the pixelChanges array
|
|
19
|
+
8. changeFrequency: what fraction of the adjacent pairs of screen shots has pixel differences
|
|
20
|
+
*/
|
|
21
|
+
const pixelmatch = require('pixelmatch');
|
|
22
|
+
const {PNG} = require('pngjs');
|
|
23
|
+
// Creates and returns a screenshot.
|
|
24
|
+
const shoot = async page => {
|
|
25
|
+
// Make a screenshot as a buffer.
|
|
26
|
+
return await page.screenshot({
|
|
27
|
+
fullPage: false,
|
|
28
|
+
omitBackground: true,
|
|
29
|
+
timeout: 3000
|
|
30
|
+
})
|
|
31
|
+
.catch(error => {
|
|
32
|
+
console.log(`ERROR: Screenshot failed(${error.message})`);
|
|
33
|
+
return '';
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
// Recursively creates and returns screenshots.
|
|
37
|
+
const shootAll = async (page, delay, interval, count, toDo, buffers) => {
|
|
38
|
+
// Wait.
|
|
39
|
+
await page.waitForTimeout(toDo === count ? delay : interval);
|
|
40
|
+
// Make a screenshot.
|
|
41
|
+
const buffer = await shoot(
|
|
42
|
+
page, `${page.url().replace(/^.+\/\/|\/$/g, '').replace(/\//g, '+')}-${count - toDo}`
|
|
43
|
+
);
|
|
44
|
+
// Get its dimensions.
|
|
45
|
+
if (buffer.length) {
|
|
46
|
+
buffers.push(buffer);
|
|
47
|
+
if (toDo > 1) {
|
|
48
|
+
return shootAll(page, delay, interval, count, toDo - 1, buffers);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return buffers;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
// Returns a number rounded to 2 decimal digits.
|
|
59
|
+
const round = (num, precision) => Number.parseFloat(num.toPrecision(precision));
|
|
60
|
+
// Reports motion in a page.
|
|
61
|
+
exports.reporter = async (page, delay, interval, count) => {
|
|
62
|
+
// Make screenshots and get their image buffers.
|
|
63
|
+
const shots = await shootAll(page, delay, interval, count, count, []);
|
|
64
|
+
// If the shooting succeeded:
|
|
65
|
+
if (shots.length === count) {
|
|
66
|
+
// Get the sizes of the shots in bytes of code.
|
|
67
|
+
const bytes = shots.map(shot => shot.length);
|
|
68
|
+
// Get their ratios between adjacent pairs of shots.
|
|
69
|
+
const localRatios = bytes.slice(1).map((size, index) => round(
|
|
70
|
+
(size > bytes[index] ? size / bytes[index] : bytes[index] / size), 4
|
|
71
|
+
));
|
|
72
|
+
// Get the mean and maximum of those ratios.
|
|
73
|
+
const meanLocalRatio = round(
|
|
74
|
+
localRatios.reduce((sum, currentRatio) => sum + currentRatio) / localRatios.length, 4
|
|
75
|
+
);
|
|
76
|
+
const maxLocalRatio = Math.max(...localRatios);
|
|
77
|
+
// Get the ratio between the largest and smallest shot.
|
|
78
|
+
const globalRatio = round((Math.max(...bytes) / Math.min(...bytes)), 4);
|
|
79
|
+
// Get the shots as PNG images.
|
|
80
|
+
const pngs = shots.map(shot => PNG.sync.read(shot));
|
|
81
|
+
// Get their dimensions.
|
|
82
|
+
const {width, height} = pngs[0];
|
|
83
|
+
// Get the counts of differing pixels between adjacent pairs of shots.
|
|
84
|
+
const pixelChanges = pngs
|
|
85
|
+
.slice(1)
|
|
86
|
+
.map((png, index) => pixelmatch(pngs[index].data, png.data, null, width, height));
|
|
87
|
+
// Get the mean and maximum of those counts.
|
|
88
|
+
const meanPixelChange = Math.floor(
|
|
89
|
+
pixelChanges.reduce((sum, currentChange) => sum + currentChange) / pixelChanges.length
|
|
90
|
+
);
|
|
91
|
+
const maxPixelChange = Math.max(...pixelChanges);
|
|
92
|
+
const changeFrequency = round(
|
|
93
|
+
pixelChanges.reduce((count, change) => count + (change ? 1 : 0), 0) / pixelChanges.length, 2
|
|
94
|
+
);
|
|
95
|
+
// Return the result.
|
|
96
|
+
return {
|
|
97
|
+
result: {
|
|
98
|
+
bytes,
|
|
99
|
+
localRatios,
|
|
100
|
+
meanLocalRatio,
|
|
101
|
+
maxLocalRatio,
|
|
102
|
+
globalRatio,
|
|
103
|
+
pixelChanges,
|
|
104
|
+
meanPixelChange,
|
|
105
|
+
maxPixelChange,
|
|
106
|
+
changeFrequency
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Otherwise, i.e. if the shooting failed:
|
|
111
|
+
else {
|
|
112
|
+
// Return failure.
|
|
113
|
+
return {result: {error: 'ERROR: screenshots failed'}};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
radioSet
|
|
3
|
+
This test reports nonstandard grouping of radio buttons. It defines standard grouping to require
|
|
4
|
+
that two or more radio buttons with the same name, and no other radio buttons, be grouped in a
|
|
5
|
+
'fieldset' element with a valid 'legend' element.
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('fs/promises');
|
|
8
|
+
// Tabulates and lists radio buttons in and not in accessible field sets.
|
|
9
|
+
exports.reporter = async (page, withItems) => {
|
|
10
|
+
// Initialize the argument array to be passed to the page function.
|
|
11
|
+
const args = [withItems];
|
|
12
|
+
// If itemization is required:
|
|
13
|
+
if (withItems) {
|
|
14
|
+
// Add the body of the textOf function as a string to the array.
|
|
15
|
+
const textOfBody = await fs.readFile('procs/test/textOf.txt', 'utf8');
|
|
16
|
+
args.push(textOfBody);
|
|
17
|
+
}
|
|
18
|
+
// Get the result data.
|
|
19
|
+
const dataJSHandle = await page.evaluateHandle(args => {
|
|
20
|
+
const withItems = args[0];
|
|
21
|
+
// FUNCTION DEFINITIONS START
|
|
22
|
+
/*
|
|
23
|
+
If itemization is required, define the textOf function to get element texts.
|
|
24
|
+
The function body is read as a string and passed to this method because
|
|
25
|
+
a string can be passed in but a function cannot.
|
|
26
|
+
*/
|
|
27
|
+
const textOf = args[1] ? new Function('element', args[1]) : '';
|
|
28
|
+
// Trim excess spaces from a string.
|
|
29
|
+
const debloat = text => text.trim().replace(/\s+/g, ' ');
|
|
30
|
+
// FUNCTION DEFINITIONS END
|
|
31
|
+
// Initialize a report.
|
|
32
|
+
const data = {
|
|
33
|
+
totals: {
|
|
34
|
+
total: 0,
|
|
35
|
+
inSet: 0,
|
|
36
|
+
percent: 0
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (withItems) {
|
|
40
|
+
data.items = {
|
|
41
|
+
inSet: [],
|
|
42
|
+
notInSet: []
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Get an array of all fieldset elements.
|
|
46
|
+
const fieldsets = Array.from(document.body.querySelectorAll('fieldset'));
|
|
47
|
+
// Get an array of those with valid legends.
|
|
48
|
+
const legendSets = fieldsets.filter(fieldset => {
|
|
49
|
+
const firstChild = fieldset.firstElementChild;
|
|
50
|
+
return firstChild
|
|
51
|
+
&& firstChild.tagName === 'LEGEND'
|
|
52
|
+
&& debloat(firstChild.textContent).length;
|
|
53
|
+
});
|
|
54
|
+
// Get an array of the radio buttons in those with homogeneous radio buttons.
|
|
55
|
+
const setRadios = legendSets.reduce((radios, currentSet) => {
|
|
56
|
+
const currentRadios = Array.from(currentSet.querySelectorAll('input[type=radio]'));
|
|
57
|
+
const radioCount = currentRadios.length;
|
|
58
|
+
if (radioCount == 1) {
|
|
59
|
+
radios.push(currentRadios[0]);
|
|
60
|
+
}
|
|
61
|
+
else if (radioCount > 1) {
|
|
62
|
+
const radioName = currentRadios[0].name;
|
|
63
|
+
if (radioName && currentRadios.slice(1).every(radio => radio.name === radioName)) {
|
|
64
|
+
radios.push(...currentRadios);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return radios;
|
|
68
|
+
}, []);
|
|
69
|
+
// Get an array of all radio buttons.
|
|
70
|
+
const allRadios = Array.from(document.body.querySelectorAll('input[type=radio'));
|
|
71
|
+
// Tabulate the results.
|
|
72
|
+
const totals = data.totals;
|
|
73
|
+
totals.total = allRadios.length;
|
|
74
|
+
totals.inSet = setRadios.length;
|
|
75
|
+
totals.percent = totals.total ? Math.floor(100 * totals.inSet / totals.total) : 'N.A.';
|
|
76
|
+
// If itemization is required:
|
|
77
|
+
if (withItems) {
|
|
78
|
+
// Add it to the results.
|
|
79
|
+
const nonSetRadios = allRadios.filter(radio => ! setRadios.includes(radio));
|
|
80
|
+
const items = data.items;
|
|
81
|
+
items.inSet = setRadios.map(radio => textOf(radio));
|
|
82
|
+
items.notInSet = nonSetRadios.map(radio => textOf(radio));
|
|
83
|
+
}
|
|
84
|
+
return {result: data};
|
|
85
|
+
}, args);
|
|
86
|
+
return await dataJSHandle.jsonValue();
|
|
87
|
+
};
|
package/tests/role.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/*
|
|
2
|
+
role
|
|
3
|
+
This test reports role assignment that violate either an applicable standard or an applicable
|
|
4
|
+
recommendation from WAI-ARIA. Invalid roles include those that are abstract and thus prohibited
|
|
5
|
+
from direct use, and those that are implicit in HTML elements and thus advised against. The math
|
|
6
|
+
role has been removed, because of poor adoption and exclusion from HTML5. The img role has
|
|
7
|
+
accessibility uses, so is not classified as deprecated. See:
|
|
8
|
+
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Role_Img
|
|
9
|
+
https://www.w3.org/TR/html-aria/
|
|
10
|
+
https://www.w3.org/TR/wai-aria/#roles_categorization
|
|
11
|
+
*/
|
|
12
|
+
exports.reporter = async page => await page.$eval('body', body => {
|
|
13
|
+
// CONSTANTS
|
|
14
|
+
const badRoles = new Set([
|
|
15
|
+
'article',
|
|
16
|
+
'banner',
|
|
17
|
+
'button',
|
|
18
|
+
'cell',
|
|
19
|
+
'checkbox',
|
|
20
|
+
'columnheader',
|
|
21
|
+
'combobox',
|
|
22
|
+
'complementary',
|
|
23
|
+
'contentinfo',
|
|
24
|
+
'definition',
|
|
25
|
+
'dialog',
|
|
26
|
+
'document',
|
|
27
|
+
'figure',
|
|
28
|
+
'graphics-document',
|
|
29
|
+
'gridcell',
|
|
30
|
+
'group',
|
|
31
|
+
'heading',
|
|
32
|
+
'link',
|
|
33
|
+
'list',
|
|
34
|
+
'listbox',
|
|
35
|
+
'listitem',
|
|
36
|
+
'main',
|
|
37
|
+
'navigation',
|
|
38
|
+
'option',
|
|
39
|
+
'progressbar',
|
|
40
|
+
'radio',
|
|
41
|
+
'region',
|
|
42
|
+
'row',
|
|
43
|
+
'rowgroup',
|
|
44
|
+
'rowheader',
|
|
45
|
+
'searchbox',
|
|
46
|
+
'separator',
|
|
47
|
+
'slider',
|
|
48
|
+
'spinbutton',
|
|
49
|
+
'status',
|
|
50
|
+
'table',
|
|
51
|
+
'term',
|
|
52
|
+
'textbox'
|
|
53
|
+
]);
|
|
54
|
+
// All non-abstract roles
|
|
55
|
+
const goodRoles = new Set([
|
|
56
|
+
'alert',
|
|
57
|
+
'alertdialog',
|
|
58
|
+
'application',
|
|
59
|
+
'article',
|
|
60
|
+
'banner',
|
|
61
|
+
'button',
|
|
62
|
+
'cell',
|
|
63
|
+
'checkbox',
|
|
64
|
+
'columnheader',
|
|
65
|
+
'combobox',
|
|
66
|
+
'complementary',
|
|
67
|
+
'contentinfo',
|
|
68
|
+
'definition',
|
|
69
|
+
'dialog',
|
|
70
|
+
'directory',
|
|
71
|
+
'document',
|
|
72
|
+
'feed',
|
|
73
|
+
'figure',
|
|
74
|
+
'form',
|
|
75
|
+
'grid',
|
|
76
|
+
'gridcell',
|
|
77
|
+
'group',
|
|
78
|
+
'heading',
|
|
79
|
+
'img',
|
|
80
|
+
'link',
|
|
81
|
+
'list',
|
|
82
|
+
'listbox',
|
|
83
|
+
'listitem',
|
|
84
|
+
'log',
|
|
85
|
+
'main',
|
|
86
|
+
'marquee',
|
|
87
|
+
'menu',
|
|
88
|
+
'menubar',
|
|
89
|
+
'menuitem',
|
|
90
|
+
'menuitemcheckbox',
|
|
91
|
+
'menuitemradio',
|
|
92
|
+
'navigation',
|
|
93
|
+
'none',
|
|
94
|
+
'note',
|
|
95
|
+
'option',
|
|
96
|
+
'presentation',
|
|
97
|
+
'progressbar',
|
|
98
|
+
'radio',
|
|
99
|
+
'radiogroup',
|
|
100
|
+
'region',
|
|
101
|
+
'row',
|
|
102
|
+
'rowgroup',
|
|
103
|
+
'rowheader',
|
|
104
|
+
'scrollbar',
|
|
105
|
+
'search',
|
|
106
|
+
'searchbox',
|
|
107
|
+
'separator',
|
|
108
|
+
'separator',
|
|
109
|
+
'slider',
|
|
110
|
+
'spinbutton',
|
|
111
|
+
'status',
|
|
112
|
+
'switch',
|
|
113
|
+
'tab',
|
|
114
|
+
'table',
|
|
115
|
+
'tablist',
|
|
116
|
+
'tabpanel',
|
|
117
|
+
'term',
|
|
118
|
+
'textbox',
|
|
119
|
+
'timer',
|
|
120
|
+
'toolbar',
|
|
121
|
+
'tooltip',
|
|
122
|
+
'tree',
|
|
123
|
+
'treegrid',
|
|
124
|
+
'treeitem',
|
|
125
|
+
]);
|
|
126
|
+
// Remove the deprecated roles from the non-abstract roles.
|
|
127
|
+
goodRoles.forEach(role => {
|
|
128
|
+
if (badRoles.has(role)) {
|
|
129
|
+
goodRoles.delete(role);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// Identify all elements with role attributes.
|
|
133
|
+
const roleElements = Array.from(body.querySelectorAll('[role]'));
|
|
134
|
+
// Identify those with roles that are either deprecated or invalid.
|
|
135
|
+
const bads = roleElements.filter(element => {
|
|
136
|
+
const role = element.getAttribute('role');
|
|
137
|
+
return badRoles.has(role) || ! goodRoles.has(role);
|
|
138
|
+
});
|
|
139
|
+
// Initialize the result.
|
|
140
|
+
const data = {
|
|
141
|
+
roleElements: roleElements.length,
|
|
142
|
+
badRoleElements: bads.length,
|
|
143
|
+
tagNames: {}
|
|
144
|
+
};
|
|
145
|
+
// For each element with a deprecated role:
|
|
146
|
+
bads.forEach(element => {
|
|
147
|
+
// Identify its facts.
|
|
148
|
+
const tagName = element.tagName;
|
|
149
|
+
const role = element.getAttribute('role');
|
|
150
|
+
// Add them to the result.
|
|
151
|
+
if (data.tagNames[tagName]) {
|
|
152
|
+
if (data.tagNames[tagName][role]) {
|
|
153
|
+
data.tagNames[tagName][role]++;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
data.tagNames[tagName][role] = 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
data.tagNames[tagName] = {[role]: 1};
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return {result: data};
|
|
164
|
+
});
|