Skip to content

Commit 7f13a92

Browse files
authored
Merge branch 'master' into fix-heading-has-content-components
2 parents 0f75437 + 82dc999 commit 7f13a92

22 files changed

+941
-21
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
language: node_js
22
node_js:
3+
- "10"
4+
- "9"
35
- "8"
46
- "7"
57
- "6"
@@ -27,5 +29,6 @@ matrix:
2729
- node_js: "node"
2830
env: LINT=true TEST=false
2931
allow_failures:
32+
- node_js: "9"
3033
- node_js: "7"
3134
- node_js: "5"

__mocks__/JSXElementMock.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
export default function JSXElementMock(tagName, attributes, children = []) {
1+
/**
2+
* @flow
3+
*/
4+
5+
import JSXAttributeMock from './JSXAttributeMock';
6+
7+
export default function JSXElementMock(
8+
tagName: string,
9+
attributes: Array<JSXAttributeMock>,
10+
children: Array<Node> = [],
11+
) {
212
return {
313
type: 'JSXElement',
414
openingElement: {

__mocks__/JSXSpreadAttributeMock.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @flow
3+
*/
4+
5+
import IdentifierMock from './IdentifierMock';
6+
7+
export default function JSXSpreadAttributeMock(identifier: string) {
8+
return {
9+
type: 'JSXSpreadAttribute',
10+
argument: IdentifierMock(identifier),
11+
};
12+
}

__mocks__/JSXTextMock.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @flow
3+
*/
4+
export default function JSXTextMock(value: string) {
5+
return {
6+
type: 'JSXText',
7+
value,
8+
raw: value,
9+
};
10+
}

__mocks__/LiteralMock.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @flow
3+
*/
4+
export default function LiteralMock(value: string) {
5+
return {
6+
type: 'Literal',
7+
value,
8+
raw: value,
9+
};
10+
}

__tests__/__util__/ruleOptionsMapperFactory.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ type ESLintTestRunnerTestCase = {
1111

1212
export default function ruleOptionsMapperFactory(ruleOptions: Array<mixed> = []) {
1313
// eslint-disable-next-line
14-
return ({ code, errors, options, parserOptions }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => ({
15-
code,
16-
errors,
17-
options: (options || []).concat(ruleOptions),
18-
parserOptions,
19-
});
14+
return ({ code, errors, options, parserOptions }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => {
15+
return {
16+
code,
17+
errors,
18+
// Flatten the array of objects in an array of one object.
19+
options: (options || []).concat(ruleOptions).reduce((acc, item) => [{
20+
...acc[0],
21+
...item,
22+
}], [{}]),
23+
parserOptions,
24+
};
25+
};
2026
}

__tests__/src/rules/heading-has-content-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ ruleTester.run('heading-has-content', rule, {
4343
{ code: '<h1>{foo.bar}</h1>' },
4444
{ code: '<h1 dangerouslySetInnerHTML={{ __html: "foo" }} />' },
4545
{ code: '<h1 children={children} />' },
46-
4746
// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
4847
{ code: '<Heading>Foo</Heading>', options: components },
4948
{ code: '<Title>Foo</Title>', options: components },
@@ -52,6 +51,7 @@ ruleTester.run('heading-has-content', rule, {
5251
{ code: '<Heading>{foo.bar}</Heading>', options: components },
5352
{ code: '<Heading dangerouslySetInnerHTML={{ __html: "foo" }} />', options: components },
5453
{ code: '<Heading children={children} />', options: components },
54+
{ code: '<h1 aria-hidden />' },
5555
].map(parserOptionsMapper),
5656
invalid: [
5757
// DEFAULT ELEMENT TESTS
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/* eslint-env jest */
2+
/**
3+
* @fileoverview Enforce label tags have an associated control.
4+
* @author Jesse Beach
5+
*/
6+
7+
// -----------------------------------------------------------------------------
8+
// Requirements
9+
// -----------------------------------------------------------------------------
10+
11+
import { RuleTester } from 'eslint';
12+
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
13+
import rule from '../../../src/rules/label-has-associated-control';
14+
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
15+
16+
// -----------------------------------------------------------------------------
17+
// Tests
18+
// -----------------------------------------------------------------------------
19+
20+
const ruleTester = new RuleTester();
21+
22+
const ruleName = 'label-has-associated-control';
23+
24+
const expectedError = {
25+
message: 'A form label must be associated with a control.',
26+
type: 'JSXOpeningElement',
27+
};
28+
29+
const htmlForValid = [
30+
{ code: '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }] },
31+
{ code: '<label htmlFor="js_id" aria-label="A label" />' },
32+
{ code: '<label htmlFor="js_id" aria-labelledby="A label" />' },
33+
// Custom label component.
34+
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }] },
35+
{ code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
36+
// Custom label attributes.
37+
{ code: '<label htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'] }] },
38+
];
39+
const nestingValid = [
40+
{ code: '<label>A label<input /></label>' },
41+
{ code: '<label><img alt="A label" /><input /></label>' },
42+
{ code: '<label><img aria-label="A label" /><input /></label>' },
43+
{ code: '<label><span>A label<input /></span></label>' },
44+
{ code: '<label><span><span>A label<input /></span></span></label>', options: [{ depth: 3 }] },
45+
{ code: '<label><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }] },
46+
{ code: '<label><span><span><span><span>A label</span><input /></span></span></span></label>', options: [{ depth: 5 }] },
47+
{ code: '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>', options: [{ depth: 5 }] },
48+
{ code: '<label><span><span><span><input aria-label="A label" /></span></span></span></label>', options: [{ depth: 5 }] },
49+
// Custom controlComponents.
50+
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }] },
51+
{ code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }] },
52+
{ code: '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }] },
53+
];
54+
55+
const bothValid = [
56+
{ code: '<label htmlFor="js_id"><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }] },
57+
{ code: '<label htmlFor="js_id" aria-label="A label"><input /></label>' },
58+
{ code: '<label htmlFor="js_id" aria-labelledby="A label"><input /></label>' },
59+
// Custom label component.
60+
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><input /></CustomLabel>', options: [{ labelComponents: ['CustomLabel'] }] },
61+
{ code: '<CustomLabel htmlFor="js_id" label="A label"><input /></CustomLabel>', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
62+
// Custom label attributes.
63+
{ code: '<label htmlFor="js_id" label="A label"><input /></label>', options: [{ labelAttributes: ['label'] }] },
64+
];
65+
66+
const alwaysValid = [
67+
{ code: '<div />' },
68+
{ code: '<CustomElement />' },
69+
];
70+
71+
const htmlForInvalid = [
72+
{ code: '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }], errors: [expectedError] },
73+
{ code: '<label htmlFor="js_id" aria-label="A label" />', errors: [expectedError] },
74+
{ code: '<label htmlFor="js_id" aria-labelledby="A label" />', errors: [expectedError] },
75+
// Custom label component.
76+
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }], errors: [expectedError] },
77+
{ code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
78+
// Custom label attributes.
79+
{ code: '<label htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'] }], errors: [expectedError] },
80+
];
81+
const nestingInvalid = [
82+
{ code: '<label>A label<input /></label>', errors: [expectedError] },
83+
{ code: '<label><img alt="A label" /><input /></label>', errors: [expectedError] },
84+
{ code: '<label><img aria-label="A label" /><input /></label>', errors: [expectedError] },
85+
{ code: '<label><span>A label<input /></span></label>', errors: [expectedError] },
86+
{ code: '<label><span><span>A label<input /></span></span></label>', options: [{ depth: 3 }], errors: [expectedError] },
87+
{ code: '<label><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }], errors: [expectedError] },
88+
{ code: '<label><span><span><span><span>A label</span><input /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
89+
{ code: '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
90+
{ code: '<label><span><span><span><input aria-label="A label" /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
91+
// Custom controlComponents.
92+
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }], errors: [expectedError] },
93+
{ code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
94+
{ code: '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }], errors: [expectedError] },
95+
];
96+
97+
const neverValid = [
98+
{ code: '<label htmlFor="js_id" />', errors: [expectedError] },
99+
{ code: '<label htmlFor="js_id"><input /></label>', errors: [expectedError] },
100+
{ code: '<label></label>', errors: [expectedError] },
101+
{ code: '<label>A label</label>', errors: [expectedError] },
102+
{ code: '<div><label /><input /></div>', errors: [expectedError] },
103+
{ code: '<div><label>A label</label><input /></div>', errors: [expectedError] },
104+
// Custom label component.
105+
{ code: '<CustomLabel aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }], errors: [expectedError] },
106+
{ code: '<CustomLabel label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
107+
// Custom label attributes.
108+
{ code: '<label label="A label" />', options: [{ labelAttributes: ['label'] }], errors: [expectedError] },
109+
// Custom controlComponents.
110+
{ code: '<label><span><CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }], errors: [expectedError] },
111+
{ code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
112+
{ code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }], errors: [expectedError] },
113+
];
114+
// htmlFor valid
115+
ruleTester.run(ruleName, rule, {
116+
valid: [
117+
...alwaysValid,
118+
...htmlForValid,
119+
]
120+
.map(ruleOptionsMapperFactory({
121+
assert: 'htmlFor',
122+
}))
123+
.map(parserOptionsMapper),
124+
invalid: [
125+
...neverValid,
126+
...nestingInvalid,
127+
]
128+
.map(ruleOptionsMapperFactory({
129+
assert: 'htmlFor',
130+
}))
131+
.map(parserOptionsMapper),
132+
});
133+
134+
// nesting valid
135+
ruleTester.run(ruleName, rule, {
136+
valid: [
137+
...alwaysValid,
138+
...nestingValid,
139+
]
140+
.map(ruleOptionsMapperFactory({
141+
assert: 'nesting',
142+
}))
143+
.map(parserOptionsMapper),
144+
invalid: [
145+
...neverValid,
146+
...htmlForInvalid,
147+
]
148+
.map(ruleOptionsMapperFactory({
149+
assert: 'nesting',
150+
}))
151+
.map(parserOptionsMapper),
152+
});
153+
154+
// either valid
155+
ruleTester.run(ruleName, rule, {
156+
valid: [
157+
...alwaysValid,
158+
...htmlForValid,
159+
...nestingValid,
160+
]
161+
.map(ruleOptionsMapperFactory({
162+
assert: 'either',
163+
}))
164+
.map(parserOptionsMapper),
165+
invalid: [
166+
...neverValid,
167+
].map(parserOptionsMapper),
168+
});
169+
170+
// both valid
171+
ruleTester.run(ruleName, rule, {
172+
valid: [
173+
...alwaysValid,
174+
...bothValid,
175+
]
176+
.map(ruleOptionsMapperFactory({
177+
assert: 'both',
178+
}))
179+
.map(parserOptionsMapper),
180+
invalid: [
181+
...neverValid,
182+
].map(parserOptionsMapper),
183+
});

__tests__/src/rules/label-has-for-test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,15 @@ ruleTester.run('label-has-for', rule, {
165165
{ code: '<label>First Name</label>', errors: [expectedSomeError], options: optionsRequiredSome },
166166
{ code: '<label>{children}</label>', errors: [expectedSomeError], options: optionsRequiredSome },
167167
{ code: '<label>{children}</label>', errors: [expectedNestingError], options: optionsRequiredNesting },
168+
{
169+
code: '<form><input type="text" id="howmuch" value="1" /><label htmlFor="howmuch">How much ?</label></form>',
170+
errors: [expectedEveryError],
171+
options: optionsRequiredEvery,
172+
},
173+
{
174+
code: '<form><input type="text" id="howmuch" value="1" /><label htmlFor="howmuch">How much ?<span /></label></form>',
175+
errors: [expectedEveryError],
176+
options: optionsRequiredEvery,
177+
},
168178
].map(parserOptionsMapper),
169179
});

__tests__/src/util/hasAccessibleChild-test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ describe('hasAccessibleChild', () => {
3636
expect(hasAccessibleChild(element)).toBe(true);
3737
});
3838

39+
it('Returns true for JSXText Element', () => {
40+
const child = {
41+
type: 'JSXText',
42+
value: 'foo',
43+
};
44+
const element = JSXElementMock('div', [], [child]);
45+
expect(hasAccessibleChild(element)).toBe(true);
46+
});
47+
3948
it('Returns false for hidden child JSXElement', () => {
4049
const ariaHiddenAttr = JSXAttributeMock('aria-hidden', true);
4150
const child = JSXElementMock('div', [ariaHiddenAttr]);

0 commit comments

Comments
 (0)