forked from hanshillen/aria-test-builder
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Gruntfile.js
412 lines (373 loc) · 15.6 KB
/
Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
var javadetect = require('grunt-html/lib/javadetect');
var jar = require('vnu-jar');
var portastic = require('portastic');
module.exports = function (grunt) {
require('load-grunt-tasks')(grunt);
var Handlebars = require('handlebars');
grunt.initConfig({
// config vars
elementsJSON: grunt.file.readJSON("src/elements-roles.json"),
ariaJSON: grunt.file.readJSON("src/roles-states.json"),
template: grunt.file.read("src/templates/role-test.handlebars"),
compactTemplate: grunt.file.read("src/templates/role-test-compact.handlebars"),
//megaTest: true,
// tasks
prompt: {
init: {
options: {
questions: [{
config: "test",
type: "list",
message: "choose a task",
default: "test",
choices: [
{ name: "Create Individual Test Cases", value: "test" },
{ name: "Validate Individual Test Cases", value: "validate" },
{ name: "Generate Mistakes Report for Individual Test Cases", value: "report" },
{ name: "All of the Above", value: "full" },
// "---",
// { name: "Create Mega Test Case", value: "mega" },
// { name: "Validate Mega Test Case", value: "validate:mega" },
// { name: "Generate Mistakes Report for Mega Test Case", value: "report:mega" },
// { name: "All of the Above (Mega)", value: "fullmega" }
]
}],
then: function (results, done) {
grunt.task.run(results.test);
done();
}
}
}
},
clean: {
dist: ['dist/*'],
testcases: ['dist/testcases/'],
validation: ['dist/validation/'],
report: ['dist/validator-mistakes.html'],
mega: ["dist/testcases/mega-test.min.html", "dist/testcases/mega-test.html"]
},
vnuserver: {},
htmlmin: {
mega: {
options: {
removeComments: true,
collapseWhitespace: true
},
files: {
"dist/testcases/mega-test.min.html": ["dist/testcases/mega-test.html"]
}
}
},
htmllint: {
all: {
options: {
errorLevels: ['error', 'warning'],
reporter: "json",
force: true,
ignore: /(.*is missing required attribute.*)|(.*must be contained in.*)|(.*is not (yet )?supported in all browsers.*)|(.*is missing one or more of the following attributes.*)|(.*not allowed as child of element.*)|(.*element being open.*)|(.*Stray (start|end) tag.*)|(.*empty.*)|(.*element must have a.*)|(.*must have attribute.*)|(.*is missing a required instance.*)|(.*does not need a.*)|(.*element is obsolete.*)|(.*consider.*)|(.*format.*)|(.*Duplicate ID.*)/i,
reporterOutput: "dist/validation/<%= grunt.task.current.args[0] %>.json"
},
src: `dist/testcases/<%= grunt.task.current.args[0] %>`
}
}
});
// Main Grunt Tasks
grunt.registerTask("default", "Provide Options", ['prompt:init']);
grunt.registerTask("test", "Generate test files", ['clean:testcases', 'build-tests']);
grunt.registerTask("validate", "perform validation and store results", ['clean:validation', 'vnuserver', 'multi-validate']);
grunt.registerTask("report", "check validation resuls for mistake and create report", ['clean:report', 'create-report']);
grunt.registerTask("full", "Generate test files, validate them, create report", ['test', 'validate', 'report']);
//grunt.registerTask("mega", "Generate one big test file", ['clean:mega', 'build-test', 'htmlmin:mega']);
//grunt.registerTask("full-mega", "Generate one big test file, validate it, create report", ['mega', 'validate', 'report:megaTest']);
grunt.registerTask("build-test", "Create a single test case based on template", function (elementId) {
grunt.config.requires('ariaJSON', 'elementsJSON');
var elementsJSON = grunt.config('elementsJSON');
var ariaJSON = grunt.config('ariaJSON');
var megaTest = !elementId;
var template = grunt.config(megaTest ? 'compactTemplate' : 'template');
var elementsToTest = {};
var title = "";
if (megaTest) { // one test for all elements
elementsToTest = elementsJSON;
title = "all elements";
grunt.log.write(`Building mega test case...`);
} else { // one test for one element
grunt.log.write(`Building ${elementId} test case...`);
elementsToTest[elementId] = elementsJSON[elementId];
title = elementsJSON[elementId].name;
}
var context = {
elementsToTest: elementsToTest,
title: title,
ariaJSON: ariaJSON
};
compileTemplate(elementId, template, context);
grunt.log.ok();
});
grunt.registerTask("build-tests", "loop over elements and create test file for each", function () {
grunt.config.requires('elementsJSON');
var elementsJSON = grunt.config('elementsJSON');
for (let elementId in elementsJSON) {
grunt.task.run(`build-test:${elementId}`);
}
});
grunt.registerTask("multi-validate", "validate all test cases and store the results", function () {
var tasks = [];
grunt.log.write('validating all test cases...');
// Loop over all testcase files, queue up htmllint tasks for each and run them
grunt.file.recurse("dist/testcases", function (abspath, rootdir, subdir, filename) {
tasks.push(`htmllint:all:${filename}`);
});
grunt.task.run(tasks);
});
grunt.registerTask("create-report", "check validation results for mistakes and create reports", function () {
grunt.config.requires('elementsJSON', 'ariaJSON');
try {
grunt.log.write("Creating dist/validator-mistakes.html...");
var reportTemplate = grunt.file.read("src/templates/validator-mistakes.handlebars");
var reportTemplateCompiled = Handlebars.compile(reportTemplate);
var elementsJSON = grunt.config('elementsJSON');
var ariaJSON = grunt.config('ariaJSON');
if (grunt.config('megaTest')) {
validatorMistakes = testMegaValidationResults(elementsJSON, ariaJSON);
} else {
validatorMistakes = testValidationResults(elementsJSON, ariaJSON);
}
var output = reportTemplateCompiled({ validatorMistakes: validatorMistakes });
grunt.file.write(`dist/validator-mistakes.html`, output);
grunt.log.ok();
} catch (err) {
grunt.log.error(err);
}
});
// Task copied from https://www.npmjs.com/package/grunt-vnuserver
// to avoid outdated vnu-jar dependency
grunt.registerTask('vnuserver', 'Start the Nu Html Checker server.', function () {
let opt = this.options({ port: 8888, skippable: false, persist: false });
let done = this.async();
portastic.test(opt.port, function (open) {
if (!open) {
if (opt.skippable) {
grunt.log.debug('Port ' + opt.port + ' in use. Skipping server startup.');
done();
} else {
done(Error('Port ' + opt.port + ' in use. To ignore, set skippable: false.'));
}
return;
}
let child;
let cleanup = function () {
let killing = grunt.log.write('Killing vnuserver...');
child.kill('SIGKILL');
killing.ok();
};
if (!opt.persist) {
process.on('exit', cleanup);
let exit = grunt.util.exit;
grunt.util.exit = function () { // This seems to be the only reliable on-exit hook.
cleanup();
return exit.apply(grunt.util, arguments);
};
}
javadetect(function (err, java) {
if (err) {
throw err;
}
if (java.version[0] !== '1' || (java.version[0] === '1' && java.version[2] < '8')) {
throw new Error('\nUnsupported Java version used: ' + java.version + '. v1.8 is required!');
}
let args = [(java.arch === 'ia32' ? '-Xss512k' : ''), '-cp', jar, 'nu.validator.servlet.Main', opt.port].filter(x => x);
let vnustartup = grunt.log.write('Starting vnuserver...');
child = grunt.util.spawn({ cmd: 'java', args: args }, function (error, stdout, stderr) {
if (error && (error.code !== 1 || error.killed || error.signal)) {
done(false);
}
});
var timer = setTimeout(function () {
vnustartup.ok();
done();
}, 5000); //TODO HH: why doesn't child.stderr.on('data') fire here ? worked fine in vnuserver plugin
child.stderr.on('data', function (chunk) {
clearTimeout(timer);
if (chunk.toString().indexOf('INFO:oejs.Server:main: Started') >= 0) {
vnustartup.ok();
done();
}
if (chunk.toString().indexOf('java.net.BindException: Address already in use') >= 0) {
vnustartup.error();
done(Error('Port ' + opt.port + ' in use. Shutting down.'));
cleanup();
}
});
});
});
});
// Regular functions
function testMegaValidationResults() {
// TODO: there is no way to distinguish validator results
// for mutliple test cases involving the same node name,
// if these results are in the same output doc
}
function testValidationResults(elementsJSON, ariaJSON) {
var validatorMistakes = {};
var validationResult, allowedRoles;
var RE1, RE2, nodeName, isAllowed, isAllowedByValidator, isNativeAllowedByValidator, isNativeRole, errorMsg, nativeRoleMistake, roleMistake;
for (let elementId in elementsJSON) {
nodeName = elementsJSON[elementId].nodeName;
grunt.log.write(`Checking ${elementId} results...`);
try {
validationResult = grunt.file.read(`dist/validation/${elementId}-test.html.json`);
} catch (err) {
grunt.log.error(err);
}
allowedRoles = elementsJSON[elementId].allowedRoles;
nativeRole = elementsJSON[elementId].nativeRole;
for (let role in ariaJSON.roles) {
if (ariaJSON.abstract.includes(role)) {
//No need to test abstract roles
continue;
}
isNativeRole = role === nativeRole;
RE1 = RegExp(`(Bad value “${role}” for attribute “role” on element “${nodeName}”)|(Attribute “role” not allowed)`);
RE2 = RegExp(`The “${role}” role is unnecessary for element “${nodeName}”`);
isAllowed = allowedRoles == "all" || allowedRoles.includes(role) || isNativeRole;
isAllowedByValidator = !RE1.test(validationResult);
isNativeAllowedByValidator = RE2.test(validationResult);
roleMistake = isAllowed !== isAllowedByValidator;
nativeRoleMistake = isNativeRole !== isNativeAllowedByValidator;
if (nativeRoleMistake)
grunt.log.ok(`Native mistake for ${role} role - isNativeRole: ${isNativeRole}, isNativeAllowedByValidator: ${isNativeAllowedByValidator}`);
if (!nativeRoleMistake && !roleMistake) {
continue;
}
// validator made a mistake
//Create context for report template
if (!validatorMistakes[elementId]) {
validatorMistakes[elementId] = {
name: elementsJSON[elementId].name,
mistakes: []
};
}
if (nativeRoleMistake) {
errorMsg = `
<code>role='${role}'</code>
is
<strong>incorrectly</strong>
${isNativeAllowedByValidator ? '<strong class="valid">allowed as native role</strong>' : '<strong class="invalid">not indicated as native role</strong>'}
for
<code>${nodeName}</code> element.`;
let mistake = {
role: role,
nodeName: elementsJSON[elementId].nodeName,
falseNegative: isAllowed,
errorMsg
};
validatorMistakes[elementId].mistakes.push(mistake);
} else if (roleMistake) {
errorMsg = `
<code>role='${role}'</code>
<strong>incorrectly</strong>
flagged as
${isAllowedByValidator ? '<strong class="valid">valid</strong>' : '<strong class="invalid">invalid</strong>'}
for
<code>${nodeName}</code> element.`;
let mistake = {
role: role,
nodeName: elementsJSON[elementId].nodeName,
falseNegative: isAllowed,
errorMsg
};
validatorMistakes[elementId].mistakes.push(mistake);
}
}
grunt.log.ok();
}
return validatorMistakes;
}
// Apply template for test case and store the result as a html file
function compileTemplate(elementId, template, context) {
if (!elementId) {
elementId = "mega";
}
var compiled = Handlebars.compile(template);
var elementsJSON = grunt.config('elementsJSON');
var outputHTML = "";
outputHTML += compiled(context);
grunt.file.write(`dist/testcases/${elementId}-test.html`, outputHTML);
}
function isRoleAllowed(allowedRoles, role) {
if (typeof allowedRoles === "string" && allowedRoles === "all") {
return true;
}
return allowedRoles.includes(role);
}
//Insert role attribute to base markup, optionally with other test attributes
function insertRoleToMarkup(elementId, role, addRoleOnly, index) {
grunt.config.requires('elementsJSON');
var markup = grunt.config('elementsJSON')[elementId].markup;
if (!markup) {
return "";
}
var attributeString = ` role='${role}'`;
if (!addRoleOnly) {
attributeString += ` class='role-test ${role}-test ${elementId}-test' id='${elementId}-${role}-${index}-test' aria-label='acc name' tabindex='0' `;
}
// complex base markup can contain {attributeString} to indicate where test attributes should go
if (markup.includes('{attributeString}')) {
markup = markup.replace('{attributeString}', `${attributeString}`);
} else if (markup.includes('/>')) { // self closing element
markup = markup.replace('/>', `${attributeString}/>`);
} else { //regular element
markup = markup.replace('>', `${attributeString}>`);
}
return new Handlebars.SafeString(markup);
}
function addZeroBefore(n) {
return (n < 10 ? '0' : '') + n;
}
// Template Helpers
Handlebars.registerHelper("getDate", function (options) {
let date = new Date(),
month = date.toLocaleString("en-us", { month: "long" });
return `${addZeroBefore(date.getDate())} ${month} ${date.getFullYear()} ${addZeroBefore(date.getHours())}:${addZeroBefore(date.getMinutes())}:${addZeroBefore(date.getSeconds())}`;
});
//List of roles in a category
Handlebars.registerHelper("testlist", function (categoryId, ariaJSON, allowedRoles, elementId, options) {
var roleList = ariaJSON[categoryId];
var out = ``;
for (let i = 0; i < roleList.length; i++) {
let role = roleList[i];
var roleAllowed = isRoleAllowed(allowedRoles, role);
let context = {
"role": role,
"elementId": elementId,
"roleAllowed": roleAllowed,
"roleIndex": i
};
out += options.fn(context);
}
return out;
});
// list allowed roles
Handlebars.registerHelper("allowedRolesSection", function (elementId, allowedRoles, options) {
var isRoleListNeeded = allowedRoles instanceof Array && allowedRoles.length > 0;
var roleText = "";
if (!isRoleListNeeded) {
roleText = typeof allowedRoles === "string" ?
`<strong><a href="https://w3c.github.io/html-aria/#dfn-any-role">any</a></strong>` :
`<strong>none</strong>`;
roleText = new Handlebars.SafeString(roleText);
}
var context = {
elementId: elementId,
isRoleListNeeded: isRoleListNeeded,
roleLink: roleText,
allowedRoles: allowedRoles
};
return options.fn(context);
});
Handlebars.registerHelper("testElement", function (elementId, role, addRoleOnly, index) {
var out = insertRoleToMarkup(elementId, role, addRoleOnly, index);
return out;
});
};