@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,291 @@
1
+ /**
2
+ * @fileoverview Enforce all elements that require alternative text have it.
3
+ * @author Ethan Cohen
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/alt-text';
14
+
15
+ // -----------------------------------------------------------------------------
16
+ // Tests
17
+ // -----------------------------------------------------------------------------
18
+
19
+ const ruleTester = new RuleTester();
20
+
21
+ const missingPropError = (type) => ({
22
+ message: `${type} elements must have an alt prop, either with meaningful text, or an empty string for decorative images.`,
23
+ type: 'JSXOpeningElement',
24
+ });
25
+
26
+ const altValueError = (type) => ({
27
+ message: `Invalid alt value for ${type}. \
28
+ Use alt="" for presentational images.`,
29
+ type: 'JSXOpeningElement',
30
+ });
31
+
32
+ const ariaLabelValueError = {
33
+ message: 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.',
34
+ };
35
+ const ariaLabelledbyValueError = {
36
+ message: 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.',
37
+ };
38
+
39
+ const preferAltError = () => ({
40
+ message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.',
41
+ type: 'JSXOpeningElement',
42
+ });
43
+
44
+ const objectError = {
45
+ message: 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.',
46
+ };
47
+
48
+ const areaError = {
49
+ message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
50
+ };
51
+
52
+ const inputImageError = {
53
+ message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
54
+ };
55
+
56
+ const componentsSettings = {
57
+ 'jsx-a11y': {
58
+ polymorphicPropName: 'as',
59
+ components: {
60
+ Input: 'input',
61
+ },
62
+ },
63
+ };
64
+
65
+ const array = [{
66
+ img: ['Thumbnail', 'Image'],
67
+ object: ['Object'],
68
+ area: ['Area'],
69
+ 'input[type="image"]': ['InputImage'],
70
+ }];
71
+
72
+ ruleTester.run('alt-text', rule, {
73
+ valid: parsers.all([].concat(
74
+ // DEFAULT ELEMENT 'img' TESTS
75
+ { code: '<img alt="foo" />;' },
76
+ { code: '<img alt={"foo"} />;' },
77
+ { code: '<img alt={alt} />;' },
78
+ { code: '<img ALT="foo" />;' },
79
+ { code: '<img ALT={`This is the ${alt} text`} />;' },
80
+ { code: '<img ALt="foo" />;' },
81
+ { code: '<img alt="foo" salt={undefined} />;' },
82
+ { code: '<img {...this.props} alt="foo" />' },
83
+ { code: '<a />' },
84
+ { code: '<div />' },
85
+ { code: '<img alt={function(e) {} } />' },
86
+ { code: '<div alt={function(e) {} } />' },
87
+ { code: '<img alt={() => void 0} />' },
88
+ { code: '<IMG />' },
89
+ { code: '<UX.Layout>test</UX.Layout>' },
90
+ { code: '<img alt={alt || "Alt text" } />' },
91
+ { code: '<img alt={photo.caption} />;' },
92
+ { code: '<img alt={bar()} />;' },
93
+ { code: '<img alt={foo.bar || ""} />' },
94
+ { code: '<img alt={bar() || ""} />' },
95
+ { code: '<img alt={foo.bar() || ""} />' },
96
+ { code: '<img alt="" />' },
97
+ { code: '<img alt={`${undefined}`} />' },
98
+ { code: '<img alt=" " />' },
99
+ { code: '<img alt="" role="presentation" />' },
100
+ { code: '<img alt="" role="none" />' },
101
+ { code: '<img alt="" role={`presentation`} />' },
102
+ { code: '<img alt="" role={"presentation"} />' },
103
+ { code: '<img alt="this is lit..." role="presentation" />' },
104
+ { code: '<img alt={error ? "not working": "working"} />' },
105
+ { code: '<img alt={undefined ? "working": "not working"} />' },
106
+ { code: '<img alt={plugin.name + " Logo"} />' },
107
+ { code: '<img aria-label="foo" />' },
108
+ { code: '<img aria-labelledby="id1" />' },
109
+
110
+ // DEFAULT <object> TESTS
111
+ { code: '<object aria-label="foo" />' },
112
+ { code: '<object aria-labelledby="id1" />' },
113
+ { code: '<object>Foo</object>' },
114
+ { code: '<object><p>This is descriptive!</p></object>' },
115
+ { code: '<Object />' },
116
+ { code: '<object title="An object" />' },
117
+
118
+ // DEFAULT <area> TESTS
119
+ { code: '<area aria-label="foo" />' },
120
+ { code: '<area aria-labelledby="id1" />' },
121
+ { code: '<area alt="" />' },
122
+ { code: '<area alt="This is descriptive!" />' },
123
+ { code: '<area alt={altText} />' },
124
+ { code: '<Area />' },
125
+
126
+ // DEFAULT <input type="image"> TESTS
127
+ { code: '<input />' },
128
+ { code: '<input type="foo" />' },
129
+ { code: '<input type="image" aria-label="foo" />' },
130
+ { code: '<input type="image" aria-labelledby="id1" />' },
131
+ { code: '<input type="image" alt="" />' },
132
+ { code: '<input type="image" alt="This is descriptive!" />' },
133
+ { code: '<input type="image" alt={altText} />' },
134
+ { code: '<InputImage />' },
135
+ { code: '<Input type="image" alt="" />', settings: componentsSettings },
136
+ { code: '<SomeComponent as="input" type="image" alt="" />', settings: componentsSettings },
137
+
138
+ // CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
139
+ { code: '<Thumbnail alt="foo" />;', options: array },
140
+ { code: '<Thumbnail alt={"foo"} />;', options: array },
141
+ { code: '<Thumbnail alt={alt} />;', options: array },
142
+ { code: '<Thumbnail ALT="foo" />;', options: array },
143
+ { code: '<Thumbnail ALT={`This is the ${alt} text`} />;', options: array },
144
+ { code: '<Thumbnail ALt="foo" />;', options: array },
145
+ { code: '<Thumbnail alt="foo" salt={undefined} />;', options: array },
146
+ { code: '<Thumbnail {...this.props} alt="foo" />', options: array },
147
+ { code: '<thumbnail />', options: array },
148
+ { code: '<Thumbnail alt={function(e) {} } />', options: array },
149
+ { code: '<div alt={function(e) {} } />', options: array },
150
+ { code: '<Thumbnail alt={() => void 0} />', options: array },
151
+ { code: '<THUMBNAIL />', options: array },
152
+ { code: '<Thumbnail alt={alt || "foo" } />', options: array },
153
+ { code: '<Image alt="foo" />;', options: array },
154
+ { code: '<Image alt={"foo"} />;', options: array },
155
+ { code: '<Image alt={alt} />;', options: array },
156
+ { code: '<Image ALT="foo" />;', options: array },
157
+ { code: '<Image ALT={`This is the ${alt} text`} />;', options: array },
158
+ { code: '<Image ALt="foo" />;', options: array },
159
+ { code: '<Image alt="foo" salt={undefined} />;', options: array },
160
+ { code: '<Image {...this.props} alt="foo" />', options: array },
161
+ { code: '<image />', options: array },
162
+ { code: '<Image alt={function(e) {} } />', options: array },
163
+ { code: '<div alt={function(e) {} } />', options: array },
164
+ { code: '<Image alt={() => void 0} />', options: array },
165
+ { code: '<IMAGE />', options: array },
166
+ { code: '<Image alt={alt || "foo" } />', options: array },
167
+ { code: '<Object aria-label="foo" />', options: array },
168
+ { code: '<Object aria-labelledby="id1" />', options: array },
169
+ { code: '<Object>Foo</Object>', options: array },
170
+ { code: '<Object><p>This is descriptive!</p></Object>', options: array },
171
+ { code: '<Object title="An object" />', options: array },
172
+ { code: '<Area aria-label="foo" />', options: array },
173
+ { code: '<Area aria-labelledby="id1" />', options: array },
174
+ { code: '<Area alt="" />', options: array },
175
+ { code: '<Area alt="This is descriptive!" />', options: array },
176
+ { code: '<Area alt={altText} />', options: array },
177
+ { code: '<InputImage aria-label="foo" />', options: array },
178
+ { code: '<InputImage aria-labelledby="id1" />', options: array },
179
+ { code: '<InputImage alt="" />', options: array },
180
+ { code: '<InputImage alt="This is descriptive!" />', options: array },
181
+ { code: '<InputImage alt={altText} />', options: array },
182
+ )).map(parserOptionsMapper),
183
+ invalid: parsers.all([].concat(
184
+ // DEFAULT ELEMENT 'img' TESTS
185
+ { code: '<img />;', errors: [missingPropError('img')] },
186
+ { code: '<img alt />;', errors: [altValueError('img')] },
187
+ { code: '<img alt={undefined} />;', errors: [altValueError('img')] },
188
+ { code: '<img src="xyz" />', errors: [missingPropError('img')] },
189
+ { code: '<img role />', errors: [missingPropError('img')] },
190
+ { code: '<img {...this.props} />', errors: [missingPropError('img')] },
191
+ { code: '<img alt={false || false} />', errors: [altValueError('img')] },
192
+ { code: '<img alt={undefined} role="presentation" />;', errors: [altValueError('img')] },
193
+ { code: '<img alt role="presentation" />;', errors: [altValueError('img')] },
194
+ { code: '<img role="presentation" />;', errors: [preferAltError()] },
195
+ { code: '<img role="none" />;', errors: [preferAltError()] },
196
+ { code: '<img aria-label={undefined} />', errors: [ariaLabelValueError] },
197
+ { code: '<img aria-labelledby={undefined} />', errors: [ariaLabelledbyValueError] },
198
+ { code: '<img aria-label="" />', errors: [ariaLabelValueError] },
199
+ { code: '<img aria-labelledby="" />', errors: [ariaLabelledbyValueError] },
200
+ { code: '<SomeComponent as="img" aria-label="" />', settings: componentsSettings, errors: [ariaLabelValueError] },
201
+
202
+ // DEFAULT ELEMENT 'object' TESTS
203
+ { code: '<object />', errors: [objectError] },
204
+ { code: '<object><div aria-hidden /></object>', errors: [objectError] },
205
+ { code: '<object title={undefined} />', errors: [objectError] },
206
+ { code: '<object aria-label="" />', errors: [objectError] },
207
+ { code: '<object aria-labelledby="" />', errors: [objectError] },
208
+ { code: '<object aria-label={undefined} />', errors: [objectError] },
209
+ { code: '<object aria-labelledby={undefined} />', errors: [objectError] },
210
+
211
+ // DEFAULT ELEMENT 'area' TESTS
212
+ { code: '<area />', errors: [areaError] },
213
+ { code: '<area alt />', errors: [areaError] },
214
+ { code: '<area alt={undefined} />', errors: [areaError] },
215
+ { code: '<area src="xyz" />', errors: [areaError] },
216
+ { code: '<area {...this.props} />', errors: [areaError] },
217
+ { code: '<area aria-label="" />', errors: [areaError] },
218
+ { code: '<area aria-label={undefined} />', errors: [areaError] },
219
+ { code: '<area aria-labelledby="" />', errors: [areaError] },
220
+ { code: '<area aria-labelledby={undefined} />', errors: [areaError] },
221
+
222
+ // DEFAULT ELEMENT 'input type="image"' TESTS
223
+ { code: '<input type="image" />', errors: [inputImageError] },
224
+ { code: '<input type="image" alt />', errors: [inputImageError] },
225
+ { code: '<input type="image" alt={undefined} />', errors: [inputImageError] },
226
+ { code: '<input type="image">Foo</input>', errors: [inputImageError] },
227
+ { code: '<input type="image" {...this.props} />', errors: [inputImageError] },
228
+ { code: '<input type="image" aria-label="" />', errors: [inputImageError] },
229
+ { code: '<input type="image" aria-label={undefined} />', errors: [inputImageError] },
230
+ { code: '<input type="image" aria-labelledby="" />', errors: [inputImageError] },
231
+ { code: '<input type="image" aria-labelledby={undefined} />', errors: [inputImageError] },
232
+
233
+ // CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
234
+ {
235
+ code: '<Thumbnail />;',
236
+ errors: [missingPropError('Thumbnail')],
237
+ options: array,
238
+ },
239
+ {
240
+ code: '<Thumbnail alt />;',
241
+ errors: [altValueError('Thumbnail')],
242
+ options: array,
243
+ },
244
+ {
245
+ code: '<Thumbnail alt={undefined} />;',
246
+ errors: [altValueError('Thumbnail')],
247
+ options: array,
248
+ },
249
+ {
250
+ code: '<Thumbnail src="xyz" />',
251
+ errors: [missingPropError('Thumbnail')],
252
+ options: array,
253
+ },
254
+ {
255
+ code: '<Thumbnail {...this.props} />',
256
+ errors: [missingPropError('Thumbnail')],
257
+ options: array,
258
+ },
259
+ { code: '<Image />;', errors: [missingPropError('Image')], options: array },
260
+ { code: '<Image alt />;', errors: [altValueError('Image')], options: array },
261
+ {
262
+ code: '<Image alt={undefined} />;',
263
+ errors: [altValueError('Image')],
264
+ options: array,
265
+ },
266
+ {
267
+ code: '<Image src="xyz" />',
268
+ errors: [missingPropError('Image')],
269
+ options: array,
270
+ },
271
+ {
272
+ code: '<Image {...this.props} />',
273
+ errors: [missingPropError('Image')],
274
+ options: array,
275
+ },
276
+ { code: '<Object />', errors: [objectError], options: array },
277
+ { code: '<Object><div aria-hidden /></Object>', errors: [objectError], options: array },
278
+ { code: '<Object title={undefined} />', errors: [objectError], options: array },
279
+ { code: '<Area />', errors: [areaError], options: array },
280
+ { code: '<Area alt />', errors: [areaError], options: array },
281
+ { code: '<Area alt={undefined} />', errors: [areaError], options: array },
282
+ { code: '<Area src="xyz" />', errors: [areaError], options: array },
283
+ { code: '<Area {...this.props} />', errors: [areaError], options: array },
284
+ { code: '<InputImage />', errors: [inputImageError], options: array },
285
+ { code: '<InputImage alt />', errors: [inputImageError], options: array },
286
+ { code: '<InputImage alt={undefined} />', errors: [inputImageError], options: array },
287
+ { code: '<InputImage>Foo</InputImage>', errors: [inputImageError], options: array },
288
+ { code: '<InputImage {...this.props} />', errors: [inputImageError], options: array },
289
+ { code: '<Input type="image" />', errors: [inputImageError], settings: componentsSettings },
290
+ )).map(parserOptionsMapper),
291
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @fileoverview Enforce `<a>` text to not exactly match "click here", "here", "link", or "a link".
3
+ * @author Matt Wang
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/anchor-ambiguous-text';
14
+
15
+ // -----------------------------------------------------------------------------
16
+ // Tests
17
+ // -----------------------------------------------------------------------------
18
+
19
+ const ruleTester = new RuleTester();
20
+
21
+ const DEFAULT_AMBIGUOUS_WORDS = [
22
+ 'click here',
23
+ 'here',
24
+ 'link',
25
+ 'a link',
26
+ 'learn more',
27
+ ];
28
+
29
+ const expectedErrorGenerator = (words) => ({
30
+ message: `Ambiguous text within anchor. Screen reader users rely on link text for context; the words "${words.join('", "')}" are ambiguous and do not provide enough context.`,
31
+ type: 'JSXOpeningElement',
32
+ });
33
+
34
+ const expectedError = expectedErrorGenerator(DEFAULT_AMBIGUOUS_WORDS);
35
+
36
+ ruleTester.run('anchor-ambiguous-text', rule, {
37
+ valid: parsers.all([].concat(
38
+ { code: '<a>documentation</a>;' },
39
+ { code: '<a>${here}</a>;' },
40
+ { code: '<a aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</a>;' },
41
+ { code: '<a><span aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</span></a>;' },
42
+ { code: '<a><img alt="documentation" /></a>;' },
43
+ {
44
+ code: '<a>click here</a>',
45
+ options: [{
46
+ words: ['disabling the defaults'],
47
+ }],
48
+ },
49
+ {
50
+ code: '<Link>documentation</Link>;',
51
+ settings: { 'jsx-a11y': { components: { Link: 'a' } } },
52
+ },
53
+ {
54
+ code: '<a><Image alt="documentation" /></a>;',
55
+ settings: { 'jsx-a11y': { components: { Image: 'img' } } },
56
+ },
57
+ {
58
+ code: '<Link>${here}</Link>;',
59
+ settings: { 'jsx-a11y': { components: { Link: 'a' } } },
60
+ },
61
+ {
62
+ code: '<Link aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</Link>;',
63
+ settings: { 'jsx-a11y': { components: { Link: 'a' } } },
64
+ },
65
+ {
66
+ code: '<Link>click here</Link>',
67
+ options: [{
68
+ words: ['disabling the defaults with components'],
69
+ }],
70
+ settings: { 'jsx-a11y': { components: { Link: 'a' } } },
71
+ },
72
+ )).map(parserOptionsMapper),
73
+ invalid: parsers.all([].concat(
74
+ { code: '<a>here</a>;', errors: [expectedError] },
75
+ { code: '<a>HERE</a>;', errors: [expectedError] },
76
+ { code: '<a>click here</a>;', errors: [expectedError] },
77
+ { code: '<a>learn more</a>;', errors: [expectedError] },
78
+ { code: '<a>learn more</a>;', errors: [expectedError] },
79
+ { code: '<a>learn more.</a>;', errors: [expectedError] },
80
+ { code: '<a>learn more?</a>;', errors: [expectedError] },
81
+ { code: '<a>learn more,</a>;', errors: [expectedError] },
82
+ { code: '<a>learn more!</a>;', errors: [expectedError] },
83
+ { code: '<a>learn more;</a>;', errors: [expectedError] },
84
+ { code: '<a>learn more:</a>;', errors: [expectedError] },
85
+ { code: '<a>link</a>;', errors: [expectedError] },
86
+ { code: '<a>a link</a>;', errors: [expectedError] },
87
+ { code: '<a aria-label="click here">something</a>;', errors: [expectedError] },
88
+ { code: '<a> a link </a>;', errors: [expectedError] },
89
+ { code: '<a>a<i></i> link</a>;', errors: [expectedError] },
90
+ { code: '<a><i></i>a link</a>;', errors: [expectedError] },
91
+ { code: '<a><span>click</span> here</a>;', errors: [expectedError] },
92
+ { code: '<a><span> click </span> here</a>;', errors: [expectedError] },
93
+ { code: '<a><span aria-hidden>more text</span>learn more</a>;', errors: [expectedError] },
94
+ { code: '<a><span aria-hidden="true">more text</span>learn more</a>;', errors: [expectedError] },
95
+ { code: '<a><img alt="click here"/></a>;', errors: [expectedError] },
96
+ { code: '<a alt="tutorial on using eslint-plugin-jsx-a11y">click here</a>;', errors: [expectedError] },
97
+ { code: '<a><span alt="tutorial on using eslint-plugin-jsx-a11y">click here</span></a>;', errors: [expectedError] },
98
+ { code: '<a><CustomElement>click</CustomElement> here</a>;', errors: [expectedError] },
99
+ {
100
+ code: '<Link>here</Link>',
101
+ errors: [expectedError],
102
+ settings: { 'jsx-a11y': { components: { Link: 'a' } } },
103
+ },
104
+ {
105
+ code: '<a><Image alt="click here" /></a>',
106
+ errors: [expectedError],
107
+ settings: { 'jsx-a11y': { components: { Image: 'img' } } },
108
+ },
109
+ {
110
+ code: '<a>a disallowed word</a>',
111
+ errors: [expectedErrorGenerator(['a disallowed word'])],
112
+ options: [{
113
+ words: ['a disallowed word'],
114
+ }],
115
+ },
116
+ )).map(parserOptionsMapper),
117
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @fileoverview Enforce anchor elements to contain accessible content.
3
+ * @author Lisa Ring & Niklas Holmberg
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/anchor-has-content';
14
+
15
+ // -----------------------------------------------------------------------------
16
+ // Tests
17
+ // -----------------------------------------------------------------------------
18
+
19
+ const ruleTester = new RuleTester();
20
+
21
+ const expectedError = {
22
+ message: 'Anchors must have content and the content must be accessible by a screen reader.',
23
+ type: 'JSXOpeningElement',
24
+ };
25
+
26
+ ruleTester.run('anchor-has-content', rule, {
27
+ valid: parsers.all([].concat(
28
+ { code: '<div />;' },
29
+ { code: '<a>Foo</a>' },
30
+ { code: '<a><Bar /></a>' },
31
+ { code: '<a>{foo}</a>' },
32
+ { code: '<a>{foo.bar}</a>' },
33
+ { code: '<a dangerouslySetInnerHTML={{ __html: "foo" }} />' },
34
+ { code: '<a children={children} />' },
35
+ { code: '<Link />' },
36
+ {
37
+ code: '<Link>foo</Link>',
38
+ settings: { 'jsx-a11y': { components: { Link: 'a' } } },
39
+ },
40
+ { code: '<a title={title} />' },
41
+ { code: '<a aria-label={ariaLabel} />' },
42
+ { code: '<a title={title} aria-label={ariaLabel} />' },
43
+ )).map(parserOptionsMapper),
44
+ invalid: parsers.all([].concat(
45
+ { code: '<a />', errors: [expectedError] },
46
+ { code: '<a><Bar aria-hidden /></a>', errors: [expectedError] },
47
+ { code: '<a>{undefined}</a>', errors: [expectedError] },
48
+ {
49
+ code: '<Link />',
50
+ errors: [expectedError],
51
+ settings: { 'jsx-a11y': { components: { Link: 'a' } } },
52
+ },
53
+ )).map(parserOptionsMapper),
54
+ });