testaro 13.0.2 → 14.1.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 (100) hide show
  1. package/README.md +190 -94
  2. package/actSpecs.js +17 -158
  3. package/package.json +1 -1
  4. package/run.js +31 -31
  5. package/standardize.js +338 -0
  6. package/{tests → testaro}/allHidden.js +36 -1
  7. package/{tests → testaro}/attVal.js +36 -2
  8. package/{tests → testaro}/autocomplete.js +34 -1
  9. package/{tests → testaro}/bulk.js +15 -1
  10. package/{tests → testaro}/docType.js +15 -1
  11. package/{tests → testaro}/dupAtt.js +34 -1
  12. package/{tests → testaro}/elements.js +9 -3
  13. package/testaro/embAc.js +78 -0
  14. package/{tests → testaro}/filter.js +35 -1
  15. package/{tests → testaro}/focAll.js +20 -3
  16. package/{tests → testaro}/focInd.js +45 -3
  17. package/{tests → testaro}/focOp.js +61 -2
  18. package/{tests → testaro}/focVis.js +35 -1
  19. package/{tests → testaro}/hover.js +67 -5
  20. package/{tests → testaro}/labClash.js +54 -4
  21. package/{tests → testaro}/linkTo.js +33 -1
  22. package/{tests → testaro}/linkUl.js +43 -5
  23. package/{tests → testaro}/menuNav.js +42 -1
  24. package/{tests → testaro}/miniText.js +32 -1
  25. package/{tests → testaro}/motion.js +27 -4
  26. package/{tests → testaro}/nonTable.js +32 -1
  27. package/{tests → testaro}/radioSet.js +33 -2
  28. package/{tests → testaro}/role.js +38 -1
  29. package/{tests → testaro}/styleDiff.js +43 -2
  30. package/{tests → testaro}/tabNav.js +42 -1
  31. package/{tests → testaro}/textNodes.js +6 -2
  32. package/{tests → testaro}/title.js +8 -4
  33. package/{tests → testaro}/titledEl.js +32 -1
  34. package/{tests → testaro}/zIndex.js +40 -2
  35. package/tests/alfa.js +72 -75
  36. package/tests/continuum.js +6 -2
  37. package/tests/ibm.js +14 -42
  38. package/tests/testaro.js +73 -0
  39. package/validation/tests/jobs/allHidden.json +877 -174
  40. package/validation/tests/jobs/attVal.json +57 -19
  41. package/validation/tests/jobs/autocomplete.json +34 -8
  42. package/validation/tests/jobs/bulk.json +33 -7
  43. package/validation/tests/jobs/docType.json +23 -5
  44. package/validation/tests/jobs/dupAtt.json +47 -8
  45. package/validation/tests/jobs/elements.json +231 -70
  46. package/validation/tests/jobs/embAc.json +70 -15
  47. package/validation/tests/jobs/filter.json +56 -12
  48. package/validation/tests/jobs/focAll.json +64 -16
  49. package/validation/tests/jobs/focInd.json +107 -23
  50. package/validation/tests/jobs/focOp.json +93 -21
  51. package/validation/tests/jobs/focVis.json +16 -4
  52. package/validation/tests/jobs/hover.json +246 -56
  53. package/validation/tests/jobs/labClash.json +43 -11
  54. package/validation/tests/jobs/linkTo.json +16 -4
  55. package/validation/tests/jobs/linkUl.json +79 -19
  56. package/validation/tests/jobs/menuNav.json +313 -65
  57. package/validation/tests/jobs/miniText.json +21 -5
  58. package/validation/tests/jobs/motion.json +81 -23
  59. package/validation/tests/jobs/nonTable.json +26 -6
  60. package/validation/tests/jobs/radioSet.json +43 -11
  61. package/validation/tests/jobs/role.json +93 -19
  62. package/validation/tests/jobs/styleDiff.json +124 -28
  63. package/validation/tests/jobs/tabNav.json +313 -65
  64. package/validation/tests/jobs/textNodes.json +190 -49
  65. package/validation/tests/jobs/title.json +23 -5
  66. package/validation/tests/jobs/titledEl.json +26 -6
  67. package/validation/tests/jobs/zIndex.json +28 -8
  68. package/validation/tests/old/allHidden.json +314 -0
  69. package/validation/tests/old/attVal.json +60 -0
  70. package/validation/tests/old/autocomplete.json +51 -0
  71. package/validation/tests/old/bulk.json +48 -0
  72. package/validation/tests/old/docType.json +46 -0
  73. package/validation/tests/old/dupAtt.json +51 -0
  74. package/validation/tests/old/elements.json +140 -0
  75. package/validation/tests/old/embAc.json +54 -0
  76. package/validation/tests/old/filter.json +55 -0
  77. package/validation/tests/old/focAll.json +68 -0
  78. package/validation/tests/old/focInd.json +69 -0
  79. package/validation/tests/old/focOp.json +62 -0
  80. package/validation/tests/old/focVis.json +35 -0
  81. package/validation/tests/old/hover.json +118 -0
  82. package/validation/tests/old/labClash.json +52 -0
  83. package/validation/tests/old/linkTo.json +35 -0
  84. package/validation/tests/old/linkUl.json +71 -0
  85. package/validation/tests/old/menuNav.json +106 -0
  86. package/validation/tests/old/miniText.json +36 -0
  87. package/validation/tests/old/motion.json +62 -0
  88. package/validation/tests/old/nonTable.json +37 -0
  89. package/validation/tests/old/radioSet.json +52 -0
  90. package/validation/tests/old/role.json +60 -0
  91. package/validation/tests/old/styleDiff.json +71 -0
  92. package/validation/tests/old/tabNav.json +106 -0
  93. package/validation/tests/old/temp.js +28 -0
  94. package/validation/tests/old/textNodes.json +98 -0
  95. package/validation/tests/old/title.json +46 -0
  96. package/validation/tests/old/titledEl.json +37 -0
  97. package/validation/tests/old/zIndex.json +49 -0
  98. package/validation/tests/targets/attVal/good.html +1 -1
  99. package/validation/tests/targets/elements/index.html +1 -0
  100. package/tests/embAc.js +0 -36
