/
axe.ts
149 lines (119 loc) · 4.96 KB
/
axe.ts
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
/**
* @fileoverview Runs axe-core (https://www.npmjs.com/package/axe-core)
* in the context of the page and checks if there are any issues with a11y.
*/
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
import { AxeResults, Result as AxeResult, NodeResult as AxeNodeResult } from 'axe-core'; // eslint-disable-line no-unused-vars
import { readFileAsync } from '../../utils/misc';
import { debug as d } from '../../utils/debug';
import { IAsyncHTMLElement, IRule, IRuleBuilder, Severity, ITraverseEnd } from '../../types'; // eslint-disable-line no-unused-vars
import { RuleContext } from '../../rule-context'; // eslint-disable-line no-unused-vars
const debug = d(__filename);
// ------------------------------------------------------------------------------
// Public
// ------------------------------------------------------------------------------
const rule: IRuleBuilder = {
create(context: RuleContext): IRule {
let axeConfig: object = {};
const loadRuleConfig = () => {
if (!context.ruleOptions) {
return;
}
axeConfig = context.ruleOptions;
};
const generateScript = (): string => {
// This is run in the page, not Sonar itself.
// axe.run returns a promise which fulfills with a results object
// containing any violations.
const script: string =
`function runA11yChecks() {
return window['axe'].run(document, ${JSON.stringify(axeConfig, null, 2)});
}`;
return script;
};
const getElement = async (node: AxeNodeResult): Promise<IAsyncHTMLElement> => {
const selector: string = node.target[0];
const elements: Array<IAsyncHTMLElement> = await context.querySelectorAll(selector);
return elements[0];
};
const validate = async (traverseEnd: ITraverseEnd) => {
const { resource } = traverseEnd;
const axeCore: string = await readFileAsync(require.resolve('axe-core'));
const script: string = `(function () {
${axeCore};
return (${generateScript()}());
}())`;
let result: AxeResults = null;
/* istanbul ignore next */
try {
result = await context.evaluate(script);
} catch (e) {
await context.report(resource, null, `Error executing script: "${e.message}". Please try with another connector`, null, null, Severity.warning);
debug('Error executing script %O', e);
return;
}
/* istanbul ignore next */
if (!result || !Array.isArray(result.violations)) {
debug(`Unable to parse axe results ${result}`);
return;
}
if (result.violations.length === 0) {
debug('No accessibility issues found');
return;
}
const reportPromises: Array<Promise<void>> = result.violations.reduce((promises: Array<Promise<void>>, violation: AxeResult) => {
const elementPromises = violation.nodes.map(async (node: AxeNodeResult) => {
const element = await getElement(node);
// TODO: find the right element here using node.target[0] ?
await context.report(resource, element, violation.help);
return;
});
return promises.concat(elementPromises);
}, []);
await Promise.all(reportPromises);
};
loadRuleConfig();
return { 'traverse::end': validate };
},
meta: {
docs: {
category: 'accessibility',
description: 'Runs axe-core tests in the target'
},
fixable: 'code',
recommended: true,
schema: [{
additionalProperties: false,
properties: {
rules: {
patternProperties: {
'^.+$': {
additionalProperties: false,
properties: { enabled: { type: 'boolean' } },
required: ['enabled'],
type: 'object'
}
},
type: 'object'
},
runOnly: {
additionalProperties: false,
properties: {
type: { type: 'string' },
values: {
items: { type: 'string' },
minItems: 1,
type: 'array',
uniqueItems: true
}
},
type: 'object'
}
}
}],
worksWithLocalFiles: true
}
};
module.exports = rule;