Skip to content

Commit 6475a4a

Browse files
committed
Merge pull request #36 from rackt/add-failure-filtering
Add a means of filtering failures on a per-component basis (fixes #34
2 parents 9c0f970 + 4ca5aae commit 6475a4a

3 files changed

Lines changed: 223 additions & 14 deletions

File tree

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,32 @@ yet, alias the module to nothing with webpack in production.
3434
If you want it to throw errors instead of just warnings:
3535

3636
```
37-
a11y(React, {throw: true});
37+
a11y(React, { throw: true });
38+
```
39+
40+
You can filter failures by passing a function to the `filterFn` option. The
41+
filter function will receive three arguments: the name of the Component
42+
instance or ReactElement, the id of the element, and the failure message.
43+
Note: If a ReactElement, the name will be the node type followed by the id
44+
(e.g. div#foo).
45+
46+
```
47+
var commentListFailures = (name, id, msg) => {
48+
return name === "CommentList";
49+
};
50+
51+
a11y(React, { filterFn: commentListFailures });
52+
```
53+
54+
If you want to log DOM element references for easy lookups in the DOM inspector,
55+
use the `includeSrcNode` option.
56+
57+
```
58+
a11y(React, { throw: true, includeSrcNode: true });
59+
```
60+
61+
All failures are also accessible via the `getFailures()` method.
62+
63+
```
64+
a11y.getFailures();
3865
```

lib/__tests__/index-test.js

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
var React = require('react');
22
var assert = require('assert');
3-
require('../index')(React);
3+
var a11y = require('../index');
44
var assertions = require('../assertions');
55

66
var k = () => {};
@@ -25,6 +25,15 @@ var doNotExpectWarning = (notExpected, fn) => {
2525
};
2626

2727
describe('props', () => {
28+
var createElement = React.createElement;
29+
30+
before(() => {
31+
a11y(React);
32+
});
33+
34+
after(() => {
35+
React.createElement = createElement;
36+
});
2837

2938
describe('onClick', () => {
3039

@@ -162,6 +171,16 @@ describe('props', () => {
162171
});
163172

164173
describe('tags', () => {
174+
var createElement = React.createElement;
175+
176+
before(() => {
177+
a11y(React);
178+
});
179+
180+
after(() => {
181+
React.createElement = createElement;
182+
});
183+
165184
describe('img', () => {
166185
it('requires alt attributes', () => {
167186
expectWarning(assertions.tags.img.MISSING_ALT.msg, () => {
@@ -200,3 +219,62 @@ describe('tags', () => {
200219
});
201220
});
202221
});
222+
223+
describe('filterFn', () => {
224+
var createElement = React.createElement;
225+
226+
before(() => {
227+
var barOnly = (name, id, msg) => {
228+
return id === "bar";
229+
};
230+
231+
a11y(React, { filterFn: barOnly });
232+
});
233+
234+
after(() => {
235+
React.createElement = createElement;
236+
});
237+
238+
describe('when the source element has been filtered out', () => {
239+
it('does not warn', () => {
240+
doNotExpectWarning(assertions.tags.img.MISSING_ALT.msg, () => {
241+
<img id="foo" src="foo.jpg"/>;
242+
});
243+
});
244+
});
245+
246+
describe('when there are filtered results', () => {
247+
it('warns', () => {
248+
expectWarning(assertions.tags.img.MISSING_ALT.msg, () => {
249+
<div>
250+
<img id="foo" src="foo.jpg"/>
251+
<img id="bar" src="foo.jpg"/>
252+
</div>;
253+
});
254+
});
255+
});
256+
});
257+
258+
describe('getFailures()', () => {
259+
var createElement = React.createElement;
260+
261+
before(() => {
262+
a11y(React);
263+
});
264+
265+
after(() => {
266+
React.createElement = createElement;
267+
});
268+
269+
describe('when there are failures', () => {
270+
it('returns the failures', () => {
271+
<div>
272+
<img id="foo" src="foo.jpg"/>
273+
<img id="bar" src="foo.jpg"/>
274+
</div>;
275+
276+
assert(a11y.getFailures().length == 2);
277+
});
278+
});
279+
280+
});

lib/index.js

Lines changed: 116 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,140 @@ var assertAccessibility = (tagName, props, children) => {
2020
return failures;
2121
};
2222

23-
var error = (id, msg) => {
24-
throw new Error('#' + id + ": " + msg);
23+
var filterFailures = (failureInfo, options) => {
24+
var failures = failureInfo.failures;
25+
var filterFn = options.filterFn &&
26+
options.filterFn.bind(undefined, failureInfo.name, failureInfo.id);
27+
28+
if (filterFn) {
29+
failures = failures.filter(filterFn);
30+
}
31+
32+
return failures;
2533
};
2634

27-
var warn = (id, msg) => {
28-
console.warn('#' + id, msg);
35+
var throwError = (failureInfo, options) => {
36+
var failures = filterFailures(failureInfo, options);
37+
var msg = failures.pop();
38+
var error = [failureInfo.name, msg];
39+
40+
if (options.includeSrcNode) {
41+
error.push(failureInfo.id);
42+
}
43+
44+
throw new Error(error.join(' '));
45+
};
46+
47+
var after = (host, name, cb) => {
48+
var originalFn = host[name];
49+
50+
if (originalFn) {
51+
host[name] = () => {
52+
originalFn.call(host);
53+
cb.call(host);
54+
};
55+
} else {
56+
host[name] = cb;
57+
}
58+
};
59+
60+
var logAfterRender = (component, log) => {
61+
after(component, 'componentDidMount', log);
62+
after(component, 'componentDidUpdate', log);
63+
};
64+
65+
var logWarning = (component, failureInfo, options) => {
66+
var includeSrcNode = options.includeSrcNode;
67+
68+
var warn = () => {
69+
var failures = filterFailures(failureInfo, options);
70+
71+
failures.forEach((failure) => {
72+
var msg = failure;
73+
var warning = [failureInfo.name, msg];
74+
75+
if (includeSrcNode) {
76+
warning.push(document.getElementById(failureInfo.id));
77+
}
78+
79+
console.warn.apply(console, warning);
80+
});
81+
82+
totalFailures.push(failureInfo);
83+
};
84+
85+
if (component && includeSrcNode) {
86+
// Cannot log a node reference until the component is in the DOM,
87+
// so defer the document.getElementById call until componentDidMount
88+
// or componentDidUpdate.
89+
logAfterRender(component._instance, warn);
90+
} else {
91+
warn();
92+
}
2993
};
3094

3195
var nextId = 0;
32-
module.exports = (React, options) => {
96+
var totalFailures;
97+
98+
var reactA11y = (React, options) => {
3399
if (!React && !React.createElement) {
34100
throw new Error('Missing parameter: React');
35101
}
36102
assertions.setReact(React);
37103

104+
totalFailures = [];
38105
var _createElement = React.createElement;
39-
var log = options && options.throw ? error : warn;
40-
React.createElement = function (type, _props, ...children) {
106+
var includeSrcNode = options && !!options.includeSrcNode;
107+
108+
React.createElement = (type, _props, ...children) => {
41109
var props = _props || {};
110+
var reactEl;
111+
42112
if (typeof type === 'string') {
43-
var failures = assertAccessibility(type, props, children);
113+
let failures = assertAccessibility(type, props, children);
44114
if (failures.length) {
45115
// Generate an id if one doesn't exist
46116
props.id = (props.id || 'a11y-' + nextId++);
117+
reactEl = _createElement.apply(this, [type, props].concat(children));
118+
119+
let reactComponent = reactEl._owner;
47120

48-
for (var i = 0; i < failures.length; i++)
49-
log(props.id, failures[i]);
121+
// If a Component instance, use the component's name,
122+
// if a ReactElement instance, use the node DOM + id (e.g. div#foo)
123+
let name = reactComponent && reactComponent.getName() ||
124+
reactEl.type + '#' + props.id;
125+
126+
let failureInfo = {
127+
'name': name ,
128+
'id': props.id,
129+
'failures': failures
130+
};
131+
132+
let notifyOpts = {
133+
'includeSrcNode': includeSrcNode,
134+
'filterFn': options && options.filterFn
135+
};
136+
137+
if (options && options.throw) {
138+
throwError(failureInfo, notifyOpts);
139+
} else {
140+
logWarning(reactComponent, failureInfo, notifyOpts);
141+
}
142+
143+
} else {
144+
reactEl = _createElement.apply(this, [type, props].concat(children));
50145
}
146+
} else {
147+
reactEl = _createElement.apply(this, [type, props].concat(children));
51148
}
52-
// make sure props with the id is passed down, even if no props were passed in.
53-
return _createElement.apply(this, [type, props].concat(children));
149+
150+
return reactEl;
54151
};
152+
153+
reactA11y.getFailures = () => {
154+
return totalFailures;
155+
};
156+
55157
};
158+
159+
module.exports = reactA11y;

0 commit comments

Comments
 (0)