@@ -21,15 +21,20 @@ const ruleTester = new RuleTester();
2121
2222const ruleName = 'label-has-associated-control' ;
2323
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.' ,
3230} ;
31+ const expectedErrors = { } ;
32+ Object . keys ( errorMessages ) . forEach ( ( key ) => {
33+ expectedErrors [ key ] = {
34+ message : errorMessages [ key ] ,
35+ type : 'JSXOpeningElement' ,
36+ } ;
37+ } ) ;
3338
3439const componentsSettings = {
3540 'jsx-a11y' : {
@@ -123,59 +128,68 @@ const alwaysValid = [
123128 { code : '<input type="hidden" />' } ,
124129] ;
125130
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+ } ;
156167
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+ } ;
179193// htmlFor valid
180194ruleTester . run ( ruleName , rule , {
181195 valid : parsers . all ( [ ] . concat (
@@ -187,8 +201,8 @@ ruleTester.run(ruleName, rule, {
187201 } ) )
188202 . map ( parserOptionsMapper ) ,
189203 invalid : parsers . all ( [ ] . concat (
190- ...neverValid ,
191- ...nestingInvalid ,
204+ ...neverValid ( 'htmlFor' ) ,
205+ ...nestingInvalid ( 'htmlFor' ) ,
192206 ) )
193207 . map ( ruleOptionsMapperFactory ( {
194208 assert : 'htmlFor' ,
@@ -207,8 +221,8 @@ ruleTester.run(ruleName, rule, {
207221 } ) )
208222 . map ( parserOptionsMapper ) ,
209223 invalid : parsers . all ( [ ] . concat (
210- ...neverValid ,
211- ...htmlForInvalid ,
224+ ...neverValid ( 'nesting' ) ,
225+ ...htmlForInvalid ( 'nesting' ) ,
212226 ) )
213227 . map ( ruleOptionsMapperFactory ( {
214228 assert : 'nesting' ,
@@ -228,8 +242,10 @@ ruleTester.run(ruleName, rule, {
228242 } ) )
229243 . map ( parserOptionsMapper ) ,
230244 invalid : parsers . all ( [ ] . concat (
231- ...neverValid ,
232- ) ) . map ( parserOptionsMapper ) ,
245+ ...neverValid ( 'either' ) ,
246+ ) ) . map ( ruleOptionsMapperFactory ( {
247+ assert : 'either' ,
248+ } ) ) . map ( parserOptionsMapper ) ,
233249} ) ;
234250
235251// both valid
@@ -243,6 +259,10 @@ ruleTester.run(ruleName, rule, {
243259 } ) )
244260 . map ( parserOptionsMapper ) ,
245261 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 ) ,
248268} ) ;
0 commit comments