package/standardize.js ADDED
@@ -0,0 +1,338 @@
1
+ /*
2
+ standardize.js
3
+ Converts test results to the standard format.
4
+ */
5
+
6
+ // ########## FUNCTIONS
7
+
8
+ // Limits the length of and unilinearizes a string.
9
+ const cap = rawString => {
10
+ const string = rawString.replace(/\s+/g, ' ');
11
+ if (string && string.length > 400) {
12
+ return `${string.slice(0, 200)} ... ${string.slice(-200)}`;
13
+ }
14
+ else if (string) {
15
+ return string;
16
+ }
17
+ else {
18
+ return '';
19
+ }
20
+ };
21
+ // Converts issue instances at an axe certainty level.
22
+ const doAxe = (result, standardResult, certainty) => {
23
+ if (result.details && result.details[certainty]) {
24
+ result.details[certainty].forEach(rule => {
25
+ rule.nodes.forEach(node => {
26
+ const whatSet = new Set([
27
+ rule.help,
28
+ ... node.any.map(anyItem => anyItem.message),
29
+ ... node.all.map(allItem => allItem.message)
30
+ ]);
31
+ const initialSeverity = ['minor', 'moderate', 'serious', 'critical'].indexOf(node.impact);
32
+ const moreSeverity = certainty === 'violations' ? 4 : 0;
33
+ const instance = {
34
+ issueID: rule.id,
35
+ what: Array.from(whatSet.values()).join('; '),
36
+ ordinalSeverity: initialSeverity + moreSeverity,
37
+ location: {
38
+ doc: 'dom',
39
+ type: 'selector',
40
+ spec: node.target && node.target.length ? node.target[0] : ''
41
+ },
42
+ excerpt: cap(node.html)
43
+ };
44
+ standardResult.instances.push(instance);
45
+ });
46
+ });
47
+ }
48
+ };
49
+ // Converts issue instances at an htmlcs severity level.
50
+ const doHTMLCS = (result, standardResult, severity) => {
51
+ if (result[severity]) {
52
+ Object.keys(result[severity]).forEach(ruleID => {
53
+ const ruleData = result[severity][ruleID];
54
+ Object.keys(ruleData).forEach(what => {
55
+ ruleData[what].forEach(item => {
56
+ const {tagName, code} = item;
57
+ const instance = {
58
+ issueID: ruleID,
59
+ what,
60
+ ordinalSeverity: ['Warning', 'Error'].indexOf(severity),
61
+ location: {
62
+ doc: 'dom',
63
+ type: '',
64
+ spec: ''
65
+ },
66
+ excerpt: cap(`${tagName ? tagName + ': ' : ''}${code || ''}`)
67
+ };
68
+ standardResult.instances.push(instance);
69
+ });
70
+ });
71
+ });
72
+ }
73
+ };
74
+ // Converts issue instances from a nuVal document type.
75
+ const doNuVal = (result, standardResult, docType) => {
76
+ const items = result[docType] && result[docType].messages;
77
+ if (items && items.length) {
78
+ items.forEach(item => {
79
+ const instance = {
80
+ issueID: item.message,
81
+ what: item.message,
82
+ ordinalSeverity: -1,
83
+ location: {
84
+ doc: docType === 'pageContent' ? 'dom' : 'source',
85
+ type: 'line',
86
+ spec: item.lastLine.toString()
87
+ },
88
+ excerpt: cap(item.extract)
89
+ };
90
+ const {type, subType} = item;
91
+ if (type === 'info' && subType === 'warning') {
92
+ instance.ordinalSeverity = 0;
93
+ }
94
+ else if (type === 'error') {
95
+ instance.ordinalSeverity = subType === 'fatal' ? 2 : 1;
96
+ }
97
+ standardResult.instances.push(instance);
98
+ });
99
+ }
100
+ };
101
+ // Converts instances of a qualWeb rule class.
102
+ const doQualWeb = (result, standardResult, ruleClassName) => {
103
+ if (result.modules && result.modules[ruleClassName]) {
104
+ const ruleClass = result.modules[ruleClassName];
105
+ let classSeverity = 0;
106
+ if (ruleClass.metadata) {
107
+ classSeverity = 2 * [
108
+ 'best-practices', 'wcag-techniques', 'act-rules'
109
+ ].indexOf(ruleClassName);
110
+ standardResult.totals[classSeverity] += ruleClass.metadata.warning;
111
+ standardResult.totals[classSeverity + 1] += ruleClass.metadata.failed;
112
+ }
113
+ Object.keys(ruleClass.assertions).forEach(rule => {
114
+ ruleClass.assertions[rule].results.forEach(item => {
115
+ item.elements.forEach(element => {
116
+ const instance = {
117
+ issueID: ruleClass.assertions[rule].name,
118
+ what: ruleClass.assertions[rule].description,
119
+ ordinalSeverity: classSeverity + (item.verdict === 'failed' ? 1 : 0),
120
+ location: {
121
+ doc: 'dom',
122
+ type: 'selector',
123
+ spec: element.pointer
124
+ },
125
+ excerpt: cap(element.htmlCode)
126
+ };
127
+ standardResult.instances.push(instance);
128
+ });
129
+ });
130
+ });
131
+ }
132
+ };
133
+ // Converts instances of a wave rule category.
134
+ const doWAVE = (result, standardResult, categoryName) => {
135
+ if (result.categories && result.categories[categoryName]) {
136
+ const category = result.categories[categoryName];
137
+ const ordinalSeverity = categoryName === 'alert' ? 0 : 1;
138
+ Object.keys(category.items).forEach(rule => {
139
+ category.items[rule].selectors.forEach(selector => {
140
+ const instance = {
141
+ issueID: rule,
142
+ what: category.items[rule].description,
143
+ ordinalSeverity,
144
+ location: {
145
+ doc: 'dom',
146
+ type: 'selector',
147
+ spec: selector
148
+ },
149
+ excerpt: ''
150
+ };
151
+ standardResult.instances.push(instance);
152
+ });
153
+ });
154
+ }
155
+ };
156
+ // Converts a result.
157
+ const convert = (testName, result, standardResult) => {
158
+ // alfa
159
+ if (testName === 'alfa' && result.totals) {
160
+ standardResult.totals = [result.totals.warnings, result.totals.failures];
161
+ result.items.forEach(item => {
162
+ const instance = {
163
+ issueID: item.rule.ruleID,
164
+ what: item.rule.ruleSummary,
165
+ ordinalSeverity: ['cantTell', 'failed'].indexOf(item.verdict),
166
+ location: {
167
+ doc: 'dom',
168
+ type: 'xpath',
169
+ spec: item.target.path
170
+ },
171
+ excerpt: cap(item.target.codeLines.join(' '))
172
+ };
173
+ standardResult.instances.push(instance);
174
+ });
175
+ }
176
+ // axe
177
+ else if (
178
+ testName === 'axe'
179
+ && result.totals
180
+ && (result.totals.rulesWarned || result.totals.rulesViolated)
181
+ ) {
182
+ const {totals} = result;
183
+ standardResult.totals = [
184
+ totals.warnings.minor,
185
+ totals.warnings.moderate,
186
+ totals.warnings.serious,
187
+ totals.warnings.critical,
188
+ totals.violations.minor,
189
+ totals.violations.moderate,
190
+ totals.violations.serious,
191
+ totals.violations.critical
192
+ ];
193
+ doAxe(result, standardResult, 'incomplete');
194
+ doAxe(result, standardResult, 'violations');
195
+ }
196
+ // continuum
197
+ else if (testName === 'continuum' && Array.isArray(result) && result.length) {
198
+ standardResult.totals = [result.length];
199
+ result.forEach(item => {
200
+ const instance = {
201
+ issueID: item.engineTestId.toString(),
202
+ what: item.attributeDetail,
203
+ ordinalSeverity: 0,
204
+ location: {
205
+ doc: 'dom',
206
+ type: 'selector',
207
+ spec: item.path
208
+ },
209
+ excerpt: item.element
210
+ };
211
+ standardResult.instances.push(instance);
212
+ });
213
+ }
214
+ // htmlcs
215
+ else if (testName === 'htmlcs' && result) {
216
+ doHTMLCS(result, standardResult, 'Warning');
217
+ doHTMLCS(result, standardResult, 'Error');
218
+ const {instances} = standardResult;
219
+ standardResult.totals = [
220
+ instances.filter(instance => instance.ordinalSeverity === 0).length,
221
+ instances.filter(instance => instance.ordinalSeverity === 1).length
222
+ ];
223
+ }
224
+ // ibm
225
+ else if (testName === 'ibm' && result.totals) {
226
+ standardResult.totals = [result.totals.recommendation, result.totals.violation];
227
+ result.items.forEach(item => {
228
+ const instance = {
229
+ issueID: item.ruleId,
230
+ what: item.message,
231
+ ordinalSeverity: ['recommendation', 'violation'].indexOf(item.level),
232
+ location: {
233
+ doc: 'dom',
234
+ type: 'xpath',
235
+ spec: item.path.dom
236
+ },
237
+ excerpt: cap(item.snippet)
238
+ };
239
+ standardResult.instances.push(instance);
240
+ });
241
+ }
242
+ // nuVal
243
+ else if (testName === 'nuVal' && (result.pageContent || result.rawPage)) {
244
+ if (result.pageContent) {
245
+ doNuVal(result, standardResult, 'pageContent');
246
+ }
247
+ if (result.rawPage) {
248
+ doNuVal(result, standardResult, 'rawPage');
249
+ }
250
+ const {instances} = standardResult;
251
+ standardResult.totals = [
252
+ instances.filter(instance => instance.ordinalSeverity === 0).length,
253
+ instances.filter(instance => instance.ordinalSeverity === 1).length,
254
+ instances.filter(instance => instance.ordinalSeverity === 2).length
255
+ ];
256
+ }
257
+ // qualWeb
258
+ else if (
259
+ testName === 'qualWeb'
260
+ && result.modules
261
+ && (
262
+ result.modules['act-rules']
263
+ || result.modules['wcag-techniques']
264
+ || result.modules['best-practices']
265
+ )
266
+ ) {
267
+ standardResult.totals = [0, 0, 0, 0, 0, 0];
268
+ if (result.modules['act-rules']) {
269
+ doQualWeb(result, standardResult, 'act-rules');
270
+ }
271
+ if (result.modules['wcag-techniques']) {
272
+ doQualWeb(result, standardResult, 'wcag-techniques');
273
+ }
274
+ if (result.modules['best-practices']) {
275
+ doQualWeb(result, standardResult, 'best-practices');
276
+ }
277
+ }
278
+ // tenon
279
+ else if (testName === 'tenon' && result.data && result.data.resultSet) {
280
+ result.data.resultSet.forEach(item => {
281
+ const instance = {
282
+ issueID: item.tID ? item.tID.toString() : '',
283
+ what: item.errorTitle || '',
284
+ ordinalSeverity: Math.min(
285
+ 5, Math.max(0, Math.round((item.certainty || 0) * (item.priority || 0) / 2000))
286
+ ),
287
+ location: {
288
+ doc: 'dom',
289
+ type: 'xpath',
290
+ spec: item.xpath || ''
291
+ },
292
+ excerpt: cap(item.errorSnippet || '')
293
+ };
294
+ standardResult.instances.push(instance);
295
+ });
296
+ standardResult.totals = [0, 0, 0, 0, 0, 0];
297
+ standardResult.instances.forEach(instance => {
298
+ standardResult.totals[instance.ordinalSeverity]++;
299
+ });
300
+ }
301
+ // testaro
302
+ else if (testName === 'testaro') {
303
+ const rules = Object.keys(result.rules);
304
+ standardResult.totals = [0, 0, 0, 0];
305
+ rules.forEach(rule => {
306
+ const ruleResult = result.rules[rule];
307
+ standardResult.totals.forEach((total, index) => {
308
+ standardResult.totals[index] += ruleResult.totals[index];
309
+ });
310
+ standardResult.instances.push(... ruleResult.standardInstances);
311
+ });
312
+ standardResult.totals = standardResult.totals.map(total => Math.round(total));
313
+ }
314
+ // wave
315
+ else if (
316
+ testName === 'wave'
317
+ && result.categories
318
+ && (
319
+ result.categories.error
320
+ || result.categories.contrast
321
+ || result.categories.alert
322
+ )
323
+ ) {
324
+ const {categories} = result;
325
+ standardResult.totals = [
326
+ categories.alert.count || 0, (categories.error.count || 0) + (categories.contrast.count || 0)
327
+ ];
328
+ ['error', 'contrast', 'alert'].forEach(categoryName => {
329
+ doWAVE(result, standardResult, categoryName);
330
+ });
331
+ }
332
+ };
333
+ // Converts the results.
334
+ exports.standardize = act => {
335
+ const {which} = act;
336
+ const {result, standardResult} = act;
337
+ convert(which, result, standardResult);
338
+ };
@@ -45,5 +45,40 @@ exports.reporter = async page => {
45
45
  });
