@@ -21,15 +21,20 @@ const ruleTester = new RuleTester();
21
21
22
22
const ruleName = 'label-has-associated-control' ;
23
23
24
- const expectedError = {
25
- message : 'A form label must be associated with a control.' ,
26
- type : 'JSXOpeningElement' ,
27
- } ;
28
-
29
- const expectedErrorNoLabel = {
30
- message : 'A form label must have accessible text.' ,
31
- type : 'JSXOpeningElement' ,
24
+ const errorMessages = {
25
+ accessibleLabel : 'A form label must have accessible text.' ,
26
+ htmlFor : 'A form label must have a valid htmlFor attribute.' ,
27
+ nesting : 'A form label must have an associated control as a descendant.' ,
28
+ either : 'A form label must either have a valid htmlFor attribute or a control as a descendant.' ,
29
+ both : 'A form label must have a valid htmlFor attribute and a control as a descendant.' ,
32
30
} ;
31
+ const expectedErrors = { } ;
32
+ Object . keys ( errorMessages ) . forEach ( ( key ) => {
33
+ expectedErrors [ key ] = {
34
+ message : errorMessages [ key ] ,
35
+ type : 'JSXOpeningElement' ,
36
+ } ;
37
+ } ) ;
33
38
34
39
const componentsSettings = {
35
40
'jsx-a11y' : {
@@ -123,59 +128,68 @@ const alwaysValid = [
123
128
{ code : '<input type="hidden" />' } ,
124
129
] ;
125
130
126
- const htmlForInvalid = [
127
- { code : '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>' , options : [ { depth : 4 } ] , errors : [ expectedError ] } ,
128
- { code : '<label htmlFor="js_id" aria-label="A label" />' , errors : [ expectedError ] } ,
129
- { code : '<label htmlFor="js_id" aria-labelledby="A label" />' , errors : [ expectedError ] } ,
130
- // Custom label component.
131
- { code : '<CustomLabel htmlFor="js_id" aria-label="A label" />' , options : [ { labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
132
- { code : '<CustomLabel htmlFor="js_id" label="A label" />' , options : [ { labelAttributes : [ 'label' ] , labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
133
- { code : '<CustomLabel htmlFor="js_id" aria-label="A label" />' , settings : componentsSettings , errors : [ expectedError ] } ,
134
- // Custom label attributes.
135
- { code : '<label htmlFor="js_id" label="A label" />' , options : [ { labelAttributes : [ 'label' ] } ] , errors : [ expectedError ] } ,
136
- ] ;
137
- const nestingInvalid = [
138
- { code : '<label>A label<input /></label>' , errors : [ expectedError ] } ,
139
- { code : '<label>A label<textarea /></label>' , errors : [ expectedError ] } ,
140
- { code : '<label><img alt="A label" /><input /></label>' , errors : [ expectedError ] } ,
141
- { code : '<label><img aria-label="A label" /><input /></label>' , errors : [ expectedError ] } ,
142
- { code : '<label><span>A label<input /></span></label>' , errors : [ expectedError ] } ,
143
- { code : '<label><span><span>A label<input /></span></span></label>' , options : [ { depth : 3 } ] , errors : [ expectedError ] } ,
144
- { code : '<label><span><span><span>A label<input /></span></span></span></label>' , options : [ { depth : 4 } ] , errors : [ expectedError ] } ,
145
- { code : '<label><span><span><span><span>A label</span><input /></span></span></span></label>' , options : [ { depth : 5 } ] , errors : [ expectedError ] } ,
146
- { code : '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>' , options : [ { depth : 5 } ] , errors : [ expectedError ] } ,
147
- { code : '<label><span><span><span><input aria-label="A label" /></span></span></span></label>' , options : [ { depth : 5 } ] , errors : [ expectedError ] } ,
148
- // Custom controlComponents.
149
- { code : '<label>A label<OtherCustomInput /></label>' , options : [ { controlComponents : [ 'CustomInput' ] } ] , errors : [ expectedError ] } ,
150
- { code : '<label><span>A label<CustomInput /></span></label>' , options : [ { controlComponents : [ 'CustomInput' ] } ] , errors : [ expectedError ] } ,
151
- { code : '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>' , options : [ { controlComponents : [ 'CustomInput' ] , labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
152
- { code : '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>' , options : [ { controlComponents : [ 'CustomInput' ] , labelComponents : [ 'CustomLabel' ] , labelAttributes : [ 'label' ] } ] , errors : [ expectedError ] } ,
153
- { code : '<label><span>A label<CustomInput /></span></label>' , settings : componentsSettings , errors : [ expectedError ] } ,
154
- { code : '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>' , settings : componentsSettings , errors : [ expectedError ] } ,
155
- ] ;
131
+ const htmlForInvalid = ( assertType ) => {
132
+ const expectedError = expectedErrors [ assertType ] ;
133
+ return [
134
+ { code : '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>' , options : [ { depth : 4 } ] , errors : [ expectedError ] } ,
135
+ { code : '<label htmlFor="js_id" aria-label="A label" />' , errors : [ expectedError ] } ,
136
+ { code : '<label htmlFor="js_id" aria-labelledby="A label" />' , errors : [ expectedError ] } ,
137
+ // Custom label component.
138
+ { code : '<CustomLabel htmlFor="js_id" aria-label="A label" />' , options : [ { labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
139
+ { code : '<CustomLabel htmlFor="js_id" label="A label" />' , options : [ { labelAttributes : [ 'label' ] , labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
140
+ { code : '<CustomLabel htmlFor="js_id" aria-label="A label" />' , settings : componentsSettings , errors : [ expectedError ] } ,
141
+ // Custom label attributes.
142
+ { code : '<label htmlFor="js_id" label="A label" />' , options : [ { labelAttributes : [ 'label' ] } ] , errors : [ expectedError ] } ,
143
+ ] ;
144
+ } ;
145
+ const nestingInvalid = ( assertType ) => {
146
+ const expectedError = expectedErrors [ assertType ] ;
147
+ return [
148
+ { code : '<label>A label<input /></label>' , errors : [ expectedError ] } ,
149
+ { code : '<label>A label<textarea /></label>' , errors : [ expectedError ] } ,
150
+ { code : '<label><img alt="A label" /><input /></label>' , errors : [ expectedError ] } ,
151
+ { code : '<label><img aria-label="A label" /><input /></label>' , errors : [ expectedError ] } ,
152
+ { code : '<label><span>A label<input /></span></label>' , errors : [ expectedError ] } ,
153
+ { code : '<label><span><span>A label<input /></span></span></label>' , options : [ { depth : 3 } ] , errors : [ expectedError ] } ,
154
+ { code : '<label><span><span><span>A label<input /></span></span></span></label>' , options : [ { depth : 4 } ] , errors : [ expectedError ] } ,
155
+ { code : '<label><span><span><span><span>A label</span><input /></span></span></span></label>' , options : [ { depth : 5 } ] , errors : [ expectedError ] } ,
156
+ { code : '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>' , options : [ { depth : 5 } ] , errors : [ expectedError ] } ,
157
+ { code : '<label><span><span><span><input aria-label="A label" /></span></span></span></label>' , options : [ { depth : 5 } ] , errors : [ expectedError ] } ,
158
+ // Custom controlComponents.
159
+ { code : '<label>A label<OtherCustomInput /></label>' , options : [ { controlComponents : [ 'CustomInput' ] } ] , errors : [ expectedError ] } ,
160
+ { code : '<label><span>A label<CustomInput /></span></label>' , options : [ { controlComponents : [ 'CustomInput' ] } ] , errors : [ expectedError ] } ,
161
+ { code : '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>' , options : [ { controlComponents : [ 'CustomInput' ] , labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
162
+ { code : '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>' , options : [ { controlComponents : [ 'CustomInput' ] , labelComponents : [ 'CustomLabel' ] , labelAttributes : [ 'label' ] } ] , errors : [ expectedError ] } ,
163
+ { code : '<label><span>A label<CustomInput /></span></label>' , settings : componentsSettings , errors : [ expectedError ] } ,
164
+ { code : '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>' , settings : componentsSettings , errors : [ expectedError ] } ,
165
+ ] ;
166
+ } ;
156
167
157
- const neverValid = [
158
- { code : '<label htmlFor="js_id" />' , errors : [ expectedErrorNoLabel ] } ,
159
- { code : '<label htmlFor="js_id"><input /></label>' , errors : [ expectedErrorNoLabel ] } ,
160
- { code : '<label htmlFor="js_id"><textarea /></label>' , errors : [ expectedErrorNoLabel ] } ,
161
- { code : '<label></label>' , errors : [ expectedErrorNoLabel ] } ,
162
- { code : '<label>A label</label>' , errors : [ expectedError ] } ,
163
- { code : '<div><label /><input /></div>' , errors : [ expectedErrorNoLabel ] } ,
164
- { code : '<div><label>A label</label><input /></div>' , errors : [ expectedError ] } ,
165
- // Custom label component.
166
- { code : '<CustomLabel aria-label="A label" />' , options : [ { labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
167
- { code : '<MUILabel aria-label="A label" />' , options : [ { labelComponents : [ '???Label' ] } ] , errors : [ expectedError ] } ,
168
- { code : '<CustomLabel label="A label" />' , options : [ { labelAttributes : [ 'label' ] , labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
169
- { code : '<CustomLabel aria-label="A label" />' , settings : componentsSettings , errors : [ expectedError ] } ,
170
- // Custom label attributes.
171
- { code : '<label label="A label" />' , options : [ { labelAttributes : [ 'label' ] } ] , errors : [ expectedError ] } ,
172
- // Custom controlComponents.
173
- { code : '<label><span><CustomInput /></span></label>' , options : [ { controlComponents : [ 'CustomInput' ] } ] , errors : [ expectedErrorNoLabel ] } ,
174
- { code : '<CustomLabel><span><CustomInput /></span></CustomLabel>' , options : [ { controlComponents : [ 'CustomInput' ] , labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedErrorNoLabel ] } ,
175
- { code : '<CustomLabel><span><CustomInput /></span></CustomLabel>' , options : [ { controlComponents : [ 'CustomInput' ] , labelComponents : [ 'CustomLabel' ] , labelAttributes : [ 'label' ] } ] , errors : [ expectedErrorNoLabel ] } ,
176
- { code : '<label><span><CustomInput /></span></label>' , settings : componentsSettings , errors : [ expectedErrorNoLabel ] } ,
177
- { code : '<CustomLabel><span><CustomInput /></span></CustomLabel>' , settings : componentsSettings , errors : [ expectedErrorNoLabel ] } ,
178
- ] ;
168
+ const neverValid = ( assertType ) => {
169
+ const expectedError = expectedErrors [ assertType ] ;
170
+ return [
171
+ { code : '<label htmlFor="js_id" />' , errors : [ expectedErrors . accessibleLabel ] } ,
172
+ { code : '<label htmlFor="js_id"><input /></label>' , errors : [ expectedErrors . accessibleLabel ] } ,
173
+ { code : '<label htmlFor="js_id"><textarea /></label>' , errors : [ expectedErrors . accessibleLabel ] } ,
174
+ { code : '<label></label>' , errors : [ expectedErrors . accessibleLabel ] } ,
175
+ { code : '<label>A label</label>' , errors : [ expectedError ] } ,
176
+ { code : '<div><label /><input /></div>' , errors : [ expectedErrors . accessibleLabel ] } ,
177
+ { code : '<div><label>A label</label><input /></div>' , errors : [ expectedError ] } ,
178
+ // Custom label component.
179
+ { code : '<CustomLabel aria-label="A label" />' , options : [ { labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
180
+ { code : '<MUILabel aria-label="A label" />' , options : [ { labelComponents : [ '???Label' ] } ] , errors : [ expectedError ] } ,
181
+ { code : '<CustomLabel label="A label" />' , options : [ { labelAttributes : [ 'label' ] , labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedError ] } ,
182
+ { code : '<CustomLabel aria-label="A label" />' , settings : componentsSettings , errors : [ expectedError ] } ,
183
+ // Custom label attributes.
184
+ { code : '<label label="A label" />' , options : [ { labelAttributes : [ 'label' ] } ] , errors : [ expectedError ] } ,
185
+ // Custom controlComponents.
186
+ { code : '<label><span><CustomInput /></span></label>' , options : [ { controlComponents : [ 'CustomInput' ] } ] , errors : [ expectedErrors . accessibleLabel ] } ,
187
+ { code : '<CustomLabel><span><CustomInput /></span></CustomLabel>' , options : [ { controlComponents : [ 'CustomInput' ] , labelComponents : [ 'CustomLabel' ] } ] , errors : [ expectedErrors . accessibleLabel ] } ,
188
+ { code : '<CustomLabel><span><CustomInput /></span></CustomLabel>' , options : [ { controlComponents : [ 'CustomInput' ] , labelComponents : [ 'CustomLabel' ] , labelAttributes : [ 'label' ] } ] , errors : [ expectedErrors . accessibleLabel ] } ,
189
+ { code : '<label><span><CustomInput /></span></label>' , settings : componentsSettings , errors : [ expectedErrors . accessibleLabel ] } ,
190
+ { code : '<CustomLabel><span><CustomInput /></span></CustomLabel>' , settings : componentsSettings , errors : [ expectedErrors . accessibleLabel ] } ,
191
+ ] ;
192
+ } ;
179
193
// htmlFor valid
180
194
ruleTester . run ( ruleName , rule , {
181
195
valid : parsers . all ( [ ] . concat (
@@ -187,8 +201,8 @@ ruleTester.run(ruleName, rule, {
187
201
} ) )
188
202
. map ( parserOptionsMapper ) ,
189
203
invalid : parsers . all ( [ ] . concat (
190
- ...neverValid ,
191
- ...nestingInvalid ,
204
+ ...neverValid ( 'htmlFor' ) ,
205
+ ...nestingInvalid ( 'htmlFor' ) ,
192
206
) )
193
207
. map ( ruleOptionsMapperFactory ( {
194
208
assert : 'htmlFor' ,
@@ -207,8 +221,8 @@ ruleTester.run(ruleName, rule, {
207
221
} ) )
208
222
. map ( parserOptionsMapper ) ,
209
223
invalid : parsers . all ( [ ] . concat (
210
- ...neverValid ,
211
- ...htmlForInvalid ,
224
+ ...neverValid ( 'nesting' ) ,
225
+ ...htmlForInvalid ( 'nesting' ) ,
212
226
) )
213
227
. map ( ruleOptionsMapperFactory ( {
214
228
assert : 'nesting' ,
@@ -228,8 +242,10 @@ ruleTester.run(ruleName, rule, {
228
242
} ) )
229
243
. map ( parserOptionsMapper ) ,
230
244
invalid : parsers . all ( [ ] . concat (
231
- ...neverValid ,
232
- ) ) . map ( parserOptionsMapper ) ,
245
+ ...neverValid ( 'either' ) ,
246
+ ) ) . map ( ruleOptionsMapperFactory ( {
247
+ assert : 'either' ,
248
+ } ) ) . map ( parserOptionsMapper ) ,
233
249
} ) ;
234
250
235
251
// both valid
@@ -243,6 +259,10 @@ ruleTester.run(ruleName, rule, {
243
259
} ) )
244
260
. map ( parserOptionsMapper ) ,
245
261
invalid : parsers . all ( [ ] . concat (
246
- ...neverValid ,
247
- ) ) . map ( parserOptionsMapper ) ,
262
+ ...neverValid ( 'both' ) ,
263
+ ...htmlForInvalid ( 'both' ) ,
264
+ ...nestingInvalid ( 'both' ) ,
265
+ ) ) . map ( ruleOptionsMapperFactory ( {
266
+ assert : 'both' ,
267
+ } ) ) . map ( parserOptionsMapper ) ,
248
268
} ) ;
0 commit comments