Skip to content

Commit

Permalink
Merge pull request #36 from rackt/add-failure-filtering
Browse files Browse the repository at this point in the history
Add a means of filtering failures on a per-component basis (fixes #34
  • Loading branch information
angus-c committed May 20, 2015
2 parents 9c0f970 + 4ca5aae commit 6475a4a
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 14 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,32 @@ yet, alias the module to nothing with webpack in production.
If you want it to throw errors instead of just warnings:

```
a11y(React, {throw: true});
a11y(React, { throw: true });
```

You can filter failures by passing a function to the `filterFn` option. The
filter function will receive three arguments: the name of the Component
instance or ReactElement, the id of the element, and the failure message.
Note: If a ReactElement, the name will be the node type followed by the id
(e.g. div#foo).

```
var commentListFailures = (name, id, msg) => {
return name === "CommentList";
};
a11y(React, { filterFn: commentListFailures });
```

If you want to log DOM element references for easy lookups in the DOM inspector,
use the `includeSrcNode` option.

```
a11y(React, { throw: true, includeSrcNode: true });
```

All failures are also accessible via the `getFailures()` method.

```
a11y.getFailures();
```
80 changes: 79 additions & 1 deletion lib/__tests__/index-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
var React = require('react');
var assert = require('assert');
require('../index')(React);
var a11y = require('../index');
var assertions = require('../assertions');

var k = () => {};
Expand All @@ -25,6 +25,15 @@ var doNotExpectWarning = (notExpected, fn) => {
};

describe('props', () => {
var createElement = React.createElement;

before(() => {
a11y(React);
});

after(() => {
React.createElement = createElement;
});

describe('onClick', () => {

Expand Down Expand Up @@ -162,6 +171,16 @@ describe('props', () => {
});

describe('tags', () => {
var createElement = React.createElement;

before(() => {
a11y(React);
});

after(() => {
React.createElement = createElement;
});

describe('img', () => {
it('requires alt attributes', () => {
expectWarning(assertions.tags.img.MISSING_ALT.msg, () => {
Expand Down Expand Up @@ -200,3 +219,62 @@ describe('tags', () => {
});
});
});

describe('filterFn', () => {
var createElement = React.createElement;

before(() => {
var barOnly = (name, id, msg) => {
return id === "bar";
};

a11y(React, { filterFn: barOnly });
});

after(() => {
React.createElement = createElement;
});

describe('when the source element has been filtered out', () => {
it('does not warn', () => {
doNotExpectWarning(assertions.tags.img.MISSING_ALT.msg, () => {
<img id="foo" src="foo.jpg"/>;
});
});
});

describe('when there are filtered results', () => {
it('warns', () => {
expectWarning(assertions.tags.img.MISSING_ALT.msg, () => {
<div>
<img id="foo" src="foo.jpg"/>
<img id="bar" src="foo.jpg"/>
</div>;
});
});
});
});

describe('getFailures()', () => {
var createElement = React.createElement;

before(() => {
a11y(React);
});

after(() => {
React.createElement = createElement;
});

describe('when there are failures', () => {
it('returns the failures', () => {
<div>
<img id="foo" src="foo.jpg"/>
<img id="bar" src="foo.jpg"/>
</div>;

assert(a11y.getFailures().length == 2);
});
});

});
128 changes: 116 additions & 12 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,140 @@ var assertAccessibility = (tagName, props, children) => {
return failures;
};

var error = (id, msg) => {
throw new Error('#' + id + ": " + msg);
var filterFailures = (failureInfo, options) => {
var failures = failureInfo.failures;
var filterFn = options.filterFn &&
options.filterFn.bind(undefined, failureInfo.name, failureInfo.id);

if (filterFn) {
failures = failures.filter(filterFn);
}

return failures;
};

var warn = (id, msg) => {
console.warn('#' + id, msg);
var throwError = (failureInfo, options) => {
var failures = filterFailures(failureInfo, options);
var msg = failures.pop();
var error = [failureInfo.name, msg];

if (options.includeSrcNode) {
error.push(failureInfo.id);
}

throw new Error(error.join(' '));
};

var after = (host, name, cb) => {
var originalFn = host[name];

if (originalFn) {
host[name] = () => {
originalFn.call(host);
cb.call(host);
};
} else {
host[name] = cb;
}
};

var logAfterRender = (component, log) => {
after(component, 'componentDidMount', log);
after(component, 'componentDidUpdate', log);
};

var logWarning = (component, failureInfo, options) => {
var includeSrcNode = options.includeSrcNode;

var warn = () => {
var failures = filterFailures(failureInfo, options);

failures.forEach((failure) => {
var msg = failure;
var warning = [failureInfo.name, msg];

if (includeSrcNode) {
warning.push(document.getElementById(failureInfo.id));
}

console.warn.apply(console, warning);
});

totalFailures.push(failureInfo);
};

if (component && includeSrcNode) {
// Cannot log a node reference until the component is in the DOM,
// so defer the document.getElementById call until componentDidMount
// or componentDidUpdate.
logAfterRender(component._instance, warn);
} else {
warn();
}
};

var nextId = 0;
module.exports = (React, options) => {
var totalFailures;

var reactA11y = (React, options) => {
if (!React && !React.createElement) {
throw new Error('Missing parameter: React');
}
assertions.setReact(React);

totalFailures = [];
var _createElement = React.createElement;
var log = options && options.throw ? error : warn;
React.createElement = function (type, _props, ...children) {
var includeSrcNode = options && !!options.includeSrcNode;

React.createElement = (type, _props, ...children) => {
var props = _props || {};
var reactEl;

if (typeof type === 'string') {
var failures = assertAccessibility(type, props, children);
let failures = assertAccessibility(type, props, children);
if (failures.length) {
// Generate an id if one doesn't exist
props.id = (props.id || 'a11y-' + nextId++);
reactEl = _createElement.apply(this, [type, props].concat(children));

let reactComponent = reactEl._owner;

for (var i = 0; i < failures.length; i++)
log(props.id, failures[i]);
// If a Component instance, use the component's name,
// if a ReactElement instance, use the node DOM + id (e.g. div#foo)
let name = reactComponent && reactComponent.getName() ||
reactEl.type + '#' + props.id;

let failureInfo = {
'name': name ,
'id': props.id,
'failures': failures
};

let notifyOpts = {
'includeSrcNode': includeSrcNode,
'filterFn': options && options.filterFn
};

if (options && options.throw) {
throwError(failureInfo, notifyOpts);
} else {
logWarning(reactComponent, failureInfo, notifyOpts);
}

} else {
reactEl = _createElement.apply(this, [type, props].concat(children));
}
} else {
reactEl = _createElement.apply(this, [type, props].concat(children));
}
// make sure props with the id is passed down, even if no props were passed in.
return _createElement.apply(this, [type, props].concat(children));

return reactEl;
};

reactA11y.getFailures = () => {
return totalFailures;
};

};

module.exports = reactA11y;

0 comments on commit 6475a4a

Please sign in to comment.