From ef9f86b0348f8f77db6fd48df730948446b65f17 Mon Sep 17 00:00:00 2001 From: Pascal Corpet Date: Fri, 25 Mar 2022 10:26:41 +0100 Subject: [PATCH] [New] Introduce a plugin-wide setting for custom components. For #174 --- __tests__/__util__/parserOptionsMapper.js | 2 + .../__util__/ruleOptionsMapperFactory.js | 6 +- __tests__/src/rules/accessible-emoji-test.js | 6 + .../src/rules/anchor-has-content-test.js | 10 ++ __tests__/src/rules/anchor-is-valid-test.js | 16 +- ...aria-activedescendant-has-tabindex-test.js | 9 ++ __tests__/src/rules/aria-role-test.js | 20 +++ .../rules/aria-unsupported-elements-test.js | 6 +- .../src/rules/autocomplete-valid-test.js | 11 ++ .../click-events-have-key-events-test.js | 2 + .../control-has-associated-label-test.js | 2 + .../src/rules/heading-has-content-test.js | 14 ++ __tests__/src/rules/html-has-lang-test.js | 2 + __tests__/src/rules/iframe-has-title-test.js | 10 ++ __tests__/src/rules/img-redundant-alt-test.js | 11 ++ .../rules/interactive-supports-focus-test.js | 17 ++ .../label-has-associated-control-test.js | 19 +++ __tests__/src/rules/lang-test.js | 12 +- __tests__/src/rules/media-has-caption-test.js | 72 +++++++++ ...oninteractive-element-interactions-test.js | 11 ++ __tests__/src/util/getElementType-test.js | 56 +++++++ .../src/util/mayContainChildComponent-test.js | 23 +++ flow/eslint.js | 8 + src/rules/accessible-emoji.js | 54 ++++--- src/rules/anchor-has-content.js | 47 +++--- src/rules/anchor-is-valid.js | 150 +++++++++--------- .../aria-activedescendant-has-tabindex.js | 68 ++++---- src/rules/aria-role.js | 4 +- src/rules/aria-unsupported-elements.js | 58 +++---- src/rules/autocomplete-valid.js | 72 +++++---- src/rules/click-events-have-key-events.js | 70 ++++---- src/rules/control-has-associated-label.js | 4 +- src/rules/heading-has-content.js | 53 ++++--- src/rules/html-has-lang.js | 38 +++-- src/rules/iframe-has-title.js | 38 +++-- src/rules/img-redundant-alt.js | 68 ++++---- src/rules/interactive-supports-focus.js | 103 ++++++------ src/rules/label-has-associated-control.js | 5 +- src/rules/lang.js | 68 ++++---- src/rules/media-has-caption.js | 80 +++++----- .../no-noninteractive-element-interactions.js | 3 +- src/util/getElementType.js | 32 ++++ src/util/mayContainChildComponent.js | 5 +- 43 files changed, 897 insertions(+), 468 deletions(-) create mode 100644 __tests__/src/util/getElementType-test.js create mode 100644 src/util/getElementType.js diff --git a/__tests__/__util__/parserOptionsMapper.js b/__tests__/__util__/parserOptionsMapper.js index c3d2f77bd..74db94a78 100644 --- a/__tests__/__util__/parserOptionsMapper.js +++ b/__tests__/__util__/parserOptionsMapper.js @@ -11,6 +11,7 @@ export default function parserOptionsMapper({ errors, options = [], parserOptions = {}, + settings, }) { return { code, @@ -20,5 +21,6 @@ export default function parserOptionsMapper({ ...defaultParserOptions, ...parserOptions, }, + settings, }; } diff --git a/__tests__/__util__/ruleOptionsMapperFactory.js b/__tests__/__util__/ruleOptionsMapperFactory.js index 7175bf91b..1e1b628b0 100644 --- a/__tests__/__util__/ruleOptionsMapperFactory.js +++ b/__tests__/__util__/ruleOptionsMapperFactory.js @@ -6,7 +6,8 @@ type ESLintTestRunnerTestCase = { code: string, errors: ?Array<{ message: string, type: string }>, options: ?Array, - parserOptions: ?Array + parserOptions: ?Array, + settings?: {[string]: mixed}, }; type RuleOptionsMapperFactoryType = ( @@ -15,7 +16,7 @@ type RuleOptionsMapperFactoryType = ( export default function ruleOptionsMapperFactory(ruleOptions: Array = []): RuleOptionsMapperFactoryType { // eslint-disable-next-line - return ({ code, errors, options, parserOptions }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => { + return ({ code, errors, options, parserOptions, settings }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => { return { code, errors, @@ -25,6 +26,7 @@ export default function ruleOptionsMapperFactory(ruleOptions: Array = []) ...item, }], [{}]), parserOptions, + settings, }; }; } diff --git a/__tests__/src/rules/accessible-emoji-test.js b/__tests__/src/rules/accessible-emoji-test.js index bcdb42964..891035a11 100644 --- a/__tests__/src/rules/accessible-emoji-test.js +++ b/__tests__/src/rules/accessible-emoji-test.js @@ -37,6 +37,11 @@ ruleTester.run('accessible-emoji', rule, { { code: '' }, { code: '🐼' }, { code: '' }, + { code: '🐼' }, + { + code: '🐼', + settings: { 'jsx-a11y': { components: { input: ['CustomInput'] } } }, + }, ].map(parserOptionsMapper), invalid: [ { code: '🐼', errors: [expectedError] }, @@ -46,5 +51,6 @@ ruleTester.run('accessible-emoji', rule, { { code: '🐼', errors: [expectedError] }, { code: '🐼', errors: [expectedError] }, { code: '🐼', errors: [expectedError] }, + { code: '🐼', errors: [expectedError] }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/anchor-has-content-test.js b/__tests__/src/rules/anchor-has-content-test.js index 77559da31..b59d0d834 100644 --- a/__tests__/src/rules/anchor-has-content-test.js +++ b/__tests__/src/rules/anchor-has-content-test.js @@ -31,10 +31,20 @@ ruleTester.run('anchor-has-content', rule, { { code: '{foo.bar}' }, { code: '' }, { code: '' }, + { code: '' }, + { + code: 'foo', + settings: { 'jsx-a11y': { components: { a: ['Link'] } } }, + }, ].map(parserOptionsMapper), invalid: [ { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, { code: '{undefined}', errors: [expectedError] }, + { + code: '', + errors: [expectedError], + settings: { 'jsx-a11y': { components: { a: ['Link'] } } }, + }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/anchor-is-valid-test.js b/__tests__/src/rules/anchor-is-valid-test.js index fd7f44940..b9a21f386 100644 --- a/__tests__/src/rules/anchor-is-valid-test.js +++ b/__tests__/src/rules/anchor-is-valid-test.js @@ -78,10 +78,18 @@ const componentsAndSpecialLinkAndNoHrefAspect = [{ aspects: ['noHref'], }]; +const componentsSettings = { + 'jsx-a11y': { + components: { + a: ['Anchor', 'Link'], + }, + }, +}; + ruleTester.run('anchor-is-valid', rule, { valid: [ // DEFAULT ELEMENT 'a' TESTS - { code: ';' }, + { code: '' }, { code: '' }, { code: '' }, { code: '' }, @@ -119,6 +127,7 @@ ruleTester.run('anchor-is-valid', rule, { { code: '', options: components }, { code: '', options: components }, { code: '', options: components }, + { code: '', settings: componentsSettings }, // CUSTOM PROP TESTS { code: '', options: specialLink }, @@ -332,6 +341,11 @@ ruleTester.run('anchor-is-valid', rule, { errors: [preferButtonexpectedError], options: components, }, + { + code: ' void 0} />', + errors: [preferButtonexpectedError], + settings: componentsSettings, + }, // CUSTOM PROP TESTS // NO HREF diff --git a/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js b/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js index 917898e80..d16fe0329 100644 --- a/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js +++ b/__tests__/src/rules/aria-activedescendant-has-tabindex-test.js @@ -36,6 +36,10 @@ ruleTester.run('aria-activedescendant-has-tabindex', rule, { { code: ';', }, + { + code: ';', + settings: { 'jsx-a11y': { components: { div: ['CustomComponent'] } } }, + }, { code: '
;', }, @@ -81,5 +85,10 @@ ruleTester.run('aria-activedescendant-has-tabindex', rule, { code: '
;', errors: [expectedError], }, + { + code: ';', + errors: [expectedError], + settings: { 'jsx-a11y': { components: { div: ['CustomComponent'] } } }, + }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/aria-role-test.js b/__tests__/src/rules/aria-role-test.js index a9cdfeb51..09d0addd6 100644 --- a/__tests__/src/rules/aria-role-test.js +++ b/__tests__/src/rules/aria-role-test.js @@ -47,6 +47,14 @@ const ignoreNonDOMSchema = [{ ignoreNonDOM: true, }]; +const customDivSettings = { + 'jsx-a11y': { + components: { + div: ['Div'], + }, + }, +}; + ruleTester.run('aria-role', rule, { valid: [ // Variables should pass, as we are only testing literals. @@ -66,6 +74,11 @@ ruleTester.run('aria-role', rule, { { code: '', options: ignoreNonDOMSchema }, { code: '', options: ignoreNonDOMSchema }, { code: '', options: ignoreNonDOMSchema }, + { + code: '
', + errors: [errorMessage], + settings: customDivSettings, + }, ].concat(validTests).map(parserOptionsMapper), invalid: [ @@ -82,5 +95,12 @@ ruleTester.run('aria-role', rule, { { code: '
', errors: [errorMessage] }, { code: '', errors: [errorMessage] }, { code: '', errors: [errorMessage] }, + { code: '
', errors: [errorMessage], settings: customDivSettings }, + { + code: '
', + errors: [errorMessage], + options: ignoreNonDOMSchema, + settings: customDivSettings, + }, ].concat(invalidTests).map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/aria-unsupported-elements-test.js b/__tests__/src/rules/aria-unsupported-elements-test.js index d34b517eb..54118c3e7 100644 --- a/__tests__/src/rules/aria-unsupported-elements-test.js +++ b/__tests__/src/rules/aria-unsupported-elements-test.js @@ -54,7 +54,11 @@ const invalidRoleValidityTests = domElements .map((reservedElem) => ({ code: `<${reservedElem} role {...props} />`, errors: [errorMessage('role')], - })); + })).concat({ + code: '', + errors: [errorMessage('aria-hidden')], + settings: { 'jsx-a11y': { components: { meta: ['Meta'] } } }, + }); const invalidAriaValidityTests = domElements .filter((element) => Boolean(dom.get(element).reserved)) diff --git a/__tests__/src/rules/autocomplete-valid-test.js b/__tests__/src/rules/autocomplete-valid-test.js index ef731b19c..0e3e3df13 100644 --- a/__tests__/src/rules/autocomplete-valid-test.js +++ b/__tests__/src/rules/autocomplete-valid-test.js @@ -28,6 +28,14 @@ const inappropriateAutocomplete = [{ type: 'JSXOpeningElement', }]; +const componentsSettings = { + 'jsx-a11y': { + components: { + input: ['Input'], + }, + }, +}; + ruleTester.run('autocomplete-valid', rule, { valid: [ // INAPPLICABLE @@ -46,6 +54,8 @@ ruleTester.run('autocomplete-valid', rule, { { code: ';' }, { code: ';' }, { code: ';' }, + { code: '', settings: componentsSettings }, + { code: '' }, // PASSED "autocomplete-appropriate" // see also: https://github.com/dequelabs/axe-core/issues/2912 @@ -61,5 +71,6 @@ ruleTester.run('autocomplete-valid', rule, { { code: ';', errors: invalidAutocomplete }, { code: ';', errors: invalidAutocomplete }, { code: ';', errors: invalidAutocomplete, options: [{ inputComponents: ['Bar'] }] }, + { code: '', errors: invalidAutocomplete, settings: componentsSettings }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/click-events-have-key-events-test.js b/__tests__/src/rules/click-events-have-key-events-test.js index 89965a57b..8f7197f27 100644 --- a/__tests__/src/rules/click-events-have-key-events-test.js +++ b/__tests__/src/rules/click-events-have-key-events-test.js @@ -51,6 +51,7 @@ ruleTester.run('click-events-have-key-events', rule, { { code: '
void 0} role="none" />;' }, { code: '' }, { code: '' }, { code: '' }, @@ -255,6 +256,7 @@ const neverValid = [ { code: '', errors: [expectedError] }, { code: '', options: [{ depth: 3 }], errors: [expectedError] }, { code: '', options: [{ depth: 3, controlComponents: ['CustomControl'] }], errors: [expectedError] }, + { code: '', errors: [expectedError], settings: { 'jsx-a11y': { components: { button: ['CustomControl'] } } } }, { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, diff --git a/__tests__/src/rules/heading-has-content-test.js b/__tests__/src/rules/heading-has-content-test.js index e52930452..e2d164115 100644 --- a/__tests__/src/rules/heading-has-content-test.js +++ b/__tests__/src/rules/heading-has-content-test.js @@ -26,6 +26,15 @@ const components = [{ components: ['Heading', 'Title'], }]; +const componentsSettings = { + 'jsx-a11y': { + components: { + h2: ['Heading'], + h1: ['Title'], + }, + }, +}; + ruleTester.run('heading-has-content', rule, { valid: [ // DEFAULT ELEMENT TESTS @@ -51,6 +60,8 @@ ruleTester.run('heading-has-content', rule, { { code: '', options: components }, { code: '', options: components }, { code: '