46
46
  return data;
47
47
  });
48
- return {result: data};
48
+ const standardInstances = [];
49
+ const reportables = {
50
+ hidden: [0, 'hidden'],
51
+ reallyHidden: [1, 'effectively hidden'],
52
+ visHidden: [0, 'visually hidden'],
53
+ ariaHidden: [1, 'hidden by ARIA'],
54
+ document: [1, 'Document', 'document.documentElement'],
55
+ body: [1, 'Document body', 'document.body'],
56
+ main: [0, 'main region', 'main, [role="main"]']
57
+ };
58
+ ['document', 'body', 'main'].forEach(region => {
59
+ ['hidden', 'reallyHidden', 'visHidden', 'ariaHidden'].forEach(hider => {
60
+ if (data[hider][region]) {
61
+ standardInstances.push({
62
+ issueID: `allHidden-${hider}-${region}`,
63
+ what: `${reportables[region][1]} ${reportables[hider][1]}`,
64
+ ordinalSeverity: reportables[region][0] + reportables[hider][0] || 0,
65
+ location: {
66
+ doc: 'dom',
67
+ type: 'selector',
68
+ spec: reportables[region][2]
69
+ },
70
+ excerpt: ''
71
+ });
72
+ }
73
+ });
74
+ });
75
+ const totals = [0, 0, 0, 0];
76
+ standardInstances.forEach(instance => {
77
+ totals[instance.ordinalSeverity]++;
78
+ });
79
+ return {
80
+ data,
81
+ totals,
82
+ standardInstances
83
+ };
49
84
  };
