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,99 @@
1
+ /*
2
+ focInd
3
+ This test reports focusable elements without focus indicators, with non-outline focus
4
+ indicators, and with outline focus indicators.
5
+
6
+ It as based on the assumption that outlines are the standard and thus most familiar
7
+ focus indicator. Other focus indicators are assumed better than none, but more likely
8
+ to be misunderstood. For example, underlines may be mistaken for selection indicators.
9
+
10
+ Bug: This test fails to recognize outlines when run with firefox.
11
+ */
12
+ exports.reporter = async (page, withItems, revealAll) => {
13
+ // If required, make all elements visible.
14
+ if (revealAll) {
15
+ await require('../procs/test/allVis').allVis(page);
16
+ }
17
+ // Get data on the focusable visible elements with and without indicators.
18
+ const data = await page.$$eval('body *:visible', (elements, withItems) => {
19
+ // Initialize the data.
20
+ const data = {
21
+ totals: {
22
+ total: 0,
23
+ types: {
24
+ indicatorMissing: {
25
+ total: 0,
26
+ tagNames: {}
27
+ },
28
+ nonOutlinePresent: {
29
+ total: 0,
30
+ tagNames: {}
31
+ },
32
+ outlinePresent: {
33
+ total: 0,
34
+ tagNames: {}
35
+ }
36
+ }
37
+ }
38
+ };
39
+ if (withItems) {
40
+ data.items = {
41
+ indicatorMissing: [],
42
+ nonOutlinePresent: [],
43
+ outlinePresent: []
44
+ };
45
+ }
46
+ const addElementFacts = (element, status) => {
47
+ const type = data.totals.types[status];
48
+ type.total++;
49
+ const tagName = element.tagName;
50
+ if (type.tagNames[tagName]) {
51
+ type.tagNames[tagName]++;
52
+ }
53
+ else {
54
+ type.tagNames[tagName] = 1;
55
+ }
56
+ if (withItems) {
57
+ data.items[status].push({
58
+ tagName,
59
+ text: element.textContent.trim().replace(/\s{2,}/g, ' ').slice(0, 100)
60
+ });
61
+ }
62
+ };
63
+ elements.forEach(element => {
64
+ if (element.tabIndex === 0) {
65
+ data.totals.total++;
66
+ const styleBlurred = Object.assign({}, window.getComputedStyle(element));
67
+ element.focus({preventScroll: true});
68
+ const styleFocused = window.getComputedStyle(element);
69
+ const hasOutline
70
+ = styleBlurred.outlineWidth === '0px'
71
+ && styleFocused.outlineWidth !== '0px';
72
+ if (hasOutline) {
73
+ addElementFacts(element, 'outlinePresent');
74
+ }
75
+ else {
76
+ const diff = prop => styleFocused[prop] !== styleBlurred[prop];
77
+ const hasIndicator
78
+ = diff('borderStyle')
79
+ && styleBlurred.borderWidth !== '0px'
80
+ && styleFocused.borderWidth !== '0px'
81
+ || (styleFocused.borderStyle !== 'none' && diff('borderWidth'))
82
+ || diff('outlineStyle')
83
+ && styleBlurred.outlineWidth !== '0px'
84
+ && styleFocused.outlineWidth !== '0px'
85
+ || (styleFocused.outlineStyle !== 'none' && diff('outlineWidth'))
86
+ || diff('fontSize')
87
+ || diff('fontStyle')
88
+ || diff('textDecorationLine')
89
+ || diff('textDecorationStyle')
90
+ || diff('textDecorationThickness');
91
+ const status = hasIndicator ? 'nonOutlinePresent' : 'indicatorMissing';
92
+ addElementFacts(element, status);
93
+ }
94
+ }
95
+ });
96
+ return data;
97
+ }, withItems);
98
+ return {result: data};
99
+ };
package/tests/focOp.js ADDED
@@ -0,0 +1,132 @@
1
+ /*
2
+ focOp
3
+ This test reports descrepancies between Tab-focusability and operability. The standard
4
+ practice is to make focusable elements operable and vice versa. If focusable elements are not
5
+ operable, users are likely to be surprised that nothing happens when they try to operate such
6
+ elements. If operable elements are not focusable, users depending on keyboard navigation are
7
+ prevented from operating those elements. The test considers an element operable if it has a
8
+ non-inherited pointer cursor and is not a 'LABEL' element, or has an operable tag name ('A',
9
+ 'BUTTON', 'IFRAME', 'INPUT', 'SELECT', 'TEXTAREA'), or has an 'onclick' attribute. The test
10
+ considers an element Tab-focusable if its tabIndex property has the value 0.
11
+ */
12
+ exports.reporter = async (page, withItems) => {
13
+ // Get data on focusability-operability-discrepant elements.
14
+ const data = await page.$$eval('body *', (elements, withItems) => {
15
+ // Initialize the data.
16
+ const data = {
17
+ totals: {
18
+ total: 0,
19
+ types: {
20
+ onlyFocusable: {
21
+ total: 0,
22
+ tagNames: {}
23
+ },
24
+ onlyOperable: {
25
+ total: 0,
26
+ tagNames: {}
27
+ },
28
+ focusableAndOperable: {
29
+ total: 0,
30
+ tagNames: {}
31
+ }
32
+ }
33
+ }
34
+ };
35
+ if (withItems) {
36
+ data.items = {
37
+ onlyFocusable: [],
38
+ onlyOperable: [],
39
+ focusableAndOperable: []
40
+ };
41
+ }
42
+ // FUNCTION DEFINITIONS START
43
+ // Returns data on an element’s operability and prevents it from propagating a pointer.
44
+ const operabilityOf = element => {
45
+ const opTags = new Set(['A', 'BUTTON', 'IFRAME', 'INPUT', 'SELECT', 'TEXTAREA']);
46
+ const hasPointer = window.getComputedStyle(element).cursor === 'pointer';
47
+ const opBases = [
48
+ opTags.has(element.tagName),
49
+ element.hasAttribute('onclick'),
50
+ hasPointer && element.tagName !== 'LABEL'
51
+ ];
52
+ const result = {operable: opBases.some(basis => basis)};
53
+ if (result.operable) {
54
+ result.byTag = opBases[0];
55
+ result.byOnClick = opBases[1];
56
+ result.byPointer = opBases[2];
57
+ }
58
+ // If the cursor is a pointer:
59
+ if (hasPointer) {
60
+ // Change it to the browser default to prevent pointer propagation.
61
+ element.style.cursor = 'default';
62
+ }
63
+ return result;
64
+ };
65
+ // Adds facts about an element to data.
66
+ const addFacts = (element, status, byTag, byOnClick, byPointer) => {
67
+ const statusNames = {
68
+ f: 'onlyFocusable',
69
+ o: 'onlyOperable',
70
+ b: 'focusableAndOperable'
71
+ };
72
+ const statusName = statusNames[status];
73
+ data.totals.types[statusName].total++;
74
+ const tagNames = data.totals.types[statusName].tagNames;
75
+ const {id, tagName} = element;
76
+ tagNames[tagName] = (tagNames[tagName] || 0) + 1;
77
+ if (withItems) {
78
+ const elementData = {
79
+ tagName: element.tagName,
80
+ id: id || '',
81
+ text: (element.textContent.trim() || element.outerHTML.trim())
82
+ .replace(/\s{2,}/sg, ' ')
83
+ .slice(0, 80)
84
+ };
85
+ if (status !== 'f') {
86
+ elementData.byTag = byTag;
87
+ elementData.byOnClick = byOnClick;
88
+ elementData.byPointer = byPointer;
89
+ }
90
+ data.items[statusName].push(elementData);
91
+ }
92
+ };
93
+ // FUNCTION DEFINITIONS END
94
+ // For each element:
95
+ elements.forEach(element => {
96
+ // If its tab index is 0, deem it focusable and:
97
+ if (element.tabIndex === 0) {
98
+ // Increment the grand total.
99
+ data.totals.total++;
100
+ // Determine whether and how it is operable.
101
+ const {operable, byTag, byOnClick, byPointer} = operabilityOf(element);
102
+ // If it is:
103
+ if (operable) {
104
+ // Add its data to the result.
105
+ addFacts(element, 'b', byTag, byOnClick, byPointer);
106
+ }
107
+ // Otherwise, i.e. if it is not operable:
108
+ else {
109
+ // Add its data to the result.
110
+ addFacts(element, 'f');
111
+ }
112
+ }
113
+ // Otherwise, i.e. if it is not focusable:
114
+ else {
115
+ // Determine whether and how it is operable.
116
+ const {operable, byTag, byOnClick, byPointer} = operabilityOf(element);
117
+ // If it is:
118
+ if (operable) {
119
+ // Increment the grand total.
120
+ data.totals.total++;
121
+ // Add its data to the result.
122
+ addFacts(element, 'o', byTag, byOnClick, byPointer);
123
+ }
124
+ }
125
+ });
126
+ return data;
127
+ }, withItems)
128
+ .catch(error => {
129
+ console.log(`ERROR getting focOp data (${error.message})`);
130
+ });
131
+ return {result: data};
132
+ };
package/tests/hover.js ADDED
@@ -0,0 +1,195 @@
1
+ /*
2
+ hover
3
+ This test reports unexpected effects of hovering. The effects include elements that are made
4
+ visible, elements whose opacities are changed, elements with ancestors whose opacities are
5
+ changed, and elements that cannot be hovered over. Only Playwright-visible elements that have
6
+ 'A', 'BUTTON', and 'LI' tag names or have 'onmouseenter' or 'onmouseover' attributes are
7
+ considered as hovering targets. The elements considered when the effects of hovering are
8
+ examined are the descendants of the grandparent of the element hovered over if that element
9
+ has the tag name 'A' or 'BUTTON' or otherwise the descendants of the element. The only
10
+ elements counted as being made visible by hovering are those with tag names 'A', 'BUTTON',
11
+ 'INPUT', and 'SPAN', and those with 'role="menuitem"' attributes.
12
+ */
13
+ exports.reporter = async (page, withItems) => {
14
+ // Initialize a counter.
15
+ let elementsChecked = 0;
16
+ // Identify the elements that are likely to trigger disclosures on hover.
17
+ const triggers = await page.$$(
18
+ 'body a:visible, body button:visible, body li:visible, body [onmouseenter]:visible, body [onmouseover]:visible'
19
+ )
20
+ .catch(error => {
21
+ console.log(`ERROR getting hover triggers (${error.message})`);
22
+ return [];
23
+ });
24
+ // Identify the selectors of active elements likely to be disclosed by a hover.
25
+ const targetSelectors = ['a', 'button', 'input', '[role=menuitem]', 'span']
26
+ .map(selector => `${selector}:visible`)
27
+ .join(', ');
28
+ // Initialize the result.
29
+ const data = {
30
+ totals: {
31
+ triggers: 0,
32
+ madeVisible: 0,
33
+ opacityChanged: 0,
34
+ opacityAffected: 0,
35
+ unhoverables: 0
36
+ }
37
+ };
38
+ if (withItems) {
39
+ data.items = {
40
+ triggers: [],
41
+ unhoverables: []
42
+ };
43
+ }
44
+ let triggerTag = '';
45
+ // FUNCTION DEFINITION START
46
+ // Recursively finds and reports triggers and targets.
47
+ const find = async triggers => {
48
+ // If any potential disclosure triggers remain:
49
+ if (triggers.length) {
50
+ // Identify the first of them.
51
+ const firstTrigger = triggers[0];
52
+ const tagNameJSHandle = await firstTrigger.getProperty('tagName')
53
+ .catch(error => {
54
+ console.log(`ERROR getting trigger tag name (${error.message})`);
55
+ return '';
56
+ });
57
+ if (tagNameJSHandle) {
58
+ const tagName = await tagNameJSHandle.jsonValue();
59
+ // Identify the root of a subtree likely to contain disclosed elements.
60
+ let root = firstTrigger;
61
+ if (['A', 'BUTTON'].includes(tagName)) {
62
+ const rootJSHandle = await page.evaluateHandle(
63
+ firstTrigger => {
64
+ const parent = firstTrigger.parentElement;
65
+ if (parent) {
66
+ return parent.parentElement || parent;
67
+ }
68
+ else {
69
+ return firstTrigger;
70
+ }
71
+ },
72
+ firstTrigger
73
+ );
74
+ root = rootJSHandle.asElement();
75
+ }
76
+ // Identify the visible active descendants of the root before the hover.
77
+ const preVisibles = await root.$$(targetSelectors);
78
+ // Identify all the descendants of the root.
79
+ const descendants = await root.$$('*');
80
+ // Identify their opacities before the hover.
81
+ const preOpacities = await page.evaluate(
82
+ elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
83
+ );
84
+ try {
85
+ // Hover over the potential trigger.
86
+ await firstTrigger.hover({timeout: 700});
87
+ // Identify whether it controls other elements.
88
+ const isController = await page.evaluate(
89
+ element => element.ariaHasPopup || element.hasAttribute('aria-controls'), firstTrigger
90
+ );
91
+ // Wait for any delayed and/or slowed hover reaction if likely.
92
+ await page.waitForTimeout(
93
+ elementsChecked++ < 10 || tagName !== triggerTag || isController ? 1200 : 200
94
+ );
95
+ await root.waitForElementState('stable');
96
+ // Identify the visible active descendants of the root during the hover.
97
+ const postVisibles = await root.$$(targetSelectors);
98
+ // Identify the opacities of the descendants of the root during the hover.
99
+ const postOpacities = await page.evaluate(
100
+ elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
101
+ );
102
+ // Identify the elements with opacity changes.
103
+ const opacityTargets = descendants
104
+ .filter((descendant, index) => postOpacities[index] !== preOpacities[index]);
105
+ // Count them and their descendants.
106
+ const opacityAffected = opacityTargets.length
107
+ ? await page.evaluate(elements => elements.reduce(
108
+ (total, current) => total + 1 + current.querySelectorAll('*').length, 0
109
+ ), opacityTargets)
110
+ : 0;
111
+ // If hovering disclosed any element or changed any opacity:
112
+ if (postVisibles.length > preVisibles.length || opacityAffected) {
113
+ // Preserve the lengthened reaction wait, if any, for the next 5 tries.
114
+ if (elementsChecked < 11) {
115
+ elementsChecked = 5;
116
+ }
117
+ // Hover over the upper-left corner of the page, to undo any hover reactions.
118
+ await page.hover('body', {
119
+ position: {
120
+ x: 0,
121
+ y: 0
122
+ }
123
+ });
124
+ // Wait for any delayed and/or slowed hover reaction.
125
+ await page.waitForTimeout(200);
126
+ await root.waitForElementState('stable');
127
+ // Increment the counts of triggers and targets.
128
+ data.totals.triggers++;
129
+ const madeVisible = Math.max(0, postVisibles.length - preVisibles.length);
130
+ data.totals.madeVisible += madeVisible;
131
+ data.totals.opacityChanged += opacityTargets.length;
132
+ data.totals.opacityAffected += opacityAffected;
133
+ // If details are to be reported:
134
+ if (withItems) {
135
+ // Report them.
136
+ const triggerDataJSHandle = await page.evaluateHandle(args => {
137
+ // Returns the text of an element.
138
+ const textOf = (element, limit) => {
139
+ const text = element.textContent.trim() || element.outerHTML.trim();
140
+ return text.replace(/\s{2,}/sg, ' ').slice(0, limit);
141
+ };
142
+ const trigger = args[0];
143
+ const preVisibles = args[1];
144
+ const postVisibles = args[2];
145
+ const madeVisible = postVisibles
146
+ .filter(el => ! preVisibles.includes(el))
147
+ .map(el => ({
148
+ tagName: el.tagName,
149
+ text: textOf(el, 50)
150
+ }));
151
+ const opacityChanged = args[3].map(el => ({
152
+ tagName: el.tagName,
153
+ text: textOf(el, 50)
154
+ }));
155
+ return {
156
+ tagName: trigger.tagName,
157
+ id: trigger.id || '',
158
+ text: textOf(trigger, 50),
159
+ madeVisible,
160
+ opacityChanged
161
+ };
162
+ }, [firstTrigger, preVisibles, postVisibles, opacityTargets]);
163
+ const triggerData = await triggerDataJSHandle.jsonValue();
164
+ data.items.triggers.push(triggerData);
165
+ }
166
+ }
167
+ }
168
+ catch (error) {
169
+ console.log('ERROR hovering');
170
+ // Returns the text of an element.
171
+ const textOf = async (element, limit) => {
172
+ let text = await element.textContent();
173
+ text = text.trim() || await element.innerHTML();
174
+ return text.trim().replace(/\s*/sg, '').slice(0, limit);
175
+ };
176
+ data.totals.unhoverables++;
177
+ if (withItems) {
178
+ data.items.unhoverables.push({
179
+ tagName: tagName,
180
+ id: firstTrigger.id || '',
181
+ text: await textOf(firstTrigger, 50)
182
+ });
183
+ }
184
+ }
185
+ triggerTag = tagName;
186
+ }
187
+ // Process the remaining potential triggers.
188
+ await find(triggers.slice(1));
189
+ }
190
+ };
191
+ // Find and document the hover-triggered disclosures.
192
+ await find(triggers);
193
+ // Return the result.
194
+ return {result: data};
195
+ };
package/tests/ibm.js ADDED
@@ -0,0 +1,89 @@
1
+ /*
2
+ ibm
3
+ This test implements the IBM Equal Access ruleset for accessibility.
4
+ The 'withNewContent' argument determines whether the test package should be
5
+ given the URL of the page to be tested (true), should be given the page content
6
+ (false), or should test in both ways (omitted).
7
+
8
+ Before using this test, you must:
9
+ 0. Create a file named aceconfig.js.
10
+ 1. Locate that file in the directory in which you call Testaro.
11
+ 2. Populate that file with this content:
12
+
13
+ module.exports = {
14
+ reportLevels: [
15
+ 'violation',
16
+ 'recommendation'
17
+ ],
18
+ outputFolder: 'temp/ibm'
19
+ };
20
+ */
21
+ // Import required modules.
22
+ const fs = require('fs/promises');
23
+ const {getCompliance} = require('accessibility-checker');
24
+ // Runs the IBM test.
25
+ const run = async content => {
26
+ const nowLabel = (new Date()).toISOString().slice(0, 19);
27
+ // Return the result of a test.
28
+ const report = await getCompliance(content, nowLabel);
29
+ /*
30
+ let timeoutID;
31
+ const deadline = new Promise(resolve => {
32
+ timeoutID = setTimeout(() => {
33
+ resolve('');
34
+ }, 20000);
35
+ });
36
+ const result = Promise.race([report, deadline]);
37
+ clearTimeout(timeoutID);
38
+ return result;
39
+ */
40
+ return report;
41
+ };
42
+ // Reports the result of an IBM test.
43
+ const report = (result, withItems) => {
44
+ const data = {};
45
+ if (result) {
46
+ data.totals = result.report.summary.counts;
47
+ if (withItems) {
48
+ data.items = result.report.results;
49
+ data.items.forEach(item => {
50
+ delete item.apiArgs;
51
+ delete item.category;
52
+ delete item.ignored;
53
+ delete item.messageArgs;
54
+ delete item.reasonId;
55
+ delete item.ruleTime;
56
+ delete item.value;
57
+ });
58
+ }
59
+ }
60
+ else {
61
+ data.error = 'ERROR: ibm test failed';
62
+ }
63
+ return data;
64
+ };
65
+ const all = {};
66
+ // Returns results of an IBM test.
67
+ exports.reporter = async (page, withItems, withNewContent) => {
68
+ // If the test is to be conducted with existing content:
69
+ if (! withNewContent) {
70
+ // Conduct and report it.
71
+ const content = await page.content();
72
+ const result = await run(content);
73
+ all.content = report(result, withItems);
74
+ }
75
+ // If the test is to be conducted with a URL:
76
+ if ([true, undefined].includes(withNewContent)) {
77
+ // Conduct and report it.
78
+ const content = page.url();
79
+ const result = await run(content);
80
+ all.url = report(result, withItems);
81
+ }
82
+ // Delete the report files.
83
+ fs.rm('temp/ibm', {recursive: true})
84
+ .catch(error => {
85
+ console.log(`ERROR deleting temporary ibm files (${error.message})`);
86
+ });
87
+ // Return the result.
88
+ return {result: all};
89
+ };
@@ -0,0 +1,157 @@
1
+ /*
2
+ labClash
3
+ This test reports defects in the labeling of buttons, non-hidden inputs, select lists, and
4
+ text areas. The defects include missing labels and redundant labels. Redundant labels are
5
+ labels that are superseded by other labels. Explicit and implicit (wrapped) labels are
6
+ additive, not conflicting.
7
+ */
8
+ exports.reporter = async (page, withItems) => {
9
+ return await page.$eval('body', (body, withItems) => {
10
+ // FUNCTION DEFINITION START
11
+ const debloat = text => text.replace(/\s+/g, ' ').trim();
12
+ // FUNCTION DEFINITION END
13
+ // Initialize a report.
14
+ const data = {
15
+ totals: {
16
+ mislabeled: 0,
17
+ unlabeled: 0,
18
+ wellLabeled: 0
19
+ }
20
+ };
21
+ if (withItems) {
22
+ data.items = {
23
+ mislabeled: [],
24
+ unlabeled: [],
25
+ wellLabeled: []
26
+ };
27
+ }
28
+ // Get data on the labelable form controls.
29
+ const labelees = Array.from(
30
+ body.querySelectorAll('button, input:not([type=hidden]), select, textarea')
31
+ );
32
+ // For each one:
33
+ labelees.forEach((labelee, index) => {
34
+ // Determine whether it has any or clashing labels and, if required, the label texts.
35
+ let labelTypes = [];
36
+ let texts = {};
37
+ // Attribute label.
38
+ if (labelee.hasAttribute('aria-label')) {
39
+ const trimmedLabel = debloat(labelee.ariaLabel);
40
+ if (trimmedLabel) {
41
+ labelTypes.push('aria-label');
42
+ if (withItems) {
43
+ texts.attribute = labelee.ariaLabel;
44
+ }
45
+ }
46
+ }
47
+ // Reference label.
48
+ if (labelee.hasAttribute('aria-labelledby')) {
49
+ labelTypes.push('aria-labelledby');
50
+ if (withItems) {
51
+ const labelerIDs = debloat(labelee.getAttribute('aria-labelledby')).split(' ');
52
+ const labelerTexts = labelerIDs
53
+ .map(id => {
54
+ const labeler = document.getElementById(id);
55
+ return labeler ? debloat(labeler.textContent) : '';
56
+ })
57
+ .filter(text => text);
58
+ if (labelerTexts.length) {
59
+ texts.referred = labelerTexts;
60
+ }
61
+ }
62
+ }
63
+ // Explicit and implicit labels.
64
+ const labels = Array.from(labelee.labels);
65
+ if (labels.length) {
66
+ labelTypes.push('label');
67
+ if (withItems) {
68
+ const labelTexts = labels.map(label => debloat(label.textContent)).filter(text => text);
69
+ if (labelTexts.length) {
70
+ texts.label = labelTexts;
71
+ }
72
+ }
73
+ }
74
+ // Content label if details required.
75
+ if (withItems) {
76
+ // Of button.
77
+ if (labelee.tagName === 'BUTTON') {
78
+ const content = debloat(labelee.textContent);
79
+ if (content) {
80
+ texts.content = content;
81
+ }
82
+ }
83
+ // Of submit input.
84
+ else if (labelee.tagName === 'INPUT' && labelee.type === 'submit' && labelee.value) {
85
+ const content = debloat(labelee.value);
86
+ if (content) {
87
+ texts.content = content;
88
+ }
89
+ }
90
+ }
91
+ const {totals} = data;
92
+ const labelTypeCount = labelTypes.length;
93
+ // If it is well labeled:
94
+ if (
95
+ labelTypeCount === 1
96
+ || ! labelTypeCount && (
97
+ labelee.tagName === 'BUTTON' && debloat(labelee.textContent).length
98
+ || labelee.tagName === 'INPUT' && labelee.type === 'submit' && labelee.value
99
+ )
100
+ ) {
101
+ // Increment the count of well-labeled items in the report.
102
+ totals.wellLabeled++;
103
+ // Add data on the item to the report, if required.
104
+ if (withItems) {
105
+ data.items.wellLabeled.push({
106
+ index,
107
+ tagName: labelee.tagName,
108
+ type: labelee.type,
109
+ labelType: labelTypes[0],
110
+ texts
111
+ });
112
+ }
113
+ }
114
+ // Otherwise, if it is unlabeled:
115
+ else if (! labelTypeCount) {
116
+ // Increment the count of unlabeled items in the report.
117
+ totals.unlabeled++;
118
+ // Add data on the item to the report, if required.
119
+ if (withItems) {
120
+ const item = {
121
+ index,
122
+ tagName: labelee.tagName,
123
+ type: labelee.type
124
+ };
125
+ if (
126
+ labelee.tagName === 'BUTTON'
127
+ || (labelee.tagName === 'INPUT' && labelee.type === 'submit')
128
+ ) {
129
+ item.content = texts.content || `{${debloat(labelee.outerHTML)}}`;
130
+ }
131
+ data.items.unlabeled.push(item);
132
+ }
133
+ }
134
+ // Otherwise, if it has clashing labels:
135
+ else if (labelTypeCount > 1) {
136
+ // Increment the count of labeling clashes in the report.
137
+ totals.mislabeled++;
138
+ // Add the data on the item to the report, if required.
139
+ if (withItems) {
140
+ data.items.mislabeled.push({
141
+ index,
142
+ tagName: labelee.tagName,
143
+ type: labelee.type,
144
+ labelTypes,
145
+ texts
146
+ });
147
+ }
148
+ }
149
+ });
150
+ return {result: data};
151
+ }, withItems)
152
+ .catch(error => {
153
+ console.log(`ERROR: labClash failed (${error.message})`);
154
+ const data = {error: 'ERROR: labClash failed'};
155
+ return {result: data};
156
+ });
157
+ };