@arcaauth/eslint-plugin-jsx-a11y 6.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/.babelrc +17 -0
  2. package/.eslintrc +44 -0
  3. package/CHANGELOG.md +774 -0
  4. package/LICENSE.md +8 -0
  5. package/README.md +423 -0
  6. package/__mocks__/IdentifierMock.js +15 -0
  7. package/__mocks__/JSXAttributeMock.js +39 -0
  8. package/__mocks__/JSXElementMock.js +37 -0
  9. package/__mocks__/JSXExpressionContainerMock.js +15 -0
  10. package/__mocks__/JSXSpreadAttributeMock.js +18 -0
  11. package/__mocks__/JSXTextMock.js +17 -0
  12. package/__mocks__/LiteralMock.js +17 -0
  13. package/__mocks__/genInteractives.js +218 -0
  14. package/__tests__/__util__/axeMapping.js +6 -0
  15. package/__tests__/__util__/helpers/getESLintCoreRule.js +9 -0
  16. package/__tests__/__util__/helpers/parsers.js +186 -0
  17. package/__tests__/__util__/parserOptionsMapper.js +53 -0
  18. package/__tests__/__util__/ruleOptionsMapperFactory.js +33 -0
  19. package/__tests__/index-test.js +40 -0
  20. package/__tests__/src/rules/accessible-emoji-test.js +66 -0
  21. package/__tests__/src/rules/alt-text-test.js +291 -0
  22. package/__tests__/src/rules/anchor-ambiguous-text-test.js +117 -0
  23. package/__tests__/src/rules/anchor-has-content-test.js +54 -0
  24. package/__tests__/src/rules/anchor-is-valid-test.js +532 -0
  25. package/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js +95 -0
  26. package/__tests__/src/rules/aria-props-test.js +69 -0
  27. package/__tests__/src/rules/aria-proptypes-test.js +311 -0
  28. package/__tests__/src/rules/aria-role-test.js +118 -0
  29. package/__tests__/src/rules/aria-unsupported-elements-test.js +75 -0
  30. package/__tests__/src/rules/autocomplete-valid-test.js +77 -0
  31. package/__tests__/src/rules/click-events-have-key-events-test.js +77 -0
  32. package/__tests__/src/rules/control-has-associated-label-test.js +327 -0
  33. package/__tests__/src/rules/heading-has-content-test.js +85 -0
  34. package/__tests__/src/rules/html-has-lang-test.js +42 -0
  35. package/__tests__/src/rules/iframe-has-title-test.js +55 -0
  36. package/__tests__/src/rules/img-redundant-alt-test.js +137 -0
  37. package/__tests__/src/rules/interactive-supports-focus-test.js +267 -0
  38. package/__tests__/src/rules/label-has-associated-control-test.js +243 -0
  39. package/__tests__/src/rules/label-has-for-test.js +235 -0
  40. package/__tests__/src/rules/lang-test.js +59 -0
  41. package/__tests__/src/rules/media-has-caption-test.js +220 -0
  42. package/__tests__/src/rules/mouse-events-have-key-events-test.js +154 -0
  43. package/__tests__/src/rules/no-access-key-test.js +48 -0
  44. package/__tests__/src/rules/no-aria-hidden-on-focusable-test.js +44 -0
  45. package/__tests__/src/rules/no-autofocus-test.js +68 -0
  46. package/__tests__/src/rules/no-distracting-elements-test.js +51 -0
  47. package/__tests__/src/rules/no-interactive-element-to-noninteractive-role-test.js +405 -0
  48. package/__tests__/src/rules/no-noninteractive-element-interactions-test.js +502 -0
  49. package/__tests__/src/rules/no-noninteractive-element-to-interactive-role-test.js +500 -0
  50. package/__tests__/src/rules/no-noninteractive-tabindex-test.js +123 -0
  51. package/__tests__/src/rules/no-onchange-test.js +57 -0
  52. package/__tests__/src/rules/no-redundant-roles-test.js +98 -0
  53. package/__tests__/src/rules/no-static-element-interactions-test.js +501 -0
  54. package/__tests__/src/rules/prefer-tag-over-role-test.js +63 -0
  55. package/__tests__/src/rules/role-has-required-aria-props-test.js +134 -0
  56. package/__tests__/src/rules/role-supports-aria-props-test.js +570 -0
  57. package/__tests__/src/rules/scope-test.js +50 -0
  58. package/__tests__/src/rules/tabindex-no-positive-test.js +55 -0
  59. package/__tests__/src/util/attributesComparator-test.js +91 -0
  60. package/__tests__/src/util/getAccessibleChildText-test.js +174 -0
  61. package/__tests__/src/util/getComputedRole-test.js +71 -0
  62. package/__tests__/src/util/getElementType-test.js +154 -0
  63. package/__tests__/src/util/getExplicitRole-test.js +35 -0
  64. package/__tests__/src/util/getImplicitRole-test.js +25 -0
  65. package/__tests__/src/util/getSuggestion-test.js +33 -0
  66. package/__tests__/src/util/getTabIndex-test.js +85 -0
  67. package/__tests__/src/util/hasAccessibleChild-test.js +157 -0
  68. package/__tests__/src/util/implicitRoles/input-test.js +87 -0
  69. package/__tests__/src/util/implicitRoles/menu-test.js +20 -0
  70. package/__tests__/src/util/implicitRoles/menuitem-test.js +38 -0
  71. package/__tests__/src/util/isAbstractRole-test.js +51 -0
  72. package/__tests__/src/util/isContentEditable-test.js +52 -0
  73. package/__tests__/src/util/isDOMElement-test.js +30 -0
  74. package/__tests__/src/util/isDisabledElement-test.js +88 -0
  75. package/__tests__/src/util/isFocusable-test.js +111 -0
  76. package/__tests__/src/util/isInteractiveElement-test.js +104 -0
  77. package/__tests__/src/util/isInteractiveRole-test.js +59 -0
  78. package/__tests__/src/util/isNonInteractiveElement-test.js +97 -0
  79. package/__tests__/src/util/isNonInteractiveRole-test.js +59 -0
  80. package/__tests__/src/util/isNonLiteralProperty-test.js +52 -0
  81. package/__tests__/src/util/isSemanticRoleElement-test.js +72 -0
  82. package/__tests__/src/util/mayContainChildComponent-test.js +219 -0
  83. package/__tests__/src/util/mayHaveAccessibleLabel-test.js +256 -0
  84. package/__tests__/src/util/parserOptionsMapper-test.js +93 -0
  85. package/__tests__/src/util/schemas-test.js +35 -0
  86. package/docs/rules/accessible-emoji.md +30 -0
  87. package/docs/rules/alt-text.md +168 -0
  88. package/docs/rules/anchor-ambiguous-text.md +91 -0
  89. package/docs/rules/anchor-has-content.md +64 -0
  90. package/docs/rules/anchor-is-valid.md +270 -0
  91. package/docs/rules/aria-activedescendant-has-tabindex.md +52 -0
  92. package/docs/rules/aria-props.md +29 -0
  93. package/docs/rules/aria-proptypes.md +30 -0
  94. package/docs/rules/aria-role.md +51 -0
  95. package/docs/rules/aria-unsupported-elements.md +30 -0
  96. package/docs/rules/autocomplete-valid.md +49 -0
  97. package/docs/rules/click-events-have-key-events.md +28 -0
  98. package/docs/rules/control-has-associated-label.md +113 -0
  99. package/docs/rules/heading-has-content.md +67 -0
  100. package/docs/rules/html-has-lang.md +31 -0
  101. package/docs/rules/iframe-has-title.md +37 -0
  102. package/docs/rules/img-redundant-alt.md +48 -0
  103. package/docs/rules/interactive-supports-focus.md +156 -0
  104. package/docs/rules/label-has-associated-control.md +152 -0
  105. package/docs/rules/label-has-for.md +130 -0
  106. package/docs/rules/lang.md +31 -0
  107. package/docs/rules/media-has-caption.md +48 -0
  108. package/docs/rules/mouse-events-have-key-events.md +58 -0
  109. package/docs/rules/no-access-key.md +30 -0
  110. package/docs/rules/no-aria-hidden-on-focusable.md +37 -0
  111. package/docs/rules/no-autofocus.md +43 -0
  112. package/docs/rules/no-distracting-elements.md +41 -0
  113. package/docs/rules/no-interactive-element-to-noninteractive-role.md +73 -0
  114. package/docs/rules/no-noninteractive-element-interactions.md +145 -0
  115. package/docs/rules/no-noninteractive-element-to-interactive-role.md +76 -0
  116. package/docs/rules/no-noninteractive-tabindex.md +115 -0
  117. package/docs/rules/no-onchange.md +36 -0
  118. package/docs/rules/no-redundant-roles.md +46 -0
  119. package/docs/rules/no-static-element-interactions.md +114 -0
  120. package/docs/rules/prefer-tag-over-role.md +32 -0
  121. package/docs/rules/role-has-required-aria-props.md +31 -0
  122. package/docs/rules/role-supports-aria-props.md +39 -0
  123. package/docs/rules/scope.md +30 -0
  124. package/docs/rules/tabindex-no-positive.md +32 -0
  125. package/lib/configs/flat-config-base.js +11 -0
  126. package/lib/configs/legacy-config-base.js +9 -0
  127. package/lib/index.js +209 -0
  128. package/lib/rules/accessible-emoji.js +63 -0
  129. package/lib/rules/alt-text.js +218 -0
  130. package/lib/rules/anchor-ambiguous-text.js +64 -0
  131. package/lib/rules/anchor-has-content.js +60 -0
  132. package/lib/rules/anchor-is-valid.js +122 -0
  133. package/lib/rules/aria-activedescendant-has-tabindex.js +66 -0
  134. package/lib/rules/aria-props.js +59 -0
  135. package/lib/rules/aria-proptypes.js +114 -0
  136. package/lib/rules/aria-role.js +89 -0
  137. package/lib/rules/aria-unsupported-elements.js +64 -0
  138. package/lib/rules/autocomplete-valid.js +67 -0
  139. package/lib/rules/click-events-have-key-events.js +68 -0
  140. package/lib/rules/control-has-associated-label.js +103 -0
  141. package/lib/rules/heading-has-content.js +61 -0
  142. package/lib/rules/html-has-lang.js +50 -0
  143. package/lib/rules/iframe-has-title.js +50 -0
  144. package/lib/rules/img-redundant-alt.js +88 -0
  145. package/lib/rules/interactive-supports-focus.js +87 -0
  146. package/lib/rules/label-has-associated-control.js +127 -0
  147. package/lib/rules/label-has-for.js +150 -0
  148. package/lib/rules/lang.js +68 -0
  149. package/lib/rules/media-has-caption.js +96 -0
  150. package/lib/rules/mouse-events-have-key-events.js +94 -0
  151. package/lib/rules/no-access-key.js +43 -0
  152. package/lib/rules/no-aria-hidden-on-focusable.js +47 -0
  153. package/lib/rules/no-autofocus.js +62 -0
  154. package/lib/rules/no-distracting-elements.js +54 -0
  155. package/lib/rules/no-interactive-element-to-noninteractive-role.js +81 -0
  156. package/lib/rules/no-noninteractive-element-interactions.js +95 -0
  157. package/lib/rules/no-noninteractive-element-to-interactive-role.js +80 -0
  158. package/lib/rules/no-noninteractive-tabindex.js +109 -0
  159. package/lib/rules/no-onchange.js +52 -0
  160. package/lib/rules/no-redundant-roles.js +86 -0
  161. package/lib/rules/no-static-element-interactions.js +102 -0
  162. package/lib/rules/prefer-tag-over-role.js +75 -0
  163. package/lib/rules/role-has-required-aria-props.js +88 -0
  164. package/lib/rules/role-supports-aria-props.js +78 -0
  165. package/lib/rules/scope.js +58 -0
  166. package/lib/rules/tabindex-no-positive.js +53 -0
  167. package/lib/util/attributesComparator.js +34 -0
  168. package/lib/util/getAccessibleChildText.js +55 -0
  169. package/lib/util/getComputedRole.js +19 -0
  170. package/lib/util/getElementType.js +30 -0
  171. package/lib/util/getExplicitRole.js +27 -0
  172. package/lib/util/getImplicitRole.js +24 -0
  173. package/lib/util/getSuggestion.js +32 -0
  174. package/lib/util/getTabIndex.js +34 -0
  175. package/lib/util/hasAccessibleChild.js +30 -0
  176. package/lib/util/implicitRoles/a.js +17 -0
  177. package/lib/util/implicitRoles/area.js +17 -0
  178. package/lib/util/implicitRoles/article.js +13 -0
  179. package/lib/util/implicitRoles/aside.js +13 -0
  180. package/lib/util/implicitRoles/body.js +13 -0
  181. package/lib/util/implicitRoles/button.js +13 -0
  182. package/lib/util/implicitRoles/datalist.js +13 -0
  183. package/lib/util/implicitRoles/details.js +13 -0
  184. package/lib/util/implicitRoles/dialog.js +13 -0
  185. package/lib/util/implicitRoles/form.js +13 -0
  186. package/lib/util/implicitRoles/h1.js +13 -0
  187. package/lib/util/implicitRoles/h2.js +13 -0
  188. package/lib/util/implicitRoles/h3.js +13 -0
  189. package/lib/util/implicitRoles/h4.js +13 -0
  190. package/lib/util/implicitRoles/h5.js +13 -0
  191. package/lib/util/implicitRoles/h6.js +13 -0
  192. package/lib/util/implicitRoles/hr.js +13 -0
  193. package/lib/util/implicitRoles/img.js +31 -0
  194. package/lib/util/implicitRoles/index.js +82 -0
  195. package/lib/util/implicitRoles/input.js +38 -0
  196. package/lib/util/implicitRoles/li.js +13 -0
  197. package/lib/util/implicitRoles/link.js +17 -0
  198. package/lib/util/implicitRoles/menu.js +19 -0
  199. package/lib/util/implicitRoles/menuitem.js +28 -0
  200. package/lib/util/implicitRoles/meter.js +13 -0
  201. package/lib/util/implicitRoles/nav.js +13 -0
  202. package/lib/util/implicitRoles/ol.js +13 -0
  203. package/lib/util/implicitRoles/option.js +13 -0
  204. package/lib/util/implicitRoles/output.js +13 -0
  205. package/lib/util/implicitRoles/progress.js +13 -0
  206. package/lib/util/implicitRoles/section.js +13 -0
  207. package/lib/util/implicitRoles/select.js +13 -0
  208. package/lib/util/implicitRoles/tbody.js +13 -0
  209. package/lib/util/implicitRoles/textarea.js +13 -0
  210. package/lib/util/implicitRoles/tfoot.js +13 -0
  211. package/lib/util/implicitRoles/thead.js +13 -0
  212. package/lib/util/implicitRoles/ul.js +13 -0
  213. package/lib/util/isAbstractRole.js +23 -0
  214. package/lib/util/isContentEditable.js +13 -0
  215. package/lib/util/isDOMElement.js +15 -0
  216. package/lib/util/isDisabledElement.js +23 -0
  217. package/lib/util/isFocusable.js +23 -0
  218. package/lib/util/isHiddenFromScreenReader.js +26 -0
  219. package/lib/util/isInteractiveElement.js +116 -0
  220. package/lib/util/isInteractiveRole.js +54 -0
  221. package/lib/util/isNonInteractiveElement.js +131 -0
  222. package/lib/util/isNonInteractiveRole.js +55 -0
  223. package/lib/util/isNonLiteralProperty.js +29 -0
  224. package/lib/util/isPresentationRole.js +13 -0
  225. package/lib/util/isSemanticRoleElement.js +54 -0
  226. package/lib/util/mayContainChildComponent.js +50 -0
  227. package/lib/util/mayHaveAccessibleLabel.js +95 -0
  228. package/lib/util/schemas.js +52 -0
  229. package/package.json +120 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @fileoverview Enforce img alt attribute does not have the word image, picture, or photo.