@@ -2,7 +2,8 @@
2
2
  attVal
3
3
  This test reports attributes with illicit values.
4
4
  */
5
- exports.reporter = async (page, attributeName, areLicit, values, withItems) => {
5
+ exports.reporter = async (page, withItems, attributeName, areLicit, values) => {
6
+ console.log('Starting reporter');
6
7
  // Identify the elements that have the specified attribute with illicit values.
7
8
  const badAttributeData = await page.evaluate(
8
9
  async args => {
@@ -41,8 +42,41 @@ exports.reporter = async (page, attributeName, areLicit, values, withItems) => {
41
42
  const data = {
42
43
  total: badAttributeData.length
43
44
  };
45
+ const standardInstances = [];
44
46
  if (withItems) {
45
47
  data.items = badAttributeData;
48
+ badAttributeData.forEach(item => {
49
+ standardInstances.push({
50
+ issueID: `attVal-${item.tagName}-${attributeName}`,
51
+ what:
52
+ `${item.tagName} element has attribute ${attributeName} with illicit value ${item.attributeValue}`,
53
+ ordinalSeverity: 2,
54
+ location: {
55
+ doc: '',
56
+ type: '',
57
+ spec: ''
58
+ },
59
+ excerpt: item.textStart
60
+ });
61
+ });
62
+ }
63
+ else if (data.total) {
64
+ standardInstances.push({
65
+ issueID: 'attVal',
66
+ what: 'Elements have attributes with illicit values',
67
+ ordinalSeverity: 2,
68
+ location: {
69
+ doc: '',
70
+ type: '',
71
+ spec: ''
72
+ },
73
+ excerpt: ''
74
+ });
46
75
  }
47
- return {result: data};
76
+ const totals = [0, 0, data.total, 0];
77
+ return {
78
+ data,
79
+ totals,
80
+ standardInstances
81
+ };
48
82
  };
@@ -70,6 +70,39 @@ exports.reporter = async (page, withItems) => {
70
70
  }
71
71
  }
72
72
  }
73
+ const standardInstances = [];
74
+ if (data.items) {
75
+ data.items.forEach(item => {
76
+ standardInstances.push({
77
+ issueID: `autocomplete-${item[0]}`,
78
+ what: `Input is missing the required autocomplete attribute with value ${item[0]}`,
79
+ ordinalSeverity: 2,
80
+ location: {
81
+ doc: '',
82
+ type: '',
83
+ spec: ''
84
+ },
85
+ excerpt: item[1]
86
+ });
87
+ });
88
+ }
89
+ else if (data.total) {
90
+ standardInstances.push({
91
+ issueID: 'autocomplete',
92
+ what: 'Inputs are missing required autocomplete attributes',
93
+ ordinalSeverity: 2,
94
+ location: {
95
+ doc: '',
96
+ type: '',
97
+ spec: ''
98
+ },
99
+ excerpt: ''
100
+ });
101
+ }
73
102
  // Return the data.
74
- return {result: data};
103
+ return {
104
+ data,
105
+ totals: [0, 0, data.total, 0],
106
+ standardInstances
107
+ };
75
108
  };
@@ -18,5 +18,19 @@ exports.reporter = async page => {
18
18
  });