' }, + // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS + { code: 'Foo', settings: componentsSettings }, ].map(parserOptionsMapper), invalid: [ // DEFAULT ELEMENT TESTS @@ -62,5 +73,8 @@ ruleTester.run('heading-has-content', rule, { { code: '', errors: [expectedError], options: components }, { code: '', errors: [expectedError], options: components }, { code: '{undefined}', errors: [expectedError], options: components }, + + // CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS + { code: '', errors: [expectedError], settings: componentsSettings }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/html-has-lang-test.js b/__tests__/src/rules/html-has-lang-test.js index 61348f676..6ea3fc341 100644 --- a/__tests__/src/rules/html-has-lang-test.js +++ b/__tests__/src/rules/html-has-lang-test.js @@ -30,10 +30,12 @@ ruleTester.run('html-has-lang', rule, { { code: '' }, { code: '' }, { code: '' }, + { code: '', errors: [expectedError], settings: { 'jsx-a11y': { components: { html: ['HTMLTop'] } } } }, ].map(parserOptionsMapper), invalid: [ { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, { code: '', errors: [expectedError] }, + { code: '', errors: [expectedError], settings: { 'jsx-a11y': { components: { html: ['HTMLTop'] } } } }, ].map(parserOptionsMapper), }); diff --git a/__tests__/src/rules/iframe-has-title-test.js b/__tests__/src/rules/iframe-has-title-test.js index 195fdba44..e34e683c6 100644 --- a/__tests__/src/rules/iframe-has-title-test.js +++ b/__tests__/src/rules/iframe-has-title-test.js @@ -22,12 +22,21 @@ const expectedError = { type: 'JSXOpeningElement', }; +const componentsSettings = { + 'jsx-a11y': { + components: { + iframe: ['FooComponent'], + }, + }, +}; + ruleTester.run('html-has-lang', rule, { valid: [ { code: '
;' }, { code: '