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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +502 -0
  3. package/aceconfig.js +7 -0
  4. package/commands.js +249 -0
  5. package/index.js +1248 -0
  6. package/package.json +39 -0
  7. package/procs/score/asp09.js +555 -0
  8. package/procs/test/allText.js +76 -0
  9. package/procs/test/allVis.js +17 -0
  10. package/procs/test/linksByType.js +90 -0
  11. package/procs/test/textOf.txt +73 -0
  12. package/scoring/correlation.js +74 -0
  13. package/scoring/correlations.json +327 -0
  14. package/scoring/data.json +26021 -0
  15. package/scoring/dupCounts.js +39 -0
  16. package/scoring/dupCounts.json +112 -0
  17. package/scoring/duplications.json +253 -0
  18. package/scoring/issues.json +304 -0
  19. package/scoring/packageData.js +171 -0
  20. package/scoring/packageIssues.js +34 -0
  21. package/scoring/rulesetData.json +15 -0
  22. package/tests/aatt.js +64 -0
  23. package/tests/alfa.js +107 -0
  24. package/tests/axe.js +109 -0
  25. package/tests/bulk.js +21 -0
  26. package/tests/embAc.js +36 -0
  27. package/tests/focAll.js +62 -0
  28. package/tests/focInd.js +99 -0
  29. package/tests/focOp.js +132 -0
  30. package/tests/hover.js +195 -0
  31. package/tests/ibm.js +89 -0
  32. package/tests/labClash.js +157 -0
  33. package/tests/linkUl.js +65 -0
  34. package/tests/menuNav.js +254 -0
  35. package/tests/motion.js +115 -0
  36. package/tests/radioSet.js +87 -0
  37. package/tests/role.js +164 -0
  38. package/tests/styleDiff.js +146 -0
  39. package/tests/tabNav.js +282 -0
  40. package/tests/wave.js +44 -0
  41. package/tests/zIndex.js +49 -0
  42. package/validation/batches/sample.json +13 -0
  43. package/validation/executors/sample.js +11 -0
  44. package/validation/scripts/app/sample.json +21 -0
  45. package/validation/scripts/test/bulk.json +39 -0
  46. package/validation/scripts/test/embAc.json +45 -0
  47. package/validation/scripts/test/focAll.json +59 -0
  48. package/validation/scripts/test/focInd.json +55 -0
  49. package/validation/scripts/test/focOp.json +53 -0
  50. package/validation/scripts/test/hover.json +47 -0
  51. package/validation/scripts/test/labClash.json +43 -0
  52. package/validation/scripts/test/linkUl.json +62 -0
  53. package/validation/scripts/test/menuNav.json +97 -0
  54. package/validation/scripts/test/motion.json +53 -0
  55. package/validation/scripts/test/radioSet.json +43 -0
  56. package/validation/scripts/test/role.json +42 -0
  57. package/validation/scripts/test/styleDiff.json +61 -0
  58. package/validation/scripts/test/tabNav.json +97 -0
  59. package/validation/scripts/test/zIndex.json +40 -0
  60. package/validation/targets/bulk/bad.html +48 -0
  61. package/validation/targets/bulk/good.html +15 -0
  62. package/validation/targets/embAc/bad.html +21 -0
  63. package/validation/targets/embAc/good.html +15 -0
  64. package/validation/targets/focAll/good.html +15 -0
  65. package/validation/targets/focAll/less.html +15 -0
  66. package/validation/targets/focAll/more.html +16 -0
  67. package/validation/targets/focInd/bad.html +31 -0
  68. package/validation/targets/focInd/good.html +22 -0
  69. package/validation/targets/focOp/bad.html +18 -0
  70. package/validation/targets/focOp/good.html +15 -0
  71. package/validation/targets/hover/bad.html +19 -0
  72. package/validation/targets/hover/good.html +15 -0
  73. package/validation/targets/labClash/bad.html +20 -0
  74. package/validation/targets/labClash/good.html +18 -0
  75. package/validation/targets/linkUl/bad.html +16 -0
  76. package/validation/targets/linkUl/good.html +30 -0
  77. package/validation/targets/linkUl/na.html +20 -0
  78. package/validation/targets/menuNav/bad.html +106 -0
  79. package/validation/targets/menuNav/bad.js +348 -0
  80. package/validation/targets/menuNav/good.html +106 -0
  81. package/validation/targets/menuNav/good.js +365 -0
  82. package/validation/targets/menuNav/style.css +22 -0
  83. package/validation/targets/motion/bad.css +15 -0
  84. package/validation/targets/motion/bad.html +16 -0
  85. package/validation/targets/motion/good.html +15 -0
  86. package/validation/targets/radioSet/bad.html +34 -0
  87. package/validation/targets/radioSet/good.html +27 -0
  88. package/validation/targets/role/bad.html +26 -0
  89. package/validation/targets/role/good.html +22 -0
  90. package/validation/targets/styleDiff/bad.html +35 -0
  91. package/validation/targets/styleDiff/good.html +36 -0
  92. package/validation/targets/tabNav/bad.html +51 -0
  93. package/validation/targets/tabNav/bad.js +35 -0
  94. package/validation/targets/tabNav/good.html +53 -0
  95. package/validation/targets/tabNav/good.js +83 -0
  96. package/validation/targets/tabNav/goodMoz.js +206 -0
  97. package/validation/targets/tabNav/style.css +34 -0
  98. package/validation/targets/zIndex/bad.html +17 -0
  99. package/validation/targets/zIndex/good.html +15 -0
@@ -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
+ };
@@ -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
+ };
@@ -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
+ });