19
19
  const visibleElements = await page.$$('body :visible');
20
20
  data.visibleElements = visibleElements.length;
21
- return {result: data};
21
+ return {
22
+ data,
23
+ totals: [Math.round(data.visibleElements / 400), 0, 0, 0],
24
+ standardInstances: data.visibleElements < 200 ? [] : [{
25
+ issueID: 'bulk',
26
+ what: 'Page contains a large number of visible elements',
27
+ ordinalSeverity: 0,
28
+ location: {
29
+ doc: '',
30
+ type: '',
31
+ spec: ''
32
+ },
33
+ excerpt: ''
34
+ }]
35
+ };
22
36
  };
@@ -10,5 +10,19 @@ exports.reporter = async page => {
10
10
  const docHasType = !! docType && docType.name && docType.name.toLowerCase() === 'html';
11
11
  return docHasType;
12
12
  });
13
- return {result: {docHasType}};
13
+ return {
14
+ data: {docHasType},
15
+ totals: [0, 0, 0, docHasType ? 0 : 1],
16
+ standardInstances: docHasType ? [] : [{
17
+ issueID: 'docType',
18
+ what: 'Document has no standard HTML doctype preamble',
19
+ ordinalSeverity: 3,
20
+ location: {
21
+ doc: '',
22
+ type: '',
23
+ spec: ''
24
+ },
25
+ excerpt: ''
26
+ }]
27
+ };
14
28
  };