3
+ * @author Ethan Cohen
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { RuleTester } from 'eslint';
11
+ import semver from 'semver';
12
+ import { version as eslintVersion } from 'eslint/package.json';
13
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
14
+ import parsers from '../../__util__/helpers/parsers';
15
+ import rule from '../../../src/rules/img-redundant-alt';
16
+
17
+ // -----------------------------------------------------------------------------
18
+ // Tests
19
+ // -----------------------------------------------------------------------------
20
+
21
+ const array = [{
22
+ components: ['Image'],
23
+ words: ['Word1', 'Word2'],
24
+ }];
25
+
26
+ const componentsSettings = {
27
+ 'jsx-a11y': {
28
+ components: {
29
+ Image: 'img',
30
+ },
31
+ },
32
+ };
33
+
34
+ const ruleTester = new RuleTester();
35
+
36
+ const expectedError = {
37
+ message: 'Redundant alt attribute. Screen-readers already announce `img` tags as an image. You don’t need to use the words `image`, `photo,` or `picture` (or any specified custom words) in the alt prop.',
38
+ type: 'JSXOpeningElement',
39
+ };
40
+
41
+ ruleTester.run('img-redundant-alt', rule, {
42
+ valid: parsers.all([].concat(
43
+ { code: '<img alt="foo" />;' },
44
+ { code: '<img alt="picture of me taking a photo of an image" aria-hidden />' },
45
+ { code: '<img aria-hidden alt="photo of image" />' },
46
+ { code: '<img ALt="foo" />;' },
47
+ { code: '<img {...this.props} alt="foo" />' },
48
+ { code: '<img {...this.props} alt={"foo"} />' },
49
+ { code: '<img {...this.props} alt={alt} />' },
50
+ { code: '<a />' },
51
+ { code: '<img />' },
52
+ { code: '<IMG />' },
53
+ { code: '<img alt={undefined} />' },
54
+ { code: '<img alt={`this should pass for ${now}`} />' },
55
+ { code: '<img alt={`this should pass for ${photo}`} />' },
56
+ { code: '<img alt={`this should pass for ${image}`} />' },
57
+ { code: '<img alt={`this should pass for ${picture}`} />' },
58
+ { code: '<img alt={`${photo}`} />' },
59
+ { code: '<img alt={`${image}`} />' },
60
+ { code: '<img alt={`${picture}`} />' },
61
+ { code: '<img alt={"undefined"} />' },
62
+ { code: '<img alt={() => {}} />' },
63
+ { code: '<img alt={function(e){}} />' },
64
+ { code: '<img aria-hidden={false} alt="Doing cool things." />' },
65
+ { code: '<UX.Layout>test</UX.Layout>' },
66
+ { code: '<img alt />' },
67
+ { code: '<img alt={imageAlt} />' },
68
+ { code: '<img alt={imageAlt.name} />' },
69
+ semver.satisfies(eslintVersion, '>= 6') ? [
70
+ { code: '<img alt={imageAlt?.name} />', languageOptions: { ecmaVersion: 2020 } },
71
+ { code: '<img alt="Doing cool things" aria-hidden={foo?.bar}/>', languageOptions: { ecmaVersion: 2020 } },
72
+ ] : [],
73
+ { code: '<img alt="Photography" />;' },
74
+ { code: '<img alt="ImageMagick" />;' },
75
+ { code: '<Image alt="Photo of a friend" />' },
76
+ { code: '<Image alt="Foo" />', settings: componentsSettings },
77
+ { code: '<img alt="画像" />', options: [{ words: ['イメージ'] }] },
78
+ )).map(parserOptionsMapper),
79
+ invalid: parsers.all([].concat(
80
+ { code: '<img alt="Photo of friend." />;', errors: [expectedError] },
81
+ { code: '<img alt="Picture of friend." />;', errors: [expectedError] },
82
+ { code: '<img alt="Image of friend." />;', errors: [expectedError] },
83
+ { code: '<img alt="PhOtO of friend." />;', errors: [expectedError] },
84
+ { code: '<img alt={"photo"} />;', errors: [expectedError] },
85
+ { code: '<img alt="piCTUre of friend." />;', errors: [expectedError] },
86
+ { code: '<img alt="imAGE of friend." />;', errors: [expectedError] },
87
+ {
88
+ code: '<img alt="photo of cool person" aria-hidden={false} />',
89
+ errors: [expectedError],
90
+ },
91
+ {
92
+ code: '<img alt="picture of cool person" aria-hidden={false} />',
93
+ errors: [expectedError],
94
+ },
95
+ {
96
+ code: '<img alt="image of cool person" aria-hidden={false} />',
97
+ errors: [expectedError],
98
+ },
99
+ { code: '<img alt="photo" {...this.props} />', errors: [expectedError] },
100
+ { code: '<img alt="image" {...this.props} />', errors: [expectedError] },
101
+ { code: '<img alt="picture" {...this.props} />', errors: [expectedError] },
102
+ {
103
+ code: '<img alt={`picture doing ${things}`} {...this.props} />',
104
+ errors: [expectedError],
105
+ },
106
+ {
107
+ code: '<img alt={`photo doing ${things}`} {...this.props} />',
108
+ errors: [expectedError],
109
+ },
110
+ {
111
+ code: '<img alt={`image doing ${things}`} {...this.props} />',
112
+ errors: [expectedError],
113
+ },
114
+ {
115
+ code: '<img alt={`picture doing ${picture}`} {...this.props} />',
116
+ errors: [expectedError],
117
+ },
118
+ {
119
+ code: '<img alt={`photo doing ${photo}`} {...this.props} />',
120
+ errors: [expectedError],
121
+ },
122
+ {
123
+ code: '<img alt={`image doing ${image}`} {...this.props} />',
124
+ errors: [expectedError],
125
+ },
126
+ { code: '<Image alt="Photo of a friend" />', errors: [expectedError], settings: componentsSettings },
127
+
128
+ // TESTS FOR ARRAY OPTION TESTS
129
+ { code: '<img alt="Word1" />;', options: array, errors: [expectedError] },
130
+ { code: '<img alt="Word2" />;', options: array, errors: [expectedError] },
131
+ { code: '<Image alt="Word1" />;', options: array, errors: [expectedError] },
132
+ { code: '<Image alt="Word2" />;', options: array, errors: [expectedError] },
133
+
134
+ { code: '<img alt="イメージ" />', options: [{ words: ['イメージ'] }], errors: [expectedError] },
135
+ { code: '<img alt="イメージです" />', options: [{ words: ['イメージ'] }], errors: [expectedError] },
136
+ )).map(parserOptionsMapper),
137
+ });
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @fileoverview Enforce that elements with onClick handlers must be focusable.
3
+ * @author Ethan Cohen
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import includes from 'array-includes';
11
+ import { RuleTester } from 'eslint';
12
+ import {
13
+ eventHandlers,
14
+ eventHandlersByType,
15
+ } from 'jsx-ast-utils';
16
+ import { configs } from '../../../src/index';
17
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
18
+ import parsers from '../../__util__/helpers/parsers';
19
+ import rule from '../../../src/rules/interactive-supports-focus';
20
+ import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
21
+
22
+ // -----------------------------------------------------------------------------
23
+ // Tests
24
+ // -----------------------------------------------------------------------------
25
+
26
+ const ruleTester = new RuleTester();
27
+
28
+ function template(strings, ...keys) {
29
+ return (...values) => keys.reduce(
30
+ (acc, k, i) => acc + (values[k] || '') + strings[i + 1],
31
+ strings[0],
32
+ );
33
+ }
34
+
35
+ const ruleName = 'interactive-supports-focus';
36
+ const type = 'JSXOpeningElement';
37
+ const codeTemplate = template`<${0} role="${1}" ${2}={() => void 0} />`;
38
+ const tabindexTemplate = template`<${0} role="${1}" ${2}={() => void 0} tabIndex="0" />`;
39
+ const tabbableTemplate = template`Elements with the '${0}' interactive role must be tabbable.`;
40
+ const focusableTemplate = template`Elements with the '${0}' interactive role must be focusable.`;
41
+
42
+ const componentsSettings = {
43
+ 'jsx-a11y': {
44
+ components: {
45
+ Div: 'div',
46
+ },
47
+ },
48
+ };
49
+
50
+ const buttonError = { message: tabbableTemplate('button'), type };
51
+
52
+ const recommendedOptions = configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {};
53
+
54
+ const strictOptions = configs.strict.rules[`jsx-a11y/${ruleName}`][1] || {};
55
+
56
+ const alwaysValid = [
57
+ { code: '<div />' },
58
+ { code: '<div aria-hidden onClick={() => void 0} />' },
59
+ { code: '<div aria-hidden={true == true} onClick={() => void 0} />' },
60
+ { code: '<div aria-hidden={true === true} onClick={() => void 0} />' },
61
+ { code: '<div aria-hidden={hidden !== false} onClick={() => void 0} />' },
62
+ { code: '<div aria-hidden={hidden != false} onClick={() => void 0} />' },
63
+ { code: '<div aria-hidden={1 < 2} onClick={() => void 0} />' },
64
+ { code: '<div aria-hidden={1 <= 2} onClick={() => void 0} />' },
65
+ { code: '<div aria-hidden={2 > 1} onClick={() => void 0} />' },
66
+ { code: '<div aria-hidden={2 >= 1} onClick={() => void 0} />' },
67
+ { code: '<div onClick={() => void 0} />;' },
68
+ { code: '<div onClick={() => void 0} tabIndex={undefined} />;' },
69
+ { code: '<div onClick={() => void 0} tabIndex="bad" />;' },
70
+ { code: '<div onClick={() => void 0} role={undefined} />;' },
71
+ { code: '<div role="section" onClick={() => void 0} />' },
72
+ { code: '<div onClick={() => void 0} aria-hidden={false} />;' },
73
+ { code: '<div onClick={() => void 0} {...props} />;' },
74
+ { code: '<input type="text" onClick={() => void 0} />' },
75
+ { code: '<input type="hidden" onClick={() => void 0} tabIndex="-1" />' },
76
+ { code: '<input type="hidden" onClick={() => void 0} tabIndex={-1} />' },
77
+ { code: '<input onClick={() => void 0} />' },
78
+ { code: '<input onClick={() => void 0} role="combobox" />' },
79
+ { code: '<button onClick={() => void 0} className="foo" />' },
80
+ { code: '<option onClick={() => void 0} className="foo" />' },
81
+ { code: '<select onClick={() => void 0} className="foo" />' },
82
+ { code: '<area href="#" onClick={() => void 0} className="foo" />' },
83
+ { code: '<area onClick={() => void 0} className="foo" />' },
84
+ { code: '<summary onClick={() => void 0} />' },
85
+ { code: '<textarea onClick={() => void 0} className="foo" />' },
86
+ { code: '<a onClick="showNextPage();">Next page</a>' },
87
+ { code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>' },
88
+ { code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>' },
89
+ { code: '<a onClick={() => void 0} />' },
90
+ { code: '<a tabIndex="0" onClick={() => void 0} />' },
91
+ { code: '<a tabIndex={dynamicTabIndex} onClick={() => void 0} />' },
92
+ { code: '<a tabIndex={0} onClick={() => void 0} />' },
93
+ { code: '<a role="button" href="#" onClick={() => void 0} />' },
94
+ { code: '<a onClick={() => void 0} href="http://x.y.z" />' },
95
+ { code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
96
+ { code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex={0} />' },
97
+ { code: '<a onClick={() => void 0} href="http://x.y.z" role="button" />' },
98
+ { code: '<TestComponent onClick={doFoo} />' },
99
+ { code: '<input onClick={() => void 0} type="hidden" />;' },
100
+ { code: '<span onClick="submitForm();">Submit</span>' },
101
+ { code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>' },
102
+ { code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>' },
103
+ { code: '<span onClick="doSomething();" tabIndex="0">Click me!</span>' },
104
+ { code: '<span onClick="doSomething();" tabIndex={0}>Click me!</span>' },
105
+ { code: '<span onClick="doSomething();" tabIndex="-1">Click me too!</span>' },
106
+ {
107
+ code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>',
108
+ },
109
+ { code: '<section onClick={() => void 0} />;' },
110
+ { code: '<main onClick={() => void 0} />;' },
111
+ { code: '<article onClick={() => void 0} />;' },
112
+ { code: '<header onClick={() => void 0} />;' },
113
+ { code: '<footer onClick={() => void 0} />;' },
114
+ { code: '<div role="button" tabIndex="0" onClick={() => void 0} />' },
115
+ { code: '<div role="checkbox" tabIndex="0" onClick={() => void 0} />' },
116
+ { code: '<div role="link" tabIndex="0" onClick={() => void 0} />' },
117
+ { code: '<div role="menuitem" tabIndex="0" onClick={() => void 0} />' },
118
+ { code: '<div role="menuitemcheckbox" tabIndex="0" onClick={() => void 0} />' },
119
+ { code: '<div role="menuitemradio" tabIndex="0" onClick={() => void 0} />' },
120
+ { code: '<div role="option" tabIndex="0" onClick={() => void 0} />' },
121
+ { code: '<div role="radio" tabIndex="0" onClick={() => void 0} />' },
122
+ { code: '<div role="spinbutton" tabIndex="0" onClick={() => void 0} />' },
123
+ { code: '<div role="switch" tabIndex="0" onClick={() => void 0} />' },
124
+ { code: '<div role="tablist" tabIndex="0" onClick={() => void 0} />' },
125
+ { code: '<div role="tab" tabIndex="0" onClick={() => void 0} />' },
126
+ { code: '<div role="textbox" tabIndex="0" onClick={() => void 0} />' },
127
+ { code: '<div role="textbox" aria-disabled="true" onClick={() => void 0} />' },
128
+ { code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;' },
129
+ { code: '<Input onClick={() => void 0} type="hidden" />;' },
130
+ { code: '<Div onClick={() => void 0} role="button" tabIndex="0" />', settings: componentsSettings },
131
+ ];
132
+
133
+ const neverValid = [
134
+ { code: '<Div onClick={() => void 0} role="button" />', errors: [buttonError], settings: componentsSettings },
135
+ ];
136
+
137
+ const interactiveRoles = [
138
+ 'button',
139
+ 'checkbox',
140
+ 'link',
141
+ 'gridcell',
142
+ 'menuitem',
143
+ 'menuitemcheckbox',
144
+ 'menuitemradio',
145
+ 'option',
146
+ 'radio',
147
+ 'searchbox',
148
+ 'slider',
149
+ 'spinbutton',
150
+ 'switch',
151
+ 'tab',
152
+ 'textbox',
153
+ 'treeitem',
154
+ ];
155
+
156
+ const recommendedRoles = [
157
+ 'button',
158
+ 'checkbox',
159
+ 'link',
160
+ 'searchbox',
161
+ 'spinbutton',
162
+ 'switch',
163
+ 'textbox',
164
+ ];
165
+
166
+ const strictRoles = [
167
+ 'button',
168
+ 'checkbox',
169
+ 'link',
170
+ 'progressbar',
171
+ 'searchbox',
172
+ 'slider',
173
+ 'spinbutton',
174
+ 'switch',
175
+ 'textbox',
176
+ ];
177
+
178
+ const staticElements = [
179
+ 'div',
180
+ ];
181
+
182
+ const triggeringHandlers = [
183
+ ...eventHandlersByType.mouse,
184
+ ...eventHandlersByType.keyboard,
185
+ ];
186
+
187
+ const passReducer = (roles, handlers, messageTemplate) => (
188
+ staticElements.reduce((elementAcc, element) => (
189
+ elementAcc.concat(roles.reduce((roleAcc, role) => (
190
+ roleAcc.concat(handlers.map((handler) => ({
191
+ code: messageTemplate(element, role, handler),
192
+ })))
193
+ ), []))
194
+ ), [])
195
+ );
196
+
197
+ const failReducer = (roles, handlers, messageTemplate) => (
198
+ staticElements.reduce((elementAcc, element) => (
199
+ elementAcc.concat(roles.reduce((roleAcc, role) => (
200
+ roleAcc.concat(handlers.map((handler) => ({
201
+ code: codeTemplate(element, role, handler),
202
+ errors: [{
203
+ type,
204
+ message: messageTemplate(role),
205
+ }],
206
+ })))
207
+ ), []))
208
+ ), [])
209
+ );
210
+
211
+ ruleTester.run(`${ruleName}:recommended`, rule, {
212
+ valid: parsers.all([].concat(
213
+ ...alwaysValid,
214
+ ...passReducer(
215
+ interactiveRoles,
216
+ eventHandlers.filter((handler) => !includes(triggeringHandlers, handler)),
217
+ codeTemplate,
218
+ ),
219
+ ...passReducer(
220
+ interactiveRoles.filter((role) => !includes(recommendedRoles, role)),
221
+ eventHandlers.filter((handler) => includes(triggeringHandlers, handler)),
222
+ tabindexTemplate,
223
+ ),
224
+ ))
225
+ .map(ruleOptionsMapperFactory(recommendedOptions))
226
+ .map(parserOptionsMapper),
227
+ invalid: parsers.all([].concat(
228
+ ...neverValid,
229
+ ...failReducer(recommendedRoles, triggeringHandlers, tabbableTemplate),
230
+ ...failReducer(
231
+ interactiveRoles.filter((role) => !includes(recommendedRoles, role)),
232
+ triggeringHandlers,
233
+ focusableTemplate,
234
+ ),
235
+ ))
236
+ .map(ruleOptionsMapperFactory(recommendedOptions))
237
+ .map(parserOptionsMapper),
238
+ });
239
+
240
+ ruleTester.run(`${ruleName}:strict`, rule, {
241
+ valid: parsers.all([].concat(
242
+ ...alwaysValid,
243
+ ...passReducer(
244
+ interactiveRoles,
245
+ eventHandlers.filter((handler) => !includes(triggeringHandlers, handler)),
246
+ codeTemplate,
247
+ ),
248
+ ...passReducer(
249
+ interactiveRoles.filter((role) => !includes(strictRoles, role)),
250
+ eventHandlers.filter((handler) => includes(triggeringHandlers, handler)),
251
+ tabindexTemplate,
252
+ ),
253
+ ))
254
+ .map(ruleOptionsMapperFactory(strictOptions))
255
+ .map(parserOptionsMapper),
256
+ invalid: parsers.all([].concat(
257
+ ...neverValid,
258
+ ...failReducer(strictRoles, triggeringHandlers, tabbableTemplate),
259
+ ...failReducer(
260
+ interactiveRoles.filter((role) => !includes(strictRoles, role)),
261
+ triggeringHandlers,
262
+ focusableTemplate,
263
+ ),
264
+ ))
265
+ .map(ruleOptionsMapperFactory(strictOptions))
266
+ .map(parserOptionsMapper),
267
+ });
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @fileoverview Enforce label tags have an associated control.
3
+ * @author Jesse Beach
4
+ */
5
+
6
+ // -----------------------------------------------------------------------------
7
+ // Requirements
8
+ // -----------------------------------------------------------------------------
9
+
10
+ import { RuleTester } from 'eslint';
11
+ import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12
+ import parsers from '../../__util__/helpers/parsers';
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 expectedErrorNoLabel = {
30
+ message: 'A form label must have accessible text.',
31
+ type: 'JSXOpeningElement',
32
+ };
33
+
34
+ const componentsSettings = {
35
+ 'jsx-a11y': {
36
+ components: {
37
+ CustomInput: 'input',
38
+ CustomLabel: 'label',
39
+ },
40
+ },
41
+ };
42
+
43
+ const attributesSettings = {
44
+ 'jsx-a11y': {
45
+ attributes: {
46
+ for: ['htmlFor', 'for'],
47
+ },
48
+ },
49
+ };
50
+
51
+ const htmlForValid = [
52
+ { code: '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }] },
53
+ { code: '<label htmlFor="js_id" aria-label="A label" />' },
54
+ { code: '<label htmlFor="js_id" aria-labelledby="A label" />' },
55
+ { code: '<div><label htmlFor="js_id">A label</label><input id="js_id" /></div>' },
56
+ { code: '<label for="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }], settings: attributesSettings },
57
+ { code: '<label for="js_id" aria-label="A label" />', settings: attributesSettings },
58
+ { code: '<label for="js_id" aria-labelledby="A label" />', settings: attributesSettings },
59
+ { code: '<div><label for="js_id">A label</label><input id="js_id" /></div>', settings: attributesSettings },
60
+ // Custom label component.
61
+ { code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }] },
62
+ { code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
63
+ { code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', settings: componentsSettings },
64
+ // Custom label attributes.
65
+ { code: '<label htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'] }] },
66
+ // Glob support for controlComponents option.
67
+ { code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ controlComponents: ['Custom*'] }] },
68
+ { code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ controlComponents: ['*Label'] }] },
69
+ // Rule does not error if presence of accessible label cannot be determined
70
+ { code: '<div><label htmlFor="js_id"><CustomText /></label><input id="js_id" /></div>' },
71
+ ];
72
+ const nestingValid = [
73
+ { code: '<label>A label<input /></label>' },
74
+ { code: '<label>A label<textarea /></label>' },
75
+ { code: '<label><img alt="A label" /><input /></label>' },
76
+ { code: '<label><img aria-label="A label" /><input /></label>' },
77
+ { code: '<label><span>A label<input /></span></label>' },
78
+ { code: '<label><span><span>A label<input /></span></span></label>', options: [{ depth: 3 }] },
79
+ { code: '<label><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }] },
80
+ { code: '<label><span><span><span><span>A label</span><input /></span></span></span></label>', options: [{ depth: 5 }] },
81
+ { code: '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>', options: [{ depth: 5 }] },
82
+ { code: '<label><span><span><span><input aria-label="A label" /></span></span></span></label>', options: [{ depth: 5 }] },
83
+ // Other controls
84
+ { code: '<label>foo<meter /></label>' },
85
+ { code: '<label>foo<output /></label>' },
86
+ { code: '<label>foo<progress /></label>' },
87
+ { code: '<label>foo<textarea /></label>' },
88
+ // Custom controlComponents.
89
+ { code: '<label>A label<CustomInput /></label>', options: [{ controlComponents: ['CustomInput'] }] },
90
+ { code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }] },
91
+ { code: '<label><span>A label<CustomInput /></span></label>', settings: componentsSettings },
92
+ { code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }] },
93
+ { code: '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }] },
94
+ // Glob support for controlComponents option.
95
+ { code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['Custom*'] }] },
96
+ { code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['*Input'] }] },
97
+ // Rule does not error if presence of accessible label cannot be determined
98
+ { code: '<label><CustomText /><input /></label>' },
99
+ ];
100
+
101
+ const bothValid = [
102
+ { code: '<label htmlFor="js_id"><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }] },
103
+ { code: '<label htmlFor="js_id" aria-label="A label"><input /></label>' },
104
+ { code: '<label htmlFor="js_id" aria-labelledby="A label"><input /></label>' },
105
+ { code: '<label htmlFor="js_id" aria-labelledby="A label"><textarea /></label>' },
106
+ // Custom label component.
107
+ { code: '<CustomLabel htmlFor="js_id" aria-label="A label"><input /></CustomLabel>', options: [{ labelComponents: ['CustomLabel'] }] },
108
+ { code: '<CustomLabel htmlFor="js_id" label="A label"><input /></CustomLabel>', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
109
+ { code: '<CustomLabel htmlFor="js_id" aria-label="A label"><input /></CustomLabel>', settings: componentsSettings },
110
+ { code: '<CustomLabel htmlFor="js_id" aria-label="A label"><CustomInput /></CustomLabel>', settings: componentsSettings },
111
+ // Custom label attributes.
112
+ { code: '<label htmlFor="js_id" label="A label"><input /></label>', options: [{ labelAttributes: ['label'] }] },
113
+ { code: '<label htmlFor="selectInput">Some text<select id="selectInput" /></label>' },
114
+ ];
115
+
116
+ const alwaysValid = [
117
+ { code: '<div />' },
118
+ { code: '<CustomElement />' },
119
+ { code: '<input type="hidden" />' },
120
+ ];
121
+
122
+ const htmlForInvalid = [
123
+ { code: '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }], errors: [expectedError] },
124
+ { code: '<label htmlFor="js_id" aria-label="A label" />', errors: [expectedError] },
125
+ { code: '<label htmlFor="js_id" aria-labelledby="A label" />', errors: [expectedError] },
126
+ // Custom label component.
127
+ { code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }], errors: [expectedError] },
128
+ { code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
129
+ { code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', settings: componentsSettings, errors: [expectedError] },
130
+ // Custom label attributes.
131
+ { code: '<label htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'] }], errors: [expectedError] },
132
+ ];
133
+ const nestingInvalid = [
134
+ { code: '<label>A label<input /></label>', errors: [expectedError] },
135
+ { code: '<label>A label<textarea /></label>', errors: [expectedError] },
136
+ { code: '<label><img alt="A label" /><input /></label>', errors: [expectedError] },
137
+ { code: '<label><img aria-label="A label" /><input /></label>', errors: [expectedError] },
138
+ { code: '<label><span>A label<input /></span></label>', errors: [expectedError] },
139
+ { code: '<label><span><span>A label<input /></span></span></label>', options: [{ depth: 3 }], errors: [expectedError] },
140
+ { code: '<label><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }], errors: [expectedError] },
141
+ { code: '<label><span><span><span><span>A label</span><input /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
142
+ { code: '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
143
+ { code: '<label><span><span><span><input aria-label="A label" /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
144
+ // Custom controlComponents.
145
+ { code: '<label>A label<OtherCustomInput /></label>', options: [{ controlComponents: ['CustomInput'] }], errors: [expectedError] },
146
+ { code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }], errors: [expectedError] },
147
+ { code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
148
+ { code: '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }], errors: [expectedError] },
149
+ { code: '<label><span>A label<CustomInput /></span></label>', settings: componentsSettings, errors: [expectedError] },
150
+ { code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', settings: componentsSettings, errors: [expectedError] },
151
+ ];
152
+
153
+ const neverValid = [
154
+ { code: '<label htmlFor="js_id" />', errors: [expectedErrorNoLabel] },
155
+ { code: '<label htmlFor="js_id"><input /></label>', errors: [expectedErrorNoLabel] },
156
+ { code: '<label htmlFor="js_id"><textarea /></label>', errors: [expectedErrorNoLabel] },
157
+ { code: '<label></label>', errors: [expectedErrorNoLabel] },
158
+ { code: '<label>A label</label>', errors: [expectedError] },
159
+ { code: '<div><label /><input /></div>', errors: [expectedErrorNoLabel] },
160
+ { code: '<div><label>A label</label><input /></div>', errors: [expectedError] },
161
+ // Custom label component.
162
+ { code: '<CustomLabel aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }], errors: [expectedError] },
163
+ { code: '<CustomLabel label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
164
+ { code: '<CustomLabel aria-label="A label" />', settings: componentsSettings, errors: [expectedError] },
165
+ // Custom label attributes.
166
+ { code: '<label label="A label" />', options: [{ labelAttributes: ['label'] }], errors: [expectedError] },
167
+ // Custom controlComponents.
168
+ { code: '<label><span><CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }], errors: [expectedErrorNoLabel] },
169
+ { code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }], errors: [expectedErrorNoLabel] },
170
+ { code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }], errors: [expectedErrorNoLabel] },
171
+ { code: '<label><span><CustomInput /></span></label>', settings: componentsSettings, errors: [expectedErrorNoLabel] },
172
+ { code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', settings: componentsSettings, errors: [expectedErrorNoLabel] },
173
+ ];
174
+ // htmlFor valid
175
+ ruleTester.run(ruleName, rule, {
176
+ valid: parsers.all([].concat(
177
+ ...alwaysValid,
178
+ ...htmlForValid,
179
+ ))
180
+ .map(ruleOptionsMapperFactory({
181
+ assert: 'htmlFor',
182
+ }))
183
+ .map(parserOptionsMapper),
184
+ invalid: parsers.all([].concat(
185
+ ...neverValid,
186
+ ...nestingInvalid,
187
+ ))
188
+ .map(ruleOptionsMapperFactory({
189
+ assert: 'htmlFor',
190
+ }))
191
+ .map(parserOptionsMapper),
192
+ });
193
+
194
+ // nesting valid
195
+ ruleTester.run(ruleName, rule, {
196
+ valid: parsers.all([].concat(
197
+ ...alwaysValid,
198
+ ...nestingValid,
199
+ ))
200
+ .map(ruleOptionsMapperFactory({
201
+ assert: 'nesting',
202
+ }))
203
+ .map(parserOptionsMapper),
204
+ invalid: parsers.all([].concat(
205
+ ...neverValid,
206
+ ...htmlForInvalid,
207
+ ))
208
+ .map(ruleOptionsMapperFactory({
209
+ assert: 'nesting',
210
+ }))
211
+ .map(parserOptionsMapper),
212
+ });
213
+
214
+ // either valid
215
+ ruleTester.run(ruleName, rule, {
216
+ valid: parsers.all([].concat(
217
+ ...alwaysValid,
218
+ ...htmlForValid,
219
+ ...nestingValid,
220
+ ))
221
+ .map(ruleOptionsMapperFactory({
222
+ assert: 'either',
223
+ }))
224
+ .map(parserOptionsMapper),
225
+ invalid: parsers.all([].concat(
226
+ ...neverValid,
227
+ )).map(parserOptionsMapper),
228
+ });
229
+
230
+ // both valid
231
+ ruleTester.run(ruleName, rule, {
232
+ valid: parsers.all([].concat(
233
+ ...alwaysValid,
234
+ ...bothValid,
235
+ ))
236
+ .map(ruleOptionsMapperFactory({
237
+ assert: 'both',
238
+ }))
239
+ .map(parserOptionsMapper),
240
+ invalid: parsers.all([].concat(
241
+ ...neverValid,
242
+ )).map(parserOptionsMapper),
243
+ });