Skip to content

Commit 9916990

Browse files
committed
new autocomplete-valid rule
1 parent 82f598e commit 9916990

File tree

8 files changed

+334
-0
lines changed

8 files changed

+334
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:
103103
- [aria-proptypes](docs/rules/aria-proptypes.md): Enforce ARIA state and property values are valid.
104104
- [aria-role](docs/rules/aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.
105105
- [aria-unsupported-elements](docs/rules/aria-unsupported-elements.md): Enforce that elements that do not support ARIA roles, states, and properties do not have those attributes.
106+
- [autocomplete-valid](docs/rules/autocomplete-valid.md): Enforce that autocomplete attributes are used correctly.
106107
- [click-events-have-key-events](docs/rules/click-events-have-key-events.md): Enforce a clickable non-interactive element has at least one keyboard event listener.
107108
- [heading-has-content](docs/rules/heading-has-content.md): Enforce heading (`h1`, `h2`, etc) elements contain accessible content.
108109
- [html-has-lang](docs/rules/html-has-lang.md): Enforce `<html>` element has `lang` prop.
@@ -141,6 +142,7 @@ Rule | Recommended | Strict
141142
[aria-proptypes](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-proptypes.md) | error | error
142143
[aria-role](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-role.md) | error | error
143144
[aria-unsupported-elements](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-unsupported-elements.md) | error | error
145+
[autocomplete-valid](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/autocomplete-valid.md) | error | error
144146
[click-events-have-key-events](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/click-events-have-key-events.md) | error | error
145147
[heading-has-content](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/heading-has-content.md) | error | error
146148
[html-has-lang](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/html-has-lang.md) | error | error

__tests__/__util__/axeMapping.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* eslint-disable import/prefer-default-export, no-underscore-dangle */
2+
import * as axe from 'axe-core';
3+
4+
export function axeFailMessage(checkId, data) {
5+
return axe._audit.data.checks[checkId].messages.fail(data);
6+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* eslint-env jest */
2+
/**
3+
* @fileoverview Ensure autocomplete attribute is correct.
4+
* @author Wilco Fiers
5+
*/
6+
7+
// -----------------------------------------------------------------------------
8+
// Requirements
9+
// -----------------------------------------------------------------------------
10+
11+
import { RuleTester } from 'eslint';
12+
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
13+
import { axeFailMessage } from '../../__util__/axeMapping';
14+
import rule from '../../../src/rules/autocomplete-valid';
15+
16+
// -----------------------------------------------------------------------------
17+
// Tests
18+
// -----------------------------------------------------------------------------
19+
20+
const ruleTester = new RuleTester();
21+
22+
const invalidAutocomplete = [{
23+
message: axeFailMessage('autocomplete-valid'),
24+
type: 'JSXOpeningElement',
25+
}];
26+
27+
const inappropriateAutocomplete = [{
28+
message: axeFailMessage('autocomplete-appropriate'),
29+
type: 'JSXOpeningElement',
30+
}];
31+
32+
const ignoreNonDOMSchema = [{
33+
ignoreNonDOM: true,
34+
}];
35+
36+
ruleTester.run('autocomplete-valid', rule, {
37+
valid: [
38+
// INAPPLICABLE
39+
{ code: '<input type="text" />;' },
40+
// // PASSED AUTOCOMPLETE
41+
{ code: '<input type="text" autocomplete="name" />;' },
42+
{ code: '<input type="text" autocomplete="" />;' },
43+
{ code: '<input type="text" autocomplete="off" />;' },
44+
{ code: '<input type="text" autocomplete="on" />;' },
45+
{ code: '<input type="text" autocomplete="billing family-name" />;' },
46+
{ code: '<input type="text" autocomplete="section-blue shipping street-address" />;' },
47+
{ code: '<input type="text" autocomplete="section-somewhere shipping work email" />;' },
48+
{ code: '<input type="text" autocomplete />;' },
49+
{ code: '<input type="text" autocomplete={autocompl} />;' },
50+
{ code: '<input type="text" autocomplete={autocompl || "name"} />;' },
51+
{ code: '<input type="text" autocomplete={autocompl || "foo"} />;' },
52+
{ code: '<Foo autocomplete="bar"></Foo>;', options: ignoreNonDOMSchema },
53+
].map(parserOptionsMapper),
54+
invalid: [
55+
// FAILED "autocomplete-valid"
56+
{ code: '<input type="text" autocomplete="foo" />;', errors: invalidAutocomplete },
57+
{ code: '<input type="text" autocomplete="name invalid" />;', errors: invalidAutocomplete },
58+
{ code: '<input type="text" autocomplete="invalid name" />;', errors: invalidAutocomplete },
59+
{ code: '<input type="text" autocomplete="home url" />;', errors: invalidAutocomplete },
60+
{ code: '<Bar autocomplete="baz"></Bar>;', errors: invalidAutocomplete },
61+
62+
// FAILED "autocomplete-appropriate"
63+
{ code: '<input type="date" autocomplete="email" />;', errors: inappropriateAutocomplete },
64+
{ code: '<input type="number" autocomplete="url" />;', errors: inappropriateAutocomplete },
65+
{ code: '<input type="month" autocomplete="tel" />;', errors: inappropriateAutocomplete },
66+
{ code: '<Foo type="month" autocomplete="tel"></Foo>;', errors: inappropriateAutocomplete },
67+
].map(parserOptionsMapper),
68+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/* eslint-env mocha */
2+
import expect from 'expect';
3+
import { AbstractVirtualNode } from 'axe-core';
4+
import JSXVirtualNode from '../../../src/util/JSXVirtualNode';
5+
import JSXElementMock from '../../../__mocks__/JSXElementMock';
6+
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
7+
import IdentifierMock from '../../../__mocks__/IdentifierMock';
8+
9+
describe('JSXVirtualNode', () => {
10+
it('extends AbstractVirtualNode', () => {
11+
expect(new JSXVirtualNode()).toBeInstanceOf(AbstractVirtualNode);
12+
});
13+
14+
it('sets default props when no props are provided', () => {
15+
const { props } = new JSXVirtualNode();
16+
expect(props).toHaveProperty('nodeType', 1);
17+
expect(props).toHaveProperty('nodeName', 'div');
18+
});
19+
20+
it('sets props not part of the default', () => {
21+
const { props } = new JSXVirtualNode({
22+
props: {
23+
nodeName: 'span',
24+
foo: 'bar',
25+
},
26+
});
27+
expect(props).toHaveProperty('nodeType', 1); // default
28+
expect(props).toHaveProperty('nodeName', 'span');
29+
expect(props).toHaveProperty('foo', 'bar');
30+
});
31+
32+
describe('.attr()', () => {
33+
it('returns null when the attribute does not exist', () => {
34+
const vNode = new JSXVirtualNode();
35+
expect(vNode.attr('id')).toBeNull();
36+
});
37+
38+
it('returns the attribute value when set', () => {
39+
const idAttr = JSXAttributeMock('id', 'foo');
40+
const vNode = new JSXVirtualNode({
41+
attrs: [idAttr],
42+
});
43+
expect(vNode.attr('id')).toBe('foo');
44+
});
45+
46+
it('returns null when the attribute value is not a literal', () => {
47+
const identifierExpression = IdentifierMock('theIdentifier');
48+
const idAttr = JSXAttributeMock('id', identifierExpression);
49+
const vNode = new JSXVirtualNode({
50+
attrs: [idAttr],
51+
});
52+
expect(vNode.attr('id')).toBeNull();
53+
});
54+
});
55+
56+
describe('.hasAttr()', () => {
57+
it('returns false when the attribute does not exist', () => {
58+
const vNode = new JSXVirtualNode();
59+
expect(vNode.hasAttr('id')).toBe(false);
60+
});
61+
62+
it('returns true attribute value when set', () => {
63+
const idAttr = JSXAttributeMock('id', 'foo');
64+
const vNode = new JSXVirtualNode({
65+
attrs: [idAttr],
66+
});
67+
expect(vNode.hasAttr('id')).toBe(true);
68+
});
69+
70+
it('returns true when the attribute has no value', () => {
71+
const idAttr = JSXAttributeMock('id');
72+
const vNode = new JSXVirtualNode({
73+
attrs: [idAttr],
74+
});
75+
expect(vNode.hasAttr('id')).toBe(true);
76+
});
77+
78+
it('returns true when the attribute value is not a literal', () => {
79+
const identifierExpression = IdentifierMock('theIdentifier');
80+
const idAttr = JSXAttributeMock('id', identifierExpression);
81+
const vNode = new JSXVirtualNode({
82+
attrs: [idAttr],
83+
});
84+
expect(vNode.hasAttr('id')).toBe(true);
85+
});
86+
});
87+
88+
describe('::fromJSXOpeningElement()', () => {
89+
it('creates a virtual node', () => {
90+
const { openingElement } = JSXElementMock('div');
91+
const vNode = JSXVirtualNode.fromJSXOpeningElement(openingElement);
92+
expect(vNode).toBeInstanceOf(AbstractVirtualNode);
93+
});
94+
95+
it('sets the correct props', () => {
96+
const { openingElement } = JSXElementMock('span');
97+
const { props } = JSXVirtualNode.fromJSXOpeningElement(openingElement);
98+
expect(props).toHaveProperty('nodeType', 1);
99+
expect(props).toHaveProperty('nodeName', 'span');
100+
});
101+
102+
it('sets the correct attribute', () => {
103+
const { openingElement } = JSXElementMock('input', [
104+
JSXAttributeMock('id', 'foo'),
105+
JSXAttributeMock('type', 'text'),
106+
]);
107+
const vNode = JSXVirtualNode.fromJSXOpeningElement(openingElement);
108+
expect(vNode.attr('id')).toBe('foo');
109+
expect(vNode.attr('type')).toBe('text');
110+
});
111+
});
112+
});

docs/rules/autocomplete-valid.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# autocomplete-valid
2+
3+
Ensure the autocomplete attribute is correct and suitable for the form field it is used with.
4+
5+
#### References
6+
1. [axe-core, autocomplete-valid](https://dequeuniversity.com/rules/axe/3.2/autocomplete-valid)
7+
2. [HTML 5.2, Autocomplete requirements](https://www.w3.org/TR/html52/sec-forms.html#autofilling-form-controls-the-autocomplete-attribute)
8+
9+
## Rule details
10+
11+
This rule takes one optional object argument of type object:
12+
13+
```
14+
{
15+
"rules": {
16+
"jsx-a11y/autocomplete-valid": [ 2, {
17+
"ignoreNonDOM": true
18+
}],
19+
}
20+
}
21+
```
22+
23+
### Succeed
24+
```jsx
25+
<!-- Good: the autocomplete attribute is used according to the HTML specification -->
26+
<input type="text" autocomplete="name" />
27+
28+
<!-- Good: ignoreNonDOM is set to true -->
29+
<MyInput autocomplete="incorrect" />
30+
```
31+
32+
### Fail
33+
```jsx
34+
<!-- Bad: the autocomplete attribute has an invalid value -->
35+
<input type="text" autocomplete="incorrect" />
36+
37+
<!-- Bad: the autocomplete attribute is on an inappropriate input element -->
38+
<input type="email" autocomplete="url" />
39+
40+
<!-- Bad: ignoreNonDOM is undefined or set to false -->
41+
<MyInput autocomplete="incorrect" />
42+
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"aria-query": "^3.0.0",
6363
"array-includes": "^3.0.3",
6464
"ast-types-flow": "^0.0.7",
65+
"axe-core": "^3.2.2-canary.d45f81e",
6566
"axobject-query": "^2.0.2",
6667
"damerau-levenshtein": "^1.0.4",
6768
"emoji-regex": "^7.0.2",

src/rules/autocomplete-valid.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @fileoverview Ensure autocomplete attribute is correct.
3+
* @author Wilco Fiers
4+
*/
5+
6+
// ----------------------------------------------------------------------------
7+
// Rule Definition
8+
// ----------------------------------------------------------------------------
9+
import { dom } from 'aria-query';
10+
import { runVirtualRule } from 'axe-core';
11+
import { elementType } from 'jsx-ast-utils';
12+
import { generateObjSchema } from '../util/schemas';
13+
import JSXVirtualNode from '../util/JSXVirtualNode';
14+
15+
const schema = generateObjSchema({
16+
ignoreNonDOM: {
17+
type: 'boolean',
18+
default: false,
19+
},
20+
});
21+
22+
module.exports = {
23+
meta: {
24+
docs: {
25+
url: 'https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules/autocomplete-valid.md',
26+
},
27+
schema: [schema],
28+
},
29+
30+
create: context => ({
31+
JSXOpeningElement: (node) => {
32+
// Determine if ignoreNonDOM is set to true
33+
// If true, then do not run rule.
34+
const options = context.options[0] || {};
35+
const ignoreNonDOM = !!options.ignoreNonDOM;
36+
const nodeName = elementType(node);
37+
const isDOMNode = dom.get(nodeName);
38+
if (ignoreNonDOM && !isDOMNode) {
39+
return;
40+
}
41+
42+
// If not a DOM node, assume an input element
43+
const vNode = new JSXVirtualNode({
44+
props: { nodeName: isDOMNode ? nodeName : 'input' },
45+
attrs: node.attributes,
46+
});
47+
48+
const { violations } = runVirtualRule('autocomplete-valid', vNode);
49+
if (violations.length === 0) {
50+
return;
51+
}
52+
context.report({
53+
node,
54+
message: violations[0].nodes[0].all[0].message,
55+
});
56+
},
57+
}),
58+
};

src/util/JSXVirtualNode.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* eslint-disable no-underscore-dangle */
2+
import { AbstractVirtualNode } from 'axe-core';
3+
4+
const defaultProps = {
5+
nodeType: 1,
6+
nodeName: 'div',
7+
};
8+
9+
export default class JSXVirtualNode extends AbstractVirtualNode {
10+
constructor({
11+
props = {},
12+
attrs = [],
13+
} = {}) {
14+
super();
15+
this._attrs = [...attrs];
16+
this._props = {
17+
...defaultProps,
18+
...props,
19+
};
20+
}
21+
22+
get props() {
23+
return this._props;
24+
}
25+
26+
attr(attrName) {
27+
const jsxAttr = this._attrs.find(({ name }) => name.name === attrName);
28+
return (jsxAttr && jsxAttr.value && jsxAttr.value.value
29+
? jsxAttr.value.value
30+
: null
31+
);
32+
}
33+
34+
hasAttr(attrName) {
35+
return this._attrs.some(({ name }) => name.name === attrName);
36+
}
37+
38+
static fromJSXOpeningElement(jsxNode) {
39+
const { name = {}, attributes } = jsxNode;
40+
return new JSXVirtualNode({
41+
props: { nodeName: name.name || undefined },
42+
attrs: attributes,
43+
});
44+
}
45+
}

0 commit comments

Comments
 (0)