@@ -76,6 +76,39 @@ exports.reporter = async (page, withItems) => {
76
76
  }
77
77
  }
78
78
  });
79
+ const standardInstances = [];
80
+ if (data.items) {
81
+ data.items.forEach(item => {
82
+ standardInstances.push({
83
+ issueID: 'dupAtt',
84
+ what: `Element ${item.tagName} has 2 attributes with the same name`,
85
+ ordinalSeverity: 2,
86
+ location: {
87
+ doc: '',
88
+ type: '',
89
+ spec: ''
90
+ },
91
+ excerpt: item.attributes.join(' ... ')
92
+ });
93
+ });
94
+ }
95
+ else if (data.total) {
96
+ standardInstances.push({
97
+ issueID: 'dupAtt',
98
+ what: 'In some elements 2 attributes have the same name',
99
+ ordinalSeverity: 2,
100
+ location: {
101
+ doc: '',
102
+ type: '',
103
+ spec: ''
104
+ },
105
+ excerpt: ''
106
+ });
107
+ }
79
108
  // Return the data.
80
- return {result: data};
109
+ return {
110
+ data,
111
+ totals: [0, 0, data.total, 0],
112
+ standardInstances
113
+ };
81
114
  };
@@ -1,13 +1,15 @@
1
1
  /*
2
2
  elements
3
- This test reports data about specified elements.
3
+ This test reports data about specified elements within the document body.
4
4
  Meanings of detailLevel values:
5
5
  0. Only total element count; no detail.
6
6
  1. Also data on each specified element.
7
7
  2. Data on each specified element also include the text content of the parent element.
8
8
  3. Data on each specified element also include data on its sibling nodes.
9
9
  */
