Skip to content

Commit 1bd81ca

Browse files
authored
Merge branch 'master' into feature/manage-ids
2 parents f234698 + ead1767 commit 1bd81ca

File tree

7 files changed

+621
-4
lines changed

7 files changed

+621
-4
lines changed

__mocks__/JSXElementMock.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import JSXAttributeMock from './JSXAttributeMock';
66

77
export default function JSXElementMock(
88
tagName: string,
9-
attributes: Array<JSXAttributeMock>,
9+
attributes: Array<JSXAttributeMock> = [],
1010
children: Array<Node> = [],
1111
) {
1212
return {
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/* eslint-env jest */
2+
/**
3+
* @fileoverview Control elements must be associated with a text label
4+
* @author jessebeach
5+
*/
6+
7+
// -----------------------------------------------------------------------------
8+
// Requirements
9+
// -----------------------------------------------------------------------------
10+
11+
import { RuleTester } from 'eslint';
12+
import { configs } from '../../../src/index';
13+
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
14+
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
15+
import rule from '../../../src/rules/control-has-associated-label';
16+
17+
// -----------------------------------------------------------------------------
18+
// Tests
19+
// -----------------------------------------------------------------------------
20+
21+
const ruleTester = new RuleTester();
22+
23+
const ruleName = 'jsx-a11y/control-has-associated-label';
24+
25+
const expectedError = {
26+
message: 'A control must be associated with a text label.',
27+
type: 'JSXOpeningElement',
28+
};
29+
30+
const alwaysValid = [
31+
// Custom Control Components
32+
{ code: '<CustomControl><span><span>Save</span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }] },
33+
{ code: '<CustomControl><span><span label="Save"></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'], labelAttributes: ['label'] }] },
34+
// Interactive Elements
35+
{ code: '<button>Save</button>' },
36+
{ code: '<button><span>Save</span></button>' },
37+
{ code: '<button><span><span>Save</span></span></button>', options: [{ depth: 3 }] },
38+
{ code: '<button><span><span><span><span><span><span><span><span>Save</span></span></span></span></span></span></span></span></button>', options: [{ depth: 9 }] },
39+
{ code: '<button><img alt="Save" /></button>' },
40+
{ code: '<button aria-label="Save" />' },
41+
{ code: '<button><span aria-label="Save" /></button>' },
42+
{ code: '<button aria-labelledby="js_1" />' },
43+
{ code: '<button><span aria-labelledby="js_1" /></button>' },
44+
{ code: '<button>{sureWhyNot}</button>' },
45+
{ code: '<button><span><span label="Save"></span></span></button>', options: [{ depth: 3, labelAttributes: ['label'] }] },
46+
{ code: '<a href="#">Save</a>' },
47+
{ code: '<area href="#">Save</area>' },
48+
{ code: '<label>Save</label>' },
49+
{ code: '<link>Save</link>' },
50+
{ code: '<menuitem>Save</menuitem>' },
51+
{ code: '<option>Save</option>' },
52+
{ code: '<th>Save</th>' },
53+
// Interactive Roles
54+
{ code: '<div role="button">Save</div>' },
55+
{ code: '<div role="checkbox">Save</div>' },
56+
{ code: '<div role="columnheader">Save</div>' },
57+
{ code: '<div role="combobox">Save</div>' },
58+
{ code: '<div role="gridcell">Save</div>' },
59+
{ code: '<div role="link">Save</div>' },
60+
{ code: '<div role="menuitem">Save</div>' },
61+
{ code: '<div role="menuitemcheckbox">Save</div>' },
62+
{ code: '<div role="menuitemradio">Save</div>' },
63+
{ code: '<div role="option">Save</div>' },
64+
{ code: '<div role="progressbar">Save</div>' },
65+
{ code: '<div role="radio">Save</div>' },
66+
{ code: '<div role="rowheader">Save</div>' },
67+
{ code: '<div role="searchbox">Save</div>' },
68+
{ code: '<div role="slider">Save</div>' },
69+
{ code: '<div role="spinbutton">Save</div>' },
70+
{ code: '<div role="switch">Save</div>' },
71+
{ code: '<div role="tab">Save</div>' },
72+
{ code: '<div role="textbox">Save</div>' },
73+
{ code: '<div role="treeitem">Save</div>' },
74+
{ code: '<div role="button" aria-label="Save" />' },
75+
{ code: '<div role="checkbox" aria-label="Save" />' },
76+
{ code: '<div role="columnheader" aria-label="Save" />' },
77+
{ code: '<div role="combobox" aria-label="Save" />' },
78+
{ code: '<div role="gridcell" aria-label="Save" />' },
79+
{ code: '<div role="link" aria-label="Save" />' },
80+
{ code: '<div role="menuitem" aria-label="Save" />' },
81+
{ code: '<div role="menuitemcheckbox" aria-label="Save" />' },
82+
{ code: '<div role="menuitemradio" aria-label="Save" />' },
83+
{ code: '<div role="option" aria-label="Save" />' },
84+
{ code: '<div role="progressbar" aria-label="Save" />' },
85+
{ code: '<div role="radio" aria-label="Save" />' },
86+
{ code: '<div role="rowheader" aria-label="Save" />' },
87+
{ code: '<div role="searchbox" aria-label="Save" />' },
88+
{ code: '<div role="slider" aria-label="Save" />' },
89+
{ code: '<div role="spinbutton" aria-label="Save" />' },
90+
{ code: '<div role="switch" aria-label="Save" />' },
91+
{ code: '<div role="tab" aria-label="Save" />' },
92+
{ code: '<div role="textbox" aria-label="Save" />' },
93+
{ code: '<div role="treeitem" aria-label="Save" />' },
94+
{ code: '<div role="button" aria-labelledby="js_1" />' },
95+
{ code: '<div role="checkbox" aria-labelledby="js_1" />' },
96+
{ code: '<div role="columnheader" aria-labelledby="js_1" />' },
97+
{ code: '<div role="combobox" aria-labelledby="js_1" />' },
98+
{ code: '<div role="gridcell" aria-labelledby="Save" />' },
99+
{ code: '<div role="link" aria-labelledby="js_1" />' },
100+
{ code: '<div role="menuitem" aria-labelledby="js_1" />' },
101+
{ code: '<div role="menuitemcheckbox" aria-labelledby="js_1" />' },
102+
{ code: '<div role="menuitemradio" aria-labelledby="js_1" />' },
103+
{ code: '<div role="option" aria-labelledby="js_1" />' },
104+
{ code: '<div role="progressbar" aria-labelledby="js_1" />' },
105+
{ code: '<div role="radio" aria-labelledby="js_1" />' },
106+
{ code: '<div role="rowheader" aria-labelledby="js_1" />' },
107+
{ code: '<div role="searchbox" aria-labelledby="js_1" />' },
108+
{ code: '<div role="slider" aria-labelledby="js_1" />' },
109+
{ code: '<div role="spinbutton" aria-labelledby="js_1" />' },
110+
{ code: '<div role="switch" aria-labelledby="js_1" />' },
111+
{ code: '<div role="tab" aria-labelledby="js_1" />' },
112+
{ code: '<div role="textbox" aria-labelledby="js_1" />' },
113+
{ code: '<div role="treeitem" aria-labelledby="js_1" />' },
114+
// Non-interactive Elements
115+
{ code: '<abbr />' },
116+
{ code: '<article />' },
117+
{ code: '<blockquote />' },
118+
{ code: '<br />' },
119+
{ code: '<caption />' },
120+
{ code: '<dd />' },
121+
{ code: '<details />' },
122+
{ code: '<dfn />' },
123+
{ code: '<dialog />' },
124+
{ code: '<dir />' },
125+
{ code: '<dl />' },
126+
{ code: '<dt />' },
127+
{ code: '<fieldset />' },
128+
{ code: '<figcaption />' },
129+
{ code: '<figure />' },
130+
{ code: '<footer />' },
131+
{ code: '<form />' },
132+
{ code: '<frame />' },
133+
{ code: '<h1 />' },
134+
{ code: '<h2 />' },
135+
{ code: '<h3 />' },
136+
{ code: '<h4 />' },
137+
{ code: '<h5 />' },
138+
{ code: '<h6 />' },
139+
{ code: '<hr />' },
140+
{ code: '<iframe />' },
141+
{ code: '<img />' },
142+
{ code: '<legend />' },
143+
{ code: '<li />' },
144+
{ code: '<main />' },
145+
{ code: '<mark />' },
146+
{ code: '<marquee />' },
147+
{ code: '<menu />' },
148+
{ code: '<meter />' },
149+
{ code: '<nav />' },
150+
{ code: '<ol />' },
151+
{ code: '<p />' },
152+
{ code: '<pre />' },
153+
{ code: '<progress />' },
154+
{ code: '<ruby />' },
155+
{ code: '<section />' },
156+
{ code: '<table />' },
157+
{ code: '<tbody />' },
158+
{ code: '<td />' },
159+
{ code: '<tfoot />' },
160+
{ code: '<thead />' },
161+
{ code: '<time />' },
162+
{ code: '<ul />' },
163+
// Non-interactive Roles
164+
{ code: '<div role="alert" />' },
165+
{ code: '<div role="alertdialog" />' },
166+
{ code: '<div role="application" />' },
167+
{ code: '<div role="article" />' },
168+
{ code: '<div role="banner" />' },
169+
{ code: '<div role="cell" />' },
170+
{ code: '<div role="complementary" />' },
171+
{ code: '<div role="contentinfo" />' },
172+
{ code: '<div role="definition" />' },
173+
{ code: '<div role="dialog" />' },
174+
{ code: '<div role="directory" />' },
175+
{ code: '<div role="document" />' },
176+
{ code: '<div role="feed" />' },
177+
{ code: '<div role="figure" />' },
178+
{ code: '<div role="form" />' },
179+
{ code: '<div role="group" />' },
180+
{ code: '<div role="heading" />' },
181+
{ code: '<div role="img" />' },
182+
{ code: '<div role="list" />' },
183+
{ code: '<div role="listitem" />' },
184+
{ code: '<div role="log" />' },
185+
{ code: '<div role="main" />' },
186+
{ code: '<div role="marquee" />' },
187+
{ code: '<div role="math" />' },
188+
{ code: '<div role="navigation" />' },
189+
{ code: '<div role="none" />' },
190+
{ code: '<div role="note" />' },
191+
{ code: '<div role="presentation" />' },
192+
{ code: '<div role="region" />' },
193+
{ code: '<div role="rowgroup" />' },
194+
{ code: '<div role="scrollbar" />' },
195+
{ code: '<div role="search" />' },
196+
{ code: '<div role="separator" />' },
197+
{ code: '<div role="status" />' },
198+
{ code: '<div role="table" />' },
199+
{ code: '<div role="tabpanel" />' },
200+
{ code: '<div role="term" />' },
201+
{ code: '<div role="timer" />' },
202+
{ code: '<div role="tooltip" />' },
203+
// Via config
204+
// Inputs. Ignore them because they might get a label from a wrapping label element.
205+
{ code: '<input />' },
206+
{ code: '<input type="button" />' },
207+
{ code: '<input type="checkbox" />' },
208+
{ code: '<input type="color" />' },
209+
{ code: '<input type="date" />' },
210+
{ code: '<input type="datetime" />' },
211+
{ code: '<input type="email" />' },
212+
{ code: '<input type="file" />' },
213+
{ code: '<input type="image" />' },
214+
{ code: '<input type="month" />' },
215+
{ code: '<input type="number" />' },
216+
{ code: '<input type="password" />' },
217+
{ code: '<input type="radio" />' },
218+
{ code: '<input type="range" />' },
219+
{ code: '<input type="reset" />' },
220+
{ code: '<input type="search" />' },
221+
{ code: '<input type="submit" />' },
222+
{ code: '<input type="tel" />' },
223+
{ code: '<input type="text" />' },
224+
{ code: '<input type="time" />' },
225+
{ code: '<input type="url" />' },
226+
{ code: '<input type="week" />' },
227+
// Marginal interactive elements. It is difficult to insist that these
228+
// elements contain a text label.
229+
{ code: '<audio />' },
230+
{ code: '<canvas />' },
231+
{ code: '<embed />' },
232+
{ code: '<textarea />' },
233+
{ code: '<tr />' },
234+
{ code: '<video />' },
235+
// Interactive roles to ignore
236+
{ code: '<div role="grid" />' },
237+
{ code: '<div role="listbox" />' },
238+
{ code: '<div role="menu" />' },
239+
{ code: '<div role="menubar" />' },
240+
{ code: '<div role="radiogroup" />' },
241+
{ code: '<div role="row" />' },
242+
{ code: '<div role="tablist" />' },
243+
{ code: '<div role="toolbar" />' },
244+
{ code: '<div role="tree" />' },
245+
{ code: '<div role="treegrid" />' },
246+
];
247+
const neverValid = [
248+
{ code: '<button />', errors: [expectedError] },
249+
{ code: '<button><span /></button>', errors: [expectedError] },
250+
{ code: '<button><img /></button>', errors: [expectedError] },
251+
{ code: '<button><span title="This is not a real label" /></button>', errors: [expectedError] },
252+
{ code: '<button><span><span><span>Save</span></span></span></button>', options: [{ depth: 3 }], errors: [expectedError] },
253+
{ code: '<CustomControl><span><span></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }], errors: [expectedError] },
254+
{ code: '<a href="#" />', errors: [expectedError] },
255+
{ code: '<area href="#" />', errors: [expectedError] },
256+
{ code: '<label />', errors: [expectedError] },
257+
{ code: '<link />', errors: [expectedError] },
258+
{ code: '<menuitem />', errors: [expectedError] },
259+
{ code: '<option />', errors: [expectedError] },
260+
{ code: '<th />', errors: [expectedError] },
261+
// Interactive Roles
262+
{ code: '<div role="button" />', errors: [expectedError] },
263+
{ code: '<div role="checkbox" />', errors: [expectedError] },
264+
{ code: '<div role="columnheader" />', errors: [expectedError] },
265+
{ code: '<div role="combobox" />', errors: [expectedError] },
266+
{ code: '<div role="link" />', errors: [expectedError] },
267+
{ code: '<div role="gridcell" />', errors: [expectedError] },
268+
{ code: '<div role="menuitem" />', errors: [expectedError] },
269+
{ code: '<div role="menuitemcheckbox" />', errors: [expectedError] },
270+
{ code: '<div role="menuitemradio" />', errors: [expectedError] },
271+
{ code: '<div role="option" />', errors: [expectedError] },
272+
{ code: '<div role="progressbar" />', errors: [expectedError] },
273+
{ code: '<div role="radio" />', errors: [expectedError] },
274+
{ code: '<div role="rowheader" />', errors: [expectedError] },
275+
{ code: '<div role="searchbox" />', errors: [expectedError] },
276+
{ code: '<div role="slider" />', errors: [expectedError] },
277+
{ code: '<div role="spinbutton" />', errors: [expectedError] },
278+
{ code: '<div role="switch" />', errors: [expectedError] },
279+
{ code: '<div role="tab" />', errors: [expectedError] },
280+
{ code: '<div role="textbox" />', errors: [expectedError] },
281+
];
282+
283+
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
284+
ruleTester.run(`${ruleName}:recommended`, rule, {
285+
valid: [
286+
...alwaysValid,
287+
]
288+
.map(ruleOptionsMapperFactory(recommendedOptions))
289+
.map(parserOptionsMapper),
290+
invalid: [
291+
...neverValid,
292+
]
293+
.map(ruleOptionsMapperFactory(recommendedOptions))
294+
.map(parserOptionsMapper),
295+
});
296+
297+
const strictOptions = (configs.strict.rules[ruleName][1] || {});
298+
ruleTester.run(`${ruleName}:strict`, rule, {
299+
valid: [
300+
...alwaysValid,
301+
]
302+
.map(ruleOptionsMapperFactory(strictOptions))
303+
.map(parserOptionsMapper),
304+
invalid: [
305+
...neverValid,
306+
]
307+
.map(ruleOptionsMapperFactory(strictOptions))
308+
.map(parserOptionsMapper),
309+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/* eslint-env mocha */
2+
import { dom } from 'aria-query';
3+
import expect from 'expect';
4+
import { elementType } from 'jsx-ast-utils';
5+
import isDOMElement from '../../../src/util/isDOMElement';
6+
import JSXElementMock from '../../../__mocks__/JSXElementMock';
7+
8+
const domElements = [...dom.keys()];
9+
10+
describe('isDOMElement', () => {
11+
describe('DOM elements', () => {
12+
domElements.forEach((el) => {
13+
it(`should identify ${el} as a DOM element`, () => {
14+
const element = JSXElementMock(el);
15+
expect(isDOMElement(elementType(element.openingElement)))
16+
.toBe(true);
17+
});
18+
});
19+
});
20+
describe('Custom Element', () => {
21+
it('should not identify a custom element', () => {
22+
const element = JSXElementMock('CustomElement');
23+
expect(isDOMElement(element))
24+
.toBe(false);
25+
});
26+
});
27+
});

0 commit comments

Comments
 (0)