@caweb/a11y-webpack-plugin 1.1.0 → 2.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/index.js +1 -1
- package/package.json +5 -4
- package/reporter.js +390 -0
package/index.js
CHANGED
|
@@ -173,7 +173,7 @@ class A11yPlugin {
|
|
|
173
173
|
// // )
|
|
174
174
|
// }
|
|
175
175
|
|
|
176
|
-
console.log(`<i> ${boldGreen('[webpack-dev-middleware] IBM Accessibilty Report can be viewed at')} ${ boldBlue(new URL(`${auditUrl}
|
|
176
|
+
console.log(`<i> ${boldGreen('[webpack-dev-middleware] IBM Accessibilty Report can be viewed at')} ${ boldBlue(new URL(`${auditUrl}${staticDir.publicPath}/${this.config.outputFilename}.html`).toString()) }`);
|
|
177
177
|
|
|
178
178
|
});
|
|
179
179
|
})
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caweb/a11y-webpack-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "CAWebPublishing Webpack Plugin to run Accessibility Scans",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"aceconfig.js",
|
|
9
9
|
"index.js",
|
|
10
|
+
"reporter.js",
|
|
10
11
|
"README.md"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
@@ -32,12 +33,12 @@
|
|
|
32
33
|
"webpack"
|
|
33
34
|
],
|
|
34
35
|
"dependencies": {
|
|
35
|
-
"accessibility-checker": "^
|
|
36
|
+
"accessibility-checker": "^4.0.7",
|
|
36
37
|
"check-valid-url": "^0.1.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"webpack": "^5.
|
|
40
|
-
"webpack-cli": "^
|
|
40
|
+
"webpack": "^5.101.0",
|
|
41
|
+
"webpack-cli": "^6.0.1"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"@caweb/template": "^1.0.2"
|
package/reporter.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import HandleBars from 'handlebars';
|
|
7
|
+
import htmlFormat from 'html-format';
|
|
8
|
+
|
|
9
|
+
import endsWith from '@caweb/webpack/helpers/logic/endsWith.js';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const templateDir = path.resolve( 'node_modules', '@caweb', 'template' );
|
|
13
|
+
|
|
14
|
+
let templatePartials = {
|
|
15
|
+
'header': 'semantics/header.html',
|
|
16
|
+
'footer': 'semantics/footer.html',
|
|
17
|
+
'utilityHeader': 'semantics/utility-header.html',
|
|
18
|
+
'branding': 'semantics/branding.html',
|
|
19
|
+
'mobileControls': 'semantics/mobile-controls.html',
|
|
20
|
+
'navHeader': 'semantics/nav-header.html',
|
|
21
|
+
'navFooter': 'semantics/nav-footer.html',
|
|
22
|
+
'alert': 'components/alert/alert.html',
|
|
23
|
+
'searchForm': 'forms/search.html'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let sortedReport = {
|
|
27
|
+
errors: [],
|
|
28
|
+
warnings: [],
|
|
29
|
+
info: []
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let title = `IBM Accessibility Equal Access Toolkit: Accessibility Checker Report`;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Process data
|
|
37
|
+
*
|
|
38
|
+
* Data Object
|
|
39
|
+
* {
|
|
40
|
+
* functions,
|
|
41
|
+
* options,
|
|
42
|
+
* errors,
|
|
43
|
+
* globals,
|
|
44
|
+
* unused,
|
|
45
|
+
* member,
|
|
46
|
+
* file
|
|
47
|
+
* }
|
|
48
|
+
*/
|
|
49
|
+
function addBreakdown({
|
|
50
|
+
functions,
|
|
51
|
+
options,
|
|
52
|
+
errors,
|
|
53
|
+
implieds,
|
|
54
|
+
globals,
|
|
55
|
+
unused,
|
|
56
|
+
member,
|
|
57
|
+
file
|
|
58
|
+
}){
|
|
59
|
+
let functionList = [];
|
|
60
|
+
let errorList = [];
|
|
61
|
+
let unusedList = [];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process function data
|
|
65
|
+
*
|
|
66
|
+
* Function Data Object
|
|
67
|
+
* {
|
|
68
|
+
* name,
|
|
69
|
+
* param,
|
|
70
|
+
* line,
|
|
71
|
+
* character,
|
|
72
|
+
* last,
|
|
73
|
+
* lastcharacter,
|
|
74
|
+
* metrics {
|
|
75
|
+
* complexity,
|
|
76
|
+
* parameters,
|
|
77
|
+
* statements
|
|
78
|
+
* }
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
if( functions ){
|
|
82
|
+
functions.forEach(({ name, param, line, character, metrics }) => {
|
|
83
|
+
let { complexity, parameters, statements } = metrics;
|
|
84
|
+
|
|
85
|
+
functionList.push(
|
|
86
|
+
'<li>',
|
|
87
|
+
`<p><b>Name:</b> ${name}</p>`,
|
|
88
|
+
param && param.length ? `<p><b>Parameters:</b> ${param}</p>` : '',
|
|
89
|
+
`<p><b>Line:</b> ${line}</p>`,
|
|
90
|
+
`<p><b>Col:</b> ${character}</p>`,
|
|
91
|
+
`<p><b>Metrics:</b></p>`,
|
|
92
|
+
'<ul>',
|
|
93
|
+
`<li><b>Cyclomatic Complexity Number:</b> ${complexity}</li>`,
|
|
94
|
+
`<li><b>Arguments:</b> ${parameters}</li>`,
|
|
95
|
+
`<li><b>Statements:</b> ${statements}</li>`,
|
|
96
|
+
'</ul>',
|
|
97
|
+
'</li>'
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Process error data
|
|
105
|
+
*
|
|
106
|
+
* Error Data Object
|
|
107
|
+
* {
|
|
108
|
+
* id,
|
|
109
|
+
* raw,
|
|
110
|
+
* code,
|
|
111
|
+
* evidence,
|
|
112
|
+
* line,
|
|
113
|
+
* character,
|
|
114
|
+
* scope,
|
|
115
|
+
* a,
|
|
116
|
+
* b,
|
|
117
|
+
* c,
|
|
118
|
+
* d,
|
|
119
|
+
* reason
|
|
120
|
+
* }
|
|
121
|
+
*/
|
|
122
|
+
if( errors ){
|
|
123
|
+
errors.forEach(({reason, evidence, line, character}) => {
|
|
124
|
+
errorList.push(
|
|
125
|
+
'<li>',
|
|
126
|
+
`<p><b>Reason:</b> ${reason}</p>`,
|
|
127
|
+
`<p><b>Evidence:</b> ${evidence}</p>`,
|
|
128
|
+
`<p><b>Line:</b> ${line}</p>`,
|
|
129
|
+
`<p><b>Col:</b> ${character}</p>`,
|
|
130
|
+
'</li>'
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Unused Data
|
|
137
|
+
* {
|
|
138
|
+
* name,
|
|
139
|
+
* line,
|
|
140
|
+
* character
|
|
141
|
+
* }
|
|
142
|
+
*/
|
|
143
|
+
if( unused ){
|
|
144
|
+
unused.forEach(({name, line, character}) => {
|
|
145
|
+
unusedList.push(
|
|
146
|
+
'<li>',
|
|
147
|
+
`<p><b>Name:</b> ${name}</p>`,
|
|
148
|
+
`<p><b>Line:</b> ${line}</p>`,
|
|
149
|
+
`<p><b>Col:</b> ${character}</p>`,
|
|
150
|
+
'</li>'
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return `<section id="${file.replace(/[\\:\.]/g, '-').toLowerCase()}" class="mb-5 border border-2">
|
|
156
|
+
<div class="bg-light p-4"><h4>File: <a href="file://${file}" target="_blank" class="fst-italic fs-md text-break">${file}</a></h4></div>
|
|
157
|
+
<div class="p-4">
|
|
158
|
+
<h5>Functions: <span class="bg-light rounded-circle p-2">${functions.length}</span></h5>
|
|
159
|
+
${ functionList.length ? `<ol>${functionList.join('\n')}</ol>` : ''}
|
|
160
|
+
<h5>Errors: <span class="bg-light rounded-circle p-2">${errors ? errors.length : 0}</span></h5>
|
|
161
|
+
${ errorList.length ? `<ol>${errorList.join('\n')}</ol>` : '' }
|
|
162
|
+
<h5>Unused: <span class="bg-light rounded-circle p-2">${unused ? unused.length : 0}</span></h5>
|
|
163
|
+
${ unusedList.length ? `<ol>${unusedList.join('\n')}</ol>` : '' }
|
|
164
|
+
</div>
|
|
165
|
+
</section>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function initHandleBars(){
|
|
169
|
+
// Register partials.
|
|
170
|
+
Object.entries(templatePartials).forEach(([p, f]) => HandleBars.registerPartial(p, fs.readFileSync(path.resolve(templateDir, f )).toString() ) );
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
// Register custom helpers.
|
|
174
|
+
HandleBars.registerHelper('endsWith', endsWith )
|
|
175
|
+
|
|
176
|
+
return HandleBars.compile(fs.readFileSync(path.resolve(templateDir, 'patterns', 'index.html')).toString() )
|
|
177
|
+
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* JSHint Reporter
|
|
182
|
+
*
|
|
183
|
+
* @param {*} results
|
|
184
|
+
* @param {*} data
|
|
185
|
+
* @param {*} opts
|
|
186
|
+
*/
|
|
187
|
+
function reporter(data, opts){
|
|
188
|
+
let output = [];
|
|
189
|
+
|
|
190
|
+
let {
|
|
191
|
+
outputFolder,
|
|
192
|
+
outputFilename
|
|
193
|
+
} = opts;
|
|
194
|
+
|
|
195
|
+
let {counts, startScan, URL } = data?.summary;
|
|
196
|
+
|
|
197
|
+
let totalIssues = (counts?.violation || 0) +
|
|
198
|
+
(counts?.potentialviolation || 0) +
|
|
199
|
+
(counts?.recommendation || 0) +
|
|
200
|
+
(counts?.potentialrecommendation || 0) +
|
|
201
|
+
(counts?.manual || 0);
|
|
202
|
+
let totalViolations = counts?.violation || 0;
|
|
203
|
+
let totalReviewsNeeded = counts?.potentialviolation || 0;
|
|
204
|
+
let totalRecommendations = (counts?.recommendation || 0) +
|
|
205
|
+
(counts?.potentialrecommendation || 0) +
|
|
206
|
+
(counts?.manual || 0);
|
|
207
|
+
|
|
208
|
+
// currentStatus is the total number of elements minus
|
|
209
|
+
// the number of elementsViolationReview minus the recommendations
|
|
210
|
+
// all divided by the total number of elements
|
|
211
|
+
let currentStatus = (
|
|
212
|
+
(counts?.elements || 0) -
|
|
213
|
+
( (counts?.elementsViolationReview || 0) - (counts?.recommendation || 0) )
|
|
214
|
+
) / (counts?.elements || 0);
|
|
215
|
+
|
|
216
|
+
let violationIcon = '<span class="ca-gov-icon-close-line text-danger align-bottom mx-2"></span>';
|
|
217
|
+
let reviewIcon = '<span class="ca-gov-icon-warning-triangle text-warning align-bottom mx-2"></span>';
|
|
218
|
+
let recommendationIcon = '<span class="ca-gov-icon-info text-primary align-bottom mx-2"></span>';
|
|
219
|
+
|
|
220
|
+
output.push(
|
|
221
|
+
'<div class="container">', // open container
|
|
222
|
+
'<div class="row">', // open row
|
|
223
|
+
'<div class="col-12">', // open column
|
|
224
|
+
`<h1 class="page-title my-4">${title}</h1>`,
|
|
225
|
+
'</div>', // end col-12
|
|
226
|
+
'</div>', // end row
|
|
227
|
+
'<div class="row">', // open row
|
|
228
|
+
'<div class="col-3">', // open column 3
|
|
229
|
+
`<p>${ new Date(startScan).toLocaleString() }</p>`,
|
|
230
|
+
'<strong>Scanned page:</strong>',
|
|
231
|
+
`<p class="text-break">${ URL }</p>`,
|
|
232
|
+
'</div>', // end col-3
|
|
233
|
+
'<div class="col-9">', // open column 9
|
|
234
|
+
'<div class="d-flex p-4" style="background-color: #e8daff; border: 1px solid #8a3ffc">', // open div
|
|
235
|
+
'<div class="w-25">', // open div
|
|
236
|
+
'<p class="fw-bold">Current status</p>',
|
|
237
|
+
`<strong class="fs-1">${Math.ceil(currentStatus * 100)}%</strong>`,
|
|
238
|
+
'<p>Percentage of elements with no detected violations or items to review</p>',
|
|
239
|
+
'</div>', // end div
|
|
240
|
+
'<div class="ps-4 w-75">', // open div
|
|
241
|
+
'<p>This report summarizes automated tests and is generated by <a href="https://www.ibm.com/able/toolkit/tools/#develop" target="_blank">IBM Equal Access Tools</a>. You have to perform additional manual tests to complete accessibility assessments. Use the <a href="https://ibm.com/able/toolkit" target="_blank">IBM Equal Access Toolkit</a> to guide you.</p>',
|
|
242
|
+
'<p class="mb-0">More resources:</p>',
|
|
243
|
+
'<ul class="list-group list-group-flush">',
|
|
244
|
+
'<li class="list-group-item bg-transparent p-0 border-0"><a href="https://www.ibm.com/able/toolkit/develop/overview/#unit-testing" target="_blank">Quick unit test for developers</a></li>',
|
|
245
|
+
'<li class="list-group-item bg-transparent p-0 border-0"><a href="https://www.ibm.com/able/toolkit/verify/overview" target="_blank">Full accessibility test process</a></li>',
|
|
246
|
+
'</ul>',
|
|
247
|
+
'</div>', // end div
|
|
248
|
+
'</div>', // end div
|
|
249
|
+
'<div class="d-flex my-4">', // open div
|
|
250
|
+
'<div class="flex-grow-1 cursor-pointer border border-2 p-2 me-2">', // open div
|
|
251
|
+
`<strong>Violations${violationIcon}</strong>`,
|
|
252
|
+
`<strong class="fs-1 d-block">${totalViolations}</strong>`,
|
|
253
|
+
'<span>Accessibility failures that need to be corrected</span>',
|
|
254
|
+
'</div>', // end div
|
|
255
|
+
'<div class="flex-grow-1 cursor-pointer border border-2 p-2 me-2">', // open div
|
|
256
|
+
`<strong>Needs review${reviewIcon}</strong>`,
|
|
257
|
+
`<strong class="fs-1 d-block">${totalReviewsNeeded}</strong>`,
|
|
258
|
+
'<span>Issues that may not be a violation; manual review is needed</span>',
|
|
259
|
+
'</div>', // end div
|
|
260
|
+
'<div class="flex-grow-1 cursor-pointer border border-2 p-2 me-2">', // open div
|
|
261
|
+
`<strong>Recommendations${recommendationIcon}</strong>`,
|
|
262
|
+
`<strong class="fs-1 d-block">${totalRecommendations}</strong>`,
|
|
263
|
+
'<span>Opportunities to apply best practices to further improve accessibility</span>',
|
|
264
|
+
'</div>', // end div
|
|
265
|
+
'</div>', // end div
|
|
266
|
+
'<div class="d-flex">', // open div
|
|
267
|
+
// '<select>', // open select
|
|
268
|
+
// '<option value="review"><span class="ca-gov-icon-warning-triangle text-warning align-bottom me-2"></span>Needs review</option>', // option
|
|
269
|
+
// '<option value="recommendations"><span class="ca-gov-icon-info text-primary align-bottom me-2"></span>Recommendations</option>', // option
|
|
270
|
+
// '<option value="rules"><span class="ca-gov-icon-close-line text-danger align-bottom me-2"></span>Violations</option>', // option
|
|
271
|
+
// '</select>', // end select
|
|
272
|
+
`<p class="ms-auto me-2">${violationIcon}${totalViolations}</p>`,
|
|
273
|
+
`<p class="mx-2">${reviewIcon}${totalReviewsNeeded}</p>`,
|
|
274
|
+
`<p class="mx-2">${recommendationIcon}${totalRecommendations}</p>`,
|
|
275
|
+
`<p></p>`,
|
|
276
|
+
`<p class="ms-5">${totalIssues} issues found</p>`,
|
|
277
|
+
'</div>', // end div
|
|
278
|
+
'</div>', // end col-9
|
|
279
|
+
'</div>', // end row
|
|
280
|
+
'<div class="row">', // open row
|
|
281
|
+
'<div class="col-12">', // open column
|
|
282
|
+
'<table class="table">', // open table
|
|
283
|
+
'<thead>', // open thead
|
|
284
|
+
'<th>Issues</th>', // th
|
|
285
|
+
'<th>Element roles</th>', // th
|
|
286
|
+
'<th>Requirements</th>', // th
|
|
287
|
+
'<th>Rules</th>', // th
|
|
288
|
+
'</thead>', // end thead
|
|
289
|
+
'<tbody>', // open tbody
|
|
290
|
+
...data?.results?.map( result => {
|
|
291
|
+
if( 'pass' === result.level ){
|
|
292
|
+
return null; // we skip the passed results
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let icon = recommendationIcon;
|
|
296
|
+
|
|
297
|
+
if( 'violation' === result.level ){
|
|
298
|
+
icon = violationIcon;
|
|
299
|
+
}else if( 'potentialviolation' === result.level ){
|
|
300
|
+
icon = reviewIcon;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return `<tr><td>${icon}</td><td>${result.path.aria}</td><td>${result.category}</td><td>${result.ruleId}</td></tr>`
|
|
304
|
+
}).filter(Boolean),
|
|
305
|
+
'</tbody>', // end tbody
|
|
306
|
+
'</table>', // end table
|
|
307
|
+
'</div>', // end col-12
|
|
308
|
+
'</div>', // end row
|
|
309
|
+
'</div>', // end container
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
HandleBars.registerPartial('index', output.join('\n') );
|
|
313
|
+
|
|
314
|
+
let template = initHandleBars();
|
|
315
|
+
|
|
316
|
+
fs.mkdirSync( outputFolder, {recursive: true} );
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
// we generate the outputFilename
|
|
320
|
+
fs.writeFileSync(
|
|
321
|
+
path.join(outputFolder, outputFilename),
|
|
322
|
+
htmlFormat(
|
|
323
|
+
template({
|
|
324
|
+
title,
|
|
325
|
+
scheme: 'oceanside',
|
|
326
|
+
assets: [
|
|
327
|
+
fs.existsSync( path.join(outputFolder, 'a11y.update.js' ) ) ?
|
|
328
|
+
'a11y.update.js' : '' // if the hot module update file exists, add it
|
|
329
|
+
].filter(Boolean)
|
|
330
|
+
}),
|
|
331
|
+
" ".repeat(4), 250
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function landingPage(data, opts ){
|
|
337
|
+
let output = [];
|
|
338
|
+
|
|
339
|
+
let {
|
|
340
|
+
outputFolder,
|
|
341
|
+
outputFilename
|
|
342
|
+
} = opts;
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
output.push(
|
|
346
|
+
`<div class="container"><div class="row"><div class="col-12"><h1 class="page-title my-4">${title} for ${process.cwd().split('\\').pop()}</h1></div></div></div>`,
|
|
347
|
+
'<div class="container"><div class="row"><div class="col-12">',
|
|
348
|
+
'<table class="table"><thead><tr><th>Page Auditted</th><th>Audit</th></thead><tbody>',
|
|
349
|
+
...data.sort().map(file => {
|
|
350
|
+
// remove the .json extension from the file name
|
|
351
|
+
file = file.replace(/\.json$/, '');
|
|
352
|
+
|
|
353
|
+
return `<tr><td><a href="/${file}" target="_blank">/${file}</a></td><td><a href="${file}" target="_blank">${file}</a></td></tr>`
|
|
354
|
+
}),
|
|
355
|
+
'</tbody></table>',
|
|
356
|
+
'</div></div></div>'
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
HandleBars.registerPartial('index', output.join('\n') );
|
|
360
|
+
|
|
361
|
+
let template = initHandleBars();
|
|
362
|
+
|
|
363
|
+
fs.mkdirSync( outputFolder, {recursive: true} );
|
|
364
|
+
|
|
365
|
+
// we generate the outputFilename
|
|
366
|
+
fs.writeFileSync(
|
|
367
|
+
path.join(outputFolder, `${outputFilename}.html`),
|
|
368
|
+
htmlFormat(
|
|
369
|
+
template({
|
|
370
|
+
title: `${title} for ${process.cwd().split('\\').pop()}`,
|
|
371
|
+
scheme: 'oceanside',
|
|
372
|
+
assets: [
|
|
373
|
+
fs.existsSync( path.join(outputFolder, 'a11y.update.js' ) ) ?
|
|
374
|
+
'a11y.update.js' : '' // if the hot module update file exists, add it
|
|
375
|
+
].filter(Boolean)
|
|
376
|
+
}),
|
|
377
|
+
" ".repeat(4), 250
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function capitalCase(str){
|
|
384
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export {
|
|
388
|
+
reporter,
|
|
389
|
+
landingPage
|
|
390
|
+
}
|