10
- exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) => {
10
+ exports.reporter = async (
11
+ page, withItems, detailLevel = 0, tagName = null, onlyVisible = false, attribute
12
+ ) => {
11
13
  // Determine a selector of the specified elements, including any descendants of open shadow roots.
12
14
  let selector = `body ${tagName ? tagName.toLowerCase() : '*'}`;
13
15
  if (attribute) {
@@ -155,5 +157,9 @@ exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) =>
155
157
  };
156
158
  }
157
159
  // Return the result.
158
- return {result: data};
160
+ return {
161
+ data,
162
+ totals: [],
163
+ standardInstances: []
164
+ };
159
165
  };
@@ -0,0 +1,78 @@
1
+ /*
2
+ embAc
3
+ This test reports interactive elements (links, buttons, inputs, and select lists)
4
+ contained by links or buttons. Such embedding not only violates the HTML standard,
5
+ but also complicates user interaction and creates risks of error. It becomes
6
+ non-obvious what a user will activate with a click.
7
+ */
8
+ exports.reporter = async (page, withItems) => await page.$$eval(
9
+ 'a a, a button, a input, a select, button a, button button, button input, button select',
10
+ (bads, withItems) => {
11
+ // FUNCTION DEFINITION START
12
+ // Returns a space-minimized copy of a string.
13
+ const compact = string => string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim();
14
+ // FUNCTION DEFINITION END
15
+ const totals = {
16
+ links: 0,
17
+ buttons: 0,
18
+ inputs: 0,
19
+ selects: 0
20
+ };
21
+ const items = [];
22
+ // Total and, if requested, itemize the faulty elements.
23
+ bads.forEach(bad => {
24
+ totals[Object.keys(totals)[['A', 'BUTTON', 'INPUT', 'SELECT'].indexOf(bad.tagName)]]++;
25
+ let container;
26
+ if (withItems) {
27
+ if (['A', 'BUTTON'].includes(bad.parentElement.tagName)) {
28
+ container = bad.parentElement;
29
+ }
30
+ else {
31
+ container = bad.parentElement.parentElement;
32
+ }
33
+ items.push({
34
+ embeddedElement: bad.tagName,
35
+ excerpt: compact(container.outerHTML)
36
+ });
37
+ }
38
+ });
39
+ // Return the result.
40
+ const data = {totals};
41
+ const standardInstances = [];
42
+ const total = Object.values(data.totals).reduce((sum, current) => sum + current);
43
+ if (withItems) {
44
+ data.items = items;
45
+ items.forEach(item => {
46
+ standardInstances.push({
47
+ issueID: `embAc-${item.embeddedElement}`,
48
+ what: `Element ${item.embeddedElement} is embedded in a link or button`,
49
+ ordinalSeverity: 2,
50
+ location: {
51
+ doc: '',
52
+ type: '',
53
+ spec: ''
54
+ },
55
+ excerpt: item.excerpt
56
+ });
57
+ });
58
+ }
59
+ else if (total) {
60
+ standardInstances.push({
61
+ issueID: 'embAc',
62
+ what: 'Interactive elements are contained by links or buttons',
63
+ ordinalSeverity: 2,
64
+ location: {
65
+ doc: '',
66
+ type: '',
67
+ spec: ''
68
+ },
69
+ excerpt: ''
70
+ });
71
+ }
72
+ return {
73
+ data,
74
+ totals: [0, 0, total, 0],
75
+ standardInstances
76
+ };
77
+ }, withItems
78
+ );